From b00e50e0f8a89b2ccf0c69f3e00bdc1913038a4e Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Wed, 25 Feb 2026 12:11:03 +0100 Subject: [PATCH 1/3] Added creation of dashboards to EMS setup asset, added power consumption fluctuations to EMS demo rules and other improvements to forecast simulation demo rules --- .../manager/EmsOptimisationSetupService.java | 101 +++- .../ems/dashboards/EmsBatteryDashboard.json | 203 ++++++++ .../ems/dashboards/EmsOverviewDashboard.json | 443 ++++++++++++++++++ .../ems/rules/EmsDemoSimulationRules.groovy | 102 ++-- 4 files changed, 804 insertions(+), 45 deletions(-) create mode 100644 ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json create mode 100644 ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java index b975a8b..8d0e176 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java @@ -22,21 +22,29 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; -import org.openremote.extension.ems.agent.*; import org.openremote.container.persistence.PersistenceService; +import org.openremote.extension.ems.agent.*; import org.openremote.manager.app.ConfigurationService; import org.openremote.manager.asset.AssetProcessingService; import org.openremote.manager.asset.AssetStorageService; +import org.openremote.manager.dashboard.DashboardStorageService; import org.openremote.manager.event.ClientEventService; +import org.openremote.model.Constants; import org.openremote.model.Container; import org.openremote.model.ContainerService; import org.openremote.model.asset.AssetFilter; +import org.openremote.model.attribute.Attribute; import org.openremote.model.attribute.AttributeEvent; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.dashboard.Dashboard; import org.openremote.model.manager.ManagerAppConfig; import org.openremote.model.query.AssetQuery; import org.openremote.model.rules.RealmRuleset; import org.openremote.model.syslog.SyslogCategory; import org.openremote.model.util.UniqueIdentifierGenerator; +import org.openremote.model.util.ValueUtil; +import org.openremote.model.value.MetaItemType; +import org.openremote.model.value.ValueType; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -60,6 +68,7 @@ public class EmsOptimisationSetupService implements ContainerService { protected AssetStorageService assetStorageService; protected ClientEventService clientEventService; private ConfigurationService configurationService; + protected DashboardStorageService dashboardStorageService; protected PersistenceService persistenceService; protected ScheduledExecutorService scheduledExecutorService; @@ -70,6 +79,7 @@ public void init(Container container) throws Exception { assetStorageService = container.getService(AssetStorageService.class); clientEventService = container.getService(ClientEventService.class); configurationService = container.getService(ConfigurationService.class); + dashboardStorageService = container.getService(DashboardStorageService.class); persistenceService = container.getService(PersistenceService.class); scheduledExecutorService = container.getScheduledExecutor(); } @@ -120,16 +130,15 @@ private void processAttributeEventEnergyOptimisationSetupAsset(AttributeEvent at try { updateManagerConfig(); String infoFieldMessage = createEnergyManagementSystem(setupAsset); + // Add a 1-second delay to ensure the info field is updated after all assets and rules are merged scheduledExecutorService.schedule(() -> assetProcessingService.sendAttributeEvent(new AttributeEvent(setupAsset.getId(), EmsEnergyOptimisationSetupAsset.INFO_FIELD, infoFieldMessage), getClass().getSimpleName()), 1, TimeUnit.SECONDS); - } catch (Exception e) { LOG.warning(String.format("assetName='%s', assetId='%s'; An exception occurred during energy management system creation; Exception: %s", setupAsset.getName(), setupAsset.getId(), e)); } } } - @SuppressWarnings("unchecked") private void updateManagerConfig() { ManagerAppConfig managerConfig = configurationService.getManagerConfig(); @@ -161,7 +170,8 @@ private Map getEmsAssetTypesConfig() { String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); - return mapper.convertValue(mapper.readTree(json), new TypeReference<>() {}); + return mapper.convertValue(mapper.readTree(json), new TypeReference<>() { + }); } catch (Exception e) { throw new RuntimeException("Failed to read resource: " + path, e); } @@ -195,12 +205,14 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu return infoFieldMessage; } + // Create Energy Optimisation Asset EmsEnergyOptimisationAsset energyOptimisationAsset = new EmsEnergyOptimisationAsset(energyOptimisationAssetName); energyOptimisationAsset.setId(UniqueIdentifierGenerator.generateId()).setParent(setupAsset); double powerLimitMaximum = 500.0; double powerLimitMinimum = -500.0; + double powerFluctuation = 40.0; Double powerConsumption = 400.0; String powerLimitMaximumProfileCsv = generatePowerLimitProfile(powerLimitMaximum); @@ -219,6 +231,19 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu // .setPowerNet(powerConsumption) ; + energyOptimisationAsset.addOrReplaceAttributes( + new Attribute<>("powerLimitMaximumFluctuationMargin", ValueType.POSITIVE_NUMBER, powerFluctuation) + .addOrReplaceMeta( + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)) + ), + new Attribute<>("powerLimitMinimumFluctuationMargin", ValueType.POSITIVE_NUMBER, powerFluctuation) + .addOrReplaceMeta( + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)) + ) + ); + assetStorageService.merge(energyOptimisationAsset); // Create Electricity Battery Asset @@ -260,33 +285,38 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu assetStorageService.merge(gopacsAsset); } + // Setup rules + String realmName = setupAsset.getRealm(); + String energyOptimisationAssetId = energyOptimisationAsset.getId(); + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + try (InputStream inputStream = EmsOptimisationService.class.getResourceAsStream("/ems/rules/EmsDemoSimulationRules.groovy")) { if (inputStream != null) { - String realmName = setupAsset.getRealm(); String rulesName = energyManagementSystemName + ": Demo simulation rules"; String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); // General variables rules - rules = rules.replaceFirst("setId1", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("setId1", energyOptimisationAssetId); rules = rules.replaceFirst("setValue1", String.valueOf(powerConsumption)); rules = rules.replaceFirst("setValue2", "1"); + rules = rules.replaceFirst("setValue3", String.valueOf(powerFluctuation)); // Link forecasts rule - rules = rules.replaceFirst("linkInputAssetId1", electricityBatteryAsset.getId()); + rules = rules.replaceFirst("linkInputAssetId1", electricityBatteryAssetId); rules = rules.replaceFirst("linkInputAttributeName1", EmsElectricityBatteryAsset.POWER_SETPOINT.getName()); - rules = rules.replaceFirst("linkOutputAssetId1", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("linkOutputAssetId1", energyOptimisationAssetId); rules = rules.replaceFirst("linkOutputAttributeName1", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); // Sum forecasts rule - rules = rules.replaceFirst("sumInputAssetId1", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("sumInputAssetId1", energyOptimisationAssetId); rules = rules.replaceFirst("sumInputAttributeName1", EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName()); - rules = rules.replaceFirst("sumInputAssetId2", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("sumInputAssetId2", energyOptimisationAssetId); rules = rules.replaceFirst("sumInputAttributeName2", EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName()); - rules = rules.replaceFirst("sumInputAssetId3", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("sumInputAssetId3", energyOptimisationAssetId); rules = rules.replaceFirst("sumInputAttributeName3", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); - rules = rules.replaceFirst("sumOutputAssetId", energyOptimisationAsset.getId()); + rules = rules.replaceFirst("sumOutputAssetId", energyOptimisationAssetId); rules = rules.replaceFirst("sumOutputAttributeName", EmsEnergyOptimisationAsset.POWER_NET.getName()); RealmRuleset districtRuleSet = new RealmRuleset(realmName, rulesName, GROOVY, rules); @@ -295,9 +325,54 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu persistenceService.doReturningTransaction(entityManager -> entityManager.merge(districtRuleSet)); } } catch (Exception e) { - LOG.warning(String.format("assetName='%s', assetId='%s'; Rule were not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); + LOG.warning(String.format("assetName='%s', assetId='%s'; Rules were not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); } + + // Create main dashboard + try (InputStream inputStream = EmsOptimisationService.class.getResourceAsStream("/ems/dashboards/EmsOverviewDashboard.json")) { + String dashboardName = energyManagementSystemName + ": Overview"; + if (inputStream != null) { + + String dashboardStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + dashboardStr = dashboardStr + .replace("setId1", energyOptimisationAssetId) + ; + + Dashboard dashboard = ValueUtil.JSON.readValue(dashboardStr, Dashboard.class); + + dashboard.setRealm(realmName); + dashboard.setDisplayName(dashboardName); + + dashboardStorageService.createNew(dashboard); + } + } catch (Exception e) { + LOG.warning(String.format("assetName='%s', assetId='%s'; Dashboard was not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); + } + + // Create battery dashboard + try (InputStream inputStream = EmsOptimisationService.class.getResourceAsStream("/ems/dashboards/EmsBatteryDashboard.json")) { + String dashboardName = energyManagementSystemName + ": Battery"; + if (inputStream != null) { + + String dashboardStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + dashboardStr = dashboardStr + .replace("setId1", electricityBatteryAssetId); + + Dashboard dashboard = ValueUtil.JSON.readValue(dashboardStr, Dashboard.class); + + dashboard.setRealm(realmName); + dashboard.setDisplayName(dashboardName); + + dashboardStorageService.createNew(dashboard); + } + } catch (Exception e) { + LOG.warning(String.format("assetName='%s', assetId='%s'; Dashboard was not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); + } + + // Create info field message infoFieldMessage = "Created energy management system \"" + energyOptimisationAssetName + "\":\n" + "\n" + diff --git a/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json b/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json new file mode 100644 index 0000000..7137760 --- /dev/null +++ b/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json @@ -0,0 +1,203 @@ +{ + "createdOn": 1711029229921, + "realm": "master", + "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "access": "1", + "displayName": "New Dashboard", + "template": { + "id": "k674dhoxyc", + "columns": 12, + "widgets": [ + { + "id": "cqtsw15lro", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 6, + "id": "cqtsw15lro", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "Energy level (%)", + "widgetConfig": { + "stacked": false, + "showLegend": true, + "showZoomBar": false, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "energyLevelPercentage" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1771324682600, + "fromTimestamp": 1771321080000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "energyLevelPercentage" + }, + "#8e44ad" + ] + ], + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "us8fv5jysv", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 0, + "id": "us8fv5jysv", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "Set point", + "widgetConfig": { + "stacked": false, + "showLegend": true, + "showZoomBar": false, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerSetpoint" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1771324703690, + "fromTimestamp": 1771321080000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerSetpoint" + }, + "#8e44ad" + ] + ], + "attributeConfig": { + "steppedAttributes": [ + { + "id": "setId1", + "name": "powerSetpoint" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "gpu0lthopd", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 3, + "id": "gpu0lthopd", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "Set point - average", + "widgetConfig": { + "stacked": false, + "decimals": 2, + "showLegend": true, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerSetpoint" + } + ], + "datapointQuery": { + "type": "interval", + "toTimestamp": 1771369199999, + "fromTimestamp": 1771196400000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerSetpoint" + }, + "#8e44ad" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setId1", + "name": "powerSetpoint" + } + ] + }, + "defaultTimePrefixKey": "this", + "defaultTimeWindowKey": "Day", + "showTimestampControls": false + }, + "widgetTypeId": "barchart" + } + ], + "screenPresets": [ + { + "id": "mobile", + "breakpoint": 640, + "displayName": "dashboard.size.mobile", + "scalingPreset": "WRAP_TO_SINGLE_COLUMN" + } + ], + "maxScreenWidth": 4000, + "refreshInterval": "OFF" + } +} diff --git a/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json b/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json new file mode 100644 index 0000000..d10d60f --- /dev/null +++ b/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json @@ -0,0 +1,443 @@ +{ + "createdOn": 1711029229921, + "realm": "master", + "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "access": "1", + "displayName": "New Dashboard", + "template": { + "id": "i8hvn0yp3f", + "columns": 12, + "widgets": [ + { + "id": "saq3gkbk5a", + "gridItem": { + "h": 4, + "w": 12, + "x": 0, + "y": 7, + "id": "saq3gkbk5a", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "EMS overview", + "widgetConfig": { + "stacked": false, + "showLegend": true, + "showZoomBar": false, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerConsumption" + }, + { + "id": "setId1", + "name": "powerFlexible" + }, + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setId1", + "name": "powerNet" + }, + { + "id": "setId1", + "name": "powerProduction" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1770115622809, + "fromTimestamp": 1770112020000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerConsumption" + }, + "#c0392b" + ], + [ + { + "id": "setId1", + "name": "powerFlexible" + }, + "#8e44ad" + ], + [ + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setId1", + "name": "powerProduction" + }, + "#f1c40f" + ] + ], + "attributeConfig": { + "areaAttributes": [ + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + } + ], + "steppedAttributes": [ + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setId1", + "name": "powerFlexible" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "1mysuavjjk", + "gridItem": { + "h": 4, + "w": 12, + "x": 0, + "y": 0, + "id": "1mysuavjjk", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "Power net", + "widgetConfig": { + "stacked": false, + "showLegend": true, + "showZoomBar": false, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setId1", + "name": "powerNet" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1770118174267, + "fromTimestamp": 1770114540000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ] + ], + "attributeConfig": { + "areaAttributes": [ + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + } + ], + "steppedAttributes": [ + { + "id": "setId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setId1", + "name": "powerLimitMaximumProfileTotal" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "ca7n1welwi", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 11, + "id": "ca7n1welwi", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "EMS overview - average", + "widgetConfig": { + "stacked": false, + "decimals": 2, + "showLegend": true, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerConsumption" + }, + { + "id": "setId1", + "name": "powerFlexible" + }, + { + "id": "setId1", + "name": "powerNet" + }, + { + "id": "setId1", + "name": "powerProduction" + } + ], + "datapointQuery": { + "type": "interval", + "toTimestamp": 1771541999999, + "fromTimestamp": 1771369200000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerConsumption" + }, + "#c0392b" + ], + [ + { + "id": "setId1", + "name": "powerFlexible" + }, + "#8e44ad" + ], + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setId1", + "name": "powerProduction" + }, + "#f1c40f" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setId1", + "name": "powerConsumption" + }, + { + "id": "setId1", + "name": "powerFlexible" + }, + { + "id": "setId1", + "name": "powerNet" + }, + { + "id": "setId1", + "name": "powerProduction" + } + ] + }, + "defaultTimePrefixKey": "this", + "defaultTimeWindowKey": "Day", + "showTimestampControls": false + }, + "widgetTypeId": "barchart" + }, + { + "id": "rtz4x7ikyhj", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 4, + "id": "rtz4x7ikyhj", + "minH": 2, + "minW": 2, + "locked": false, + "noMove": false, + "noResize": false, + "minPixelH": 0, + "minPixelW": 0 + }, + "displayName": "Power net - average", + "widgetConfig": { + "stacked": false, + "decimals": 2, + "showLegend": true, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "setId1", + "name": "powerNet" + } + ], + "datapointQuery": { + "type": "interval", + "toTimestamp": 1771541999999, + "fromTimestamp": 1771369200000 + }, + "attributeColors": [ + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setId1", + "name": "powerNet" + }, + "#392BC0" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setId1", + "name": "powerNet" + } + ] + }, + "defaultTimePrefixKey": "this", + "defaultTimeWindowKey": "Day", + "showTimestampControls": false + }, + "widgetTypeId": "barchart" + } + ], + "screenPresets": [ + { + "id": "mobile", + "breakpoint": 640, + "displayName": "dashboard.size.mobile", + "scalingPreset": "WRAP_TO_SINGLE_COLUMN" + } + ], + "maxScreenWidth": 4000, + "refreshInterval": "OFF" + } +} diff --git a/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy b/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy index 635b09c..9da8fee 100644 --- a/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy +++ b/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy @@ -1,7 +1,7 @@ package ems.rules -import org.openremote.extension.ems.agent.EmsEnergyOptimisationAsset import org.openremote.container.persistence.PersistenceService +import org.openremote.extension.ems.agent.EmsEnergyOptimisationAsset import org.openremote.manager.rules.RulesBuilder import org.openremote.model.query.AssetQuery import org.openremote.model.rules.Assets @@ -12,6 +12,7 @@ import java.sql.* import java.text.SimpleDateFormat import java.time.* import java.util.Date +import java.util.concurrent.ThreadLocalRandom import java.util.logging.Logger import static java.lang.Math.PI @@ -30,6 +31,8 @@ String energyOptimisationAssetId = "setId1" // Set values: String powerConsumptionConstantStr = "setValue1" String powerConsumptionProfileNumber = "setValue2" +String powerFluctuationsStr = "setValue3" + boolean variableTariffs = false // Set tariff prices: @@ -42,6 +45,31 @@ List tariffPricesImport = [ 158.07, 133.16, 127.00, 124.96, 129.89, 126.20, 118.28, 115.00, 118.03, 116.65, 115.20, 109.62, 114.01, 112.65, 104.37, 102.97 ] +//List tariffPricesImport = [100.0, 100.0, 100.0, 100.0, // 0 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, // 4 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, // 8 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, // 12 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, // 16 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, // 20 +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0, +// 100.0, 100.0, 100.0, 100.0] + // Set the link forecast ["inputAssetId", "inputAttributeName", "outputAssetId", "outputAttributeName", "+/-"] here: def attributes = [ ["linkInputAssetId1", "linkInputAttributeName1", "linkOutputAssetId1", "linkOutputAttributeName1", "+"] @@ -57,8 +85,8 @@ def inputAttributes = [ // Set the sum forecasts ["outputAssetName", "outputAttributeName" , "outputAssetId"] here: def outputAttribute = ["sumOutputAssetName", "sumOutputAttributeName", "sumOutputAssetId"] -// Set forecast period in minutes here: -long forecastPeriodMinutes = 7 * 24 * 60 +// Set forecast period in days here: +long forecastPeriodDays = 7 // Set desired forecast interval here: long forecastIntervalMinutes = 15 @@ -67,14 +95,14 @@ long forecastIntervalMinutes = 15 Double powerConsumptionConstant = powerConsumptionConstantStr.toDouble() +double powerFluctuation = powerFluctuationsStr.toDouble() SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") // Time triggers for rules long rulesStartTimeMillis = System.currentTimeMillis() long previousTimeMillisRule1 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000) -// TODO: change to trigger on forecast update when forecast update timestamp attribute is added -long previousTimeMillisRule3 = rulesStartTimeMillis - rulesStartTimeMillis % (5 * 60 * 1000) + (6 * 60 * 1000) -long previousTimeMillisRule4 = rulesStartTimeMillis - rulesStartTimeMillis % (5 * 60 * 1000) + (6 * 60 * 1000 + 15 * 1000) +long previousTimeMillisRule3 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000) +long previousTimeMillisRule4 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000 + 15 * 60) long timestampMillisPrevious = rulesStartTimeMillis // Date triggers for rules @@ -88,16 +116,15 @@ rules.add() .priority(1) .name("Simulate power data-points rule") .when({ facts -> - boolean triggerRule = false - long currentMillis = facts.clock.currentTimeMillis + long currentTimeMillis = facts.clock.currentTimeMillis - // Trigger rule every 1 minute = 60000 ms - if (currentMillis > previousTimeMillisRule1) { - previousTimeMillisRule1 += 60000 - triggerRule = true + // Trigger rule every 1 minutes + if (currentTimeMillis > previousTimeMillisRule1) { + previousTimeMillisRule1 += (1 * 60 * 1000) + return true } - return triggerRule + return false }) .then({ facts -> // Get power set-point for each battery @@ -141,7 +168,7 @@ rules.add() energyLevelNew = (Math.round(energyLevelPercentage * energyCapacity * 10)) / 1000 } else { def timeIntervalMillis = timestampMillisCurrent - timestampMillisPrevious - double energyChange = powerSetpoint * (timeIntervalMillis / 3600000) + double energyChange = powerSetpoint * (timeIntervalMillis / 3600000.0) energyLevelNew = (Math.round((energyLevel + energyChange) * 1000.0)) / 1000 } @@ -188,7 +215,17 @@ rules.add() powerConsumption = powerConsumptionConstant } - def powerFlexible = (powerSetpointBatteries.collect { it.value.orElse(0.0) }.sum() ?: 0.0) as Double + // Add random fluctuations to power consumption + double powerNoise = 0.0 + + if (powerFluctuation > 0) { + double powerFluctuationHalf = Math.round(powerFluctuation / 2 * 1000.0) / 1000.0 + powerNoise = ThreadLocalRandom.current().nextDouble(-powerFluctuationHalf, powerFluctuationHalf) + } + + powerConsumption = powerConsumption + powerNoise + + def powerFlexible = (powerSetpointBatteries.collect { it.value.orElse(0.0) }.sum() ?: 0.0) as double def powerNet = powerConsumption + powerFlexible // Update power attributes @@ -198,9 +235,9 @@ rules.add() } } - assets.dispatch(energyOptimisationAssetId, "powerConsumption", powerConsumption) - assets.dispatch(energyOptimisationAssetId, "powerFlexible", powerFlexible) - assets.dispatch(energyOptimisationAssetId, "powerNet", powerNet) + assets.dispatch(energyOptimisationAssetId, "powerConsumption", Math.round((double) (powerConsumption * 1000.0)) / 1000.0) + assets.dispatch(energyOptimisationAssetId, "powerFlexible", Math.round((double) (powerFlexible * 1000.0)) / 1000.0) + assets.dispatch(energyOptimisationAssetId, "powerNet", Math.round((double) (powerNet * 1000.0)) / 1000.0) }) rules.add() @@ -233,11 +270,11 @@ rules.add() TreeMap datePowerConsumptionMapForecast = new TreeMap<>() - // Create forecast data-points for yesterday, today and 2 days into the future - for (int i = -1; i <= 2; i++) { + // Create forecast data-points + for (int i = -1; i <= forecastPeriodDays; i++) { for (int j = 0; j < tariffPricesImport.size(); j++) { def startOfDay = dateCurrent.atStartOfDay(zoneId).plusDays(i) as ZonedDateTime - long timeMillis = startOfDay.toInstant().toEpochMilli() + j * intervalMillis - 10 * intervalMillis + long timeMillis = startOfDay.toInstant().toEpochMilli() + j * intervalMillis //- 10 * intervalMillis def key = sdf.format(new Date(timeMillis)) as String double factor = 1.0 @@ -273,9 +310,9 @@ rules.add() .when({ facts -> long currentTimeMillis = facts.clock.currentTimeMillis - // Trigger rule every 5 minutes + // Trigger rule every 1 minutes if (currentTimeMillis > previousTimeMillisRule3) { - previousTimeMillisRule3 += (5 * 60 * 1000) + previousTimeMillisRule3 += (1 * 60 * 1000) return true } @@ -285,8 +322,8 @@ rules.add() long currentTimeMillis = facts.clock.currentTimeMillis // Forecast period - long startTimeMillis = currentTimeMillis - long endTimeMillis = currentTimeMillis + forecastPeriodMinutes * 60 * 1000 + long startTimeMillis = currentTimeMillis - currentTimeMillis % (forecastIntervalMinutes * 60 * 1000) + long endTimeMillis = currentTimeMillis + forecastPeriodDays * 24 * 60 * 60 * 1000 // Convert time in milliseconds to a string timestamp String startDateTimeStr = sdf.format(new Date(startTimeMillis)) @@ -325,9 +362,9 @@ rules.add() .when({ facts -> long currentTimeMillis = facts.clock.currentTimeMillis - // Trigger rule every 5 minutes + // Trigger rule every 1 minutes if (currentTimeMillis > previousTimeMillisRule4) { - previousTimeMillisRule4 += (5 * 60 * 1000) + previousTimeMillisRule4 += (1 * 60 * 1000) return true } @@ -340,15 +377,17 @@ rules.add() long intervalMillis = forecastIntervalMinutes * 60 * 1000 // Forecast period in milliseconds - long periodMillis = forecastPeriodMinutes * 60 * 1000 + long periodMillis = forecastPeriodDays * 24 * 60 * 60 * 1000 long startTimeMillis = currentTimeMillis - intervalMillis long endTimeMillis = currentTimeMillis + intervalMillis + periodMillis - sumForecasts(inputAttributes, outputAttribute, currentTimeMillis, intervalMillis, startTimeMillis, endTimeMillis) + boolean updateOnlyFutureDatapoints = false + + sumForecasts(inputAttributes, outputAttribute, currentTimeMillis, intervalMillis, startTimeMillis, endTimeMillis, updateOnlyFutureDatapoints) }) -private void sumForecasts(List> inputAttributes, List outputAttribute, long currentTimeMillis, long intervalMillis, long startTimeMillis, long endTimeMillis) { +private void sumForecasts(List> inputAttributes, List outputAttribute, long currentTimeMillis, long intervalMillis, long startTimeMillis, long endTimeMillis, boolean updateOnlyFutureDatapoints) { // Map with all the input forecasts for summation Map> interpolatedForecastsMap = new HashMap() @@ -430,8 +469,7 @@ private void sumForecasts(List> inputAttributes, List outpu for (String dateTime : dateTimeListGeneral) { long timeMillis = sdf.parse(dateTime).getTime() - // Update only future data points - if (timeMillis > currentTimeMillis) { + if (!updateOnlyFutureDatapoints || (updateOnlyFutureDatapoints && timeMillis > currentTimeMillis)) { List forecastValueList = new ArrayList() // Find all values per date-time From 036ec68cece84ad3423d676ee885bbda97443c86 Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Mon, 23 Mar 2026 12:05:50 +0100 Subject: [PATCH 2/3] Improvements to dashboard creation and EMS demo rules --- .../manager/EmsOptimisationSetupService.java | 26 +++- .../ems/dashboards/EmsBatteryDashboard.json | 38 ++--- .../ems/dashboards/EmsOverviewDashboard.json | 134 ++++++++---------- .../ems/rules/EmsDemoSimulationRules.groovy | 14 +- 4 files changed, 101 insertions(+), 111 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java index 8d0e176..1a3fb94 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; import org.openremote.container.persistence.PersistenceService; +import org.openremote.container.timer.TimerService; import org.openremote.extension.ems.agent.*; import org.openremote.manager.app.ConfigurationService; import org.openremote.manager.asset.AssetProcessingService; @@ -50,10 +51,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -71,7 +69,7 @@ public class EmsOptimisationSetupService implements ContainerService { protected DashboardStorageService dashboardStorageService; protected PersistenceService persistenceService; protected ScheduledExecutorService scheduledExecutorService; - + protected TimerService timerService; @Override public void init(Container container) throws Exception { @@ -82,6 +80,7 @@ public void init(Container container) throws Exception { dashboardStorageService = container.getService(DashboardStorageService.class); persistenceService = container.getService(PersistenceService.class); scheduledExecutorService = container.getScheduledExecutor(); + timerService = container.getService(TimerService.class); } @Override @@ -328,6 +327,7 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu LOG.warning(String.format("assetName='%s', assetId='%s'; Rules were not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); } + Date currentDate = Date.from(timerService.getNow()); // Create main dashboard try (InputStream inputStream = EmsOptimisationService.class.getResourceAsStream("/ems/dashboards/EmsOverviewDashboard.json")) { @@ -337,11 +337,17 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu String dashboardStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8); dashboardStr = dashboardStr - .replace("setId1", energyOptimisationAssetId) + .replace("setAssetId1", energyOptimisationAssetId) + .replace("setTemplateId1", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId1", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId2", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId3", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId4", UniqueIdentifierGenerator.generateId()) ; Dashboard dashboard = ValueUtil.JSON.readValue(dashboardStr, Dashboard.class); + dashboard.setCreatedOn(currentDate); dashboard.setRealm(realmName); dashboard.setDisplayName(dashboardName); @@ -359,10 +365,16 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu String dashboardStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8); dashboardStr = dashboardStr - .replace("setId1", electricityBatteryAssetId); + .replace("setAssetId1", electricityBatteryAssetId) + .replace("setTemplateId1", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId1", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId2", UniqueIdentifierGenerator.generateId()) + .replace("setWidgetId3", UniqueIdentifierGenerator.generateId()) + ; Dashboard dashboard = ValueUtil.JSON.readValue(dashboardStr, Dashboard.class); + dashboard.setCreatedOn(currentDate); dashboard.setRealm(realmName); dashboard.setDisplayName(dashboardName); diff --git a/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json b/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json index 7137760..679523a 100644 --- a/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json +++ b/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json @@ -1,21 +1,21 @@ { - "createdOn": 1711029229921, - "realm": "master", - "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "createdOn": 0, + "realm": "", + "ownerId": "", "access": "1", - "displayName": "New Dashboard", + "displayName": "", "template": { - "id": "k674dhoxyc", + "id": "setTemplateId1", "columns": 12, "widgets": [ { - "id": "cqtsw15lro", + "id": "setWidgetId1", "gridItem": { "h": 3, "w": 12, "x": 0, "y": 6, - "id": "cqtsw15lro", + "id": "setWidgetId1", "minH": 2, "minW": 2, "locked": false, @@ -39,7 +39,7 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "energyLevelPercentage" } ], @@ -51,7 +51,7 @@ "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "energyLevelPercentage" }, "#8e44ad" @@ -63,13 +63,13 @@ "widgetTypeId": "linechart" }, { - "id": "us8fv5jysv", + "id": "setWidgetId2", "gridItem": { "h": 3, "w": 12, "x": 0, "y": 0, - "id": "us8fv5jysv", + "id": "setWidgetId2", "minH": 2, "minW": 2, "locked": false, @@ -93,7 +93,7 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" } ], @@ -105,7 +105,7 @@ "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" }, "#8e44ad" @@ -114,7 +114,7 @@ "attributeConfig": { "steppedAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" } ] @@ -125,13 +125,13 @@ "widgetTypeId": "linechart" }, { - "id": "gpu0lthopd", + "id": "setWidgetId3", "gridItem": { "h": 3, "w": 12, "x": 0, "y": 3, - "id": "gpu0lthopd", + "id": "setWidgetId3", "minH": 2, "minW": 2, "locked": false, @@ -155,7 +155,7 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" } ], @@ -167,7 +167,7 @@ "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" }, "#8e44ad" @@ -177,7 +177,7 @@ "attributeSettings": { "methodAvgAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerSetpoint" } ] diff --git a/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json b/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json index d10d60f..488729a 100644 --- a/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json +++ b/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json @@ -1,21 +1,21 @@ { - "createdOn": 1711029229921, - "realm": "master", - "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "createdOn": 0, + "realm": "", + "ownerId": "", "access": "1", - "displayName": "New Dashboard", + "displayName": "", "template": { - "id": "i8hvn0yp3f", + "id": "setTemplateId1", "columns": 12, "widgets": [ { - "id": "saq3gkbk5a", + "id": "setWidgetId1", "gridItem": { "h": 4, "w": 12, "x": 0, "y": 7, - "id": "saq3gkbk5a", + "id": "setWidgetId1", "minH": 2, "minW": 2, "locked": false, @@ -39,74 +39,69 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerConsumption" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerProduction" } ], - "datapointQuery": { - "type": "lttb", - "toTimestamp": 1770115622809, - "fromTimestamp": 1770112020000 - }, "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerConsumption" }, "#c0392b" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" }, "#8e44ad" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, "#7f8c8d" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, "#7f8c8d" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerProduction" }, "#f1c40f" @@ -115,25 +110,25 @@ "attributeConfig": { "areaAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" } ], "steppedAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" } ] @@ -144,13 +139,13 @@ "widgetTypeId": "linechart" }, { - "id": "1mysuavjjk", + "id": "setWidgetId2", "gridItem": { "h": 4, "w": 12, "x": 0, "y": 0, - "id": "1mysuavjjk", + "id": "setWidgetId2", "minH": 2, "minW": 2, "locked": false, @@ -174,41 +169,36 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" } ], - "datapointQuery": { - "type": "lttb", - "toTimestamp": 1770118174267, - "fromTimestamp": 1770114540000 - }, "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, "#7f8c8d" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, "#7f8c8d" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" @@ -217,21 +207,21 @@ "attributeConfig": { "areaAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" } ], "steppedAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMinimumProfileTotal" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerLimitMaximumProfileTotal" } ] @@ -242,13 +232,13 @@ "widgetTypeId": "linechart" }, { - "id": "ca7n1welwi", + "id": "setWidgetId3", "gridItem": { "h": 3, "w": 12, "x": 0, "y": 11, - "id": "ca7n1welwi", + "id": "setWidgetId3", "minH": 2, "minW": 2, "locked": false, @@ -272,52 +262,47 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerConsumption" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerProduction" } ], - "datapointQuery": { - "type": "interval", - "toTimestamp": 1771541999999, - "fromTimestamp": 1771369200000 - }, "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerConsumption" }, "#c0392b" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" }, "#8e44ad" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerProduction" }, "#f1c40f" @@ -327,19 +312,19 @@ "attributeSettings": { "methodAvgAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerConsumption" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerFlexible" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, { - "id": "setId1", + "id": "setAssetId1", "name": "powerProduction" } ] @@ -351,13 +336,13 @@ "widgetTypeId": "barchart" }, { - "id": "rtz4x7ikyhj", + "id": "setWidgetId4", "gridItem": { "h": 3, "w": 12, "x": 0, "y": 4, - "id": "rtz4x7ikyhj", + "id": "setWidgetId4", "minH": 2, "minW": 2, "locked": false, @@ -381,33 +366,28 @@ }, "attributeRefs": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" } ], - "datapointQuery": { - "type": "interval", - "toTimestamp": 1771541999999, - "fromTimestamp": 1771369200000 - }, "attributeColors": [ [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" ], [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" }, "#392BC0" @@ -417,7 +397,7 @@ "attributeSettings": { "methodAvgAttributes": [ { - "id": "setId1", + "id": "setAssetId1", "name": "powerNet" } ] diff --git a/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy b/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy index 9da8fee..699964c 100644 --- a/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy +++ b/ems/src/main/resources/ems/rules/EmsDemoSimulationRules.groovy @@ -102,7 +102,7 @@ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") long rulesStartTimeMillis = System.currentTimeMillis() long previousTimeMillisRule1 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000) long previousTimeMillisRule3 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000) -long previousTimeMillisRule4 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000 + 15 * 60) +long previousTimeMillisRule4 = rulesStartTimeMillis - rulesStartTimeMillis % (1 * 60 * 1000) + (1 * 60 * 1000 + 15 * 1000) long timestampMillisPrevious = rulesStartTimeMillis // Date triggers for rules @@ -219,8 +219,8 @@ rules.add() double powerNoise = 0.0 if (powerFluctuation > 0) { - double powerFluctuationHalf = Math.round(powerFluctuation / 2 * 1000.0) / 1000.0 - powerNoise = ThreadLocalRandom.current().nextDouble(-powerFluctuationHalf, powerFluctuationHalf) + double powerFluctuationHalf = powerFluctuation / 2 + powerNoise = Math.round(ThreadLocalRandom.current().nextDouble(-powerFluctuationHalf, powerFluctuationHalf) * 1000.0) / 1000.0 } powerConsumption = powerConsumption + powerNoise @@ -323,7 +323,7 @@ rules.add() // Forecast period long startTimeMillis = currentTimeMillis - currentTimeMillis % (forecastIntervalMinutes * 60 * 1000) - long endTimeMillis = currentTimeMillis + forecastPeriodDays * 24 * 60 * 60 * 1000 + long endTimeMillis = startTimeMillis - startTimeMillis % (24 * 60 * 60000) + ((forecastPeriodDays + 1) * 24 * 60 * 60000) // Convert time in milliseconds to a string timestamp String startDateTimeStr = sdf.format(new Date(startTimeMillis)) @@ -376,11 +376,9 @@ rules.add() // Interpolation interval in milliseconds long intervalMillis = forecastIntervalMinutes * 60 * 1000 - // Forecast period in milliseconds - long periodMillis = forecastPeriodDays * 24 * 60 * 60 * 1000 - + // Forecast period long startTimeMillis = currentTimeMillis - intervalMillis - long endTimeMillis = currentTimeMillis + intervalMillis + periodMillis + long endTimeMillis = startTimeMillis - startTimeMillis % (24 * 60 * 60000) + ((forecastPeriodDays + 1) * 24 * 60 * 60000) boolean updateOnlyFutureDatapoints = false From 069066a455ab81eb1fad014bd8a369559dce4a97 Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Mon, 23 Mar 2026 13:30:57 +0100 Subject: [PATCH 3/3] Change currentDate from Date to Instant --- .../extension/ems/manager/EmsOptimisationSetupService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java index 1a3fb94..d1bf9c2 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationSetupService.java @@ -49,6 +49,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @@ -327,7 +328,7 @@ private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setu LOG.warning(String.format("assetName='%s', assetId='%s'; Rules were not created for energy management system '%s'; Exception: %s", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName, e)); } - Date currentDate = Date.from(timerService.getNow()); + Instant currentDate = timerService.getNow(); // Create main dashboard try (InputStream inputStream = EmsOptimisationService.class.getResourceAsStream("/ems/dashboards/EmsOverviewDashboard.json")) {