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..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 @@ -22,30 +22,37 @@ 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.container.timer.TimerService; +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; +import java.time.Instant; 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; @@ -60,9 +67,10 @@ public class EmsOptimisationSetupService implements ContainerService { protected AssetStorageService assetStorageService; protected ClientEventService clientEventService; private ConfigurationService configurationService; + protected DashboardStorageService dashboardStorageService; protected PersistenceService persistenceService; protected ScheduledExecutorService scheduledExecutorService; - + protected TimerService timerService; @Override public void init(Container container) throws Exception { @@ -70,8 +78,10 @@ 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(); + timerService = container.getService(TimerService.class); } @Override @@ -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,67 @@ 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)); + } + + Instant currentDate = timerService.getNow(); + + // 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("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); + + 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("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); + + 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..679523a --- /dev/null +++ b/ems/src/main/resources/ems/dashboards/EmsBatteryDashboard.json @@ -0,0 +1,203 @@ +{ + "createdOn": 0, + "realm": "", + "ownerId": "", + "access": "1", + "displayName": "", + "template": { + "id": "setTemplateId1", + "columns": 12, + "widgets": [ + { + "id": "setWidgetId1", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 6, + "id": "setWidgetId1", + "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": "setAssetId1", + "name": "energyLevelPercentage" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1771324682600, + "fromTimestamp": 1771321080000 + }, + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "energyLevelPercentage" + }, + "#8e44ad" + ] + ], + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "setWidgetId2", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 0, + "id": "setWidgetId2", + "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": "setAssetId1", + "name": "powerSetpoint" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1771324703690, + "fromTimestamp": 1771321080000 + }, + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerSetpoint" + }, + "#8e44ad" + ] + ], + "attributeConfig": { + "steppedAttributes": [ + { + "id": "setAssetId1", + "name": "powerSetpoint" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "setWidgetId3", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 3, + "id": "setWidgetId3", + "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": "setAssetId1", + "name": "powerSetpoint" + } + ], + "datapointQuery": { + "type": "interval", + "toTimestamp": 1771369199999, + "fromTimestamp": 1771196400000 + }, + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerSetpoint" + }, + "#8e44ad" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setAssetId1", + "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..488729a --- /dev/null +++ b/ems/src/main/resources/ems/dashboards/EmsOverviewDashboard.json @@ -0,0 +1,423 @@ +{ + "createdOn": 0, + "realm": "", + "ownerId": "", + "access": "1", + "displayName": "", + "template": { + "id": "setTemplateId1", + "columns": 12, + "widgets": [ + { + "id": "setWidgetId1", + "gridItem": { + "h": 4, + "w": 12, + "x": 0, + "y": 7, + "id": "setWidgetId1", + "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": "setAssetId1", + "name": "powerConsumption" + }, + { + "id": "setAssetId1", + "name": "powerFlexible" + }, + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerNet" + }, + { + "id": "setAssetId1", + "name": "powerProduction" + } + ], + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerConsumption" + }, + "#c0392b" + ], + [ + { + "id": "setAssetId1", + "name": "powerFlexible" + }, + "#8e44ad" + ], + [ + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setAssetId1", + "name": "powerProduction" + }, + "#f1c40f" + ] + ], + "attributeConfig": { + "areaAttributes": [ + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + } + ], + "steppedAttributes": [ + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerFlexible" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "setWidgetId2", + "gridItem": { + "h": 4, + "w": 12, + "x": 0, + "y": 0, + "id": "setWidgetId2", + "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": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerNet" + } + ], + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + "#7f8c8d" + ], + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ] + ], + "attributeConfig": { + "areaAttributes": [ + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + } + ], + "steppedAttributes": [ + { + "id": "setAssetId1", + "name": "powerLimitMinimumProfileTotal" + }, + { + "id": "setAssetId1", + "name": "powerLimitMaximumProfileTotal" + } + ] + }, + "defaultTimePresetKey": "thisDay", + "showTimestampControls": false + }, + "widgetTypeId": "linechart" + }, + { + "id": "setWidgetId3", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 11, + "id": "setWidgetId3", + "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": "setAssetId1", + "name": "powerConsumption" + }, + { + "id": "setAssetId1", + "name": "powerFlexible" + }, + { + "id": "setAssetId1", + "name": "powerNet" + }, + { + "id": "setAssetId1", + "name": "powerProduction" + } + ], + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerConsumption" + }, + "#c0392b" + ], + [ + { + "id": "setAssetId1", + "name": "powerFlexible" + }, + "#8e44ad" + ], + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setAssetId1", + "name": "powerProduction" + }, + "#f1c40f" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setAssetId1", + "name": "powerConsumption" + }, + { + "id": "setAssetId1", + "name": "powerFlexible" + }, + { + "id": "setAssetId1", + "name": "powerNet" + }, + { + "id": "setAssetId1", + "name": "powerProduction" + } + ] + }, + "defaultTimePrefixKey": "this", + "defaultTimeWindowKey": "Day", + "showTimestampControls": false + }, + "widgetTypeId": "barchart" + }, + { + "id": "setWidgetId4", + "gridItem": { + "h": 3, + "w": 12, + "x": 0, + "y": 4, + "id": "setWidgetId4", + "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": "setAssetId1", + "name": "powerNet" + } + ], + "attributeColors": [ + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ], + [ + { + "id": "setAssetId1", + "name": "powerNet" + }, + "#392BC0" + ] + ], + "defaultInterval": "15Minutes", + "attributeSettings": { + "methodAvgAttributes": [ + { + "id": "setAssetId1", + "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..699964c 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 * 1000) 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 = powerFluctuation / 2 + powerNoise = Math.round(ThreadLocalRandom.current().nextDouble(-powerFluctuationHalf, powerFluctuationHalf) * 1000.0) / 1000.0 + } + + 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 = 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)) @@ -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 } @@ -339,16 +376,16 @@ rules.add() // Interpolation interval in milliseconds long intervalMillis = forecastIntervalMinutes * 60 * 1000 - // Forecast period in milliseconds - long periodMillis = forecastPeriodMinutes * 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 - sumForecasts(inputAttributes, outputAttribute, currentTimeMillis, intervalMillis, startTimeMillis, endTimeMillis) + 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 +467,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