diff --git a/.github/workflows/client-tests.yml b/.github/workflows/client-tests.yml index 83e6d285..1e0b82ed 100644 --- a/.github/workflows/client-tests.yml +++ b/.github/workflows/client-tests.yml @@ -25,9 +25,9 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 1c2a4e20..530ea5b1 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -14,9 +14,9 @@ jobs: run: working-directory: server steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" @@ -31,7 +31,7 @@ jobs: - name: Copy templates.json out of the container run: docker cp $CONTAINER_ID:/server/scripts/templates.json $TEMPLATES_FILE_NAME - name: Upload templates.json as an artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: template path: ${{github.workspace}}/${{env.TEMPLATES_FILE_NAME}} @@ -52,21 +52,21 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" cache-dependency-path: client/package-lock.json - name: Configure AWS CLI - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-2 - name: Update template.json - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: template path: ${{github.workspace}}/client/src/data diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 42dbd800..66fb2aea 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -27,9 +27,9 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Node ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 879f9ba9..c4477856 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -14,9 +14,9 @@ jobs: run: working-directory: server steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" @@ -31,7 +31,7 @@ jobs: - name: Copy templates.json out of the container run: docker cp $CONTAINER_ID:/server/scripts/templates.json $TEMPLATES_FILE_NAME - name: Upload templates.json as an artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: template path: ${{github.workspace}}/${{env.TEMPLATES_FILE_NAME}} @@ -52,21 +52,21 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} cache: "npm" cache-dependency-path: client/package-lock.json - name: Configure AWS CLI - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-2 - name: Update template.json - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: template path: ${{github.workspace}}/client/src/data diff --git a/client/src/data/templates.json b/client/src/data/templates.json index 7f275c58..01f523c6 100644 --- a/client/src/data/templates.json +++ b/client/src/data/templates.json @@ -2,17 +2,7 @@ "templates": [ { "modelicaPath": "Buildings.Templates.AirHandlersFans.VAVMultiZone", - "scheduleOptionPaths": [ - "Buildings.Templates.Components.Actuators.Damper.dat", - "Buildings.Templates.AirHandlersFans.Components.Interfaces.PartialOutdoorSection.dat", - "Buildings.Templates.Components.Interfaces.PartialFan.dat", - "Buildings.Templates.AirHandlersFans.Components.Interfaces.PartialReliefReturnSection.dat", - "Buildings.Templates.AirHandlersFans.Components.Interfaces.PartialOutdoorReliefReturnSection.dat", - "Buildings.Templates.Components.Interfaces.PartialCoil.dat", - "Buildings.Templates.Components.Actuators.Valve.dat", - "Buildings.Templates.AirHandlersFans.Components.Interfaces.PartialController.dat", - "Buildings.Templates.AirHandlersFans.Interfaces.PartialAirHandler.dat" - ], + "scheduleOptionPaths": [], "systemTypes": [ "Buildings.Templates.AirHandlersFans" ], @@ -27,13 +17,7 @@ }, { "modelicaPath": "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly", - "scheduleOptionPaths": [ - "Buildings.Templates.Components.Actuators.Valve.dat", - "Buildings.Templates.Components.Interfaces.PartialCoil.dat", - "Buildings.Templates.Components.Actuators.Damper.dat", - "Buildings.Templates.ZoneEquipment.Components.Interfaces.PartialController.dat", - "Buildings.Templates.ZoneEquipment.Interfaces.PartialAirTerminal.dat" - ], + "scheduleOptionPaths": [], "systemTypes": [ "Buildings.Templates.ZoneEquipment" ], @@ -45,13 +29,7 @@ }, { "modelicaPath": "Buildings.Templates.ZoneEquipment.VAVBoxReheat", - "scheduleOptionPaths": [ - "Buildings.Templates.Components.Actuators.Valve.dat", - "Buildings.Templates.Components.Interfaces.PartialCoil.dat", - "Buildings.Templates.Components.Actuators.Damper.dat", - "Buildings.Templates.ZoneEquipment.Components.Interfaces.PartialController.dat", - "Buildings.Templates.ZoneEquipment.Interfaces.PartialAirTerminal.dat" - ], + "scheduleOptionPaths": [], "systemTypes": [ "Buildings.Templates.ZoneEquipment" ], @@ -390,20 +368,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -553,12 +521,7 @@ "group": "Mechanical", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -864,19 +827,10 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.DamperBlades.Opposed", - "Buildings.Templates.Components.Types.DamperBlades.Parallel", - "Buildings.Templates.Components.Types.DamperBlades.VAV" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -950,12 +904,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -975,12 +924,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -1196,12 +1140,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "use_deltaM" - ] - }, + "enable": "use_deltaM", "modifiers": {}, "replaceable": false, "options": [], @@ -2749,19 +2688,10 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.IntegrationPoint.None", - "Buildings.Templates.Components.Types.IntegrationPoint.Return", - "Buildings.Templates.Components.Types.IntegrationPoint.Supply" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -3376,19 +3306,10 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.SensorVolumeFlowRate.AFMS", - "Buildings.Templates.Components.Types.SensorVolumeFlowRate.FlowCross", - "Buildings.Templates.Components.Types.SensorVolumeFlowRate.FlowMeter" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -3841,12 +3762,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -3866,12 +3782,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -3891,19 +3802,10 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.IntegrationPoint.None", - "Buildings.Templates.Components.Types.IntegrationPoint.Return", - "Buildings.Templates.Components.Types.IntegrationPoint.Supply" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -4069,19 +3971,10 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.SensorTemperature.Standard", - "Buildings.Templates.Components.Types.SensorTemperature.Averaging", - "Buildings.Templates.Components.Types.SensorTemperature.InWell" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -4283,12 +4176,7 @@ "group": "Heat transfer", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "transferHeat" - ] - }, + "enable": "transferHeat", "modifiers": {}, "replaceable": false, "options": [], @@ -4308,12 +4196,7 @@ "group": "Heat transfer", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "transferHeat" - ] - }, + "enable": "transferHeat", "modifiers": {}, "replaceable": false, "options": [], @@ -5104,20 +4987,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5128,20 +5001,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5152,20 +5015,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5176,20 +5029,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5200,20 +5043,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5295,20 +5128,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -5319,12 +5142,7 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": { "Buildings.Templates.AirHandlersFans.Components.Data.OutdoorReliefReturnSection.nFanRet.start": { "expression": { @@ -5376,12 +5194,7 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": { "Buildings.Templates.AirHandlersFans.Components.Data.OutdoorReliefReturnSection.nFanRel.start": { "expression": { @@ -5657,20 +5470,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -7213,19 +7016,11 @@ "group": "", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "recordBinding": true, - "options": [ - "Buildings.Fluid.Movers.BaseClasses.Characteristics.efficiencyParameters_yMot.y", - "Buildings.Fluid.Movers.BaseClasses.Characteristics.efficiencyParameters_yMot.eta" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -9285,12 +9080,7 @@ "group": "", "tab": "Advanced", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "verifyFlowReversal" - ] - }, + "enable": "verifyFlowReversal", "modifiers": {}, "replaceable": false, "options": [], @@ -11276,12 +11066,7 @@ "group": "Motor speed", "tab": "Dynamics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "use_riseTime" - ] - }, + "enable": "use_riseTime", "modifiers": {}, "replaceable": false, "options": [], @@ -11569,12 +11354,7 @@ "group": "Motor speed", "tab": "Dynamics", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_riseTime" - ] - }, + "enable": "use_riseTime", "modifiers": {}, "replaceable": false, "options": [], @@ -11594,12 +11374,7 @@ "group": "Motor speed", "tab": "Dynamics", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_riseTime" - ] - }, + "enable": "use_riseTime", "modifiers": {}, "replaceable": false, "options": [], @@ -12493,19 +12268,10 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.FanSingle.Housed", - "Buildings.Templates.Components.Types.FanSingle.Plug", - "Buildings.Templates.Components.Types.FanSingle.Propeller" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -12579,12 +12345,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -12604,12 +12365,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -19986,22 +19742,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Coil.ElectricHeating", - "Buildings.Templates.Components.Types.Coil.EvaporatorMultiStage", - "Buildings.Templates.Components.Types.Coil.EvaporatorVariableSpeed", - "Buildings.Templates.Components.Types.Coil.None", - "Buildings.Templates.Components.Types.Coil.WaterBasedCooling", - "Buildings.Templates.Components.Types.Coil.WaterBasedHeating" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -20012,21 +19756,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Valve.None", - "Buildings.Templates.Components.Types.Valve.ThreeWayModulating", - "Buildings.Templates.Components.Types.Valve.ThreeWayTwoPosition", - "Buildings.Templates.Components.Types.Valve.TwoWayModulating", - "Buildings.Templates.Components.Types.Valve.TwoWayTwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -20225,12 +19958,7 @@ "group": "Nominal condition", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "have_sou" - ] - }, + "enable": "have_sou", "modifiers": { "Buildings.Templates.Components.Data.Coil.mWat_flow_nominal.min": { "expression": { @@ -20309,12 +20037,7 @@ "group": "Nominal condition", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "have_sou" - ] - }, + "enable": "have_sou", "modifiers": { "Buildings.Templates.Components.Data.Coil.dpWat_nominal.min": { "expression": { @@ -20684,12 +20407,7 @@ "group": "Nominal condition", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "have_sou" - ] - }, + "enable": "have_sou", "modifiers": { "Buildings.Templates.Components.Data.Coil.TWatEnt_nominal.min": { "expression": { @@ -20777,12 +20495,7 @@ "group": "Nominal condition", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "have_sou" - ] - }, + "enable": "have_sou", "modifiers": { "Buildings.Templates.Components.Data.Coil.TAirEnt_nominal.min": { "expression": { @@ -22166,21 +21879,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Valve.None", - "Buildings.Templates.Components.Types.Valve.ThreeWayModulating", - "Buildings.Templates.Components.Types.Valve.ThreeWayTwoPosition", - "Buildings.Templates.Components.Types.Valve.TwoWayModulating", - "Buildings.Templates.Components.Types.Valve.TwoWayTwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -22640,12 +22342,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -22665,12 +22362,7 @@ "group": "", "tab": "Graphics", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -26135,12 +25827,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.BaseClasses.PartialEffectivenessNTU.Q_flow_nominal.fixed": { "expression": { @@ -26164,12 +25851,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.BaseClasses.PartialEffectivenessNTU.T_a1_nominal.fixed": { "expression": { @@ -26193,12 +25875,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.BaseClasses.PartialEffectivenessNTU.T_a2_nominal.fixed": { "expression": { @@ -29579,12 +29256,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.WetCoilEffectivenessNTU.Q_flow_nominal.fixed": { "expression": { @@ -29608,12 +29280,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.WetCoilEffectivenessNTU.T_a1_nominal.fixed": { "expression": { @@ -29637,12 +29304,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.WetCoilEffectivenessNTU.T_a2_nominal.fixed": { "expression": { @@ -29666,12 +29328,7 @@ "group": "Nominal thermal performance", "tab": "", "visible": false, - "enable": { - "operator": "none", - "operands": [ - "use_Q_flow_nominal" - ] - }, + "enable": "use_Q_flow_nominal", "modifiers": { "Buildings.Fluid.HeatExchangers.WetCoilEffectivenessNTU.w_a2_nominal.start": { "expression": { @@ -32798,12 +32455,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -34816,12 +34468,7 @@ "group": "", "tab": "Freeze protection", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [], @@ -34841,12 +34488,7 @@ "group": "Heating coil PID Controller", "tab": "Freeze protection", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -34871,12 +34513,7 @@ "group": "Heating coil PID Controller", "tab": "Freeze protection", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [], @@ -34994,12 +34631,7 @@ "group": "Heating coil PID Controller", "tab": "Freeze protection", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [], @@ -35019,12 +34651,7 @@ "group": "Heating coil PID Controller", "tab": "Freeze protection", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [], @@ -37487,12 +37114,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -37518,12 +37140,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -37547,12 +37164,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -37576,12 +37188,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -38003,12 +37610,7 @@ "group": "", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [], @@ -45405,12 +45007,7 @@ "group": "Hysteresis", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "use_enthalpy" - ] - }, + "enable": "use_enthalpy", "modifiers": {}, "replaceable": false, "options": [], @@ -48394,12 +47991,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_hol" - ] - }, + "enable": "have_hol", "modifiers": {}, "replaceable": false, "options": [], @@ -61729,12 +61321,7 @@ "group": "Economizer", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "secOutRel.have_eco" - ] - }, + "enable": "secOutRel.have_eco", "modifiers": {}, "replaceable": false, "options": [ @@ -61847,12 +61434,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_frePro" - ] - }, + "enable": "have_frePro", "modifiers": {}, "replaceable": false, "options": [ @@ -62558,18 +62140,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.AirHandlersFans.Types.Controller.G36VAVMultiZone", - "Buildings.Templates.AirHandlersFans.Types.Controller.OpenLoop" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -62580,20 +62154,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -62604,20 +62168,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -62628,20 +62182,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Fan.None", - "Buildings.Templates.Components.Types.Fan.SingleConstant", - "Buildings.Templates.Components.Types.Fan.SingleVariable", - "Buildings.Templates.Components.Types.Fan.ArrayVariable" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -63425,12 +62969,7 @@ "tab": "", "visible": true, "modifiers": {}, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "choiceModifiers": {}, "replaceable": true, "options": [ @@ -63472,12 +63011,7 @@ "group": "", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "cfg.have_souChiWat" - ] - }, + "enable": "cfg.have_souChiWat", "modifiers": {}, "replaceable": false, "options": [], @@ -63497,12 +63031,7 @@ "group": "", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "cfg.have_souHeaWat" - ] - }, + "enable": "cfg.have_souHeaWat", "modifiers": {}, "replaceable": false, "options": [], @@ -63622,12 +63151,7 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, "options": [], @@ -65551,19 +65075,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Controls.OBC.ASHRAE.G36.Types.OutdoorAirSection.DedicatedDampersAirflow", - "Buildings.Controls.OBC.ASHRAE.G36.Types.OutdoorAirSection.DedicatedDampersPressure", - "Buildings.Controls.OBC.ASHRAE.G36.Types.OutdoorAirSection.SingleDamper" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -65574,21 +65089,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Controls.OBC.ASHRAE.G36.Types.PressureControl.BarometricRelief", - "Buildings.Controls.OBC.ASHRAE.G36.Types.PressureControl.ReliefDamper", - "Buildings.Controls.OBC.ASHRAE.G36.Types.PressureControl.ReliefFan", - "Buildings.Controls.OBC.ASHRAE.G36.Types.PressureControl.ReturnFanMeasuredAir", - "Buildings.Controls.OBC.ASHRAE.G36.Types.PressureControl.ReturnFanDp" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -65599,18 +65103,10 @@ "group": "Energy and ventilation standards", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Controls.OBC.ASHRAE.G36.Types.VentilationStandard.ASHRAE62_1", - "Buildings.Controls.OBC.ASHRAE.G36.Types.VentilationStandard.California_Title_24" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -66472,20 +65968,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -66496,20 +65982,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -66520,20 +65996,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -66544,20 +66010,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.Components.Types.Damper.Modulating", - "Buildings.Templates.Components.Types.Damper.None", - "Buildings.Templates.Components.Types.Damper.PressureIndependent", - "Buildings.Templates.Components.Types.Damper.TwoPosition" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -67484,19 +66940,10 @@ "group": "Configuration", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Templates.ZoneEquipment.Types.Controller.G36VAVBoxCoolingOnly", - "Buildings.Templates.ZoneEquipment.Types.Controller.G36VAVBoxReheat", - "Buildings.Templates.ZoneEquipment.Types.Controller.OpenLoop" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -68060,12 +67507,7 @@ "tab": "", "visible": true, "modifiers": {}, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "choiceModifiers": {}, "replaceable": true, "options": [ @@ -68127,12 +67569,7 @@ "group": "", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "cfg.have_souChiWat" - ] - }, + "enable": "cfg.have_souChiWat", "modifiers": {}, "replaceable": false, "options": [], @@ -68152,12 +67589,7 @@ "group": "", "tab": "Advanced", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "cfg.have_souHeaWat" - ] - }, + "enable": "cfg.have_souHeaWat", "modifiers": {}, "replaceable": false, "options": [], @@ -69245,18 +68677,10 @@ "group": "Energy and ventilation standards", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - false - ] - }, + "enable": false, "modifiers": {}, "replaceable": false, - "options": [ - "Buildings.Controls.OBC.ASHRAE.G36.Types.VentilationStandard.ASHRAE62_1", - "Buildings.Controls.OBC.ASHRAE.G36.Types.VentilationStandard.California_Title_24" - ], + "options": [], "definition": false, "shortExclType": false }, @@ -78399,12 +77823,7 @@ "group": "", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_occSen" - ] - }, + "enable": "have_occSen", "modifiers": {}, "replaceable": false, "options": [], @@ -80758,12 +80177,7 @@ "group": "Setpoint adjustable setting", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_locAdj" - ] - }, + "enable": "have_locAdj", "modifiers": {}, "replaceable": false, "options": [], @@ -80883,12 +80297,7 @@ "group": "Setpoints limits setting", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_winSen" - ] - }, + "enable": "have_winSen", "modifiers": {}, "replaceable": false, "options": [], @@ -80908,12 +80317,7 @@ "group": "Setpoints limits setting", "tab": "", "visible": true, - "enable": { - "operator": "none", - "operands": [ - "have_winSen" - ] - }, + "enable": "have_winSen", "modifiers": {}, "replaceable": false, "options": [], diff --git a/docs/linkage-schema.md b/docs/linkage-schema.md index fbc72355..5d3b340c 100644 --- a/docs/linkage-schema.md +++ b/docs/linkage-schema.md @@ -12,8 +12,7 @@ Read-Only Data types (static data extracted from modelica packages) - `SystemType`: template category - `Template`: Modelica Template - `Option`: a node of info from the template that either specifies how to render a dropdown or bit of UI, or links to a list of child `Option`s -- `ScheduleOption`: Like an `Option` but it also has a list of `ScheduleCategory`s -- `ScheduleCategory`: Category for `ScheduleOption` +- `ScheduleOption`: a `Table` object representing the column headers and cells of a schedule table derived from a Modelica record class Write Data types (data created from user input): @@ -152,24 +151,67 @@ export type Expression = { `Operator`: a string indicating which operation. `none` is a special operator indiciating the operand is a `Literal` that requires no evaluation. `Operands`: a list that can either be a literal value (like the number `4`) or another expression to resolve -### Schedule Table reorganization +### Schedule Table -Inputs for the schedule table are generically extracted in the same option tree used for configurations. The parser needs to be updated to unpack the tree into more convenient format as well as include grouping information for the schedule table. +Scheduled parameters are extracted from Modelica record classes specified for each template using the class annotation `__ctrlFlow(schedule={"modelica://..."})`. -`ScheduleGroup`: +The schedule parser transforms these records into a structured `Table` object that represents both the header structure (columns) and data cells. + +`LeafColumn` — a concrete parameter column: ```typescript -interface ScheduleGroup { - modelicaPath: string; - name: string; +interface LeafColumn { + kind: "leaf"; + instanceName: string; // relative instance name, e.g. "ctl.k" + label: string; // display label, e.g. "Air flow rate" + type: string; + value?: number | string | boolean | Expression; // default binding + final?: boolean; // true if locked by a final modifier + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + enable?: boolean | Expression; +} +``` + +`GroupColumn` — a named column group, nestable arbitrarily deep: +- Created from `Dialog(group=...)` or `Dialog(tab=...)` annotations +- Created for record-typed component instances (children are the record's parameters) + +```typescript +interface GroupColumn { + kind: "group"; + label: string; // e.g. "Cooling coil" + instanceName?: string; // relative instance name, present only for record instances + type?: string; + children: Column[]; // nested LeafColumn or GroupColumn entries + enable?: boolean | Expression; +} +``` + +```typescript +type Column = LeafColumn | GroupColumn; +``` + +`Cell` — a single data value for a given row and column: + +```typescript +interface Cell { + rowIndex: number; + columnInstanceName: string; // matches a LeafColumn instanceName + value: string | number | boolean | null; } ``` -`ScheduleOption`: +`Table` — the complete schedule table: ```typescript -interface ScheduleOption extends OptionInterface { - ScheduleGroups: string[]; // in-order list of schedule groups +interface Table { + configuration: string; // effective type path of the 'cfg' component + columns: Column[]; // tree of headers + cells: Cell[]; // data cells, filled at UI runtime } ``` @@ -178,15 +220,17 @@ From this example table: ``` | group1 | | subgroup1 | subgroup2 | subgroup3 | - |--------|-----------------|--------|--------|-----------------| - | param1 | param2 | param3 | param4 | param5 | param6 | param7 | + Row |--------|-----------------|--------|--------|-----------------| + index | param1 | param2 | param3 | param4 | param5 | param6 | param7 | |-------|--------|--------|--------|--------|--------|--------|--------| -| row1 | val1 | val2 | val3 | val4 | val5 | val6 | val7 | -| row2 | ... +| 1 | val1 | val2 | val3 | val4 | val5 | val6 | val7 | +| 2 | ... ``` -- Each 'val' cell is its own `Option` with a unique modelica path -- Each `Option` can be grouped in an arbitrary number of groups. `val1` is in no groups, `val2` in one ('subgroup1') and `val4` in two ('group1', 'subgroup2'). The `ScheduleGroups` array for `val4` would look as follows: `['group1', 'subgroup2']` +- `param1` is a `LeafColumn` at the top level (`columns` array directly). +- `param2` and `param3` are `LeafColumn` entries nested inside a `GroupColumn` with `label: "subgroup1"`. +- `param4` and `param5` are `LeafColumn` entries nested inside a `GroupColumn` with `label: "subgroup2"`, which is itself nested inside a `GroupColumn` with `label: "group1"`. +- Each 'val' cell is a `Cell` whose `columnInstanceName` matches the corresponding `LeafColumn`'s `instanceName`. ## Write Data - User Configurations diff --git a/server/src/parser/index.ts b/server/src/parser/index.ts index b25fb965..2c1fad0e 100644 --- a/server/src/parser/index.ts +++ b/server/src/parser/index.ts @@ -2,7 +2,9 @@ import path from "path"; import * as loader from "./loader"; import * as parser from "./parser"; import * as templates from "./template"; +import * as schedule from "./schedule"; export { SystemTypeN as SystemType, Template } from "./template"; +export { buildParameterTable } from "./schedule"; /** * diff --git a/server/src/parser/loader.ts b/server/src/parser/loader.ts index c9a871b0..33e7c7ff 100644 --- a/server/src/parser/loader.ts +++ b/server/src/parser/loader.ts @@ -90,16 +90,15 @@ function systemGrep(regExp: string, dirPath: string): string[] | null { * Finds all entry points that contain the template identifier for a given package. * - LIMITATION: This function requires that the package uses * [Directory Hierarchy Mapping](https://specification.modelica.org/maint/3.6/packages.html#directory-hierarchy-mapping) - * - Currently, only entryPoints, TEMPLATE_LIST and PACKAGE_LIST are used. - * - In the future, when the UI can handle nested packages, entryPoints should be removed - * and templateNodes should be used to create the file tree structure. + * - Currently, templateNodes is flattened before being returned. + * - In the future, when the UI can handle nested packages, templateNodes should be + * used directly to create the file tree structure. * @param packageName - The Modelica class name of the package to search for entry points * @returns An array of objects containing the path and parsed JSON for each entry point found */ export function findPackageEntryPoints( packageName: string, ): { className: string; json: Object | undefined }[] { - const entryPoints: { className: string; json: Object | undefined }[] = []; MODELICA_JSON_PATH.forEach((dir) => { // We can simply convert the class name to a relative path without adding any file extension // because we only need a top directory to look up for entry points. @@ -208,13 +207,13 @@ function getPathFromClassName( * @returns The loaded JSON object or undefined if not found */ export function loader(className: string): Object | undefined { - // TODO: allow modelica paths - if (!className.startsWith("Modelica")) { - for (const dir of MODELICA_JSON_PATH) { - const jsonFile = getPathFromClassName(className, dir); - if (jsonFile && fs.existsSync(jsonFile)) { - return require(jsonFile); - } + if (className.startsWith("Modelica") && !className.startsWith("Modelica.Units")) { + return undefined; + } + for (const dir of MODELICA_JSON_PATH) { + const jsonFile = getPathFromClassName(className, dir); + if (jsonFile && fs.existsSync(jsonFile)) { + return require(jsonFile); } } } diff --git a/server/src/parser/parser.ts b/server/src/parser/parser.ts index 9bf932e6..35ed0274 100644 --- a/server/src/parser/parser.ts +++ b/server/src/parser/parser.ts @@ -516,6 +516,14 @@ export class ShortClass extends Element { this.description = definition.description?.description_string; this.replaceable = definition.replaceable; + if (specifier.value?.class_modification) { + this.mods = getModificationList( + specifier.value, + this.modelicaPath, + this.type, + ); + } + if (this.replaceable) { initializeReplaceable(this, definition, basePath); } @@ -714,7 +722,7 @@ function setUIInfo(instance: Element): void { instance.group = group ? evaluateExpression(group) : ""; instance.tab = tab ? evaluateExpression(tab) : ""; const _enable = isDisabledGroup ? false : true; - instance.enable = enable ? enable : _enable; + instance.enable = enable ? evaluateExpression(enable) : _enable; } else { instance.enable = isDisabledGroup ? instance.enable : true; } @@ -1068,12 +1076,20 @@ function _constructElement( let element: Element | undefined; switch (elementType) { - case "type": + case "type": { // May only be predefined types, enumerations, array of type, or classes extending from type. - // Synctatically, these are short class definitions, but they need to be treated specifically - // as they define enumerations that are used in the parameter dialogs. - element = new Enumeration(definition, basePath, elementType); + // Route to Enumeration only if the short class specifier has an enum_list; + // otherwise it's a type alias (e.g. MassFlowRate = Real(...)) and goes to ShortClass. + const typeSpecifier = + (definition.class_definition ?? definition).class_specifier + .short_class_specifier; + if (typeSpecifier?.value?.enum_list) { + element = new Enumeration(definition, basePath, elementType); + } else { + element = new ShortClass(definition, basePath, elementType); + } break; + } case "class": case "connector": case "model": diff --git a/server/src/parser/schedule.ts b/server/src/parser/schedule.ts new file mode 100644 index 00000000..2874f236 --- /dev/null +++ b/server/src/parser/schedule.ts @@ -0,0 +1,724 @@ +/** + * Schedule builder - creates hierarchical table structures from Modelica record classes + * + * This module traverses a Modelica record class hierarchy and extracts parameter + * information into a table structure suitable for UI rendering. + * + * ## Example Usage + * + * ```typescript + * import { buildParameterTable } from "./parser/schedule"; + * + * // Build a parameter table for the record + * const table = buildParameterTable("Buildings.Templates.AirHandlersFans.Data.VAVMultiZone"); + * + * // Access the hierarchical columns + * table.columns.forEach(column => { + * if (column.kind === "leaf") { + * console.log(`Parameter: ${column.label}`); + * console.log(` Instance name: ${column.instanceName}`); // relative instance path, e.g. "ctl.k" + * console.log(` Unit: ${column.unit}`); + * console.log(` Display Unit: ${column.displayUnit}`); + * console.log(` Min: ${column.min}, Max: ${column.max}`); + * console.log(` Enable: ${column.enable}`); // undefined means always enabled + * } else { + * console.log(`Group: ${column.label} with ${column.children.length} children`); + * } + * }); + * + * // Add cells at runtime; rowIndex groups cells into logical rows. + * // To retrieve the enable expression for a cell, look up the LeafColumn by columnInstanceName. + * table.cells.push( + * { rowIndex: 0, columnInstanceName: "ctl.k", value: 1.5 }, + * { rowIndex: 0, columnInstanceName: "coiHeaReh.dpAir_nominal", value: 250 }, + * ); + * ``` + * + * ## Column Structure + * + * - **LeafColumn**: Represents a single parameter + * - `instanceName`: instance path relative to the top-level record + * - `label`: description string or parameter name + * - `unit`, `displayUnit`: resolved by walking the type alias chain + * (e.g. `PressureDifference → Pressure → Real(unit="Pa")`) + * - `min`, `max`, `start`: numeric bounds or runtime Expression objects + * - `enable`: `false` or an Expression if conditionally disabled; absent means always enabled. + * Determined by `__ctrlFlow(enable=false)` (takes precedence) or `Dialog(enable=...)`. + * + * - **GroupColumn**: Represents a collection of parameters. + * - Created from `Dialog(group=...)` or `Dialog(tab=...)` annotations + * - Created for record-typed component instances (children are the record's parameters) + * - Can be nested arbitrarily deep + */ + +import { + typeStore, + Element, + LongClass, + ShortClass, + Component, + MLS_PREDEFINED_TYPES, + isInputGroup, +} from "./parser"; +import { Modification } from "./modification"; +import { evaluateExpression, Expression, Literal } from "./expression"; + +// --- Header definitions --- + +export interface LeafColumn { + kind: "leaf"; + instanceName: string; // relative instance name, e.g. "ctl.k" + label: string; // display label, e.g. "Air flow rate" + type: string; + value?: number | string | boolean | Expression; // default binding; absent if unset + final?: boolean; // true if the value is locked by a final modifier + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + enable?: boolean | Expression; // __ctrlFlow.enable=false overrides Dialog.enable; otherwise use Dialog.enable +} + +export interface GroupColumn { + kind: "group"; + label: string; // e.g. "Cooling coil" + instanceName?: string; // relative instance name, present only for record instances + type?: string; + children: Column[]; // can nest arbitrarily deep + enable?: boolean | Expression; // __ctrlFlow.enable=false overrides Dialog.enable; otherwise use Dialog.enable +} + +export type Column = LeafColumn | GroupColumn; + +// --- Cell --- + +export interface Cell { + rowIndex: number; + columnInstanceName: string; // matches a LeafColumn instanceName; use table.columns to look up column.enable + value: string | number | boolean | null; +} + +// --- Full table --- + +export interface Table { + configuration: string; // final type (after redeclare) of the component with instance name 'cfg' + columns: Column[]; // tree of headers + cells: Cell[]; +} + +/** + * Builds a parameter table from a Modelica class by traversing its + * inheritance and composition tree. + */ +export function buildParameterTable(className: string): Table { + const element = typeStore.get(className); + + if (!element) { + throw new Error(`Class ${className} not found in typeStore`); + } + + const columns = buildColumnsFromElement(element); + + // Find the 'cfg' component and resolve its effective type after any redeclare + const childElements = (element as LongClass).getChildElements?.() ?? []; + const cfgComponent = childElements.find( + (c) => + c.elementType === "component_clause" && (c as Component).name === "cfg", + ) as Component | undefined; + const redeclareMod = (element as LongClass).mods?.find( + (m) => m.name === "cfg" && m.redeclare, + ); + const configuration = redeclareMod?.redeclare ?? cfgComponent?.type ?? ""; + + return { + configuration, + columns, + cells: [], // Empty cells - to be filled at UI runtime + }; +} + +/** + * Builds a modifier map from a component's mod.mods for propagation into nested records. + * Maps local param name (e.g. "typ") -> overriding value (Expression or Literal). + * Merges with any inherited map passed from the parent scope. + */ +function buildModifierMap( + mod: Modification | null | undefined, + inherited: ModifierMap, +): ModifierMap { + const map: ModifierMap = new Map(inherited); + if (!mod?.mods) return map; + for (const nestedMod of mod.mods) { + if (nestedMod.value !== undefined && nestedMod.name) { + map.set(nestedMod.name, nestedMod.value); + } + } + return map; +} + +type ModifierMap = Map; + +/** + * Recursively substitutes any string operand in an Expression that matches a + * key in the modifier map with the corresponding value. + * Also prepends the instance prefix to operands whose root name appears in + * the set of known local instance names (localNames). + */ +function applyModifiers( + value: Expression | Literal, + modMap: ModifierMap, + instancePrefix: string, + localNames: Set, +): Expression | Literal { + if (typeof value !== "object" || value === null) { + // Plain literal — if it's a string reference in the mod map, substitute + if (typeof value === "string") { + // Check top-level name (e.g. "typ") and dotted first segment (e.g. "cfg" in "cfg.typCoiCoo") + const topName = value.split(".")[0]; + if (modMap.has(value)) { + return modMap.get(value)!; + } + if (modMap.has(topName)) { + // e.g. "cfg.typCoiCoo" with map key "cfg" -> substitute cfg part + const replacement = modMap.get(topName)!; + const rest = value.slice(topName.length + 1); + if (rest && typeof replacement === "string") { + return `${replacement}.${rest}`; + } + return replacement; + } + // Only prepend instance prefix for operands whose root name is a known + // local component name in the enclosing record scope. + if ( + instancePrefix && + localNames.has(topName) && + !value.startsWith(instancePrefix) + ) { + return `${instancePrefix}.${value}`; + } + } + return value; + } + // Expression object — recurse into operands + return { + operator: (value as Expression).operator, + operands: (value as Expression).operands.map((op) => + applyModifiers(op, modMap, instancePrefix, localNames), + ), + }; +} + +/** + * Rebase a component's modelicaPath from its type root to the instance path. + * e.g. component.modelicaPath = "Buildings.Templates.Components.Data.Coil.mAir_flow_nominal" + * typeRootPath = "Buildings.Templates.Components.Data.Coil" + * instancePath = "coiCoo" + * -> "coiCoo.mAir_flow_nominal" + */ +function rebasePath( + modelicaPath: string, + typeRootPath: string, + instancePath: string, +): string { + if (modelicaPath.startsWith(typeRootPath + ".")) { + return instancePath + modelicaPath.slice(typeRootPath.length); + } + return modelicaPath; +} + +/** + * Recursively builds columns from a record, traversing both inheritance and + * composition trees. + * + * @param element - The record type definition to traverse + * @param instancePath - The dotted instance path relative to the top-level record + * (e.g. "coiCoo" or "coiCoo.dat"). Used to rebase instance names. + * @param modMap - Modifier overrides propagated from parent declarations + * (e.g. coiCoo(typ=cfg.typCoiCoo) produces {typ -> cfg.typCoiCoo}) + */ +function buildColumnsFromElement( + element: Element, + instancePath = "", + modMap: ModifierMap = new Map(), +): Column[] { + const columns: Column[] = []; + const groupedColumns = new Map(); // group/tab name -> columns + + // Merge this element's extends mods (non-redeclare, value-bearing) into the modMap + // so that e.g. `extends Parent(x=someValue)` overrides are visible to all child columns. + for (const mod of (element as LongClass).mods ?? []) { + if ( + mod.value !== undefined && + mod.name && + !mod.redeclare && + !modMap.has(mod.name) + ) { + modMap.set(mod.name, mod.value); + } + } + + // Get all child elements (includes inherited elements) + const childElements = + element.elementType === "record" + ? (element as LongClass).getChildElements() + : []; + + // Build the set of local component names so applyModifiers can discriminate + // which expression operands are local references (and should be prefixed with + // instancePath) vs. absolute type paths / numeric literals. + const localNames = new Set( + childElements + .filter((c) => c.elementType === "component_clause") + .map((c) => (c as Component).name) + .filter(Boolean), + ); + + for (const child of childElements) { + // Skip non-component elements + if (child.elementType !== "component_clause") { + continue; + } + + const component = child as Component; + + // Skip outer, final, or invisible parameters + if (component.outer || component.final || component.deadEnd) { + continue; + } + + // Build the instance name as a relative path from the top-level record. + // For nested records, rebasePath strips the type-root prefix and replaces it + // with the instance path. For top-level components (instancePath="") and for + // inherited components whose modelicaPath is rooted at a parent class (not + // element.modelicaPath), we fall back to using component.name directly so + // the name is always a short relative name, never an absolute type path. + const rebasedName = instancePath + ? rebasePath(component.modelicaPath, element.modelicaPath, instancePath) + : component.name; + // If rebasePath couldn't strip the type prefix (inherited component from a + // parent class), fall back to composing the name from component.name directly. + const instanceName = rebasedName.startsWith(instancePath) + ? rebasedName + : instancePath + ? `${instancePath}.${component.name}` + : component.name; + + // Check if this component is redeclared in the current element's extends mods. + // e.g. VAVMultiZone extends PartialAirHandler(redeclare VAVMultiZoneController ctl(...)) + // stores a Modification with name="ctl", redeclare="...VAVMultiZoneController" in element.mods. + const redeclareMod = (element as LongClass).mods?.find( + (m) => m.name === component.name && m.redeclare, + ); + const effectiveTypePath = redeclareMod?.redeclare ?? component.type; + + // Determine if this is a predefined (leaf) type or a record (group) type + const isPredefinedType = MLS_PREDEFINED_TYPES.includes(effectiveTypePath); + const componentType = typeStore.get(effectiveTypePath); + const isRecordType = + componentType && componentType.elementType === "record"; + + let column: Column; + + if (isPredefinedType || !isRecordType) { + // Create a leaf column for predefined types and non-record types + const attributes = extractAttributes( + component, + element, + modMap, + instancePath, + localNames, + ); + column = { + kind: "leaf", + instanceName: instanceName, + label: component.description || component.name, + type: effectiveTypePath, + ...attributes, + }; + } else { + // Build the nested instance path and inherit/merge modifiers from this component. + // If there's a redeclare, also merge its nested mods (e.g. ctl(typSecOut=cfg.typSecOut)). + const nestedInstancePath = instancePath + ? `${instancePath}.${component.name}` + : component.name; + let nestedModMap = buildModifierMap(component.mod, modMap); + if (redeclareMod?.mods?.length) { + nestedModMap = buildModifierMap(redeclareMod, nestedModMap); + } + + // Create a group column for record types, recursing with context + const recordColumns = buildColumnsFromElement( + componentType, + nestedInstancePath, + nestedModMap, + ); + const groupAttributes = extractAttributes( + component, + element, + modMap, + instancePath, + localNames, + ); + column = { + kind: "group", + instanceName: nestedInstancePath, + label: component.description || component.name, + type: effectiveTypePath, + children: recordColumns, + ...(groupAttributes.enable !== undefined && { + enable: groupAttributes.enable, + }), + }; + } + + // Group by tab or group annotation if present + const groupKey = component.tab || component.group; + if (groupKey) { + if (!groupedColumns.has(groupKey)) { + groupedColumns.set(groupKey, []); + } + groupedColumns.get(groupKey)!.push(column); + } else { + columns.push(column); + } + } + + // Add grouped columns as GroupColumn entries + for (const [groupLabel, groupChildren] of groupedColumns.entries()) { + columns.push({ + kind: "group", + label: groupLabel, + children: groupChildren, + }); + } + + return columns; +} + +/** + * Extracts unit, displayUnit, min, max, start, enable attributes from a component. + * For non-predefined types, walks up the inheritance chain if attributes are missing. + * Applies modMap substitutions and rebases references to instancePrefix. + * + * @param localNames - Set of local component names in the enclosing record scope, + * used to safely discriminate which expression operands should + * be prefixed with instancePrefix. + */ +function extractAttributes( + component: Component, + element: Element, + modMap: ModifierMap = new Map(), + instancePrefix = "", + localNames: Set = new Set(), +): { + value?: number | string | boolean | Expression; + final?: boolean; + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + enable?: boolean | Expression; +} { + const result: { + value?: number | string | boolean | Expression; + final?: boolean; + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + enable?: boolean | Expression; + } = {}; + + // enable: __ctrlFlow.enable=false overrides Dialog.enable; otherwise use Dialog.enable + const ctrlFlowEnable = component.getLinkageKeywordValue(); + if (ctrlFlowEnable === false) { + result.enable = false; + } else if (component.enable !== undefined && component.enable !== true) { + result.enable = applyModifiersToAttr( + component.enable, + modMap, + instancePrefix, + localNames, + ); + } + + // value: check modMap first (parent override), then component's own declaration binding. + // A modMap entry for component.name means an enclosing record has set this parameter. + const modMapValue = modMap.get(component.name); + const rawValue = modMapValue !== undefined ? modMapValue : component.value; + if (rawValue !== undefined) { + result.value = applyModifiersToAttr( + rawValue, + modMap, + instancePrefix, + localNames, + ); + } + + // final: true if component.mod is marked final, or if a modifier in element.mods + // targets this component with final=true (e.g. extends Foo(final x=1)). + const parentFinalMod = (element as LongClass).mods?.find( + (m) => m.name === component.name && m.final, + ); + if (component.mod?.final || parentFinalMod) { + result.final = true; + } + + const isPredefinedType = MLS_PREDEFINED_TYPES.includes(component.type); + const isReal = component.type === "Real"; + + // Only retrieve quantity attributes for Real types or non-predefined types + if (!isReal && isPredefinedType) { + return result; + } + + // First try to get attributes from the type's TemplateInput modifiers + const typeElement = typeStore.get(component.type); + if (typeElement && !isPredefinedType) { + const typeInputs = typeElement.getInputs({}, false); + const typeInput = typeInputs[component.type]; + if (typeInput && typeInput.modifiers) { + extractFromTemplateInputModifiers( + component.modelicaPath, + typeInput.modifiers, + result, + ); + } + } + + // Also check component's own modifiers + if (component.mod) { + extractAttributesFromModifiers( + component.modelicaPath, + component.mod, + result, + ); + } + + // If type is not predefined and any attributes are still missing, walk up the + // type chain until all attributes are found or no parent remains. + if (!isPredefinedType && typeElement) { + let current: Element | undefined = typeElement; + while (current) { + if ( + result.unit !== undefined && + result.displayUnit !== undefined && + result.min !== undefined && + result.max !== undefined && + result.start !== undefined + ) { + break; + } + if ("mods" in current && (current as any).mods) { + const parentResult: typeof result = {}; + extractAttributesFromModifiers( + current.modelicaPath, + { mods: (current as any).mods }, + parentResult, + ); + result.unit = result.unit ?? parentResult.unit; + result.displayUnit = result.displayUnit ?? parentResult.displayUnit; + result.min = result.min ?? parentResult.min; + result.max = result.max ?? parentResult.max; + result.start = result.start ?? parentResult.start; + } + current = nextInTypeChain(current); + } + } + + // Apply modifier substitutions to numeric/expression attributes + if (result.min !== undefined) { + result.min = applyModifiersToAttr( + result.min, + modMap, + instancePrefix, + localNames, + ); + } + if (result.max !== undefined) { + result.max = applyModifiersToAttr( + result.max, + modMap, + instancePrefix, + localNames, + ); + } + if (result.start !== undefined) { + result.start = applyModifiersToAttr( + result.start, + modMap, + instancePrefix, + localNames, + ); + } + + return result; +} + +/** + * Applies modifier substitutions to an attribute value (literal or Expression). + */ +function applyModifiersToAttr( + value: any, + modMap: ModifierMap, + instancePrefix: string, + localNames: Set, +): any { + if (typeof value !== "object") { + return value; + } + return applyModifiers( + value as Expression, + modMap, + instancePrefix, + localNames, + ); +} + +/** + * Unwraps a mod value which may be a raw Literal or an Expression object. + * - If the value reduces to a scalar literal, returns it directly. + * - If it is a complex Expression that cannot be reduced, returns the Expression + * so the client can evaluate it at runtime. + * - Returns undefined for null/undefined values. + */ +function resolveModValue( + value: any, +): string | number | boolean | Expression | undefined { + if (value === null || value === undefined) { + return undefined; + } + if (typeof value !== "object") { + return value as string | number | boolean; + } + // Expression object — try to reduce to a literal + const evaluated = evaluateExpression(value as Expression); + if (evaluated === null) { + return undefined; + } + // Return the literal if simple, otherwise keep the full Expression for client eval + return evaluated as string | number | boolean | Expression; +} + +/** + * Extracts attributes from a component's modification object + */ +function extractAttributesFromModifiers( + modelicaPath: string, + mod: any, + result: { + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + }, +): void { + if (!mod || !mod.mods) { + return; + } + + // Look through nested modifiers for attribute modifiers + for (const nestedMod of mod.mods) { + const modPath = nestedMod.modelicaPath || ""; + const raw = resolveModValue(nestedMod.value); + + if ( + modPath.endsWith(".unit") && + raw !== undefined && + typeof raw !== "object" + ) { + result.unit = String(raw); + } else if ( + modPath.endsWith(".displayUnit") && + raw !== undefined && + typeof raw !== "object" + ) { + result.displayUnit = String(raw); + } else if (modPath.endsWith(".min") && raw !== undefined) { + result.min = typeof raw === "object" ? raw : Number(raw); + } else if (modPath.endsWith(".max") && raw !== undefined) { + result.max = typeof raw === "object" ? raw : Number(raw); + } else if (modPath.endsWith(".start") && raw !== undefined) { + result.start = typeof raw === "object" ? raw : Number(raw); + } + } +} + +/** + * Extracts attributes from a TemplateInput's modifiers array + */ +function extractFromTemplateInputModifiers( + modelicaPath: string, + modifiers: any[], + result: { + unit?: string; + displayUnit?: string; + min?: number | Expression; + max?: number | Expression; + start?: number | Expression; + }, +): void { + if (!modifiers || !Array.isArray(modifiers)) { + return; + } + + // Iterate through the modifiers array + for (const mod of modifiers) { + if (!mod || !mod.mods || !Array.isArray(mod.mods)) { + continue; + } + + // Look through nested modifiers for attribute modifiers + for (const nestedMod of mod.mods) { + const modPath = nestedMod.modelicaPath || ""; + const raw = resolveModValue(nestedMod.value); + + if ( + modPath.endsWith(".unit") && + raw !== undefined && + typeof raw !== "object" + ) { + result.unit = String(raw); + } else if ( + modPath.endsWith(".displayUnit") && + raw !== undefined && + typeof raw !== "object" + ) { + result.displayUnit = String(raw); + } else if (modPath.endsWith(".min") && raw !== undefined) { + result.min = typeof raw === "object" ? raw : Number(raw); + } else if (modPath.endsWith(".max") && raw !== undefined) { + result.max = typeof raw === "object" ? raw : Number(raw); + } else if (modPath.endsWith(".start") && raw !== undefined) { + result.start = typeof raw === "object" ? raw : Number(raw); + } + } + } +} + +/** + * Returns the next element in the type chain: + * - LongClass: follows extendElement (explicit extends clause) + * - ShortClass: follows the aliased type (e.g. PressureDifference -> Pressure -> Real) + * - stops at predefined MLS types + */ +function nextInTypeChain(current: Element): Element | undefined { + if (current instanceof LongClass) { + return current.extendElement; + } + if (current instanceof ShortClass) { + const aliasedType = current.type; + if ( + aliasedType && + aliasedType !== current.modelicaPath && + !MLS_PREDEFINED_TYPES.includes(aliasedType) + ) { + return typeStore.get(aliasedType) ?? undefined; + } + } + return undefined; +} diff --git a/server/src/parser/template.ts b/server/src/parser/template.ts index 6d686992..811d6c65 100644 --- a/server/src/parser/template.ts +++ b/server/src/parser/template.ts @@ -11,6 +11,7 @@ import * as parser from "./parser"; import { evaluateExpression, Expression, Literal } from "./expression"; import { Modification } from "./modification"; +import { buildParameterTable, Table } from "./schedule"; const templateStore = new Map(); const systemTypeStore = new Map(); @@ -29,16 +30,16 @@ export function getProject(): Project { } type Options = { [key: string]: Option }; -type ScheduleOptions = { [key: string]: ScheduleOption }; +type ScheduleOptions = { [key: string]: Table }; export function getOptions(): { options: Option[]; - scheduleOptions: ScheduleOption[]; + scheduleOptions: Table[]; } { const templates = [...templateStore.values()]; let allConfigOptions = {}; - let allScheduleOptions = {}; + let allScheduleOptions: ScheduleOptions = {}; templates.map((t) => { const { options, scheduleOptions } = t.getOptions(); @@ -240,7 +241,7 @@ export interface ModifiersN { export class Template { scheduleOptionPaths: string[] = []; options: Options = {}; - scheduleOptions: ScheduleOptions = {}; + scheduleOptions: { [key: string]: Table } = {}; systemTypes: SystemTypeN[] = []; mods: { [key: string]: Expression } = {}; pathMods: { [key: string]: string } = {}; @@ -316,6 +317,44 @@ export class Template { return Object.keys(redeclaredTypes); } + /** + * Extracts schedule class paths from the `__ctrlFlow(schedule=...)` class annotation. + * + * The annotation value is a Modelica array literal of URI strings, e.g. + * `{"modelica://Buildings/Templates/AirHandlersFans/Data/VAVMultiZone.mo"}` + * + * Each URI ending in ".mo" is converted to dot-notation: + * `modelica://Buildings/Templates/AirHandlersFans/Data/VAVMultiZone.mo` + * → `Buildings.Templates.AirHandlersFans.Data.VAVMultiZone` + */ + _extractSchedulePaths(element: parser.Element): string[] { + const ctrlFlowAnnotation = element.annotation?.find( + (m) => m.name === "__ctrlFlow", + ); + if (!ctrlFlowAnnotation) return []; + + const scheduleMod = ctrlFlowAnnotation.mods.find( + (m) => m.name === "schedule", + ); + if (!scheduleMod) return []; + + const rawValue = evaluateExpression(scheduleMod.value); + if (typeof rawValue !== "string") return []; + + // Parse the Modelica array literal: strip outer { } then split on commas + const inner = rawValue.trim().replace(/^\{|\}$/g, ""); + return inner + .split(",") + .map((s) => s.trim().replace(/^"|"$/g, "")) + .filter((uri) => uri.endsWith(".mo")) + .map((uri) => + uri + .replace(/^modelica:\/\//, "") + .replace(/\//g, ".") + .replace(/\.mo$/, ""), + ); + } + _extractOptions(element: parser.Element) { let inputs = element.getInputs(); const redeclaredInputs = this._findRedeclareTypes(inputs) @@ -327,11 +366,17 @@ export class Template { inputs = { ...inputs, ...redeclaredInputs }; - const datEntryPoints = Object.values(inputs).filter((i) => { - return i.modelicaPath.endsWith(".dat"); - }); + this.scheduleOptionPaths = this._extractSchedulePaths(element); - this.scheduleOptionPaths = datEntryPoints.map((i) => i.modelicaPath); + this.scheduleOptions = {}; + for (const classPath of this.scheduleOptionPaths) { + try { + const table = buildParameterTable(classPath); + this.scheduleOptions[classPath] = table; + } catch { + // class not yet loaded in typeStore; skip + } + } this.options = {}; Object.entries(inputs).map(([key, input]) => { @@ -341,7 +386,6 @@ export class Template { // kludge: 'Modelica.Icons.Record' is useful for schematics but // never for 'Options' const modelicaIconsPath = "Modelica.Icons.Record"; - delete this.scheduleOptions[modelicaIconsPath]; delete this.options[modelicaIconsPath]; } @@ -406,7 +450,7 @@ export class Template { } getOptions() { - return { options: this.options, scheduleOptions: {} }; + return { options: this.options, scheduleOptions: this.scheduleOptions }; } getSystemTypes() { diff --git a/server/tests/integration/parser/schedule.test.ts b/server/tests/integration/parser/schedule.test.ts new file mode 100644 index 00000000..0a897d7b --- /dev/null +++ b/server/tests/integration/parser/schedule.test.ts @@ -0,0 +1,316 @@ +import { buildParameterTable, Table } from "../../../src/parser/schedule"; +import * as parser from "../../../src/parser/parser"; +import { + prependToModelicaJsonPath, + TEMPLATE_LIST, +} from "../../../src/parser/loader"; +import { getTemplates } from "../../../src/parser/template"; +import * as fs from "fs"; +import * as path from "path"; +import { execSync } from "child_process"; + +// Run with DEBUG_SCHEDULE=1 to log all schedule keys, operands and tables +const DEBUG = !!process.env.DEBUG_SCHEDULE; + +const staticDataDir = "tests/static-data"; +const jsonRecordPath = path.join(staticDataDir, "json"); +// Test package with the annotation __ctrlFlow(schedule={"modelica://Buildings/Templates/AirHandlersFans/Data/VAVMultiZone.mo"}) +const archivePath = path.join(staticDataDir, "json.tar.gz"); +const templateClassName = "Buildings.Templates.AirHandlersFans.VAVMultiZone"; +const scheduleRecordPath = + "Buildings.Templates.AirHandlersFans.Data.VAVMultiZone"; + +describe("Schedule builder", () => { + let table: Table; + let template: ReturnType[number] | undefined; + + beforeAll(() => { + execSync(`tar -xzf ${archivePath} -C ${staticDataDir}`); + prependToModelicaJsonPath([jsonRecordPath]); + TEMPLATE_LIST.push(templateClassName); + parser.getFile(templateClassName); + template = getTemplates().find((t) => t.modelicaPath === templateClassName); + table = template!.scheduleOptions[scheduleRecordPath]; + }); + + afterAll(() => { + fs.rmSync(jsonRecordPath, { recursive: true, force: true }); + }); + + // Kept as a separate block for DEBUG output + beforeAll(() => { + if (DEBUG) { + const keys: string[] = []; + const operands: string[] = []; + + const collect = (columns: any[]) => { + for (const col of columns) { + if (col.kind === "leaf") { + if (col.instanceName) keys.push(col.instanceName); + const collectOperands = (val: any) => { + if (val === null || val === undefined) return; + if (typeof val === "object" && "operands" in val) { + for (const op of val.operands) collectOperands(op); + } else if (typeof val === "string") { + operands.push(val); + } + }; + for (const attr of ["min", "max", "start", "enable"]) { + collectOperands((col as any)[attr]); + } + } else if (col.kind === "group" && col.children) { + collect(col.children); + } + } + }; + + collect(table.columns); + fs.writeFileSync("keys.log", keys.join("\n") + "\n"); + fs.writeFileSync("operands.log", operands.join("\n") + "\n"); + fs.writeFileSync("table.json", JSON.stringify(table, null, 2)); + } + }); + + it("throws an error for non-existent class", () => { + expect(() => buildParameterTable("NonExistent.Class")).toThrow( + "Class NonExistent.Class not found in typeStore", + ); + }); + + it("populates scheduleOptionPaths with dot-notation class names derived from Modelica URIs", () => { + expect(template).toBeDefined(); + expect(template!.scheduleOptionPaths).toEqual([scheduleRecordPath]); + }); + + it("populates scheduleOptions with a Table keyed by the record class path", () => { + expect(template).toBeDefined(); + expect(Object.keys(template!.scheduleOptions)).toEqual([ + scheduleRecordPath, + ]); + }); + + it("returns a table with columns and empty cells", () => { + expect(table).toBeDefined(); + expect(table.columns).toBeDefined(); + expect(Array.isArray(table.columns)).toBe(true); + expect(table.cells).toBeDefined(); + expect(Array.isArray(table.cells)).toBe(true); + expect(table.cells.length).toBe(0); + }); + + it("creates leaf columns for predefined type parameters", () => { + // Find any leaf columns (should exist for String, Boolean, Real, Integer types) + const leafColumns = table.columns.filter((col) => col.kind === "leaf"); + expect(leafColumns.length).toBeGreaterThan(0); + + // Check structure of a leaf column + const firstLeaf = leafColumns[0] as any; + expect(firstLeaf.kind).toBe("leaf"); + expect(firstLeaf.instanceName).toBeDefined(); + expect(typeof firstLeaf.instanceName).toBe("string"); + expect(firstLeaf.label).toBeDefined(); + expect(typeof firstLeaf.label).toBe("string"); + }); + + it("extracts unit, displayUnit, min, max attributes for Real types", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // At least some leaf columns should have these attributes defined + // (for Real types with modifiers) + const hasAttributeColumn = leafColumns.some( + (col: any) => + col.displayUnit !== undefined || + col.unit !== undefined || + col.min !== undefined || + col.max !== undefined, + ); + expect(hasAttributeColumn).toBe(true); + + // For columns that do have these attributes, verify their types + leafColumns.forEach((col: any) => { + if (col.unit !== undefined) expect(typeof col.unit).toBe("string"); + if (col.displayUnit !== undefined) + expect(typeof col.displayUnit).toBe("string"); + if (col.min !== undefined) + expect(typeof col.min === "number" || typeof col.min === "object").toBe( + true, + ); + if (col.max !== undefined) + expect(typeof col.max === "number" || typeof col.max === "object").toBe( + true, + ); + }); + }); + + it("extracts displayUnit from modifiers", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // Check that displayUnit can be extracted when present + leafColumns.forEach((col: any) => { + if (col.displayUnit !== undefined) { + expect(typeof col.displayUnit).toBe("string"); + } + }); + }); + + it("extracts min and max from modifiers as numbers", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // Check that min/max are numbers when present + leafColumns.forEach((col: any) => { + if (col.min !== undefined) { + expect(typeof col.min === "number" || typeof col.min === "object").toBe( + true, + ); + } + if (col.max !== undefined) { + expect(typeof col.max === "number" || typeof col.max === "object").toBe( + true, + ); + } + }); + }); + + it("does not extract attributes for non-Real predefined types", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // String, Boolean, Integer types should not have unit/displayUnit/min/max set + const nonRealColumns = leafColumns.filter( + (col: any) => + col.instanceName.toLowerCase().includes("string") || + col.instanceName.toLowerCase().includes("bool") || + col.instanceName.toLowerCase().includes("integer"), + ); + + nonRealColumns.forEach((col: any) => { + expect(col.unit).toBeUndefined(); + expect(col.displayUnit).toBeUndefined(); + expect(col.min).toBeUndefined(); + expect(col.max).toBeUndefined(); + }); + }); + + it("creates group columns for parameters with Dialog group annotation", () => { + // Look for group columns + const groupColumns = table.columns.filter((col) => col.kind === "group"); + expect(groupColumns.length).toBeGreaterThan(0); + + // Check structure of a group column + const firstGroup = groupColumns[0] as any; + expect(firstGroup.kind).toBe("group"); + expect(firstGroup.label).toBeDefined(); + expect(typeof firstGroup.label).toBe("string"); + expect(firstGroup.children).toBeDefined(); + expect(Array.isArray(firstGroup.children)).toBe(true); + }); + + it("creates group columns for record type parameters", () => { + // The test template has a 'dat' parameter which is a record type + // This should be represented as a group column with nested children + const allColumns = getAllColumns(table.columns); + const recordColumn = allColumns.find( + (col: any) => + col.kind === "group" && + col.label && + col.children && + col.children.length > 0, + ); + + expect(recordColumn).toBeDefined(); + }); + + it("does not include outer, final, or deadEnd parameters", () => { + // Get all leaf columns + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // Check that no final parameters are included + const hasFinalParam = leafColumns.some((col: any) => + col.instanceName.includes("should_ignore"), + ); + expect(hasFinalParam).toBe(false); + }); + + it("handles nested group structures", () => { + // Find a group column and verify it can have children + const groupColumns = table.columns.filter((col) => col.kind === "group"); + if (groupColumns.length > 0) { + const group = groupColumns[0] as any; + expect(group.children).toBeDefined(); + expect(Array.isArray(group.children)).toBe(true); + } + }); + + it("includes parameters from inherited classes", () => { + // TestTemplate extends ExtendInterface which has parameters + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // Should have parameters from both the class itself and inherited classes + expect(leafColumns.length).toBeGreaterThan(0); + }); + + it("uses parameter description as label", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // At least some parameters should have meaningful labels + const hasDescriptiveLabel = leafColumns.some( + (col: any) => col.label && col.label.length > 0, + ); + expect(hasDescriptiveLabel).toBe(true); + }); + + it("handles tab annotations", () => { + // The test data has a parameter with Dialog(tab="Tabby") + const allColumns = getAllColumns(table.columns); + const tabGroup = allColumns.find( + (col: any) => col.kind === "group" && col.label === "Tabby", + ); + + // Tab should create a group if parameters use it + if (tabGroup) { + expect(tabGroup).toBeDefined(); + } + }); + + it("creates unique keys for leaf columns", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + const keys = leafColumns.map((col: any) => col.instanceName); + + // All keys should be unique + const uniqueKeys = new Set(keys); + expect(uniqueKeys.size).toBe(keys.length); + }); + + it("handles inheritance of attributes from parent types", () => { + const allColumns = getAllColumns(table.columns); + const leafColumns = allColumns.filter((col: any) => col.kind === "leaf"); + + // If a parameter has a custom type that extends Real, + // it should inherit attributes from the parent type + // This test just verifies the mechanism doesn't break + expect(leafColumns.length).toBeGreaterThan(0); + }); +}); + +/** + * Helper function to flatten all columns (including nested ones) into a single array + */ +function getAllColumns(columns: any[]): any[] { + const result: any[] = []; + + for (const col of columns) { + result.push(col); + if (col.kind === "group" && col.children) { + result.push(...getAllColumns(col.children)); + } + } + + return result; +} diff --git a/server/tests/static-data/json.tar.gz b/server/tests/static-data/json.tar.gz new file mode 100644 index 00000000..a1040cdf Binary files /dev/null and b/server/tests/static-data/json.tar.gz differ