diff --git a/build.gradle b/build.gradle index cbe69f0..0de7845 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,17 @@ scmVersion { } } +buildscript { + repositories { + maven { + url = 'https://plugins.gradle.org/m2' + } + } + dependencies { + classpath 'com.diffplug.spotless:spotless-plugin-gradle:8.8.0' + } +} + allprojects { // Apply common project setup apply from: "${project.rootDir}/project.gradle" diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java index 633046a..7e11930 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java @@ -1,9 +1,6 @@ /* * Copyright 2017, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,38 +12,37 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import java.util.Arrays; +import java.util.List; + import org.openremote.model.Container; import org.openremote.model.setup.Setup; import org.openremote.model.setup.SetupTasks; -import java.util.Arrays; -import java.util.List; - -/** - * Demo setup tasks. - */ +/** Demo setup tasks. */ public class DemoSetupTasks implements SetupTasks { - public static final String DEMO_SETUP_TYPE = "demo"; + public static final String DEMO_SETUP_TYPE = "demo"; - @Override - public List createTasks(Container container, String setupType, boolean keycloakEnabled) { + @Override + public List createTasks(Container container, String setupType, boolean keycloakEnabled) { - if (DEMO_SETUP_TYPE.equals(setupType)) { - return Arrays.asList( - new KeycloakDemoSetup(container), - new ManagerDemoSetup(container), - new RulesDemoSetup(container), - new ManagerDemoAgentSetup(container), - new ManagerDemoDashboardSetup(container) - ); - } - - // Do nothing otherwise - return null; + if (DEMO_SETUP_TYPE.equals(setupType)) { + return Arrays.asList( + new KeycloakDemoSetup(container), + new ManagerDemoSetup(container), + new RulesDemoSetup(container), + new ManagerDemoAgentSetup(container), + new ManagerDemoDashboardSetup(container)); } + + // Do nothing otherwise + return null; + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java index 2566a8c..aa23af7 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,15 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import java.util.Arrays; +import java.util.logging.Logger; + import org.openremote.manager.setup.AbstractKeycloakSetup; import org.openremote.model.Constants; import org.openremote.model.Container; @@ -26,59 +28,94 @@ import org.openremote.model.security.Realm; import org.openremote.model.security.User; -import java.util.Arrays; -import java.util.logging.Logger; - /** * We have the following demo users: - *
    - *
  • admin - The superuser in the "master" realm with all access
  • - *
  • smartcity - (Password: smartcity) A user in the "smartcity" realm with read access
  • - *
  • manufacturer - (Password: manufacturer) A user in the "manufacturer" realm with read access
  • - *
  • manufacturer - customer - (Password: customer) A user in the "manufacturer" realm with restricted access to his assets
  • * + *
      + *
    • admin - The superuser in the "master" realm with all access + *
    • smartcity - (Password: smartcity) A user in the "smartcity" realm with read + * access + *
    • manufacturer - (Password: manufacturer) A user in the "manufacturer" realm + * with read access + *
    • manufacturer - customer - (Password: customer) A user in the "manufacturer" + * realm with restricted access to his assets *
    */ public class KeycloakDemoSetup extends AbstractKeycloakSetup { - private static final Logger LOG = Logger.getLogger(KeycloakDemoSetup.class.getName()); + private static final Logger LOG = Logger.getLogger(KeycloakDemoSetup.class.getName()); - public String smartCityUserId; - public static String manufacturerUserId; - public static String customerUserId; - public Realm realmMaster; - public Realm realmCity; - public static Realm realmManufacturer; + public String smartCityUserId; + public static String manufacturerUserId; + public static String customerUserId; + public Realm realmMaster; + public Realm realmCity; + public static Realm realmManufacturer; - public KeycloakDemoSetup(Container container) { - super(container); - } + public KeycloakDemoSetup(Container container) { + super(container); + } - @Override - public void onStart() throws Exception { - super.onStart(); + @Override + public void onStart() throws Exception { + super.onStart(); - // Realms - realmMaster = identityService.getIdentityProvider().getRealm(Constants.MASTER_REALM); - realmCity = createRealm("smartcity", "Smart City", true); - realmManufacturer = createRealm("manufacturer", "Manufacturer", true); - removeManageAccount("smartcity"); - removeManageAccount("manufacturer"); + // Realms + realmMaster = identityService.getIdentityProvider().getRealm(Constants.MASTER_REALM); + realmCity = createRealm("smartcity", "Smart City", true); + realmManufacturer = createRealm("manufacturer", "Manufacturer", true); + removeManageAccount("smartcity"); + removeManageAccount("manufacturer"); - // Don't allow demo users to write assets - ClientRole[] demoUserRoles = Arrays.stream(AbstractKeycloakSetup.REGULAR_USER_ROLES) + // Don't allow demo users to write assets + ClientRole[] demoUserRoles = + Arrays.stream(AbstractKeycloakSetup.REGULAR_USER_ROLES) .filter(clientRole -> clientRole != ClientRole.WRITE_ASSETS) .toArray(ClientRole[]::new); - // Users - User smartCityUser = createUser(realmCity.getName(), "smartcity", "smartcity", "Smart", "City", null, true, demoUserRoles); - this.smartCityUserId = smartCityUser.getId(); - keycloakProvider.updateUserClientRoles(realmCity.getName(), smartCityUserId, "account"); // Remove all roles for account client - User manufacturerUser = createUser(realmManufacturer.getName(), "manufacturer", "manufacturer", "Agri", "Tech", null, true, demoUserRoles); - manufacturerUserId = manufacturerUser.getId(); - keycloakProvider.updateUserClientRoles(realmManufacturer.getName(), manufacturerUserId, "account"); // Remove all roles for account client - User customerUser = createUser(realmManufacturer.getName(), "customer", "customer", "Bert", "Frederiks", null, true, demoUserRoles); - customerUserId = customerUser.getId(); - keycloakProvider.updateUserClientRoles(realmManufacturer.getName(), customerUserId, "account"); // Remove all roles for account client - } + // Users + User smartCityUser = + createUser( + realmCity.getName(), + "smartcity", + "smartcity", + "Smart", + "City", + null, + true, + demoUserRoles); + this.smartCityUserId = smartCityUser.getId(); + keycloakProvider.updateUserClientRoles( + realmCity.getName(), smartCityUserId, "account"); // Remove all roles for account client + User manufacturerUser = + createUser( + realmManufacturer.getName(), + "manufacturer", + "manufacturer", + "Agri", + "Tech", + null, + true, + demoUserRoles); + manufacturerUserId = manufacturerUser.getId(); + keycloakProvider.updateUserClientRoles( + realmManufacturer.getName(), + manufacturerUserId, + "account"); // Remove all roles for account client + User customerUser = + createUser( + realmManufacturer.getName(), + "customer", + "customer", + "Bert", + "Frederiks", + null, + true, + demoUserRoles); + customerUserId = customerUser.getId(); + keycloakProvider.updateUserClientRoles( + realmManufacturer.getName(), + customerUserId, + "account"); // Remove all roles for account client + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java index de20ca6..a701311 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,81 +12,98 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import java.util.logging.Logger; + import org.openremote.agent.protocol.knx.KNXAgent; import org.openremote.agent.protocol.velbus.VelbusTCPAgent; -import org.openremote.model.util.MapAccess; import org.openremote.manager.setup.ManagerSetup; import org.openremote.model.Container; import org.openremote.model.security.Realm; - -import java.util.logging.Logger; +import org.openremote.model.util.MapAccess; public class ManagerDemoAgentSetup extends ManagerSetup { - private static final Logger LOG = Logger.getLogger(ManagerDemoAgentSetup.class.getName()); - - public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX = "OR_SETUP_IMPORT_DEMO_AGENT_KNX"; - public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP = "OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP"; - public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP = "OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP"; - - public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS = "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS"; - public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST = "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST"; - public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT = "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT"; - - public String realmMasterName; - - final protected boolean knx; - final protected String knxGatewayIp; - final protected String knxLocalIp; - - final protected boolean velbus; - final protected String velbusHost; - final protected Integer velbusPort; - - public ManagerDemoAgentSetup(Container container) { - super(container); - - this.knx = MapAccess.getBoolean(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX, false); - this.knxGatewayIp = MapAccess.getString(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP, "localhost"); - this.knxLocalIp = MapAccess.getString(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP, "localhost"); - - this.velbus = MapAccess.getBoolean(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS, false); - this.velbusHost = MapAccess.getString(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST, "localhost"); - this.velbusPort = MapAccess.getInteger(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT, 6000); + private static final Logger LOG = Logger.getLogger(ManagerDemoAgentSetup.class.getName()); + + public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX = "OR_SETUP_IMPORT_DEMO_AGENT_KNX"; + public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP = + "OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP"; + public static final String OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP = + "OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP"; + + public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS = + "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS"; + public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST = + "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST"; + public static final String OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT = + "OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT"; + + public String realmMasterName; + + protected final boolean knx; + protected final String knxGatewayIp; + protected final String knxLocalIp; + + protected final boolean velbus; + protected final String velbusHost; + protected final Integer velbusPort; + + public ManagerDemoAgentSetup(Container container) { + super(container); + + this.knx = MapAccess.getBoolean(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX, false); + this.knxGatewayIp = + MapAccess.getString( + container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX_GATEWAY_IP, "localhost"); + this.knxLocalIp = + MapAccess.getString( + container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_KNX_LOCAL_IP, "localhost"); + + this.velbus = + MapAccess.getBoolean(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS, false); + this.velbusHost = + MapAccess.getString( + container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_HOST, "localhost"); + this.velbusPort = + MapAccess.getInteger(container.getConfig(), OR_SETUP_IMPORT_DEMO_AGENT_VELBUS_PORT, 6000); + } + + @Override + public void onStart() throws Exception { + super.onStart(); + + KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); + Realm realmMaster = keycloakDemoSetup.realmMaster; + realmMasterName = realmMaster.getName(); + + if (knx) { + LOG.info("Enable KNX demo agent, gateway/local IP: " + knxGatewayIp + "/" + knxLocalIp); + + KNXAgent agent = + new KNXAgent("Demo KNX agent") + .setRealm(realmMasterName) + .setHost(knxGatewayIp) + .setBindHost(knxLocalIp); + + agent = assetStorageService.merge(agent); } - @Override - public void onStart() throws Exception { - super.onStart(); - - KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); - Realm realmMaster = keycloakDemoSetup.realmMaster; - realmMasterName = realmMaster.getName(); - - if (knx) { - LOG.info("Enable KNX demo agent, gateway/local IP: " + knxGatewayIp + "/" + knxLocalIp); - - KNXAgent agent = new KNXAgent("Demo KNX agent") - .setRealm(realmMasterName) - .setHost(knxGatewayIp) - .setBindHost(knxLocalIp); - - agent = assetStorageService.merge(agent); - } - - if (velbus) { - LOG.info("Enable Velbus demo agent, host/port: " + velbusHost + "/" + velbusPort); + if (velbus) { + LOG.info("Enable Velbus demo agent, host/port: " + velbusHost + "/" + velbusPort); - VelbusTCPAgent agent = new VelbusTCPAgent("Demo VELBUS agent") - .setRealm(realmMasterName) - .setHost(velbusHost) - .setPort(velbusPort); + VelbusTCPAgent agent = + new VelbusTCPAgent("Demo VELBUS agent") + .setRealm(realmMasterName) + .setHost(velbusHost) + .setPort(velbusPort); - agent = assetStorageService.merge(agent); - } + agent = assetStorageService.merge(agent); } + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java index 91773da..948c9c1 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java @@ -1,9 +1,6 @@ /* * Copyright 2024, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,42 +12,47 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import java.io.InputStream; + import org.openremote.manager.dashboard.DashboardStorageService; import org.openremote.manager.setup.ManagerSetup; import org.openremote.model.Container; import org.openremote.model.dashboard.Dashboard; import org.openremote.model.util.ValueUtil; -import java.io.InputStream; - public class ManagerDemoDashboardSetup extends ManagerSetup { - protected final DashboardStorageService dashboardStorageService; - - public ManagerDemoDashboardSetup(Container container) { - super(container); - this.dashboardStorageService = container.getService(DashboardStorageService.class); - } + protected final DashboardStorageService dashboardStorageService; - @Override - public void onStart() throws Exception { - super.onStart(); + public ManagerDemoDashboardSetup(Container container) { + super(container); + this.dashboardStorageService = container.getService(DashboardStorageService.class); + } - // SmartCity - try (InputStream inputStream = ManagerDemoDashboardSetup.class.getResourceAsStream("/demo/dashboards/smartcity/parking.json")) { - Dashboard dashboard = ValueUtil.JSON.readValue(inputStream, Dashboard.class); - dashboardStorageService.createNew(dashboard); - } + @Override + public void onStart() throws Exception { + super.onStart(); - // Manufacturer - try (InputStream inputStream = ManagerDemoDashboardSetup.class.getResourceAsStream("/demo/dashboards/manufacturer/harvesting.json")) { - Dashboard dashboard = ValueUtil.JSON.readValue(inputStream, Dashboard.class); - dashboardStorageService.createNew(dashboard); - } + // SmartCity + try (InputStream inputStream = + ManagerDemoDashboardSetup.class.getResourceAsStream( + "/demo/dashboards/smartcity/parking.json")) { + Dashboard dashboard = ValueUtil.JSON.readValue(inputStream, Dashboard.class); + dashboardStorageService.createNew(dashboard); + } + // Manufacturer + try (InputStream inputStream = + ManagerDemoDashboardSetup.class.getResourceAsStream( + "/demo/dashboards/manufacturer/harvesting.json")) { + Dashboard dashboard = ValueUtil.JSON.readValue(inputStream, Dashboard.class); + dashboardStorageService.createNew(dashboard); } + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java index a395c04..7aa8f04 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java @@ -1,9 +1,6 @@ /* * Copyright 2016, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,27 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import static java.time.temporal.ChronoField.SECOND_OF_DAY; +import static org.openremote.model.Constants.*; +import static org.openremote.model.value.MetaItemType.*; +import static org.openremote.model.value.ValueType.MultivaluedStringMap; + +import java.time.Duration; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; + import org.openremote.agent.protocol.http.HTTPAgent; import org.openremote.agent.protocol.http.HTTPAgentLink; import org.openremote.agent.protocol.simulator.SimulatorAgent; @@ -35,10 +49,6 @@ import org.openremote.extension.energy.model.ElectricityProducerAsset; import org.openremote.extension.energy.model.ElectricityProducerSolarAsset; import org.openremote.extension.energy.model.ElectricityStorageAsset; -import org.openremote.model.attribute.AttributeMap; -import org.openremote.model.datapoint.ValueDatapoint; -import org.openremote.model.query.AssetQuery; -import org.openremote.model.util.UniqueIdentifierGenerator; import org.openremote.manager.security.ManagerIdentityProvider; import org.openremote.manager.setup.ManagerSetup; import org.openremote.model.Constants; @@ -49,2345 +59,4219 @@ import org.openremote.model.asset.impl.*; import org.openremote.model.attribute.Attribute; import org.openremote.model.attribute.AttributeLink; +import org.openremote.model.attribute.AttributeMap; import org.openremote.model.attribute.AttributeRef; import org.openremote.model.attribute.MetaItem; +import org.openremote.model.datapoint.ValueDatapoint; import org.openremote.model.geo.GeoJSONPoint; +import org.openremote.model.query.AssetQuery; import org.openremote.model.security.Realm; import org.openremote.model.simulator.SimulatorReplayDatapoint; +import org.openremote.model.util.UniqueIdentifierGenerator; import org.openremote.model.value.*; -import java.time.Duration; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Random; -import java.util.function.Supplier; +public class ManagerDemoSetup extends ManagerSetup { -import static java.time.temporal.ChronoField.SECOND_OF_DAY; -import static org.openremote.model.Constants.*; -import static org.openremote.model.value.MetaItemType.*; -import static org.openremote.model.value.ValueType.MultivaluedStringMap; + public static final int HISTORIC_SIMULATED_DATA_DAYS = 28; + public static GeoJSONPoint STATIONSPLEIN_LOCATION = new GeoJSONPoint(4.470175, 51.923464); + public String realmMasterName; + public String realmCityName; + public String realmManufacturerName; + public String area1Id; + public String smartcitySimulatorAgentId; + public String manufacturerSimulatorAgentId; + public String energyManagementId; + public String weatherHttpApiAgentId; -public class ManagerDemoSetup extends ManagerSetup { + public String paprikaId; + public String irrigation9Id; + public String harvestRobot5Id; + public String irrigation10Id; + public String irrigation11Id; + public String soilSensor4Id; - public static final int HISTORIC_SIMULATED_DATA_DAYS = 28; - public static GeoJSONPoint STATIONSPLEIN_LOCATION = new GeoJSONPoint(4.470175, 51.923464); - public String realmMasterName; - public String realmCityName; - public String realmManufacturerName; - public String area1Id; - public String smartcitySimulatorAgentId; - public String manufacturerSimulatorAgentId; - public String energyManagementId; - public String weatherHttpApiAgentId; - - public String paprikaId; - public String irrigation9Id; - public String harvestRobot5Id; - public String irrigation10Id; - public String irrigation11Id; - public String soilSensor4Id; - - private final long halfHourInMillis = Duration.ofMinutes(30).toMillis(); - - public ManagerDemoSetup(Container container) { - super(container); - } + private final long halfHourInMillis = Duration.ofMinutes(30).toMillis(); - private static SimulatorProtocol.Schedule createDailySchedule() { - return new SimulatorProtocol.Schedule(null, null, "FREQ=DAILY"); - } + public ManagerDemoSetup(Container container) { + super(container); + } - private static int getRandomNumberInRange(int min, int max) { - if (min >= max) { - throw new IllegalArgumentException("max must be greater than min"); - } - Random r = new Random(); - return r.nextInt((max - min) + 1) + min; - } + private static SimulatorProtocol.Schedule createDailySchedule() { + return new SimulatorProtocol.Schedule(null, null, "FREQ=DAILY"); + } - // ################################ Realm manufacturer methods ################################### - protected HarvestRobotAsset createDemoHarvestRobotAsset(String name, Asset parent, GeoJSONPoint location, - OperationMode operationMode, VegetableType vegetableType, int direction, int harvestedTotal, Supplier> agentLinker) { - HarvestRobotAsset harvestRobotAsset = new HarvestRobotAsset(name); - harvestRobotAsset.setParent(parent); - harvestRobotAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.OPERATION_MODE) - .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) - .setValue(operationMode); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.VEGETABLE_TYPE) - .addMeta(new MetaItem<>(RULE_STATE)) - .setValue(vegetableType); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.DIRECTION) - .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) - .setValue(direction); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.SPEED) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.HARVESTED_SESSION) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - harvestRobotAsset.getAttributes().getOrCreate(HarvestRobotAsset.HARVESTED_TOTAL) - .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) - .setValue(harvestedTotal); - - return harvestRobotAsset; + private static int getRandomNumberInRange(int min, int max) { + if (min >= max) { + throw new IllegalArgumentException("max must be greater than min"); } + Random r = new Random(); + return r.nextInt((max - min) + 1) + min; + } - protected IrrigationAsset createDemoIrrigationAsset(String name, Asset parent, GeoJSONPoint location, Supplier> agentLinker) { - IrrigationAsset irrigationAsset = new IrrigationAsset(name); - irrigationAsset.setParent(parent); - irrigationAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); - irrigationAsset.getAttributes().getOrCreate(IrrigationAsset.FLOW_WATER) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - irrigationAsset.getAttributes().getOrCreate(IrrigationAsset.FLOW_NUTRIENTS) - .addMeta(new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - irrigationAsset.getAttributes().getOrCreate(IrrigationAsset.FLOW_TOTAL) - .addMeta(new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - irrigationAsset.getAttributes().getOrCreate(IrrigationAsset.TANK_LEVEL) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)); - - return irrigationAsset; - } - protected SoilSensorAsset createDemoSoilSensorAsset(String name, Asset parent, GeoJSONPoint location, - int soilTensionMin, int soilTensionMax, Supplier> agentLinker) { - SoilSensorAsset soilSensorAsset = new SoilSensorAsset(name); - soilSensorAsset.setParent(parent); - soilSensorAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); - soilSensorAsset.getAttributes().getOrCreate(SoilSensorAsset.SOIL_TENSION_MEASURED) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(RULE_STATE), new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(READ_ONLY)); - soilSensorAsset.getAttributes().getOrCreate(SoilSensorAsset.SOIL_TENSION_MIN) - .addMeta(new MetaItem<>(RULE_STATE)) - .setValue(soilTensionMin); - soilSensorAsset.getAttributes().getOrCreate(SoilSensorAsset.SOIL_TENSION_MAX) - .addMeta(new MetaItem<>(RULE_STATE)) - .setValue(soilTensionMax); - soilSensorAsset.getAttributes().getOrCreate(SoilSensorAsset.TEMPERATURE) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(RULE_STATE), new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(READ_ONLY)); - soilSensorAsset.getAttributes().getOrCreate(SoilSensorAsset.SALINITY) - .addMeta(new MetaItem<>(AGENT_LINK, agentLinker.get()), - new MetaItem<>(RULE_STATE), new MetaItem<>(STORE_DATA_POINTS), new MetaItem<>(READ_ONLY)); - - return soilSensorAsset; - } + // ################################ Realm manufacturer methods ################################### + protected HarvestRobotAsset createDemoHarvestRobotAsset( + String name, + Asset parent, + GeoJSONPoint location, + OperationMode operationMode, + VegetableType vegetableType, + int direction, + int harvestedTotal, + Supplier> agentLinker) { + HarvestRobotAsset harvestRobotAsset = new HarvestRobotAsset(name); + harvestRobotAsset.setParent(parent); + harvestRobotAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.OPERATION_MODE) + .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) + .setValue(operationMode); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.VEGETABLE_TYPE) + .addMeta(new MetaItem<>(RULE_STATE)) + .setValue(vegetableType); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.DIRECTION) + .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) + .setValue(direction); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.SPEED) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.HARVESTED_SESSION) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); + harvestRobotAsset + .getAttributes() + .getOrCreate(HarvestRobotAsset.HARVESTED_TOTAL) + .addMeta(new MetaItem<>(RULE_STATE), new MetaItem<>(READ_ONLY)) + .setValue(harvestedTotal); - @Override - public void onStart() throws Exception { - super.onStart(); - - KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); - Realm realmMaster = keycloakDemoSetup.realmMaster; - Realm realmCity = keycloakDemoSetup.realmCity; - Realm realmManufacturer = KeycloakDemoSetup.realmManufacturer; - realmMasterName = realmMaster.getName(); - this.realmCityName = realmCity.getName(); - realmManufacturerName = realmManufacturer.getName(); - - // ################################ Realm smartcity ################################### - - SimulatorAgent smartcitySimulatorAgent = new SimulatorAgent("Simulator agent"); - smartcitySimulatorAgent.setRealm(this.realmCityName); - - smartcitySimulatorAgent = assetStorageService.merge(smartcitySimulatorAgent); - smartcitySimulatorAgentId = smartcitySimulatorAgent.getId(); - - LocalTime midnight = LocalTime.of(0, 0); - - // ################################ Realm smartcity - Energy Management ################################### - - ThingAsset energyManagement = new ThingAsset("Energy management"); - energyManagement.setRealm(this.realmCityName); - energyManagement.getAttributes().addOrReplace( - new Attribute<>("powerTotalProducers", ValueType.NUMBER) - .addOrReplaceMeta( - new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), - new MetaItem<>(MetaItemType.READ_ONLY, true), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS, true), - new MetaItem<>(MetaItemType.RULE_STATE, true)), - new Attribute<>("powerTotalConsumers", ValueType.NUMBER).addOrReplaceMeta( - new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), - new MetaItem<>(MetaItemType.READ_ONLY, true), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS, true), - new MetaItem<>(MetaItemType.RULE_STATE, true)) - ); - energyManagement.setId(UniqueIdentifierGenerator.generateId(energyManagement.getName())); - energyManagement = assetStorageService.merge(energyManagement); - energyManagementId = energyManagement.getId(); - - // ### De Rotterdam ### - BuildingAsset building1Asset = new BuildingAsset("De Rotterdam"); - building1Asset.setParent(energyManagement); - building1Asset.getAttributes().addOrReplace( - new Attribute<>(BuildingAsset.STREET, "Wilhelminakade 139"), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3072 AP"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.488324, 51.906577)), - new Attribute<>("powerBalance", ValueType.NUMBER).addMeta( - new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) - ); - building1Asset.setId(UniqueIdentifierGenerator.generateId(building1Asset.getName() + "building")); - building1Asset = assetStorageService.merge(building1Asset); - - ElectricityStorageAsset storage1Asset = createDemoElectricityStorageAsset("Battery De Rotterdam", building1Asset, new GeoJSONPoint(4.488324, 51.906577)); - storage1Asset.setManufacturer("Super-B"); - storage1Asset.setModel("Nomia"); - storage1Asset.setId(UniqueIdentifierGenerator.generateId(storage1Asset.getName())); - storage1Asset = assetStorageService.merge(storage1Asset); - - ElectricityConsumerAsset consumption1Asset = createDemoElectricityConsumerAsset("Consumption De Rotterdam", building1Asset, new GeoJSONPoint(4.487519, 51.906544)); - consumption1Asset.getAttribute(ElectricityConsumerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 20), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 41), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 54), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 63), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 76), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 80), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 79), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 84), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 76), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 82), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 83), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 77), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 71), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 63), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 41), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 27), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 24), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 20) - }) - ) - ); - }); + return harvestRobotAsset; + } + + protected IrrigationAsset createDemoIrrigationAsset( + String name, Asset parent, GeoJSONPoint location, Supplier> agentLinker) { + IrrigationAsset irrigationAsset = new IrrigationAsset(name); + irrigationAsset.setParent(parent); + irrigationAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + irrigationAsset + .getAttributes() + .getOrCreate(IrrigationAsset.FLOW_WATER) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); + irrigationAsset + .getAttributes() + .getOrCreate(IrrigationAsset.FLOW_NUTRIENTS) + .addMeta( + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); + irrigationAsset + .getAttributes() + .getOrCreate(IrrigationAsset.FLOW_TOTAL) + .addMeta( + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); + irrigationAsset + .getAttributes() + .getOrCreate(IrrigationAsset.TANK_LEVEL) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(RULE_STATE), + new MetaItem<>(READ_ONLY)); - consumption1Asset.setId(UniqueIdentifierGenerator.generateId(consumption1Asset.getName())); - consumption1Asset = assetStorageService.merge(consumption1Asset); + return irrigationAsset; + } - ElectricityProducerSolarAsset production1Asset = createDemoElectricitySolarProducerAsset("Solar De Rotterdam", building1Asset, new GeoJSONPoint(4.488592, 51.907047)); - production1Asset.setManufacturer("AEG"); - production1Asset.setModel("AS-P60"); - production1Asset.getAttribute(ElectricityProducerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + protected SoilSensorAsset createDemoSoilSensorAsset( + String name, + Asset parent, + GeoJSONPoint location, + int soilTensionMin, + int soilTensionMax, + Supplier> agentLinker) { + SoilSensorAsset soilSensorAsset = new SoilSensorAsset(name); + soilSensorAsset.setParent(parent); + soilSensorAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + soilSensorAsset + .getAttributes() + .getOrCreate(SoilSensorAsset.SOIL_TENSION_MEASURED) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(RULE_STATE), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(READ_ONLY)); + soilSensorAsset + .getAttributes() + .getOrCreate(SoilSensorAsset.SOIL_TENSION_MIN) + .addMeta(new MetaItem<>(RULE_STATE)) + .setValue(soilTensionMin); + soilSensorAsset + .getAttributes() + .getOrCreate(SoilSensorAsset.SOIL_TENSION_MAX) + .addMeta(new MetaItem<>(RULE_STATE)) + .setValue(soilTensionMax); + soilSensorAsset + .getAttributes() + .getOrCreate(SoilSensorAsset.TEMPERATURE) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(RULE_STATE), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(READ_ONLY)); + soilSensorAsset + .getAttributes() + .getOrCreate(SoilSensorAsset.SALINITY) + .addMeta( + new MetaItem<>(AGENT_LINK, agentLinker.get()), + new MetaItem<>(RULE_STATE), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>(READ_ONLY)); + + return soilSensorAsset; + } + + @Override + public void onStart() throws Exception { + super.onStart(); + + KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); + Realm realmMaster = keycloakDemoSetup.realmMaster; + Realm realmCity = keycloakDemoSetup.realmCity; + Realm realmManufacturer = KeycloakDemoSetup.realmManufacturer; + realmMasterName = realmMaster.getName(); + this.realmCityName = realmCity.getName(); + realmManufacturerName = realmManufacturer.getName(); + + // ################################ Realm smartcity ################################### + + SimulatorAgent smartcitySimulatorAgent = new SimulatorAgent("Simulator agent"); + smartcitySimulatorAgent.setRealm(this.realmCityName); + + smartcitySimulatorAgent = assetStorageService.merge(smartcitySimulatorAgent); + smartcitySimulatorAgentId = smartcitySimulatorAgent.getId(); + + LocalTime midnight = LocalTime.of(0, 0); + + // ################################ Realm smartcity - Energy Management + // ################################### + + ThingAsset energyManagement = new ThingAsset("Energy management"); + energyManagement.setRealm(this.realmCityName); + energyManagement + .getAttributes() + .addOrReplace( + new Attribute<>("powerTotalProducers", ValueType.NUMBER) + .addOrReplaceMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), -10), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), -15), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), -39), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), -52), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), -50), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), -48), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), -36), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), -23), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), -24), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), -18), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), -10), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), -8), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), -3), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - }); - production1Asset.setEnergyExportTotal(152689d); - production1Asset.setPowerExportMax(89.6); - production1Asset.setEfficiencyExport(93); - production1Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.EAST_WEST); - production1Asset.setPanelAzimuth(30); - production1Asset.setPanelPitch(20); - production1Asset.setId(UniqueIdentifierGenerator.generateId(production1Asset.getName())); - production1Asset = assetStorageService.merge(production1Asset); - - // ### Stadhuis ### - - BuildingAsset building2Asset = new BuildingAsset("Stadhuis"); - building2Asset.setParent(energyManagement); - building2Asset.getAttributes().addOrReplace( - new Attribute<>(BuildingAsset.STREET, "Coolsingel 40"), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3011 AD"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.47985, 51.92274)) - ); - building2Asset.setId(UniqueIdentifierGenerator.generateId(building2Asset.getName() + "building")); - building2Asset = assetStorageService.merge(building2Asset); - - ElectricityStorageAsset storage2Asset = createDemoElectricityStorageAsset("Battery Stadhuis", building2Asset, new GeoJSONPoint(4.47985, 51.92274)); - storage2Asset.setManufacturer("LG Chem"); - storage2Asset.setModel("ESS Industrial"); - storage2Asset.setId(UniqueIdentifierGenerator.generateId(storage2Asset.getName())); - storage2Asset = assetStorageService.merge(storage2Asset); - - ElectricityConsumerAsset consumption2Asset = createDemoElectricityConsumerAsset("Consumption Stadhuis", building2Asset, new GeoJSONPoint(4.47933, 51.92259)); - consumption2Asset.getAttribute(ElectricityConsumerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + MetaItemType.UNITS, + Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), + new MetaItem<>(MetaItemType.READ_ONLY, true), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS, true), + new MetaItem<>(MetaItemType.RULE_STATE, true)), + new Attribute<>("powerTotalConsumers", ValueType.NUMBER) + .addOrReplaceMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 30), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 39), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 44), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 47), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 44), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 33), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 8) - } - ) - ) - ); - }); - consumption2Asset.setId(UniqueIdentifierGenerator.generateId(consumption2Asset.getName())); - consumption2Asset = assetStorageService.merge(consumption2Asset); + MetaItemType.UNITS, + Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), + new MetaItem<>(MetaItemType.READ_ONLY, true), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS, true), + new MetaItem<>(MetaItemType.RULE_STATE, true))); + energyManagement.setId(UniqueIdentifierGenerator.generateId(energyManagement.getName())); + energyManagement = assetStorageService.merge(energyManagement); + energyManagementId = energyManagement.getId(); - ElectricityProducerSolarAsset production2Asset = createDemoElectricitySolarProducerAsset("Solar Stadhuis", building2Asset, new GeoJSONPoint(4.47945, 51.92301)); - production2Asset.getAttribute(ElectricityProducerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + // ### De Rotterdam ### + BuildingAsset building1Asset = new BuildingAsset("De Rotterdam"); + building1Asset.setParent(energyManagement); + building1Asset + .getAttributes() + .addOrReplace( + new Attribute<>(BuildingAsset.STREET, "Wilhelminakade 139"), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3072 AP"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.488324, 51.906577)), + new Attribute<>("powerBalance", ValueType.NUMBER) + .addMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), -2), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), -3), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), -8), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), -14), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), -12), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), -10), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), -7), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), -5), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), -7), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), -5), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), -3), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), -2), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - }); - production2Asset.setEnergyExportTotal(88961d); - production2Asset.setPowerExportMax(19.2); - production2Asset.setEfficiencyExport(79); - production2Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); - production2Asset.setPanelAzimuth(10); - production2Asset.setPanelPitch(40); - production2Asset.setManufacturer("Solarwatt"); - production2Asset.setModel("EasyIn 60M"); - production2Asset.setId(UniqueIdentifierGenerator.generateId(production2Asset.getName())); - production2Asset = assetStorageService.merge(production2Asset); - - // ### Markthal ### - - BuildingAsset building3Asset = new BuildingAsset("Markthal"); - building3Asset.setParent(energyManagement); - building3Asset.getAttributes().addOrReplace( - new Attribute<>(BuildingAsset.STREET, "Dominee Jan Scharpstraat 298"), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3011 GZ"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.47945, 51.92301)), - new Attribute<>("allChargersInUse", ValueType.BOOLEAN) - .addMeta( - new MetaItem<>(MetaItemType.READ_ONLY)) - ); - building3Asset.setId(UniqueIdentifierGenerator.generateId(building3Asset.getName() + "building")); - building3Asset = assetStorageService.merge(building3Asset); - - ElectricityProducerSolarAsset production3Asset = createDemoElectricitySolarProducerAsset("Solar Markthal", building3Asset, new GeoJSONPoint(4.47945, 51.92301)); - production3Asset.getAttribute(ElectricityProducerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { + MetaItemType.UNITS, + Constants.units(Constants.UNITS_KILO, Constants.UNITS_WATT)), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS))); + building1Asset.setId( + UniqueIdentifierGenerator.generateId(building1Asset.getName() + "building")); + building1Asset = assetStorageService.merge(building1Asset); + + ElectricityStorageAsset storage1Asset = + createDemoElectricityStorageAsset( + "Battery De Rotterdam", building1Asset, new GeoJSONPoint(4.488324, 51.906577)); + storage1Asset.setManufacturer("Super-B"); + storage1Asset.setModel("Nomia"); + storage1Asset.setId(UniqueIdentifierGenerator.generateId(storage1Asset.getName())); + storage1Asset = assetStorageService.merge(storage1Asset); + + ElectricityConsumerAsset consumption1Asset = + createDemoElectricityConsumerAsset( + "Consumption De Rotterdam", building1Asset, new GeoJSONPoint(4.487519, 51.906544)); + consumption1Asset + .getAttribute(ElectricityConsumerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 20), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 41), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 54), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 63), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 76), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 80), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 79), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 84), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 76), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 82), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 83), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 77), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 71), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 63), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 41), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 27), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 24), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 20) + }))); + }); + + consumption1Asset.setId(UniqueIdentifierGenerator.generateId(consumption1Asset.getName())); + consumption1Asset = assetStorageService.merge(consumption1Asset); + + ElectricityProducerSolarAsset production1Asset = + createDemoElectricitySolarProducerAsset( + "Solar De Rotterdam", building1Asset, new GeoJSONPoint(4.488592, 51.907047)); + production1Asset.setManufacturer("AEG"); + production1Asset.setModel("AS-P60"); + production1Asset + .getAttribute(ElectricityProducerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), -2), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), -6), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), -10), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), -13), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), -21), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), -14), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), -17), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), -10), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), -9), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), -7), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), -5), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), -4), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), -2), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - }); - production3Asset.setEnergyExportTotal(24134d); - production3Asset.setPowerExportMax(29.8); - production3Asset.setEfficiencyExport(91); - production3Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); - production3Asset.setManufacturer("Sunpower"); - production3Asset.setModel("E20-327"); - production3Asset.setPanelAzimuth(10); - production3Asset.setPanelPitch(5); - production3Asset.setId(UniqueIdentifierGenerator.generateId(production3Asset.getName())); - production3Asset = assetStorageService.merge(production3Asset); - - ElectricityChargerAsset charger1Asset = createDemoElectricityChargerAsset("Charger 1 Markthal", building3Asset, new GeoJSONPoint(4.486143, 51.920058)); - charger1Asset.setPower(0d); - charger1Asset.getAttributes().getOrCreate(ElectricityChargerAsset.POWER).addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 35), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 17), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - charger1Asset.setManufacturer("Allego"); - charger1Asset.setModel("HPC"); - charger1Asset.setId(UniqueIdentifierGenerator.generateId(charger1Asset.getName())); - charger1Asset = assetStorageService.merge(charger1Asset); - - ElectricityChargerAsset charger2Asset = createDemoElectricityChargerAsset("Charger 2 Markthal", building3Asset, new GeoJSONPoint(4.486188, 51.919957)); - charger2Asset.setPower(0d); - charger2Asset.getAttributes().getOrCreate(ElectricityChargerAsset.POWER) - .addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 17), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 28), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 26), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - charger2Asset.setManufacturer("Bosch"); - charger2Asset.setModel("EV800"); - charger2Asset.setId(UniqueIdentifierGenerator.generateId(charger2Asset.getName())); - charger2Asset = assetStorageService.merge(charger2Asset); - - ElectricityChargerAsset charger3Asset = createDemoElectricityChargerAsset("Charger 3 Markthal", building3Asset, new GeoJSONPoint(4.486232, 51.919856)); - charger3Asset.setPower(0d); - charger3Asset.getAttributes().getOrCreate(ElectricityChargerAsset.POWER) - .addMeta( + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), -10), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), -15), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), -39), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), -52), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), -50), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), -48), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), -36), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), -23), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), -24), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), -18), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), -10), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), -8), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), -3), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + }); + production1Asset.setEnergyExportTotal(152689d); + production1Asset.setPowerExportMax(89.6); + production1Asset.setEfficiencyExport(93); + production1Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.EAST_WEST); + production1Asset.setPanelAzimuth(30); + production1Asset.setPanelPitch(20); + production1Asset.setId(UniqueIdentifierGenerator.generateId(production1Asset.getName())); + production1Asset = assetStorageService.merge(production1Asset); + + // ### Stadhuis ### + + BuildingAsset building2Asset = new BuildingAsset("Stadhuis"); + building2Asset.setParent(energyManagement); + building2Asset + .getAttributes() + .addOrReplace( + new Attribute<>(BuildingAsset.STREET, "Coolsingel 40"), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3011 AD"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.47985, 51.92274))); + building2Asset.setId( + UniqueIdentifierGenerator.generateId(building2Asset.getName() + "building")); + building2Asset = assetStorageService.merge(building2Asset); + + ElectricityStorageAsset storage2Asset = + createDemoElectricityStorageAsset( + "Battery Stadhuis", building2Asset, new GeoJSONPoint(4.47985, 51.92274)); + storage2Asset.setManufacturer("LG Chem"); + storage2Asset.setModel("ESS Industrial"); + storage2Asset.setId(UniqueIdentifierGenerator.generateId(storage2Asset.getName())); + storage2Asset = assetStorageService.merge(storage2Asset); + + ElectricityConsumerAsset consumption2Asset = + createDemoElectricityConsumerAsset( + "Consumption Stadhuis", building2Asset, new GeoJSONPoint(4.47933, 51.92259)); + consumption2Asset + .getAttribute(ElectricityConsumerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 30), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 39), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 44), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 47), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 44), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 33), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 8) + }))); + }); + consumption2Asset.setId(UniqueIdentifierGenerator.generateId(consumption2Asset.getName())); + consumption2Asset = assetStorageService.merge(consumption2Asset); + + ElectricityProducerSolarAsset production2Asset = + createDemoElectricitySolarProducerAsset( + "Solar Stadhuis", building2Asset, new GeoJSONPoint(4.47945, 51.92301)); + production2Asset + .getAttribute(ElectricityProducerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), -2), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), -3), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), -8), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), -14), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), -12), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), -10), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), -7), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), -5), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), -7), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), -5), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), -3), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), -2), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + }); + production2Asset.setEnergyExportTotal(88961d); + production2Asset.setPowerExportMax(19.2); + production2Asset.setEfficiencyExport(79); + production2Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); + production2Asset.setPanelAzimuth(10); + production2Asset.setPanelPitch(40); + production2Asset.setManufacturer("Solarwatt"); + production2Asset.setModel("EasyIn 60M"); + production2Asset.setId(UniqueIdentifierGenerator.generateId(production2Asset.getName())); + production2Asset = assetStorageService.merge(production2Asset); + + // ### Markthal ### + + BuildingAsset building3Asset = new BuildingAsset("Markthal"); + building3Asset.setParent(energyManagement); + building3Asset + .getAttributes() + .addOrReplace( + new Attribute<>(BuildingAsset.STREET, "Dominee Jan Scharpstraat 298"), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3011 GZ"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.47945, 51.92301)), + new Attribute<>("allChargersInUse", ValueType.BOOLEAN) + .addMeta(new MetaItem<>(MetaItemType.READ_ONLY))); + building3Asset.setId( + UniqueIdentifierGenerator.generateId(building3Asset.getName() + "building")); + building3Asset = assetStorageService.merge(building3Asset); + + ElectricityProducerSolarAsset production3Asset = + createDemoElectricitySolarProducerAsset( + "Solar Markthal", building3Asset, new GeoJSONPoint(4.47945, 51.92301)); + production3Asset + .getAttribute(ElectricityProducerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), -2), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), -6), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), -10), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), -13), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), -21), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), -14), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), -17), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), -10), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), -9), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), -7), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), -5), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), -4), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), -2), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + }); + production3Asset.setEnergyExportTotal(24134d); + production3Asset.setPowerExportMax(29.8); + production3Asset.setEfficiencyExport(91); + production3Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); + production3Asset.setManufacturer("Sunpower"); + production3Asset.setModel("E20-327"); + production3Asset.setPanelAzimuth(10); + production3Asset.setPanelPitch(5); + production3Asset.setId(UniqueIdentifierGenerator.generateId(production3Asset.getName())); + production3Asset = assetStorageService.merge(production3Asset); + + ElectricityChargerAsset charger1Asset = + createDemoElectricityChargerAsset( + "Charger 1 Markthal", building3Asset, new GeoJSONPoint(4.486143, 51.920058)); + charger1Asset.setPower(0d); + charger1Asset + .getAttributes() + .getOrCreate(ElectricityChargerAsset.POWER) + .addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 35), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 17), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + charger1Asset.setManufacturer("Allego"); + charger1Asset.setModel("HPC"); + charger1Asset.setId(UniqueIdentifierGenerator.generateId(charger1Asset.getName())); + charger1Asset = assetStorageService.merge(charger1Asset); + + ElectricityChargerAsset charger2Asset = + createDemoElectricityChargerAsset( + "Charger 2 Markthal", building3Asset, new GeoJSONPoint(4.486188, 51.919957)); + charger2Asset.setPower(0d); + charger2Asset + .getAttributes() + .getOrCreate(ElectricityChargerAsset.POWER) + .addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 17), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 28), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 26), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + charger2Asset.setManufacturer("Bosch"); + charger2Asset.setModel("EV800"); + charger2Asset.setId(UniqueIdentifierGenerator.generateId(charger2Asset.getName())); + charger2Asset = assetStorageService.merge(charger2Asset); + + ElectricityChargerAsset charger3Asset = + createDemoElectricityChargerAsset( + "Charger 3 Markthal", building3Asset, new GeoJSONPoint(4.486232, 51.919856)); + charger3Asset.setPower(0d); + charger3Asset + .getAttributes() + .getOrCreate(ElectricityChargerAsset.POWER) + .addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 18), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 29), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + charger3Asset.setManufacturer("Siemens"); + charger3Asset.setModel("CPC 50"); + charger3Asset.setId(UniqueIdentifierGenerator.generateId(charger3Asset.getName())); + charger3Asset = assetStorageService.merge(charger3Asset); + + ElectricityChargerAsset charger4Asset = + createDemoElectricityChargerAsset( + "Charger 4 Markthal", building3Asset, new GeoJSONPoint(4.486286, 51.919733)); + charger4Asset.setPower(0d); + charger4Asset + .getAttributes() + .getOrCreate(ElectricityChargerAsset.POWER) + .addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 17), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 30), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 4) + }))); + + charger4Asset.setManufacturer("SemaConnect"); + charger4Asset.setModel("The Series 6"); + charger4Asset.setId(UniqueIdentifierGenerator.generateId(charger4Asset.getName())); + charger4Asset = assetStorageService.merge(charger4Asset); + + // ### Erasmianum ### + + BuildingAsset building4Asset = new BuildingAsset("Erasmianum"); + building4Asset.setParent(energyManagement); + building4Asset + .getAttributes() + .addOrReplace( + new Attribute<>(BuildingAsset.STREET, "Wytemaweg 25"), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3015 CN"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.468324, 51.912062))); + building4Asset.setId( + UniqueIdentifierGenerator.generateId(building4Asset.getName() + "building")); + building4Asset = assetStorageService.merge(building4Asset); + + ElectricityConsumerAsset consumption4Asset = + createDemoElectricityConsumerAsset( + "Consumption Erasmianum", building4Asset, new GeoJSONPoint(4.468324, 51.912062)); + consumption4Asset + .getAttribute(ElectricityConsumerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 37), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 41), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 47), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 49), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 51), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 43), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 48), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 45), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 46), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 41), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 30), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 19), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 6) + }))); + }); + consumption4Asset.setId(UniqueIdentifierGenerator.generateId(consumption4Asset.getName())); + consumption4Asset = assetStorageService.merge(consumption4Asset); + + // ### Oostelijk zwembad ### + + BuildingAsset building5Asset = new BuildingAsset("Oostelijk zwembad"); + building5Asset.setParent(energyManagement); + building5Asset + .getAttributes() + .addOrReplace( + new Attribute<>(BuildingAsset.STREET, "Gerdesiaweg 480"), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3061 RA"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.498048, 51.925770))); + building5Asset.setId( + UniqueIdentifierGenerator.generateId(building5Asset.getName() + "building")); + building5Asset = assetStorageService.merge(building5Asset); + + ElectricityConsumerAsset consumption5Asset = + createDemoElectricityConsumerAsset( + "Consumption Zwembad", building5Asset, new GeoJSONPoint(4.498048, 51.925770)); + consumption5Asset + .getAttribute(ElectricityConsumerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 17), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 16), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 24), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 35), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 33), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 33), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 31), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 37), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 37), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 35), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 24), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 19) + }))); + }); + consumption5Asset.setId(UniqueIdentifierGenerator.generateId(consumption5Asset.getName())); + consumption5Asset = assetStorageService.merge(consumption5Asset); + + ElectricityProducerSolarAsset production5Asset = + createDemoElectricitySolarProducerAsset( + "Solar Zwembad", building5Asset, new GeoJSONPoint(4.498281, 51.925507)); + production5Asset + .getAttribute(ElectricityProducerAsset.POWER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), -1), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), -3), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), -8), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), -30), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), -44), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), -42), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), -41), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), -29), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), -19), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), -16), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), -11), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), -4), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), -3), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), -2), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0) + }))); + }); + production5Asset.setEnergyExportTotal(23461d); + production5Asset.setPowerExportMax(76.2); + production5Asset.setEfficiencyExport(86); + production5Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); + production5Asset.setManufacturer("S-Energy"); + production5Asset.setModel("SN260P-10"); + production5Asset.setPanelAzimuth(50); + production5Asset.setPanelPitch(15); + production5Asset.setId(UniqueIdentifierGenerator.generateId(production5Asset.getName())); + production5Asset = assetStorageService.merge(production5Asset); + + // ### Weather ### + HTTPAgent weatherHttpApiAgent = new HTTPAgent("Weather Agent"); + weatherHttpApiAgent.setParent(energyManagement); + weatherHttpApiAgent.setBaseURI("https://api.openweathermap.org/data/2.5/"); + + MultivaluedStringMap queryParams = new MultivaluedStringMap(); + queryParams.put("appid", Collections.singletonList("c3ecbf09be5267cd280676a01acd3360")); + queryParams.put("lat", Collections.singletonList("51.918849")); + queryParams.put("lon", Collections.singletonList("4.463250")); + queryParams.put("units", Collections.singletonList("metric")); + weatherHttpApiAgent.setRequestQueryParameters(queryParams); + + MultivaluedStringMap headers = new MultivaluedStringMap(); + headers.put("Accept", Collections.singletonList("application/json")); + weatherHttpApiAgent.setRequestHeaders(headers); + + weatherHttpApiAgent = assetStorageService.merge(weatherHttpApiAgent); + weatherHttpApiAgentId = weatherHttpApiAgent.getId(); + + WeatherAsset weather = new WeatherAsset("Weather"); + weather.setParent(energyManagement); + weather.setId(UniqueIdentifierGenerator.generateId(weather.getName())); + + HTTPAgentLink agentLink = new HTTPAgentLink(weatherHttpApiAgentId); + agentLink.setPath("weather"); + agentLink.setPollingMillis((int) halfHourInMillis); + + weather + .getAttributes() + .addOrReplace( + new Attribute<>("currentWeather") + .addMeta( + new MetaItem<>(MetaItemType.AGENT_LINK, agentLink), + new MetaItem<>(MetaItemType.LABEL, "Open Weather Map API weather end point"), + new MetaItem<>(MetaItemType.READ_ONLY, true), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS, false), + new MetaItem<>(MetaItemType.RULE_STATE, false), new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 18), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 29), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - charger3Asset.setManufacturer("Siemens"); - charger3Asset.setModel("CPC 50"); - charger3Asset.setId(UniqueIdentifierGenerator.generateId(charger3Asset.getName())); - charger3Asset = assetStorageService.merge(charger3Asset); - - ElectricityChargerAsset charger4Asset = createDemoElectricityChargerAsset("Charger 4 Markthal", building3Asset, new GeoJSONPoint(4.486286, 51.919733)); - charger4Asset.setPower(0d); - charger4Asset.getAttributes().getOrCreate(ElectricityChargerAsset.POWER) - .addMeta( + MetaItemType.ATTRIBUTE_LINKS, + new AttributeLink[] { + createWeatherApiAttributeLink( + weather.getId(), "main", "temp", "temperature"), + createWeatherApiAttributeLink( + weather.getId(), "main", "humidity", "humidity"), + createWeatherApiAttributeLink( + weather.getId(), "wind", "speed", "windSpeed"), + createWeatherApiAttributeLink( + weather.getId(), "wind", "deg", "windDirection") + }))); + weather + .getAttribute("windSpeed") + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(MetaItemType.STORE_DATA_POINTS), + new MetaItem<>(MetaItemType.RULE_STATE)); + }); + weather + .getAttribute("temperature") + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(MetaItemType.STORE_DATA_POINTS), + new MetaItem<>(MetaItemType.RULE_STATE)); + }); + weather + .getAttribute("windDirection") + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(MetaItemType.STORE_DATA_POINTS), + new MetaItem<>(MetaItemType.RULE_STATE)); + }); + weather + .getAttribute("humidity") + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(MetaItemType.STORE_DATA_POINTS), + new MetaItem<>(MetaItemType.RULE_STATE)); + }); + new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.463250, 51.918849)); + weather = assetStorageService.merge(weather); + + // ################################ Realm smartcity - Environment monitor + // ################################### + + Asset environmentMonitor = new ThingAsset("Environment monitor"); + environmentMonitor.setRealm(this.realmCityName); + environmentMonitor.setId(UniqueIdentifierGenerator.generateId(environmentMonitor.getName())); + environmentMonitor = assetStorageService.merge(environmentMonitor); + + EnvironmentSensorAsset environment1Asset = + createDemoEnvironmentAsset( + "Oudehaven", + environmentMonitor, + new GeoJSONPoint(4.49313, 51.91885), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + EnvironmentSensorAsset environment2Asset = + createDemoEnvironmentAsset( + "Kaappark", + environmentMonitor, + new GeoJSONPoint(4.480434, 51.899287), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + EnvironmentSensorAsset environment3Asset = + createDemoEnvironmentAsset( + "Museumpark", + environmentMonitor, + new GeoJSONPoint(4.472457, 51.912047), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + EnvironmentSensorAsset environment4Asset = + createDemoEnvironmentAsset( + "Eendrachtsplein", + environmentMonitor, + new GeoJSONPoint(4.473599, 51.916292), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + + EnvironmentSensorAsset[] environmentArray = { + environment1Asset, environment2Asset, environment3Asset, environment4Asset + }; + for (EnvironmentSensorAsset asset : environmentArray) { + asset.setManufacturer("Intemo"); + asset.setModel("Josene outdoor"); + asset + .getAttribute(EnvironmentSensorAsset.OZONE) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 17), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 30), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 4) - } - ) - ) - ); - - charger4Asset.setManufacturer("SemaConnect"); - charger4Asset.setModel("The Series 6"); - charger4Asset.setId(UniqueIdentifierGenerator.generateId(charger4Asset.getName())); - charger4Asset = assetStorageService.merge(charger4Asset); - - // ### Erasmianum ### - - BuildingAsset building4Asset = new BuildingAsset("Erasmianum"); - building4Asset.setParent(energyManagement); - building4Asset.getAttributes().addOrReplace( - new Attribute<>(BuildingAsset.STREET, "Wytemaweg 25"), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3015 CN"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.468324, 51.912062)) - ); - building4Asset.setId(UniqueIdentifierGenerator.generateId(building4Asset.getName() + "building")); - building4Asset = assetStorageService.merge(building4Asset); - - ElectricityConsumerAsset consumption4Asset = createDemoElectricityConsumerAsset("Consumption Erasmianum", building4Asset, new GeoJSONPoint(4.468324, 51.912062)); - consumption4Asset.getAttribute(ElectricityConsumerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + new SimulatorReplayDatapoint( + midnight.get(SECOND_OF_DAY), getRandomNumberInRange(80, 90)), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 90)), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 90)), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 90)), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), + getRandomNumberInRange(75, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 110)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), + getRandomNumberInRange(85, 110)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), + getRandomNumberInRange(85, 115)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), + getRandomNumberInRange(85, 115)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), + getRandomNumberInRange(85, 115)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), + getRandomNumberInRange(105, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), + getRandomNumberInRange(105, 125)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 125)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), + getRandomNumberInRange(90, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), + getRandomNumberInRange(90, 115)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), + getRandomNumberInRange(90, 110)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 95)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 90)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), + getRandomNumberInRange(80, 90)) + }))); + }); + asset.setId(UniqueIdentifierGenerator.generateId(asset.getName())); + asset = assetStorageService.merge(asset); + } + + GroundwaterSensorAsset groundwater1Asset = + createDemoGroundwaterAsset( + "Leuvehaven", environmentMonitor, new GeoJSONPoint(4.48413, 51.91431)); + GroundwaterSensorAsset groundwater2Asset = + createDemoGroundwaterAsset( + "Steiger", environmentMonitor, new GeoJSONPoint(4.482887, 51.920082)); + GroundwaterSensorAsset groundwater3Asset = + createDemoGroundwaterAsset( + "Stadhuis", environmentMonitor, new GeoJSONPoint(4.480876, 51.923212)); + + GroundwaterSensorAsset[] groundwaterArray = { + groundwater1Asset, groundwater2Asset, groundwater3Asset + }; + for (GroundwaterSensorAsset asset : groundwaterArray) { + asset.setManufacturer("Eijkelkamp"); + asset.setModel("TeleControlNet"); + asset + .getAttribute(GroundwaterSensorAsset.SOIL_TEMPERATURE) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 37), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 41), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 47), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 49), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 51), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 43), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 48), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 45), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 46), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 41), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 30), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 19), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 6) - } - ) - ) - ); - }); - consumption4Asset.setId(UniqueIdentifierGenerator.generateId(consumption4Asset.getName())); - consumption4Asset = assetStorageService.merge(consumption4Asset); - - // ### Oostelijk zwembad ### - - BuildingAsset building5Asset = new BuildingAsset("Oostelijk zwembad"); - building5Asset.setParent(energyManagement); - building5Asset.getAttributes().addOrReplace( - new Attribute<>(BuildingAsset.STREET, "Gerdesiaweg 480"), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3061 RA"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands"), - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.498048, 51.925770)) - ); - building5Asset.setId(UniqueIdentifierGenerator.generateId(building5Asset.getName() + "building")); - building5Asset = assetStorageService.merge(building5Asset); - - ElectricityConsumerAsset consumption5Asset = createDemoElectricityConsumerAsset("Consumption Zwembad", building5Asset, new GeoJSONPoint(4.498048, 51.925770)); - consumption5Asset.getAttribute(ElectricityConsumerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 12.2), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 12.0), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 11.8), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 11.7), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 11.7), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 11.9), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 12.8), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 13.5), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 13.9), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 15.2), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 15.3), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 15.5), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 15.5), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 15.4), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 15.2), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 15.2), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 14.6), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 14.2), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 13.8), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 13.4), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 12.8), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 12.3) + }))); + }); + asset + .getAttribute(GroundwaterSensorAsset.WATER_LEVEL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 17), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 16), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 24), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 35), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 33), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 33), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 31), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 37), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 37), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 35), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 24), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 19) - } - ) - ) - ); - }); - consumption5Asset.setId(UniqueIdentifierGenerator.generateId(consumption5Asset.getName())); - consumption5Asset = assetStorageService.merge(consumption5Asset); + new SimulatorReplayDatapoint( + midnight.get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), + getRandomNumberInRange(100, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), + getRandomNumberInRange(100, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), + getRandomNumberInRange(90, 110)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), + getRandomNumberInRange(100, 110)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), + getRandomNumberInRange(100, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), + getRandomNumberInRange(110, 120)) + }))); + }); + asset.setId(UniqueIdentifierGenerator.generateId(asset.getName())); + asset = assetStorageService.merge(asset); + } + + // ################################ Realm smartcity - Mobility and Safety + // ################################### - ElectricityProducerSolarAsset production5Asset = createDemoElectricitySolarProducerAsset("Solar Zwembad", building5Asset, new GeoJSONPoint(4.498281, 51.925507)); - production5Asset.getAttribute(ElectricityProducerAsset.POWER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( + Asset mobilityAndSafety = new ThingAsset("Mobility and safety"); + mobilityAndSafety.setRealm(this.realmCityName); + mobilityAndSafety.setId(UniqueIdentifierGenerator.generateId(mobilityAndSafety.getName())); + mobilityAndSafety = assetStorageService.merge(mobilityAndSafety); + + // ### Parking ### + + GroupAsset parkingGroupAsset = new GroupAsset("Parking group", ParkingAsset.class); + parkingGroupAsset.setParent(mobilityAndSafety); + parkingGroupAsset + .getAttributes() + .addOrReplace( + new Attribute<>("totalOccupancy", ValueType.POSITIVE_INTEGER) + .addMeta( + new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_PERCENTAGE)), new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), -1), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), -3), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), -8), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), -30), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), -44), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), -42), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), -41), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), -29), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), -19), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), -16), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), -11), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), -4), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), -3), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), -2), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0) - } - ) - ) - ); - }); - production5Asset.setEnergyExportTotal(23461d); - production5Asset.setPowerExportMax(76.2); - production5Asset.setEfficiencyExport(86); - production5Asset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH); - production5Asset.setManufacturer("S-Energy"); - production5Asset.setModel("SN260P-10"); - production5Asset.setPanelAzimuth(50); - production5Asset.setPanelPitch(15); - production5Asset.setId(UniqueIdentifierGenerator.generateId(production5Asset.getName())); - production5Asset = assetStorageService.merge(production5Asset); - - // ### Weather ### - HTTPAgent weatherHttpApiAgent = new HTTPAgent("Weather Agent"); - weatherHttpApiAgent.setParent(energyManagement); - weatherHttpApiAgent.setBaseURI("https://api.openweathermap.org/data/2.5/"); - - MultivaluedStringMap queryParams = new MultivaluedStringMap(); - queryParams.put("appid", Collections.singletonList("c3ecbf09be5267cd280676a01acd3360")); - queryParams.put("lat", Collections.singletonList("51.918849")); - queryParams.put("lon", Collections.singletonList("4.463250")); - queryParams.put("units", Collections.singletonList("metric")); - weatherHttpApiAgent.setRequestQueryParameters(queryParams); - - MultivaluedStringMap headers = new MultivaluedStringMap(); - headers.put("Accept", Collections.singletonList("application/json")); - weatherHttpApiAgent.setRequestHeaders(headers); - - weatherHttpApiAgent = assetStorageService.merge(weatherHttpApiAgent); - weatherHttpApiAgentId = weatherHttpApiAgent.getId(); - - WeatherAsset weather = new WeatherAsset("Weather"); - weather.setParent(energyManagement); - weather.setId(UniqueIdentifierGenerator.generateId(weather.getName())); - - HTTPAgentLink agentLink = new HTTPAgentLink(weatherHttpApiAgentId); - agentLink.setPath("weather"); - agentLink.setPollingMillis((int)halfHourInMillis); - - weather.getAttributes().addOrReplace( - new Attribute<>("currentWeather") - .addMeta( - new MetaItem<>(MetaItemType.AGENT_LINK, agentLink), - new MetaItem<>(MetaItemType.LABEL, "Open Weather Map API weather end point"), - new MetaItem<>(MetaItemType.READ_ONLY, true), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS, false), - new MetaItem<>(MetaItemType.RULE_STATE, false), - new MetaItem<>(MetaItemType.ATTRIBUTE_LINKS, new AttributeLink[] { - createWeatherApiAttributeLink(weather.getId(), "main", "temp", "temperature"), - createWeatherApiAttributeLink(weather.getId(), "main", "humidity", "humidity"), - createWeatherApiAttributeLink(weather.getId(), "wind", "speed", "windSpeed"), - createWeatherApiAttributeLink(weather.getId(), "wind", "deg", "windDirection") - }) - )); - weather.getAttribute("windSpeed").ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.STORE_DATA_POINTS), - new MetaItem<>(MetaItemType.RULE_STATE) - ); - }); - weather.getAttribute("temperature").ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.STORE_DATA_POINTS), - new MetaItem<>(MetaItemType.RULE_STATE) - ); - }); - weather.getAttribute("windDirection").ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.STORE_DATA_POINTS), - new MetaItem<>(MetaItemType.RULE_STATE) - ); - }); - weather.getAttribute("humidity").ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.STORE_DATA_POINTS), - new MetaItem<>(MetaItemType.RULE_STATE) - ); - }); - new Attribute<>(Asset.LOCATION, new GeoJSONPoint(4.463250, 51.918849)); - weather = assetStorageService.merge(weather); - - // ################################ Realm smartcity - Environment monitor ################################### - - Asset environmentMonitor = new ThingAsset("Environment monitor"); - environmentMonitor.setRealm(this.realmCityName); - environmentMonitor.setId(UniqueIdentifierGenerator.generateId(environmentMonitor.getName())); - environmentMonitor = assetStorageService.merge(environmentMonitor); - - EnvironmentSensorAsset environment1Asset = createDemoEnvironmentAsset("Oudehaven", environmentMonitor, new GeoJSONPoint(4.49313, 51.91885), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - EnvironmentSensorAsset environment2Asset = createDemoEnvironmentAsset("Kaappark", environmentMonitor, new GeoJSONPoint(4.480434, 51.899287), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - EnvironmentSensorAsset environment3Asset = createDemoEnvironmentAsset("Museumpark", environmentMonitor, new GeoJSONPoint(4.472457, 51.912047), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - EnvironmentSensorAsset environment4Asset = createDemoEnvironmentAsset("Eendrachtsplein", environmentMonitor, new GeoJSONPoint(4.473599, 51.916292), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - - EnvironmentSensorAsset[] environmentArray = {environment1Asset, environment2Asset, environment3Asset, environment4Asset}; - for (EnvironmentSensorAsset asset : environmentArray) { - asset.setManufacturer("Intemo"); - asset.setModel("Josene outdoor"); - asset.getAttribute(EnvironmentSensorAsset.OZONE).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), getRandomNumberInRange(80,90)), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), getRandomNumberInRange(75,90)), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), getRandomNumberInRange(75,90)), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), getRandomNumberInRange(75,90)), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), getRandomNumberInRange(75,95)), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), getRandomNumberInRange(75,95)), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), getRandomNumberInRange(75,95)), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), getRandomNumberInRange(80,95)), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), getRandomNumberInRange(80,95)), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), getRandomNumberInRange(80,110)), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), getRandomNumberInRange(85,110)), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), getRandomNumberInRange(85,115)), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), getRandomNumberInRange(85,115)), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), getRandomNumberInRange(85,115)), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), getRandomNumberInRange(105,120)), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), getRandomNumberInRange(105,125)), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), getRandomNumberInRange(110,125)), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), getRandomNumberInRange(90,120)), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), getRandomNumberInRange(90,115)), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), getRandomNumberInRange(90,110)), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), getRandomNumberInRange(80,95)), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), getRandomNumberInRange(80,95)), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), getRandomNumberInRange(80,90)), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), getRandomNumberInRange(80,90)) - } - ) - ) - ); + MetaItemType.CONSTRAINTS, + ValueConstraint.constraints( + new ValueConstraint.Min(0), new ValueConstraint.Max(100))), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS))); + parkingGroupAsset.setId(UniqueIdentifierGenerator.generateId(parkingGroupAsset.getName())); + parkingGroupAsset = assetStorageService.merge(parkingGroupAsset); + + ParkingAsset parking1Asset = + createDemoParkingAsset("Markthal", parkingGroupAsset, new GeoJSONPoint(4.48527, 51.91984)) + .setManufacturer("SKIDATA") + .setModel("Barrier.Gate"); + parking1Asset + .getAttribute(ParkingAsset.SPACES_OCCUPIED) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 37), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 31), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 39), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 47), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 53), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 165), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 301), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 417), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 442), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 489), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 467), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 490), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 438), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 457), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 402), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 379), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 336), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 257), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 204), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 112), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 75) + }))); }); - asset.setId(UniqueIdentifierGenerator.generateId(asset.getName())); - asset = assetStorageService.merge(asset); - } - - GroundwaterSensorAsset groundwater1Asset = createDemoGroundwaterAsset("Leuvehaven", environmentMonitor, new GeoJSONPoint(4.48413, 51.91431)); - GroundwaterSensorAsset groundwater2Asset = createDemoGroundwaterAsset("Steiger", environmentMonitor, new GeoJSONPoint(4.482887, 51.920082)); - GroundwaterSensorAsset groundwater3Asset = createDemoGroundwaterAsset("Stadhuis", environmentMonitor, new GeoJSONPoint(4.480876, 51.923212)); - - GroundwaterSensorAsset[] groundwaterArray = {groundwater1Asset, groundwater2Asset, groundwater3Asset}; - for (GroundwaterSensorAsset asset : groundwaterArray) { - asset.setManufacturer("Eijkelkamp"); - asset.setModel("TeleControlNet"); - asset.getAttribute(GroundwaterSensorAsset.SOIL_TEMPERATURE).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 12.2), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 12.0), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 11.8), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 11.7), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 11.7), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 11.9), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 12.8), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 13.5), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 13.9), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 15.2), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 15.3), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 15.5), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 15.5), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 15.4), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 15.2), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 15.2), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 14.6), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 14.2), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 13.8), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 13.4), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 12.8), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 12.3) - } - ) - ) - ); + parking1Asset.setPriceHourly(3.75); + parking1Asset.setPriceDaily(25.00); + parking1Asset.setSpacesTotal(512); + parking1Asset + .getAttribute(ParkingAsset.SPACES_TOTAL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta(new MetaItem<>(MetaItemType.RULE_STATE)); }); - asset.getAttribute(GroundwaterSensorAsset.WATER_LEVEL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), getRandomNumberInRange(100, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), getRandomNumberInRange(100, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), getRandomNumberInRange(90, 110)), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), getRandomNumberInRange(100, 110)), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), getRandomNumberInRange(100, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), getRandomNumberInRange(110, 120)) - } - ) - ) - ); + parking1Asset.setId(UniqueIdentifierGenerator.generateId(parking1Asset.getName())); + parking1Asset = assetStorageService.merge(parking1Asset); + + ParkingAsset parking2Asset = + createDemoParkingAsset("Lijnbaan", parkingGroupAsset, new GeoJSONPoint(4.47681, 51.91849)); + parking2Asset.setManufacturer("SKIDATA"); + parking2Asset.setModel("Barrier.Gate"); + parking2Asset + .getAttribute(ParkingAsset.SPACES_OCCUPIED) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 31), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 24), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 46), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 48), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 52), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 89), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 142), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 246), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 231), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 367), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 345), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 386), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 312), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 363), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 276), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 249), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 256), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 123), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 153), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 83), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 25) + }))); }); - asset.setId(UniqueIdentifierGenerator.generateId(asset.getName())); - asset = assetStorageService.merge(asset); - } - - // ################################ Realm smartcity - Mobility and Safety ################################### - - Asset mobilityAndSafety = new ThingAsset("Mobility and safety"); - mobilityAndSafety.setRealm(this.realmCityName); - mobilityAndSafety.setId(UniqueIdentifierGenerator.generateId(mobilityAndSafety.getName())); - mobilityAndSafety = assetStorageService.merge(mobilityAndSafety); - - // ### Parking ### - - GroupAsset parkingGroupAsset = new GroupAsset("Parking group", ParkingAsset.class); - parkingGroupAsset.setParent(mobilityAndSafety); - parkingGroupAsset.getAttributes().addOrReplace( - new Attribute<>("totalOccupancy", ValueType.POSITIVE_INTEGER) - .addMeta( - new MetaItem<>(MetaItemType.UNITS, Constants.units(Constants.UNITS_PERCENTAGE)), - new MetaItem<>(MetaItemType.CONSTRAINTS, ValueConstraint.constraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100))), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - )); - parkingGroupAsset.setId(UniqueIdentifierGenerator.generateId(parkingGroupAsset.getName())); - parkingGroupAsset = assetStorageService.merge(parkingGroupAsset); - - ParkingAsset parking1Asset = createDemoParkingAsset("Markthal", parkingGroupAsset, new GeoJSONPoint(4.48527, 51.91984)) - .setManufacturer("SKIDATA") - .setModel("Barrier.Gate"); - parking1Asset.getAttribute(ParkingAsset.SPACES_OCCUPIED).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 37), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 31), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 39), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 47), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 53), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 165), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 301), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 417), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 442), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 489), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 467), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 490), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 438), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 457), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 402), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 379), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 336), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 257), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 204), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 112), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 75) - } - ) - ) - ); - }); - parking1Asset.setPriceHourly(3.75); - parking1Asset.setPriceDaily(25.00); - parking1Asset.setSpacesTotal(512); - parking1Asset.getAttribute(ParkingAsset.SPACES_TOTAL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.RULE_STATE)); - }); - parking1Asset.setId(UniqueIdentifierGenerator.generateId(parking1Asset.getName())); - parking1Asset = assetStorageService.merge(parking1Asset); - - ParkingAsset parking2Asset = createDemoParkingAsset("Lijnbaan", parkingGroupAsset, new GeoJSONPoint(4.47681, 51.91849)); - parking2Asset.setManufacturer("SKIDATA"); - parking2Asset.setModel("Barrier.Gate"); - parking2Asset.getAttribute(ParkingAsset.SPACES_OCCUPIED).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 31), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 24), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 46), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 48), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 52), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 89), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 142), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 246), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 231), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 367), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 345), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 386), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 312), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 363), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 276), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 249), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 256), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 123), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 153), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 83), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 25) - } - ) - ) - ); - }); - parking2Asset.setPriceHourly(3.50); - parking2Asset.setPriceDaily(23.00); - parking2Asset.setSpacesTotal(390); - parking2Asset.getAttribute(ParkingAsset.SPACES_TOTAL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.RULE_STATE)); - }); - parking2Asset.setId(UniqueIdentifierGenerator.generateId(parking2Asset.getName())); - parking2Asset = assetStorageService.merge(parking2Asset); - - ParkingAsset parking3Asset = createDemoParkingAsset("Erasmusbrug", parkingGroupAsset, new GeoJSONPoint(4.48207, 51.91127)); - parking3Asset.setManufacturer("Kiestra"); - parking3Asset.setModel("Genius Rainbow"); - parking3Asset.getAttribute(ParkingAsset.SPACES_OCCUPIED).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 25), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 18), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 29), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 119), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 257), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 357), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 368), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 362), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 349), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 370), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 367), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 355), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 314), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 254), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 215), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 165), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 149), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 108), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 47) - } - ) - ) - ); - }); - parking3Asset.setPriceHourly(3.40); - parking3Asset.setPriceDaily(20.00); - parking3Asset.setSpacesTotal(373); - parking3Asset.getAttribute(ParkingAsset.SPACES_TOTAL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>(MetaItemType.RULE_STATE)); + parking2Asset.setPriceHourly(3.50); + parking2Asset.setPriceDaily(23.00); + parking2Asset.setSpacesTotal(390); + parking2Asset + .getAttribute(ParkingAsset.SPACES_TOTAL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta(new MetaItem<>(MetaItemType.RULE_STATE)); }); - parking3Asset.setId(UniqueIdentifierGenerator.generateId(parking3Asset.getName())); - parking3Asset = assetStorageService.merge(parking3Asset); - - // ### Crowd control ### - - ThingAsset assetAreaStation = new ThingAsset("Stationsplein"); - assetAreaStation.setParent(mobilityAndSafety) - .getAttributes().addOrReplace( - new Attribute<>(Asset.LOCATION, STATIONSPLEIN_LOCATION), - new Attribute<>(BuildingAsset.POSTAL_CODE, "3013 AK"), - new Attribute<>(BuildingAsset.CITY, "Rotterdam"), - new Attribute<>(BuildingAsset.COUNTRY, "Netherlands") - ); - assetAreaStation.setId(UniqueIdentifierGenerator.generateId(assetAreaStation.getName())); - assetAreaStation = assetStorageService.merge(assetAreaStation); - area1Id = assetAreaStation.getId(); - - PeopleCounterAsset peopleCounter1Asset = createDemoPeopleCounterAsset("People Counter South", assetAreaStation, new GeoJSONPoint(4.470147, 51.923171), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - peopleCounter1Asset.setManufacturer("ViNotion"); - peopleCounter1Asset.setModel("ViSense"); - peopleCounter1Asset.getAttribute(PeopleCounterAsset.COUNT_GROWTH_PER_MINUTE).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0.2), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0.3), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0.1), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0.0), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0.2), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0.4), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0.5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0.7), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 1.8), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 2.1), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 2.4), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 1.9), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 1.8), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 2.1), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 1.8), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 1.7), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 2.3), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 3.1), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 2.8), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 2.2), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 1.6), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 1.7), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 1.1), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0.8) - } - ) - ) - ); - }); - peopleCounter1Asset.setId(UniqueIdentifierGenerator.generateId(peopleCounter1Asset.getName())); - peopleCounter1Asset = assetStorageService.merge(peopleCounter1Asset); - - Asset peopleCounter2Asset = createDemoPeopleCounterAsset("People Counter North", assetAreaStation, new GeoJSONPoint(4.469329, 51.923700), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId)); - peopleCounter2Asset.setManufacturer("Axis"); - peopleCounter2Asset.setModel("P1375-E"); - peopleCounter2Asset.getAttribute(PeopleCounterAsset.COUNT_GROWTH_PER_MINUTE).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0.3), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), 0.2), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), 0.3), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), 0.1), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), 0.0), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), 0.3), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 0.7), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 0.6), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 1.9), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 2.2), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 2.8), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 1.6), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 1.9), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 2.2), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 1.9), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 1.6), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 2.4), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 3.2), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 2.9), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 2.3), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 1.7), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 1.4), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 1.2), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 0.7) - } - ) - ) - ); - }); - peopleCounter2Asset.setId(UniqueIdentifierGenerator.generateId(peopleCounter2Asset.getName())); - peopleCounter2Asset = assetStorageService.merge(peopleCounter2Asset); - - MicrophoneAsset microphone1Asset = createDemoMicrophoneAsset("Microphone South", assetAreaStation, new GeoJSONPoint(4.470362, 51.923201), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), getRandomNumberInRange(50,60)), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), getRandomNumberInRange(60,70)), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), getRandomNumberInRange(60,70)), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), getRandomNumberInRange(50,60)) - } - )); - microphone1Asset.setManufacturer("Sorama"); - microphone1Asset.setModel("CAM1K"); - microphone1Asset.setId(UniqueIdentifierGenerator.generateId(microphone1Asset.getName())); - microphone1Asset = assetStorageService.merge(microphone1Asset); - - MicrophoneAsset microphone2Asset = createDemoMicrophoneAsset("Microphone North", assetAreaStation, new GeoJSONPoint(4.469190, 51.923786), () -> - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[] { - new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), getRandomNumberInRange(50,60)), - new SimulatorReplayDatapoint(midnight.plusHours(1).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(2).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(3).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(4).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(5).get(SECOND_OF_DAY), getRandomNumberInRange(45,50)), - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), getRandomNumberInRange(50,55)), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), getRandomNumberInRange(60,70)), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), getRandomNumberInRange(55,60)), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), getRandomNumberInRange(60,70)), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), getRandomNumberInRange(60,65)), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), getRandomNumberInRange(50,60)) - } - )); - microphone2Asset.setManufacturer("Sorama"); - microphone2Asset.setModel("CAM1K"); - microphone2Asset.setId(UniqueIdentifierGenerator.generateId(microphone2Asset.getName())); - microphone2Asset = assetStorageService.merge(microphone2Asset); - - LightAsset lightStation1Asset = createDemoLightAsset("Station Light NW", assetAreaStation, new GeoJSONPoint(4.468874, 51.923881)); - lightStation1Asset.setManufacturer("Philips"); - lightStation1Asset.setModel("CityTouch"); - lightStation1Asset.setId(UniqueIdentifierGenerator.generateId(lightStation1Asset.getName())); - lightStation1Asset = assetStorageService.merge(lightStation1Asset); - - LightAsset lightStation2Asset = createDemoLightAsset("Station Light NE", assetAreaStation, new GeoJSONPoint(4.470539, 51.923991)); - lightStation2Asset.setManufacturer("Philips"); - lightStation2Asset.setModel("CityTouch"); - lightStation2Asset.setId(UniqueIdentifierGenerator.generateId(lightStation2Asset.getName())); - lightStation2Asset = assetStorageService.merge(lightStation2Asset); - - LightAsset lightStation3Asset = createDemoLightAsset("Station Light S", assetAreaStation, new GeoJSONPoint(4.470558, 51.923186)); - lightStation3Asset.setManufacturer("Philips"); - lightStation3Asset.setModel("CityTouch"); - lightStation3Asset.setId(UniqueIdentifierGenerator.generateId(lightStation3Asset.getName())); - lightStation3Asset = assetStorageService.merge(lightStation3Asset); - - // ### Lighting controller ### - - LightAsset lightingControllerOPAsset = createDemoLightControllerAsset("Lighting Noordereiland", mobilityAndSafety, new GeoJSONPoint(4.496177, 51.915060)); - lightingControllerOPAsset.setManufacturer("Pharos"); - lightingControllerOPAsset.setModel("LPC X"); - lightingControllerOPAsset.setId(UniqueIdentifierGenerator.generateId(lightingControllerOPAsset.getName())); - lightingControllerOPAsset = assetStorageService.merge(lightingControllerOPAsset); - - LightAsset lightOP1Asset = createDemoLightAsset("Ons Park 1", lightingControllerOPAsset, new GeoJSONPoint(4.49626, 51.91516)); - lightOP1Asset.setManufacturer("Schréder"); - lightOP1Asset.setModel("Axia 2"); - lightOP1Asset.setId(UniqueIdentifierGenerator.generateId(lightOP1Asset.getName())); - lightOP1Asset = assetStorageService.merge(lightOP1Asset); - - LightAsset lightOP2Asset = createDemoLightAsset("Ons Park 2", lightingControllerOPAsset, new GeoJSONPoint(4.49705, 51.91549)); - lightOP2Asset.setManufacturer("Schréder"); - lightOP2Asset.setModel("Axia 2"); - lightOP2Asset.setId(UniqueIdentifierGenerator.generateId(lightOP2Asset.getName())); - lightOP2Asset = assetStorageService.merge(lightOP2Asset); - - LightAsset lightOP3Asset = createDemoLightAsset("Ons Park 3", lightingControllerOPAsset, new GeoJSONPoint(4.49661, 51.91495)); - lightOP3Asset.setManufacturer("Schréder"); - lightOP3Asset.setModel("Axia 2"); - lightOP3Asset.setId(UniqueIdentifierGenerator.generateId(lightOP3Asset.getName())); - lightOP3Asset = assetStorageService.merge(lightOP3Asset); - - LightAsset lightOP4Asset = createDemoLightAsset("Ons Park 4", lightingControllerOPAsset, new GeoJSONPoint(4.49704, 51.91520)); - lightOP4Asset.setManufacturer("Schréder"); - lightOP4Asset.setModel("Axia 2"); - lightOP4Asset.setId(UniqueIdentifierGenerator.generateId(lightOP4Asset.getName())); - lightOP4Asset = assetStorageService.merge(lightOP4Asset); - - LightAsset lightOP5Asset = createDemoLightAsset("Ons Park 5", lightingControllerOPAsset, new GeoJSONPoint(4.49758, 51.91440)); - lightOP5Asset.setManufacturer("Schréder"); - lightOP5Asset.setModel("Axia 2"); - lightOP5Asset.setId(UniqueIdentifierGenerator.generateId(lightOP5Asset.getName())); - lightOP5Asset = assetStorageService.merge(lightOP5Asset); - - LightAsset lightOP6Asset = createDemoLightAsset("Ons Park 6", lightingControllerOPAsset, new GeoJSONPoint(4.49786, 51.91452)); - lightOP6Asset.setManufacturer("Schréder"); - lightOP6Asset.setModel("Axia 2"); - lightOP6Asset.setId(UniqueIdentifierGenerator.generateId(lightOP6Asset.getName())); - lightOP6Asset = assetStorageService.merge(lightOP6Asset); - - // ### Ships ### - - GroupAsset shipGroupAsset = new GroupAsset("Ship group", ShipAsset.class); - shipGroupAsset.setParent(mobilityAndSafety); - shipGroupAsset.setId(UniqueIdentifierGenerator.generateId(shipGroupAsset.getName())); - shipGroupAsset = assetStorageService.merge(shipGroupAsset); - - ShipAsset ship1Asset = createDemoShipAsset("Hotel New York", shipGroupAsset, new GeoJSONPoint(4.482669, 51.916436)); - ship1Asset.setLength(12); - ship1Asset.setShipType("Passenger"); - ship1Asset.setIMONumber(9183527); - ship1Asset.setMSSINumber(244650537); - ship1Asset.getAttribute(ShipAsset.DIRECTION).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 187), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 7) - } - ) - ) - ); - }); - ship1Asset.getAttribute(Asset.LOCATION).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(smartcitySimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(5).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(10).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(15).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(20).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(25).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), new GeoJSONPoint(4.484374, 51.903518)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(35).get(SECOND_OF_DAY), new GeoJSONPoint(4.479779, 51.904404)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(40).get(SECOND_OF_DAY), new GeoJSONPoint(4.482914, 51.906769)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(45).get(SECOND_OF_DAY), new GeoJSONPoint(4.486156, 51.908570)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(50).get(SECOND_OF_DAY), new GeoJSONPoint(4.483362, 51.911897)), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(55).get(SECOND_OF_DAY), new GeoJSONPoint(4.482669, 51.916436)) - } - ) - ) - ); - }); - ship1Asset.setId(UniqueIdentifierGenerator.generateId(ship1Asset.getName())); - ship1Asset = assetStorageService.merge(ship1Asset); - - // ################################ Realm Manufaturer Simulator ################################### - - SimulatorAgent manufacturerSimulatorAgent = new SimulatorAgent("Simulator"); - manufacturerSimulatorAgent.setRealm(this.realmManufacturerName); - - manufacturerSimulatorAgent = assetStorageService.merge(manufacturerSimulatorAgent); - manufacturerSimulatorAgentId = manufacturerSimulatorAgent.getId(); - - // ################################ Manufacturer realm assets ################################### - - // ### Greenhouse equipment distribution ### - - Asset distributor1 = new ThingAsset("GreenEquipment Distribution"); - distributor1.setRealm(this.realmManufacturerName); - distributor1.setId(UniqueIdentifierGenerator.generateId(distributor1.getName())); - distributor1 = assetStorageService.merge(distributor1); - - BuildingAsset vegetablesAndMore = new BuildingAsset("Vegetables & More"); - vegetablesAndMore.setParent(distributor1); - vegetablesAndMore.setId(UniqueIdentifierGenerator.generateId(vegetablesAndMore.getName())); - vegetablesAndMore = assetStorageService.merge(vegetablesAndMore); - - HarvestRobotAsset harvestRobot1 = createDemoHarvestRobotAsset("Robot 1", vegetablesAndMore, new GeoJSONPoint(4.279166, 51.978078), OperationMode.CUTTING, VegetableType.BELL_PEPPER, 26, 45, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - harvestRobot1.setId(UniqueIdentifierGenerator.generateId(harvestRobot1.getName())); - harvestRobot1 = assetStorageService.merge(harvestRobot1); - HarvestRobotAsset harvestRobot2 = createDemoHarvestRobotAsset("Robot 2", vegetablesAndMore, new GeoJSONPoint(4.277852, 51.977487), OperationMode.SCANNING, VegetableType.BELL_PEPPER, 26, 45, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - harvestRobot2.setId(UniqueIdentifierGenerator.generateId(harvestRobot2.getName())); - harvestRobot2 = assetStorageService.merge(harvestRobot2); - SoilSensorAsset soilSensor1 = createDemoSoilSensorAsset("Water sensor", vegetablesAndMore, new GeoJSONPoint(4.279010, 51.977391), 41, 53, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - soilSensor1.setId(UniqueIdentifierGenerator.generateId(soilSensor1.getName())); - soilSensor1 = assetStorageService.merge(soilSensor1); - - BuildingAsset moreauHorticulture = new BuildingAsset("Qoreau Horticulture"); - moreauHorticulture.setParent(distributor1); - moreauHorticulture.setId(UniqueIdentifierGenerator.generateId(moreauHorticulture.getName())); - moreauHorticulture = assetStorageService.merge(moreauHorticulture); - - IrrigationAsset irrigation1 = createDemoIrrigationAsset("Soil drip 1", moreauHorticulture, new GeoJSONPoint(4.311493, 51.961554), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - IrrigationAsset irrigation2 = createDemoIrrigationAsset("Soil drip 2", moreauHorticulture, new GeoJSONPoint(4.310893, 51.961354), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - IrrigationAsset irrigation3 = createDemoIrrigationAsset("Soil drip 3", moreauHorticulture, new GeoJSONPoint(4.310293, 51.961154), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - IrrigationAsset irrigation4 = createDemoIrrigationAsset("Soil drip 4", moreauHorticulture, new GeoJSONPoint(4.309617, 51.960975), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - IrrigationAsset irrigation5 = createDemoIrrigationAsset("Soil drip 5", moreauHorticulture, new GeoJSONPoint(4.309153, 51.960901), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation1.setId(UniqueIdentifierGenerator.generateId(irrigation1.getName())); - irrigation1 = assetStorageService.merge(irrigation1); - irrigation2.setId(UniqueIdentifierGenerator.generateId(irrigation2.getName())); - irrigation2 = assetStorageService.merge(irrigation2); - irrigation3.setId(UniqueIdentifierGenerator.generateId(irrigation3.getName())); - irrigation3 = assetStorageService.merge(irrigation3); - irrigation4.setId(UniqueIdentifierGenerator.generateId(irrigation4.getName())); - irrigation4 = assetStorageService.merge(irrigation4); - irrigation5.setId(UniqueIdentifierGenerator.generateId(irrigation5.getName())); - irrigation5 = assetStorageService.merge(irrigation5); - SoilSensorAsset soilSensor2 = createDemoSoilSensorAsset("Soil measurement", moreauHorticulture, new GeoJSONPoint(4.310436, 51.961354), 27, 35, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - soilSensor2.setId(UniqueIdentifierGenerator.generateId(soilSensor2.getName())); - soilSensor2 = assetStorageService.merge(soilSensor2); - - BuildingAsset paprika = new BuildingAsset("Paprika Perfect BV"); - paprika.setParent(distributor1); - paprika.getAttributes().getOrCreate("flowPerMeter", ValueType.NUMBER) - .addMeta(new MetaItem<>(READ_ONLY), new MetaItem<>(RULE_STATE), new MetaItem<>(STORE_DATA_POINTS), - new MetaItem<>(UNITS, Constants.units(UNITS_LITRE, UNITS_PER, UNITS_METRE, UNITS_SQUARED, UNITS_HOUR))); - paprika.getAttribute(BuildingAsset.AREA).ifPresent(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(RULE_STATE)) - .setValue(1800);}); - paprika.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - paprika.setId(UniqueIdentifierGenerator.generateId(paprika.getName())); - paprikaId = paprika.getId(); - paprika = assetStorageService.merge(paprika); - - HarvestRobotAsset harvestRobot5 = createDemoHarvestRobotAsset("Harvest Robot 1", paprika, new GeoJSONPoint(4.282415, 51.975951), OperationMode.UNLOADING, VegetableType.TOMATO, 26, 45, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - harvestRobot5.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - harvestRobot5.getAttribute(HarvestRobotAsset.HARVESTED_SESSION).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 814), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 814), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 815), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 816), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 816), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 816), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 816), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 817), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 818), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 819), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 820), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 821), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 0), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 34), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 86), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 112), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 156), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 289), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 348), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 456), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 516), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 624), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 684), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 713), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 762), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 787), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 792), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 798), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 804), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 808), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 812), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 813), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 813), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 814), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 814) - } - ) - ) - ); - }); - harvestRobot5.getAttribute(HarvestRobotAsset.SPEED).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 8), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 1), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 1), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1) - } - ) - ) - ); - }); - harvestRobot5.setId(UniqueIdentifierGenerator.generateId(harvestRobot5.getName())); - harvestRobot5Id = harvestRobot5.getId(); - harvestRobot5 = assetStorageService.merge(harvestRobot5); - - IrrigationAsset irrigation9 = createDemoIrrigationAsset("Irrigation 1", paprika, new GeoJSONPoint(4.283731, 51.976526), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation9.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - irrigation9.getAttribute(IrrigationAsset.TANK_LEVEL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 1000), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 995), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 990), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 986), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 980), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 975), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 970), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 965), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 960), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 956), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 950), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 945), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 941), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 936), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 929), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 925), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 919), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 914), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 910), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 906), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 900), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 895), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 891), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 1200), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 1195), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 1190), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 1184), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 1173), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 1145), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 1105), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 1074), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 1032), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 1027), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 1022), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 1017), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1006) - } - ) - ) - ); - }); - irrigation9.getAttribute(IrrigationAsset.FLOW_WATER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 10.5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 10.8), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 11.3), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 11.9), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 12.5), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 10.8), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 9.6), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 9.4), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 10.4), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 11.8), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 12.3), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 11.3), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 11.7), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 10.2), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 10.3), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 10.1), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 10), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 9), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 10.2), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 10.1) - } - ) - ) - ); - }); - irrigation9.setId(UniqueIdentifierGenerator.generateId(irrigation9.getName())); - irrigation9Id = irrigation9.getId(); - irrigation9 = assetStorageService.merge(irrigation9); - IrrigationAsset irrigation10 = createDemoIrrigationAsset("Irrigation 2", paprika, new GeoJSONPoint(4.285047, 51.975652), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation10.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - irrigation10.getAttribute(IrrigationAsset.TANK_LEVEL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 1200), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 1193), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 1185), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 1178), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 1170), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1162), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 1155), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1148), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 1140), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 1132), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 1124), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 1117), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 1110), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 1102), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 1093), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 1085), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 1076), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 1070), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 1063), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 1055), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 1048), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 1041), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 1035), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 1028), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 1022), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 1021), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 1018), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 1015), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 1010), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 1002), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 995), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 1050), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 1150), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 1290), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 1250), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1210) - } - ) - ) - ); - }); - irrigation10.getAttribute(IrrigationAsset.FLOW_WATER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 12.5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 12.6), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 14.2), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 13.3), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 14.5), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 12.8), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 13.2), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 11.6), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 11.4), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 11.2), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 12.3), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 13.8), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 12.3), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 13.1), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 13.2), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 12.9), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 13.7), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 11.8), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 12.6), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 12.2), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 12.1) - } - ) - ) - ); - }); - irrigation10.setId(UniqueIdentifierGenerator.generateId(irrigation10.getName())); - irrigation10Id = irrigation10.getId(); - irrigation10 = assetStorageService.merge(irrigation10); - IrrigationAsset irrigation11 = createDemoIrrigationAsset("Irrigation 3", paprika, new GeoJSONPoint(4.286504, 51.974613), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation11.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - irrigation11.getAttribute(IrrigationAsset.TANK_LEVEL).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 1210), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 1190), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 1180), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 1172), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 1160), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1152), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 1145), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1141), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 1132), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 1125), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 1120), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 1110), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 1100), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 1102), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 1083), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 1071), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 1066), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 1060), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 1055), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 1035), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 1028), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 1021), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 1015), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 1006), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 1002), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 1001), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 997), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 985), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 985), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 982), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 975), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 1040), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 1200), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 1250), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 1250), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1220) - } - ) - ) - ); - }); - irrigation11.getAttribute(IrrigationAsset.FLOW_WATER).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 13.5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 13.5), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 14.2), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 13.1), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 15.2), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 15.4), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 13.8), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 14.2), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 12.6), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 12.2), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 12.2), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 13.2), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 13.4), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 14.7), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 13.2), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 14), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 14.1), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 14.6), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 12.9), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 13), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 13.4), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 13.1), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 12.9), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 12.1), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 13.2), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 13.1) - } - ) - ) - ); - }); - irrigation11.setId(UniqueIdentifierGenerator.generateId(irrigation11.getName())); - irrigation11Id = irrigation11.getId(); - irrigation11 = assetStorageService.merge(irrigation11); - - SoilSensorAsset soilSensor4 = createDemoSoilSensorAsset("Soil sensor", paprika, new GeoJSONPoint(4.285034, 51.973881), 34, 47, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - soilSensor4.getAttributes().stream().forEach(assetAttribute -> { - assetAttribute.addMeta(new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE));}); - soilSensor4.getAttribute(SoilSensorAsset.SALINITY).ifPresent(assetAttribute -> { - assetAttribute.addMeta( - new MetaItem<>( - MetaItemType.AGENT_LINK, - new SimulatorAgentLink(manufacturerSimulatorAgentId).setSchedule(createDailySchedule()).setReplayData( - new SimulatorReplayDatapoint[]{ - new SimulatorReplayDatapoint(midnight.plusHours(6).get(SECOND_OF_DAY), 2), - new SimulatorReplayDatapoint(midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(7).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 12), - new SimulatorReplayDatapoint(midnight.plusHours(8).get(SECOND_OF_DAY), 17), - new SimulatorReplayDatapoint(midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(9).get(SECOND_OF_DAY), 28), - new SimulatorReplayDatapoint(midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 33), - new SimulatorReplayDatapoint(midnight.plusHours(10).get(SECOND_OF_DAY), 32), - new SimulatorReplayDatapoint(midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 38), - new SimulatorReplayDatapoint(midnight.plusHours(11).get(SECOND_OF_DAY), 36), - new SimulatorReplayDatapoint(midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 31), - new SimulatorReplayDatapoint(midnight.plusHours(12).get(SECOND_OF_DAY), 25), - new SimulatorReplayDatapoint(midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 28), - new SimulatorReplayDatapoint(midnight.plusHours(13).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(14).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 23), - new SimulatorReplayDatapoint(midnight.plusHours(15).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(16).get(SECOND_OF_DAY), 20), - new SimulatorReplayDatapoint(midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 18), - new SimulatorReplayDatapoint(midnight.plusHours(17).get(SECOND_OF_DAY), 21), - new SimulatorReplayDatapoint(midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 22), - new SimulatorReplayDatapoint(midnight.plusHours(18).get(SECOND_OF_DAY), 19), - new SimulatorReplayDatapoint(midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 20), - new SimulatorReplayDatapoint(midnight.plusHours(19).get(SECOND_OF_DAY), 15), - new SimulatorReplayDatapoint(midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 11), - new SimulatorReplayDatapoint(midnight.plusHours(20).get(SECOND_OF_DAY), 6), - new SimulatorReplayDatapoint(midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 7), - new SimulatorReplayDatapoint(midnight.plusHours(21).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 5), - new SimulatorReplayDatapoint(midnight.plusHours(22).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 4), - new SimulatorReplayDatapoint(midnight.plusHours(23).get(SECOND_OF_DAY), 3), - new SimulatorReplayDatapoint(midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1) - } - ) - ) - ); + parking2Asset.setId(UniqueIdentifierGenerator.generateId(parking2Asset.getName())); + parking2Asset = assetStorageService.merge(parking2Asset); + + ParkingAsset parking3Asset = + createDemoParkingAsset( + "Erasmusbrug", parkingGroupAsset, new GeoJSONPoint(4.48207, 51.91127)); + parking3Asset.setManufacturer("Kiestra"); + parking3Asset.setModel("Genius Rainbow"); + parking3Asset + .getAttribute(ParkingAsset.SPACES_OCCUPIED) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 25), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 18), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 29), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 119), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 257), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 357), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 368), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 362), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 349), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 370), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 367), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 355), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 314), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 254), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 215), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 165), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 149), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 108), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 47) + }))); + }); + parking3Asset.setPriceHourly(3.40); + parking3Asset.setPriceDaily(20.00); + parking3Asset.setSpacesTotal(373); + parking3Asset + .getAttribute(ParkingAsset.SPACES_TOTAL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta(new MetaItem<>(MetaItemType.RULE_STATE)); + }); + parking3Asset.setId(UniqueIdentifierGenerator.generateId(parking3Asset.getName())); + parking3Asset = assetStorageService.merge(parking3Asset); + + // ### Crowd control ### + + ThingAsset assetAreaStation = new ThingAsset("Stationsplein"); + assetAreaStation + .setParent(mobilityAndSafety) + .getAttributes() + .addOrReplace( + new Attribute<>(Asset.LOCATION, STATIONSPLEIN_LOCATION), + new Attribute<>(BuildingAsset.POSTAL_CODE, "3013 AK"), + new Attribute<>(BuildingAsset.CITY, "Rotterdam"), + new Attribute<>(BuildingAsset.COUNTRY, "Netherlands")); + assetAreaStation.setId(UniqueIdentifierGenerator.generateId(assetAreaStation.getName())); + assetAreaStation = assetStorageService.merge(assetAreaStation); + area1Id = assetAreaStation.getId(); + + PeopleCounterAsset peopleCounter1Asset = + createDemoPeopleCounterAsset( + "People Counter South", + assetAreaStation, + new GeoJSONPoint(4.470147, 51.923171), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + peopleCounter1Asset.setManufacturer("ViNotion"); + peopleCounter1Asset.setModel("ViSense"); + peopleCounter1Asset + .getAttribute(PeopleCounterAsset.COUNT_GROWTH_PER_MINUTE) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0.2), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0.3), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0.1), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0.0), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0.2), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0.4), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0.5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 0.7), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 1.8), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 2.1), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 2.4), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 1.9), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 1.8), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 2.1), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 1.8), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 1.7), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 2.3), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 3.1), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 2.8), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 2.2), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 1.6), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 1.7), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 1.1), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0.8) + }))); + }); + peopleCounter1Asset.setId(UniqueIdentifierGenerator.generateId(peopleCounter1Asset.getName())); + peopleCounter1Asset = assetStorageService.merge(peopleCounter1Asset); + + Asset peopleCounter2Asset = + createDemoPeopleCounterAsset( + "People Counter North", + assetAreaStation, + new GeoJSONPoint(4.469329, 51.923700), + () -> new SimulatorAgentLink(smartcitySimulatorAgentId)); + peopleCounter2Asset.setManufacturer("Axis"); + peopleCounter2Asset.setModel("P1375-E"); + peopleCounter2Asset + .getAttribute(PeopleCounterAsset.COUNT_GROWTH_PER_MINUTE) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint(midnight.get(SECOND_OF_DAY), 0.3), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), 0.2), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), 0.3), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), 0.1), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), 0.0), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), 0.3), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 0.7), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 0.6), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 1.9), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 2.2), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 2.8), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 1.6), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 1.9), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 2.2), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 1.9), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 1.6), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 2.4), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 3.2), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 2.9), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 2.3), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 1.7), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 1.4), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 1.2), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 0.7) + }))); + }); + peopleCounter2Asset.setId(UniqueIdentifierGenerator.generateId(peopleCounter2Asset.getName())); + peopleCounter2Asset = assetStorageService.merge(peopleCounter2Asset); + + MicrophoneAsset microphone1Asset = + createDemoMicrophoneAsset( + "Microphone South", + assetAreaStation, + new GeoJSONPoint(4.470362, 51.923201), + () -> + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.get(SECOND_OF_DAY), getRandomNumberInRange(50, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 70)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 70)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 60)) + })); + microphone1Asset.setManufacturer("Sorama"); + microphone1Asset.setModel("CAM1K"); + microphone1Asset.setId(UniqueIdentifierGenerator.generateId(microphone1Asset.getName())); + microphone1Asset = assetStorageService.merge(microphone1Asset); + + MicrophoneAsset microphone2Asset = + createDemoMicrophoneAsset( + "Microphone North", + assetAreaStation, + new GeoJSONPoint(4.469190, 51.923786), + () -> + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.get(SECOND_OF_DAY), getRandomNumberInRange(50, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(1).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(2).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(3).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(4).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(5).get(SECOND_OF_DAY), + getRandomNumberInRange(45, 50)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 55)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 70)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), + getRandomNumberInRange(55, 60)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 70)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), + getRandomNumberInRange(60, 65)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), + getRandomNumberInRange(50, 60)) + })); + microphone2Asset.setManufacturer("Sorama"); + microphone2Asset.setModel("CAM1K"); + microphone2Asset.setId(UniqueIdentifierGenerator.generateId(microphone2Asset.getName())); + microphone2Asset = assetStorageService.merge(microphone2Asset); + + LightAsset lightStation1Asset = + createDemoLightAsset( + "Station Light NW", assetAreaStation, new GeoJSONPoint(4.468874, 51.923881)); + lightStation1Asset.setManufacturer("Philips"); + lightStation1Asset.setModel("CityTouch"); + lightStation1Asset.setId(UniqueIdentifierGenerator.generateId(lightStation1Asset.getName())); + lightStation1Asset = assetStorageService.merge(lightStation1Asset); + + LightAsset lightStation2Asset = + createDemoLightAsset( + "Station Light NE", assetAreaStation, new GeoJSONPoint(4.470539, 51.923991)); + lightStation2Asset.setManufacturer("Philips"); + lightStation2Asset.setModel("CityTouch"); + lightStation2Asset.setId(UniqueIdentifierGenerator.generateId(lightStation2Asset.getName())); + lightStation2Asset = assetStorageService.merge(lightStation2Asset); + + LightAsset lightStation3Asset = + createDemoLightAsset( + "Station Light S", assetAreaStation, new GeoJSONPoint(4.470558, 51.923186)); + lightStation3Asset.setManufacturer("Philips"); + lightStation3Asset.setModel("CityTouch"); + lightStation3Asset.setId(UniqueIdentifierGenerator.generateId(lightStation3Asset.getName())); + lightStation3Asset = assetStorageService.merge(lightStation3Asset); + + // ### Lighting controller ### + + LightAsset lightingControllerOPAsset = + createDemoLightControllerAsset( + "Lighting Noordereiland", mobilityAndSafety, new GeoJSONPoint(4.496177, 51.915060)); + lightingControllerOPAsset.setManufacturer("Pharos"); + lightingControllerOPAsset.setModel("LPC X"); + lightingControllerOPAsset.setId( + UniqueIdentifierGenerator.generateId(lightingControllerOPAsset.getName())); + lightingControllerOPAsset = assetStorageService.merge(lightingControllerOPAsset); + + LightAsset lightOP1Asset = + createDemoLightAsset( + "Ons Park 1", lightingControllerOPAsset, new GeoJSONPoint(4.49626, 51.91516)); + lightOP1Asset.setManufacturer("Schréder"); + lightOP1Asset.setModel("Axia 2"); + lightOP1Asset.setId(UniqueIdentifierGenerator.generateId(lightOP1Asset.getName())); + lightOP1Asset = assetStorageService.merge(lightOP1Asset); + + LightAsset lightOP2Asset = + createDemoLightAsset( + "Ons Park 2", lightingControllerOPAsset, new GeoJSONPoint(4.49705, 51.91549)); + lightOP2Asset.setManufacturer("Schréder"); + lightOP2Asset.setModel("Axia 2"); + lightOP2Asset.setId(UniqueIdentifierGenerator.generateId(lightOP2Asset.getName())); + lightOP2Asset = assetStorageService.merge(lightOP2Asset); + + LightAsset lightOP3Asset = + createDemoLightAsset( + "Ons Park 3", lightingControllerOPAsset, new GeoJSONPoint(4.49661, 51.91495)); + lightOP3Asset.setManufacturer("Schréder"); + lightOP3Asset.setModel("Axia 2"); + lightOP3Asset.setId(UniqueIdentifierGenerator.generateId(lightOP3Asset.getName())); + lightOP3Asset = assetStorageService.merge(lightOP3Asset); + + LightAsset lightOP4Asset = + createDemoLightAsset( + "Ons Park 4", lightingControllerOPAsset, new GeoJSONPoint(4.49704, 51.91520)); + lightOP4Asset.setManufacturer("Schréder"); + lightOP4Asset.setModel("Axia 2"); + lightOP4Asset.setId(UniqueIdentifierGenerator.generateId(lightOP4Asset.getName())); + lightOP4Asset = assetStorageService.merge(lightOP4Asset); + + LightAsset lightOP5Asset = + createDemoLightAsset( + "Ons Park 5", lightingControllerOPAsset, new GeoJSONPoint(4.49758, 51.91440)); + lightOP5Asset.setManufacturer("Schréder"); + lightOP5Asset.setModel("Axia 2"); + lightOP5Asset.setId(UniqueIdentifierGenerator.generateId(lightOP5Asset.getName())); + lightOP5Asset = assetStorageService.merge(lightOP5Asset); + + LightAsset lightOP6Asset = + createDemoLightAsset( + "Ons Park 6", lightingControllerOPAsset, new GeoJSONPoint(4.49786, 51.91452)); + lightOP6Asset.setManufacturer("Schréder"); + lightOP6Asset.setModel("Axia 2"); + lightOP6Asset.setId(UniqueIdentifierGenerator.generateId(lightOP6Asset.getName())); + lightOP6Asset = assetStorageService.merge(lightOP6Asset); + + // ### Ships ### + + GroupAsset shipGroupAsset = new GroupAsset("Ship group", ShipAsset.class); + shipGroupAsset.setParent(mobilityAndSafety); + shipGroupAsset.setId(UniqueIdentifierGenerator.generateId(shipGroupAsset.getName())); + shipGroupAsset = assetStorageService.merge(shipGroupAsset); + + ShipAsset ship1Asset = + createDemoShipAsset( + "Hotel New York", shipGroupAsset, new GeoJSONPoint(4.482669, 51.916436)); + ship1Asset.setLength(12); + ship1Asset.setShipType("Passenger"); + ship1Asset.setIMONumber(9183527); + ship1Asset.setMSSINumber(244650537); + ship1Asset + .getAttribute(ShipAsset.DIRECTION) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 187), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 7) + }))); + }); + ship1Asset + .getAttribute(Asset.LOCATION) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(smartcitySimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(5).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(10).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(15).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(20).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(25).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), + new GeoJSONPoint(4.484374, 51.903518)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(35).get(SECOND_OF_DAY), + new GeoJSONPoint(4.479779, 51.904404)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(40).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482914, 51.906769)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(45).get(SECOND_OF_DAY), + new GeoJSONPoint(4.486156, 51.908570)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(50).get(SECOND_OF_DAY), + new GeoJSONPoint(4.483362, 51.911897)), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(55).get(SECOND_OF_DAY), + new GeoJSONPoint(4.482669, 51.916436)) + }))); + }); + ship1Asset.setId(UniqueIdentifierGenerator.generateId(ship1Asset.getName())); + ship1Asset = assetStorageService.merge(ship1Asset); + + // ################################ Realm Manufaturer Simulator + // ################################### + + SimulatorAgent manufacturerSimulatorAgent = new SimulatorAgent("Simulator"); + manufacturerSimulatorAgent.setRealm(this.realmManufacturerName); + + manufacturerSimulatorAgent = assetStorageService.merge(manufacturerSimulatorAgent); + manufacturerSimulatorAgentId = manufacturerSimulatorAgent.getId(); + + // ################################ Manufacturer realm assets + // ################################### + + // ### Greenhouse equipment distribution ### + + Asset distributor1 = new ThingAsset("GreenEquipment Distribution"); + distributor1.setRealm(this.realmManufacturerName); + distributor1.setId(UniqueIdentifierGenerator.generateId(distributor1.getName())); + distributor1 = assetStorageService.merge(distributor1); + + BuildingAsset vegetablesAndMore = new BuildingAsset("Vegetables & More"); + vegetablesAndMore.setParent(distributor1); + vegetablesAndMore.setId(UniqueIdentifierGenerator.generateId(vegetablesAndMore.getName())); + vegetablesAndMore = assetStorageService.merge(vegetablesAndMore); + + HarvestRobotAsset harvestRobot1 = + createDemoHarvestRobotAsset( + "Robot 1", + vegetablesAndMore, + new GeoJSONPoint(4.279166, 51.978078), + OperationMode.CUTTING, + VegetableType.BELL_PEPPER, + 26, + 45, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + harvestRobot1.setId(UniqueIdentifierGenerator.generateId(harvestRobot1.getName())); + harvestRobot1 = assetStorageService.merge(harvestRobot1); + HarvestRobotAsset harvestRobot2 = + createDemoHarvestRobotAsset( + "Robot 2", + vegetablesAndMore, + new GeoJSONPoint(4.277852, 51.977487), + OperationMode.SCANNING, + VegetableType.BELL_PEPPER, + 26, + 45, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + harvestRobot2.setId(UniqueIdentifierGenerator.generateId(harvestRobot2.getName())); + harvestRobot2 = assetStorageService.merge(harvestRobot2); + SoilSensorAsset soilSensor1 = + createDemoSoilSensorAsset( + "Water sensor", + vegetablesAndMore, + new GeoJSONPoint(4.279010, 51.977391), + 41, + 53, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + soilSensor1.setId(UniqueIdentifierGenerator.generateId(soilSensor1.getName())); + soilSensor1 = assetStorageService.merge(soilSensor1); + + BuildingAsset moreauHorticulture = new BuildingAsset("Qoreau Horticulture"); + moreauHorticulture.setParent(distributor1); + moreauHorticulture.setId(UniqueIdentifierGenerator.generateId(moreauHorticulture.getName())); + moreauHorticulture = assetStorageService.merge(moreauHorticulture); + + IrrigationAsset irrigation1 = + createDemoIrrigationAsset( + "Soil drip 1", + moreauHorticulture, + new GeoJSONPoint(4.311493, 51.961554), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + IrrigationAsset irrigation2 = + createDemoIrrigationAsset( + "Soil drip 2", + moreauHorticulture, + new GeoJSONPoint(4.310893, 51.961354), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + IrrigationAsset irrigation3 = + createDemoIrrigationAsset( + "Soil drip 3", + moreauHorticulture, + new GeoJSONPoint(4.310293, 51.961154), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + IrrigationAsset irrigation4 = + createDemoIrrigationAsset( + "Soil drip 4", + moreauHorticulture, + new GeoJSONPoint(4.309617, 51.960975), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + IrrigationAsset irrigation5 = + createDemoIrrigationAsset( + "Soil drip 5", + moreauHorticulture, + new GeoJSONPoint(4.309153, 51.960901), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation1.setId(UniqueIdentifierGenerator.generateId(irrigation1.getName())); + irrigation1 = assetStorageService.merge(irrigation1); + irrigation2.setId(UniqueIdentifierGenerator.generateId(irrigation2.getName())); + irrigation2 = assetStorageService.merge(irrigation2); + irrigation3.setId(UniqueIdentifierGenerator.generateId(irrigation3.getName())); + irrigation3 = assetStorageService.merge(irrigation3); + irrigation4.setId(UniqueIdentifierGenerator.generateId(irrigation4.getName())); + irrigation4 = assetStorageService.merge(irrigation4); + irrigation5.setId(UniqueIdentifierGenerator.generateId(irrigation5.getName())); + irrigation5 = assetStorageService.merge(irrigation5); + SoilSensorAsset soilSensor2 = + createDemoSoilSensorAsset( + "Soil measurement", + moreauHorticulture, + new GeoJSONPoint(4.310436, 51.961354), + 27, + 35, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + soilSensor2.setId(UniqueIdentifierGenerator.generateId(soilSensor2.getName())); + soilSensor2 = assetStorageService.merge(soilSensor2); + + BuildingAsset paprika = new BuildingAsset("Paprika Perfect BV"); + paprika.setParent(distributor1); + paprika + .getAttributes() + .getOrCreate("flowPerMeter", ValueType.NUMBER) + .addMeta( + new MetaItem<>(READ_ONLY), + new MetaItem<>(RULE_STATE), + new MetaItem<>(STORE_DATA_POINTS), + new MetaItem<>( + UNITS, + Constants.units(UNITS_LITRE, UNITS_PER, UNITS_METRE, UNITS_SQUARED, UNITS_HOUR))); + paprika + .getAttribute(BuildingAsset.AREA) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta(new MetaItem<>(RULE_STATE)).setValue(1800); + }); + paprika.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + paprika.setId(UniqueIdentifierGenerator.generateId(paprika.getName())); + paprikaId = paprika.getId(); + paprika = assetStorageService.merge(paprika); + + HarvestRobotAsset harvestRobot5 = + createDemoHarvestRobotAsset( + "Harvest Robot 1", + paprika, + new GeoJSONPoint(4.282415, 51.975951), + OperationMode.UNLOADING, + VegetableType.TOMATO, + 26, + 45, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + harvestRobot5.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + harvestRobot5 + .getAttribute(HarvestRobotAsset.HARVESTED_SESSION) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 814), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 814), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 815), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 816), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 816), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 816), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 816), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 817), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 818), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 819), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 820), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 821), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 0), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 34), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 86), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 112), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 156), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 289), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 348), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 456), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 516), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 624), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 684), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 713), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 762), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 787), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 792), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 798), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 804), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 808), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 812), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 813), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 813), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 814), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 814) + }))); + }); + harvestRobot5 + .getAttribute(HarvestRobotAsset.SPEED) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 8), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 1), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 1), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1) + }))); + }); + harvestRobot5.setId(UniqueIdentifierGenerator.generateId(harvestRobot5.getName())); + harvestRobot5Id = harvestRobot5.getId(); + harvestRobot5 = assetStorageService.merge(harvestRobot5); + + IrrigationAsset irrigation9 = + createDemoIrrigationAsset( + "Irrigation 1", + paprika, + new GeoJSONPoint(4.283731, 51.976526), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation9.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + irrigation9 + .getAttribute(IrrigationAsset.TANK_LEVEL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 1000), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 995), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 990), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 986), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 980), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 975), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 970), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 965), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 960), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 956), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 950), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 945), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 941), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 936), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 929), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 925), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 919), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 914), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 910), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 906), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 900), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 895), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 891), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + 1200), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 1195), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + 1190), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 1184), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + 1173), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 1145), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), + 1105), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 1074), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + 1032), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 1027), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), + 1022), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 1017), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1006) + }))); + }); + irrigation9 + .getAttribute(IrrigationAsset.FLOW_WATER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 10.5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 10.8), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 11.3), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 11.9), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + 12.5), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + 10.8), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 9.6), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 9.4), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + 10.4), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + 11.8), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + 12.3), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + 11.3), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + 11.7), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), + 10.2), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 10.3), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + 10.1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 10), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 9), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 10.2), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 10.1) + }))); + }); + irrigation9.setId(UniqueIdentifierGenerator.generateId(irrigation9.getName())); + irrigation9Id = irrigation9.getId(); + irrigation9 = assetStorageService.merge(irrigation9); + IrrigationAsset irrigation10 = + createDemoIrrigationAsset( + "Irrigation 2", + paprika, + new GeoJSONPoint(4.285047, 51.975652), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation10.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + irrigation10 + .getAttribute(IrrigationAsset.TANK_LEVEL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 1200), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 1193), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 1185), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 1178), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 1170), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1162), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 1155), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1148), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 1140), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + 1132), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 1124), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + 1117), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 1110), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), + 1102), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 1093), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), + 1085), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 1076), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + 1070), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 1063), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + 1055), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 1048), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + 1041), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 1035), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + 1028), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 1022), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + 1021), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 1018), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + 1015), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 1010), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), + 1002), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 995), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + 1050), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 1150), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), + 1290), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 1250), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1210) + }))); + }); + irrigation10 + .getAttribute(IrrigationAsset.FLOW_WATER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 12.5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 12.6), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 14.2), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 13.3), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + 14.5), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + 12.8), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 13.2), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), + 11.6), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), + 11.4), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 11.2), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + 12.3), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + 13.8), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + 12.3), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 13.1), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + 13.2), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 12.9), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + 13.7), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 11.8), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), + 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 12.6), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 12.2), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 12.1) + }))); + }); + irrigation10.setId(UniqueIdentifierGenerator.generateId(irrigation10.getName())); + irrigation10Id = irrigation10.getId(); + irrigation10 = assetStorageService.merge(irrigation10); + IrrigationAsset irrigation11 = + createDemoIrrigationAsset( + "Irrigation 3", + paprika, + new GeoJSONPoint(4.286504, 51.974613), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation11.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + irrigation11 + .getAttribute(IrrigationAsset.TANK_LEVEL) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 1210), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 1190), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 1180), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 1172), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 1160), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 1152), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 1145), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 1141), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 1132), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + 1125), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 1120), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + 1110), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 1100), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), + 1102), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 1083), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), + 1071), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 1066), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + 1060), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 1055), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + 1035), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 1028), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + 1021), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 1015), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), + 1006), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 1002), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), + 1001), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 997), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 985), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 985), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 982), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 975), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + 1040), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 1200), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), + 1250), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 1250), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1220) + }))); + }); + irrigation11 + .getAttribute(IrrigationAsset.FLOW_WATER) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 13.5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 13.5), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 14.2), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 13.1), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 15.2), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), + 15.4), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), + 13.8), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 14.2), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), + 12.6), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 12.2), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), + 12.2), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 13.2), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), + 13.4), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), + 14.7), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), + 13.2), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 14), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 14.1), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), + 14.6), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 12.9), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 13), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 13.4), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), + 13.1), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 12.9), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), + 12.1), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 13.2), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 13.1) + }))); + }); + irrigation11.setId(UniqueIdentifierGenerator.generateId(irrigation11.getName())); + irrigation11Id = irrigation11.getId(); + irrigation11 = assetStorageService.merge(irrigation11); + + SoilSensorAsset soilSensor4 = + createDemoSoilSensorAsset( + "Soil sensor", + paprika, + new GeoJSONPoint(4.285034, 51.973881), + 34, + 47, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + soilSensor4.getAttributes().stream() + .forEach( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>(ACCESS_RESTRICTED_READ), new MetaItem<>(ACCESS_RESTRICTED_WRITE)); + }); + soilSensor4 + .getAttribute(SoilSensorAsset.SALINITY) + .ifPresent( + assetAttribute -> { + assetAttribute.addMeta( + new MetaItem<>( + MetaItemType.AGENT_LINK, + new SimulatorAgentLink(manufacturerSimulatorAgentId) + .setSchedule(createDailySchedule()) + .setReplayData( + new SimulatorReplayDatapoint[] { + new SimulatorReplayDatapoint( + midnight.plusHours(6).get(SECOND_OF_DAY), 2), + new SimulatorReplayDatapoint( + midnight.plusHours(6).plusMinutes(30).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(7).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(7).plusMinutes(30).get(SECOND_OF_DAY), 12), + new SimulatorReplayDatapoint( + midnight.plusHours(8).get(SECOND_OF_DAY), 17), + new SimulatorReplayDatapoint( + midnight.plusHours(8).plusMinutes(30).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(9).get(SECOND_OF_DAY), 28), + new SimulatorReplayDatapoint( + midnight.plusHours(9).plusMinutes(30).get(SECOND_OF_DAY), 33), + new SimulatorReplayDatapoint( + midnight.plusHours(10).get(SECOND_OF_DAY), 32), + new SimulatorReplayDatapoint( + midnight.plusHours(10).plusMinutes(30).get(SECOND_OF_DAY), 38), + new SimulatorReplayDatapoint( + midnight.plusHours(11).get(SECOND_OF_DAY), 36), + new SimulatorReplayDatapoint( + midnight.plusHours(11).plusMinutes(30).get(SECOND_OF_DAY), 31), + new SimulatorReplayDatapoint( + midnight.plusHours(12).get(SECOND_OF_DAY), 25), + new SimulatorReplayDatapoint( + midnight.plusHours(12).plusMinutes(30).get(SECOND_OF_DAY), 28), + new SimulatorReplayDatapoint( + midnight.plusHours(13).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(13).plusMinutes(30).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(14).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(14).plusMinutes(30).get(SECOND_OF_DAY), 23), + new SimulatorReplayDatapoint( + midnight.plusHours(15).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(15).plusMinutes(30).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(16).get(SECOND_OF_DAY), 20), + new SimulatorReplayDatapoint( + midnight.plusHours(16).plusMinutes(30).get(SECOND_OF_DAY), 18), + new SimulatorReplayDatapoint( + midnight.plusHours(17).get(SECOND_OF_DAY), 21), + new SimulatorReplayDatapoint( + midnight.plusHours(17).plusMinutes(30).get(SECOND_OF_DAY), 22), + new SimulatorReplayDatapoint( + midnight.plusHours(18).get(SECOND_OF_DAY), 19), + new SimulatorReplayDatapoint( + midnight.plusHours(18).plusMinutes(30).get(SECOND_OF_DAY), 20), + new SimulatorReplayDatapoint( + midnight.plusHours(19).get(SECOND_OF_DAY), 15), + new SimulatorReplayDatapoint( + midnight.plusHours(19).plusMinutes(30).get(SECOND_OF_DAY), 11), + new SimulatorReplayDatapoint( + midnight.plusHours(20).get(SECOND_OF_DAY), 6), + new SimulatorReplayDatapoint( + midnight.plusHours(20).plusMinutes(30).get(SECOND_OF_DAY), 7), + new SimulatorReplayDatapoint( + midnight.plusHours(21).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(21).plusMinutes(30).get(SECOND_OF_DAY), 5), + new SimulatorReplayDatapoint( + midnight.plusHours(22).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(22).plusMinutes(30).get(SECOND_OF_DAY), 4), + new SimulatorReplayDatapoint( + midnight.plusHours(23).get(SECOND_OF_DAY), 3), + new SimulatorReplayDatapoint( + midnight.plusHours(23).plusMinutes(30).get(SECOND_OF_DAY), 1) + }))); + }); + soilSensor4.setId(UniqueIdentifierGenerator.generateId(soilSensor4.getName())); + soilSensor4Id = soilSensor4.getId(); + soilSensor4 = assetStorageService.merge(soilSensor4); + + // ### Distributor 2 ### + + Asset distributor2 = new ThingAsset("High-Tech Greenhouse Distribution"); + distributor2.setRealm(this.realmManufacturerName); + distributor2.setId(UniqueIdentifierGenerator.generateId(distributor2.getName())); + distributor2 = assetStorageService.merge(distributor2); + + BuildingAsset bertHaanen = new BuildingAsset("Haanen Vegetables BV"); + bertHaanen.setParent(distributor2); + bertHaanen.setId(UniqueIdentifierGenerator.generateId(bertHaanen.getName())); + bertHaanen = assetStorageService.merge(bertHaanen); + + HarvestRobotAsset harvestRobot3 = + createDemoHarvestRobotAsset( + "Harvest", + bertHaanen, + new GeoJSONPoint(4.286209, 51.983544), + OperationMode.UNLOADING, + VegetableType.TOMATO, + 26, + 45, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + harvestRobot3.setId(UniqueIdentifierGenerator.generateId(harvestRobot3.getName())); + harvestRobot3 = assetStorageService.merge(harvestRobot3); + + BuildingAsset rtd = new BuildingAsset("RTD Vegetable Grower"); + rtd.setParent(distributor2); + rtd.setId(UniqueIdentifierGenerator.generateId(rtd.getName())); + rtd = assetStorageService.merge(rtd); + + HarvestRobotAsset harvestRobot4 = + createDemoHarvestRobotAsset( + "Harvester", + rtd, + new GeoJSONPoint(4.408010, 51.986839), + OperationMode.UNLOADING, + VegetableType.TOMATO, + 26, + 45, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + harvestRobot4.setId(UniqueIdentifierGenerator.generateId(harvestRobot4.getName())); + harvestRobot4 = assetStorageService.merge(harvestRobot4); + + IrrigationAsset irrigation6 = + createDemoIrrigationAsset( + "Irrigation N", + rtd, + new GeoJSONPoint(4.408287, 51.987239), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation6.setId(UniqueIdentifierGenerator.generateId(irrigation6.getName())); + irrigation6 = assetStorageService.merge(irrigation6); + IrrigationAsset irrigation7 = + createDemoIrrigationAsset( + "Irrigation S", + rtd, + new GeoJSONPoint(4.408313, 51.986357), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation7.setId(UniqueIdentifierGenerator.generateId(irrigation7.getName())); + irrigation7 = assetStorageService.merge(irrigation7); + IrrigationAsset irrigation8 = + createDemoIrrigationAsset( + "Irrigation W", + rtd, + new GeoJSONPoint(4.407037, 51.986598), + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + irrigation8.setId(UniqueIdentifierGenerator.generateId(irrigation8.getName())); + irrigation8 = assetStorageService.merge(irrigation8); + + SoilSensorAsset soilSensor3 = + createDemoSoilSensorAsset( + "Soil monitor", + rtd, + new GeoJSONPoint(4.408088, 51.986793), + 44, + 68, + () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); + soilSensor3.setId(UniqueIdentifierGenerator.generateId(soilSensor3.getName())); + soilSensor3 = assetStorageService.merge(soilSensor3); + + // ############################## Add historic simulated data ############################### + + upsertSimulatedHistoricData(); + + // ################################ Link users and assets ################################### + + assetStorageService.storeUserAssetLinks( + Arrays.asList( + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, paprikaId), + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, irrigation9Id), + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, irrigation10Id), + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, irrigation11Id), + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, harvestRobot5Id), + new UserAssetLink( + this.realmManufacturerName, KeycloakDemoSetup.customerUserId, soilSensor4Id))); + + // ################################ Make user restricted ################################### + ManagerIdentityProvider identityProvider = identityService.getIdentityProvider(); + identityProvider.updateUserRealmRoles( + realmManufacturer.getName(), + KeycloakDemoSetup.customerUserId, + identityProvider.addUserRealmRoles( + realmManufacturer.getName(), + KeycloakDemoSetup.customerUserId, + RESTRICTED_USER_REALM_ROLE)); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + protected static AttributeLink createWeatherApiAttributeLink( + String assetId, String jsonParentName, String jsonName, String parameter) { + return new AttributeLink( + new AttributeRef(assetId, parameter), + null, + new ValueFilter[] { + new JsonPathFilter("$." + jsonParentName + "." + jsonName, true, false), }); - soilSensor4.setId(UniqueIdentifierGenerator.generateId(soilSensor4.getName())); - soilSensor4Id = soilSensor4.getId(); - soilSensor4 = assetStorageService.merge(soilSensor4); - - // ### Distributor 2 ### - - Asset distributor2 = new ThingAsset("High-Tech Greenhouse Distribution"); - distributor2.setRealm(this.realmManufacturerName); - distributor2.setId(UniqueIdentifierGenerator.generateId(distributor2.getName())); - distributor2 = assetStorageService.merge(distributor2); - - BuildingAsset bertHaanen = new BuildingAsset("Haanen Vegetables BV"); - bertHaanen.setParent(distributor2); - bertHaanen.setId(UniqueIdentifierGenerator.generateId(bertHaanen.getName())); - bertHaanen = assetStorageService.merge(bertHaanen); - - HarvestRobotAsset harvestRobot3 = createDemoHarvestRobotAsset("Harvest", bertHaanen, new GeoJSONPoint(4.286209, 51.983544), OperationMode.UNLOADING, VegetableType.TOMATO, 26, 45, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - harvestRobot3.setId(UniqueIdentifierGenerator.generateId(harvestRobot3.getName())); - harvestRobot3 = assetStorageService.merge(harvestRobot3); - - BuildingAsset rtd = new BuildingAsset("RTD Vegetable Grower"); - rtd.setParent(distributor2); - rtd.setId(UniqueIdentifierGenerator.generateId(rtd.getName())); - rtd = assetStorageService.merge(rtd); - - HarvestRobotAsset harvestRobot4 = createDemoHarvestRobotAsset("Harvester", rtd, new GeoJSONPoint(4.408010, 51.986839), OperationMode.UNLOADING, VegetableType.TOMATO, 26, 45, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - harvestRobot4.setId(UniqueIdentifierGenerator.generateId(harvestRobot4.getName())); - harvestRobot4 = assetStorageService.merge(harvestRobot4); - - IrrigationAsset irrigation6 = createDemoIrrigationAsset("Irrigation N", rtd, new GeoJSONPoint(4.408287, 51.987239), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation6.setId(UniqueIdentifierGenerator.generateId(irrigation6.getName())); - irrigation6 = assetStorageService.merge(irrigation6); - IrrigationAsset irrigation7 = createDemoIrrigationAsset("Irrigation S", rtd, new GeoJSONPoint(4.408313, 51.986357), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation7.setId(UniqueIdentifierGenerator.generateId(irrigation7.getName())); - irrigation7 = assetStorageService.merge(irrigation7); - IrrigationAsset irrigation8 = createDemoIrrigationAsset("Irrigation W", rtd, new GeoJSONPoint(4.407037, 51.986598), () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - irrigation8.setId(UniqueIdentifierGenerator.generateId(irrigation8.getName())); - irrigation8 = assetStorageService.merge(irrigation8); - - SoilSensorAsset soilSensor3 = createDemoSoilSensorAsset("Soil monitor", rtd, new GeoJSONPoint(4.408088, 51.986793), 44, 68, () -> new SimulatorAgentLink(manufacturerSimulatorAgentId)); - soilSensor3.setId(UniqueIdentifierGenerator.generateId(soilSensor3.getName())); - soilSensor3 = assetStorageService.merge(soilSensor3); - - // ############################## Add historic simulated data ############################### - - upsertSimulatedHistoricData(); - - // ################################ Link users and assets ################################### - - assetStorageService.storeUserAssetLinks(Arrays.asList( - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - paprikaId), - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - irrigation9Id), - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - irrigation10Id), - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - irrigation11Id), - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - harvestRobot5Id), - new UserAssetLink(this.realmManufacturerName, - KeycloakDemoSetup.customerUserId, - soilSensor4Id))); - - // ################################ Make user restricted ################################### - ManagerIdentityProvider identityProvider = identityService.getIdentityProvider(); - identityProvider.updateUserRealmRoles(realmManufacturer.getName(), KeycloakDemoSetup.customerUserId, identityProvider - .addUserRealmRoles(realmManufacturer.getName(), - KeycloakDemoSetup.customerUserId, RESTRICTED_USER_REALM_ROLE)); - } + } - @SuppressWarnings("OptionalGetWithoutIsPresent") - protected static AttributeLink createWeatherApiAttributeLink(String assetId, String jsonParentName, String jsonName, String parameter) { - return new AttributeLink( - new AttributeRef(assetId, parameter), - null, - new ValueFilter[]{ - new JsonPathFilter("$." + jsonParentName + "." + jsonName, true, false), - } - ); - } + /** + * Adds {@link #HISTORIC_SIMULATED_DATA_DAYS} days of historic datapoints based on the configured + * simulator data. + */ + private void upsertSimulatedHistoricData() { + for (Asset asset : assetStorageService.findAll(new AssetQuery())) { + AttributeMap attributes = asset.getAttributes(); + for (Attribute attribute : attributes.values()) { + attribute + .getMeta() + .get(AGENT_LINK) + .ifPresent( + agentLinkMetaItem -> { + agentLinkMetaItem + .getValue() + .ifPresent( + agentLink -> { + if (agentLink instanceof SimulatorAgentLink simulatorAgentLink) { + simulatorAgentLink + .getReplayData() + .ifPresent( + replayData -> { + List> valuesAndTimestamps = + new ArrayList<>(); + ZonedDateTime midnight = + ZonedDateTime.now() + .with(LocalTime.MIDNIGHT) + .minusDays(HISTORIC_SIMULATED_DATA_DAYS); - /** - * Adds {@link #HISTORIC_SIMULATED_DATA_DAYS} days of historic datapoints based on the configured simulator data. - */ - private void upsertSimulatedHistoricData() { - for (Asset asset : assetStorageService.findAll(new AssetQuery())) { - AttributeMap attributes = asset.getAttributes(); - for (Attribute attribute : attributes.values()) { - attribute.getMeta().get(AGENT_LINK).ifPresent(agentLinkMetaItem -> { - agentLinkMetaItem.getValue().ifPresent(agentLink -> { - if (agentLink instanceof SimulatorAgentLink simulatorAgentLink) { - simulatorAgentLink.getReplayData().ifPresent(replayData -> { - List> valuesAndTimestamps = new ArrayList<>(); - ZonedDateTime midnight = ZonedDateTime.now().with(LocalTime.MIDNIGHT).minusDays(HISTORIC_SIMULATED_DATA_DAYS); - - while (midnight.isBefore(ZonedDateTime.now())) { - for (SimulatorReplayDatapoint datapoint : replayData) { - ZonedDateTime timestamp = midnight.plusSeconds(datapoint.getTimestamp()); - if (timestamp.isBefore(ZonedDateTime.now())) { - valuesAndTimestamps.add(new ValueDatapoint<>(timestamp.toInstant().toEpochMilli(), datapoint.getValue().get())); - } - } + while (midnight.isBefore(ZonedDateTime.now())) { + for (SimulatorReplayDatapoint datapoint : replayData) { + ZonedDateTime timestamp = + midnight.plusSeconds(datapoint.getTimestamp()); + if (timestamp.isBefore(ZonedDateTime.now())) { + valuesAndTimestamps.add( + new ValueDatapoint<>( + timestamp.toInstant().toEpochMilli(), + datapoint.getValue().get())); + } + } - midnight = midnight.plusDays(1); - } + midnight = midnight.plusDays(1); + } - assetDatapointService.upsertValues(asset.getId(), attribute.getName(), valuesAndTimestamps); - }); - } - }); + assetDatapointService.upsertValues( + asset.getId(), + attribute.getName(), + valuesAndTimestamps); + }); + } + }); }); - } - } + } } + } - protected ElectricityStorageAsset createDemoElectricityStorageAsset(String name, Asset area, - GeoJSONPoint location) { - ElectricityStorageAsset electricityStorageAsset = new ElectricityBatteryAsset(name); - electricityStorageAsset.setParent(area); - electricityStorageAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + protected ElectricityStorageAsset createDemoElectricityStorageAsset( + String name, Asset area, GeoJSONPoint location) { + ElectricityStorageAsset electricityStorageAsset = new ElectricityBatteryAsset(name); + electricityStorageAsset.setParent(area); + electricityStorageAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); - return electricityStorageAsset; - } + return electricityStorageAsset; + } - protected ElectricityProducerSolarAsset createDemoElectricitySolarProducerAsset(String name, Asset area, - GeoJSONPoint location) { - ElectricityProducerSolarAsset electricityProducerAsset = new ElectricityProducerSolarAsset(name); - electricityProducerAsset.setParent(area); - electricityProducerAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + protected ElectricityProducerSolarAsset createDemoElectricitySolarProducerAsset( + String name, Asset area, GeoJSONPoint location) { + ElectricityProducerSolarAsset electricityProducerAsset = + new ElectricityProducerSolarAsset(name); + electricityProducerAsset.setParent(area); + electricityProducerAsset + .getAttributes() + .addOrReplace(new Attribute<>(Asset.LOCATION, location)); - return electricityProducerAsset; - } + return electricityProducerAsset; + } - protected ElectricityConsumerAsset createDemoElectricityConsumerAsset(String name, Asset area, - GeoJSONPoint location) { - ElectricityConsumerAsset electricityConsumerAsset = new ElectricityConsumerAsset(name); - electricityConsumerAsset.setParent(area); - electricityConsumerAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + protected ElectricityConsumerAsset createDemoElectricityConsumerAsset( + String name, Asset area, GeoJSONPoint location) { + ElectricityConsumerAsset electricityConsumerAsset = new ElectricityConsumerAsset(name); + electricityConsumerAsset.setParent(area); + electricityConsumerAsset + .getAttributes() + .addOrReplace(new Attribute<>(Asset.LOCATION, location)); - return electricityConsumerAsset; - } + return electricityConsumerAsset; + } - protected ElectricityChargerAsset createDemoElectricityChargerAsset(String name, Asset area, - GeoJSONPoint location) { - ElectricityChargerAsset electricityChargerAsset = new ElectricityChargerAsset(name); - electricityChargerAsset.setParent(area); - electricityChargerAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); + protected ElectricityChargerAsset createDemoElectricityChargerAsset( + String name, Asset area, GeoJSONPoint location) { + ElectricityChargerAsset electricityChargerAsset = new ElectricityChargerAsset(name); + electricityChargerAsset.setParent(area); + electricityChargerAsset.getAttributes().addOrReplace(new Attribute<>(Asset.LOCATION, location)); - return electricityChargerAsset; - } + return electricityChargerAsset; + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java index 6379b67..5eeb020 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,209 +12,304 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + import org.apache.commons.io.IOUtils; import org.openremote.manager.setup.ManagerSetup; import org.openremote.model.Container; import org.openremote.model.rules.RealmRuleset; import org.openremote.model.rules.Ruleset; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.logging.Logger; - public class RulesDemoSetup extends ManagerSetup { - private static final Logger LOG = Logger.getLogger(RulesDemoSetup.class.getName()); - - public RulesDemoSetup(Container container) { - super(container); - } - - public Long realmSmartCityRulesetId; - public Long realmManufacturerRulesetId; - - @Override - public void onStart() throws Exception { - - KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); - ManagerDemoSetup managerDemoSetup = setupService.getTaskOfType(ManagerDemoSetup.class); - - LOG.info("Importing demo rulesets"); - - // ################################ Rules demo data ################################### - - // SmartCity geofences - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/DeKuip.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "De Kuip", Ruleset.Lang.JSON, rules - ).setAccessPublicRead(true).setShowOnList(true); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/Euromast.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Euromast", Ruleset.Lang.JSON, rules - ).setAccessPublicRead(true).setShowOnList(true); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/Markthal.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Markthal", Ruleset.Lang.JSON, rules - ).setAccessPublicRead(true).setShowOnList(true); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/MarkthalChargersInUse.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Markthal: All chargers in use", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/OnsParkBrightStrongWinds.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Ons Park: Brighten lights", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/OnsParkDimLightWinds.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Ons Park: Dim lights", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/StationCrowded.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Station: Crowded square", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/EnvironmentAlerts.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Environment monitoring: Alerts", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/TotalPowerConsumption.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Total power consumption", Ruleset.Lang.FLOW, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/TotalSolarProduction.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Total power production", Ruleset.Lang.FLOW, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/RotterdamPowerBalance.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "De Rotterdam: Power balance", Ruleset.Lang.FLOW, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/ParkingOccupiedPercentage.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Parking: Occupied spaces", Ruleset.Lang.FLOW, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/ParkingFull.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Parking: Almost full", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/RotterdamBatteryUse.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "De Rotterdam: Battery use", Ruleset.Lang.JSON, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/LightGroupOnOff.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmCity.getName(), "Light group: On/Off", Ruleset.Lang.FLOW, rules - ); - realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - /// Manufacturer rules - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/FlowPerMeter.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "KPI: Flow per m2", Ruleset.Lang.FLOW, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/SalinityBetween20And25.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "Salinity 20 < 25", Ruleset.Lang.JSON, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/SalinityGreaterThan25.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "Salinity > 25", Ruleset.Lang.JSON, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/SalinityLessThan3.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "Salinity < 3", Ruleset.Lang.JSON, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/IrrigationTankLow.json")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "Irrigation tank low", Ruleset.Lang.JSON, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } - - try (InputStream inputStream = RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/TotalFlow.flow")) { - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - Ruleset ruleset = new RealmRuleset( - keycloakDemoSetup.realmManufacturer.getName(), "Irrigation flow total", Ruleset.Lang.FLOW, rules - ); - realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); - } + private static final Logger LOG = Logger.getLogger(RulesDemoSetup.class.getName()); + + public RulesDemoSetup(Container container) { + super(container); + } + + public Long realmSmartCityRulesetId; + public Long realmManufacturerRulesetId; + + @Override + public void onStart() throws Exception { + + KeycloakDemoSetup keycloakDemoSetup = setupService.getTaskOfType(KeycloakDemoSetup.class); + ManagerDemoSetup managerDemoSetup = setupService.getTaskOfType(ManagerDemoSetup.class); + + LOG.info("Importing demo rulesets"); + + // ################################ Rules demo data ################################### + + // SmartCity geofences + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/DeKuip.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), "De Kuip", Ruleset.Lang.JSON, rules) + .setAccessPublicRead(true) + .setShowOnList(true); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/Euromast.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), "Euromast", Ruleset.Lang.JSON, rules) + .setAccessPublicRead(true) + .setShowOnList(true); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/Markthal.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), "Markthal", Ruleset.Lang.JSON, rules) + .setAccessPublicRead(true) + .setShowOnList(true); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/MarkthalChargersInUse.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Markthal: All chargers in use", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/OnsParkBrightStrongWinds.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Ons Park: Brighten lights", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/OnsParkDimLightWinds.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Ons Park: Dim lights", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/StationCrowded.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Station: Crowded square", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/EnvironmentAlerts.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Environment monitoring: Alerts", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/TotalPowerConsumption.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Total power consumption", + Ruleset.Lang.FLOW, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/TotalSolarProduction.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Total power production", + Ruleset.Lang.FLOW, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/RotterdamPowerBalance.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "De Rotterdam: Power balance", + Ruleset.Lang.FLOW, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/ParkingOccupiedPercentage.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Parking: Occupied spaces", + Ruleset.Lang.FLOW, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/ParkingFull.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Parking: Almost full", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/smartcity/RotterdamBatteryUse.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "De Rotterdam: Battery use", + Ruleset.Lang.JSON, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/smartcity/LightGroupOnOff.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmCity.getName(), + "Light group: On/Off", + Ruleset.Lang.FLOW, + rules); + realmSmartCityRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + /// Manufacturer rules + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/FlowPerMeter.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "KPI: Flow per m2", + Ruleset.Lang.FLOW, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/manufacturer/SalinityBetween20And25.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "Salinity 20 < 25", + Ruleset.Lang.JSON, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/manufacturer/SalinityGreaterThan25.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "Salinity > 25", + Ruleset.Lang.JSON, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/manufacturer/SalinityLessThan3.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "Salinity < 3", + Ruleset.Lang.JSON, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream( + "/demo/rules/manufacturer/IrrigationTankLow.json")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "Irrigation tank low", + Ruleset.Lang.JSON, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); + } + + try (InputStream inputStream = + RulesDemoSetup.class.getResourceAsStream("/demo/rules/manufacturer/TotalFlow.flow")) { + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + Ruleset ruleset = + new RealmRuleset( + keycloakDemoSetup.realmManufacturer.getName(), + "Irrigation flow total", + Ruleset.Lang.FLOW, + rules); + realmManufacturerRulesetId = rulesetStorageService.merge(ruleset).getId(); } + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java index 04a28c5..5ad25c8 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,66 +12,67 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup.model; +import static org.openremote.model.Constants.*; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.value.AttributeDescriptor; import org.openremote.model.value.ValueDescriptor; import org.openremote.model.value.ValueType; -import static org.openremote.model.Constants.*; - import jakarta.persistence.Entity; @Entity public class HarvestRobotAsset extends Asset { - public enum OperationMode { - MOVING, - SCANNING, - CUTTING, - UNLOADING, - CHARGING - } - public static final ValueDescriptor OPERATION_MODE_VALUE = new ValueDescriptor<>("operationMode", - OperationMode.class); - public static final AttributeDescriptor OPERATION_MODE = new AttributeDescriptor<>( - "operationMode", OPERATION_MODE_VALUE); + public enum OperationMode { + MOVING, + SCANNING, + CUTTING, + UNLOADING, + CHARGING + } + + public static final ValueDescriptor OPERATION_MODE_VALUE = + new ValueDescriptor<>("operationMode", OperationMode.class); + public static final AttributeDescriptor OPERATION_MODE = + new AttributeDescriptor<>("operationMode", OPERATION_MODE_VALUE); - public enum VegetableType { - TOMATO, - CUCUMBER, - BELL_PEPPER - } - public static final ValueDescriptor VEGETABLE_TYPE_VALUE = new ValueDescriptor<>("vegetableType", - VegetableType.class); - public static final AttributeDescriptor VEGETABLE_TYPE = new AttributeDescriptor<>( - "vegetableType", VEGETABLE_TYPE_VALUE); + public enum VegetableType { + TOMATO, + CUCUMBER, + BELL_PEPPER + } - public static final AttributeDescriptor DIRECTION = new AttributeDescriptor<>("direction", - ValueType.DIRECTION); - public static final AttributeDescriptor SPEED = new AttributeDescriptor<>("speed", - ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_METRE, UNITS_PER, UNITS_HOUR); + public static final ValueDescriptor VEGETABLE_TYPE_VALUE = + new ValueDescriptor<>("vegetableType", VegetableType.class); + public static final AttributeDescriptor VEGETABLE_TYPE = + new AttributeDescriptor<>("vegetableType", VEGETABLE_TYPE_VALUE); - public static final AttributeDescriptor HARVESTED_SESSION = new AttributeDescriptor<>("harvestedSession", - ValueType.POSITIVE_INTEGER); - public static final AttributeDescriptor HARVESTED_TOTAL = new AttributeDescriptor<>("harvestedTotal", - ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor DIRECTION = + new AttributeDescriptor<>("direction", ValueType.DIRECTION); + public static final AttributeDescriptor SPEED = + new AttributeDescriptor<>("speed", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_METRE, UNITS_PER, UNITS_HOUR); - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("robot-industrial", "38761d", HarvestRobotAsset.class); + public static final AttributeDescriptor HARVESTED_SESSION = + new AttributeDescriptor<>("harvestedSession", ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor HARVESTED_TOTAL = + new AttributeDescriptor<>("harvestedTotal", ValueType.POSITIVE_INTEGER); - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected HarvestRobotAsset() { - } + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("robot-industrial", "38761d", HarvestRobotAsset.class); - public HarvestRobotAsset(String name) { - super(name); - } + /** For use by hydrators (i.e. JPA/Jackson) */ + protected HarvestRobotAsset() {} + public HarvestRobotAsset(String name) { + super(name); + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java index a0f9b05..8a15203 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,41 +12,43 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup.model; +import static org.openremote.model.Constants.*; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.value.AttributeDescriptor; import org.openremote.model.value.ValueType; -import static org.openremote.model.Constants.*; - import jakarta.persistence.Entity; @Entity public class IrrigationAsset extends Asset { - public static final AttributeDescriptor FLOW_WATER = new AttributeDescriptor<>("flowWater", - ValueType.POSITIVE_NUMBER).withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); - public static final AttributeDescriptor FLOW_NUTRIENTS = new AttributeDescriptor<>("flowNutrients", - ValueType.POSITIVE_NUMBER).withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); - public static final AttributeDescriptor FLOW_TOTAL = new AttributeDescriptor<>("flowTotal", - ValueType.POSITIVE_NUMBER).withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); - public static final AttributeDescriptor TANK_LEVEL = new AttributeDescriptor<>("tankLevel", - ValueType.POSITIVE_NUMBER).withUnits(UNITS_LITRE); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("water-pump", "3d85c6", IrrigationAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected IrrigationAsset() { - } - - public IrrigationAsset(String name) { - super(name); - } - + public static final AttributeDescriptor FLOW_WATER = + new AttributeDescriptor<>("flowWater", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); + public static final AttributeDescriptor FLOW_NUTRIENTS = + new AttributeDescriptor<>("flowNutrients", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); + public static final AttributeDescriptor FLOW_TOTAL = + new AttributeDescriptor<>("flowTotal", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_LITRE, UNITS_PER, UNITS_HOUR); + public static final AttributeDescriptor TANK_LEVEL = + new AttributeDescriptor<>("tankLevel", ValueType.POSITIVE_NUMBER).withUnits(UNITS_LITRE); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("water-pump", "3d85c6", IrrigationAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected IrrigationAsset() {} + + public IrrigationAsset(String name) { + super(name); + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java index 8e99a6a..5987840 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup.model; @@ -23,8 +22,8 @@ public class ManufacturerAssetModelProvider implements AssetModelProvider { - @Override - public boolean useAutoScan() { - return true; - } + @Override + public boolean useAutoScan() { + return true; + } } diff --git a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java index 0d05adf..71a448c 100644 --- a/demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.demosetup.model; +import static org.openremote.model.Constants.*; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.value.AttributeDescriptor; @@ -27,38 +28,32 @@ import jakarta.persistence.Entity; -import static org.openremote.model.Constants.*; - @Entity public class SoilSensorAsset extends Asset { - public static final AttributeDescriptor SOIL_TENSION_MEASURED = new AttributeDescriptor<>("soilTensionMeasured", - ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_PASCAL); - public static final AttributeDescriptor SOIL_TENSION_MIN = new AttributeDescriptor<>("soilTensionMin", - ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_PASCAL) - .withConstraints(new ValueConstraint.Min(0),new ValueConstraint.Max(80)); - public static final AttributeDescriptor SOIL_TENSION_MAX = new AttributeDescriptor<>("soilTensionMax", - ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_PASCAL) - .withConstraints(new ValueConstraint.Min(0),new ValueConstraint.Max(80)); - public static final AttributeDescriptor TEMPERATURE = new AttributeDescriptor<>("temperature", - ValueType.NUMBER) - .withUnits(UNITS_CELSIUS); - public static final AttributeDescriptor SALINITY = new AttributeDescriptor<>("salinity", - ValueType.NUMBER); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("water-percent", "993333", SoilSensorAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected SoilSensorAsset() { - } - - public SoilSensorAsset(String name) { - super(name); - } - + public static final AttributeDescriptor SOIL_TENSION_MEASURED = + new AttributeDescriptor<>("soilTensionMeasured", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_PASCAL); + public static final AttributeDescriptor SOIL_TENSION_MIN = + new AttributeDescriptor<>("soilTensionMin", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_PASCAL) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(80)); + public static final AttributeDescriptor SOIL_TENSION_MAX = + new AttributeDescriptor<>("soilTensionMax", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_PASCAL) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(80)); + public static final AttributeDescriptor TEMPERATURE = + new AttributeDescriptor<>("temperature", ValueType.NUMBER).withUnits(UNITS_CELSIUS); + public static final AttributeDescriptor SALINITY = + new AttributeDescriptor<>("salinity", ValueType.NUMBER); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("water-percent", "993333", SoilSensorAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected SoilSensorAsset() {} + + public SoilSensorAsset(String name) { + super(name); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsAgentModelProvider.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsAgentModelProvider.java index c2dbde2..d264d21 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsAgentModelProvider.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsAgentModelProvider.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; @@ -23,8 +22,8 @@ public class EmsAgentModelProvider implements AssetModelProvider { - @Override - public boolean useAutoScan() { - return true; - } + @Override + public boolean useAutoScan() { + return true; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsDayAheadAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsDayAheadAsset.java index d63bc26..97507b3 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsDayAheadAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsDayAheadAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,11 +12,16 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; -import jakarta.persistence.Entity; +import static org.openremote.model.Constants.*; + +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; @@ -29,71 +31,75 @@ import org.openremote.model.value.MetaItemType; import org.openremote.model.value.ValueType; -import java.util.Optional; - -import static org.openremote.model.Constants.*; +import jakarta.persistence.Entity; @Entity public class EmsDayAheadAsset extends Asset { - public static final AttributeDescriptor COLLECT_TIME_FORECASTS = new AttributeDescriptor<>("collectTimeForecasts", ValueType.TEXT); - - public static final AttributeDescriptor LAST_UPDATE_FORECASTS = new AttributeDescriptor<>("lastUpdateForecasts", ValueType.TEXT, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ); - - public static final AttributeDescriptor TARIFF_EXPORT_DAY_AHEAD = new AttributeDescriptor<>("tariffExportDayAheadForecast", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor TARIFF_IMPORT_DAY_AHEAD = new AttributeDescriptor<>("tariffImportDayAheadForecast", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor USE_TARIFF_DAY_AHEAD_FORECASTS = new AttributeDescriptor<>("useTariffDayAheadForecasts", ValueType.BOOLEAN); - - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("calendar-clock", "000000", EmsDayAheadAsset.class); - - protected EmsDayAheadAsset() { - } - - public EmsDayAheadAsset(String name) { - super(name); - } - - - public Optional getCollectTimeForecasts() { - return getAttribute(COLLECT_TIME_FORECASTS).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getLastUpdateForecasts() { - return getAttribute(LAST_UPDATE_FORECASTS).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getLastUpdateForecastsTimestamp() { - return getAttribute(LAST_UPDATE_FORECASTS).flatMap(Attribute::getTimestamp); - } - - public Optional getUseTariffDayAheadForecasts() { - return getAttribute(USE_TARIFF_DAY_AHEAD_FORECASTS).flatMap(AbstractNameValueHolder::getValue); - } - - - public EmsDayAheadAsset setCollectTimeForecasts(String value) { - getAttributes().getOrCreate(COLLECT_TIME_FORECASTS).setValue(value); - return this; - } - - public EmsDayAheadAsset setUseTariffDayAheadForecasts(Boolean value) { - getAttributes().getOrCreate(USE_TARIFF_DAY_AHEAD_FORECASTS).setValue(value); - return this; - } - + public static final AttributeDescriptor COLLECT_TIME_FORECASTS = + new AttributeDescriptor<>("collectTimeForecasts", ValueType.TEXT); + + public static final AttributeDescriptor LAST_UPDATE_FORECASTS = + new AttributeDescriptor<>( + "lastUpdateForecasts", + ValueType.TEXT, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)); + + public static final AttributeDescriptor TARIFF_EXPORT_DAY_AHEAD = + new AttributeDescriptor<>( + "tariffExportDayAheadForecast", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor TARIFF_IMPORT_DAY_AHEAD = + new AttributeDescriptor<>( + "tariffImportDayAheadForecast", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor USE_TARIFF_DAY_AHEAD_FORECASTS = + new AttributeDescriptor<>("useTariffDayAheadForecasts", ValueType.BOOLEAN); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("calendar-clock", "000000", EmsDayAheadAsset.class); + + protected EmsDayAheadAsset() {} + + public EmsDayAheadAsset(String name) { + super(name); + } + + public Optional getCollectTimeForecasts() { + return getAttribute(COLLECT_TIME_FORECASTS).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getLastUpdateForecasts() { + return getAttribute(LAST_UPDATE_FORECASTS).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getLastUpdateForecastsTimestamp() { + return getAttribute(LAST_UPDATE_FORECASTS).flatMap(Attribute::getTimestamp); + } + + public Optional getUseTariffDayAheadForecasts() { + return getAttribute(USE_TARIFF_DAY_AHEAD_FORECASTS).flatMap(AbstractNameValueHolder::getValue); + } + + public EmsDayAheadAsset setCollectTimeForecasts(String value) { + getAttributes().getOrCreate(COLLECT_TIME_FORECASTS).setValue(value); + return this; + } + + public EmsDayAheadAsset setUseTariffDayAheadForecasts(Boolean value) { + getAttributes().getOrCreate(USE_TARIFF_DAY_AHEAD_FORECASTS).setValue(value); + return this; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsElectricityBatteryAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsElectricityBatteryAsset.java index 2c9839e..e442108 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsElectricityBatteryAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsElectricityBatteryAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,235 +12,266 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; -import jakarta.persistence.Entity; +import static org.openremote.model.Constants.*; + +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.*; -import java.util.Optional; - -import static org.openremote.model.Constants.*; +import jakarta.persistence.Entity; @Entity public class EmsElectricityBatteryAsset extends Asset { - public static final AttributeDescriptor ALLOW_CHARGING = new AttributeDescriptor<>("allowCharging", ValueType.BOOLEAN); - - public static final AttributeDescriptor ALLOW_DISCHARGING = new AttributeDescriptor<>("allowDischarging", ValueType.BOOLEAN); - - public enum EmsElectricityBatteryConnectionStatusValueType { - connected, - disconnected - } - - public static final AttributeDescriptor CHARGE_EFFICIENCY = new AttributeDescriptor<>("chargeEfficiency", ValueType.POSITIVE_INTEGER - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor CHARGE_POWER_MAXIMUM = new AttributeDescriptor<>("chargePowerMaximum", ValueType.POSITIVE_NUMBER - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final ValueDescriptor CONNECTION_STATUS_VALUE_TYPE = new ValueDescriptor<>("EmsElectricityBatteryConnectionStatusValueType", EmsElectricityBatteryConnectionStatusValueType.class); - - public static final AttributeDescriptor CONNECTION_STATUS = new AttributeDescriptor<>("connectionStatus", CONNECTION_STATUS_VALUE_TYPE, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ); - - public static final AttributeDescriptor DISCHARGE_EFFICIENCY = new AttributeDescriptor<>("dischargeEfficiency", ValueType.POSITIVE_INTEGER - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor DISCHARGE_POWER_MAXIMUM = new AttributeDescriptor<>("dischargePowerMaximum", ValueType.NEGATIVE_NUMBER - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor ENERGY_CAPACITY = new AttributeDescriptor<>("energyCapacity", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.RULE_STATE) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor ENERGY_LEVEL = new AttributeDescriptor<>("energyLevel", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE = new AttributeDescriptor<>("energyLevelPercentage", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MAXIMUM = new AttributeDescriptor<>("energyLevelPercentageMaximum", ValueType.POSITIVE_INTEGER - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MINIMUM = new AttributeDescriptor<>("energyLevelPercentageMinimum", ValueType.POSITIVE_INTEGER - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor POWER = new AttributeDescriptor<>("power", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_SETPOINT = new AttributeDescriptor<>("powerSetpoint", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - -// // Placeholders -// public static final AttributeDescriptor INCLUDE_BATTERY_COST = new AttributeDescriptor<>("includeBatteryCost", ValueType.BOOLEAN -// ); -// -// public static final AttributeDescriptor CHARGE_COST = new AttributeDescriptor<>("chargeCost", ValueType.NUMBER, -// new MetaItem<>(MetaItemType.LABEL, "Charge cost (€/kWh)") -// ); -// -// public static final AttributeDescriptor DISCHARGE_COST = new AttributeDescriptor<>("dischargeCost", ValueType.NUMBER, -// new MetaItem<>(MetaItemType.LABEL, "Discharge cost (€/kWh)") -// ); - - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("battery-charging", "1B7C89", EmsElectricityBatteryAsset.class); - - protected EmsElectricityBatteryAsset() { - } - - public EmsElectricityBatteryAsset(String name) { - super(name); - } - - public Optional getAllowCharging() { - return getAttribute(ALLOW_CHARGING).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getAllowDischarging() { - return getAttribute(ALLOW_DISCHARGING).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getConnectionStatus() { - return getAttribute(CONNECTION_STATUS).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getChargeEfficiency() { - return getAttribute(CHARGE_EFFICIENCY).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getChargePowerMaximum() { - return getAttribute(CHARGE_POWER_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getDischargeEfficiency() { - return getAttribute(DISCHARGE_EFFICIENCY).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getDischargePowerMaximum() { - return getAttribute(DISCHARGE_POWER_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyCapacity() { - return getAttribute(ENERGY_CAPACITY).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyLevelPercentage() { - return getAttribute(ENERGY_LEVEL_PERCENTAGE).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyLevelPercentageTimestamp() { - return getAttribute(ENERGY_LEVEL_PERCENTAGE).flatMap(Attribute::getTimestamp); - } - - public Optional getEnergyLevelPercentageMaximum() { - return getAttribute(ENERGY_LEVEL_PERCENTAGE_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyLevelPercentageMinimum() { - return getAttribute(ENERGY_LEVEL_PERCENTAGE_MINIMUM).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPower() { - return getAttribute(POWER).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerTimestamp() { - return getAttribute(POWER).flatMap(Attribute::getTimestamp); - } - - public Optional getPowerSetpoint() { - return getAttribute(POWER_SETPOINT).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerSetpointTimestamp() { - return getAttribute(POWER_SETPOINT).flatMap(Attribute::getTimestamp); - } - - - public EmsElectricityBatteryAsset setAllowCharging(Boolean value) { - getAttributes().getOrCreate(ALLOW_CHARGING).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setAllowDischarging(Boolean value) { - getAttributes().getOrCreate(ALLOW_DISCHARGING).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setChargeEfficiency(Integer value) { - getAttributes().getOrCreate(CHARGE_EFFICIENCY).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setChargePowerMaximum(Double value) { - getAttributes().getOrCreate(CHARGE_POWER_MAXIMUM).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setDischargeEfficiency(Integer value) { - getAttributes().getOrCreate(DISCHARGE_EFFICIENCY).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setDischargePowerMaximum(Double value) { - getAttributes().getOrCreate(DISCHARGE_POWER_MAXIMUM).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setEnergyCapacity(Double value) { - getAttributes().getOrCreate(ENERGY_CAPACITY).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setEnergyLevelPercentage(Double value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setEnergyLevelPercentageMaximum(Integer value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MAXIMUM).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setEnergyLevelPercentageMinimum(Integer value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MINIMUM).setValue(value); - return this; - } - - public EmsElectricityBatteryAsset setPower(Double value) { - getAttributes().getOrCreate(POWER).setValue(value); - return this; - } + public static final AttributeDescriptor ALLOW_CHARGING = + new AttributeDescriptor<>("allowCharging", ValueType.BOOLEAN); + + public static final AttributeDescriptor ALLOW_DISCHARGING = + new AttributeDescriptor<>("allowDischarging", ValueType.BOOLEAN); + + public enum EmsElectricityBatteryConnectionStatusValueType { + connected, + disconnected + } + + public static final AttributeDescriptor CHARGE_EFFICIENCY = + new AttributeDescriptor<>("chargeEfficiency", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor CHARGE_POWER_MAXIMUM = + new AttributeDescriptor<>("chargePowerMaximum", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final ValueDescriptor + CONNECTION_STATUS_VALUE_TYPE = + new ValueDescriptor<>( + "EmsElectricityBatteryConnectionStatusValueType", + EmsElectricityBatteryConnectionStatusValueType.class); + + public static final AttributeDescriptor + CONNECTION_STATUS = + new AttributeDescriptor<>( + "connectionStatus", + CONNECTION_STATUS_VALUE_TYPE, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)); + + public static final AttributeDescriptor DISCHARGE_EFFICIENCY = + new AttributeDescriptor<>("dischargeEfficiency", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor DISCHARGE_POWER_MAXIMUM = + new AttributeDescriptor<>("dischargePowerMaximum", ValueType.NEGATIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor ENERGY_CAPACITY = + new AttributeDescriptor<>( + "energyCapacity", ValueType.POSITIVE_NUMBER, new MetaItem<>(MetaItemType.RULE_STATE)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor ENERGY_LEVEL = + new AttributeDescriptor<>( + "energyLevel", + ValueType.POSITIVE_NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE = + new AttributeDescriptor<>( + "energyLevelPercentage", + ValueType.POSITIVE_NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MAXIMUM = + new AttributeDescriptor<>("energyLevelPercentageMaximum", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MINIMUM = + new AttributeDescriptor<>("energyLevelPercentageMinimum", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor POWER = + new AttributeDescriptor<>( + "power", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_SETPOINT = + new AttributeDescriptor<>( + "powerSetpoint", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + // // Placeholders + // public static final AttributeDescriptor INCLUDE_BATTERY_COST = new + // AttributeDescriptor<>("includeBatteryCost", ValueType.BOOLEAN + // ); + // + // public static final AttributeDescriptor CHARGE_COST = new + // AttributeDescriptor<>("chargeCost", ValueType.NUMBER, + // new MetaItem<>(MetaItemType.LABEL, "Charge cost (€/kWh)") + // ); + // + // public static final AttributeDescriptor DISCHARGE_COST = new + // AttributeDescriptor<>("dischargeCost", ValueType.NUMBER, + // new MetaItem<>(MetaItemType.LABEL, "Discharge cost (€/kWh)") + // ); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("battery-charging", "1B7C89", EmsElectricityBatteryAsset.class); + + protected EmsElectricityBatteryAsset() {} + + public EmsElectricityBatteryAsset(String name) { + super(name); + } + + public Optional getAllowCharging() { + return getAttribute(ALLOW_CHARGING).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getAllowDischarging() { + return getAttribute(ALLOW_DISCHARGING).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getConnectionStatus() { + return getAttribute(CONNECTION_STATUS).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getChargeEfficiency() { + return getAttribute(CHARGE_EFFICIENCY).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getChargePowerMaximum() { + return getAttribute(CHARGE_POWER_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getDischargeEfficiency() { + return getAttribute(DISCHARGE_EFFICIENCY).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getDischargePowerMaximum() { + return getAttribute(DISCHARGE_POWER_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyCapacity() { + return getAttribute(ENERGY_CAPACITY).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyLevelPercentage() { + return getAttribute(ENERGY_LEVEL_PERCENTAGE).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyLevelPercentageTimestamp() { + return getAttribute(ENERGY_LEVEL_PERCENTAGE).flatMap(Attribute::getTimestamp); + } + + public Optional getEnergyLevelPercentageMaximum() { + return getAttribute(ENERGY_LEVEL_PERCENTAGE_MAXIMUM).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyLevelPercentageMinimum() { + return getAttribute(ENERGY_LEVEL_PERCENTAGE_MINIMUM).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPower() { + return getAttribute(POWER).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerTimestamp() { + return getAttribute(POWER).flatMap(Attribute::getTimestamp); + } + + public Optional getPowerSetpoint() { + return getAttribute(POWER_SETPOINT).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerSetpointTimestamp() { + return getAttribute(POWER_SETPOINT).flatMap(Attribute::getTimestamp); + } + + public EmsElectricityBatteryAsset setAllowCharging(Boolean value) { + getAttributes().getOrCreate(ALLOW_CHARGING).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setAllowDischarging(Boolean value) { + getAttributes().getOrCreate(ALLOW_DISCHARGING).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setChargeEfficiency(Integer value) { + getAttributes().getOrCreate(CHARGE_EFFICIENCY).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setChargePowerMaximum(Double value) { + getAttributes().getOrCreate(CHARGE_POWER_MAXIMUM).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setDischargeEfficiency(Integer value) { + getAttributes().getOrCreate(DISCHARGE_EFFICIENCY).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setDischargePowerMaximum(Double value) { + getAttributes().getOrCreate(DISCHARGE_POWER_MAXIMUM).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setEnergyCapacity(Double value) { + getAttributes().getOrCreate(ENERGY_CAPACITY).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setEnergyLevelPercentage(Double value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setEnergyLevelPercentageMaximum(Integer value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MAXIMUM).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setEnergyLevelPercentageMinimum(Integer value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MINIMUM).setValue(value); + return this; + } + + public EmsElectricityBatteryAsset setPower(Double value) { + getAttributes().getOrCreate(POWER).setValue(value); + return this; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java index 12d5e4a..e225103 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,273 +12,331 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; -import jakarta.persistence.Entity; +import static org.openremote.model.Constants.*; + +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.*; -import java.util.Optional; - -import static org.openremote.model.Constants.*; +import jakarta.persistence.Entity; @Entity public class EmsEnergyOptimisationAsset extends Asset { - public static final AttributeDescriptor ADVANCED_SETTINGS_ATTRIBUTES = new AttributeDescriptor<>("advancedSettingsAttributes", ValueType.TEXT, - new MetaItem<>(MetaItemType.MULTILINE), - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor ENABLE_DETAILED_LOGGING = new AttributeDescriptor<>("enableDetailedLogging", ValueType.BOOLEAN); - - public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = new AttributeDescriptor<>("energyExportTotal", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = new AttributeDescriptor<>("energyImportTotal", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMaximumProfileManualInput", ValueType.BOOLEAN); - - public static final AttributeDescriptor GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMinimumProfileManualInput", ValueType.BOOLEAN); - - public static final AttributeDescriptor DISABLE_OPTIMISATION_SERVICE = new AttributeDescriptor<>("disableOptimisationService", ValueType.BOOLEAN); - - public enum OptimisationMethodValueType { - None, - EmsOptimisation, - EmsOptimisationBeta, - } - - public static final ValueDescriptor OPTIMISATION_METHOD_VALUE_TYPE = new ValueDescriptor<>("optimisationMethodValueType", OptimisationMethodValueType.class); - - public static final AttributeDescriptor OPTIMISATION_METHOD = new AttributeDescriptor<>("optimisationMethod", OPTIMISATION_METHOD_VALUE_TYPE - ); - - public static final AttributeDescriptor POWER_CONSUMPTION = new AttributeDescriptor<>("powerConsumption", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_FLEXIBLE = new AttributeDescriptor<>("powerFlexible", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_NET = new AttributeDescriptor<>("powerNet", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_PRODUCTION = new AttributeDescriptor<>("powerProduction", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_INPUT = new AttributeDescriptor<>("powerLimitMaximumInput", ValueType.NUMBER - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_MANUAL = new AttributeDescriptor<>("powerLimitMaximumProfileManual", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("powerLimitMaximumProfileManualInput", ValueType.TEXT, - new MetaItem<>(MetaItemType.MULTILINE) - ); - - public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_TOTAL = new AttributeDescriptor<>("powerLimitMaximumProfileTotal", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MINIMUM_INPUT = new AttributeDescriptor<>("powerLimitMinimumInput", ValueType.NUMBER - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_MANUAL = new AttributeDescriptor<>("powerLimitMinimumProfileManual", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("powerLimitMinimumProfileManualInput", ValueType.TEXT, - new MetaItem<>(MetaItemType.MULTILINE) - ); - - public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_TOTAL = new AttributeDescriptor<>("powerLimitMinimumProfileTotal", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor TARIFF_EXPORT = new AttributeDescriptor<>("tariffExport", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor TARIFF_IMPORT = new AttributeDescriptor<>("tariffImport", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.RULE_STATE), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("flash", "C4DB0D", EmsEnergyOptimisationAsset.class); - - protected EmsEnergyOptimisationAsset() { - } - - public EmsEnergyOptimisationAsset(String name) { - super(name); - } - - public Optional getAdvancedSettingsAttributes() { - return getAttribute(ADVANCED_SETTINGS_ATTRIBUTES).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnableDetailedLogging() { - return getAttribute(ENABLE_DETAILED_LOGGING).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyExportTotal() { - return getAttribute(ENERGY_EXPORT_TOTAL).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getEnergyImportTotal() { - return getAttribute(ENERGY_IMPORT_TOTAL).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getOptimisationDisabled() { - return getAttribute(DISABLE_OPTIMISATION_SERVICE).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getOptimisationMethod() { - return getAttribute(OPTIMISATION_METHOD).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerNet() { - return getAttribute(POWER_NET).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerNetTimestamp() { - return getAttribute(POWER_NET).flatMap(Attribute::getTimestamp); - } - - public Optional getPowerLimitMaximumInput() { - return getAttribute(POWER_LIMIT_MAXIMUM_INPUT).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerLimitMaximumProfileManualInput() { - return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerLimitMaximumProfileTotal() { - return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerLimitMaximumProfileTotalTimestamp() { - return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL).flatMap(Attribute::getTimestamp); - } - - public Optional getPowerLimitMinimumInput() { - return getAttribute(POWER_LIMIT_MINIMUM_INPUT).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerLimitMinimumProfileManualInput() { - return getAttribute(POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional getPowerLimitMinimumProfileTotal() { - return getAttribute(POWER_LIMIT_MINIMUM_PROFILE_TOTAL).flatMap(AbstractNameValueHolder::getValue); - } - - - public EmsEnergyOptimisationAsset setOptimisationDisabled(Boolean value) { - getAttributes().getOrCreate(DISABLE_OPTIMISATION_SERVICE).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setOptimisationMethod(OptimisationMethodValueType value) { - getAttributes().getOrCreate(OPTIMISATION_METHOD).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMaximumInput(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_INPUT).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileManual(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileManualInput(String value) { - getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileTotal(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMinimumInput(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_INPUT).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileManual(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_MANUAL).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileManualInput(String value) { - getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileTotal(Double value) { - getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_TOTAL).setValue(value); - return this; - } - - public EmsEnergyOptimisationAsset setPowerNet(Double value) { - getAttributes().getOrCreate(POWER_NET).setValue(value); - return this; - } + public static final AttributeDescriptor ADVANCED_SETTINGS_ATTRIBUTES = + new AttributeDescriptor<>( + "advancedSettingsAttributes", + ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE), + new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor ENABLE_DETAILED_LOGGING = + new AttributeDescriptor<>("enableDetailedLogging", ValueType.BOOLEAN); + + public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = + new AttributeDescriptor<>( + "energyExportTotal", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = + new AttributeDescriptor<>( + "energyImportTotal", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor + GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = + new AttributeDescriptor<>( + "generatePowerLimitMaximumProfileManualInput", ValueType.BOOLEAN); + + public static final AttributeDescriptor + GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = + new AttributeDescriptor<>( + "generatePowerLimitMinimumProfileManualInput", ValueType.BOOLEAN); + + public static final AttributeDescriptor DISABLE_OPTIMISATION_SERVICE = + new AttributeDescriptor<>("disableOptimisationService", ValueType.BOOLEAN); + + public enum OptimisationMethodValueType { + None, + EmsOptimisation, + EmsOptimisationBeta, + } + + public static final ValueDescriptor OPTIMISATION_METHOD_VALUE_TYPE = + new ValueDescriptor<>("optimisationMethodValueType", OptimisationMethodValueType.class); + + public static final AttributeDescriptor OPTIMISATION_METHOD = + new AttributeDescriptor<>("optimisationMethod", OPTIMISATION_METHOD_VALUE_TYPE); + + public static final AttributeDescriptor POWER_CONSUMPTION = + new AttributeDescriptor<>( + "powerConsumption", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_FLEXIBLE = + new AttributeDescriptor<>( + "powerFlexible", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_NET = + new AttributeDescriptor<>( + "powerNet", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_PRODUCTION = + new AttributeDescriptor<>( + "powerProduction", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_INPUT = + new AttributeDescriptor<>("powerLimitMaximumInput", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_MANUAL = + new AttributeDescriptor<>( + "powerLimitMaximumProfileManual", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = + new AttributeDescriptor<>( + "powerLimitMaximumProfileManualInput", + ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE)); + + public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_TOTAL = + new AttributeDescriptor<>( + "powerLimitMaximumProfileTotal", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MINIMUM_INPUT = + new AttributeDescriptor<>("powerLimitMinimumInput", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_MANUAL = + new AttributeDescriptor<>( + "powerLimitMinimumProfileManual", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = + new AttributeDescriptor<>( + "powerLimitMinimumProfileManualInput", + ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE)); + + public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_TOTAL = + new AttributeDescriptor<>( + "powerLimitMinimumProfileTotal", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor TARIFF_EXPORT = + new AttributeDescriptor<>( + "tariffExport", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor TARIFF_IMPORT = + new AttributeDescriptor<>( + "tariffImport", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.RULE_STATE), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("flash", "C4DB0D", EmsEnergyOptimisationAsset.class); + + protected EmsEnergyOptimisationAsset() {} + + public EmsEnergyOptimisationAsset(String name) { + super(name); + } + + public Optional getAdvancedSettingsAttributes() { + return getAttribute(ADVANCED_SETTINGS_ATTRIBUTES).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnableDetailedLogging() { + return getAttribute(ENABLE_DETAILED_LOGGING).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyExportTotal() { + return getAttribute(ENERGY_EXPORT_TOTAL).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getEnergyImportTotal() { + return getAttribute(ENERGY_IMPORT_TOTAL).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getOptimisationDisabled() { + return getAttribute(DISABLE_OPTIMISATION_SERVICE).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getOptimisationMethod() { + return getAttribute(OPTIMISATION_METHOD).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerNet() { + return getAttribute(POWER_NET).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerNetTimestamp() { + return getAttribute(POWER_NET).flatMap(Attribute::getTimestamp); + } + + public Optional getPowerLimitMaximumInput() { + return getAttribute(POWER_LIMIT_MAXIMUM_INPUT).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerLimitMaximumProfileManualInput() { + return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT) + .flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerLimitMaximumProfileTotal() { + return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL) + .flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerLimitMaximumProfileTotalTimestamp() { + return getAttribute(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL).flatMap(Attribute::getTimestamp); + } + + public Optional getPowerLimitMinimumInput() { + return getAttribute(POWER_LIMIT_MINIMUM_INPUT).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerLimitMinimumProfileManualInput() { + return getAttribute(POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT) + .flatMap(AbstractNameValueHolder::getValue); + } + + public Optional getPowerLimitMinimumProfileTotal() { + return getAttribute(POWER_LIMIT_MINIMUM_PROFILE_TOTAL) + .flatMap(AbstractNameValueHolder::getValue); + } + + public EmsEnergyOptimisationAsset setOptimisationDisabled(Boolean value) { + getAttributes().getOrCreate(DISABLE_OPTIMISATION_SERVICE).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setOptimisationMethod(OptimisationMethodValueType value) { + getAttributes().getOrCreate(OPTIMISATION_METHOD).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMaximumInput(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_INPUT).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileManual(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileManualInput(String value) { + getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMaximumProfileTotal(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MAXIMUM_PROFILE_TOTAL).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMinimumInput(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_INPUT).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileManual(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_MANUAL).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileManualInput(String value) { + getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerLimitMinimumProfileTotal(Double value) { + getAttributes().getOrCreate(POWER_LIMIT_MINIMUM_PROFILE_TOTAL).setValue(value); + return this; + } + + public EmsEnergyOptimisationAsset setPowerNet(Double value) { + getAttributes().getOrCreate(POWER_NET).setValue(value); + return this; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationSetupAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationSetupAsset.java index 9890f37..f29d6f8 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationSetupAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationSetupAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,11 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; -import jakarta.persistence.Entity; +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; @@ -27,46 +27,48 @@ import org.openremote.model.value.MetaItemType; import org.openremote.model.value.ValueType; -import java.util.Optional; +import jakarta.persistence.Entity; @Entity public class EmsEnergyOptimisationSetupAsset extends Asset { - public static final AttributeDescriptor CREATE_ENERGY_MANAGEMENT_SYSTEM = new AttributeDescriptor<>("createEnergyManagementSystem", ValueType.BOOLEAN - ); - - public static final AttributeDescriptor ENERGY_MANAGEMENT_SYSTEM_NAME = new AttributeDescriptor<>("energyManagementSystemName", ValueType.TEXT - ); + public static final AttributeDescriptor CREATE_ENERGY_MANAGEMENT_SYSTEM = + new AttributeDescriptor<>("createEnergyManagementSystem", ValueType.BOOLEAN); - public static final AttributeDescriptor INCLUDE_DAY_AHEAD_FORECASTS = new AttributeDescriptor<>("includeDayAheadForecasts", ValueType.BOOLEAN - ); + public static final AttributeDescriptor ENERGY_MANAGEMENT_SYSTEM_NAME = + new AttributeDescriptor<>("energyManagementSystemName", ValueType.TEXT); - public static final AttributeDescriptor INCLUDE_GOPACS = new AttributeDescriptor<>("includeGOPACS", ValueType.BOOLEAN - ); + public static final AttributeDescriptor INCLUDE_DAY_AHEAD_FORECASTS = + new AttributeDescriptor<>("includeDayAheadForecasts", ValueType.BOOLEAN); - public static final AttributeDescriptor INFO_FIELD = new AttributeDescriptor<>("infoField", ValueType.TEXT, - new MetaItem<>(MetaItemType.MULTILINE), - new MetaItem<>(MetaItemType.READ_ONLY) - ); + public static final AttributeDescriptor INCLUDE_GOPACS = + new AttributeDescriptor<>("includeGOPACS", ValueType.BOOLEAN); + public static final AttributeDescriptor INFO_FIELD = + new AttributeDescriptor<>( + "infoField", + ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE), + new MetaItem<>(MetaItemType.READ_ONLY)); - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("application-cog-outline", "000000", EmsEnergyOptimisationSetupAsset.class); + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>( + "application-cog-outline", "000000", EmsEnergyOptimisationSetupAsset.class); - protected EmsEnergyOptimisationSetupAsset() { - } + protected EmsEnergyOptimisationSetupAsset() {} - public EmsEnergyOptimisationSetupAsset(String name) { - super(name); - } + public EmsEnergyOptimisationSetupAsset(String name) { + super(name); + } - public Optional getIncludeDayAheadForecasts() { - return getAttributes().getValue(INCLUDE_DAY_AHEAD_FORECASTS); - } + public Optional getIncludeDayAheadForecasts() { + return getAttributes().getValue(INCLUDE_DAY_AHEAD_FORECASTS); + } - public Optional getIncludeGopacs() { - return getAttributes().getValue(INCLUDE_GOPACS); - } + public Optional getIncludeGopacs() { + return getAttributes().getValue(INCLUDE_GOPACS); + } - public Optional getEnergyManagementSystemName() { - return getAttributes().getValue(ENERGY_MANAGEMENT_SYSTEM_NAME); - } + public Optional getEnergyManagementSystemName() { + return getAttributes().getValue(ENERGY_MANAGEMENT_SYSTEM_NAME); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsGOPACSAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsGOPACSAsset.java index 85d2ac5..08c9c77 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsGOPACSAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsGOPACSAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,207 +12,238 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.agent; -import jakarta.persistence.Entity; +import static org.openremote.model.Constants.*; + +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AttributeDescriptor; import org.openremote.model.value.MetaItemType; import org.openremote.model.value.ValueType; - import org.openremote.model.value.ValueType.ObjectMap; -import java.util.Optional; - -import static org.openremote.model.Constants.*; +import jakarta.persistence.Entity; @Entity public class EmsGOPACSAsset extends Asset { - // --- UFTP / Day-Ahead attributes --- - - public static final AttributeDescriptor CONTRACTED_EAN = new AttributeDescriptor<>("contractedEAN", ValueType.TEXT - ); - - public static final AttributeDescriptor CURRENT_POWER_FLEX_REQUEST = new AttributeDescriptor<>("currentPowerFlexRequest", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER = new AttributeDescriptor<>("powerLimitMaximumProfileFlexOrder", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER = new AttributeDescriptor<>("powerLimitMinimumProfileFlexOrder", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_MAXIMUM_FLEX_REQUEST = new AttributeDescriptor<>("powerMaximumFlexRequest", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor POWER_MINIMUM_FLEX_REQUEST = new AttributeDescriptor<>("powerMinimumFlexRequest", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - // --- Redispatch configuration attributes --- - - public static final AttributeDescriptor REDISPATCH_ENABLED = new AttributeDescriptor<>("redispatchEnabled", ValueType.BOOLEAN - ); - - // --- Redispatch status attributes (read-only, system-updated) --- - - public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_ID = new AttributeDescriptor<>("redispatchAnnouncementId", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_COMPLIANCE_TYPE = new AttributeDescriptor<>("redispatchComplianceType", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_MESSAGE = new AttributeDescriptor<>("redispatchAnnouncementMessage", ValueType.TEXT, - new MetaItem<>(MetaItemType.MULTILINE), - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_START_TIME = new AttributeDescriptor<>("redispatchStartTime", ValueType.TIMESTAMP, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_END_TIME = new AttributeDescriptor<>("redispatchEndTime", ValueType.TIMESTAMP, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_BID_VALIDITY_END = new AttributeDescriptor<>("redispatchBidValidityEnd", ValueType.TIMESTAMP, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_REQUESTED_POWER = new AttributeDescriptor<>("redispatchRequestedPower", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor REDISPATCH_EAN_EFFECTIVITY = new AttributeDescriptor<>("redispatchEanEffectivity", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_REQUEST_AREA_BUY = new AttributeDescriptor<>("redispatchRequestAreaBuy", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_REQUEST_AREA_SELL = new AttributeDescriptor<>("redispatchRequestAreaSell", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - public static final AttributeDescriptor REDISPATCH_LAST_POLL = new AttributeDescriptor<>("redispatchLastPoll", ValueType.TIMESTAMP, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - // --- Redispatch bid attributes (auto-calculated, operator-overridable) --- - - public static final AttributeDescriptor REDISPATCH_SUGGESTED_POWER = new AttributeDescriptor<>("redispatchSuggestedPower", ValueType.NUMBER, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), - new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ).withUnits(UNITS_KILO, UNITS_WATT); - - public static final AttributeDescriptor REDISPATCH_SUGGESTED_VOLUME = new AttributeDescriptor<>("redispatchSuggestedVolume", ValueType.NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - - public static final AttributeDescriptor REDISPATCH_BID_PRICE = new AttributeDescriptor<>("redispatchBidPrice", ValueType.NUMBER - ).withUnits("EUR", UNITS_PER, UNITS_MEGA, UNITS_WATT, UNITS_HOUR); - - // --- Redispatch confirmation workflow --- - - public static final AttributeDescriptor REDISPATCH_CONFIRM_BID = new AttributeDescriptor<>("redispatchConfirmBid", ValueType.BOOLEAN - ); - - public static final AttributeDescriptor REDISPATCH_BID_STATUS = new AttributeDescriptor<>("redispatchBidStatus", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY) - ); - - // --- Redispatch history (stored as data points for time-series history) --- - - public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_HISTORY = new AttributeDescriptor<>("redispatchAnnouncementHistory", ValueType.JSON_OBJECT, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 90), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ); - - public static final AttributeDescriptor REDISPATCH_BID_HISTORY = new AttributeDescriptor<>("redispatchBidHistory", ValueType.JSON_OBJECT, - new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 90), - new MetaItem<>(MetaItemType.READ_ONLY), - new MetaItem<>(MetaItemType.STORE_DATA_POINTS) - ); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("transmission-tower", null, EmsGOPACSAsset.class); - - protected EmsGOPACSAsset() { - } - - public EmsGOPACSAsset(String name) { - super(name); - } - - // --- UFTP getters --- - - public Optional getContractedEan() { - return getAttributes().getValue(CONTRACTED_EAN); - } - - // --- Redispatch getters --- - - public Optional getRedispatchEnabled() { - return getAttributes().getValue(REDISPATCH_ENABLED); - } - - public Optional getRedispatchAnnouncementId() { - return getAttributes().getValue(REDISPATCH_ANNOUNCEMENT_ID); - } - - public Optional getRedispatchBidStatus() { - return getAttributes().getValue(REDISPATCH_BID_STATUS); - } - - public Optional getRedispatchBidPrice() { - return getAttributes().getValue(REDISPATCH_BID_PRICE); - } - - public Optional getRedispatchConfirmBid() { - return getAttributes().getValue(REDISPATCH_CONFIRM_BID); - } - - // --- Redispatch setters --- + // --- UFTP / Day-Ahead attributes --- + + public static final AttributeDescriptor CONTRACTED_EAN = + new AttributeDescriptor<>("contractedEAN", ValueType.TEXT); + + public static final AttributeDescriptor CURRENT_POWER_FLEX_REQUEST = + new AttributeDescriptor<>( + "currentPowerFlexRequest", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER = + new AttributeDescriptor<>( + "powerLimitMaximumProfileFlexOrder", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER = + new AttributeDescriptor<>( + "powerLimitMinimumProfileFlexOrder", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_MAXIMUM_FLEX_REQUEST = + new AttributeDescriptor<>( + "powerMaximumFlexRequest", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor POWER_MINIMUM_FLEX_REQUEST = + new AttributeDescriptor<>( + "powerMinimumFlexRequest", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + // --- Redispatch configuration attributes --- + + public static final AttributeDescriptor REDISPATCH_ENABLED = + new AttributeDescriptor<>("redispatchEnabled", ValueType.BOOLEAN); + + // --- Redispatch status attributes (read-only, system-updated) --- + + public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_ID = + new AttributeDescriptor<>( + "redispatchAnnouncementId", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_COMPLIANCE_TYPE = + new AttributeDescriptor<>( + "redispatchComplianceType", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_MESSAGE = + new AttributeDescriptor<>( + "redispatchAnnouncementMessage", + ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE), + new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_START_TIME = + new AttributeDescriptor<>( + "redispatchStartTime", ValueType.TIMESTAMP, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_END_TIME = + new AttributeDescriptor<>( + "redispatchEndTime", ValueType.TIMESTAMP, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_BID_VALIDITY_END = + new AttributeDescriptor<>( + "redispatchBidValidityEnd", ValueType.TIMESTAMP, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_REQUESTED_POWER = + new AttributeDescriptor<>( + "redispatchRequestedPower", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor REDISPATCH_EAN_EFFECTIVITY = + new AttributeDescriptor<>( + "redispatchEanEffectivity", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_REQUEST_AREA_BUY = + new AttributeDescriptor<>( + "redispatchRequestAreaBuy", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_REQUEST_AREA_SELL = + new AttributeDescriptor<>( + "redispatchRequestAreaSell", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + public static final AttributeDescriptor REDISPATCH_LAST_POLL = + new AttributeDescriptor<>( + "redispatchLastPoll", ValueType.TIMESTAMP, new MetaItem<>(MetaItemType.READ_ONLY)); + + // --- Redispatch bid attributes (auto-calculated, operator-overridable) --- + + public static final AttributeDescriptor REDISPATCH_SUGGESTED_POWER = + new AttributeDescriptor<>( + "redispatchSuggestedPower", + ValueType.NUMBER, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), + new MetaItem<>(MetaItemType.HAS_PREDICTED_DATA_POINTS), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor REDISPATCH_SUGGESTED_VOLUME = + new AttributeDescriptor<>( + "redispatchSuggestedVolume", ValueType.NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + + public static final AttributeDescriptor REDISPATCH_BID_PRICE = + new AttributeDescriptor<>("redispatchBidPrice", ValueType.NUMBER) + .withUnits("EUR", UNITS_PER, UNITS_MEGA, UNITS_WATT, UNITS_HOUR); + + // --- Redispatch confirmation workflow --- + + public static final AttributeDescriptor REDISPATCH_CONFIRM_BID = + new AttributeDescriptor<>("redispatchConfirmBid", ValueType.BOOLEAN); + + public static final AttributeDescriptor REDISPATCH_BID_STATUS = + new AttributeDescriptor<>( + "redispatchBidStatus", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + + // --- Redispatch history (stored as data points for time-series history) --- + + public static final AttributeDescriptor REDISPATCH_ANNOUNCEMENT_HISTORY = + new AttributeDescriptor<>( + "redispatchAnnouncementHistory", + ValueType.JSON_OBJECT, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 90), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)); + + public static final AttributeDescriptor REDISPATCH_BID_HISTORY = + new AttributeDescriptor<>( + "redispatchBidHistory", + ValueType.JSON_OBJECT, + new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 90), + new MetaItem<>(MetaItemType.READ_ONLY), + new MetaItem<>(MetaItemType.STORE_DATA_POINTS)); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("transmission-tower", null, EmsGOPACSAsset.class); + + protected EmsGOPACSAsset() {} + + public EmsGOPACSAsset(String name) { + super(name); + } + + // --- UFTP getters --- + + public Optional getContractedEan() { + return getAttributes().getValue(CONTRACTED_EAN); + } + + // --- Redispatch getters --- + + public Optional getRedispatchEnabled() { + return getAttributes().getValue(REDISPATCH_ENABLED); + } + + public Optional getRedispatchAnnouncementId() { + return getAttributes().getValue(REDISPATCH_ANNOUNCEMENT_ID); + } + + public Optional getRedispatchBidStatus() { + return getAttributes().getValue(REDISPATCH_BID_STATUS); + } + + public Optional getRedispatchBidPrice() { + return getAttributes().getValue(REDISPATCH_BID_PRICE); + } + + public Optional getRedispatchConfirmBid() { + return getAttributes().getValue(REDISPATCH_CONFIRM_BID); + } + + // --- Redispatch setters --- - public EmsGOPACSAsset setRedispatchEnabled(Boolean enabled) { - getAttributes().getOrCreate(REDISPATCH_ENABLED).setValue(enabled); - return this; - } + public EmsGOPACSAsset setRedispatchEnabled(Boolean enabled) { + getAttributes().getOrCreate(REDISPATCH_ENABLED).setValue(enabled); + return this; + } - public EmsGOPACSAsset setRedispatchBidPrice(Double bidPrice) { - getAttributes().getOrCreate(REDISPATCH_BID_PRICE).setValue(bidPrice); - return this; - } + public EmsGOPACSAsset setRedispatchBidPrice(Double bidPrice) { + getAttributes().getOrCreate(REDISPATCH_BID_PRICE).setValue(bidPrice); + return this; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationService.java b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationService.java index 3077c1b..5b6563e 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationService.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/EmsOptimisationService.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,29 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager; +import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; +import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; +import static org.openremote.manager.gateway.GatewayService.isNotForGateway; +import static org.openremote.model.syslog.SyslogCategory.DATA; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + import org.apache.camel.builder.RouteBuilder; import org.openremote.container.message.MessageBrokerService; import org.openremote.container.timer.TimerService; @@ -45,850 +61,1135 @@ import org.openremote.model.query.AssetQuery; import org.openremote.model.syslog.SyslogCategory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.text.SimpleDateFormat; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; -import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; -import static org.openremote.manager.gateway.GatewayService.isNotForGateway; -import static org.openremote.model.syslog.SyslogCategory.DATA; - public class EmsOptimisationService extends RouteBuilder implements ContainerService { - protected static final Logger LOG = SyslogCategory.getLogger(DATA, EmsOptimisationService.class.getName()); - protected Services services; - - protected GOPACSHandler.Factory gopacsHandlerFactory; - protected GOPACSRedispatchHandler.Factory gopacsRedispatchHandlerFactory; - - private final Map> energyOptimisationAssetsMap = new ConcurrentHashMap<>(); - private final Map energyOptimisationTimersMap = new HashMap<>(); - private final Map gopacsHandlerMap = new HashMap<>(); - private final Map gopacsRedispatchHandlerMap = new HashMap<>(); - - - @SuppressWarnings("unchecked") - @Override - public void configure() throws Exception { - from(PERSISTENCE_TOPIC) - .routeId("Persistence-EmsEnergyOptimisation") - .filter(isPersistenceEventForEntityType(Asset.class)) - .filter(isNotForGateway(services.getGatewayService())) - .process(exchange -> processAssetChange(exchange.getIn().getBody(PersistenceEvent.class))); - } - - @Override - public void init(Container container) throws Exception { - services = Services.builder() - .withAssetDatapointService(container.getService(AssetDatapointService.class)) - .withAssetPredictedDatapointService(container.getService(AssetPredictedDatapointService.class)) - .withAssetProcessingService(container.getService(AssetProcessingService.class)) - .withAssetStorageService(container.getService(AssetStorageService.class)) - .withClientEventService(container.getService(ClientEventService.class)) - .withGatewayService(container.getService(GatewayService.class)) - .withMessageBrokerService(container.getService(MessageBrokerService.class)) - .withScheduledExecutorService(container.getScheduledExecutor()) - .withTimerService(container.getService(TimerService.class)) - .build(); - - gopacsHandlerFactory = new GOPACSHandler.Factory(container); - gopacsRedispatchHandlerFactory = new GOPACSRedispatchHandler.Factory(container); - } - - @Override - public void start(Container container) throws Exception { - // Add service - services.getMessageBrokerService().getContext().addRoutes(this); - - // Find all energy optimisation assets - List energyOptimisationAssets = services.getAssetStorageService() - .findAll(new AssetQuery().types(EmsEnergyOptimisationAsset.class)) - .stream() - .map(asset -> (EmsEnergyOptimisationAsset) asset) - .toList(); - - // Start optimisation for enabled energy optimisation assets - if (!energyOptimisationAssets.isEmpty()) { - List enabledEnergyOptimisationAssetIds = energyOptimisationAssets - .stream() - .filter(energyOptimisationAsset -> services.getGatewayService().getLocallyRegisteredGatewayId(energyOptimisationAsset.getId(), null) == null) - .filter(energyOptimisationAsset -> !energyOptimisationAsset.getOptimisationDisabled().orElse(false)) - .map(Asset::getId) - .toList(); - - LOG.info(String.format("Number of enabled '%s' assets = %s", EmsEnergyOptimisationAsset.class.getSimpleName(), enabledEnergyOptimisationAssetIds.size())); - - enabledEnergyOptimisationAssetIds.forEach(this::startOptimisation); - } - - // Start GOPACS handler for all GOPACS assets - services.getAssetStorageService() - .findAll(new AssetQuery().types(EmsGOPACSAsset.class).attributeName(EmsGOPACSAsset.CONTRACTED_EAN.getName())) - .stream() - .map(asset -> (EmsGOPACSAsset) asset) - .forEach(gopacsAsset -> { - startGopacsHandler(gopacsAsset.getContractedEan().orElse(""), gopacsAsset.getRealm(), gopacsAsset.getId()); - - // Start redispatch handler if enabled - if (gopacsAsset.getRedispatchEnabled().orElse(false)) { - startRedispatchHandler(gopacsAsset.getContractedEan().orElse(""), gopacsAsset.getRealm(), gopacsAsset.getId()); - } - }); - - - // List of asset types that are part of the core EMS service - String[] assetTypes = { - EmsElectricityBatteryAsset.DESCRIPTOR.getName(), - EmsEnergyOptimisationAsset.DESCRIPTOR.getName(), - EmsGOPACSAsset.DESCRIPTOR.getName() - }; - - // Listen to attribute events of listed asset types - services.getClientEventService().addSubscription( - AttributeEvent.class, - new AssetFilter().setAssetTypes(assetTypes), - this::processAttributeEvent); + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, EmsOptimisationService.class.getName()); + protected Services services; + + protected GOPACSHandler.Factory gopacsHandlerFactory; + protected GOPACSRedispatchHandler.Factory gopacsRedispatchHandlerFactory; + + private final Map> energyOptimisationAssetsMap = + new ConcurrentHashMap<>(); + private final Map energyOptimisationTimersMap = new HashMap<>(); + private final Map gopacsHandlerMap = new HashMap<>(); + private final Map gopacsRedispatchHandlerMap = new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public void configure() throws Exception { + from(PERSISTENCE_TOPIC) + .routeId("Persistence-EmsEnergyOptimisation") + .filter(isPersistenceEventForEntityType(Asset.class)) + .filter(isNotForGateway(services.getGatewayService())) + .process(exchange -> processAssetChange(exchange.getIn().getBody(PersistenceEvent.class))); + } + + @Override + public void init(Container container) throws Exception { + services = + Services.builder() + .withAssetDatapointService(container.getService(AssetDatapointService.class)) + .withAssetPredictedDatapointService( + container.getService(AssetPredictedDatapointService.class)) + .withAssetProcessingService(container.getService(AssetProcessingService.class)) + .withAssetStorageService(container.getService(AssetStorageService.class)) + .withClientEventService(container.getService(ClientEventService.class)) + .withGatewayService(container.getService(GatewayService.class)) + .withMessageBrokerService(container.getService(MessageBrokerService.class)) + .withScheduledExecutorService(container.getScheduledExecutor()) + .withTimerService(container.getService(TimerService.class)) + .build(); + + gopacsHandlerFactory = new GOPACSHandler.Factory(container); + gopacsRedispatchHandlerFactory = new GOPACSRedispatchHandler.Factory(container); + } + + @Override + public void start(Container container) throws Exception { + // Add service + services.getMessageBrokerService().getContext().addRoutes(this); + + // Find all energy optimisation assets + List energyOptimisationAssets = + services + .getAssetStorageService() + .findAll(new AssetQuery().types(EmsEnergyOptimisationAsset.class)) + .stream() + .map(asset -> (EmsEnergyOptimisationAsset) asset) + .toList(); + + // Start optimisation for enabled energy optimisation assets + if (!energyOptimisationAssets.isEmpty()) { + List enabledEnergyOptimisationAssetIds = + energyOptimisationAssets.stream() + .filter( + energyOptimisationAsset -> + services + .getGatewayService() + .getLocallyRegisteredGatewayId(energyOptimisationAsset.getId(), null) + == null) + .filter( + energyOptimisationAsset -> + !energyOptimisationAsset.getOptimisationDisabled().orElse(false)) + .map(Asset::getId) + .toList(); + + LOG.info( + String.format( + "Number of enabled '%s' assets = %s", + EmsEnergyOptimisationAsset.class.getSimpleName(), + enabledEnergyOptimisationAssetIds.size())); + + enabledEnergyOptimisationAssetIds.forEach(this::startOptimisation); } - @Override - public void stop(Container container) throws Exception { - gopacsRedispatchHandlerMap.forEach((ean, handler) -> handler.stopPolling()); - gopacsRedispatchHandlerMap.clear(); - energyOptimisationAssetsMap.forEach((assetId, scheduledFuture) -> stopOptimisation(assetId)); - energyOptimisationTimersMap.clear(); - } + // Start GOPACS handler for all GOPACS assets + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .types(EmsGOPACSAsset.class) + .attributeName(EmsGOPACSAsset.CONTRACTED_EAN.getName())) + .stream() + .map(asset -> (EmsGOPACSAsset) asset) + .forEach( + gopacsAsset -> { + startGopacsHandler( + gopacsAsset.getContractedEan().orElse(""), + gopacsAsset.getRealm(), + gopacsAsset.getId()); + + // Start redispatch handler if enabled + if (gopacsAsset.getRedispatchEnabled().orElse(false)) { + startRedispatchHandler( + gopacsAsset.getContractedEan().orElse(""), + gopacsAsset.getRealm(), + gopacsAsset.getId()); + } + }); - private void startOptimisation(String assetId) { - final Runnable command = () -> { - try { - runOptimisation(assetId); - } catch (Exception e) { - LOG.severe(String.format("assetType='%s', assetId='%s'; Failed to run energy optimisation; Exception: %s", EmsEnergyOptimisationAsset.class.getSimpleName(), assetId, e)); - } + // List of asset types that are part of the core EMS service + String[] assetTypes = { + EmsElectricityBatteryAsset.DESCRIPTOR.getName(), + EmsEnergyOptimisationAsset.DESCRIPTOR.getName(), + EmsGOPACSAsset.DESCRIPTOR.getName() + }; + + // Listen to attribute events of listed asset types + services + .getClientEventService() + .addSubscription( + AttributeEvent.class, + new AssetFilter().setAssetTypes(assetTypes), + this::processAttributeEvent); + } + + @Override + public void stop(Container container) throws Exception { + gopacsRedispatchHandlerMap.forEach((ean, handler) -> handler.stopPolling()); + gopacsRedispatchHandlerMap.clear(); + energyOptimisationAssetsMap.forEach((assetId, scheduledFuture) -> stopOptimisation(assetId)); + energyOptimisationTimersMap.clear(); + } + + private void startOptimisation(String assetId) { + final Runnable command = + () -> { + try { + runOptimisation(assetId); + } catch (Exception e) { + LOG.severe( + String.format( + "assetType='%s', assetId='%s'; Failed to run energy optimisation; Exception: %s", + EmsEnergyOptimisationAsset.class.getSimpleName(), assetId, e)); + } }; - ScheduledFuture scheduledFuture = services.getScheduledExecutorService().scheduleAtFixedRate(command, 1, 1, TimeUnit.MINUTES); - energyOptimisationAssetsMap.put(assetId, scheduledFuture); + ScheduledFuture scheduledFuture = + services.getScheduledExecutorService().scheduleAtFixedRate(command, 1, 1, TimeUnit.MINUTES); + energyOptimisationAssetsMap.put(assetId, scheduledFuture); - energyOptimisationTimersMap.put(assetId, 0L); - } + energyOptimisationTimersMap.put(assetId, 0L); + } - private void runOptimisation(String energyOptimisationAssetId) throws Exception { - // Get latest asset from database - EmsEnergyOptimisationAsset energyOptimisationAsset = (EmsEnergyOptimisationAsset) services.getAssetStorageService().find(energyOptimisationAssetId); + private void runOptimisation(String energyOptimisationAssetId) throws Exception { + // Get latest asset from database + EmsEnergyOptimisationAsset energyOptimisationAsset = + (EmsEnergyOptimisationAsset) + services.getAssetStorageService().find(energyOptimisationAssetId); - if (energyOptimisationAsset == null) { - return; - } + if (energyOptimisationAsset == null) { + return; + } - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - Long triggerTimeMillis = energyOptimisationTimersMap.get(energyOptimisationAssetId); - - // Update forecast attributes every 15 minutes - if (triggerTimeMillis != null && currentTimeMillis > triggerTimeMillis) { - // Convert milliseconds to date - ZoneId zone = ZoneId.systemDefault(); - LocalDate previousDate = Instant.ofEpochMilli(triggerTimeMillis - 60000).atZone(zone).toLocalDate(); - LocalDate currentDate = Instant.ofEpochMilli(currentTimeMillis).atZone(zone).toLocalDate(); - - // Update manual forecasts after midnight - if (!currentDate.equals(previousDate)) { - updatePowerLimitProfileManualForecasts(energyOptimisationAsset); - } - - EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); - - // Update total forecasts - updatePowerLimitProfileTotalForecasts(energyOptimisationAsset, gopacsAsset); - - // Update energy optimisation asset attributes - String[] energyOptimisationAssetAttributeNames = new String[]{ - EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), - EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), - EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), - EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), - EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), - EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName() + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + Long triggerTimeMillis = energyOptimisationTimersMap.get(energyOptimisationAssetId); + + // Update forecast attributes every 15 minutes + if (triggerTimeMillis != null && currentTimeMillis > triggerTimeMillis) { + // Convert milliseconds to date + ZoneId zone = ZoneId.systemDefault(); + LocalDate previousDate = + Instant.ofEpochMilli(triggerTimeMillis - 60000).atZone(zone).toLocalDate(); + LocalDate currentDate = Instant.ofEpochMilli(currentTimeMillis).atZone(zone).toLocalDate(); + + // Update manual forecasts after midnight + if (!currentDate.equals(previousDate)) { + updatePowerLimitProfileManualForecasts(energyOptimisationAsset); + } + + EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); + + // Update total forecasts + updatePowerLimitProfileTotalForecasts(energyOptimisationAsset, gopacsAsset); + + // Update energy optimisation asset attributes + String[] energyOptimisationAssetAttributeNames = + new String[] { + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName() + }; + + updatePowerLimitProfileAttributes( + energyOptimisationAsset, energyOptimisationAssetAttributeNames); + + if (gopacsAsset != null) { + // Update GOPACS asset attributes + String[] gopacsAttributeNames = + new String[] { + EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), + EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName() }; - updatePowerLimitProfileAttributes(energyOptimisationAsset, energyOptimisationAssetAttributeNames); + updatePowerLimitProfileAttributes(gopacsAsset, gopacsAttributeNames); + } - if (gopacsAsset != null) { - // Update GOPACS asset attributes - String[] gopacsAttributeNames = new String[]{ - EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), - EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName() - }; - - updatePowerLimitProfileAttributes(gopacsAsset, gopacsAttributeNames); - } - - // Calculate new trigger time - triggerTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60 * 1000) + (15 * 60 * 1000); - energyOptimisationTimersMap.put(energyOptimisationAssetId, triggerTimeMillis); - } - - // Run selected optimisation method - String optimisationMethodName = energyOptimisationAsset.getOptimisationMethod().orElse(EmsEnergyOptimisationAsset.OptimisationMethodValueType.None).toString(); - OptimisationMethodsLoader optimisationMethodsLoader = new OptimisationMethodsLoader(); - optimisationMethodsLoader.runOptimisationMethod(optimisationMethodName, energyOptimisationAssetId, services); + // Calculate new trigger time + triggerTimeMillis = + currentTimeMillis - currentTimeMillis % (15 * 60 * 1000) + (15 * 60 * 1000); + energyOptimisationTimersMap.put(energyOptimisationAssetId, triggerTimeMillis); } - private void stopOptimisation(String assetId) { - ScheduledFuture scheduledFuture = energyOptimisationAssetsMap.remove(assetId); - energyOptimisationTimersMap.remove(assetId); - - if (scheduledFuture != null) { - scheduledFuture.cancel(false); - } - - services.getAssetPredictedDatapointService().purgeValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName()); - services.getAssetPredictedDatapointService().purgeValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName()); - services.getAssetPredictedDatapointService().purgeValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName()); - services.getAssetPredictedDatapointService().purgeValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName()); + // Run selected optimisation method + String optimisationMethodName = + energyOptimisationAsset + .getOptimisationMethod() + .orElse(EmsEnergyOptimisationAsset.OptimisationMethodValueType.None) + .toString(); + OptimisationMethodsLoader optimisationMethodsLoader = new OptimisationMethodsLoader(); + optimisationMethodsLoader.runOptimisationMethod( + optimisationMethodName, energyOptimisationAssetId, services); + } + + private void stopOptimisation(String assetId) { + ScheduledFuture scheduledFuture = energyOptimisationAssetsMap.remove(assetId); + energyOptimisationTimersMap.remove(assetId); + + if (scheduledFuture != null) { + scheduledFuture.cancel(false); } - private void startGopacsHandler(String contractedEan, String realm, String assetId) { - if (contractedEan.isBlank()) { - LOG.warning("Unable to deploy GOPACS because EAN is blank"); - return; - } - LOG.fine("Deploying GOPACS for EAN: " + contractedEan); - gopacsHandlerMap.put(contractedEan, gopacsHandlerFactory.createHandler( - contractedEan, - realm, - assetId) - ); - LOG.fine("Deployed GOPACS for EAN: " + contractedEan); - } - - private void stopGopacsHandler(String contractedEan) { - GOPACSHandler existing = gopacsHandlerMap.get(contractedEan); - if (existing != null) { - existing.undeploy(); - gopacsHandlerMap.remove(contractedEan); - } + services + .getAssetPredictedDatapointService() + .purgeValues( + assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName()); + services + .getAssetPredictedDatapointService() + .purgeValues( + assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName()); + services + .getAssetPredictedDatapointService() + .purgeValues( + assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName()); + services + .getAssetPredictedDatapointService() + .purgeValues( + assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName()); + } + + private void startGopacsHandler(String contractedEan, String realm, String assetId) { + if (contractedEan.isBlank()) { + LOG.warning("Unable to deploy GOPACS because EAN is blank"); + return; } - - private void startRedispatchHandler(String contractedEan, String realm, String assetId) { - if (contractedEan.isBlank()) { - LOG.warning("Unable to start redispatch handler because EAN is blank"); - return; - } - LOG.fine("Starting redispatch handler for EAN: " + contractedEan); - GOPACSRedispatchHandler handler = gopacsRedispatchHandlerFactory.createHandler(contractedEan, realm, assetId); - gopacsRedispatchHandlerMap.put(contractedEan, handler); - handler.startPolling(); - LOG.fine("Started redispatch handler for EAN: " + contractedEan); - } - - private void stopRedispatchHandler(String contractedEan) { - GOPACSRedispatchHandler existing = gopacsRedispatchHandlerMap.get(contractedEan); - if (existing != null) { - existing.stopPolling(); - gopacsRedispatchHandlerMap.remove(contractedEan); - } + LOG.fine("Deploying GOPACS for EAN: " + contractedEan); + gopacsHandlerMap.put( + contractedEan, gopacsHandlerFactory.createHandler(contractedEan, realm, assetId)); + LOG.fine("Deployed GOPACS for EAN: " + contractedEan); + } + + private void stopGopacsHandler(String contractedEan) { + GOPACSHandler existing = gopacsHandlerMap.get(contractedEan); + if (existing != null) { + existing.undeploy(); + gopacsHandlerMap.remove(contractedEan); } + } - protected void processAssetChange(PersistenceEvent persistenceEvent) { - if (persistenceEvent.getEntity() instanceof EmsEnergyOptimisationAsset emsEnergyOptimisationAsset) { - if (persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE) { - stopOptimisation(emsEnergyOptimisationAsset.getId()); - } - } else if (persistenceEvent.getEntity() instanceof EmsGOPACSAsset emsGOPACSAsset) { - emsGOPACSAsset.getContractedEan().ifPresent(contractedEan -> { + private void startRedispatchHandler(String contractedEan, String realm, String assetId) { + if (contractedEan.isBlank()) { + LOG.warning("Unable to start redispatch handler because EAN is blank"); + return; + } + LOG.fine("Starting redispatch handler for EAN: " + contractedEan); + GOPACSRedispatchHandler handler = + gopacsRedispatchHandlerFactory.createHandler(contractedEan, realm, assetId); + gopacsRedispatchHandlerMap.put(contractedEan, handler); + handler.startPolling(); + LOG.fine("Started redispatch handler for EAN: " + contractedEan); + } + + private void stopRedispatchHandler(String contractedEan) { + GOPACSRedispatchHandler existing = gopacsRedispatchHandlerMap.get(contractedEan); + if (existing != null) { + existing.stopPolling(); + gopacsRedispatchHandlerMap.remove(contractedEan); + } + } + + protected void processAssetChange(PersistenceEvent persistenceEvent) { + if (persistenceEvent.getEntity() + instanceof EmsEnergyOptimisationAsset emsEnergyOptimisationAsset) { + if (persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE) { + stopOptimisation(emsEnergyOptimisationAsset.getId()); + } + } else if (persistenceEvent.getEntity() instanceof EmsGOPACSAsset emsGOPACSAsset) { + emsGOPACSAsset + .getContractedEan() + .ifPresent( + contractedEan -> { if (persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE) { - stopGopacsHandler(contractedEan); - stopRedispatchHandler(contractedEan); + stopGopacsHandler(contractedEan); + stopRedispatchHandler(contractedEan); } if (persistenceEvent.getCause() == PersistenceEvent.Cause.CREATE) { - startGopacsHandler(contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); - if (emsGOPACSAsset.getRedispatchEnabled().orElse(false)) { - startRedispatchHandler(contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); - } + startGopacsHandler( + contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); + if (emsGOPACSAsset.getRedispatchEnabled().orElse(false)) { + startRedispatchHandler( + contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); + } } if (persistenceEvent.getCause() == PersistenceEvent.Cause.UPDATE) { - stopGopacsHandler(contractedEan); - startGopacsHandler(contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); - // Redispatch handler is managed via attribute events (redispatchEnabled) + stopGopacsHandler(contractedEan); + startGopacsHandler( + contractedEan, emsGOPACSAsset.getRealm(), emsGOPACSAsset.getId()); + // Redispatch handler is managed via attribute events (redispatchEnabled) } - }); - } + }); } + } - private void processAttributeEvent(AttributeEvent attributeEvent) { - String assetType = attributeEvent.getAssetType(); - - if (assetType.equals(EmsEnergyOptimisationAsset.DESCRIPTOR.getName())) { - processAttributeEventEmsEnergyOptimisationAsset(attributeEvent); - return; - } + private void processAttributeEvent(AttributeEvent attributeEvent) { + String assetType = attributeEvent.getAssetType(); - if (assetType.equals(EmsGOPACSAsset.DESCRIPTOR.getName())) { - processAttributeEventEmsGOPACSAsset(attributeEvent); - return; - } + if (assetType.equals(EmsEnergyOptimisationAsset.DESCRIPTOR.getName())) { + processAttributeEventEmsEnergyOptimisationAsset(attributeEvent); + return; } - private void processAttributeEventEmsEnergyOptimisationAsset(AttributeEvent attributeEvent) { - String assetId = attributeEvent.getId(); - - // Get asset from database - EmsEnergyOptimisationAsset energyOptimisationAsset = (EmsEnergyOptimisationAsset) services.getAssetStorageService().find(assetId); - - // Check if asset exists - if (energyOptimisationAsset == null) { - return; - } - - String logPrefix = String.format("assetType='%s', assetId='%s', assetName='%s', attributeName='%s'", attributeEvent.getAssetType(), attributeEvent.getId(), attributeEvent.getAssetName(), attributeEvent.getName()); - String attributeName = attributeEvent.getName(); - - // Disable/enable optimisation service - if (attributeName.equals(EmsEnergyOptimisationAsset.DISABLE_OPTIMISATION_SERVICE.getName())) { - boolean disabled = (Boolean) attributeEvent.getValue().orElse(false); - - if (!disabled && !energyOptimisationAssetsMap.containsKey(assetId)) { - if (services.getGatewayService().getLocallyRegisteredGatewayId(assetId, null) == null) { - LOG.info(String.format("%s; Enabled energy optimisation service", logPrefix)); - startOptimisation(assetId); - } - } else if (disabled && energyOptimisationAssetsMap.containsKey(assetId)) { - stopOptimisation(assetId); - LOG.info(String.format("%s; Disabled energy optimisation service", logPrefix)); - } - return; - } - - // Generate power maximum profile manual input - if (attributeName.equals(EmsEnergyOptimisationAsset.GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())) { - boolean generatePowerLimitMaximumProfileManualInput = (Boolean) attributeEvent.getValue().orElse(false); - - if (!generatePowerLimitMaximumProfileManualInput) { - return; - } - - // Add 1-second delay before resetting checkbox for user-friendliness - services.getScheduledExecutorService().schedule(() -> services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(assetId, attributeName, false), getClass().getSimpleName()), 1, TimeUnit.SECONDS); - - Double powerLimitMaximumInput = energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); - - if (powerLimitMaximumInput == null) { - LOG.warning(String.format("%s; '%s' attribute value is missing. Unable to generate power limit maximum profile", logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_INPUT.getName())); - return; - } - - String powerLimitMaximumProfileManualInput = energyOptimisationAsset.getPowerLimitMaximumProfileManualInput().orElse(""); - - if (!powerLimitMaximumProfileManualInput.isBlank()) { - LOG.warning(String.format("%s; '%s' attribute is already set. Remove current power limit maximum profile to generate a new power limit profile", logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())); - return; - } - - String powerLimitMaximumProfileCsv = generatePowerLimitProfile(powerLimitMaximumInput); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT, powerLimitMaximumProfileCsv), getClass().getSimpleName()); - } - - - // Generate power limit minimum profile input - if (attributeName.equals(EmsEnergyOptimisationAsset.GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())) { - boolean generatePowerLimitMinimumProfileManualInput = (Boolean) attributeEvent.getValue().orElse(false); - - if (!generatePowerLimitMinimumProfileManualInput) { - return; - } - - // Add 1-second delay before resetting checkbox for user-friendliness - services.getScheduledExecutorService().schedule(() -> services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(assetId, attributeName, false), getClass().getSimpleName()), 1, TimeUnit.SECONDS); - - Double powerLimitMinimumInput = energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); - - if (powerLimitMinimumInput == null) { - LOG.warning(String.format("%s; '%s' attribute value is missing. Unable to generate power limit minimum profile", logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_INPUT.getName())); - return; - } - - String powerLimitMinimumProfileManualInput = energyOptimisationAsset.getPowerLimitMinimumProfileManualInput().orElse(""); - - if (!powerLimitMinimumProfileManualInput.isBlank()) { - LOG.warning(String.format("%s; '%s' attribute is already set. Remove current power limit minimum profile to generate a new power limit profile", logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())); - return; - } - - String powerLimitMinimumProfileCsv = generatePowerLimitProfile(powerLimitMinimumInput); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT, powerLimitMinimumProfileCsv), getClass().getSimpleName()); - } - - - // Update power limit maximum profile manual - if (attributeName.equals(EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())) { - if (!energyOptimisationAssetsMap.containsKey(assetId)) { - LOG.warning(String.format("%s; Optimisation is disabled. The manual power limit profile has been updated but has not been loaded into the forecast database.", logPrefix)); - return; - } - - String powerLimitMaximumProfileManualInput = (String) attributeEvent.getValue().orElse(""); - - if (powerLimitMaximumProfileManualInput.isBlank()) { - return; - } - - // Parse CSV - List parsedCSV = parseCSV(powerLimitMaximumProfileManualInput, ",", logPrefix); - - // Convert parsed CSV to database insertable data-points - List> powerLimitMaximumProfileManual = csvToValueDatapoints(parsedCSV, logPrefix); - services.getAssetPredictedDatapointService().updateValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), powerLimitMaximumProfileManual); - - List>> powerLimitProfiles = new ArrayList<>(); - powerLimitProfiles.add(powerLimitMaximumProfileManual); + if (assetType.equals(EmsGOPACSAsset.DESCRIPTOR.getName())) { + processAttributeEventEmsGOPACSAsset(attributeEvent); + return; + } + } - EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); + private void processAttributeEventEmsEnergyOptimisationAsset(AttributeEvent attributeEvent) { + String assetId = attributeEvent.getId(); - if (gopacsAsset != null) { - // Forecast data-point range of 1 week - long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + // Get asset from database + EmsEnergyOptimisationAsset energyOptimisationAsset = + (EmsEnergyOptimisationAsset) services.getAssetStorageService().find(assetId); - // Get power limit profile day-ahead - List> powerLimitMaximumProfileDayAhead = services.getAssetPredictedDatapointService().queryDatapoints(gopacsAsset.getId(), EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), assetDatapointQuery); - powerLimitProfiles.add(powerLimitMaximumProfileDayAhead); - } + // Check if asset exists + if (energyOptimisationAsset == null) { + return; + } - // Update power limit maximum profile total - List> powerLimitMaximumProfileTotal = calculatePowerLimitProfileTotal(powerLimitProfiles, "floor"); - services.getAssetPredictedDatapointService().updateValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal); + String logPrefix = + String.format( + "assetType='%s', assetId='%s', assetName='%s', attributeName='%s'", + attributeEvent.getAssetType(), + attributeEvent.getId(), + attributeEvent.getAssetName(), + attributeEvent.getName()); + String attributeName = attributeEvent.getName(); + + // Disable/enable optimisation service + if (attributeName.equals(EmsEnergyOptimisationAsset.DISABLE_OPTIMISATION_SERVICE.getName())) { + boolean disabled = (Boolean) attributeEvent.getValue().orElse(false); + + if (!disabled && !energyOptimisationAssetsMap.containsKey(assetId)) { + if (services.getGatewayService().getLocallyRegisteredGatewayId(assetId, null) == null) { + LOG.info(String.format("%s; Enabled energy optimisation service", logPrefix)); + startOptimisation(assetId); } + } else if (disabled && energyOptimisationAssetsMap.containsKey(assetId)) { + stopOptimisation(assetId); + LOG.info(String.format("%s; Disabled energy optimisation service", logPrefix)); + } + return; + } + // Generate power maximum profile manual input + if (attributeName.equals( + EmsEnergyOptimisationAsset.GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())) { + boolean generatePowerLimitMaximumProfileManualInput = + (Boolean) attributeEvent.getValue().orElse(false); + + if (!generatePowerLimitMaximumProfileManualInput) { + return; + } + + // Add 1-second delay before resetting checkbox for user-friendliness + services + .getScheduledExecutorService() + .schedule( + () -> + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent(assetId, attributeName, false), + getClass().getSimpleName()), + 1, + TimeUnit.SECONDS); + + Double powerLimitMaximumInput = + energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); + + if (powerLimitMaximumInput == null) { + LOG.warning( + String.format( + "%s; '%s' attribute value is missing. Unable to generate power limit maximum profile", + logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_INPUT.getName())); + return; + } + + String powerLimitMaximumProfileManualInput = + energyOptimisationAsset.getPowerLimitMaximumProfileManualInput().orElse(""); + + if (!powerLimitMaximumProfileManualInput.isBlank()) { + LOG.warning( + String.format( + "%s; '%s' attribute is already set. Remove current power limit maximum profile to generate a new power limit profile", + logPrefix, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())); + return; + } + + String powerLimitMaximumProfileCsv = generatePowerLimitProfile(powerLimitMaximumInput); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT, + powerLimitMaximumProfileCsv), + getClass().getSimpleName()); + } - // Update power limit minimum profile manual - if (attributeName.equals(EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())) { - if (!energyOptimisationAssetsMap.containsKey(assetId)) { - LOG.warning(String.format("%s; Optimisation is disabled. The manual power limit profile has been updated but has not been loaded into the forecast database.", logPrefix)); - return; - } - - String powerLimitMinimumProfileManualInput = (String) attributeEvent.getValue().orElse(""); - - if (powerLimitMinimumProfileManualInput.isBlank()) { - return; - } - - // Parse CSV - List parsedCSV = parseCSV(powerLimitMinimumProfileManualInput, ",", logPrefix); - - // Convert parsed CSV to database insertable data-points - List> powerLimitMinimumProfileManual = csvToValueDatapoints(parsedCSV, logPrefix); - services.getAssetPredictedDatapointService().updateValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), powerLimitMinimumProfileManual); - - List>> powerLimitProfiles = new ArrayList<>(); - powerLimitProfiles.add(powerLimitMinimumProfileManual); - - EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); - - if (gopacsAsset != null) { - // Forecast data-point range of 1 week - long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - // Get power limit profile day-ahead - List> powerLimitMinimumProfileDayAhead = services.getAssetPredictedDatapointService().queryDatapoints(gopacsAsset.getId(), EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), assetDatapointQuery); - powerLimitProfiles.add(powerLimitMinimumProfileDayAhead); - } - - // Update power limit minimum profile total - List> powerLimitMinimumProfileTotal = calculatePowerLimitProfileTotal(powerLimitProfiles, "ceil"); - services.getAssetPredictedDatapointService().updateValues(assetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal); - } + // Generate power limit minimum profile input + if (attributeName.equals( + EmsEnergyOptimisationAsset.GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())) { + boolean generatePowerLimitMinimumProfileManualInput = + (Boolean) attributeEvent.getValue().orElse(false); + + if (!generatePowerLimitMinimumProfileManualInput) { + return; + } + + // Add 1-second delay before resetting checkbox for user-friendliness + services + .getScheduledExecutorService() + .schedule( + () -> + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent(assetId, attributeName, false), + getClass().getSimpleName()), + 1, + TimeUnit.SECONDS); + + Double powerLimitMinimumInput = + energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); + + if (powerLimitMinimumInput == null) { + LOG.warning( + String.format( + "%s; '%s' attribute value is missing. Unable to generate power limit minimum profile", + logPrefix, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_INPUT.getName())); + return; + } + + String powerLimitMinimumProfileManualInput = + energyOptimisationAsset.getPowerLimitMinimumProfileManualInput().orElse(""); + + if (!powerLimitMinimumProfileManualInput.isBlank()) { + LOG.warning( + String.format( + "%s; '%s' attribute is already set. Remove current power limit minimum profile to generate a new power limit profile", + logPrefix, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())); + return; + } + + String powerLimitMinimumProfileCsv = generatePowerLimitProfile(powerLimitMinimumInput); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT, + powerLimitMinimumProfileCsv), + getClass().getSimpleName()); + } + // Update power limit maximum profile manual + if (attributeName.equals( + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT.getName())) { + if (!energyOptimisationAssetsMap.containsKey(assetId)) { + LOG.warning( + String.format( + "%s; Optimisation is disabled. The manual power limit profile has been updated but has not been loaded into the forecast database.", + logPrefix)); + return; + } + + String powerLimitMaximumProfileManualInput = (String) attributeEvent.getValue().orElse(""); + + if (powerLimitMaximumProfileManualInput.isBlank()) { + return; + } + + // Parse CSV + List parsedCSV = parseCSV(powerLimitMaximumProfileManualInput, ",", logPrefix); + + // Convert parsed CSV to database insertable data-points + List> powerLimitMaximumProfileManual = + csvToValueDatapoints(parsedCSV, logPrefix); + services + .getAssetPredictedDatapointService() + .updateValues( + assetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), + powerLimitMaximumProfileManual); + + List>> powerLimitProfiles = new ArrayList<>(); + powerLimitProfiles.add(powerLimitMaximumProfileManual); + + EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); + + if (gopacsAsset != null) { + // Forecast data-point range of 1 week + long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = + startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get power limit profile day-ahead + List> powerLimitMaximumProfileDayAhead = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + gopacsAsset.getId(), + EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), + assetDatapointQuery); + powerLimitProfiles.add(powerLimitMaximumProfileDayAhead); + } + + // Update power limit maximum profile total + List> powerLimitMaximumProfileTotal = + calculatePowerLimitProfileTotal(powerLimitProfiles, "floor"); + services + .getAssetPredictedDatapointService() + .updateValues( + assetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + powerLimitMaximumProfileTotal); + } - // Run selected optimisation method - if (attributeName.equals(EmsEnergyOptimisationAsset.OPTIMISATION_METHOD.getName())) { - if (energyOptimisationAssetsMap.containsKey(assetId)) { - // Reset advanced settings attributes field - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, null), getClass().getSimpleName()); + // Update power limit minimum profile manual + if (attributeName.equals( + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT.getName())) { + if (!energyOptimisationAssetsMap.containsKey(assetId)) { + LOG.warning( + String.format( + "%s; Optimisation is disabled. The manual power limit profile has been updated but has not been loaded into the forecast database.", + logPrefix)); + return; + } + + String powerLimitMinimumProfileManualInput = (String) attributeEvent.getValue().orElse(""); + + if (powerLimitMinimumProfileManualInput.isBlank()) { + return; + } + + // Parse CSV + List parsedCSV = parseCSV(powerLimitMinimumProfileManualInput, ",", logPrefix); + + // Convert parsed CSV to database insertable data-points + List> powerLimitMinimumProfileManual = + csvToValueDatapoints(parsedCSV, logPrefix); + services + .getAssetPredictedDatapointService() + .updateValues( + assetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), + powerLimitMinimumProfileManual); + + List>> powerLimitProfiles = new ArrayList<>(); + powerLimitProfiles.add(powerLimitMinimumProfileManual); + + EmsGOPACSAsset gopacsAsset = getGopacsAsset(energyOptimisationAsset); + + if (gopacsAsset != null) { + // Forecast data-point range of 1 week + long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = + startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get power limit profile day-ahead + List> powerLimitMinimumProfileDayAhead = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + gopacsAsset.getId(), + EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), + assetDatapointQuery); + powerLimitProfiles.add(powerLimitMinimumProfileDayAhead); + } + + // Update power limit minimum profile total + List> powerLimitMinimumProfileTotal = + calculatePowerLimitProfileTotal(powerLimitProfiles, "ceil"); + services + .getAssetPredictedDatapointService() + .updateValues( + assetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + powerLimitMinimumProfileTotal); + } - String optimisationMethodName = attributeEvent.getValue().orElse(EmsEnergyOptimisationAsset.OptimisationMethodValueType.None).toString(); - OptimisationMethodsLoader optimisationMethodsLoader = new OptimisationMethodsLoader(); - services.getScheduledExecutorService().schedule(() -> optimisationMethodsLoader.runOptimisationMethod(optimisationMethodName, assetId, services), 1, TimeUnit.SECONDS); - } - } + // Run selected optimisation method + if (attributeName.equals(EmsEnergyOptimisationAsset.OPTIMISATION_METHOD.getName())) { + if (energyOptimisationAssetsMap.containsKey(assetId)) { + // Reset advanced settings attributes field + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, + null), + getClass().getSimpleName()); + + String optimisationMethodName = + attributeEvent + .getValue() + .orElse(EmsEnergyOptimisationAsset.OptimisationMethodValueType.None) + .toString(); + OptimisationMethodsLoader optimisationMethodsLoader = new OptimisationMethodsLoader(); + services + .getScheduledExecutorService() + .schedule( + () -> + optimisationMethodsLoader.runOptimisationMethod( + optimisationMethodName, assetId, services), + 1, + TimeUnit.SECONDS); + } } + } - private void processAttributeEventEmsGOPACSAsset(AttributeEvent attributeEvent) { - String assetId = attributeEvent.getId(); + private void processAttributeEventEmsGOPACSAsset(AttributeEvent attributeEvent) { + String assetId = attributeEvent.getId(); - // Get asset from database - EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) services.getAssetStorageService().find(assetId); + // Get asset from database + EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) services.getAssetStorageService().find(assetId); - // Check if asset exists - if (gopacsAsset == null) { - return; - } + // Check if asset exists + if (gopacsAsset == null) { + return; + } - String attributeName = attributeEvent.getName(); + String attributeName = attributeEvent.getName(); - if (attributeName.equals(EmsGOPACSAsset.CONTRACTED_EAN.getName())) { - attributeEvent.getOldValue(String.class).ifPresent(oldEan -> { + if (attributeName.equals(EmsGOPACSAsset.CONTRACTED_EAN.getName())) { + attributeEvent + .getOldValue(String.class) + .ifPresent( + oldEan -> { stopGopacsHandler(oldEan); stopRedispatchHandler(oldEan); - }); - attributeEvent.getValue(String.class).ifPresent(contractedEan -> { - startGopacsHandler(contractedEan, attributeEvent.getRealm(), attributeEvent.getId()); + }); + attributeEvent + .getValue(String.class) + .ifPresent( + contractedEan -> { + startGopacsHandler( + contractedEan, attributeEvent.getRealm(), attributeEvent.getId()); if (gopacsAsset.getRedispatchEnabled().orElse(false)) { - startRedispatchHandler(contractedEan, attributeEvent.getRealm(), attributeEvent.getId()); + startRedispatchHandler( + contractedEan, attributeEvent.getRealm(), attributeEvent.getId()); } - }); - } + }); + } - // Handle redispatch enabled/disabled toggle - if (attributeName.equals(EmsGOPACSAsset.REDISPATCH_ENABLED.getName())) { - gopacsAsset.getContractedEan().ifPresent(contractedEan -> { + // Handle redispatch enabled/disabled toggle + if (attributeName.equals(EmsGOPACSAsset.REDISPATCH_ENABLED.getName())) { + gopacsAsset + .getContractedEan() + .ifPresent( + contractedEan -> { boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); if (enabled) { - stopRedispatchHandler(contractedEan); // Stop existing if any - startRedispatchHandler(contractedEan, gopacsAsset.getRealm(), gopacsAsset.getId()); + stopRedispatchHandler(contractedEan); // Stop existing if any + startRedispatchHandler( + contractedEan, gopacsAsset.getRealm(), gopacsAsset.getId()); } else { - stopRedispatchHandler(contractedEan); + stopRedispatchHandler(contractedEan); } - }); - } + }); + } - // Handle bid confirmation - if (attributeName.equals(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName())) { - boolean confirmed = (Boolean) attributeEvent.getValue().orElse(false); - if (confirmed) { - gopacsAsset.getContractedEan().ifPresent(contractedEan -> { - GOPACSRedispatchHandler handler = gopacsRedispatchHandlerMap.get(contractedEan); - if (handler != null) { - handler.handleConfirmation(); - } else { - LOG.warning("No redispatch handler running for EAN '" + contractedEan - + "' while processing bid confirmation for asset '" + gopacsAsset.getId() + "'"); - services.getAssetProcessingService().sendAttributeEvent( - new AttributeEvent(gopacsAsset.getId(), EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), false), - getClass().getSimpleName()); - } + // Handle bid confirmation + if (attributeName.equals(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName())) { + boolean confirmed = (Boolean) attributeEvent.getValue().orElse(false); + if (confirmed) { + gopacsAsset + .getContractedEan() + .ifPresent( + contractedEan -> { + GOPACSRedispatchHandler handler = gopacsRedispatchHandlerMap.get(contractedEan); + if (handler != null) { + handler.handleConfirmation(); + } else { + LOG.warning( + "No redispatch handler running for EAN '" + + contractedEan + + "' while processing bid confirmation for asset '" + + gopacsAsset.getId() + + "'"); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + gopacsAsset.getId(), + EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), + false), + getClass().getSimpleName()); + } }); - } - } + } } - - private void updatePowerLimitProfileManualForecasts(EmsEnergyOptimisationAsset energyOptimisationAsset) { - String logPrefix = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAsset.getId(), energyOptimisationAsset.getAssetName()); - String[] powerLimitTypes = {"maximum", "minimum"}; - - for (String powerLimitType : powerLimitTypes) { - String powerLimitProfileManualInput; - String powerLimitProfileManualAttributeName; - - if (powerLimitType.equals("maximum")) { - powerLimitProfileManualInput = energyOptimisationAsset.getPowerLimitMaximumProfileManualInput().orElse(""); - powerLimitProfileManualAttributeName = EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(); - } else { - powerLimitProfileManualInput = energyOptimisationAsset.getPowerLimitMinimumProfileManualInput().orElse(""); - powerLimitProfileManualAttributeName = EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(); - } - - if (powerLimitProfileManualInput.isBlank()) { - continue; - } - - // Parse CSV - List parsedCSV = parseCSV(powerLimitProfileManualInput, ",", logPrefix); - - // Convert parsed CSV to database insertable data-points - List> powerLimitProfileManual = csvToValueDatapoints(parsedCSV, logPrefix); - - // Update manual forecast - services.getAssetPredictedDatapointService().updateValues(energyOptimisationAsset.getId(), powerLimitProfileManualAttributeName, powerLimitProfileManual); - } + } + + private void updatePowerLimitProfileManualForecasts( + EmsEnergyOptimisationAsset energyOptimisationAsset) { + String logPrefix = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAsset.getId(), + energyOptimisationAsset.getAssetName()); + String[] powerLimitTypes = {"maximum", "minimum"}; + + for (String powerLimitType : powerLimitTypes) { + String powerLimitProfileManualInput; + String powerLimitProfileManualAttributeName; + + if (powerLimitType.equals("maximum")) { + powerLimitProfileManualInput = + energyOptimisationAsset.getPowerLimitMaximumProfileManualInput().orElse(""); + powerLimitProfileManualAttributeName = + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(); + } else { + powerLimitProfileManualInput = + energyOptimisationAsset.getPowerLimitMinimumProfileManualInput().orElse(""); + powerLimitProfileManualAttributeName = + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(); + } + + if (powerLimitProfileManualInput.isBlank()) { + continue; + } + + // Parse CSV + List parsedCSV = parseCSV(powerLimitProfileManualInput, ",", logPrefix); + + // Convert parsed CSV to database insertable data-points + List> powerLimitProfileManual = csvToValueDatapoints(parsedCSV, logPrefix); + + // Update manual forecast + services + .getAssetPredictedDatapointService() + .updateValues( + energyOptimisationAsset.getId(), + powerLimitProfileManualAttributeName, + powerLimitProfileManual); } - - private void updatePowerLimitProfileAttributes(Asset asset, String[] attributeNames) { - String assetId = asset.getId(); - - // Get forecast data-points for current 15 minute interval - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long startTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000); - long endTimeMillis = startTimeMillis + (15 * 60000); - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - for (String attributeName : attributeNames) { - List> forecastDatapoints = services.getAssetPredictedDatapointService().queryDatapoints(assetId, attributeName, assetDatapointQuery); - - // Check if there are forecast data-points in the database - if (forecastDatapoints.isEmpty()) { - continue; - } - - // The last data-point in list is the first data-point in the 15-minute interval - ValueDatapoint forecastDatapoint = forecastDatapoints.getLast(); - long forecastAttributeMillis = asset.getAttributes().get(attributeName).flatMap(Attribute::getTimestamp).orElse(0L); - - // Update attribute with new forecast value - if (forecastDatapoint.getTimestamp() > forecastAttributeMillis && forecastDatapoint.getTimestamp() <= currentTimeMillis) { - Double powerLimit = (Double) forecastDatapoint.getValue(); - long timestampMillis = forecastDatapoint.getTimestamp(); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(assetId, attributeName, powerLimit, timestampMillis)); - } - } + } + + private void updatePowerLimitProfileAttributes(Asset asset, String[] attributeNames) { + String assetId = asset.getId(); + + // Get forecast data-points for current 15 minute interval + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000); + long endTimeMillis = startTimeMillis + (15 * 60000); + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + for (String attributeName : attributeNames) { + List> forecastDatapoints = + services + .getAssetPredictedDatapointService() + .queryDatapoints(assetId, attributeName, assetDatapointQuery); + + // Check if there are forecast data-points in the database + if (forecastDatapoints.isEmpty()) { + continue; + } + + // The last data-point in list is the first data-point in the 15-minute interval + ValueDatapoint forecastDatapoint = forecastDatapoints.getLast(); + long forecastAttributeMillis = + asset.getAttributes().get(attributeName).flatMap(Attribute::getTimestamp).orElse(0L); + + // Update attribute with new forecast value + if (forecastDatapoint.getTimestamp() > forecastAttributeMillis + && forecastDatapoint.getTimestamp() <= currentTimeMillis) { + Double powerLimit = (Double) forecastDatapoint.getValue(); + long timestampMillis = forecastDatapoint.getTimestamp(); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent(assetId, attributeName, powerLimit, timestampMillis)); + } + } + } + + private void updatePowerLimitProfileTotalForecasts( + EmsEnergyOptimisationAsset energyOptimisationAsset, EmsGOPACSAsset gopacsAsset) { + String energyOptimisationAssetId = energyOptimisationAsset.getId(); + + // Forecast data-point range of 1 week + long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = + startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get power limit profiles + List> powerLimitMaximumProfileManual = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), + assetDatapointQuery); + List> powerLimitMinimumProfileManual = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), + assetDatapointQuery); + + List> powerLimitMaximumProfileTotal; + List> powerLimitMinimumProfileTotal; + + if (gopacsAsset != null) { + List> powerLimitMaximumProfileDayAhead = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + gopacsAsset.getId(), + EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), + assetDatapointQuery); + List> powerLimitMinimumProfileDayAhead = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + gopacsAsset.getId(), + EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), + assetDatapointQuery); + + // Calculate power limit profile totals + List>> powerLimitMaximumProfiles = new ArrayList<>(); + powerLimitMaximumProfiles.add(powerLimitMaximumProfileDayAhead); + powerLimitMaximumProfiles.add(powerLimitMaximumProfileManual); + + List>> powerLimitMinimumProfiles = new ArrayList<>(); + powerLimitMinimumProfiles.add(powerLimitMinimumProfileDayAhead); + powerLimitMinimumProfiles.add(powerLimitMinimumProfileManual); + + powerLimitMaximumProfileTotal = + calculatePowerLimitProfileTotal(powerLimitMaximumProfiles, "floor"); + powerLimitMinimumProfileTotal = + calculatePowerLimitProfileTotal(powerLimitMinimumProfiles, "ceil"); + } else { + powerLimitMaximumProfileTotal = powerLimitMaximumProfileManual; + powerLimitMinimumProfileTotal = powerLimitMinimumProfileManual; } - private void updatePowerLimitProfileTotalForecasts(EmsEnergyOptimisationAsset energyOptimisationAsset, EmsGOPACSAsset gopacsAsset) { - String energyOptimisationAssetId = energyOptimisationAsset.getId(); - - // Forecast data-point range of 1 week - long startTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = startTimeMillis - startTimeMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - // Get power limit profiles - List> powerLimitMaximumProfileManual = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_MANUAL.getName(), assetDatapointQuery); - List> powerLimitMinimumProfileManual = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_MANUAL.getName(), assetDatapointQuery); - - List> powerLimitMaximumProfileTotal; - List> powerLimitMinimumProfileTotal; - - if (gopacsAsset != null) { - List> powerLimitMaximumProfileDayAhead = services.getAssetPredictedDatapointService().queryDatapoints(gopacsAsset.getId(), EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), assetDatapointQuery); - List> powerLimitMinimumProfileDayAhead = services.getAssetPredictedDatapointService().queryDatapoints(gopacsAsset.getId(), EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), assetDatapointQuery); - - // Calculate power limit profile totals - List>> powerLimitMaximumProfiles = new ArrayList<>(); - powerLimitMaximumProfiles.add(powerLimitMaximumProfileDayAhead); - powerLimitMaximumProfiles.add(powerLimitMaximumProfileManual); - - List>> powerLimitMinimumProfiles = new ArrayList<>(); - powerLimitMinimumProfiles.add(powerLimitMinimumProfileDayAhead); - powerLimitMinimumProfiles.add(powerLimitMinimumProfileManual); - - powerLimitMaximumProfileTotal = calculatePowerLimitProfileTotal(powerLimitMaximumProfiles, "floor"); - powerLimitMinimumProfileTotal = calculatePowerLimitProfileTotal(powerLimitMinimumProfiles, "ceil"); - } else { - powerLimitMaximumProfileTotal = powerLimitMaximumProfileManual; - powerLimitMinimumProfileTotal = powerLimitMinimumProfileManual; - } - - // Update power limit profile totals - services.getAssetPredictedDatapointService().updateValues(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal); - services.getAssetPredictedDatapointService().updateValues(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal); + // Update power limit profile totals + services + .getAssetPredictedDatapointService() + .updateValues( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + powerLimitMaximumProfileTotal); + services + .getAssetPredictedDatapointService() + .updateValues( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + powerLimitMinimumProfileTotal); + } + + private EmsGOPACSAsset getGopacsAsset(EmsEnergyOptimisationAsset energyOptimisationAsset) { + EmsGOPACSAsset gopacsAsset = null; + String logPrefix = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAsset.getId(), + energyOptimisationAsset.getAssetName()); + + // Check for GOPACS assets + List gopacsAssets = + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .parents(energyOptimisationAsset.getId()) + .types(EmsGOPACSAsset.class)) + .stream() + .map(asset -> (EmsGOPACSAsset) asset) + .toList(); + + if (gopacsAssets.size() == 1) { + gopacsAsset = gopacsAssets.getFirst(); + } else if (gopacsAssets.size() > 1) { + LOG.warning( + String.format( + "%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", + logPrefix, + gopacsAssets.size(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName())); } - private EmsGOPACSAsset getGopacsAsset(EmsEnergyOptimisationAsset energyOptimisationAsset) { - EmsGOPACSAsset gopacsAsset = null; - String logPrefix = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAsset.getId(), energyOptimisationAsset.getAssetName()); + return gopacsAsset; + } - // Check for GOPACS assets - List gopacsAssets = services.getAssetStorageService() - .findAll(new AssetQuery().parents(energyOptimisationAsset.getId()).types(EmsGOPACSAsset.class)) - .stream() - .map(asset -> (EmsGOPACSAsset) asset) - .toList(); + private String generatePowerLimitProfile(double powerLimit) { + LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1); - if (gopacsAssets.size() == 1) { - gopacsAsset = gopacsAssets.getFirst(); - } else if (gopacsAssets.size() > 1) { - LOG.warning(String.format("%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", logPrefix, gopacsAssets.size(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName())); - } + List dailyIntervals = new ArrayList<>(); - return gopacsAsset; + // Generate 15-minute intervals for a day + for (LocalDateTime dateTime = startOfDay; + dateTime.isBefore(endOfDay); + dateTime = dateTime.plusMinutes(15)) { + String time = dateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm")); + dailyIntervals.add(time); } - private String generatePowerLimitProfile(double powerLimit) { - LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); - LocalDateTime endOfDay = startOfDay.plusDays(1); - - List dailyIntervals = new ArrayList<>(); + String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + StringBuilder powerLimitProfileCsv = new StringBuilder(); - // Generate 15-minute intervals for a day - for (LocalDateTime dateTime = startOfDay; dateTime.isBefore(endOfDay); dateTime = dateTime.plusMinutes(15)) { - String time = dateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm")); - dailyIntervals.add(time); - } + // Generate power limit profile for a week + for (String day : days) { + for (String time : dailyIntervals) { + String powerLimitProfileEntry = String.format("%s %s,%s\n", day, time, powerLimit); + powerLimitProfileCsv.append(powerLimitProfileEntry); + } + } - String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - StringBuilder powerLimitProfileCsv = new StringBuilder(); + return powerLimitProfileCsv.toString(); + } - // Generate power limit profile for a week - for (String day : days) { - for (String time : dailyIntervals) { - String powerLimitProfileEntry = String.format("%s %s,%s\n", day, time, powerLimit); - powerLimitProfileCsv.append(powerLimitProfileEntry); - } - } + private List parseCSV(String csvFile, String delimiter, String logPrefix) { + List data = new ArrayList<>(); - return powerLimitProfileCsv.toString(); - } + BufferedReader reader = null; - private List parseCSV(String csvFile, String delimiter, String logPrefix) { - List data = new ArrayList<>(); + try { + reader = new BufferedReader(new StringReader(csvFile)); + String line; + while ((line = reader.readLine()) != null) { + // Split data based on delimiter + String[] row = line.split(delimiter + "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); - BufferedReader reader = null; + // Handle escape character "double quotes" + for (int i = 0; i < row.length; i++) { + row[i] = row[i].replaceFirst("\"", ""); - try { - reader = new BufferedReader(new StringReader(csvFile)); - String line; - while ((line = reader.readLine()) != null) { - // Split data based on delimiter - String[] row = line.split(delimiter + "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); - - // Handle escape character "double quotes" - for (int i = 0; i < row.length; i++) { - row[i] = row[i].replaceFirst("\"", ""); - - if (row[i].endsWith("\"")) { - row[i] = row[i].substring(0, row[i].length() - 1); - } - row[i] = row[i].replace("\"\"", "\""); - } - - data.add(row); - } - } catch (IOException e) { - LOG.warning(String.format("%s; Error while parsing CSV file; Exception: %s", logPrefix, e)); - } finally { - try { - assert reader != null; - reader.close(); - } catch (IOException e) { - LOG.warning(String.format("%s; Error while closing CSV file reader; Exception: %s", logPrefix, e)); - } + if (row[i].endsWith("\"")) { + row[i] = row[i].substring(0, row[i].length() - 1); + } + row[i] = row[i].replace("\"\"", "\""); } - return data; + data.add(row); + } + } catch (IOException e) { + LOG.warning(String.format("%s; Error while parsing CSV file; Exception: %s", logPrefix, e)); + } finally { + try { + assert reader != null; + reader.close(); + } catch (IOException e) { + LOG.warning( + String.format("%s; Error while closing CSV file reader; Exception: %s", logPrefix, e)); + } } - private List> csvToValueDatapoints(List parsedCSV, String logPrefix) { - List> valueDatapointList = new ArrayList<>(); - - // Get dates for the coming 7 days starting from today - Map dayToDateMap = weekDates(); - - List incorrectlyFormattedRows = new ArrayList<>(); - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - long currentMillis = System.currentTimeMillis(); + return data; + } - for (String[] line : parsedCSV) { - int rowLength = line.length; + private List> csvToValueDatapoints(List parsedCSV, String logPrefix) { + List> valueDatapointList = new ArrayList<>(); - if (rowLength != 2) { - incorrectlyFormattedRows.add(Arrays.toString(line)); - continue; - } + // Get dates for the coming 7 days starting from today + Map dayToDateMap = weekDates(); - String dayTimeStr = line[0]; - String valueStr = line[1]; + List incorrectlyFormattedRows = new ArrayList<>(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + long currentMillis = System.currentTimeMillis(); - String dayAbbreviation = dayTimeStr.split(" ")[0]; - String dateStr = dayToDateMap.get(dayAbbreviation); - LocalDate today = LocalDate.now(); + for (String[] line : parsedCSV) { + int rowLength = line.length; - if (dateStr != null) { - // Replace the day in the input string with the date - String dateTimeStr = dayTimeStr.replaceFirst(dayAbbreviation, dateStr); + if (rowLength != 2) { + incorrectlyFormattedRows.add(Arrays.toString(line)); + continue; + } - try { - LocalDate timestampLocalDate = LocalDate.parse(dateTimeStr, dateTimeFormatter); - long timestampMillis = simpleDateFormat.parse(dateTimeStr).getTime(); - double value = Double.parseDouble(valueStr); + String dayTimeStr = line[0]; + String valueStr = line[1]; - // Add this day next week - if (today.equals(timestampLocalDate)) { - long timestampMillisPlusOneWeek = timestampMillis + 604800000L; - valueDatapointList.add((new ValueDatapoint<>(timestampMillisPlusOneWeek, value))); - } + String dayAbbreviation = dayTimeStr.split(" ")[0]; + String dateStr = dayToDateMap.get(dayAbbreviation); + LocalDate today = LocalDate.now(); - if (timestampMillis > currentMillis) { - valueDatapointList.add((new ValueDatapoint<>(timestampMillis, value))); - } - } catch (Exception e) { - incorrectlyFormattedRows.add(Arrays.toString(line)); - } + if (dateStr != null) { + // Replace the day in the input string with the date + String dateTimeStr = dayTimeStr.replaceFirst(dayAbbreviation, dateStr); - } else { - incorrectlyFormattedRows.add(Arrays.toString(line)); - } + try { + LocalDate timestampLocalDate = LocalDate.parse(dateTimeStr, dateTimeFormatter); + long timestampMillis = simpleDateFormat.parse(dateTimeStr).getTime(); + double value = Double.parseDouble(valueStr); + + // Add this day next week + if (today.equals(timestampLocalDate)) { + long timestampMillisPlusOneWeek = timestampMillis + 604800000L; + valueDatapointList.add((new ValueDatapoint<>(timestampMillisPlusOneWeek, value))); + } + + if (timestampMillis > currentMillis) { + valueDatapointList.add((new ValueDatapoint<>(timestampMillis, value))); + } + } catch (Exception e) { + incorrectlyFormattedRows.add(Arrays.toString(line)); } - if (incorrectlyFormattedRows.size() > 0) { - LOG.warning(String.format("%s; Input is incorrectly formatted at row %s;", logPrefix, incorrectlyFormattedRows)); - } + } else { + incorrectlyFormattedRows.add(Arrays.toString(line)); + } + } - return valueDatapointList; + if (incorrectlyFormattedRows.size() > 0) { + LOG.warning( + String.format( + "%s; Input is incorrectly formatted at row %s;", + logPrefix, incorrectlyFormattedRows)); } - private Map weekDates() { - String[] dayAbbreviations = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + return valueDatapointList; + } - // Map to match day abbreviations with DayOfWeek enum - Map dayMap = new HashMap<>(); - dayMap.put("Mon", DayOfWeek.MONDAY); - dayMap.put("Tue", DayOfWeek.TUESDAY); - dayMap.put("Wed", DayOfWeek.WEDNESDAY); - dayMap.put("Thu", DayOfWeek.THURSDAY); - dayMap.put("Fri", DayOfWeek.FRIDAY); - dayMap.put("Sat", DayOfWeek.SATURDAY); - dayMap.put("Sun", DayOfWeek.SUNDAY); + private Map weekDates() { + String[] dayAbbreviations = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - LocalDate today = LocalDate.now(); - Map dayToDateMap = new HashMap<>(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + // Map to match day abbreviations with DayOfWeek enum + Map dayMap = new HashMap<>(); + dayMap.put("Mon", DayOfWeek.MONDAY); + dayMap.put("Tue", DayOfWeek.TUESDAY); + dayMap.put("Wed", DayOfWeek.WEDNESDAY); + dayMap.put("Thu", DayOfWeek.THURSDAY); + dayMap.put("Fri", DayOfWeek.FRIDAY); + dayMap.put("Sat", DayOfWeek.SATURDAY); + dayMap.put("Sun", DayOfWeek.SUNDAY); - for (String dayAbbreviation : dayAbbreviations) { - // Get day of week for the day abbreviation - DayOfWeek dayOfWeek = dayMap.get(dayAbbreviation); + LocalDate today = LocalDate.now(); + Map dayToDateMap = new HashMap<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - // Get date of day of week for this week - LocalDate date = today.with(dayOfWeek); + for (String dayAbbreviation : dayAbbreviations) { + // Get day of week for the day abbreviation + DayOfWeek dayOfWeek = dayMap.get(dayAbbreviation); - // If the day is in the past for this week, add 7 days to move to next week - if (date.isBefore(today)) { - date = date.plusWeeks(1); - } + // Get date of day of week for this week + LocalDate date = today.with(dayOfWeek); - dayToDateMap.put(dayAbbreviation, date.format(formatter)); - } + // If the day is in the past for this week, add 7 days to move to next week + if (date.isBefore(today)) { + date = date.plusWeeks(1); + } - return dayToDateMap; + dayToDateMap.put(dayAbbreviation, date.format(formatter)); } - private List> calculatePowerLimitProfileTotal(List>> powerLimitProfiles, String floorOrCeil) { - List> powerLimitProfileTotal = new ArrayList<>(); - Map> mergedMap = new HashMap<>(); + return dayToDateMap; + } - for (List> powerLimitProfile : powerLimitProfiles) { - for (ValueDatapoint valueDatapoint : powerLimitProfile) { - long timestamp = valueDatapoint.getTimestamp(); - Double value = (Double) valueDatapoint.getValue(); - mergedMap.computeIfAbsent(timestamp, k -> new ArrayList<>()).add(value); - } - } + private List> calculatePowerLimitProfileTotal( + List>> powerLimitProfiles, String floorOrCeil) { + List> powerLimitProfileTotal = new ArrayList<>(); + Map> mergedMap = new HashMap<>(); - if (floorOrCeil.equals("floor")) { - mergedMap.forEach((timestamp, values) -> { - ValueDatapoint valueDatapoint = new ValueDatapoint<>(timestamp, Collections.min(values)); - powerLimitProfileTotal.add(valueDatapoint); - }); - } else if (floorOrCeil.equals("ceil")) { - mergedMap.forEach((timestamp, values) -> { - ValueDatapoint valueDatapoint = new ValueDatapoint<>(timestamp, Collections.max(values)); - powerLimitProfileTotal.add(valueDatapoint); - }); - } + for (List> powerLimitProfile : powerLimitProfiles) { + for (ValueDatapoint valueDatapoint : powerLimitProfile) { + long timestamp = valueDatapoint.getTimestamp(); + Double value = (Double) valueDatapoint.getValue(); + mergedMap.computeIfAbsent(timestamp, k -> new ArrayList<>()).add(value); + } + } - return powerLimitProfileTotal; + if (floorOrCeil.equals("floor")) { + mergedMap.forEach( + (timestamp, values) -> { + ValueDatapoint valueDatapoint = + new ValueDatapoint<>(timestamp, Collections.min(values)); + powerLimitProfileTotal.add(valueDatapoint); + }); + } else if (floorOrCeil.equals("ceil")) { + mergedMap.forEach( + (timestamp, values) -> { + ValueDatapoint valueDatapoint = + new ValueDatapoint<>(timestamp, Collections.max(values)); + powerLimitProfileTotal.add(valueDatapoint); + }); } -} \ No newline at end of file + + return powerLimitProfileTotal; + } +} 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 d1bf9c2..21f714f 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 @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,12 +12,28 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager; +import static org.openremote.model.rules.Ruleset.Lang.GROOVY; +import static org.openremote.model.syslog.SyslogCategory.DATA; + +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.*; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.io.IOUtils; import org.openremote.container.persistence.PersistenceService; import org.openremote.container.timer.TimerService; @@ -47,384 +60,446 @@ 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.*; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -import static org.openremote.model.rules.Ruleset.Lang.GROOVY; -import static org.openremote.model.syslog.SyslogCategory.DATA; - public class EmsOptimisationSetupService implements ContainerService { - protected static final Logger LOG = SyslogCategory.getLogger(DATA, EmsOptimisationService.class.getName()); - - protected AssetProcessingService assetProcessingService; - 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 { - assetProcessingService = container.getService(AssetProcessingService.class); - 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); + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, EmsOptimisationService.class.getName()); + + protected AssetProcessingService assetProcessingService; + 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 { + assetProcessingService = container.getService(AssetProcessingService.class); + 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 + public void start(Container container) throws Exception { + // List of asset types that are part of this service + String[] assetTypes = {EmsEnergyOptimisationSetupAsset.DESCRIPTOR.getName()}; + + // Listen to attribute events of listed asset types + clientEventService.addSubscription( + AttributeEvent.class, + new AssetFilter().setAssetTypes(assetTypes), + this::processAttributeEvent); + } + + @Override + public void stop(Container container) throws Exception {} + + private void processAttributeEvent(AttributeEvent attributeEvent) { + String assetType = attributeEvent.getAssetType(); + + if (assetType.equals(EmsEnergyOptimisationSetupAsset.DESCRIPTOR.getName())) { + processAttributeEventEnergyOptimisationSetupAsset(attributeEvent); } - - @Override - public void start(Container container) throws Exception { - // List of asset types that are part of this service - String[] assetTypes = { - EmsEnergyOptimisationSetupAsset.DESCRIPTOR.getName() - }; - - // Listen to attribute events of listed asset types - clientEventService.addSubscription( - AttributeEvent.class, - new AssetFilter().setAssetTypes(assetTypes), - this::processAttributeEvent); + } + + private void processAttributeEventEnergyOptimisationSetupAsset(AttributeEvent attributeEvent) { + String attributeName = attributeEvent.getName(); + String assetId = attributeEvent.getId(); + + if (attributeName.equals( + EmsEnergyOptimisationSetupAsset.CREATE_ENERGY_MANAGEMENT_SYSTEM.getName())) { + boolean checkboxValue = (Boolean) attributeEvent.getValue().orElse(false); + + // Get asset from database + EmsEnergyOptimisationSetupAsset setupAsset = + (EmsEnergyOptimisationSetupAsset) assetStorageService.find(assetId); + + if (setupAsset == null || !checkboxValue) { + return; + } + + // Add a 1-second delay before resetting checkbox for user-friendliness + scheduledExecutorService.schedule( + () -> + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + setupAsset.getId(), + EmsEnergyOptimisationSetupAsset.CREATE_ENERGY_MANAGEMENT_SYSTEM, + false), + getClass().getSimpleName()), + 1, + TimeUnit.SECONDS); + + 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)); + } } + } - @Override - public void stop(Container container) throws Exception { - } + @SuppressWarnings("unchecked") + private void updateManagerConfig() { + ManagerAppConfig managerConfig = configurationService.getManagerConfig(); - private void processAttributeEvent(AttributeEvent attributeEvent) { - String assetType = attributeEvent.getAssetType(); + Map pages = managerConfig.getPages(); + if (pages == null) { + pages = new HashMap<>(); + managerConfig.setPages(pages); + } - if (assetType.equals(EmsEnergyOptimisationSetupAsset.DESCRIPTOR.getName())) { - processAttributeEventEnergyOptimisationSetupAsset(attributeEvent); - } + Map assets = + (Map) pages.computeIfAbsent("assets", k -> new HashMap<>()); + Map viewer = + (Map) assets.computeIfAbsent("viewer", k -> new HashMap<>()); + Map assetTypes = + (Map) viewer.computeIfAbsent("assetTypes", k -> new HashMap<>()); + + try { + assetTypes.putAll(getEmsAssetTypesConfig()); + configurationService.saveManagerConfig(managerConfig); + } catch (Exception e) { + LOG.warning(String.format("Failed to update EMS asset types config; Exception: %s", e)); } + } + + private Map getEmsAssetTypesConfig() { + String path = "/ems/config/asset-types.json"; + try (InputStream is = EmsOptimisationSetupService.class.getResourceAsStream(path)) { + if (is == null) { + throw new IllegalArgumentException("Resource not found: " + path); + } + + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + return mapper.convertValue(mapper.readTree(json), new TypeReference<>() {}); + } catch (Exception e) { + throw new RuntimeException("Failed to read resource: " + path, e); + } + } - private void processAttributeEventEnergyOptimisationSetupAsset(AttributeEvent attributeEvent) { - String attributeName = attributeEvent.getName(); - String assetId = attributeEvent.getId(); + private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setupAsset) { + String energyManagementSystemName = setupAsset.getEnergyManagementSystemName().orElse(""); - if (attributeName.equals(EmsEnergyOptimisationSetupAsset.CREATE_ENERGY_MANAGEMENT_SYSTEM.getName())) { - boolean checkboxValue = (Boolean) attributeEvent.getValue().orElse(false); + // Initiate info field string + String infoFieldMessage; - // Get asset from database - EmsEnergyOptimisationSetupAsset setupAsset = (EmsEnergyOptimisationSetupAsset) assetStorageService.find(assetId); + // Check if energy management system name is provided + if (energyManagementSystemName.isBlank()) { + infoFieldMessage = + "Energy management system not created:\n" + " - Set energy management system name"; + return infoFieldMessage; + } - if (setupAsset == null || !checkboxValue) { - return; - } + String energyOptimisationAssetName = energyManagementSystemName + " Energy Optimisation"; + + // Check if energy management system already exists + List energyOptimisationAssets = + assetStorageService + .findAll( + new AssetQuery() + .types(EmsEnergyOptimisationAsset.class) + .names(energyOptimisationAssetName)) + .stream() + .map(asset -> (EmsEnergyOptimisationAsset) asset) + .toList(); + + if (!energyOptimisationAssets.isEmpty()) { + infoFieldMessage = + "Energy management system not created:\n" + + " - \"" + + energyOptimisationAssetName + + "\" asset already exists"; + 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); + String powerLimitMinimumProfileCsv = generatePowerLimitProfile(powerLimitMinimum); + + energyOptimisationAsset + .setOptimisationMethod( + EmsEnergyOptimisationAsset.OptimisationMethodValueType.EmsOptimisation) + .setPowerLimitMaximumInput(powerLimitMaximum) + .setPowerLimitMaximumProfileManual(powerLimitMaximum) + .setPowerLimitMaximumProfileManualInput(powerLimitMaximumProfileCsv) + .setPowerLimitMaximumProfileTotal(powerLimitMaximum) + .setPowerLimitMinimumInput(powerLimitMinimum) + .setPowerLimitMinimumProfileManual(powerLimitMinimum) + .setPowerLimitMinimumProfileManualInput(powerLimitMinimumProfileCsv) + .setPowerLimitMinimumProfileTotal(powerLimitMinimum) + // .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 + EmsElectricityBatteryAsset electricityBatteryAsset = + new EmsElectricityBatteryAsset(energyManagementSystemName + " Battery"); + electricityBatteryAsset + .setId(UniqueIdentifierGenerator.generateId()) + .setParent(energyOptimisationAsset); + + electricityBatteryAsset + .setAllowCharging(true) + .setAllowDischarging(true) + .setChargeEfficiency(100) + .setChargePowerMaximum(200.0) + .setDischargeEfficiency(100) + .setDischargePowerMaximum(-200.0) + .setEnergyCapacity(425.0) + .setEnergyLevelPercentage(50.0) + .setEnergyLevelPercentageMaximum(95) + .setEnergyLevelPercentageMinimum(20) + .setPower(0.0); + + assetStorageService.merge(electricityBatteryAsset); + + // Create Day Ahead Asset + if (setupAsset.getIncludeDayAheadForecasts().orElse(false)) { + EmsDayAheadAsset dayAheadAsset = + new EmsDayAheadAsset(energyManagementSystemName + " Day ahead forecasts"); + dayAheadAsset + .setId(UniqueIdentifierGenerator.generateId()) + .setParent(energyOptimisationAsset); + + dayAheadAsset.setCollectTimeForecasts("10:00").setUseTariffDayAheadForecasts(true); + + assetStorageService.merge(dayAheadAsset); + } - // Add a 1-second delay before resetting checkbox for user-friendliness - scheduledExecutorService.schedule(() -> assetProcessingService.sendAttributeEvent(new AttributeEvent(setupAsset.getId(), EmsEnergyOptimisationSetupAsset.CREATE_ENERGY_MANAGEMENT_SYSTEM, false), getClass().getSimpleName()), 1, TimeUnit.SECONDS); + // Create GOPACS Asset + if (setupAsset.getIncludeGopacs().orElse(false)) { + EmsGOPACSAsset gopacsAsset = new EmsGOPACSAsset(energyManagementSystemName + " GOPACS"); + gopacsAsset.setId(UniqueIdentifierGenerator.generateId()).setParent(energyOptimisationAsset); - try { - updateManagerConfig(); - String infoFieldMessage = createEnergyManagementSystem(setupAsset); + assetStorageService.merge(gopacsAsset); + } - // 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)); - } - } + // 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 rulesName = energyManagementSystemName + ": Demo simulation rules"; + + String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // General variables rules + 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", electricityBatteryAssetId); + rules = + rules.replaceFirst( + "linkInputAttributeName1", EmsElectricityBatteryAsset.POWER_SETPOINT.getName()); + rules = rules.replaceFirst("linkOutputAssetId1", energyOptimisationAssetId); + rules = + rules.replaceFirst( + "linkOutputAttributeName1", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); + + // Sum forecasts rule + rules = rules.replaceFirst("sumInputAssetId1", energyOptimisationAssetId); + rules = + rules.replaceFirst( + "sumInputAttributeName1", EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName()); + rules = rules.replaceFirst("sumInputAssetId2", energyOptimisationAssetId); + rules = + rules.replaceFirst( + "sumInputAttributeName2", EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName()); + rules = rules.replaceFirst("sumInputAssetId3", energyOptimisationAssetId); + rules = + rules.replaceFirst( + "sumInputAttributeName3", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); + rules = rules.replaceFirst("sumOutputAssetId", energyOptimisationAssetId); + rules = + rules.replaceFirst( + "sumOutputAttributeName", EmsEnergyOptimisationAsset.POWER_NET.getName()); + + RealmRuleset districtRuleSet = new RealmRuleset(realmName, rulesName, GROOVY, rules); + + // Merge rules into database + persistenceService.doReturningTransaction( + entityManager -> entityManager.merge(districtRuleSet)); + } + } catch (Exception 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)); } - @SuppressWarnings("unchecked") - private void updateManagerConfig() { - ManagerAppConfig managerConfig = configurationService.getManagerConfig(); - - Map pages = managerConfig.getPages(); - if (pages == null) { - pages = new HashMap<>(); - managerConfig.setPages(pages); - } - - Map assets = (Map) pages.computeIfAbsent("assets", k -> new HashMap<>()); - Map viewer = (Map) assets.computeIfAbsent("viewer", k -> new HashMap<>()); - Map assetTypes = (Map) viewer.computeIfAbsent("assetTypes", k -> new HashMap<>()); - - try { - assetTypes.putAll(getEmsAssetTypesConfig()); - configurationService.saveManagerConfig(managerConfig); - } catch (Exception e) { - LOG.warning(String.format("Failed to update EMS asset types config; Exception: %s", 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)); } - private Map getEmsAssetTypesConfig() { - String path = "/ems/config/asset-types.json"; - try (InputStream is = EmsOptimisationSetupService.class.getResourceAsStream(path)) { - if (is == null) { - throw new IllegalArgumentException("Resource not found: " + path); - } - - String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); - ObjectMapper mapper = new ObjectMapper(); - return mapper.convertValue(mapper.readTree(json), new TypeReference<>() { - }); - } catch (Exception e) { - throw new RuntimeException("Failed to read resource: " + path, 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)); } - private String createEnergyManagementSystem(EmsEnergyOptimisationSetupAsset setupAsset) { - String energyManagementSystemName = setupAsset.getEnergyManagementSystemName().orElse(""); - - // Initiate info field string - String infoFieldMessage; - - // Check if energy management system name is provided - if (energyManagementSystemName.isBlank()) { - infoFieldMessage = "Energy management system not created:\n" + - " - Set energy management system name"; - return infoFieldMessage; - } - - String energyOptimisationAssetName = energyManagementSystemName + " Energy Optimisation"; - - // Check if energy management system already exists - List energyOptimisationAssets = assetStorageService - .findAll(new AssetQuery().types(EmsEnergyOptimisationAsset.class).names(energyOptimisationAssetName)) - .stream() - .map(asset -> (EmsEnergyOptimisationAsset) asset) - .toList(); - - if (!energyOptimisationAssets.isEmpty()) { - infoFieldMessage = "Energy management system not created:\n" + - " - \"" + energyOptimisationAssetName + "\" asset already exists"; - 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); - String powerLimitMinimumProfileCsv = generatePowerLimitProfile(powerLimitMinimum); - - energyOptimisationAsset - .setOptimisationMethod(EmsEnergyOptimisationAsset.OptimisationMethodValueType.EmsOptimisation) - .setPowerLimitMaximumInput(powerLimitMaximum) - .setPowerLimitMaximumProfileManual(powerLimitMaximum) - .setPowerLimitMaximumProfileManualInput(powerLimitMaximumProfileCsv) - .setPowerLimitMaximumProfileTotal(powerLimitMaximum) - .setPowerLimitMinimumInput(powerLimitMinimum) - .setPowerLimitMinimumProfileManual(powerLimitMinimum) - .setPowerLimitMinimumProfileManualInput(powerLimitMinimumProfileCsv) - .setPowerLimitMinimumProfileTotal(powerLimitMinimum) -// .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 - EmsElectricityBatteryAsset electricityBatteryAsset = new EmsElectricityBatteryAsset(energyManagementSystemName + " Battery"); - electricityBatteryAsset.setId(UniqueIdentifierGenerator.generateId()).setParent(energyOptimisationAsset); - - electricityBatteryAsset - .setAllowCharging(true) - .setAllowDischarging(true) - .setChargeEfficiency(100) - .setChargePowerMaximum(200.0) - .setDischargeEfficiency(100) - .setDischargePowerMaximum(-200.0) - .setEnergyCapacity(425.0) - .setEnergyLevelPercentage(50.0) - .setEnergyLevelPercentageMaximum(95) - .setEnergyLevelPercentageMinimum(20) - .setPower(0.0); - - assetStorageService.merge(electricityBatteryAsset); - - // Create Day Ahead Asset - if (setupAsset.getIncludeDayAheadForecasts().orElse(false)) { - EmsDayAheadAsset dayAheadAsset = new EmsDayAheadAsset(energyManagementSystemName + " Day ahead forecasts"); - dayAheadAsset.setId(UniqueIdentifierGenerator.generateId()).setParent(energyOptimisationAsset); - - dayAheadAsset - .setCollectTimeForecasts("10:00") - .setUseTariffDayAheadForecasts(true); - - assetStorageService.merge(dayAheadAsset); - } - - // Create GOPACS Asset - if (setupAsset.getIncludeGopacs().orElse(false)) { - EmsGOPACSAsset gopacsAsset = new EmsGOPACSAsset(energyManagementSystemName + " GOPACS"); - gopacsAsset.setId(UniqueIdentifierGenerator.generateId()).setParent(energyOptimisationAsset); - - 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 rulesName = energyManagementSystemName + ": Demo simulation rules"; - - String rules = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // General variables rules - 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", electricityBatteryAssetId); - rules = rules.replaceFirst("linkInputAttributeName1", EmsElectricityBatteryAsset.POWER_SETPOINT.getName()); - rules = rules.replaceFirst("linkOutputAssetId1", energyOptimisationAssetId); - rules = rules.replaceFirst("linkOutputAttributeName1", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); - - // Sum forecasts rule - rules = rules.replaceFirst("sumInputAssetId1", energyOptimisationAssetId); - rules = rules.replaceFirst("sumInputAttributeName1", EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName()); - rules = rules.replaceFirst("sumInputAssetId2", energyOptimisationAssetId); - rules = rules.replaceFirst("sumInputAttributeName2", EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName()); - rules = rules.replaceFirst("sumInputAssetId3", energyOptimisationAssetId); - rules = rules.replaceFirst("sumInputAttributeName3", EmsEnergyOptimisationAsset.POWER_FLEXIBLE.getName()); - rules = rules.replaceFirst("sumOutputAssetId", energyOptimisationAssetId); - rules = rules.replaceFirst("sumOutputAttributeName", EmsEnergyOptimisationAsset.POWER_NET.getName()); - - RealmRuleset districtRuleSet = new RealmRuleset(realmName, rulesName, GROOVY, rules); - - // Merge rules into database - persistenceService.doReturningTransaction(entityManager -> entityManager.merge(districtRuleSet)); - } - } catch (Exception 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) { + // Create info field message + infoFieldMessage = + "Created energy management system \"" + + energyOptimisationAssetName + + "\":\n" + + "\n" + + "1) Drag the energy management system to your preferred location\n"; - 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" + - "1) Drag the energy management system to your preferred location\n"; - - if (setupAsset.getIncludeGopacs().orElse(false)) { - infoFieldMessage = infoFieldMessage + "2) Set GOPACS variables\n"; - } - - infoFieldMessage = infoFieldMessage + "\nYou can delete this setup asset after you have created your energy management system."; - - LOG.info(String.format("assetName='%s', assetId='%s'; Created energy management system '%s'", setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName)); - - return infoFieldMessage; + if (setupAsset.getIncludeGopacs().orElse(false)) { + infoFieldMessage = infoFieldMessage + "2) Set GOPACS variables\n"; } - private String generatePowerLimitProfile(double powerLimit) { - LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); - LocalDateTime endOfDay = startOfDay.plusDays(1); + infoFieldMessage = + infoFieldMessage + + "\nYou can delete this setup asset after you have created your energy management system."; - List dailyIntervals = new ArrayList<>(); + LOG.info( + String.format( + "assetName='%s', assetId='%s'; Created energy management system '%s'", + setupAsset.getName(), setupAsset.getId(), energyOptimisationAssetName)); - // Generate 15-minute intervals for a day - for (LocalDateTime dateTime = startOfDay; dateTime.isBefore(endOfDay); dateTime = dateTime.plusMinutes(15)) { - String time = dateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm")); - dailyIntervals.add(time); - } + return infoFieldMessage; + } - String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - StringBuilder powerLimitProfileCsv = new StringBuilder(); + private String generatePowerLimitProfile(double powerLimit) { + LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1); - // Generate power limit profile for a week - for (String day : days) { - for (String time : dailyIntervals) { - String powerLimitProfileEntry = String.format("%s %s,%s\n", day, time, powerLimit); - powerLimitProfileCsv.append(powerLimitProfileEntry); - } - } + List dailyIntervals = new ArrayList<>(); - return powerLimitProfileCsv.toString(); + // Generate 15-minute intervals for a day + for (LocalDateTime dateTime = startOfDay; + dateTime.isBefore(endOfDay); + dateTime = dateTime.plusMinutes(15)) { + String time = dateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm")); + dailyIntervals.add(time); } + + String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + StringBuilder powerLimitProfileCsv = new StringBuilder(); + + // Generate power limit profile for a week + for (String day : days) { + for (String time : dailyIntervals) { + String powerLimitProfileEntry = String.format("%s %s,%s\n", day, time, powerLimit); + powerLimitProfileCsv.append(powerLimitProfileEntry); + } + } + + return powerLimitProfileCsv.toString(); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/OptimisationMethodsLoader.java b/ems/src/main/java/org/openremote/extension/ems/manager/OptimisationMethodsLoader.java index 4bf5851..af35684 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/OptimisationMethodsLoader.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/OptimisationMethodsLoader.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,12 +12,13 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager; -import org.openremote.extension.ems.manager.optimisationMethods.OptimisationMethod; -import org.openremote.model.syslog.SyslogCategory; +import static org.openremote.model.syslog.SyslogCategory.DATA; import java.io.File; import java.net.URL; @@ -32,93 +30,111 @@ import java.util.jar.JarFile; import java.util.logging.Logger; -import static org.openremote.model.syslog.SyslogCategory.DATA; +import org.openremote.extension.ems.manager.optimisationMethods.OptimisationMethod; +import org.openremote.model.syslog.SyslogCategory; public class OptimisationMethodsLoader { - protected static final Logger LOG = SyslogCategory.getLogger(DATA, OptimisationMethodsLoader.class.getName()); + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, OptimisationMethodsLoader.class.getName()); - private final String OPTIMISATION_METHODS_PACKAGE_NAME = OptimisationMethodsLoader.class.getPackage().getName() + ".optimisationMethods"; + private final String OPTIMISATION_METHODS_PACKAGE_NAME = + OptimisationMethodsLoader.class.getPackage().getName() + ".optimisationMethods"; - protected void runOptimisationMethod(String currentOptimisationMethodName, String energyOptimisationAssetId, Services services) { - List optimisationMethods = loadOptimisationMethods(); + protected void runOptimisationMethod( + String currentOptimisationMethodName, String energyOptimisationAssetId, Services services) { + List optimisationMethods = loadOptimisationMethods(); - for (OptimisationMethod optimisationMethod : optimisationMethods) { - String classNameCurrent = OPTIMISATION_METHODS_PACKAGE_NAME + "." + currentOptimisationMethodName; - String classNameMethod = optimisationMethod.getClass().getName(); + for (OptimisationMethod optimisationMethod : optimisationMethods) { + String classNameCurrent = + OPTIMISATION_METHODS_PACKAGE_NAME + "." + currentOptimisationMethodName; + String classNameMethod = optimisationMethod.getClass().getName(); - if (classNameMethod.equals(classNameCurrent)) { - optimisationMethod.execute(energyOptimisationAssetId, services); - return; - } - } - if (!currentOptimisationMethodName.equals("None")) { - LOG.info(String.format("assetId='%s'; Optimisation method '%s' not found", energyOptimisationAssetId, currentOptimisationMethodName)); - } + if (classNameMethod.equals(classNameCurrent)) { + optimisationMethod.execute(energyOptimisationAssetId, services); + return; + } } + if (!currentOptimisationMethodName.equals("None")) { + LOG.info( + String.format( + "assetId='%s'; Optimisation method '%s' not found", + energyOptimisationAssetId, currentOptimisationMethodName)); + } + } - private List loadOptimisationMethods() { - List optimisationMethods = new ArrayList<>(); + private List loadOptimisationMethods() { + List optimisationMethods = new ArrayList<>(); - try { - // Get the package path - String packagePath = OPTIMISATION_METHODS_PACKAGE_NAME.replace('.', '/'); - URL packageURL = Thread.currentThread().getContextClassLoader().getResource(packagePath); + try { + // Get the package path + String packagePath = OPTIMISATION_METHODS_PACKAGE_NAME.replace('.', '/'); + URL packageURL = Thread.currentThread().getContextClassLoader().getResource(packagePath); - if (packageURL == null) { - LOG.info(String.format("Package not found; packageName='%s'", OPTIMISATION_METHODS_PACKAGE_NAME)); - return optimisationMethods; - } + if (packageURL == null) { + LOG.info( + String.format( + "Package not found; packageName='%s'", OPTIMISATION_METHODS_PACKAGE_NAME)); + return optimisationMethods; + } + + if (packageURL.getProtocol().equals("file")) { + // Handle file URL + File directory = new File(packageURL.toURI()); + + if (!directory.exists() || !directory.isDirectory()) { + LOG.info( + String.format( + "Invalid package directory; packageName='%s', directoryPath='%s'", + OPTIMISATION_METHODS_PACKAGE_NAME, directory.getAbsolutePath())); + return optimisationMethods; + } - if (packageURL.getProtocol().equals("file")) { - // Handle file URL - File directory = new File(packageURL.toURI()); - - if (!directory.exists() || !directory.isDirectory()) { - LOG.info(String.format("Invalid package directory; packageName='%s', directoryPath='%s'", OPTIMISATION_METHODS_PACKAGE_NAME, directory.getAbsolutePath())); - return optimisationMethods; - } - - // Check if the file is a class file - for (String fileName : Objects.requireNonNull(directory.list())) { - if (fileName.endsWith(".class")) { - String className = OPTIMISATION_METHODS_PACKAGE_NAME + "." + fileName.replace(".class", ""); - Class clazz = Class.forName(className); - - if (OptimisationMethod.class.isAssignableFrom(clazz) && !clazz.isInterface()) { - optimisationMethods.add((OptimisationMethod) clazz.getDeclaredConstructor().newInstance()); - } - } - } - } else if (packageURL.getProtocol().equals("jar")) { - // Handle the case where the resources are in a JAR file - String jarFilePath = packageURL.getPath().substring(5, packageURL.getPath().indexOf("!")); - JarFile jarFile = new JarFile(jarFilePath); - Enumeration entries = jarFile.entries(); - - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - - // Check if the entry is a class file - if (entry.getName().endsWith(".class") && entry.getName().startsWith(packagePath)) { - String className = entry.getName().replace('/', '.').replace(".class", ""); - Class clazz = Class.forName(className); - - if (OptimisationMethod.class.isAssignableFrom(clazz) && !clazz.isInterface()) { - optimisationMethods.add((OptimisationMethod) clazz.getDeclaredConstructor().newInstance()); - } - } - } - - jarFile.close(); - } else { - LOG.info(String.format("Unknown URL protocol; protocolName='%s'", packageURL.getProtocol())); - } + // Check if the file is a class file + for (String fileName : Objects.requireNonNull(directory.list())) { + if (fileName.endsWith(".class")) { + String className = + OPTIMISATION_METHODS_PACKAGE_NAME + "." + fileName.replace(".class", ""); + Class clazz = Class.forName(className); - } catch (Exception e) { - LOG.info(String.format("An exception occurred during loading of optimisation methods; Exception: %s", e)); + if (OptimisationMethod.class.isAssignableFrom(clazz) && !clazz.isInterface()) { + optimisationMethods.add( + (OptimisationMethod) clazz.getDeclaredConstructor().newInstance()); + } + } + } + } else if (packageURL.getProtocol().equals("jar")) { + // Handle the case where the resources are in a JAR file + String jarFilePath = packageURL.getPath().substring(5, packageURL.getPath().indexOf("!")); + JarFile jarFile = new JarFile(jarFilePath); + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + + // Check if the entry is a class file + if (entry.getName().endsWith(".class") && entry.getName().startsWith(packagePath)) { + String className = entry.getName().replace('/', '.').replace(".class", ""); + Class clazz = Class.forName(className); + + if (OptimisationMethod.class.isAssignableFrom(clazz) && !clazz.isInterface()) { + optimisationMethods.add( + (OptimisationMethod) clazz.getDeclaredConstructor().newInstance()); + } + } } - return optimisationMethods; + jarFile.close(); + } else { + LOG.info( + String.format("Unknown URL protocol; protocolName='%s'", packageURL.getProtocol())); + } + + } catch (Exception e) { + LOG.info( + String.format( + "An exception occurred during loading of optimisation methods; Exception: %s", e)); } + return optimisationMethods; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/Services.java b/ems/src/main/java/org/openremote/extension/ems/manager/Services.java index 11a26bd..74cd56e 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/Services.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/Services.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager; +import java.util.concurrent.ScheduledExecutorService; + import org.openremote.container.message.MessageBrokerService; import org.openremote.container.timer.TimerService; import org.openremote.manager.asset.AssetProcessingService; @@ -28,130 +29,128 @@ import org.openremote.manager.event.ClientEventService; import org.openremote.manager.gateway.GatewayService; -import java.util.concurrent.ScheduledExecutorService; - public class Services { - private final AssetDatapointService assetDatapointService; - private final AssetPredictedDatapointService assetPredictedDatapointService; - private final AssetProcessingService assetProcessingService; - private final AssetStorageService assetStorageService; - private final ClientEventService clientEventService; - private final GatewayService gatewayService; - private final MessageBrokerService messageBrokerService; - private final ScheduledExecutorService scheduledExecutorService; - private final TimerService timerService; - - private Services(Builder builder) { - this.assetDatapointService = builder.assetDatapointService; - this.assetPredictedDatapointService = builder.assetPredictedDatapointService; - this.assetProcessingService = builder.assetProcessingService; - this.assetStorageService = builder.assetStorageService; - this.clientEventService = builder.clientEventService; - this.gatewayService = builder.gatewayService; - this.messageBrokerService = builder.messageBrokerService; - this.scheduledExecutorService = builder.scheduledExecutorService; - this.timerService = builder.timerService; + private final AssetDatapointService assetDatapointService; + private final AssetPredictedDatapointService assetPredictedDatapointService; + private final AssetProcessingService assetProcessingService; + private final AssetStorageService assetStorageService; + private final ClientEventService clientEventService; + private final GatewayService gatewayService; + private final MessageBrokerService messageBrokerService; + private final ScheduledExecutorService scheduledExecutorService; + private final TimerService timerService; + + private Services(Builder builder) { + this.assetDatapointService = builder.assetDatapointService; + this.assetPredictedDatapointService = builder.assetPredictedDatapointService; + this.assetProcessingService = builder.assetProcessingService; + this.assetStorageService = builder.assetStorageService; + this.clientEventService = builder.clientEventService; + this.gatewayService = builder.gatewayService; + this.messageBrokerService = builder.messageBrokerService; + this.scheduledExecutorService = builder.scheduledExecutorService; + this.timerService = builder.timerService; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private AssetDatapointService assetDatapointService; + private AssetPredictedDatapointService assetPredictedDatapointService; + private AssetProcessingService assetProcessingService; + private AssetStorageService assetStorageService; + private ClientEventService clientEventService; + private GatewayService gatewayService; + private MessageBrokerService messageBrokerService; + private ScheduledExecutorService scheduledExecutorService; + private TimerService timerService; + + public Builder withAssetDatapointService(AssetDatapointService service) { + this.assetDatapointService = service; + return this; } - public static Builder builder() { - return new Builder(); + public Builder withAssetPredictedDatapointService(AssetPredictedDatapointService service) { + this.assetPredictedDatapointService = service; + return this; } - public static class Builder { - private AssetDatapointService assetDatapointService; - private AssetPredictedDatapointService assetPredictedDatapointService; - private AssetProcessingService assetProcessingService; - private AssetStorageService assetStorageService; - private ClientEventService clientEventService; - private GatewayService gatewayService; - private MessageBrokerService messageBrokerService; - private ScheduledExecutorService scheduledExecutorService; - private TimerService timerService; - - public Builder withAssetDatapointService(AssetDatapointService service) { - this.assetDatapointService = service; - return this; - } - - public Builder withAssetPredictedDatapointService(AssetPredictedDatapointService service) { - this.assetPredictedDatapointService = service; - return this; - } - - public Builder withAssetProcessingService(AssetProcessingService service) { - this.assetProcessingService = service; - return this; - } - - public Builder withAssetStorageService(AssetStorageService service) { - this.assetStorageService = service; - return this; - } - - public Builder withClientEventService(ClientEventService service) { - this.clientEventService = service; - return this; - } - - public Builder withGatewayService(GatewayService service) { - this.gatewayService = service; - return this; - } - - public Builder withMessageBrokerService(MessageBrokerService service) { - this.messageBrokerService = service; - return this; - } - - public Builder withScheduledExecutorService(ScheduledExecutorService service) { - this.scheduledExecutorService = service; - return this; - } - - public Builder withTimerService(TimerService service) { - this.timerService = service; - return this; - } - - public Services build() { - return new Services(this); - } + public Builder withAssetProcessingService(AssetProcessingService service) { + this.assetProcessingService = service; + return this; } - // Getters for each service - public AssetDatapointService getAssetDatapointService() { - return assetDatapointService; + public Builder withAssetStorageService(AssetStorageService service) { + this.assetStorageService = service; + return this; } - public AssetPredictedDatapointService getAssetPredictedDatapointService() { - return assetPredictedDatapointService; + public Builder withClientEventService(ClientEventService service) { + this.clientEventService = service; + return this; } - public AssetProcessingService getAssetProcessingService() { - return assetProcessingService; + public Builder withGatewayService(GatewayService service) { + this.gatewayService = service; + return this; } - public AssetStorageService getAssetStorageService() { - return assetStorageService; + public Builder withMessageBrokerService(MessageBrokerService service) { + this.messageBrokerService = service; + return this; } - public ClientEventService getClientEventService() { - return clientEventService; + public Builder withScheduledExecutorService(ScheduledExecutorService service) { + this.scheduledExecutorService = service; + return this; } - public GatewayService getGatewayService() { - return gatewayService; + public Builder withTimerService(TimerService service) { + this.timerService = service; + return this; } - public MessageBrokerService getMessageBrokerService() { - return messageBrokerService; + public Services build() { + return new Services(this); } + } - public ScheduledExecutorService getScheduledExecutorService() { - return scheduledExecutorService; - } + // Getters for each service + public AssetDatapointService getAssetDatapointService() { + return assetDatapointService; + } - public TimerService getTimerService() { - return timerService; - } + public AssetPredictedDatapointService getAssetPredictedDatapointService() { + return assetPredictedDatapointService; + } + + public AssetProcessingService getAssetProcessingService() { + return assetProcessingService; + } + + public AssetStorageService getAssetStorageService() { + return assetStorageService; + } + + public ClientEventService getClientEventService() { + return clientEventService; + } + + public GatewayService getGatewayService() { + return gatewayService; + } + + public MessageBrokerService getMessageBrokerService() { + return messageBrokerService; + } + + public ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + + public TimerService getTimerService() { + return timerService; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/FlexRequestISPTypeHelper.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/FlexRequestISPTypeHelper.java index f211fd0..4bfa068 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/FlexRequestISPTypeHelper.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/FlexRequestISPTypeHelper.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,61 +12,65 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; import java.time.*; public class FlexRequestISPTypeHelper { - private static final long ISP_DURATION_IN_MINUTES = 15; + private static final long ISP_DURATION_IN_MINUTES = 15; - public static LocalTime getISPStart(long ispNumber, int year, int month, int day, String timeZone) { - ZoneId zoneId = ZoneId.of(timeZone); - ZonedDateTime date = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, zoneId); + public static LocalTime getISPStart( + long ispNumber, int year, int month, int day, String timeZone) { + ZoneId zoneId = ZoneId.of(timeZone); + ZonedDateTime date = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, zoneId); - if (ispNumber == 9 && isLastSundayInMarch(year, month, day)) { - return date.plusHours(3).toLocalTime(); - } else if (ispNumber == 13 && isLastSundayInOctober(year, month, day)) { - return date.plusHours(2).toLocalTime(); - } else { - return date.plusMinutes((ispNumber - 1) * ISP_DURATION_IN_MINUTES).toLocalTime(); - } + if (ispNumber == 9 && isLastSundayInMarch(year, month, day)) { + return date.plusHours(3).toLocalTime(); + } else if (ispNumber == 13 && isLastSundayInOctober(year, month, day)) { + return date.plusHours(2).toLocalTime(); + } else { + return date.plusMinutes((ispNumber - 1) * ISP_DURATION_IN_MINUTES).toLocalTime(); } + } - public static LocalTime getISPEnd(int ispNumber, int year, int month, int day, String timeZone) { - LocalTime end = getISPStart(ispNumber, year, month, day, timeZone).plusMinutes(ISP_DURATION_IN_MINUTES); - if (ispNumber == 8 && isLastSundayInMarch(year, month, day)) { - end = end.plusHours(1); - } else if (ispNumber == 12 && isLastSundayInOctober(year, month, day)) { - end = end.minusHours(1); - } - return end; + public static LocalTime getISPEnd(int ispNumber, int year, int month, int day, String timeZone) { + LocalTime end = + getISPStart(ispNumber, year, month, day, timeZone).plusMinutes(ISP_DURATION_IN_MINUTES); + if (ispNumber == 8 && isLastSundayInMarch(year, month, day)) { + end = end.plusHours(1); + } else if (ispNumber == 12 && isLastSundayInOctober(year, month, day)) { + end = end.minusHours(1); } + return end; + } - private static boolean isLastSundayInMarch(int year, int month, int day) { - if (month != 3) { // March is 3 in Java's month numbering - return false; - } - LocalDate date = LocalDate.of(year, month, day); - int lastDayInMarch = YearMonth.of(year, month).lengthOfMonth(); - LocalDate lastSundayInMarch = LocalDate.of(year, month, lastDayInMarch); - while (lastSundayInMarch.getDayOfWeek() != DayOfWeek.SUNDAY) { - lastSundayInMarch = lastSundayInMarch.minusDays(1); - } - return date.equals(lastSundayInMarch); + private static boolean isLastSundayInMarch(int year, int month, int day) { + if (month != 3) { // March is 3 in Java's month numbering + return false; } + LocalDate date = LocalDate.of(year, month, day); + int lastDayInMarch = YearMonth.of(year, month).lengthOfMonth(); + LocalDate lastSundayInMarch = LocalDate.of(year, month, lastDayInMarch); + while (lastSundayInMarch.getDayOfWeek() != DayOfWeek.SUNDAY) { + lastSundayInMarch = lastSundayInMarch.minusDays(1); + } + return date.equals(lastSundayInMarch); + } - private static boolean isLastSundayInOctober(int year, int month, int day) { - if (month != 10) { // October is 10 in Java's month numbering - return false; - } - LocalDate date = LocalDate.of(year, month, day); - int lastDayInOctober = YearMonth.of(year, month).lengthOfMonth(); - LocalDate lastSundayInOctober = LocalDate.of(year, month, lastDayInOctober); - while (lastSundayInOctober.getDayOfWeek() != DayOfWeek.SUNDAY) { - lastSundayInOctober = lastSundayInOctober.minusDays(1); - } - return date.equals(lastSundayInOctober); + private static boolean isLastSundayInOctober(int year, int month, int day) { + if (month != 10) { // October is 10 in Java's month numbering + return false; + } + LocalDate date = LocalDate.of(year, month, day); + int lastDayInOctober = YearMonth.of(year, month).lengthOfMonth(); + LocalDate lastSundayInOctober = LocalDate.of(year, month, lastDayInOctober); + while (lastSundayInOctober.getDayOfWeek() != DayOfWeek.SUNDAY) { + lastSundayInOctober = lastSundayInOctober.minusDays(1); } + return date.equals(lastSundayInOctober); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java index a119a08..6b061cf 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; @@ -26,15 +27,12 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; - @Path("uftp-participants/v3/participants") public interface GOPACSAddressBookResource { - @GET - @Path("{uftpDomainName}") - @Produces(APPLICATION_JSON) - Response fetchParticipantByDomain( - @HeaderParam("Authorization") String authorization, - @PathParam("uftpDomainName") String uftpDomainName - ); + @GET + @Path("{uftpDomainName}") + @Produces(APPLICATION_JSON) + Response fetchParticipantByDomain( + @HeaderParam("Authorization") String authorization, + @PathParam("uftpDomainName") String uftpDomainName); } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAnnouncementResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAnnouncementResource.java index 53cb19e..d4447ad 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAnnouncementResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAnnouncementResource.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,33 +12,34 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; - /** - * RESTEasy client proxy for the GOPACS machine announcements endpoint. - * No authentication required (public API). + * RESTEasy client proxy for the GOPACS machine announcements endpoint. No authentication required + * (public API). */ @Path("/") @Produces(APPLICATION_JSON) public interface GOPACSAnnouncementResource { - @GET - @Path("machineannouncements") - Response fetchAnnouncements( - @QueryParam("postalcode") String postalCode, - @QueryParam("starttime") String startTime, - @QueryParam("endtime") String endTime, - @QueryParam("type") String type, - @QueryParam("state") String state - ); + @GET + @Path("machineannouncements") + Response fetchAnnouncements( + @QueryParam("postalcode") String postalCode, + @QueryParam("starttime") String startTime, + @QueryParam("endtime") String endTime, + @QueryParam("type") String type, + @QueryParam("state") String state); } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAuthResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAuthResource.java index a05140e..828a4d6 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAuthResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAuthResource.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,15 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.POST; @@ -26,21 +28,15 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; -import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; - -/** - * RESTEasy client interface for OAuth2 token endpoint - */ +/** RESTEasy client interface for OAuth2 token endpoint */ @Path("/") public interface GOPACSAuthResource { - @POST - @Consumes(APPLICATION_FORM_URLENCODED) - @Produces(APPLICATION_JSON) - Response getAccessToken( - @FormParam("grant_type") String grantType, - @FormParam("client_id") String clientId, - @FormParam("client_secret") String clientSecret - ); + @POST + @Consumes(APPLICATION_FORM_URLENCODED) + @Produces(APPLICATION_JSON) + Response getAccessToken( + @FormParam("grant_type") String grantType, + @FormParam("client_id") String clientId, + @FormParam("client_secret") String clientSecret); } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSEanEffectivityResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSEanEffectivityResource.java index 83d2259..7203951 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSEanEffectivityResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSEanEffectivityResource.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; @@ -26,20 +27,16 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; - /** - * RESTEasy client proxy for the GOPACS EAN solving effectivity endpoint. - * Requires an API key header for authentication. + * RESTEasy client proxy for the GOPACS EAN solving effectivity endpoint. Requires an API key header + * for authentication. */ @Path("public-api/1.0") @Produces(APPLICATION_JSON) public interface GOPACSEanEffectivityResource { - @GET - @Path("announcements/{id}/eansolvingeffectivity") - Response fetchEanSolvingEffectivity( - @PathParam("id") String announcementId, - @HeaderParam("API_KEY") String apiKey - ); + @GET + @Path("announcements/{id}/eansolvingeffectivity") + Response fetchEanSolvingEffectivity( + @PathParam("id") String announcementId, @HeaderParam("API_KEY") String apiKey); } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java index 63ccf96..b72fd97 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,17 +12,37 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static org.openremote.container.web.WebService.getStandardProviders; +import static org.openremote.container.web.WebTargetBuilder.createClient; +import static org.openremote.model.syslog.SyslogCategory.API; + +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.Response; + import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.lfenergy.shapeshifter.api.*; import org.lfenergy.shapeshifter.api.model.UftpParticipantInformation; @@ -62,626 +79,737 @@ import org.openremote.model.syslog.SyslogCategory; import org.openremote.model.util.TextUtil; -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.util.*; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Stream; - -import static org.openremote.container.web.WebService.getStandardProviders; -import static org.openremote.container.web.WebTargetBuilder.createClient; -import static org.openremote.model.syslog.SyslogCategory.API; - -/** - * This class handles the communication with the GoPacs server. - */ -public class GOPACSHandler implements UftpPayloadHandler, UftpParticipantService, ParticipantAuthorizationProvider { - - private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSHandler.class); - public static final String GOPACS_PRIVATE_KEY_FILE = "GOPACS_PRIVATE_KEY_FILE"; - public static final String GOPACS_BROKER_URL = "GOPACS_BROKER_URL"; - public static final String DEFAULT_GOPACS_BROKER_URL = "https://clc-message-broker.gopacs-services.eu"; - public static final String GOPACS_PARTICIPANT_URL = "GOPACS_PARTICIPANT_URL"; - public static final String DEFAULT_GOPACS_PARTICIPANT_URL = "https://api.gopacs-services.eu"; - public static final String GOPACS_OAUTH2_URL = "GOPACS_OAUTH2_URL"; - public static final String DEFAULT_GOPACS_OAUTH2_URL = "https://auth.gopacs-services.eu/realms/gopacs/protocol/openid-connect/token"; - public static final String GOPACS_CLIENT_ID = "GOPACS_CLIENT_ID"; - public static final String GOPACS_CLIENT_SECRET = "GOPACS_CLIENT_SECRET"; - public static final String GOPACS_RESPONSE_DELAY_SECONDS = "GOPACS_RESPONSE_DELAY_SECONDS"; - public static final String DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS = "10"; - public static final String GOPACS_FLEX_OFFER_DELAY_SECONDS = "GOPACS_FLEX_OFFER_DELAY_SECONDS"; - public static final String DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS = "30"; - public static final String DEPLOYMENT_PATH = "/gopacs"; - /** Scheme prefix GOPACS uses on congestion-point identifiers, e.g. "ean.265987182507322951". */ - public static final String EAN_PREFIX = "ean."; - - - protected static final UftpSerializer serializer = new UftpSerializer(new XmlSerializer(), new XsdValidator(new XsdSchemaProvider(new XsdFactory(new XsdSchemaFactoryPool())))); - - protected final boolean devMode; - protected final String contractedEAN; - protected final String electricitySupplierAssetId; - protected final String realm; - protected final String gopacsBrokerUrl; - protected final Map participants; - - protected final AssetProcessingService assetProcessingService; - protected final AssetPredictedDatapointService assetPredictedDatapointService; - protected final ScheduledExecutorService scheduledExecutorService; - protected final TimerService timerService; - protected final WebService webService; - - protected final ResteasyClient client; - protected final GOPACSAddressBookResource gopacsAddressBookResource; - protected final GOPACSAuthResource gopacsAuthResource; - protected final GOPACSServerResource gopacsServerResource; - - protected final ParticipantResolutionService participantResolutionService; - protected final UftpValidationService uftpValidationService; - protected final UftpReceivedMessageService uftpReceivedMessageService; - protected final UftpSendMessageService uftpSendMessageService; - protected final UftpCryptoService cryptoService; - protected final String privateKey; - protected final String clientId; - protected final String clientSecret; - - protected final int responseDelaySeconds; - protected final int flexOfferDelaySeconds; - protected ObjectMapper objectMapper; - - List> scheduledFutureList = new ArrayList<>(); - - public static class Factory { - protected Container container; - - public Factory(Container container) { - this.container = container; - } +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; - public GOPACSHandler createHandler(String contractedEan, String realm, String electricitySupplierAssetId) { - return new GOPACSHandler(contractedEan, realm, electricitySupplierAssetId, container); - } +/** This class handles the communication with the GoPacs server. */ +public class GOPACSHandler + implements UftpPayloadHandler, UftpParticipantService, ParticipantAuthorizationProvider { + + private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSHandler.class); + public static final String GOPACS_PRIVATE_KEY_FILE = "GOPACS_PRIVATE_KEY_FILE"; + public static final String GOPACS_BROKER_URL = "GOPACS_BROKER_URL"; + public static final String DEFAULT_GOPACS_BROKER_URL = + "https://clc-message-broker.gopacs-services.eu"; + public static final String GOPACS_PARTICIPANT_URL = "GOPACS_PARTICIPANT_URL"; + public static final String DEFAULT_GOPACS_PARTICIPANT_URL = "https://api.gopacs-services.eu"; + public static final String GOPACS_OAUTH2_URL = "GOPACS_OAUTH2_URL"; + public static final String DEFAULT_GOPACS_OAUTH2_URL = + "https://auth.gopacs-services.eu/realms/gopacs/protocol/openid-connect/token"; + public static final String GOPACS_CLIENT_ID = "GOPACS_CLIENT_ID"; + public static final String GOPACS_CLIENT_SECRET = "GOPACS_CLIENT_SECRET"; + public static final String GOPACS_RESPONSE_DELAY_SECONDS = "GOPACS_RESPONSE_DELAY_SECONDS"; + public static final String DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS = "10"; + public static final String GOPACS_FLEX_OFFER_DELAY_SECONDS = "GOPACS_FLEX_OFFER_DELAY_SECONDS"; + public static final String DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS = "30"; + public static final String DEPLOYMENT_PATH = "/gopacs"; + + /** Scheme prefix GOPACS uses on congestion-point identifiers, e.g. "ean.265987182507322951". */ + public static final String EAN_PREFIX = "ean."; + + protected static final UftpSerializer serializer = + new UftpSerializer( + new XmlSerializer(), + new XsdValidator(new XsdSchemaProvider(new XsdFactory(new XsdSchemaFactoryPool())))); + + protected final boolean devMode; + protected final String contractedEAN; + protected final String electricitySupplierAssetId; + protected final String realm; + protected final String gopacsBrokerUrl; + protected final Map participants; + + protected final AssetProcessingService assetProcessingService; + protected final AssetPredictedDatapointService assetPredictedDatapointService; + protected final ScheduledExecutorService scheduledExecutorService; + protected final TimerService timerService; + protected final WebService webService; + + protected final ResteasyClient client; + protected final GOPACSAddressBookResource gopacsAddressBookResource; + protected final GOPACSAuthResource gopacsAuthResource; + protected final GOPACSServerResource gopacsServerResource; + + protected final ParticipantResolutionService participantResolutionService; + protected final UftpValidationService uftpValidationService; + protected final UftpReceivedMessageService uftpReceivedMessageService; + protected final UftpSendMessageService uftpSendMessageService; + protected final UftpCryptoService cryptoService; + protected final String privateKey; + protected final String clientId; + protected final String clientSecret; + + protected final int responseDelaySeconds; + protected final int flexOfferDelaySeconds; + protected ObjectMapper objectMapper; + + List> scheduledFutureList = new ArrayList<>(); + + public static class Factory { + protected Container container; + + public Factory(Container container) { + this.container = container; } - protected GOPACSHandler(String contractedEAN, String realm, String electricitySupplierAssetId, Container container) { - this.devMode = container.isDevMode(); - this.contractedEAN = contractedEAN; - this.realm = realm; - this.electricitySupplierAssetId = electricitySupplierAssetId; - this.participants = new HashMap<>(); - - this.assetProcessingService = container.getService(AssetProcessingService.class); - this.assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); - this.scheduledExecutorService = container.getScheduledExecutor(); - this.timerService = container.getService(TimerService.class); - this.webService = container.getService(WebService.class); - - // Strip trailing slashes so the synthesised broker endpoint never contains a double slash - this.gopacsBrokerUrl = container.getConfig().getOrDefault(GOPACS_BROKER_URL, DEFAULT_GOPACS_BROKER_URL).replaceAll("/+$", ""); - this.responseDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_RESPONSE_DELAY_SECONDS, DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS)); - this.flexOfferDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_FLEX_OFFER_DELAY_SECONDS, DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS)); - - // Initialize OAuth2 configuration first - this.clientId = container.getConfig().get(GOPACS_CLIENT_ID); - this.clientSecret = container.getConfig().get(GOPACS_CLIENT_SECRET); - - if (clientId == null || clientId.isBlank() || clientSecret == null || clientSecret.isBlank()) { - throw new RuntimeException(GOPACS_CLIENT_ID + " or " + GOPACS_CLIENT_SECRET + " not defined, cannot use GOPACS."); - } - - String goPacsPrivateKeyFile = container.getConfig().get(GOPACS_PRIVATE_KEY_FILE); - if (TextUtil.isNullOrEmpty(goPacsPrivateKeyFile)) { - throw new RuntimeException(GOPACS_PRIVATE_KEY_FILE + " not defined, can not send use GOPACS."); - } - if (!Files.isReadable(Paths.get(goPacsPrivateKeyFile))) { - throw new RuntimeException(GOPACS_PRIVATE_KEY_FILE + " invalid path or file not readable: " + goPacsPrivateKeyFile); - } - // Read the private key from file - try { - this.privateKey = Files.readString(Paths.get(goPacsPrivateKeyFile)); - } catch (Exception ex) { - throw new RuntimeException("Failed to initialize GOPACSHandler for ean " + contractedEAN + ".", ex); - } - - this.client = createClient(org.openremote.container.Container.EXECUTOR); - - String addressBookUrl = container.getConfig().getOrDefault(GOPACS_PARTICIPANT_URL, DEFAULT_GOPACS_PARTICIPANT_URL); - String oAuth2Url = container.getConfig().getOrDefault(GOPACS_OAUTH2_URL, DEFAULT_GOPACS_OAUTH2_URL); - - this.gopacsAddressBookResource = client.target(addressBookUrl).proxy(GOPACSAddressBookResource.class); - this.gopacsAuthResource = client.target(oAuth2Url).proxy(GOPACSAuthResource.class); - this.gopacsServerResource = new GOPACSServerResourceImpl(this::processRawMessage); - - this.participantResolutionService = new ParticipantResolutionService(this); - this.cryptoService = new UftpCryptoService(participantResolutionService, new LazySodiumFactory(), new LazySodiumBase64Pool()); - this.uftpValidationService = new UftpValidationService(new ArrayList<>()); - this.uftpReceivedMessageService = new UftpReceivedMessageService(uftpValidationService, this); - this.uftpSendMessageService = new UftpSendMessageService(serializer, cryptoService, participantResolutionService, this, uftpValidationService); - - this.objectMapper = new ObjectMapper(); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - - deploy(container); + public GOPACSHandler createHandler( + String contractedEan, String realm, String electricitySupplierAssetId) { + return new GOPACSHandler(contractedEan, realm, electricitySupplierAssetId, container); } - - /** - * Test-support constructor. Wires the message-processing collaborators directly and skips - * remote configuration (OAuth client, private-key file) and JAX-RS deployment, so the - * day-ahead UFTP message flow can be exercised in isolation. Not used in production wiring. - */ - protected GOPACSHandler(String contractedEAN, - String realm, - String electricitySupplierAssetId, - AssetProcessingService assetProcessingService, - AssetPredictedDatapointService assetPredictedDatapointService, - TimerService timerService, - ScheduledExecutorService scheduledExecutorService, - String privateKey) { - this.devMode = false; - this.contractedEAN = contractedEAN; - this.realm = realm; - this.electricitySupplierAssetId = electricitySupplierAssetId; - this.participants = new HashMap<>(); - - this.assetProcessingService = assetProcessingService; - this.assetPredictedDatapointService = assetPredictedDatapointService; - this.timerService = timerService; - this.scheduledExecutorService = scheduledExecutorService; - this.webService = null; - - this.gopacsBrokerUrl = ""; - this.responseDelaySeconds = 0; - this.flexOfferDelaySeconds = 0; - this.clientId = null; - this.clientSecret = null; - this.privateKey = privateKey; - - this.client = null; - this.gopacsAddressBookResource = null; - this.gopacsAuthResource = null; - this.gopacsServerResource = null; - - this.participantResolutionService = new ParticipantResolutionService(this); - this.cryptoService = new UftpCryptoService(participantResolutionService, new LazySodiumFactory(), new LazySodiumBase64Pool()); - this.uftpValidationService = new UftpValidationService(new ArrayList<>()); - this.uftpReceivedMessageService = new UftpReceivedMessageService(uftpValidationService, this); - this.uftpSendMessageService = new UftpSendMessageService(serializer, cryptoService, participantResolutionService, this, uftpValidationService); - - this.objectMapper = new ObjectMapper(); + } + + protected GOPACSHandler( + String contractedEAN, String realm, String electricitySupplierAssetId, Container container) { + this.devMode = container.isDevMode(); + this.contractedEAN = contractedEAN; + this.realm = realm; + this.electricitySupplierAssetId = electricitySupplierAssetId; + this.participants = new HashMap<>(); + + this.assetProcessingService = container.getService(AssetProcessingService.class); + this.assetPredictedDatapointService = + container.getService(AssetPredictedDatapointService.class); + this.scheduledExecutorService = container.getScheduledExecutor(); + this.timerService = container.getService(TimerService.class); + this.webService = container.getService(WebService.class); + + // Strip trailing slashes so the synthesised broker endpoint never contains a double slash + this.gopacsBrokerUrl = + container + .getConfig() + .getOrDefault(GOPACS_BROKER_URL, DEFAULT_GOPACS_BROKER_URL) + .replaceAll("/+$", ""); + this.responseDelaySeconds = + Integer.parseInt( + container + .getConfig() + .getOrDefault( + GOPACS_RESPONSE_DELAY_SECONDS, DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS)); + this.flexOfferDelaySeconds = + Integer.parseInt( + container + .getConfig() + .getOrDefault( + GOPACS_FLEX_OFFER_DELAY_SECONDS, DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS)); + + // Initialize OAuth2 configuration first + this.clientId = container.getConfig().get(GOPACS_CLIENT_ID); + this.clientSecret = container.getConfig().get(GOPACS_CLIENT_SECRET); + + if (clientId == null || clientId.isBlank() || clientSecret == null || clientSecret.isBlank()) { + throw new RuntimeException( + GOPACS_CLIENT_ID + " or " + GOPACS_CLIENT_SECRET + " not defined, cannot use GOPACS."); } - protected static String getDeploymentName(String contractedEAN) { - return "GOPACS: " + contractedEAN; + String goPacsPrivateKeyFile = container.getConfig().get(GOPACS_PRIVATE_KEY_FILE); + if (TextUtil.isNullOrEmpty(goPacsPrivateKeyFile)) { + throw new RuntimeException( + GOPACS_PRIVATE_KEY_FILE + " not defined, can not send use GOPACS."); } - - protected void deploy(Container container) { - LOG.info("Deploying JAX-RS deployment for instance : " + this); - - List singletons = Stream.of(getStandardProviders(devMode), Collections.singletonList(gopacsServerResource)) - .flatMap(Collection::stream) - .toList(); - Application application = new WebApplication( - container, - null, - singletons); - - webService.deployJaxRsApplication(application, DEPLOYMENT_PATH, getDeploymentName(contractedEAN), 0, true, new CORSConfig() - .setCorsAllowedOrigins(Collections.singleton(CORSConfig.DEFAULT_CORS_ALLOW_ALL)) - .setCorsAllowedMethods(CORSConfig.DEFAULT_CORS_ALLOW_ALL) - .setCorsAllowedHeaders(CORSConfig.DEFAULT_CORS_ALLOW_ALL)); + if (!Files.isReadable(Paths.get(goPacsPrivateKeyFile))) { + throw new RuntimeException( + GOPACS_PRIVATE_KEY_FILE + " invalid path or file not readable: " + goPacsPrivateKeyFile); } - - public void undeploy() { - for (ScheduledFuture scheduledFuture : scheduledFutureList) { - scheduledFuture.cancel(true); - } - scheduledFutureList.clear(); - webService.undeploy(getDeploymentName(contractedEAN)); + // Read the private key from file + try { + this.privateKey = Files.readString(Paths.get(goPacsPrivateKeyFile)); + } catch (Exception ex) { + throw new RuntimeException( + "Failed to initialize GOPACSHandler for ean " + contractedEAN + ".", ex); } - @Override - public void notifyNewIncomingMessage(IncomingUftpMessage message) { - LOG.info("Received message with conversation ID: " + message.payloadMessage().getConversationID() + " type " + message.payloadMessage().getClass().getSimpleName()); - var messageType = message.payloadMessage().getClass(); - - if (FlexRequest.class.isAssignableFrom(messageType)) { - var flexRequest = (FlexRequest) message.payloadMessage(); - LOG.fine("Processing FlexRequest: " + flexRequest.getConversationID()); - handleFlexRequestMessage(message.sender(), flexRequest); - LOG.fine("Finished processing FlexRequest: " + flexRequest.getConversationID()); - } else if (FlexOfferResponse.class.isAssignableFrom(messageType)) { - var flexOfferResponse = (FlexOfferResponse) message.payloadMessage(); - LOG.fine("Processing FlexOfferResponse: " + flexOfferResponse.getConversationID()); - handleFlexOfferResponseMessage(message.sender(), flexOfferResponse); - LOG.fine("Finished processing FlexOfferResponse: " + flexOfferResponse.getConversationID()); - } else if (FlexOrder.class.isAssignableFrom(messageType)) { - var flexOrder = (FlexOrder) message.payloadMessage(); - LOG.fine("Processing FlexOrder: " + flexOrder.getConversationID()); - handleFlexOrderMessage(message.sender(), flexOrder); - LOG.fine("Finished processing FlexOrder: " + flexOrder.getConversationID()); - } else { - LOG.warning("Received unknown message type: " + messageType.getSimpleName()); - } - LOG.info("Finished processing message with conversation ID: " + message.payloadMessage().getConversationID() + " type " + message.payloadMessage().getClass().getSimpleName()); + this.client = createClient(org.openremote.container.Container.EXECUTOR); + + String addressBookUrl = + container.getConfig().getOrDefault(GOPACS_PARTICIPANT_URL, DEFAULT_GOPACS_PARTICIPANT_URL); + String oAuth2Url = + container.getConfig().getOrDefault(GOPACS_OAUTH2_URL, DEFAULT_GOPACS_OAUTH2_URL); + + this.gopacsAddressBookResource = + client.target(addressBookUrl).proxy(GOPACSAddressBookResource.class); + this.gopacsAuthResource = client.target(oAuth2Url).proxy(GOPACSAuthResource.class); + this.gopacsServerResource = new GOPACSServerResourceImpl(this::processRawMessage); + + this.participantResolutionService = new ParticipantResolutionService(this); + this.cryptoService = + new UftpCryptoService( + participantResolutionService, new LazySodiumFactory(), new LazySodiumBase64Pool()); + this.uftpValidationService = new UftpValidationService(new ArrayList<>()); + this.uftpReceivedMessageService = new UftpReceivedMessageService(uftpValidationService, this); + this.uftpSendMessageService = + new UftpSendMessageService( + serializer, cryptoService, participantResolutionService, this, uftpValidationService); + + this.objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + deploy(container); + } + + /** + * Test-support constructor. Wires the message-processing collaborators directly and skips remote + * configuration (OAuth client, private-key file) and JAX-RS deployment, so the day-ahead UFTP + * message flow can be exercised in isolation. Not used in production wiring. + */ + protected GOPACSHandler( + String contractedEAN, + String realm, + String electricitySupplierAssetId, + AssetProcessingService assetProcessingService, + AssetPredictedDatapointService assetPredictedDatapointService, + TimerService timerService, + ScheduledExecutorService scheduledExecutorService, + String privateKey) { + this.devMode = false; + this.contractedEAN = contractedEAN; + this.realm = realm; + this.electricitySupplierAssetId = electricitySupplierAssetId; + this.participants = new HashMap<>(); + + this.assetProcessingService = assetProcessingService; + this.assetPredictedDatapointService = assetPredictedDatapointService; + this.timerService = timerService; + this.scheduledExecutorService = scheduledExecutorService; + this.webService = null; + + this.gopacsBrokerUrl = ""; + this.responseDelaySeconds = 0; + this.flexOfferDelaySeconds = 0; + this.clientId = null; + this.clientSecret = null; + this.privateKey = privateKey; + + this.client = null; + this.gopacsAddressBookResource = null; + this.gopacsAuthResource = null; + this.gopacsServerResource = null; + + this.participantResolutionService = new ParticipantResolutionService(this); + this.cryptoService = + new UftpCryptoService( + participantResolutionService, new LazySodiumFactory(), new LazySodiumBase64Pool()); + this.uftpValidationService = new UftpValidationService(new ArrayList<>()); + this.uftpReceivedMessageService = new UftpReceivedMessageService(uftpValidationService, this); + this.uftpSendMessageService = + new UftpSendMessageService( + serializer, cryptoService, participantResolutionService, this, uftpValidationService); + + this.objectMapper = new ObjectMapper(); + } + + protected static String getDeploymentName(String contractedEAN) { + return "GOPACS: " + contractedEAN; + } + + protected void deploy(Container container) { + LOG.info("Deploying JAX-RS deployment for instance : " + this); + + List singletons = + Stream.of( + getStandardProviders(devMode), + Collections.singletonList(gopacsServerResource)) + .flatMap(Collection::stream) + .toList(); + Application application = new WebApplication(container, null, singletons); + + webService.deployJaxRsApplication( + application, + DEPLOYMENT_PATH, + getDeploymentName(contractedEAN), + 0, + true, + new CORSConfig() + .setCorsAllowedOrigins(Collections.singleton(CORSConfig.DEFAULT_CORS_ALLOW_ALL)) + .setCorsAllowedMethods(CORSConfig.DEFAULT_CORS_ALLOW_ALL) + .setCorsAllowedHeaders(CORSConfig.DEFAULT_CORS_ALLOW_ALL)); + } + + public void undeploy() { + for (ScheduledFuture scheduledFuture : scheduledFutureList) { + scheduledFuture.cancel(true); } - - @Override - public void notifyNewOutgoingMessage(OutgoingUftpMessage message) { - LOG.info("Sending message: " + message.payloadMessage().getConversationID() + " type " + message.payloadMessage().getClass().getSimpleName()); - try { - if (message.payloadMessage() instanceof FlexRequestResponse || - message.payloadMessage() instanceof FlexOffer || - message.payloadMessage() instanceof FlexOrderResponse) { - - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Content to send: " + serializer.toXml(message.payloadMessage())); - } - - String recipientDomain = message.payloadMessage().getRecipientDomain(); - uftpSendMessageService.attemptToSendMessage( - message.payloadMessage(), - new SigningDetails( - message.sender(), - this.privateKey, - new UftpParticipant(recipientDomain, UftpRoleInformation.getRecipientRoleBySenderRole(message.sender().role())) - ) - ); - } - LOG.fine("Finished sending message: " + message.payloadMessage().getConversationID()); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to send message", e); - } + scheduledFutureList.clear(); + webService.undeploy(getDeploymentName(contractedEAN)); + } + + @Override + public void notifyNewIncomingMessage(IncomingUftpMessage message) { + LOG.info( + "Received message with conversation ID: " + + message.payloadMessage().getConversationID() + + " type " + + message.payloadMessage().getClass().getSimpleName()); + var messageType = message.payloadMessage().getClass(); + + if (FlexRequest.class.isAssignableFrom(messageType)) { + var flexRequest = (FlexRequest) message.payloadMessage(); + LOG.fine("Processing FlexRequest: " + flexRequest.getConversationID()); + handleFlexRequestMessage(message.sender(), flexRequest); + LOG.fine("Finished processing FlexRequest: " + flexRequest.getConversationID()); + } else if (FlexOfferResponse.class.isAssignableFrom(messageType)) { + var flexOfferResponse = (FlexOfferResponse) message.payloadMessage(); + LOG.fine("Processing FlexOfferResponse: " + flexOfferResponse.getConversationID()); + handleFlexOfferResponseMessage(message.sender(), flexOfferResponse); + LOG.fine("Finished processing FlexOfferResponse: " + flexOfferResponse.getConversationID()); + } else if (FlexOrder.class.isAssignableFrom(messageType)) { + var flexOrder = (FlexOrder) message.payloadMessage(); + LOG.fine("Processing FlexOrder: " + flexOrder.getConversationID()); + handleFlexOrderMessage(message.sender(), flexOrder); + LOG.fine("Finished processing FlexOrder: " + flexOrder.getConversationID()); + } else { + LOG.warning("Received unknown message type: " + messageType.getSimpleName()); } - - @Override - public String getAuthorizationHeader(UftpParticipant uftpParticipant) { - LOG.fine("Getting authorization header for: " + uftpParticipant); - String authorization = fetchBearerToken(); - if (authorization.isBlank()) { - LOG.warning("No OAuth2 bearer token available for authorization header of " + uftpParticipant); + LOG.info( + "Finished processing message with conversation ID: " + + message.payloadMessage().getConversationID() + + " type " + + message.payloadMessage().getClass().getSimpleName()); + } + + @Override + public void notifyNewOutgoingMessage(OutgoingUftpMessage message) { + LOG.info( + "Sending message: " + + message.payloadMessage().getConversationID() + + " type " + + message.payloadMessage().getClass().getSimpleName()); + try { + if (message.payloadMessage() instanceof FlexRequestResponse + || message.payloadMessage() instanceof FlexOffer + || message.payloadMessage() instanceof FlexOrderResponse) { + + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Content to send: " + serializer.toXml(message.payloadMessage())); } - return authorization; - } - protected String fetchBearerToken() { - try { - try (Response response = gopacsAuthResource.getAccessToken( - "client_credentials", - this.clientId, - this.clientSecret - )) { - if (response.getStatus() == 200) { - String responseBody = response.readEntity(String.class); - OAuth2TokenResponse tokenResponse = objectMapper.readValue(responseBody, OAuth2TokenResponse.class); - return "Bearer " + tokenResponse.getAccessToken(); - } - LOG.warning("OAuth2 token request failed with status: " + response.getStatus()); - return ""; - } - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to obtain OAuth2 access token", e); - return ""; - } + String recipientDomain = message.payloadMessage().getRecipientDomain(); + uftpSendMessageService.attemptToSendMessage( + message.payloadMessage(), + new SigningDetails( + message.sender(), + this.privateKey, + new UftpParticipant( + recipientDomain, + UftpRoleInformation.getRecipientRoleBySenderRole(message.sender().role())))); + } + LOG.fine("Finished sending message: " + message.payloadMessage().getConversationID()); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to send message", e); } - - protected void handleFlexRequestMessage(UftpParticipant participant, FlexRequest flexRequest) { - LOG.fine("Received Flex Request: " + flexRequest.getConversationID()); - int year = flexRequest.getPeriod().getYear(); - int month = flexRequest.getPeriod().getMonthValue(); - int day = flexRequest.getPeriod().getDayOfMonth(); - List> powerImportMaxGopacsDatapoints = new ArrayList<>(); - List> powerExportMaxGopacsDatapoints = new ArrayList<>(); - - for (int i = 0; i < flexRequest.getISPS().size(); i++) { - FlexRequestISPType flexRequestISPType = flexRequest.getISPS().get(i); - LocalTime start = FlexRequestISPTypeHelper.getISPStart(flexRequestISPType.getStart(), year, month, day, flexRequest.getTimeZone()); - double importMax = flexRequestISPType.getMaxPower() == 0L ? 0 : (double) flexRequestISPType.getMaxPower() / 1000.0F; - double exportMax = flexRequestISPType.getMinPower() == 0L ? 0 : (double) Math.abs(flexRequestISPType.getMinPower()) / 1000.0F; - LOG.fine("importMax:" + importMax + " exportMax:" + exportMax); - this.schedulePowerUpdate(start, EmsGOPACSAsset.POWER_MAXIMUM_FLEX_REQUEST.getName(), exportMax); - this.schedulePowerUpdate(start, EmsGOPACSAsset.POWER_MINIMUM_FLEX_REQUEST.getName(), exportMax); - - // Correct usage of ZoneId.of instead of ZoneOffset.of - ZoneId zoneId = ZoneId.of(flexRequest.getTimeZone()); - long startEpochMilli = flexRequest.getPeriod().atTime(start).atZone(zoneId).toInstant().toEpochMilli(); - - powerImportMaxGopacsDatapoints.add(new ValueDatapoint<>(startEpochMilli, importMax)); - powerExportMaxGopacsDatapoints.add(new ValueDatapoint<>(startEpochMilli, exportMax)); - } - - this.setPredictedDataPoints(EmsGOPACSAsset.POWER_MAXIMUM_FLEX_REQUEST.getName(), powerImportMaxGopacsDatapoints); - this.setPredictedDataPoints(EmsGOPACSAsset.POWER_MINIMUM_FLEX_REQUEST.getName(), powerExportMaxGopacsDatapoints); - LOG.fine("Finished processing Flex Request: " + flexRequest); + } + + @Override + public String getAuthorizationHeader(UftpParticipant uftpParticipant) { + LOG.fine("Getting authorization header for: " + uftpParticipant); + String authorization = fetchBearerToken(); + if (authorization.isBlank()) { + LOG.warning( + "No OAuth2 bearer token available for authorization header of " + uftpParticipant); } - - protected void handleFlexOfferResponseMessage(UftpParticipant participant, FlexOfferResponse flexOfferResponse) { - // Handle the response to our FlexOffer - if (flexOfferResponse.getResult() == AcceptedRejectedType.ACCEPTED) { - LOG.info("FlexOffer accepted: " + flexOfferResponse.getConversationID()); - } else { - LOG.warning("FlexOffer rejected: " + flexOfferResponse.getConversationID()); + return authorization; + } + + protected String fetchBearerToken() { + try { + try (Response response = + gopacsAuthResource.getAccessToken( + "client_credentials", this.clientId, this.clientSecret)) { + if (response.getStatus() == 200) { + String responseBody = response.readEntity(String.class); + OAuth2TokenResponse tokenResponse = + objectMapper.readValue(responseBody, OAuth2TokenResponse.class); + return "Bearer " + tokenResponse.getAccessToken(); } + LOG.warning("OAuth2 token request failed with status: " + response.getStatus()); + return ""; + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to obtain OAuth2 access token", e); + return ""; + } + } + + protected void handleFlexRequestMessage(UftpParticipant participant, FlexRequest flexRequest) { + LOG.fine("Received Flex Request: " + flexRequest.getConversationID()); + int year = flexRequest.getPeriod().getYear(); + int month = flexRequest.getPeriod().getMonthValue(); + int day = flexRequest.getPeriod().getDayOfMonth(); + List> powerImportMaxGopacsDatapoints = new ArrayList<>(); + List> powerExportMaxGopacsDatapoints = new ArrayList<>(); + + for (int i = 0; i < flexRequest.getISPS().size(); i++) { + FlexRequestISPType flexRequestISPType = flexRequest.getISPS().get(i); + LocalTime start = + FlexRequestISPTypeHelper.getISPStart( + flexRequestISPType.getStart(), year, month, day, flexRequest.getTimeZone()); + double importMax = + flexRequestISPType.getMaxPower() == 0L + ? 0 + : (double) flexRequestISPType.getMaxPower() / 1000.0F; + double exportMax = + flexRequestISPType.getMinPower() == 0L + ? 0 + : (double) Math.abs(flexRequestISPType.getMinPower()) / 1000.0F; + LOG.fine("importMax:" + importMax + " exportMax:" + exportMax); + this.schedulePowerUpdate( + start, EmsGOPACSAsset.POWER_MAXIMUM_FLEX_REQUEST.getName(), exportMax); + this.schedulePowerUpdate( + start, EmsGOPACSAsset.POWER_MINIMUM_FLEX_REQUEST.getName(), exportMax); + + // Correct usage of ZoneId.of instead of ZoneOffset.of + ZoneId zoneId = ZoneId.of(flexRequest.getTimeZone()); + long startEpochMilli = + flexRequest.getPeriod().atTime(start).atZone(zoneId).toInstant().toEpochMilli(); + + powerImportMaxGopacsDatapoints.add(new ValueDatapoint<>(startEpochMilli, importMax)); + powerExportMaxGopacsDatapoints.add(new ValueDatapoint<>(startEpochMilli, exportMax)); } - protected void handleFlexOrderMessage(UftpParticipant participant, FlexOrder flexOrder) { - LOG.fine("Received FlexOrder for FlexOffer: " + flexOrder.getConversationID()); - - int year = flexOrder.getPeriod().getYear(); - int month = flexOrder.getPeriod().getMonthValue(); - int day = flexOrder.getPeriod().getDayOfMonth(); - List> powerDatapoints = new ArrayList<>(); - List> powerDatapointsFeedin = new ArrayList<>(); - List> powerDatapointsOfftake = new ArrayList<>(); - - for (int i = 0; i < flexOrder.getISPS().size(); i++) { - FlexOrderISPType flexOrderISPType = flexOrder.getISPS().get(i); - LocalTime start = FlexRequestISPTypeHelper.getISPStart(flexOrderISPType.getStart(), year, month, day, flexOrder.getTimeZone()); - double power = flexOrderISPType.getPower() / 1000.0F; - LOG.fine("power:" + power); - this.schedulePowerUpdate(start, EmsGOPACSAsset.CURRENT_POWER_FLEX_REQUEST.getName(), power); - - // Correct usage of ZoneId.of instead of ZoneOffset.of - ZoneId zoneId = ZoneId.of(flexOrder.getTimeZone()); - long startEpochMilli = flexOrder.getPeriod().atTime(start).atZone(zoneId).toInstant().toEpochMilli(); - - powerDatapoints.add(new ValueDatapoint<>(startEpochMilli, power)); - - // NOTE: this logic assumes offtake MaxPower is always higher than 0 Watt. If this is not the case this logic needs to be changed - if (power > 0.0) { - powerDatapointsOfftake.add(new ValueDatapoint<>(startEpochMilli, power)); - } else { - powerDatapointsFeedin.add(new ValueDatapoint<>(startEpochMilli, power)); - } - } - - if (!powerDatapointsOfftake.isEmpty()) { - this.setPredictedDataPoints(EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), powerDatapointsOfftake); - } else if (!powerDatapointsFeedin.isEmpty()) { - this.setPredictedDataPoints(EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), powerDatapointsFeedin); - } + this.setPredictedDataPoints( + EmsGOPACSAsset.POWER_MAXIMUM_FLEX_REQUEST.getName(), powerImportMaxGopacsDatapoints); + this.setPredictedDataPoints( + EmsGOPACSAsset.POWER_MINIMUM_FLEX_REQUEST.getName(), powerExportMaxGopacsDatapoints); + LOG.fine("Finished processing Flex Request: " + flexRequest); + } + + protected void handleFlexOfferResponseMessage( + UftpParticipant participant, FlexOfferResponse flexOfferResponse) { + // Handle the response to our FlexOffer + if (flexOfferResponse.getResult() == AcceptedRejectedType.ACCEPTED) { + LOG.info("FlexOffer accepted: " + flexOfferResponse.getConversationID()); + } else { + LOG.warning("FlexOffer rejected: " + flexOfferResponse.getConversationID()); + } + } + + protected void handleFlexOrderMessage(UftpParticipant participant, FlexOrder flexOrder) { + LOG.fine("Received FlexOrder for FlexOffer: " + flexOrder.getConversationID()); + + int year = flexOrder.getPeriod().getYear(); + int month = flexOrder.getPeriod().getMonthValue(); + int day = flexOrder.getPeriod().getDayOfMonth(); + List> powerDatapoints = new ArrayList<>(); + List> powerDatapointsFeedin = new ArrayList<>(); + List> powerDatapointsOfftake = new ArrayList<>(); + + for (int i = 0; i < flexOrder.getISPS().size(); i++) { + FlexOrderISPType flexOrderISPType = flexOrder.getISPS().get(i); + LocalTime start = + FlexRequestISPTypeHelper.getISPStart( + flexOrderISPType.getStart(), year, month, day, flexOrder.getTimeZone()); + double power = flexOrderISPType.getPower() / 1000.0F; + LOG.fine("power:" + power); + this.schedulePowerUpdate(start, EmsGOPACSAsset.CURRENT_POWER_FLEX_REQUEST.getName(), power); + + // Correct usage of ZoneId.of instead of ZoneOffset.of + ZoneId zoneId = ZoneId.of(flexOrder.getTimeZone()); + long startEpochMilli = + flexOrder.getPeriod().atTime(start).atZone(zoneId).toInstant().toEpochMilli(); + + powerDatapoints.add(new ValueDatapoint<>(startEpochMilli, power)); + + // NOTE: this logic assumes offtake MaxPower is always higher than 0 Watt. If this is not the + // case this logic needs to be changed + if (power > 0.0) { + powerDatapointsOfftake.add(new ValueDatapoint<>(startEpochMilli, power)); + } else { + powerDatapointsFeedin.add(new ValueDatapoint<>(startEpochMilli, power)); + } + } - this.setPredictedDataPoints(EmsGOPACSAsset.CURRENT_POWER_FLEX_REQUEST.getName(), powerDatapoints); + if (!powerDatapointsOfftake.isEmpty()) { + this.setPredictedDataPoints( + EmsGOPACSAsset.POWER_LIMIT_MAXIMUM_PROFILE_FLEX_ORDER.getName(), powerDatapointsOfftake); + } else if (!powerDatapointsFeedin.isEmpty()) { + this.setPredictedDataPoints( + EmsGOPACSAsset.POWER_LIMIT_MINIMUM_PROFILE_FLEX_ORDER.getName(), powerDatapointsFeedin); } - protected void sendFlexOffer(UftpParticipant recipient, FlexRequest flexRequest) { - try { - FlexOffer flexOffer = new FlexOffer(); - flexOffer.setVersion("3.0.0"); - flexOffer.setSenderDomain(flexRequest.getRecipientDomain()); - flexOffer.setRecipientDomain(flexRequest.getSenderDomain()); - flexOffer.setTimeStamp(OffsetDateTime.now(ZoneId.systemDefault())); - flexOffer.setMessageID(UUID.randomUUID().toString()); - flexOffer.setConversationID(flexRequest.getConversationID()); - flexOffer.setISPDuration(flexRequest.getISPDuration()); - flexOffer.setTimeZone(flexRequest.getTimeZone()); - flexOffer.setPeriod(flexRequest.getPeriod()); - flexOffer.setCongestionPoint(flexRequest.getCongestionPoint()); - flexOffer.setExpirationDateTime(flexRequest.getExpirationDateTime()); - flexOffer.setFlexRequestMessageID(flexRequest.getMessageID()); - flexOffer.setContractID(flexRequest.getContractID()); - flexOffer.setBaselineReference(""); - flexOffer.setCurrency("EUR"); - - // Set ISPs based on the FlexRequest ISPs - List currentOptionIsps = new ArrayList<>(); - long previousPower = 0L, currentPower = 0L; - boolean firstIsp = true; - - for (FlexRequestISPType requestIsp : flexRequest.getISPS()) { - FlexOfferOptionISPType offerIsp = new FlexOfferOptionISPType(); - offerIsp.setStart(requestIsp.getStart()); - offerIsp.setDuration(requestIsp.getDuration()); - // Set power values based on your available flexibility - currentPower = calculatePower(requestIsp); - offerIsp.setPower(currentPower); - - // Add the ISP to the current option - currentOptionIsps.add(offerIsp); - - // Check if we need to create a new FlexOfferOptionType - // when power changes from negative to positive or vice versa - // Skip the check for the first ISP - if (!firstIsp && ((previousPower < 0 && currentPower >= 0) || (previousPower >= 0 && currentPower < 0))) { - // Create a new FlexOfferOptionType for the previous set of ISPs with the same sign - FlexOfferOptionType flexOfferOptionType = new FlexOfferOptionType(); - flexOfferOptionType.setOptionReference(UUID.randomUUID().toString()); - flexOfferOptionType.setPrice(new BigDecimal("0.00")); - - // Remove the current ISP from the list as it belongs to the next option - List previousOptionIsps = new ArrayList<>(currentOptionIsps); - previousOptionIsps.removeLast(); - - flexOfferOptionType.getISPS().addAll(previousOptionIsps); - flexOffer.getOfferOptions().add(flexOfferOptionType); - - // Start a new list with just the current ISP - currentOptionIsps = new ArrayList<>(); - currentOptionIsps.add(offerIsp); - } - - previousPower = currentPower; - firstIsp = false; - } - - // Create the final FlexOfferOptionType with any remaining ISPs - if (!currentOptionIsps.isEmpty()) { - FlexOfferOptionType finalOptionType = new FlexOfferOptionType(); - finalOptionType.setOptionReference(UUID.randomUUID().toString()); - finalOptionType.setPrice(new BigDecimal("0.00")); - finalOptionType.getISPS().addAll(currentOptionIsps); - flexOffer.getOfferOptions().add(finalOptionType); - } - - // Send the FlexOffer - UftpParticipant sender = new UftpParticipant(flexRequest.getRecipientDomain(), USEFRoleType.AGR); - notifyNewOutgoingMessage(OutgoingUftpMessage.create(sender, flexOffer)); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to send FlexOffer", e); + this.setPredictedDataPoints( + EmsGOPACSAsset.CURRENT_POWER_FLEX_REQUEST.getName(), powerDatapoints); + } + + protected void sendFlexOffer(UftpParticipant recipient, FlexRequest flexRequest) { + try { + FlexOffer flexOffer = new FlexOffer(); + flexOffer.setVersion("3.0.0"); + flexOffer.setSenderDomain(flexRequest.getRecipientDomain()); + flexOffer.setRecipientDomain(flexRequest.getSenderDomain()); + flexOffer.setTimeStamp(OffsetDateTime.now(ZoneId.systemDefault())); + flexOffer.setMessageID(UUID.randomUUID().toString()); + flexOffer.setConversationID(flexRequest.getConversationID()); + flexOffer.setISPDuration(flexRequest.getISPDuration()); + flexOffer.setTimeZone(flexRequest.getTimeZone()); + flexOffer.setPeriod(flexRequest.getPeriod()); + flexOffer.setCongestionPoint(flexRequest.getCongestionPoint()); + flexOffer.setExpirationDateTime(flexRequest.getExpirationDateTime()); + flexOffer.setFlexRequestMessageID(flexRequest.getMessageID()); + flexOffer.setContractID(flexRequest.getContractID()); + flexOffer.setBaselineReference(""); + flexOffer.setCurrency("EUR"); + + // Set ISPs based on the FlexRequest ISPs + List currentOptionIsps = new ArrayList<>(); + long previousPower = 0L, currentPower = 0L; + boolean firstIsp = true; + + for (FlexRequestISPType requestIsp : flexRequest.getISPS()) { + FlexOfferOptionISPType offerIsp = new FlexOfferOptionISPType(); + offerIsp.setStart(requestIsp.getStart()); + offerIsp.setDuration(requestIsp.getDuration()); + // Set power values based on your available flexibility + currentPower = calculatePower(requestIsp); + offerIsp.setPower(currentPower); + + // Add the ISP to the current option + currentOptionIsps.add(offerIsp); + + // Check if we need to create a new FlexOfferOptionType + // when power changes from negative to positive or vice versa + // Skip the check for the first ISP + if (!firstIsp + && ((previousPower < 0 && currentPower >= 0) + || (previousPower >= 0 && currentPower < 0))) { + // Create a new FlexOfferOptionType for the previous set of ISPs with the same sign + FlexOfferOptionType flexOfferOptionType = new FlexOfferOptionType(); + flexOfferOptionType.setOptionReference(UUID.randomUUID().toString()); + flexOfferOptionType.setPrice(new BigDecimal("0.00")); + + // Remove the current ISP from the list as it belongs to the next option + List previousOptionIsps = new ArrayList<>(currentOptionIsps); + previousOptionIsps.removeLast(); + + flexOfferOptionType.getISPS().addAll(previousOptionIsps); + flexOffer.getOfferOptions().add(flexOfferOptionType); + + // Start a new list with just the current ISP + currentOptionIsps = new ArrayList<>(); + currentOptionIsps.add(offerIsp); } - } - protected void schedulePowerUpdate(LocalTime start, String attributeName, double power) { - long currentTimeMillis = timerService.getCurrentTimeMillis(); - long ispStartMillis = start.atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long delay = ispStartMillis - currentTimeMillis; - scheduledExecutorService.schedule(() -> updatePowerValues(attributeName, power), delay, TimeUnit.MILLISECONDS); + previousPower = currentPower; + firstIsp = false; + } + + // Create the final FlexOfferOptionType with any remaining ISPs + if (!currentOptionIsps.isEmpty()) { + FlexOfferOptionType finalOptionType = new FlexOfferOptionType(); + finalOptionType.setOptionReference(UUID.randomUUID().toString()); + finalOptionType.setPrice(new BigDecimal("0.00")); + finalOptionType.getISPS().addAll(currentOptionIsps); + flexOffer.getOfferOptions().add(finalOptionType); + } + + // Send the FlexOffer + UftpParticipant sender = + new UftpParticipant(flexRequest.getRecipientDomain(), USEFRoleType.AGR); + notifyNewOutgoingMessage(OutgoingUftpMessage.create(sender, flexOffer)); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to send FlexOffer", e); } - - protected void updatePowerValues(String attributeName, double power) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(electricitySupplierAssetId, attributeName, power), getClass().getSimpleName()); + } + + protected void schedulePowerUpdate(LocalTime start, String attributeName, double power) { + long currentTimeMillis = timerService.getCurrentTimeMillis(); + long ispStartMillis = + start.atDate(LocalDate.now()).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long delay = ispStartMillis - currentTimeMillis; + scheduledExecutorService.schedule( + () -> updatePowerValues(attributeName, power), delay, TimeUnit.MILLISECONDS); + } + + protected void updatePowerValues(String attributeName, double power) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent(electricitySupplierAssetId, attributeName, power), + getClass().getSimpleName()); + } + + protected void setPredictedDataPoints( + String attributeName, List> valuesAndTimestamps) { + assetPredictedDatapointService.updateValues( + electricitySupplierAssetId, attributeName, valuesAndTimestamps); + } + + protected long calculatePower(FlexRequestISPType requestIsp) { + // TODO: Implement power calculation logic here + return requestIsp.getMaxPower() == 0 ? requestIsp.getMinPower() : requestIsp.getMaxPower(); + } + + /** + * Only act on flex messages whose congestion point matches this handler's contracted EAN. Returns + * false (and logs a warning) for out-of-scope messages so they are rejected (with a rejection + * response, but without asset mutation or FlexOffer). See issue #28 for full per-contract/role + * scoping via the V3 contracts endpoint. + */ + protected boolean isWithinContractedScope( + String messageType, String conversationId, String congestionPoint) { + if (Objects.equals(toCongestionPoint(contractedEAN), toCongestionPoint(congestionPoint))) { + return true; } - - protected void setPredictedDataPoints(String attributeName, List> valuesAndTimestamps) { - assetPredictedDatapointService.updateValues(electricitySupplierAssetId, attributeName, valuesAndTimestamps); + LOG.warning( + "Rejecting " + + messageType + + " " + + conversationId + + " for out-of-scope congestion point " + + congestionPoint + + " (contracted EAN " + + contractedEAN + + ")"); + return false; + } + + /** + * Converts an EAN / congestion-point identifier to the canonical GOPACS congestion-point format + * "ean.<code>" (for example "ean.265987182507322951"). GOPACS flex messages always carry + * the congestion point with the "ean." prefix, whereas the contracted EAN may be configured with + * or without it; canonicalising both sides keeps the scope check correct either way. + */ + protected static String toCongestionPoint(String ean) { + if (ean == null) { + return null; } - - protected long calculatePower(FlexRequestISPType requestIsp) { - //TODO: Implement power calculation logic here - return requestIsp.getMaxPower() == 0 ? requestIsp.getMinPower() : requestIsp.getMaxPower(); + String trimmed = ean.trim(); + if (trimmed.regionMatches(true, 0, EAN_PREFIX, 0, EAN_PREFIX.length())) { + // Already prefixed (any case) — normalise the prefix to its canonical lower-case form. + return EAN_PREFIX + trimmed.substring(EAN_PREFIX.length()); } - - /** - * Only act on flex messages whose congestion point matches this handler's contracted EAN. Returns - * false (and logs a warning) for out-of-scope messages so they are rejected (with a rejection - * response, but without asset mutation or FlexOffer). See issue #28 for full per-contract/role - * scoping via the V3 contracts endpoint. - */ - protected boolean isWithinContractedScope(String messageType, String conversationId, String congestionPoint) { - if (Objects.equals(toCongestionPoint(contractedEAN), toCongestionPoint(congestionPoint))) { - return true; - } - LOG.warning("Rejecting " + messageType + " " + conversationId + " for out-of-scope congestion point " - + congestionPoint + " (contracted EAN " + contractedEAN + ")"); - return false; + return EAN_PREFIX + trimmed; + } + + protected void processRawMessage(String transportXml) { + try { + SignedMessage signedMessage = serializer.fromSignedXml(transportXml); + String payloadXml = cryptoService.verifySignedMessage(signedMessage); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Received message:" + payloadXml); + } + PayloadMessageType payloadMessage = serializer.fromPayloadXml(payloadXml); + + // Re-assert EAN scoping that the V2 participant lookup enforced implicitly. The V3 lookup + // resolves any participant domain, so a validly signed flex message from a participant + // outside + // this handler's contracted EAN would otherwise be applied to the asset. Full + // per-contract/role + // scoping via the V3 contracts endpoint is tracked in issue #28. + // Out-of-scope messages are not applied to the asset and get no FlexOffer, but UFTP still + // requires a response: reject with a reason instead of leaving the DSO's conversation + // dangling. + if (payloadMessage instanceof FlexMessageType flexMessage + && !isWithinContractedScope( + payloadMessage.getClass().getSimpleName(), + payloadMessage.getConversationID(), + flexMessage.getCongestionPoint())) { + PayloadMessageType rejection = + UftpValidationResponseCreator.getResponseForMessage( + payloadMessage, + ValidationResult.rejection("CongestionPoint not within contracted scope")); + UftpParticipant sender = new UftpParticipant(signedMessage); + UftpParticipant responder = + new UftpParticipant( + payloadMessage.getRecipientDomain(), + UftpRoleInformation.getRecipientRoleBySenderRole(sender.role())); + // Send delayed so the HTTP 200 on the transport call goes out first, like the accepted path + scheduledExecutorService.schedule( + () -> notifyNewOutgoingMessage(OutgoingUftpMessage.create(responder, rejection)), + this.responseDelaySeconds, + TimeUnit.SECONDS); + return; + } + + var incomingUftpMessage = + IncomingUftpMessage.create( + new UftpParticipant(signedMessage), payloadMessage, transportXml, payloadXml); + notifyNewIncomingMessage(incomingUftpMessage); + + // Send response delayed to ensure HTTP response is sent first + scheduledExecutorService.schedule( + () -> { + uftpReceivedMessageService.process(incomingUftpMessage); + }, + this.responseDelaySeconds, + TimeUnit.SECONDS); // 10s delay to ensure HTTP response is sent + + // Check if the message is a FlexRequest and schedule sendFlexOffer with delay + if (payloadMessage instanceof FlexRequest flexRequest) { + UftpParticipant participant = new UftpParticipant(signedMessage); + + // Schedule FlexOffer to be sent after a short delay to ensure HTTP response is sent first + scheduledExecutorService.schedule( + () -> { + try { + sendFlexOffer(participant, flexRequest); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error sending delayed FlexOffer", e); + } + }, + this.flexOfferDelaySeconds, + TimeUnit + .SECONDS); // 30s delay to ensure FlexRequestResponse is sent and processed by the + // other party + } + } catch (UftpConnectorException e) { + LOG.log(Level.SEVERE, "Error processing raw message", e); + Throwable rootCause = e; + while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { + rootCause = rootCause.getCause(); + } + String errorMessage = rootCause.getMessage(); + if (errorMessage == null) { + errorMessage = "Invalid message format"; + } + throw new WebApplicationException(errorMessage, Response.Status.BAD_REQUEST); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error processing raw message", e); + // Do not expose internal exception details to the client + String errorMessage = "Internal server error occurred"; + throw new WebApplicationException(errorMessage, Response.Status.INTERNAL_SERVER_ERROR); } + } - /** - * Converts an EAN / congestion-point identifier to the canonical GOPACS congestion-point format - * "ean.<code>" (for example "ean.265987182507322951"). GOPACS flex messages always carry the - * congestion point with the "ean." prefix, whereas the contracted EAN may be configured with or - * without it; canonicalising both sides keeps the scope check correct either way. - */ - protected static String toCongestionPoint(String ean) { - if (ean == null) { - return null; - } - String trimmed = ean.trim(); - if (trimmed.regionMatches(true, 0, EAN_PREFIX, 0, EAN_PREFIX.length())) { - // Already prefixed (any case) — normalise the prefix to its canonical lower-case form. - return EAN_PREFIX + trimmed.substring(EAN_PREFIX.length()); - } - return EAN_PREFIX + trimmed; + @Override + public Optional getParticipantInformation( + USEFRoleType role, String domain) { + if (participants.containsKey(domain)) { + return Optional.of(participants.get(domain)); } - protected void processRawMessage(String transportXml) { - try { - SignedMessage signedMessage = serializer.fromSignedXml(transportXml); - String payloadXml = cryptoService.verifySignedMessage(signedMessage); - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Received message:" + payloadXml); - } - PayloadMessageType payloadMessage = serializer.fromPayloadXml(payloadXml); - - // Re-assert EAN scoping that the V2 participant lookup enforced implicitly. The V3 lookup - // resolves any participant domain, so a validly signed flex message from a participant outside - // this handler's contracted EAN would otherwise be applied to the asset. Full per-contract/role - // scoping via the V3 contracts endpoint is tracked in issue #28. - // Out-of-scope messages are not applied to the asset and get no FlexOffer, but UFTP still - // requires a response: reject with a reason instead of leaving the DSO's conversation dangling. - if (payloadMessage instanceof FlexMessageType flexMessage - && !isWithinContractedScope(payloadMessage.getClass().getSimpleName(), - payloadMessage.getConversationID(), flexMessage.getCongestionPoint())) { - PayloadMessageType rejection = UftpValidationResponseCreator.getResponseForMessage( - payloadMessage, ValidationResult.rejection("CongestionPoint not within contracted scope")); - UftpParticipant sender = new UftpParticipant(signedMessage); - UftpParticipant responder = new UftpParticipant(payloadMessage.getRecipientDomain(), - UftpRoleInformation.getRecipientRoleBySenderRole(sender.role())); - // Send delayed so the HTTP 200 on the transport call goes out first, like the accepted path - scheduledExecutorService.schedule(() -> - notifyNewOutgoingMessage(OutgoingUftpMessage.create(responder, rejection)), - this.responseDelaySeconds, TimeUnit.SECONDS); - return; - } - - var incomingUftpMessage = IncomingUftpMessage.create(new UftpParticipant(signedMessage), payloadMessage, transportXml, payloadXml); - notifyNewIncomingMessage(incomingUftpMessage); - - // Send response delayed to ensure HTTP response is sent first - scheduledExecutorService.schedule(() -> { - uftpReceivedMessageService.process(incomingUftpMessage); - }, this.responseDelaySeconds, TimeUnit.SECONDS); // 10s delay to ensure HTTP response is sent - - // Check if the message is a FlexRequest and schedule sendFlexOffer with delay - if (payloadMessage instanceof FlexRequest flexRequest) { - UftpParticipant participant = new UftpParticipant(signedMessage); - - // Schedule FlexOffer to be sent after a short delay to ensure HTTP response is sent first - scheduledExecutorService.schedule(() -> { - try { - sendFlexOffer(participant, flexRequest); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error sending delayed FlexOffer", e); - } - }, this.flexOfferDelaySeconds, TimeUnit.SECONDS); // 30s delay to ensure FlexRequestResponse is sent and processed by the other party - } - } catch (UftpConnectorException e) { - LOG.log(Level.SEVERE, "Error processing raw message", e); - Throwable rootCause = e; - while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { - rootCause = rootCause.getCause(); - } - String errorMessage = rootCause.getMessage(); - if (errorMessage == null) { - errorMessage = "Invalid message format"; - } - throw new WebApplicationException(errorMessage, Response.Status.BAD_REQUEST); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error processing raw message", e); - // Do not expose internal exception details to the client - String errorMessage = "Internal server error occurred"; - throw new WebApplicationException(errorMessage, Response.Status.INTERNAL_SERVER_ERROR); - } + String authorization = fetchBearerToken(); + if (authorization.isBlank()) { + LOG.warning( + "Skipping participant lookup for " + domain + ": no OAuth2 bearer token available"); + return Optional.empty(); } - - @Override - public Optional getParticipantInformation(USEFRoleType role, String domain) { - if (participants.containsKey(domain)) { - return Optional.of(participants.get(domain)); - } - - String authorization = fetchBearerToken(); - if (authorization.isBlank()) { - LOG.warning("Skipping participant lookup for " + domain + ": no OAuth2 bearer token available"); - return Optional.empty(); - } - try (Response response = gopacsAddressBookResource.fetchParticipantByDomain(authorization, domain)) { - int status = response != null ? response.getStatus() : -1; - if (status == 200) { - ParticipantView view = response.readEntity(ParticipantView.class); - UftpParticipantInformation info = new UftpParticipantInformation(view.domain(), view.publicKey(), this.gopacsBrokerUrl + "/shapeshifter/api/v3/message", true); - participants.put(view.domain(), info); - return Optional.of(info); - } - if (status == 404) { - LOG.fine("Participant not found in GOPACS address book: " + domain); - } else { - LOG.severe("Unexpected status " + status + " when requesting participant information for " + domain); - } - } catch (Exception e) { - Throwable cause = e.getCause() instanceof IOException ? e.getCause() : e; - LOG.log(Level.SEVERE, "Exception when requesting participant information for " + domain, cause); - } - return Optional.empty(); + try (Response response = + gopacsAddressBookResource.fetchParticipantByDomain(authorization, domain)) { + int status = response != null ? response.getStatus() : -1; + if (status == 200) { + ParticipantView view = response.readEntity(ParticipantView.class); + UftpParticipantInformation info = + new UftpParticipantInformation( + view.domain(), + view.publicKey(), + this.gopacsBrokerUrl + "/shapeshifter/api/v3/message", + true); + participants.put(view.domain(), info); + return Optional.of(info); + } + if (status == 404) { + LOG.fine("Participant not found in GOPACS address book: " + domain); + } else { + LOG.severe( + "Unexpected status " + + status + + " when requesting participant information for " + + domain); + } + } catch (Exception e) { + Throwable cause = e.getCause() instanceof IOException ? e.getCause() : e; + LOG.log( + Level.SEVERE, "Exception when requesting participant information for " + domain, cause); } + return Optional.empty(); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSRedispatchHandler.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSRedispatchHandler.java index 2d36388..1dc5bc7 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSRedispatchHandler.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSRedispatchHandler.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,17 +12,29 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static org.openremote.container.web.WebTargetBuilder.createClient; +import static org.openremote.model.syslog.SyslogCategory.API; + +import java.util.*; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import jakarta.ws.rs.core.Response; + import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.openremote.container.timer.TimerService; import org.openremote.extension.ems.agent.EmsGOPACSAsset; @@ -39,508 +48,606 @@ import org.openremote.model.datapoint.ValueDatapoint; import org.openremote.model.syslog.SyslogCategory; -import java.util.*; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static org.openremote.container.web.WebTargetBuilder.createClient; -import static org.openremote.model.syslog.SyslogCategory.API; +import jakarta.ws.rs.core.Response; /** - * Handles GOPACS Redispatch (intraday congestion management) by polling for - * congestion announcements, checking EAN solving effectivity, calculating - * suggested bid parameters, and managing the operator confirmation workflow. + * Handles GOPACS Redispatch (intraday congestion management) by polling for congestion + * announcements, checking EAN solving effectivity, calculating suggested bid parameters, and + * managing the operator confirmation workflow. */ public class GOPACSRedispatchHandler { - private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSRedispatchHandler.class); - - public static final String GOPACS_REDISPATCH_API_KEY = "GOPACS_REDISPATCH_API_KEY"; - public static final String GOPACS_REDISPATCH_URL = "GOPACS_REDISPATCH_URL"; - public static final String DEFAULT_GOPACS_REDISPATCH_URL = "https://idcons.gopacs-services.eu"; - public static final String GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES = "GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES"; - public static final String DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES = "5"; - - public static final String ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT = "CONGESTIONMANAGEMENT"; - public static final String ANNOUNCEMENT_STATE_OPEN = "ANNOUNCEMENT_OPEN"; - public static final String COMPLIANCE_TYPE_MANDATORY = "MANDATORY"; - public static final String BID_STATUS_NONE = "NONE"; - public static final String BID_STATUS_PENDING_CONFIRMATION = "PENDING_CONFIRMATION"; - public static final String BID_STATUS_CONFIRMED = "CONFIRMED"; - - protected record EanEffectivityResult( - String matchedCategory, - List effectivities - ) {} - - protected final String contractedEAN; - protected final String assetId; - protected final String realm; - - protected final AssetProcessingService assetProcessingService; - protected final AssetStorageService assetStorageService; - protected final AssetPredictedDatapointService assetPredictedDatapointService; - protected final ScheduledExecutorService scheduledExecutorService; - protected final TimerService timerService; - - protected final ResteasyClient client; - protected final GOPACSAnnouncementResource announcementResource; - protected final GOPACSEanEffectivityResource eanEffectivityResource; - - protected final ObjectMapper objectMapper; - protected final String apiKey; - protected final int pollIntervalMinutes; - - private static final int MAX_RECORDED_ANNOUNCEMENT_IDS = 10_000; - - private ScheduledFuture pollingFuture; - private String lastProcessedAnnouncementId; - // Bounded LRU set so a long-running handler does not accumulate every announcement ID it has ever seen. - private final Set recordedAnnouncementIds = Collections.newSetFromMap( - new LinkedHashMap<>(16, 0.75f, false) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_RECORDED_ANNOUNCEMENT_IDS; - } - }); - - public static class Factory { - protected Container container; - - public Factory(Container container) { - this.container = container; - } - - public GOPACSRedispatchHandler createHandler(String contractedEan, String realm, String assetId) { - return new GOPACSRedispatchHandler(contractedEan, realm, assetId, container); - } - } - - protected GOPACSRedispatchHandler(String contractedEAN, String realm, String assetId, Container container) { - this.contractedEAN = contractedEAN; - this.realm = realm; - this.assetId = assetId; - - this.assetProcessingService = container.getService(AssetProcessingService.class); - this.assetStorageService = container.getService(AssetStorageService.class); - this.assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); - this.scheduledExecutorService = container.getScheduledExecutor(); - this.timerService = container.getService(TimerService.class); - - this.apiKey = container.getConfig().get(GOPACS_REDISPATCH_API_KEY); - // GOPACS recommends to poll 5 or more minutes apart - String pollIntervalConfig = container.getConfig().getOrDefault( - GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES, DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES); - int parsedPollInterval; - try { - parsedPollInterval = Integer.parseInt(pollIntervalConfig); - } catch (NumberFormatException e) { - parsedPollInterval = Integer.parseInt(DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES); - LOG.warning("Invalid " + GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES + " value '" + pollIntervalConfig - + "', falling back to default " + parsedPollInterval + " minutes"); - } - this.pollIntervalMinutes = Math.max(5, parsedPollInterval); - - String redispatchUrl = container.getConfig().getOrDefault(GOPACS_REDISPATCH_URL, DEFAULT_GOPACS_REDISPATCH_URL); - - this.client = createClient(org.openremote.container.Container.EXECUTOR); - this.announcementResource = client.target(redispatchUrl).proxy(GOPACSAnnouncementResource.class); - this.eanEffectivityResource = client.target(redispatchUrl).proxy(GOPACSEanEffectivityResource.class); + private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSRedispatchHandler.class); + + public static final String GOPACS_REDISPATCH_API_KEY = "GOPACS_REDISPATCH_API_KEY"; + public static final String GOPACS_REDISPATCH_URL = "GOPACS_REDISPATCH_URL"; + public static final String DEFAULT_GOPACS_REDISPATCH_URL = "https://idcons.gopacs-services.eu"; + public static final String GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES = + "GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES"; + public static final String DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES = "5"; + + public static final String ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT = "CONGESTIONMANAGEMENT"; + public static final String ANNOUNCEMENT_STATE_OPEN = "ANNOUNCEMENT_OPEN"; + public static final String COMPLIANCE_TYPE_MANDATORY = "MANDATORY"; + public static final String BID_STATUS_NONE = "NONE"; + public static final String BID_STATUS_PENDING_CONFIRMATION = "PENDING_CONFIRMATION"; + public static final String BID_STATUS_CONFIRMED = "CONFIRMED"; + + protected record EanEffectivityResult( + String matchedCategory, List effectivities) {} + + protected final String contractedEAN; + protected final String assetId; + protected final String realm; + + protected final AssetProcessingService assetProcessingService; + protected final AssetStorageService assetStorageService; + protected final AssetPredictedDatapointService assetPredictedDatapointService; + protected final ScheduledExecutorService scheduledExecutorService; + protected final TimerService timerService; + + protected final ResteasyClient client; + protected final GOPACSAnnouncementResource announcementResource; + protected final GOPACSEanEffectivityResource eanEffectivityResource; + + protected final ObjectMapper objectMapper; + protected final String apiKey; + protected final int pollIntervalMinutes; + + private static final int MAX_RECORDED_ANNOUNCEMENT_IDS = 10_000; + + private ScheduledFuture pollingFuture; + private String lastProcessedAnnouncementId; + // Bounded LRU set so a long-running handler does not accumulate every announcement ID it has ever + // seen. + private final Set recordedAnnouncementIds = + Collections.newSetFromMap( + new LinkedHashMap<>(16, 0.75f, false) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_RECORDED_ANNOUNCEMENT_IDS; + } + }); - this.objectMapper = new ObjectMapper(); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + public static class Factory { + protected Container container; - LOG.info("Initialized GOPACSRedispatchHandler for EAN: " + contractedEAN + " (poll interval: " + pollIntervalMinutes + " min)"); + public Factory(Container container) { + this.container = container; } - public void startPolling() { - if (apiKey == null || apiKey.isBlank()) { - LOG.severe("GOPACS_REDISPATCH_API_KEY not configured; redispatch polling will not start for EAN: " - + contractedEAN + " (the API key is required to resolve EAN effectivity per announcement)"); - return; - } - if (pollingFuture != null && !pollingFuture.isCancelled()) { - LOG.warning("Polling already active for EAN: " + contractedEAN); - return; - } + public GOPACSRedispatchHandler createHandler( + String contractedEan, String realm, String assetId) { + return new GOPACSRedispatchHandler(contractedEan, realm, assetId, container); + } + } + + protected GOPACSRedispatchHandler( + String contractedEAN, String realm, String assetId, Container container) { + this.contractedEAN = contractedEAN; + this.realm = realm; + this.assetId = assetId; + + this.assetProcessingService = container.getService(AssetProcessingService.class); + this.assetStorageService = container.getService(AssetStorageService.class); + this.assetPredictedDatapointService = + container.getService(AssetPredictedDatapointService.class); + this.scheduledExecutorService = container.getScheduledExecutor(); + this.timerService = container.getService(TimerService.class); + + this.apiKey = container.getConfig().get(GOPACS_REDISPATCH_API_KEY); + // GOPACS recommends to poll 5 or more minutes apart + String pollIntervalConfig = + container + .getConfig() + .getOrDefault( + GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES, + DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES); + int parsedPollInterval; + try { + parsedPollInterval = Integer.parseInt(pollIntervalConfig); + } catch (NumberFormatException e) { + parsedPollInterval = Integer.parseInt(DEFAULT_GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES); + LOG.warning( + "Invalid " + + GOPACS_REDISPATCH_POLL_INTERVAL_MINUTES + + " value '" + + pollIntervalConfig + + "', falling back to default " + + parsedPollInterval + + " minutes"); + } + this.pollIntervalMinutes = Math.max(5, parsedPollInterval); + + String redispatchUrl = + container.getConfig().getOrDefault(GOPACS_REDISPATCH_URL, DEFAULT_GOPACS_REDISPATCH_URL); + + this.client = createClient(org.openremote.container.Container.EXECUTOR); + this.announcementResource = + client.target(redispatchUrl).proxy(GOPACSAnnouncementResource.class); + this.eanEffectivityResource = + client.target(redispatchUrl).proxy(GOPACSEanEffectivityResource.class); + + this.objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + LOG.info( + "Initialized GOPACSRedispatchHandler for EAN: " + + contractedEAN + + " (poll interval: " + + pollIntervalMinutes + + " min)"); + } + + public void startPolling() { + if (apiKey == null || apiKey.isBlank()) { + LOG.severe( + "GOPACS_REDISPATCH_API_KEY not configured; redispatch polling will not start for EAN: " + + contractedEAN + + " (the API key is required to resolve EAN effectivity per announcement)"); + return; + } + if (pollingFuture != null && !pollingFuture.isCancelled()) { + LOG.warning("Polling already active for EAN: " + contractedEAN); + return; + } - pollingFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { - try { + pollingFuture = + scheduledExecutorService.scheduleAtFixedRate( + () -> { + try { pollAndProcess(); - } catch (Exception e) { + } catch (Exception e) { LOG.log(Level.SEVERE, "Error during redispatch poll for EAN: " + contractedEAN, e); - } - }, 0, pollIntervalMinutes, TimeUnit.MINUTES); - - LOG.info("Started redispatch polling for EAN: " + contractedEAN); - } - - public void stopPolling() { - if (pollingFuture != null) { - // Interrupt to abort any in-flight HTTP call before closing the client - pollingFuture.cancel(true); - pollingFuture = null; - } - client.close(); - LOG.info("Stopped redispatch polling for EAN: " + contractedEAN); + } + }, + 0, + pollIntervalMinutes, + TimeUnit.MINUTES); + + LOG.info("Started redispatch polling for EAN: " + contractedEAN); + } + + public void stopPolling() { + if (pollingFuture != null) { + // Interrupt to abort any in-flight HTTP call before closing the client + pollingFuture.cancel(true); + pollingFuture = null; } + client.close(); + LOG.info("Stopped redispatch polling for EAN: " + contractedEAN); + } - protected void pollAndProcess() { - LOG.fine("Redispatch poll started for EAN: " + contractedEAN); + protected void pollAndProcess() { + LOG.fine("Redispatch poll started for EAN: " + contractedEAN); - EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); - if (gopacsAsset == null) { - LOG.warning("GOPACS asset not found: " + assetId); - return; - } - - LOG.fine("Fetching announcements for EAN: " + contractedEAN); + EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); + if (gopacsAsset == null) { + LOG.warning("GOPACS asset not found: " + assetId); + return; + } - // Fetch announcements - List announcements = fetchAnnouncements(); - if (announcements == null) { - // Fetch failed (HTTP error / exception). Skip this poll and keep existing - // announcement state so a transient connectivity issue does not flap the UI. - LOG.fine("Skipping announcement processing due to fetch failure for EAN: " + contractedEAN); - return; - } - if (announcements.isEmpty()) { - LOG.fine("No announcements found for EAN: " + contractedEAN); - updateLastPoll(); - clearAnnouncementAttributes(); - return; - } + LOG.fine("Fetching announcements for EAN: " + contractedEAN); - LOG.fine("Fetched " + announcements.size() + " announcements for EAN: " + contractedEAN); + // Fetch announcements + List announcements = fetchAnnouncements(); + if (announcements == null) { + // Fetch failed (HTTP error / exception). Skip this poll and keep existing + // announcement state so a transient connectivity issue does not flap the UI. + LOG.fine("Skipping announcement processing due to fetch failure for EAN: " + contractedEAN); + return; + } + if (announcements.isEmpty()) { + LOG.fine("No announcements found for EAN: " + contractedEAN); + updateLastPoll(); + clearAnnouncementAttributes(); + return; + } - // Record every newly-seen announcement so we keep an audit trail of what GOPACS returned. - for (AnnouncementDto announcement : announcements) { - if (recordedAnnouncementIds.add(announcement.getId())) { - recordAnnouncementHistory(announcement, null, null); - } - } + LOG.fine("Fetched " + announcements.size() + " announcements for EAN: " + contractedEAN); - // The API call already filters by type/state, but re-check defensively in case - // GOPACS ever returns extras so downstream logic can rely on the invariant. - List relevant = announcements.stream() - .filter(a -> ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT.equals(a.getType())) - .filter(a -> ANNOUNCEMENT_STATE_OPEN.equals(a.getAnnouncementState())) - .toList(); - - if (relevant.isEmpty()) { - LOG.fine("No open CONGESTIONMANAGEMENT announcements for EAN: " + contractedEAN); - updateLastPoll(); - clearAnnouncementAttributes(); - return; - } + // Record every newly-seen announcement so we keep an audit trail of what GOPACS returned. + for (AnnouncementDto announcement : announcements) { + if (recordedAnnouncementIds.add(announcement.getId())) { + recordAnnouncementHistory(announcement, null, null); + } + } - LOG.fine("Found " + relevant.size() + " relevant open announcements for EAN: " + contractedEAN); - - // Check EAN effectivity to find relevant ones - AnnouncementDto selected = null; - String effectivityCategory = null; - EanEffectivityResult selectedEffectivity = null; - - // Check effectivity for all, find ones where our EAN is listed - LOG.fine("Checking EAN effectivity for " + relevant.size() + " announcements"); - for (AnnouncementDto announcement : relevant) { - EanEffectivityResult result = checkEanEffectivity(announcement.getId()); - if (result != null) { - LOG.fine("EAN " + contractedEAN + " found in category '" + result.matchedCategory() + "' for announcement " + announcement.getId()); - // Prefer MANDATORY over VOLUNTARY - if (selected == null || COMPLIANCE_TYPE_MANDATORY.equals(announcement.getComplianceType())) { - selected = announcement; - effectivityCategory = result.matchedCategory(); - selectedEffectivity = result; - } - } - } + // The API call already filters by type/state, but re-check defensively in case + // GOPACS ever returns extras so downstream logic can rely on the invariant. + List relevant = + announcements.stream() + .filter(a -> ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT.equals(a.getType())) + .filter(a -> ANNOUNCEMENT_STATE_OPEN.equals(a.getAnnouncementState())) + .toList(); + + if (relevant.isEmpty()) { + LOG.fine("No open CONGESTIONMANAGEMENT announcements for EAN: " + contractedEAN); + updateLastPoll(); + clearAnnouncementAttributes(); + return; + } - if (selected == null) { - LOG.fine("No relevant announcement found for EAN: " + contractedEAN + " after effectivity check"); - updateLastPoll(); - clearAnnouncementAttributes(); - return; + LOG.fine("Found " + relevant.size() + " relevant open announcements for EAN: " + contractedEAN); + + // Check EAN effectivity to find relevant ones + AnnouncementDto selected = null; + String effectivityCategory = null; + EanEffectivityResult selectedEffectivity = null; + + // Check effectivity for all, find ones where our EAN is listed + LOG.fine("Checking EAN effectivity for " + relevant.size() + " announcements"); + for (AnnouncementDto announcement : relevant) { + EanEffectivityResult result = checkEanEffectivity(announcement.getId()); + if (result != null) { + LOG.fine( + "EAN " + + contractedEAN + + " found in category '" + + result.matchedCategory() + + "' for announcement " + + announcement.getId()); + // Prefer MANDATORY over VOLUNTARY + if (selected == null + || COMPLIANCE_TYPE_MANDATORY.equals(announcement.getComplianceType())) { + selected = announcement; + effectivityCategory = result.matchedCategory(); + selectedEffectivity = result; } + } + } - // Check if this is a new announcement - boolean isNew = !selected.getId().equals(lastProcessedAnnouncementId); - lastProcessedAnnouncementId = selected.getId(); - - if (isNew) { - LOG.info("New redispatch announcement for EAN " + contractedEAN + ": id=" + selected.getId() - + ", compliance=" + selected.getComplianceType() - + ", org=" + selected.getOrganisationName() - + ", effectivity=" + effectivityCategory); - } else { - LOG.fine("Announcement " + selected.getId() + " unchanged for EAN: " + contractedEAN); - } + if (selected == null) { + LOG.fine( + "No relevant announcement found for EAN: " + contractedEAN + " after effectivity check"); + updateLastPoll(); + clearAnnouncementAttributes(); + return; + } - // Update asset attributes with announcement info - updateAnnouncementAttributes(selected, effectivityCategory); + // Check if this is a new announcement + boolean isNew = !selected.getId().equals(lastProcessedAnnouncementId); + lastProcessedAnnouncementId = selected.getId(); + + if (isNew) { + LOG.info( + "New redispatch announcement for EAN " + + contractedEAN + + ": id=" + + selected.getId() + + ", compliance=" + + selected.getComplianceType() + + ", org=" + + selected.getOrganisationName() + + ", effectivity=" + + effectivityCategory); + } else { + LOG.fine("Announcement " + selected.getId() + " unchanged for EAN: " + contractedEAN); + } - // Record history for new announcements - if (isNew) { - recordAnnouncementHistory(selected, effectivityCategory, - selectedEffectivity != null ? selectedEffectivity.effectivities() : null); + // Update asset attributes with announcement info + updateAnnouncementAttributes(selected, effectivityCategory); - // Store remaining problem profile as predicted data points (MW → kW) - storeRequestedPowerProfile(selected); + // Record history for new announcements + if (isNew) { + recordAnnouncementHistory( + selected, + effectivityCategory, + selectedEffectivity != null ? selectedEffectivity.effectivities() : null); - // TODO: populate redispatchSuggestedPower / redispatchSuggestedVolume from - // the linked EnergyOptimisation flex profile + Pierre's bid pricing strategy. - // Tracked separately; attributes intentionally left empty in this PR. + // Store remaining problem profile as predicted data points (MW → kW) + storeRequestedPowerProfile(selected); - // Set status to PENDING_CONFIRMATION for new announcements - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_PENDING_CONFIRMATION); + // TODO: populate redispatchSuggestedPower / redispatchSuggestedVolume from + // the linked EnergyOptimisation flex profile + Pierre's bid pricing strategy. + // Tracked separately; attributes intentionally left empty in this PR. - // Reset confirmation flag - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), false); - } + // Set status to PENDING_CONFIRMATION for new announcements + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_PENDING_CONFIRMATION); - updateLastPoll(); - LOG.fine("Redispatch poll completed for EAN: " + contractedEAN); + // Reset confirmation flag + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), false); } - /** - * Returns the fetched announcements, an empty list if the API confirms there are - * none, or {@code null} if the fetch itself failed (HTTP error / exception). The - * caller uses the null sentinel to skip processing without wiping current state. - */ - protected List fetchAnnouncements() { - try (Response response = announcementResource.fetchAnnouncements( - null, - null, - null, - ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT, - ANNOUNCEMENT_STATE_OPEN - )) { - if (response.getStatus() == 200) { - String body = response.readEntity(String.class); - LOG.fine("GOPACS announcements found: " + body); - return objectMapper.readValue(body, new TypeReference<>() { - }); - } - LOG.warning("Failed to fetch announcements: HTTP " + response.getStatus()); - return null; - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error fetching announcements for EAN: " + contractedEAN, e); - return null; - } + updateLastPoll(); + LOG.fine("Redispatch poll completed for EAN: " + contractedEAN); + } + + /** + * Returns the fetched announcements, an empty list if the API confirms there are none, or {@code + * null} if the fetch itself failed (HTTP error / exception). The caller uses the null sentinel to + * skip processing without wiping current state. + */ + protected List fetchAnnouncements() { + try (Response response = + announcementResource.fetchAnnouncements( + null, null, null, ANNOUNCEMENT_TYPE_CONGESTIONMANAGEMENT, ANNOUNCEMENT_STATE_OPEN)) { + if (response.getStatus() == 200) { + String body = response.readEntity(String.class); + LOG.fine("GOPACS announcements found: " + body); + return objectMapper.readValue(body, new TypeReference<>() {}); + } + LOG.warning("Failed to fetch announcements: HTTP " + response.getStatus()); + return null; + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error fetching announcements for EAN: " + contractedEAN, e); + return null; } - - protected EanEffectivityResult checkEanEffectivity(String announcementId) { - LOG.fine("Checking EAN effectivity for announcement " + announcementId + " and EAN " + contractedEAN); - try (Response response = eanEffectivityResource.fetchEanSolvingEffectivity(announcementId, apiKey)) { - if (response.getStatus() == 200) { - String body = response.readEntity(String.class); - LOG.fine("Fetched EAN solving effectivity: " + body); - List effectivities = objectMapper.readValue(body, new TypeReference<>() { - }); - - for (EanSolvingEffectivityDto effectivity : effectivities) { - if (effectivity.getEansByCategory() != null) { - LOG.fine("Effectivity categories: " + effectivity.getEansByCategory().keySet()); - for (Map.Entry> entry : effectivity.getEansByCategory().entrySet()) { - if (entry.getValue() != null && entry.getValue().contains(contractedEAN)) { - LOG.fine("EAN " + contractedEAN + " found in category '" + entry.getKey() + "'"); - return new EanEffectivityResult(entry.getKey(), effectivities); - } - } - } - } - LOG.fine("EAN " + contractedEAN + " not found in any effectivity category"); - } else { - LOG.warning("Failed to fetch EAN effectivity for announcement " + announcementId + ": HTTP " + response.getStatus()); + } + + protected EanEffectivityResult checkEanEffectivity(String announcementId) { + LOG.fine( + "Checking EAN effectivity for announcement " + + announcementId + + " and EAN " + + contractedEAN); + try (Response response = + eanEffectivityResource.fetchEanSolvingEffectivity(announcementId, apiKey)) { + if (response.getStatus() == 200) { + String body = response.readEntity(String.class); + LOG.fine("Fetched EAN solving effectivity: " + body); + List effectivities = + objectMapper.readValue(body, new TypeReference<>() {}); + + for (EanSolvingEffectivityDto effectivity : effectivities) { + if (effectivity.getEansByCategory() != null) { + LOG.fine("Effectivity categories: " + effectivity.getEansByCategory().keySet()); + for (Map.Entry> entry : + effectivity.getEansByCategory().entrySet()) { + if (entry.getValue() != null && entry.getValue().contains(contractedEAN)) { + LOG.fine("EAN " + contractedEAN + " found in category '" + entry.getKey() + "'"); + return new EanEffectivityResult(entry.getKey(), effectivities); + } } - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error checking EAN effectivity for announcement " + announcementId, e); + } } - return null; + LOG.fine("EAN " + contractedEAN + " not found in any effectivity category"); + } else { + LOG.warning( + "Failed to fetch EAN effectivity for announcement " + + announcementId + + ": HTTP " + + response.getStatus()); + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error checking EAN effectivity for announcement " + announcementId, e); } - - protected void updateAnnouncementAttributes(AnnouncementDto announcement, String effectivityCategory) { - LOG.fine("Updating announcement attributes for announcement " + announcement.getId() + ", effectivity=" + effectivityCategory); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_ID.getName(), announcement.getId()); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_COMPLIANCE_TYPE.getName(), announcement.getComplianceType()); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_MESSAGE.getName(), announcement.getMessage()); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_BUY.getName(), announcement.getRequestAreaDescriptionBuyOrders()); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_SELL.getName(), announcement.getRequestAreaDescriptionSellOrders()); - - if (announcement.getProblemPeriod() != null) { - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), announcement.getProblemPeriod().getStartTime()); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), announcement.getProblemPeriod().getEndTime()); - } else { - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), null); - } - - if (announcement.getBidValidityPeriod() != null) { - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), announcement.getBidValidityPeriod().getEndTime()); - } else { - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), null); - } - - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_EAN_EFFECTIVITY.getName(), effectivityCategory); + return null; + } + + protected void updateAnnouncementAttributes( + AnnouncementDto announcement, String effectivityCategory) { + LOG.fine( + "Updating announcement attributes for announcement " + + announcement.getId() + + ", effectivity=" + + effectivityCategory); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_ID.getName(), announcement.getId()); + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_COMPLIANCE_TYPE.getName(), announcement.getComplianceType()); + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_MESSAGE.getName(), announcement.getMessage()); + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_BUY.getName(), + announcement.getRequestAreaDescriptionBuyOrders()); + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_SELL.getName(), + announcement.getRequestAreaDescriptionSellOrders()); + + if (announcement.getProblemPeriod() != null) { + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), + announcement.getProblemPeriod().getStartTime()); + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), + announcement.getProblemPeriod().getEndTime()); + } else { + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), null); } - protected void clearAnnouncementAttributes() { - // Check asset state to handle stale attributes after service restart (lastProcessedAnnouncementId is in-memory only) - if (lastProcessedAnnouncementId != null) { - LOG.fine("Clearing announcement attributes (previous: " + lastProcessedAnnouncementId + ") for EAN: " + contractedEAN); - } else { - EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); - if (gopacsAsset == null || gopacsAsset.getRedispatchAnnouncementId().isEmpty()) { - return; - } - LOG.fine("Clearing stale announcement attributes for EAN: " + contractedEAN); - } - - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_ID.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_COMPLIANCE_TYPE.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_MESSAGE.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_EAN_EFFECTIVITY.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_BUY.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_SELL.getName(), null); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_NONE); - lastProcessedAnnouncementId = null; + if (announcement.getBidValidityPeriod() != null) { + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), + announcement.getBidValidityPeriod().getEndTime()); + } else { + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), null); } - protected void storeRequestedPowerProfile(AnnouncementDto announcement) { - if (announcement.getRemainingProblemProfileInMW() == null || announcement.getProblemPeriod() == null) { - LOG.fine("No power profile or problem period in announcement " + announcement.getId()); - return; - } - - Long startTime = announcement.getProblemPeriod().getStartTime(); - if (startTime == null) { - return; - } - - List> datapoints = new ArrayList<>(); - long quarterMillis = 15 * 60 * 1000L; - - for (int i = 0; i < announcement.getRemainingProblemProfileInMW().size(); i++) { - double powerMW = announcement.getRemainingProblemProfileInMW().get(i); - double powerKW = powerMW * 1000.0; // Convert MW to kW - long timestamp = startTime + (long) i * quarterMillis; - datapoints.add(new ValueDatapoint<>(timestamp, powerKW)); - } - - LOG.fine("Storing " + datapoints.size() + " power profile data points for announcement " + announcement.getId() - + " (profile MW: " + announcement.getRemainingProblemProfileInMW() + ")"); - assetPredictedDatapointService.updateValues(assetId, EmsGOPACSAsset.REDISPATCH_REQUESTED_POWER.getName(), datapoints); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_EAN_EFFECTIVITY.getName(), effectivityCategory); + } + + protected void clearAnnouncementAttributes() { + // Check asset state to handle stale attributes after service restart + // (lastProcessedAnnouncementId is in-memory only) + if (lastProcessedAnnouncementId != null) { + LOG.fine( + "Clearing announcement attributes (previous: " + + lastProcessedAnnouncementId + + ") for EAN: " + + contractedEAN); + } else { + EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); + if (gopacsAsset == null || gopacsAsset.getRedispatchAnnouncementId().isEmpty()) { + return; + } + LOG.fine("Clearing stale announcement attributes for EAN: " + contractedEAN); } - protected void recordAnnouncementHistory(AnnouncementDto announcement, String effectivityCategory, - List effectivities) { - try { - ObjectNode historyEntry = objectMapper.createObjectNode(); - historyEntry.put("announcementId", announcement.getId()); - historyEntry.put("type", announcement.getType()); - historyEntry.put("complianceType", announcement.getComplianceType()); - historyEntry.put("organisationName", announcement.getOrganisationName()); - if (announcement.getProblemPeriod() != null) { - historyEntry.put("startTime", announcement.getProblemPeriod().getStartTime()); - historyEntry.put("endTime", announcement.getProblemPeriod().getEndTime()); - } - if (announcement.getRemainingProblemProfileInMW() != null) { - historyEntry.put("maxRequestedPowerMW", announcement.getRemainingProblemProfileInMW().stream() - .mapToDouble(Double::doubleValue).max().orElse(0)); - } - if (effectivityCategory != null) { - historyEntry.put("eanEffectivity", effectivityCategory); - } - if (effectivities != null && !effectivities.isEmpty()) { - historyEntry.set("eanEffectivityDetails", objectMapper.valueToTree(effectivities)); - } - historyEntry.put("receivedAt", timerService.getCurrentTimeMillis()); - - Map historyMap = objectMapper.convertValue(historyEntry, new TypeReference<>() { - }); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_HISTORY.getName(), historyMap); - } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to record announcement history", e); - } + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_ID.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_COMPLIANCE_TYPE.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_MESSAGE.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_START_TIME.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_END_TIME.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_VALIDITY_END.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_EAN_EFFECTIVITY.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_BUY.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_REQUEST_AREA_SELL.getName(), null); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_NONE); + lastProcessedAnnouncementId = null; + } + + protected void storeRequestedPowerProfile(AnnouncementDto announcement) { + if (announcement.getRemainingProblemProfileInMW() == null + || announcement.getProblemPeriod() == null) { + LOG.fine("No power profile or problem period in announcement " + announcement.getId()); + return; } - public void handleConfirmation() { - LOG.fine("Processing bid confirmation for EAN: " + contractedEAN); - EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); - if (gopacsAsset == null) { - LOG.warning("Cannot confirm bid: GOPACS asset not found: " + assetId); - return; - } - - String announcementId = gopacsAsset.getRedispatchAnnouncementId().orElse(null); - Double bidPrice = gopacsAsset.getRedispatchBidPrice().orElse(null); - - if (announcementId == null) { - LOG.warning("Cannot confirm bid: no active announcement for EAN " + contractedEAN); - resetConfirmFlag(); - return; - } - - if (bidPrice == null || bidPrice <= 0) { - LOG.warning("Cannot confirm bid: bid price not set or invalid for EAN " + contractedEAN); - resetConfirmFlag(); - return; - } - - LOG.info("Bid confirmed for EAN " + contractedEAN + ": announcement=" + announcementId + ", price=" + bidPrice + " EUR/MWh"); - - // Update status - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_CONFIRMED); - - // Record bid history - updateBidHistory(BID_STATUS_CONFIRMED, announcementId, bidPrice); + Long startTime = announcement.getProblemPeriod().getStartTime(); + if (startTime == null) { + return; + } - // Reset confirm flag - resetConfirmFlag(); + List> datapoints = new ArrayList<>(); + long quarterMillis = 15 * 60 * 1000L; - // Placeholder for trading platform integration - placeBidOnPlatform(announcementId, bidPrice); + for (int i = 0; i < announcement.getRemainingProblemProfileInMW().size(); i++) { + double powerMW = announcement.getRemainingProblemProfileInMW().get(i); + double powerKW = powerMW * 1000.0; // Convert MW to kW + long timestamp = startTime + (long) i * quarterMillis; + datapoints.add(new ValueDatapoint<>(timestamp, powerKW)); } - protected void placeBidOnPlatform(String announcementId, Double bidPrice) { - // TODO: Integrate with trading platform (ETPA, EPEX SPOT, or NordPool) when decided - LOG.info("Bid placement placeholder for EAN " + contractedEAN + ": announcement=" + announcementId + - ", price=" + bidPrice + " EUR/MWh. Trading platform integration pending."); + LOG.fine( + "Storing " + + datapoints.size() + + " power profile data points for announcement " + + announcement.getId() + + " (profile MW: " + + announcement.getRemainingProblemProfileInMW() + + ")"); + assetPredictedDatapointService.updateValues( + assetId, EmsGOPACSAsset.REDISPATCH_REQUESTED_POWER.getName(), datapoints); + } + + protected void recordAnnouncementHistory( + AnnouncementDto announcement, + String effectivityCategory, + List effectivities) { + try { + ObjectNode historyEntry = objectMapper.createObjectNode(); + historyEntry.put("announcementId", announcement.getId()); + historyEntry.put("type", announcement.getType()); + historyEntry.put("complianceType", announcement.getComplianceType()); + historyEntry.put("organisationName", announcement.getOrganisationName()); + if (announcement.getProblemPeriod() != null) { + historyEntry.put("startTime", announcement.getProblemPeriod().getStartTime()); + historyEntry.put("endTime", announcement.getProblemPeriod().getEndTime()); + } + if (announcement.getRemainingProblemProfileInMW() != null) { + historyEntry.put( + "maxRequestedPowerMW", + announcement.getRemainingProblemProfileInMW().stream() + .mapToDouble(Double::doubleValue) + .max() + .orElse(0)); + } + if (effectivityCategory != null) { + historyEntry.put("eanEffectivity", effectivityCategory); + } + if (effectivities != null && !effectivities.isEmpty()) { + historyEntry.set("eanEffectivityDetails", objectMapper.valueToTree(effectivities)); + } + historyEntry.put("receivedAt", timerService.getCurrentTimeMillis()); + + Map historyMap = + objectMapper.convertValue(historyEntry, new TypeReference<>() {}); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_ANNOUNCEMENT_HISTORY.getName(), historyMap); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to record announcement history", e); } - - protected void updateBidHistory(String status, String announcementId, Double bidPrice) { - try { - ObjectNode historyEntry = objectMapper.createObjectNode(); - historyEntry.put("announcementId", announcementId); - historyEntry.put("bidPrice", bidPrice); - historyEntry.put("status", status); - historyEntry.put("timestamp", timerService.getCurrentTimeMillis()); - - Map historyMap = objectMapper.convertValue(historyEntry, new TypeReference<>() { - }); - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_HISTORY.getName(), historyMap); - } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to record bid history", e); - } + } + + public void handleConfirmation() { + LOG.fine("Processing bid confirmation for EAN: " + contractedEAN); + EmsGOPACSAsset gopacsAsset = (EmsGOPACSAsset) assetStorageService.find(assetId); + if (gopacsAsset == null) { + LOG.warning("Cannot confirm bid: GOPACS asset not found: " + assetId); + return; } - private void resetConfirmFlag() { - scheduledExecutorService.schedule(() -> - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), false), 1, TimeUnit.SECONDS); + String announcementId = gopacsAsset.getRedispatchAnnouncementId().orElse(null); + Double bidPrice = gopacsAsset.getRedispatchBidPrice().orElse(null); + + if (announcementId == null) { + LOG.warning("Cannot confirm bid: no active announcement for EAN " + contractedEAN); + resetConfirmFlag(); + return; } - private void updateLastPoll() { - sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_LAST_POLL.getName(), timerService.getCurrentTimeMillis()); + if (bidPrice == null || bidPrice <= 0) { + LOG.warning("Cannot confirm bid: bid price not set or invalid for EAN " + contractedEAN); + resetConfirmFlag(); + return; } - private void sendAttributeEvent(String attributeName, Object value) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(assetId, attributeName, value), getClass().getSimpleName()); + LOG.info( + "Bid confirmed for EAN " + + contractedEAN + + ": announcement=" + + announcementId + + ", price=" + + bidPrice + + " EUR/MWh"); + + // Update status + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_STATUS.getName(), BID_STATUS_CONFIRMED); + + // Record bid history + updateBidHistory(BID_STATUS_CONFIRMED, announcementId, bidPrice); + + // Reset confirm flag + resetConfirmFlag(); + + // Placeholder for trading platform integration + placeBidOnPlatform(announcementId, bidPrice); + } + + protected void placeBidOnPlatform(String announcementId, Double bidPrice) { + // TODO: Integrate with trading platform (ETPA, EPEX SPOT, or NordPool) when decided + LOG.info( + "Bid placement placeholder for EAN " + + contractedEAN + + ": announcement=" + + announcementId + + ", price=" + + bidPrice + + " EUR/MWh. Trading platform integration pending."); + } + + protected void updateBidHistory(String status, String announcementId, Double bidPrice) { + try { + ObjectNode historyEntry = objectMapper.createObjectNode(); + historyEntry.put("announcementId", announcementId); + historyEntry.put("bidPrice", bidPrice); + historyEntry.put("status", status); + historyEntry.put("timestamp", timerService.getCurrentTimeMillis()); + + Map historyMap = + objectMapper.convertValue(historyEntry, new TypeReference<>() {}); + sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_BID_HISTORY.getName(), historyMap); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to record bid history", e); } + } + + private void resetConfirmFlag() { + scheduledExecutorService.schedule( + () -> sendAttributeEvent(EmsGOPACSAsset.REDISPATCH_CONFIRM_BID.getName(), false), + 1, + TimeUnit.SECONDS); + } + + private void updateLastPoll() { + sendAttributeEvent( + EmsGOPACSAsset.REDISPATCH_LAST_POLL.getName(), timerService.getCurrentTimeMillis()); + } + + private void sendAttributeEvent(String attributeName, Object value) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent(assetId, attributeName, value), getClass().getSimpleName()); + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResource.java index 3c591cd..bdb73d6 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResource.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,21 +12,23 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; +import static jakarta.ws.rs.core.MediaType.APPLICATION_XML; +import static jakarta.ws.rs.core.MediaType.TEXT_XML; + import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import static jakarta.ws.rs.core.MediaType.APPLICATION_XML; -import static jakarta.ws.rs.core.MediaType.TEXT_XML; - @Path("message") public interface GOPACSServerResource { - @POST - @Consumes({APPLICATION_XML, TEXT_XML}) - void inMessage(String transportXml); + @POST + @Consumes({APPLICATION_XML, TEXT_XML}) + void inMessage(String transportXml); } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResourceImpl.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResourceImpl.java index dcad0ae..8746a2c 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResourceImpl.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSServerResourceImpl.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; @@ -23,17 +22,16 @@ public class GOPACSServerResourceImpl implements GOPACSServerResource { - protected Consumer messageConsumer; + protected Consumer messageConsumer; + public GOPACSServerResourceImpl(Consumer messageConsumer) { + this.messageConsumer = messageConsumer; + } - public GOPACSServerResourceImpl(Consumer messageConsumer) { - this.messageConsumer = messageConsumer; - } - - @Override - public void inMessage(String transportXml) { - if (messageConsumer != null) { - messageConsumer.accept(transportXml); - } + @Override + public void inMessage(String transportXml) { + if (messageConsumer != null) { + messageConsumer.accept(transportXml); } + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/OAuth2TokenResponse.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/OAuth2TokenResponse.java index af0e812..f2b2c67 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/OAuth2TokenResponse.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/OAuth2TokenResponse.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,63 +12,62 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -/** - * OAuth2 token response model for client credentials flow - */ +/** OAuth2 token response model for client credentials flow */ @JsonIgnoreProperties(ignoreUnknown = true) public class OAuth2TokenResponse { - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("token_type") - private String tokenType; - - @JsonProperty("expires_in") - private Long expiresIn; - - @JsonProperty("scope") - private String scope; - - public OAuth2TokenResponse() { - } - - public String getAccessToken() { - return accessToken; - } - - public void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } - - public String getTokenType() { - return tokenType; - } - - public void setTokenType(String tokenType) { - this.tokenType = tokenType; - } - - public Long getExpiresIn() { - return expiresIn; - } - - public void setExpiresIn(Long expiresIn) { - this.expiresIn = expiresIn; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("expires_in") + private Long expiresIn; + + @JsonProperty("scope") + private String scope; + + public OAuth2TokenResponse() {} + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public Long getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Long expiresIn) { + this.expiresIn = expiresIn; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java index f956a3a..58f0981 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs; diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/AnnouncementDto.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/AnnouncementDto.java index 9de1873..25694d7 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/AnnouncementDto.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/AnnouncementDto.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,156 +12,155 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs.dto; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.util.List; -/** - * DTO for GOPACS congestion announcement entries from the /machineannouncements endpoint. - */ +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** DTO for GOPACS congestion announcement entries from the /machineannouncements endpoint. */ @JsonIgnoreProperties(ignoreUnknown = true) public class AnnouncementDto { - private String id; - private String message; - private String type; - private String complianceType; - private String announcementState; - private String organisationName; - private String problemAreaDescription; - private String requestAreaDescriptionBuyOrders; - private String requestAreaDescriptionSellOrders; - private Long createdTimestamp; - private Long lastUpdatedTimestamp; - private Long day; - private TimeSpanDto problemPeriod; - private TimeSpanDto bidValidityPeriod; - private List remainingProblemProfileInMW; - - public AnnouncementDto() { - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getComplianceType() { - return complianceType; - } - - public void setComplianceType(String complianceType) { - this.complianceType = complianceType; - } - - public String getAnnouncementState() { - return announcementState; - } - - public void setAnnouncementState(String announcementState) { - this.announcementState = announcementState; - } - - public String getOrganisationName() { - return organisationName; - } - - public void setOrganisationName(String organisationName) { - this.organisationName = organisationName; - } - - public String getProblemAreaDescription() { - return problemAreaDescription; - } - - public void setProblemAreaDescription(String problemAreaDescription) { - this.problemAreaDescription = problemAreaDescription; - } - - public String getRequestAreaDescriptionBuyOrders() { - return requestAreaDescriptionBuyOrders; - } - - public void setRequestAreaDescriptionBuyOrders(String requestAreaDescriptionBuyOrders) { - this.requestAreaDescriptionBuyOrders = requestAreaDescriptionBuyOrders; - } - - public String getRequestAreaDescriptionSellOrders() { - return requestAreaDescriptionSellOrders; - } - - public void setRequestAreaDescriptionSellOrders(String requestAreaDescriptionSellOrders) { - this.requestAreaDescriptionSellOrders = requestAreaDescriptionSellOrders; - } - - public Long getCreatedTimestamp() { - return createdTimestamp; - } - - public void setCreatedTimestamp(Long createdTimestamp) { - this.createdTimestamp = createdTimestamp; - } - - public Long getLastUpdatedTimestamp() { - return lastUpdatedTimestamp; - } - - public void setLastUpdatedTimestamp(Long lastUpdatedTimestamp) { - this.lastUpdatedTimestamp = lastUpdatedTimestamp; - } - - public Long getDay() { - return day; - } - - public void setDay(Long day) { - this.day = day; - } + private String id; + private String message; + private String type; + private String complianceType; + private String announcementState; + private String organisationName; + private String problemAreaDescription; + private String requestAreaDescriptionBuyOrders; + private String requestAreaDescriptionSellOrders; + private Long createdTimestamp; + private Long lastUpdatedTimestamp; + private Long day; + private TimeSpanDto problemPeriod; + private TimeSpanDto bidValidityPeriod; + private List remainingProblemProfileInMW; + + public AnnouncementDto() {} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getComplianceType() { + return complianceType; + } + + public void setComplianceType(String complianceType) { + this.complianceType = complianceType; + } + + public String getAnnouncementState() { + return announcementState; + } + + public void setAnnouncementState(String announcementState) { + this.announcementState = announcementState; + } + + public String getOrganisationName() { + return organisationName; + } + + public void setOrganisationName(String organisationName) { + this.organisationName = organisationName; + } + + public String getProblemAreaDescription() { + return problemAreaDescription; + } + + public void setProblemAreaDescription(String problemAreaDescription) { + this.problemAreaDescription = problemAreaDescription; + } + + public String getRequestAreaDescriptionBuyOrders() { + return requestAreaDescriptionBuyOrders; + } + + public void setRequestAreaDescriptionBuyOrders(String requestAreaDescriptionBuyOrders) { + this.requestAreaDescriptionBuyOrders = requestAreaDescriptionBuyOrders; + } + + public String getRequestAreaDescriptionSellOrders() { + return requestAreaDescriptionSellOrders; + } + + public void setRequestAreaDescriptionSellOrders(String requestAreaDescriptionSellOrders) { + this.requestAreaDescriptionSellOrders = requestAreaDescriptionSellOrders; + } + + public Long getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(Long createdTimestamp) { + this.createdTimestamp = createdTimestamp; + } + + public Long getLastUpdatedTimestamp() { + return lastUpdatedTimestamp; + } + + public void setLastUpdatedTimestamp(Long lastUpdatedTimestamp) { + this.lastUpdatedTimestamp = lastUpdatedTimestamp; + } + + public Long getDay() { + return day; + } + + public void setDay(Long day) { + this.day = day; + } - public TimeSpanDto getProblemPeriod() { - return problemPeriod; - } + public TimeSpanDto getProblemPeriod() { + return problemPeriod; + } - public void setProblemPeriod(TimeSpanDto problemPeriod) { - this.problemPeriod = problemPeriod; - } + public void setProblemPeriod(TimeSpanDto problemPeriod) { + this.problemPeriod = problemPeriod; + } - public TimeSpanDto getBidValidityPeriod() { - return bidValidityPeriod; - } + public TimeSpanDto getBidValidityPeriod() { + return bidValidityPeriod; + } - public void setBidValidityPeriod(TimeSpanDto bidValidityPeriod) { - this.bidValidityPeriod = bidValidityPeriod; - } + public void setBidValidityPeriod(TimeSpanDto bidValidityPeriod) { + this.bidValidityPeriod = bidValidityPeriod; + } - public List getRemainingProblemProfileInMW() { - return remainingProblemProfileInMW; - } + public List getRemainingProblemProfileInMW() { + return remainingProblemProfileInMW; + } - public void setRemainingProblemProfileInMW(List remainingProblemProfileInMW) { - this.remainingProblemProfileInMW = remainingProblemProfileInMW; - } + public void setRemainingProblemProfileInMW(List remainingProblemProfileInMW) { + this.remainingProblemProfileInMW = remainingProblemProfileInMW; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/EanSolvingEffectivityDto.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/EanSolvingEffectivityDto.java index ebc4479..1d3648c 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/EanSolvingEffectivityDto.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/EanSolvingEffectivityDto.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,44 +12,45 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs.dto; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.util.Map; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * DTO for the GOPACS EAN solving effectivity response from * /public-api/1.0/announcements/{id}/eansolvingeffectivity. - *

    - * The {@code eansByCategory} maps effectivity category names to sets of EANs - * that fall into that category for the given announcement. + * + *

    The {@code eansByCategory} maps effectivity category names to sets of EANs that fall into that + * category for the given announcement. */ @JsonIgnoreProperties(ignoreUnknown = true) public class EanSolvingEffectivityDto { - private TimeSpanDto during; - private Map> eansByCategory; + private TimeSpanDto during; + private Map> eansByCategory; - public EanSolvingEffectivityDto() { - } + public EanSolvingEffectivityDto() {} - public TimeSpanDto getDuring() { - return during; - } + public TimeSpanDto getDuring() { + return during; + } - public void setDuring(TimeSpanDto during) { - this.during = during; - } + public void setDuring(TimeSpanDto during) { + this.during = during; + } - public Map> getEansByCategory() { - return eansByCategory; - } + public Map> getEansByCategory() { + return eansByCategory; + } - public void setEansByCategory(Map> eansByCategory) { - this.eansByCategory = eansByCategory; - } + public void setEansByCategory(Map> eansByCategory) { + this.eansByCategory = eansByCategory; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/TimeSpanDto.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/TimeSpanDto.java index d20ae29..b3cfe2e 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/TimeSpanDto.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/dto/TimeSpanDto.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,56 +12,57 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.gopacs.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; /** - * DTO for GOPACS time span objects used in announcements and effectivity responses. - * All time values are epoch milliseconds. + * DTO for GOPACS time span objects used in announcements and effectivity responses. All time values + * are epoch milliseconds. */ @JsonIgnoreProperties(ignoreUnknown = true) public class TimeSpanDto { - private Long startTime; - private Long endTime; - private Double durationInHours; - private Integer numberOfQuartersInTimeSpan; + private Long startTime; + private Long endTime; + private Double durationInHours; + private Integer numberOfQuartersInTimeSpan; - public TimeSpanDto() { - } + public TimeSpanDto() {} - public Long getStartTime() { - return startTime; - } + public Long getStartTime() { + return startTime; + } - public void setStartTime(Long startTime) { - this.startTime = startTime; - } + public void setStartTime(Long startTime) { + this.startTime = startTime; + } - public Long getEndTime() { - return endTime; - } + public Long getEndTime() { + return endTime; + } - public void setEndTime(Long endTime) { - this.endTime = endTime; - } + public void setEndTime(Long endTime) { + this.endTime = endTime; + } - public Double getDurationInHours() { - return durationInHours; - } + public Double getDurationInHours() { + return durationInHours; + } - public void setDurationInHours(Double durationInHours) { - this.durationInHours = durationInHours; - } + public void setDurationInHours(Double durationInHours) { + this.durationInHours = durationInHours; + } - public Integer getNumberOfQuartersInTimeSpan() { - return numberOfQuartersInTimeSpan; - } + public Integer getNumberOfQuartersInTimeSpan() { + return numberOfQuartersInTimeSpan; + } - public void setNumberOfQuartersInTimeSpan(Integer numberOfQuartersInTimeSpan) { - this.numberOfQuartersInTimeSpan = numberOfQuartersInTimeSpan; - } + public void setNumberOfQuartersInTimeSpan(Integer numberOfQuartersInTimeSpan) { + this.numberOfQuartersInTimeSpan = numberOfQuartersInTimeSpan; + } } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java index ce98b6d..f7c51a7 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,23 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.optimisationMethods; +import static org.openremote.model.syslog.SyslogCategory.DATA; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import org.openremote.extension.ems.agent.EmsDayAheadAsset; import org.openremote.extension.ems.agent.EmsElectricityBatteryAsset; import org.openremote.extension.ems.agent.EmsEnergyOptimisationAsset; @@ -31,1632 +41,2246 @@ import org.openremote.model.query.AssetQuery; import org.openremote.model.syslog.SyslogCategory; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.openremote.model.syslog.SyslogCategory.DATA; - public class EmsOptimisation implements OptimisationMethod { - protected static final Logger LOG = SyslogCategory.getLogger(DATA, EmsOptimisation.class.getName()); - private final String optimisationMethodName = EmsOptimisation.class.getSimpleName(); - - // Maximum interval between data-points send by the device to be considered connected - private final int ACTIVE_PERIOD_MINUTES = 5; - - // EMS power limits settings - private final int POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE = 5; - private final int POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE = 5; - private final int POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; - private final int POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; - - // Battery settings - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL = 3; - private final int BATTERY_EFFICIENCY_BUFFER_PERCENTAGE = 3; - + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, EmsOptimisation.class.getName()); + private final String optimisationMethodName = EmsOptimisation.class.getSimpleName(); + + // Maximum interval between data-points send by the device to be considered connected + private final int ACTIVE_PERIOD_MINUTES = 5; + + // EMS power limits settings + private final int POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE = 5; + private final int POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE = 5; + private final int POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; + private final int POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; + + // Battery settings + private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; + private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL = 3; + private final int BATTERY_EFFICIENCY_BUFFER_PERCENTAGE = 3; + + @Override + public void execute(String energyOptimisationAssetId, Services services) { + runOptimisationMethodEms(energyOptimisationAssetId, services); + } + + private void runOptimisationMethodEms(String energyOptimisationAssetId, Services services) { + // Get energy optimisation asset + EmsEnergyOptimisationAsset energyOptimisationAsset = + (EmsEnergyOptimisationAsset) + services.getAssetStorageService().find(energyOptimisationAssetId); + + if (energyOptimisationAsset == null) { + return; + } - @Override - public void execute(String energyOptimisationAssetId, Services services) { - runOptimisationMethodEms(energyOptimisationAssetId, services); + String logPrefixEnergyOptimisation = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAssetId, + energyOptimisationAsset.getAssetName()); + + // Get all battery assets + List electricityBatteryAssets = + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .parents(energyOptimisationAssetId) + .types(EmsElectricityBatteryAsset.class)) + .stream() + .map(asset -> (EmsElectricityBatteryAsset) asset) + .toList(); + + // TODO: Add order to battery assets from longest to smallest discharge duration + + if (energyOptimisationAsset.getEnableDetailedLogging().orElse(false)) { + int allowChargingSize = + electricityBatteryAssets.stream() + .filter( + electricityBatteryAsset -> + electricityBatteryAsset.getAllowCharging().orElse(false)) + .toList() + .size(); + + int allowDischargingSize = + electricityBatteryAssets.stream() + .filter( + electricityBatteryAsset -> + electricityBatteryAsset.getAllowDischarging().orElse(false)) + .toList() + .size(); + + LOG.info( + String.format( + "%s; Energy optimisation asset has %s battery asset(s). Number of batteries with '%s'= %s and '%s'= %s", + logPrefixEnergyOptimisation, + electricityBatteryAssets.size(), + EmsElectricityBatteryAsset.ALLOW_CHARGING.getName(), + allowChargingSize, + EmsElectricityBatteryAsset.ALLOW_DISCHARGING.getName(), + allowDischargingSize)); } - private void runOptimisationMethodEms(String energyOptimisationAssetId, Services services) { - // Get energy optimisation asset - EmsEnergyOptimisationAsset energyOptimisationAsset = (EmsEnergyOptimisationAsset) services.getAssetStorageService().find(energyOptimisationAssetId); + // Check if power net is connected + Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); - if (energyOptimisationAsset == null) { - return; - } + if (powerNet == null) { + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. '%s' attribute is not connected", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_NET.getName())); + } - String logPrefixEnergyOptimisation = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAssetId, energyOptimisationAsset.getAssetName()); + // Validate power limits + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); + Long powerLimitMaximumProfileTimestampMillis = + energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); - // Get all battery assets - List electricityBatteryAssets = services.getAssetStorageService() - .findAll(new AssetQuery().parents(energyOptimisationAssetId).types(EmsElectricityBatteryAsset.class)) - .stream() - .map(asset -> (EmsElectricityBatteryAsset) asset) - .toList(); + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); - // TODO: Add order to battery assets from longest to smallest discharge duration + String powerLimitMaximumProfileDateTime = ""; - if (energyOptimisationAsset.getEnableDetailedLogging().orElse(false)) { - int allowChargingSize = electricityBatteryAssets - .stream() - .filter(electricityBatteryAsset -> electricityBatteryAsset.getAllowCharging().orElse(false)) - .toList() - .size(); + if (powerLimitMaximumProfileTimestampMillis != null) { + powerLimitMaximumProfileDateTime = + formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); + } - int allowDischargingSize = electricityBatteryAssets - .stream() - .filter(electricityBatteryAsset -> electricityBatteryAsset.getAllowDischarging().orElse(false)) - .toList() - .size(); + if (powerLimitMaximumProfileTotal != null + && powerLimitMinimumProfileTotal != null + && powerLimitMaximumProfileTotal <= powerLimitMinimumProfileTotal) { + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. The '%s'= %s kW is lower than or equal to '%s'= %s kW for timestamp='%s'", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + powerLimitMaximumProfileTotal, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + powerLimitMinimumProfileTotal, + powerLimitMaximumProfileDateTime)); + } - LOG.info(String.format("%s; Energy optimisation asset has %s battery asset(s). Number of batteries with '%s'= %s and '%s'= %s", - logPrefixEnergyOptimisation, electricityBatteryAssets.size(), EmsElectricityBatteryAsset.ALLOW_CHARGING.getName(), allowChargingSize, EmsElectricityBatteryAsset.ALLOW_DISCHARGING.getName(), allowDischargingSize)); - } + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); - // Check if power net is connected - Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); + // Update day ahead asset at specific time of day + if (dayAheadAsset != null) { + updateDayAheadAsset(energyOptimisationAsset, dayAheadAsset, services); + } - if (powerNet == null) { - LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. '%s' attribute is not connected", logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_NET.getName())); - } + // Check for battery assets + if (electricityBatteryAssets.isEmpty()) { + return; + } - // Validate power limits - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - Long powerLimitMaximumProfileTimestampMillis = energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); + // Check connection status of battery assets + batteryCheckConnection(electricityBatteryAssets, services); + + // Check if all required attributes are connected/set and create log messages + batteryCheckSetup(electricityBatteryAssets); + + // Calculate new battery power set-points + Map powerSetpointsNew = + batteryCalculatePowerSetpoints( + energyOptimisationAsset, + electricityBatteryAssets, + services, + logPrefixEnergyOptimisation); + + // Update battery power set-points + batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); + } + + private int batteryCalculateEnergyLevelPercentageDefault( + Integer energyLevelPercentageMaximumBattery, + Integer energyLevelPercentageMinimumBattery, + EmsEnergyOptimisationAsset energyOptimisationAsset) { + if (energyLevelPercentageMaximumBattery == null + || energyLevelPercentageMinimumBattery == null) { + return BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT; + } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); + + int energyLevelPercentageDefault; + + if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { + energyLevelPercentageDefault = energyLevelPercentageMinimumBattery; + } else if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { + energyLevelPercentageDefault = energyLevelPercentageMaximumBattery; + } else { + energyLevelPercentageDefault = + (int) + Math.round( + (double) + (energyLevelPercentageMaximumBattery + - energyLevelPercentageMinimumBattery) + / 2 + + energyLevelPercentageMinimumBattery); + } - String powerLimitMaximumProfileDateTime = ""; + return energyLevelPercentageDefault; + } + + private Map batteryCalculateForecasts( + List electricityBatteryAssets, + EmsEnergyOptimisationAsset energyOptimisationAsset, + Services services) { + Map batteryEnergyLevelPercentageTargets = new HashMap<>(); + + // Get power consumption forecast for 1 week + long intervalMillis = 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis; + long endTimeMillis = startTimeMillis + 7 * 24 * 60 * 60000; + + String energyOptimisationAssetId = energyOptimisationAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> energyOptimisationPowerConsumptionPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName(), + assetDatapointQueryConsumptionPredicted); + + // System.out.println("energyOptimisationPowerConsumptionPredicted = " + + // energyOptimisationPowerConsumptionPredicted); + + if (energyOptimisationPowerConsumptionPredicted.isEmpty()) { + return batteryEnergyLevelPercentageTargets; + } - if (powerLimitMaximumProfileTimestampMillis != null) { - powerLimitMaximumProfileDateTime = formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); - } + // Calculate power average for each 15-minute interval + List> totalPowerConsumptionAveraged = + intervalAverage(energyOptimisationPowerConsumptionPredicted, intervalMillis); + totalPowerConsumptionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal != null && powerLimitMaximumProfileTotal <= powerLimitMinimumProfileTotal) { - LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. The '%s'= %s kW is lower than or equal to '%s'= %s kW for timestamp='%s'", - logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal, - EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal, powerLimitMaximumProfileDateTime)); - } + // System.out.println("totalPowerConsumptionAveraged = " + + // totalPowerConsumptionAveraged); - // Get day ahead asset - EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + long startTimestampMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); + long endTimestampMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); - // Update day ahead asset at specific time of day - if (dayAheadAsset != null) { - updateDayAheadAsset(energyOptimisationAsset, dayAheadAsset, services); - } + // Interpolate power average values for each 15-minute interval + List> totalPowerConsumptionInterpolated = + intervalInterpolate( + totalPowerConsumptionAveraged, + startTimestampMillis, + endTimestampMillis, + intervalMillis); - // Check for battery assets - if (electricityBatteryAssets.isEmpty()) { - return; - } + List timestampsMillisList = new ArrayList<>(); + List totalPowerConsumptionList = new ArrayList<>(); - // Check connection status of battery assets - batteryCheckConnection(electricityBatteryAssets, services); + for (ValueDatapoint datapoint : totalPowerConsumptionInterpolated) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); - // Check if all required attributes are connected/set and create log messages - batteryCheckSetup(electricityBatteryAssets); + timestampsMillisList.add(timestampMillis); + totalPowerConsumptionList.add(value); + } - // Calculate new battery power set-points - Map powerSetpointsNew = batteryCalculatePowerSetpoints(energyOptimisationAsset, electricityBatteryAssets, services, logPrefixEnergyOptimisation); + // System.out.println("timestampsMillisList = " + timestampsMillisList); + // System.out.println("totalPowerConsumptionList = " + totalPowerConsumptionList); - // Update battery power set-points - batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); + if (totalPowerConsumptionList.isEmpty()) { + return batteryEnergyLevelPercentageTargets; } - private int batteryCalculateEnergyLevelPercentageDefault(Integer energyLevelPercentageMaximumBattery, Integer energyLevelPercentageMinimumBattery, EmsEnergyOptimisationAsset energyOptimisationAsset) { - if (energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { - return BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT; - } - - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); + AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = + new AssetDatapointAllQuery(startTimestampMillis, endTimestampMillis); + List> energyOptimisationPowerProductionPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName(), + assetDatapointQueryPeriodPredicted); + + // System.out.println("energyOptimisationPowerProductionPredicted = " + + // energyOptimisationPowerProductionPredicted); + + Map totalPowerProductionMap = new HashMap<>(); + + if (!energyOptimisationPowerProductionPredicted.isEmpty()) { + // Calculate power average for each 15-minute interval + List> totalPowerProductionAveraged = + intervalAverage(energyOptimisationPowerProductionPredicted, intervalMillis); + totalPowerProductionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + + // Interpolate power average values for each 15-minute interval + List> totalPowerProductionInterpolated = + intervalInterpolate( + totalPowerProductionAveraged, + totalPowerProductionAveraged.getFirst().getTimestamp(), + totalPowerProductionAveraged.getLast().getTimestamp(), + intervalMillis); + + totalPowerProductionMap = + totalPowerProductionInterpolated.stream() + .collect( + Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); + } - int energyLevelPercentageDefault; + // List to store the sum of total power consumption, production and flexible power + List totalPowerConsumptionProductionFlexList = new ArrayList<>(); - if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { - energyLevelPercentageDefault = energyLevelPercentageMinimumBattery; - } else if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { - energyLevelPercentageDefault = energyLevelPercentageMaximumBattery; - } else { - energyLevelPercentageDefault = (int) Math.round((double) (energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) / 2 + energyLevelPercentageMinimumBattery); - } + // Sum total power consumption and production + for (int i = 0; i < timestampsMillisList.size(); i++) { + Double powerConsumption = totalPowerConsumptionList.get(i); + Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); - return energyLevelPercentageDefault; + if (powerProduction != null) { + double sum = powerConsumption + powerProduction; + totalPowerConsumptionProductionFlexList.add(sum); + } else { + totalPowerConsumptionProductionFlexList.add(powerConsumption); + } } - private Map batteryCalculateForecasts(List electricityBatteryAssets, EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - Map batteryEnergyLevelPercentageTargets = new HashMap<>(); - - // Get power consumption forecast for 1 week - long intervalMillis = 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long startTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis; - long endTimeMillis = startTimeMillis + 7 * 24 * 60 * 60000; - - String energyOptimisationAssetId = energyOptimisationAsset.getId(); - AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> energyOptimisationPowerConsumptionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName(), assetDatapointQueryConsumptionPredicted); + // System.out.println("totalPowerConsumptionProductionFlexList = " + + // totalPowerConsumptionProductionFlexList); + // System.out.println(); + + // Get power limits forecasts + List> energyOptimisationPowerLimitMaximumPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + assetDatapointQueryPeriodPredicted); + List> energyOptimisationPowerLimitMinimumPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + assetDatapointQueryPeriodPredicted); + + Map powerLimitMaximumMap = new HashMap<>(); + Map powerLimitMinimumMap = new HashMap<>(); + + for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { + powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + } -// System.out.println("energyOptimisationPowerConsumptionPredicted = " + energyOptimisationPowerConsumptionPredicted); + for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { + powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + } - if (energyOptimisationPowerConsumptionPredicted.isEmpty()) { - return batteryEnergyLevelPercentageTargets; + List chargePowerAvailableTotalList = new ArrayList<>(); + List dischargePowerAvailableTotalList = new ArrayList<>(); + + // Add current available charge and discharge power + chargePowerAvailableTotalList.add(0.0); + dischargePowerAvailableTotalList.add(0.0); + + double intervalHour = (double) intervalMillis / (60 * 60000); + + // Calculate forecast for each battery + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String batteryAssetId = electricityBatteryAsset.getId(); + Integer chargeEfficiencyBattery = electricityBatteryAsset.getChargeEfficiency().orElse(null); + Double chargePowerMaximumBattery = + electricityBatteryAsset.getChargePowerMaximum().orElse(null); + Integer dischargeEfficiencyBattery = + electricityBatteryAsset.getDischargeEfficiency().orElse(null); + Double dischargePowerMaximumBattery = + electricityBatteryAsset.getDischargePowerMaximum().orElse(null); + Double energyCapacityBattery = electricityBatteryAsset.getEnergyCapacity().orElse(null); + Double energyLevelPercentageBattery = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximumBattery = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimumBattery = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + + if (chargeEfficiencyBattery == null + || chargePowerMaximumBattery == null + || dischargeEfficiencyBattery == null + || dischargePowerMaximumBattery == null + || energyCapacityBattery == null + || energyLevelPercentageBattery == null + || energyLevelPercentageMaximumBattery == null + || energyLevelPercentageMinimumBattery == null) { + continue; + } + + // Check if the energy level percentage maximum and minimum are valid + if ((energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) + < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { + continue; + } + + // Calculate total available charge and discharge power + for (int i = 0; i < timestampsMillisList.size(); i++) { + long timestampMillis = timestampsMillisList.get(i); + double totalPower = totalPowerConsumptionProductionFlexList.get(i); + + Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); + Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); + + if (powerLimitMaximum != null) { + double powerLimitMaximumVirtual = + powerLimitMaximum * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; + chargePowerAvailableTotalList.add(chargePowerTotalAvailable); + } else { + chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); } - // Calculate power average for each 15-minute interval - List> totalPowerConsumptionAveraged = intervalAverage(energyOptimisationPowerConsumptionPredicted, intervalMillis); - totalPowerConsumptionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - -// System.out.println("totalPowerConsumptionAveraged = " + totalPowerConsumptionAveraged); - - long startTimestampMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); - long endTimestampMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); - - // Interpolate power average values for each 15-minute interval - List> totalPowerConsumptionInterpolated = intervalInterpolate(totalPowerConsumptionAveraged, startTimestampMillis, endTimestampMillis, intervalMillis); - - List timestampsMillisList = new ArrayList<>(); - List totalPowerConsumptionList = new ArrayList<>(); - - for (ValueDatapoint datapoint : totalPowerConsumptionInterpolated) { - long timestampMillis = datapoint.getTimestamp(); - Double value = (Double) datapoint.getValue(); - - timestampsMillisList.add(timestampMillis); - totalPowerConsumptionList.add(value); + if (powerLimitMinimum != null) { + double powerLimitMinimumVirtual = + powerLimitMinimum * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; + dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); + } else { + dischargePowerAvailableTotalList.add(Double.NEGATIVE_INFINITY); } - -// System.out.println("timestampsMillisList = " + timestampsMillisList); -// System.out.println("totalPowerConsumptionList = " + totalPowerConsumptionList); - - if (totalPowerConsumptionList.isEmpty()) { - return batteryEnergyLevelPercentageTargets; + } + + int numberOfDataPoints = chargePowerAvailableTotalList.size(); + + // System.out.println("numberOfDataPoints = " + numberOfDataPoints); + // System.out.println("chargePowerAvailableTotalList = " + + // chargePowerAvailableTotalList); + // System.out.println("dischargePowerAvailableTotalList = " + + // dischargePowerAvailableTotalList); + + List chargeNeededTotalList = + dischargePowerAvailableTotalList.stream().map(x -> x > 0 ? x : 0).toList(); + List dischargeNeededTotalList = + chargePowerAvailableTotalList.stream().map(x -> x < 0 ? x : 0).toList(); + + // System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); + // System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); + + List chargePowerAvailableBatteryList = + chargePowerAvailableTotalList.stream() + .map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0) + .toList(); + List dischargePowerAvailableBatteryList = + dischargePowerAvailableTotalList.stream() + .map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0) + .toList(); + + // System.out.println("chargeAvailableBatteryList = " + + // chargePowerAvailableBatteryList); + // System.out.println("dischargeAvailableBatteryList = " + + // dischargePowerAvailableBatteryList); + // System.out.println(); + + // Convert power to energy level percentage + List chargePercentageNeededTotalList = new ArrayList<>(); + List dischargePercentageNeededTotalList = new ArrayList<>(); + + chargeEfficiencyBattery = chargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; + dischargeEfficiencyBattery = + dischargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; + + for (int i = 0; i < numberOfDataPoints; i++) { + double c = + intervalHour + * chargeNeededTotalList.get(i) + * chargeEfficiencyBattery + / energyCapacityBattery; + double d = + 10000 + * intervalHour + * dischargeNeededTotalList.get(i) + / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageNeededTotalList.add(c); + dischargePercentageNeededTotalList.add(d); + } + + // System.out.println("chargePercentageNeededTotalList = " + + // chargePercentageNeededTotalList); + // System.out.println("dischargePercentageNeededTotalList = " + + // dischargePercentageNeededTotalList); + + List chargePercentageAvailableBatteryList = new ArrayList<>(); + List dischargePercentageAvailableBatteryList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + double c = + intervalHour + * chargePowerAvailableBatteryList.get(i) + * chargeEfficiencyBattery + / energyCapacityBattery; + double d = + 10000 + * intervalHour + * dischargePowerAvailableBatteryList.get(i) + / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageAvailableBatteryList.add(c); + dischargePercentageAvailableBatteryList.add(d); + } + + // System.out.println("chargePercentageAvailableBatteryList = " + + // chargePercentageAvailableBatteryList); + // System.out.println("dischargePercentageAvailableBatteryList = " + + // dischargePercentageAvailableBatteryList); + // System.out.println(); + + // Calculate forecast when current energy level percentage is outside limits + int getToLimitIndex = 0; + double getToLimitPercentage = energyLevelPercentageBattery; + List getToLimitPercentageList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + getToLimitIndex = i; + + if (getToLimitPercentage > energyLevelPercentageMaximumBattery) { + double dischargePercentageNeeded = + energyLevelPercentageMaximumBattery - getToLimitPercentage; + double dischargePercentageAvailable = + Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; + dischargePercentageAvailableBatteryList.set( + i, dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable); + getToLimitPercentageList.add(getToLimitPercentage); + } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { + double chargePercentageNeeded = + energyLevelPercentageMinimumBattery - getToLimitPercentage; + double chargePercentageAvailable = + Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; + chargePercentageAvailableBatteryList.set( + i, chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable); + getToLimitPercentageList.add(getToLimitPercentage); + } else { + break; } - - AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = new AssetDatapointAllQuery(startTimestampMillis, endTimestampMillis); - List> energyOptimisationPowerProductionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName(), assetDatapointQueryPeriodPredicted); - -// System.out.println("energyOptimisationPowerProductionPredicted = " + energyOptimisationPowerProductionPredicted); - - Map totalPowerProductionMap = new HashMap<>(); - - if (!energyOptimisationPowerProductionPredicted.isEmpty()) { - // Calculate power average for each 15-minute interval - List> totalPowerProductionAveraged = intervalAverage(energyOptimisationPowerProductionPredicted, intervalMillis); - totalPowerProductionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - - // Interpolate power average values for each 15-minute interval - List> totalPowerProductionInterpolated = intervalInterpolate(totalPowerProductionAveraged, totalPowerProductionAveraged.getFirst().getTimestamp(), totalPowerProductionAveraged.getLast().getTimestamp(), intervalMillis); - - totalPowerProductionMap = totalPowerProductionInterpolated.stream().collect(Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); + } + + // System.out.println("getToLimitIndex = " + getToLimitIndex); + // System.out.println("getToLimitPercentage = " + getToLimitPercentage); + // System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); + // System.out.println("chargePercentageAvailableBatteryList = " + + // chargePercentageAvailableBatteryList); + // System.out.println("dischargePercentageAvailableBatteryList = " + + // dischargePercentageAvailableBatteryList); + + List energyLevelPredictionList = new ArrayList<>(); + + if (getToLimitPercentage > energyLevelPercentageMaximumBattery + || getToLimitPercentage < energyLevelPercentageMinimumBattery) { + // Energy level percentage forecast when whole forecast is outside of battery percentage + // limits + energyLevelPredictionList = getToLimitPercentageList; + } else { + // Calculate energy level percentage forecast starting from value within battery percentage + // limits + List chargePercentageWantBatteryList = new ArrayList<>(); + List dischargePercentageWantBatteryList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + chargePercentageWantBatteryList.add( + Math.min( + chargePercentageNeededTotalList.get(i), + chargePercentageAvailableBatteryList.get(i))); + dischargePercentageWantBatteryList.add( + Math.max( + dischargePercentageNeededTotalList.get(i), + dischargePercentageAvailableBatteryList.get(i))); } - // List to store the sum of total power consumption, production and flexible power - List totalPowerConsumptionProductionFlexList = new ArrayList<>(); + // System.out.println("chargePercentageWantBatteryList = " + + // chargePercentageWantBatteryList); + // System.out.println("dischargePercentageWantBatteryList = " + + // dischargePercentageWantBatteryList); - // Sum total power consumption and production - for (int i = 0; i < timestampsMillisList.size(); i++) { - Double powerConsumption = totalPowerConsumptionList.get(i); - Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); + List chargeAndDischargePercentageWantBatteryList = new ArrayList<>(); - if (powerProduction != null) { - double sum = powerConsumption + powerProduction; - totalPowerConsumptionProductionFlexList.add(sum); - } else { - totalPowerConsumptionProductionFlexList.add(powerConsumption); - } + for (int i = 0; i < numberOfDataPoints; i++) { + chargeAndDischargePercentageWantBatteryList.add( + chargePercentageWantBatteryList.get(i) + dischargePercentageWantBatteryList.get(i)); } -// System.out.println("totalPowerConsumptionProductionFlexList = " + totalPowerConsumptionProductionFlexList); -// System.out.println(); - - // Get power limits forecasts - List> energyOptimisationPowerLimitMaximumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); - List> energyOptimisationPowerLimitMinimumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); - - Map powerLimitMaximumMap = new HashMap<>(); - Map powerLimitMinimumMap = new HashMap<>(); + // System.out.println("chargeAndDischargePercentageWantBatteryList = " + + // chargeAndDischargePercentageWantBatteryList); + // System.out.println(); - for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { - powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); - } + // Calculate forecast without battery percentage limits + int startRunningSumIndex = getToLimitIndex > 0 ? getToLimitIndex - 1 : 0; + List runningSumList = new ArrayList<>(); + double sum = getToLimitPercentage; - for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { - powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { + sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); + runningSumList.add(sum); } - List chargePowerAvailableTotalList = new ArrayList<>(); - List dischargePowerAvailableTotalList = new ArrayList<>(); - - // Add current available charge and discharge power - chargePowerAvailableTotalList.add(0.0); - dischargePowerAvailableTotalList.add(0.0); - - double intervalHour = (double) intervalMillis / (60 * 60000); - - // Calculate forecast for each battery - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String batteryAssetId = electricityBatteryAsset.getId(); - Integer chargeEfficiencyBattery = electricityBatteryAsset.getChargeEfficiency().orElse(null); - Double chargePowerMaximumBattery = electricityBatteryAsset.getChargePowerMaximum().orElse(null); - Integer dischargeEfficiencyBattery = electricityBatteryAsset.getDischargeEfficiency().orElse(null); - Double dischargePowerMaximumBattery = electricityBatteryAsset.getDischargePowerMaximum().orElse(null); - Double energyCapacityBattery = electricityBatteryAsset.getEnergyCapacity().orElse(null); - Double energyLevelPercentageBattery = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Integer energyLevelPercentageMaximumBattery = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimumBattery = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - - if (chargeEfficiencyBattery == null || chargePowerMaximumBattery == null || dischargeEfficiencyBattery == null || dischargePowerMaximumBattery == null || energyCapacityBattery == null || - energyLevelPercentageBattery == null || energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { - continue; - } - - // Check if the energy level percentage maximum and minimum are valid - if ((energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - continue; - } - - // Calculate total available charge and discharge power - for (int i = 0; i < timestampsMillisList.size(); i++) { - long timestampMillis = timestampsMillisList.get(i); - double totalPower = totalPowerConsumptionProductionFlexList.get(i); - - Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); - Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); - - if (powerLimitMaximum != null) { - double powerLimitMaximumVirtual = powerLimitMaximum * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); - double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; - chargePowerAvailableTotalList.add(chargePowerTotalAvailable); - } else { - chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); - } - - if (powerLimitMinimum != null) { - double powerLimitMinimumVirtual = powerLimitMinimum * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); - double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; - dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); - } else { - dischargePowerAvailableTotalList.add(Double.NEGATIVE_INFINITY); - } - } - - int numberOfDataPoints = chargePowerAvailableTotalList.size(); - -// System.out.println("numberOfDataPoints = " + numberOfDataPoints); -// System.out.println("chargePowerAvailableTotalList = " + chargePowerAvailableTotalList); -// System.out.println("dischargePowerAvailableTotalList = " + dischargePowerAvailableTotalList); - - List chargeNeededTotalList = dischargePowerAvailableTotalList.stream().map(x -> x > 0 ? x : 0).toList(); - List dischargeNeededTotalList = chargePowerAvailableTotalList.stream().map(x -> x < 0 ? x : 0).toList(); + // System.out.println("getToLimitPercentageList = " + + // getToLimitPercentageList); + // System.out.println("runningSumList = " + runningSumList); + // System.out.println(); -// System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); -// System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); - - List chargePowerAvailableBatteryList = chargePowerAvailableTotalList.stream().map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0).toList(); - List dischargePowerAvailableBatteryList = dischargePowerAvailableTotalList.stream().map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0).toList(); - -// System.out.println("chargeAvailableBatteryList = " + chargePowerAvailableBatteryList); -// System.out.println("dischargeAvailableBatteryList = " + dischargePowerAvailableBatteryList); -// System.out.println(); - - // Convert power to energy level percentage - List chargePercentageNeededTotalList = new ArrayList<>(); - List dischargePercentageNeededTotalList = new ArrayList<>(); - - chargeEfficiencyBattery = chargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; - dischargeEfficiencyBattery = dischargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; - - for (int i = 0; i < numberOfDataPoints; i++) { - double c = intervalHour * chargeNeededTotalList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; - double d = 10000 * intervalHour * dischargeNeededTotalList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageNeededTotalList.add(c); - dischargePercentageNeededTotalList.add(d); - } - -// System.out.println("chargePercentageNeededTotalList = " + chargePercentageNeededTotalList); -// System.out.println("dischargePercentageNeededTotalList = " + dischargePercentageNeededTotalList); - - List chargePercentageAvailableBatteryList = new ArrayList<>(); - List dischargePercentageAvailableBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - double c = intervalHour * chargePowerAvailableBatteryList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; - double d = 10000 * intervalHour * dischargePowerAvailableBatteryList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageAvailableBatteryList.add(c); - dischargePercentageAvailableBatteryList.add(d); - } - -// System.out.println("chargePercentageAvailableBatteryList = " + chargePercentageAvailableBatteryList); -// System.out.println("dischargePercentageAvailableBatteryList = " + dischargePercentageAvailableBatteryList); -// System.out.println(); - - // Calculate forecast when current energy level percentage is outside limits - int getToLimitIndex = 0; - double getToLimitPercentage = energyLevelPercentageBattery; - List getToLimitPercentageList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - getToLimitIndex = i; - - if (getToLimitPercentage > energyLevelPercentageMaximumBattery) { - double dischargePercentageNeeded = energyLevelPercentageMaximumBattery - getToLimitPercentage; - double dischargePercentageAvailable = Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); - getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; - dischargePercentageAvailableBatteryList.set(i, dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable); - getToLimitPercentageList.add(getToLimitPercentage); - } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { - double chargePercentageNeeded = energyLevelPercentageMinimumBattery - getToLimitPercentage; - double chargePercentageAvailable = Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); - getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; - chargePercentageAvailableBatteryList.set(i, chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable); - getToLimitPercentageList.add(getToLimitPercentage); - } else { - break; - } - } - -// System.out.println("getToLimitIndex = " + getToLimitIndex); -// System.out.println("getToLimitPercentage = " + getToLimitPercentage); -// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); -// System.out.println("chargePercentageAvailableBatteryList = " + chargePercentageAvailableBatteryList); -// System.out.println("dischargePercentageAvailableBatteryList = " + dischargePercentageAvailableBatteryList); - - List energyLevelPredictionList = new ArrayList<>(); - - if (getToLimitPercentage > energyLevelPercentageMaximumBattery || getToLimitPercentage < energyLevelPercentageMinimumBattery) { - // Energy level percentage forecast when whole forecast is outside of battery percentage limits - energyLevelPredictionList = getToLimitPercentageList; - } else { - // Calculate energy level percentage forecast starting from value within battery percentage limits - List chargePercentageWantBatteryList = new ArrayList<>(); - List dischargePercentageWantBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - chargePercentageWantBatteryList.add(Math.min(chargePercentageNeededTotalList.get(i), chargePercentageAvailableBatteryList.get(i))); - dischargePercentageWantBatteryList.add(Math.max(dischargePercentageNeededTotalList.get(i), dischargePercentageAvailableBatteryList.get(i))); - } - -// System.out.println("chargePercentageWantBatteryList = " + chargePercentageWantBatteryList); -// System.out.println("dischargePercentageWantBatteryList = " + dischargePercentageWantBatteryList); - - List chargeAndDischargePercentageWantBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - chargeAndDischargePercentageWantBatteryList.add(chargePercentageWantBatteryList.get(i) + dischargePercentageWantBatteryList.get(i)); - } - -// System.out.println("chargeAndDischargePercentageWantBatteryList = " + chargeAndDischargePercentageWantBatteryList); -// System.out.println(); - - // Calculate forecast without battery percentage limits - int startRunningSumIndex = getToLimitIndex > 0 ? getToLimitIndex - 1 : 0; - List runningSumList = new ArrayList<>(); - double sum = getToLimitPercentage; - - for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { - sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); - runningSumList.add(sum); - } - -// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); -// System.out.println("runningSumList = " + runningSumList); -// System.out.println(); - - // Combine outside limits and running sum energy level percentages - if (getToLimitPercentageList.size() > 1) { - energyLevelPredictionList.addAll(getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); + // Combine outside limits and running sum energy level percentages + if (getToLimitPercentageList.size() > 1) { + energyLevelPredictionList.addAll( + getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); + } + energyLevelPredictionList.addAll(runningSumList); + + // System.out.println("energyLevelPredictionList = " + + // energyLevelPredictionList); + // System.out.println(); + + // Calculate forecast starting within battery percentage limits + double energyLevelPredictionMaximum = + Collections.max( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionList.size())); + double energyLevelPredictionMinimum = + Collections.min( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionList.size())); + + long dtStart = services.getTimerService().getCurrentTimeMillis(); + long dt = 0; + long timeoutMillis = 10000; + + int intervalEndIndex = startRunningSumIndex; + int predictionListIndex = startRunningSumIndex; + + while (energyLevelPredictionMaximum > energyLevelPercentageMaximumBattery + || energyLevelPredictionMinimum < energyLevelPercentageMinimumBattery) { + String chargeOrDischarge = ""; + int intervalStartIndex = startRunningSumIndex; + + for (int i = intervalEndIndex; i < energyLevelPredictionList.size(); i++) { + predictionListIndex = i; + + if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { + intervalEndIndex = i; + + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) >= energyLevelPercentageMaximumBattery) { + intervalStartIndex = j; + break; } - energyLevelPredictionList.addAll(runningSumList); - -// System.out.println("energyLevelPredictionList = " + energyLevelPredictionList); -// System.out.println(); - - // Calculate forecast starting within battery percentage limits - double energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); - double energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); - - long dtStart = services.getTimerService().getCurrentTimeMillis(); - long dt = 0; - long timeoutMillis = 10000; - - int intervalEndIndex = startRunningSumIndex; - int predictionListIndex = startRunningSumIndex; - - while (energyLevelPredictionMaximum > energyLevelPercentageMaximumBattery || energyLevelPredictionMinimum < energyLevelPercentageMinimumBattery) { - String chargeOrDischarge = ""; - int intervalStartIndex = startRunningSumIndex; - - for (int i = intervalEndIndex; i < energyLevelPredictionList.size(); i++) { - predictionListIndex = i; - - if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { - intervalEndIndex = i; - - for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { - if (energyLevelPredictionList.get(j) >= energyLevelPercentageMaximumBattery) { - intervalStartIndex = j; - break; - } - } - - chargeOrDischarge = "charge"; - -// System.out.println("CHARGE"); -// System.out.println("intervalStartIndex = " + intervalStartIndex); -// System.out.println("intervalEndIndex = " + intervalEndIndex); - - break; - } else if (energyLevelPredictionList.get(i) > energyLevelPercentageMaximumBattery) { - intervalEndIndex = i; - - for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { - if (energyLevelPredictionList.get(j) <= energyLevelPercentageMinimumBattery) { - intervalStartIndex = j; - break; - } - } - - chargeOrDischarge = "discharge"; - -// System.out.println("DISCHARGE"); -// System.out.println("intervalStartIndex = " + intervalStartIndex); -// System.out.println("intervalEndIndex = " + intervalEndIndex); - - break; - } - } + } - int changeIndex = -1; - double energyLevelPredictionValueBefore = energyLevelPredictionList.get(intervalEndIndex); - - if (chargeOrDischarge.equals("charge")) { - double chargeAvailable = 0; - double chargeAvailableInterval = energyLevelPercentageMaximumBattery - Collections.max(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); - - if (chargeAvailableInterval > 0) { - for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { - if (i - 1 < 0) { - break; - } - - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; - } - - if (chargeAvailableLeft > 0) { - changeIndex = i; - chargeAvailable = Math.min(chargeAvailableInterval, chargeAvailableLeft); - break; - } - } - } - - double chargeNeeded = energyLevelPercentageMinimumBattery - energyLevelPredictionList.get(intervalEndIndex); - double changeValue = Math.min(chargeAvailable, chargeNeeded); - - if (changeIndex == -1) { - changeIndex = intervalEndIndex; - changeValue = chargeNeeded; - } - - for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { - energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); - } - } else if (chargeOrDischarge.equals("discharge")) { - double dischargeAvailable = 0; - double dischargeAvailableInterval = energyLevelPercentageMinimumBattery - Collections.min(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); - - if (dischargeAvailableInterval > 0) { - for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { - if (i - 1 < 0) { - break; - } - - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; - } - if (dischargeAvailableLeft < 0) { - changeIndex = i; - dischargeAvailable = Math.max(dischargeAvailableInterval, dischargeAvailableLeft); - break; - } - } - } - - double dischargeNeeded = energyLevelPercentageMaximumBattery - energyLevelPredictionList.get(intervalEndIndex); - double changeValue = Math.max(dischargeAvailable, dischargeNeeded); - - if (changeIndex == -1) { - changeIndex = intervalEndIndex; - changeValue = dischargeNeeded; - } - - for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { - energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); - } - } - - if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { - LOG.warning(String.format("Battery energy level percentage calculation error at index = %s", predictionListIndex)); -// System.out.println("ERROR: calculation error at index = " + predictionListIndex); - break; - } else if (dt > timeoutMillis) { - LOG.warning(String.format("Battery energy level percentage calculation timed out at index = %s", predictionListIndex)); -// System.out.println("ERROR: calculation timeout at index = " + predictionListIndex); - break; - } + chargeOrDischarge = "charge"; - energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); - energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); - dt = services.getTimerService().getCurrentTimeMillis() - dtStart; - } - -// System.out.println("After limits: energyLevelPredictionList = " + energyLevelPredictionList + "\n"); + // System.out.println("CHARGE"); + // System.out.println("intervalStartIndex = " + + // intervalStartIndex); + // System.out.println("intervalEndIndex = " + + // intervalEndIndex); - // Get day ahead asset - EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); - boolean useDayAheadTariffs = false; + break; + } else if (energyLevelPredictionList.get(i) > energyLevelPercentageMaximumBattery) { + intervalEndIndex = i; - if (dayAheadAsset != null) { - useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) <= energyLevelPercentageMinimumBattery) { + intervalStartIndex = j; + break; } + } - // Get the tariff forecasts from energy optimisation asset for 1 week - List> tariffExportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), services); - List> tariffImportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), services); - - // Overwrite the current tariff forecasts with the day ahead tariff forecasts - if (useDayAheadTariffs) { - List> tariffExportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); - List> tariffImportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); - - // Combine tariff export forecasts - if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); - - for (ValueDatapoint dp : tariffExportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - tariffExportDatapoints = new ArrayList<>(mergedMap.values()); - tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - } - - // Combine tariff import forecasts - if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); + chargeOrDischarge = "discharge"; - for (ValueDatapoint dp : tariffImportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } + // System.out.println("DISCHARGE"); + // System.out.println("intervalStartIndex = " + + // intervalStartIndex); + // System.out.println("intervalEndIndex = " + + // intervalEndIndex); - for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - tariffImportDatapoints = new ArrayList<>(mergedMap.values()); - tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - } + break; + } + } + + int changeIndex = -1; + double energyLevelPredictionValueBefore = energyLevelPredictionList.get(intervalEndIndex); + + if (chargeOrDischarge.equals("charge")) { + double chargeAvailable = 0; + double chargeAvailableInterval = + energyLevelPercentageMaximumBattery + - Collections.max( + energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + + if (chargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; } - // Calculate battery energy level percentage default - double energyLevelPercentageDefault = batteryCalculateEnergyLevelPercentageDefault(energyLevelPercentageMaximumBattery, energyLevelPercentageMinimumBattery, energyOptimisationAsset); - - // TODO: make window dynamic based on time needed to reach energyLevelPercentageDefault - // Calculate optimal charge and discharge zone for each day based on tariffs - Map chargeAndDischargeZonesMap = calculateTariffChargeAndDischargeZones(tariffImportDatapoints, tariffExportDatapoints, 4); - - // Charge zone = 1, discharge zone = -1 - List chargeAndDischargeZonesList = new ArrayList<>(Collections.nCopies(energyLevelPredictionList.size(), 0)); - - for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { - long timestampMillis = entry.getKey(); - int value = entry.getValue(); + double chargeAvailableLeft = 0; - int timestampIndex = timestampsMillisList.indexOf(timestampMillis); - - if (timestampIndex != -1) { - // Add 1 to align list indices - chargeAndDischargeZonesList.set(timestampIndex + 1, value); - } + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; } -// System.out.println("chargeAndDischargeZonesList = " + chargeAndDischargeZonesList); - - // Default energy level percentage for each interval - List energyLevelPercentageDefaultList = new ArrayList<>(Collections.nCopies(chargeAndDischargeZonesList.size(), energyLevelPercentageDefault)); - - for (int i = 0; i < chargeAndDischargeZonesList.size(); i++) { - if (chargeAndDischargeZonesList.get(i) == 1) { - energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMaximumBattery)); - } else if (chargeAndDischargeZonesList.get(i) == -1) { - energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMinimumBattery)); - } - } - -// System.out.println("energyLevelPercentageDefaultList = " + energyLevelPercentageDefaultList); - - // TODO: handle debounce interval in forecast - // Optimise forecast based on optimal tariffs - for (int i = 1; i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionList.size(); i++) { - // Only adjust forecast if predicted energy level percentage is outside battery debounce interval - if (Math.abs(energyLevelPredictionList.get(i) - energyLevelPercentageDefaultList.get(i)) <= BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - continue; - } - - if (chargeAndDischargeZonesList.get(i) == 1 && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { - Double energyLevelIntervalMaximum = Collections.max(energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); - - double chargeSpaceLeft = energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum; - - if (chargeSpaceLeft > 0) { - double chargeNeeded = energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; - } - - if (chargeAvailableLeft > 0) { - chargeAvailableLeft = Math.min(Math.min(chargeSpaceLeft, chargeAvailableLeft), chargeNeeded); - - for (int j = i; j < energyLevelPredictionList.size(); j++) { - energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + chargeAvailableLeft); - } - } - } else { - double chargeZoneEnergyLevel = energyLevelPredictionList.get(i); - - int energyLevelIntervalMaximumIndex = energyLevelPredictionList.size(); - - if (energyLevelIntervalMaximum >= energyLevelPercentageMaximumBattery) { - energyLevelIntervalMaximumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); - } - - for (int k = i; k <= energyLevelIntervalMaximumIndex; k++) { - double chargeNeeded = energyLevelPredictionList.get(k) - chargeZoneEnergyLevel; - - if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; - } - - if (chargeAvailableLeft > 0) { - double energyLevelIntervalMaximum2 = Collections.max(energyLevelPredictionList.subList(i, k - 1)); - double chargeSpaceInterval = energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2; - - if (chargeSpaceInterval > 0) { - chargeAvailableLeft = Math.min(Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); - - for (int j = i; j < k; j++) { - energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + chargeAvailableLeft); - } - } - } - - break; - } - } - } - } else if (chargeAndDischargeZonesList.get(i) == -1 && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { - Double energyLevelIntervalMinimum = Collections.min(energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); - - double dischargeSpaceLeft = energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum; - - if (dischargeSpaceLeft < 0) { - double dischargeNeeded = energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; - } - - if (dischargeAvailableLeft < 0) { - dischargeAvailableLeft = Math.max(Math.max(dischargeSpaceLeft, dischargeAvailableLeft), dischargeNeeded); - - for (int j = i; j < energyLevelPredictionList.size(); j++) { - energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); - } - } - } else { - double dischargeZoneEnergyLevel = energyLevelPredictionList.get(i); - - int energyLevelIntervalMinimumIndex = energyLevelPredictionList.size(); - - if (energyLevelIntervalMinimum <= energyLevelPercentageMinimumBattery) { - energyLevelIntervalMinimumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); - } - - for (int k = i; k <= energyLevelIntervalMinimumIndex; k++) { - double dischargeNeeded = energyLevelPredictionList.get(k) - dischargeZoneEnergyLevel; - - if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; - } - - if (dischargeAvailableLeft < 0) { - double energyLevelIntervalMinimum2 = Collections.min(energyLevelPredictionList.subList(i, k - 1)); - double dischargeSpaceInterval = energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2; - - if (dischargeSpaceInterval < 0) { - dischargeAvailableLeft = Math.max(Math.max(dischargeAvailableLeft, dischargeNeeded), dischargeSpaceInterval); - - for (int j = i; j < k; j++) { - energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); - } - } - } - - break; - } - } - } - } + if (chargeAvailableLeft > 0) { + changeIndex = i; + chargeAvailable = Math.min(chargeAvailableInterval, chargeAvailableLeft); + break; } + } } -// System.out.println("After Tariffs: energyLevelPredictionList = " + energyLevelPredictionList + "\n"); + double chargeNeeded = + energyLevelPercentageMinimumBattery + - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.min(chargeAvailable, chargeNeeded); - // Get the current energy level percentage target - int batteryEnergyLevelPercentageTarget = (int) Math.round(energyLevelPredictionList.get(1)); - batteryEnergyLevelPercentageTargets.put(batteryAssetId, batteryEnergyLevelPercentageTarget); - - // Calculate energy level percentage change per interval - List percentageChangeBatteryList = new ArrayList<>(); - - for (int i = 0; i < energyLevelPredictionList.size() - 1; i++) { - double percentageChange = energyLevelPredictionList.get(i + 1) - energyLevelPredictionList.get(i); - percentageChangeBatteryList.add(percentageChange); + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = chargeNeeded; } -// System.out.println("percentageChangeBatteryList = " + percentageChangeBatteryList); - - // Calculate total power change for the EMS - List powerChangeTotalList = new ArrayList<>(); - - for (double percentageChange : percentageChangeBatteryList) { - double power = 0; - - if (percentageChange > 0) { - power = percentageChange * energyCapacityBattery / (intervalHour * chargeEfficiencyBattery); - } else if (percentageChange < 0) { - power = percentageChange * energyCapacityBattery * dischargeEfficiencyBattery / (10000 * intervalHour); - } - - powerChangeTotalList.add(round(power, 3)); + for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { + energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); } + } else if (chargeOrDischarge.equals("discharge")) { + double dischargeAvailable = 0; + double dischargeAvailableInterval = + energyLevelPercentageMinimumBattery + - Collections.min( + energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + + if (dischargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; + } -// System.out.println("powerChangeTotalList = " + powerChangeTotalList); + double dischargeAvailableLeft = 0; - for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { - totalPowerConsumptionProductionFlexList.set(i, totalPowerConsumptionProductionFlexList.get(i) - powerChangeTotalList.get(i)); + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + if (dischargeAvailableLeft < 0) { + changeIndex = i; + dischargeAvailable = Math.max(dischargeAvailableInterval, dischargeAvailableLeft); + break; + } + } } -// System.out.println("totalPowerConsumptionProductionFlexList = " + totalPowerConsumptionProductionFlexList); - - List> energyLevelPercentageForecast = new ArrayList<>(); - List> powerSetpointForecast = new ArrayList<>(); + double dischargeNeeded = + energyLevelPercentageMaximumBattery + - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.max(dischargeAvailable, dischargeNeeded); - // Update energy level percentage forecast starting after current time - for (int i = 1; i < timestampsMillisList.size(); i++) { - energyLevelPercentageForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = dischargeNeeded; } - // Update power set-point starting from current power limit - for (int i = 0; i < timestampsMillisList.size(); i++) { - powerSetpointForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); + for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { + energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); } - - services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), energyLevelPercentageForecast); - services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), powerSetpointForecast); - -// System.out.println("UPDATED forecasts"); + } + + if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { + LOG.warning( + String.format( + "Battery energy level percentage calculation error at index = %s", + predictionListIndex)); + // System.out.println("ERROR: calculation error at index = " + + // predictionListIndex); + break; + } else if (dt > timeoutMillis) { + LOG.warning( + String.format( + "Battery energy level percentage calculation timed out at index = %s", + predictionListIndex)); + // System.out.println("ERROR: calculation timeout at index = " + + // predictionListIndex); + break; + } + + energyLevelPredictionMaximum = + Collections.max( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionList.size())); + energyLevelPredictionMinimum = + Collections.min( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionList.size())); + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; } - return batteryEnergyLevelPercentageTargets; - } - - private Map batteryCalculatePowerFlexibleAvailable(List electricityBatteryAssets) { - HashMap powerFlexibleAvailable = new HashMap<>(); + // System.out.println("After limits: energyLevelPredictionList = " + + // energyLevelPredictionList + "\n"); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); - boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + boolean useDayAheadTariffs = false; - double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); - double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + if (dayAheadAsset != null) { + useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); + } - if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { - chargePowerMaximum = 0.0; - } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { - chargePowerMaximum = 0.0; + // Get the tariff forecasts from energy optimisation asset for 1 week + List> tariffExportDatapoints = + getTariffDatapoints( + energyOptimisationAsset, + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + services); + List> tariffImportDatapoints = + getTariffDatapoints( + energyOptimisationAsset, + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), + services); + + // Overwrite the current tariff forecasts with the day ahead tariff forecasts + if (useDayAheadTariffs) { + List> tariffExportDayAheadAssetDatapoints = + getTariffDayAheadDatapoints( + dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); + List> tariffImportDayAheadAssetDatapoints = + getTariffDayAheadDatapoints( + dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); + + // Combine tariff export forecasts + if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); + + for (ValueDatapoint dp : tariffExportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - if (!allowDischarging || energyLevelPercentageMinimum == null || energyLevelPercentage == null) { - dischargePowerMaximum = 0.0; - } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { - dischargePowerMaximum = 0.0; + for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - powerFlexibleAvailable.put(electricityBatteryAsset.getId(), new double[]{chargePowerMaximum, dischargePowerMaximum}); - } - - return powerFlexibleAvailable; - } - - private Map batteryCalculatePowerSetpoints(EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, Services services, String logPrefix) { - Map powerFlexibleAvailable = batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); - Map powerSetpointsNew; + tariffExportDatapoints = new ArrayList<>(mergedMap.values()); + tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + } - // Get latest power set-point across all batteries - long powerSetpointTimestampMillisLatest = 0L; + // Combine tariff import forecasts + if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - long powerSetpointTimestampMillis = electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); - - if (powerSetpointTimestampMillis > powerSetpointTimestampMillisLatest) { - powerSetpointTimestampMillisLatest = powerSetpointTimestampMillis; + for (ValueDatapoint dp : tariffImportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - } - - // Check if power net is updated since last battery power set-points update - Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); - long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); - // Turn off batteries that do not have flexible power available during main power meter disconnect - if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { - powerSetpointsNew = batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); - return powerSetpointsNew; - } - - // Check if power limits are present and calculate additional power limits - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - - Double powerLimitMaximumBatteries = null; - - if (powerLimitMaximumProfileTotal != null) { - powerLimitMaximumBatteries = round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), 3); - } - - Double powerLimitMinimumBatteries = null; - - if (powerLimitMinimumProfileTotal != null) { - powerLimitMinimumBatteries = round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), 3); - } - - // TODO: add battery limits cross-over warning - // TODO: add advanced settings - - int currentMinute = LocalDateTime.now().getMinute(); - Map batteryEnergyLevelPercentageTargets = batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); + for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } - // Calculate battery energy level forecast - // TODO: add 15 minute skip protection with forecast update timestamp for robust forecast updating - if ((currentMinute % 15) == 0) { - batteryEnergyLevelPercentageTargets = batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); + tariffImportDatapoints = new ArrayList<>(mergedMap.values()); + tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + } } - // Calculate virtual power consumption - Map powerSetpointsCurrent = new HashMap<>(); - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - powerSetpointsCurrent.put(electricityBatteryAssetId, powerSetpoint); + // Calculate battery energy level percentage default + double energyLevelPercentageDefault = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximumBattery, + energyLevelPercentageMinimumBattery, + energyOptimisationAsset); + + // TODO: make window dynamic based on time needed to reach energyLevelPercentageDefault + // Calculate optimal charge and discharge zone for each day based on tariffs + Map chargeAndDischargeZonesMap = + calculateTariffChargeAndDischargeZones( + tariffImportDatapoints, tariffExportDatapoints, 4); + + // Charge zone = 1, discharge zone = -1 + List chargeAndDischargeZonesList = + new ArrayList<>(Collections.nCopies(energyLevelPredictionList.size(), 0)); + + for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { + long timestampMillis = entry.getKey(); + int value = entry.getValue(); + + int timestampIndex = timestampsMillisList.indexOf(timestampMillis); + + if (timestampIndex != -1) { + // Add 1 to align list indices + chargeAndDischargeZonesList.set(timestampIndex + 1, value); + } } - double powerSetpointsCurrentSum = powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; - - // Calculate new power set-points - if (powerLimitMaximumProfileTotal != null && powerConsumptionVirtual > powerLimitMaximumProfileTotal) { - Double powerLimitMaximumVirtual = round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), 3); - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMaximumVirtual, "max"); - } else if (powerLimitMinimumProfileTotal != null && powerConsumptionVirtual < powerLimitMinimumProfileTotal) { - Double powerLimitMinimumVirtual = round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), 3); - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMinimumVirtual, "min"); - } else { - powerSetpointsNew = batteryRestoreEnergyLevels(energyOptimisationAsset, electricityBatteryAssets, powerFlexibleAvailable, powerNet, powerSetpointsCurrentSum, powerLimitMinimumBatteries, powerLimitMaximumBatteries, batteryEnergyLevelPercentageTargets); + // System.out.println("chargeAndDischargeZonesList = " + + // chargeAndDischargeZonesList); + + // Default energy level percentage for each interval + List energyLevelPercentageDefaultList = + new ArrayList<>( + Collections.nCopies( + chargeAndDischargeZonesList.size(), energyLevelPercentageDefault)); + + for (int i = 0; i < chargeAndDischargeZonesList.size(); i++) { + if (chargeAndDischargeZonesList.get(i) == 1) { + energyLevelPercentageDefaultList.set( + i, Double.valueOf(energyLevelPercentageMaximumBattery)); + } else if (chargeAndDischargeZonesList.get(i) == -1) { + energyLevelPercentageDefaultList.set( + i, Double.valueOf(energyLevelPercentageMinimumBattery)); + } } - // Create log messages on power limit breach - double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; - - if (powerLimitMaximumProfileTotal != null && powerNet > powerLimitMaximumProfileTotal) { - double powerExceededAmount = round((powerNet - powerLimitMaximumProfileTotal), 3); - LOG.warning(String.format("%s; Power net is %s kW above power limit maximum", logPrefix, powerExceededAmount)); - - if (powerNetNewVirtual > powerLimitMaximumProfileTotal) { - double powerReductionShortage = round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); - LOG.warning(String.format("%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", logPrefix, powerReductionShortage)); - } - } else if (powerLimitMinimumProfileTotal != null && powerNet < powerLimitMinimumProfileTotal) { - double powerExceededAmount = round((powerNet - powerLimitMinimumProfileTotal), 3); - LOG.warning(String.format("%s; Power net is %s kW below power limit minimum", logPrefix, powerExceededAmount)); + // System.out.println("energyLevelPercentageDefaultList = " + + // energyLevelPercentageDefaultList); + + // TODO: handle debounce interval in forecast + // Optimise forecast based on optimal tariffs + for (int i = 1; + i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionList.size(); + i++) { + // Only adjust forecast if predicted energy level percentage is outside battery debounce + // interval + if (Math.abs(energyLevelPredictionList.get(i) - energyLevelPercentageDefaultList.get(i)) + <= BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { + continue; + } + + if (chargeAndDischargeZonesList.get(i) == 1 + && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { + Double energyLevelIntervalMaximum = + Collections.max( + energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); + + double chargeSpaceLeft = + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum; + + if (chargeSpaceLeft > 0) { + double chargeNeeded = + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + } + + if (chargeAvailableLeft > 0) { + chargeAvailableLeft = + Math.min(Math.min(chargeSpaceLeft, chargeAvailableLeft), chargeNeeded); + + for (int j = i; j < energyLevelPredictionList.size(); j++) { + energyLevelPredictionList.set( + j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + } + } + } else { + double chargeZoneEnergyLevel = energyLevelPredictionList.get(i); + + int energyLevelIntervalMaximumIndex = energyLevelPredictionList.size(); + + if (energyLevelIntervalMaximum >= energyLevelPercentageMaximumBattery) { + energyLevelIntervalMaximumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); + } + + for (int k = i; k <= energyLevelIntervalMaximumIndex; k++) { + double chargeNeeded = energyLevelPredictionList.get(k) - chargeZoneEnergyLevel; + + if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + } + + if (chargeAvailableLeft > 0) { + double energyLevelIntervalMaximum2 = + Collections.max(energyLevelPredictionList.subList(i, k - 1)); + double chargeSpaceInterval = + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2; + + if (chargeSpaceInterval > 0) { + chargeAvailableLeft = + Math.min( + Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set( + j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + } + } + } - if (powerNetNewVirtual < powerLimitMinimumProfileTotal) { - double powerReductionShortage = round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); - LOG.warning(String.format("%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", logPrefix, powerReductionShortage)); + break; + } + } } - } - - return powerSetpointsNew; - } - - private Map batteryCalculatePowerSetpointsOnLimitBreach(Map powerFlexibleAvailable, Double powerConsumptionVirtual, Double powerLimitVirtual, String maxOrMin) { - Map powerSetpointsNew = new HashMap<>(); - boolean isMin = "min".equals(maxOrMin); - - // Calculate the total power adjustment needed - double powerReduction = powerConsumptionVirtual - powerLimitVirtual; - - if (isMin) { - powerReduction = -powerReduction; - } - - // Iterate over each battery asset - for (Map.Entry entry : powerFlexibleAvailable.entrySet()) { - String electricityBatteryAssetId = entry.getKey(); - double[] powerBatteryAvailableChargeDischarge = entry.getValue(); - - // Choose available power based on breach type - double powerBatteryAvailable = isMin ? -powerBatteryAvailableChargeDischarge[0] : powerBatteryAvailableChargeDischarge[1]; - - if (powerReduction > 0) { - // Limit the power set-point to available battery power or remaining reduction needed - double setpoint = round(Math.max(-powerReduction, powerBatteryAvailable), 3); - - if (isMin) { - setpoint = -setpoint; + } else if (chargeAndDischargeZonesList.get(i) == -1 + && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { + Double energyLevelIntervalMinimum = + Collections.min( + energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); + + double dischargeSpaceLeft = + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum; + + if (dischargeSpaceLeft < 0) { + double dischargeNeeded = + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + + if (dischargeAvailableLeft < 0) { + dischargeAvailableLeft = + Math.max(Math.max(dischargeSpaceLeft, dischargeAvailableLeft), dischargeNeeded); + + for (int j = i; j < energyLevelPredictionList.size(); j++) { + energyLevelPredictionList.set( + j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); } - - powerSetpointsNew.put(electricityBatteryAssetId, setpoint); - powerReduction += setpoint; + } } else { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + double dischargeZoneEnergyLevel = energyLevelPredictionList.get(i); + + int energyLevelIntervalMinimumIndex = energyLevelPredictionList.size(); + + if (energyLevelIntervalMinimum <= energyLevelPercentageMinimumBattery) { + energyLevelIntervalMinimumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); + } + + for (int k = i; k <= energyLevelIntervalMinimumIndex; k++) { + double dischargeNeeded = + energyLevelPredictionList.get(k) - dischargeZoneEnergyLevel; + + if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + + if (dischargeAvailableLeft < 0) { + double energyLevelIntervalMinimum2 = + Collections.min(energyLevelPredictionList.subList(i, k - 1)); + double dischargeSpaceInterval = + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2; + + if (dischargeSpaceInterval < 0) { + dischargeAvailableLeft = + Math.max( + Math.max(dischargeAvailableLeft, dischargeNeeded), + dischargeSpaceInterval); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set( + j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); + } + } + } + + break; + } + } } + } + } + } + + // System.out.println("After Tariffs: energyLevelPredictionList = " + + // energyLevelPredictionList + "\n"); + + // Get the current energy level percentage target + int batteryEnergyLevelPercentageTarget = (int) Math.round(energyLevelPredictionList.get(1)); + batteryEnergyLevelPercentageTargets.put(batteryAssetId, batteryEnergyLevelPercentageTarget); + + // Calculate energy level percentage change per interval + List percentageChangeBatteryList = new ArrayList<>(); + + for (int i = 0; i < energyLevelPredictionList.size() - 1; i++) { + double percentageChange = + energyLevelPredictionList.get(i + 1) - energyLevelPredictionList.get(i); + percentageChangeBatteryList.add(percentageChange); + } + + // System.out.println("percentageChangeBatteryList = " + + // percentageChangeBatteryList); + + // Calculate total power change for the EMS + List powerChangeTotalList = new ArrayList<>(); + + for (double percentageChange : percentageChangeBatteryList) { + double power = 0; + + if (percentageChange > 0) { + power = + percentageChange * energyCapacityBattery / (intervalHour * chargeEfficiencyBattery); + } else if (percentageChange < 0) { + power = + percentageChange + * energyCapacityBattery + * dischargeEfficiencyBattery + / (10000 * intervalHour); } - return powerSetpointsNew; + powerChangeTotalList.add(round(power, 3)); + } + + // System.out.println("powerChangeTotalList = " + powerChangeTotalList); + + for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { + totalPowerConsumptionProductionFlexList.set( + i, totalPowerConsumptionProductionFlexList.get(i) - powerChangeTotalList.get(i)); + } + + // System.out.println("totalPowerConsumptionProductionFlexList = " + + // totalPowerConsumptionProductionFlexList); + + List> energyLevelPercentageForecast = new ArrayList<>(); + List> powerSetpointForecast = new ArrayList<>(); + + // Update energy level percentage forecast starting after current time + for (int i = 1; i < timestampsMillisList.size(); i++) { + energyLevelPercentageForecast.add( + new ValueDatapoint<>( + timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); + } + + // Update power set-point starting from current power limit + for (int i = 0; i < timestampsMillisList.size(); i++) { + powerSetpointForecast.add( + new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); + } + + services + .getAssetPredictedDatapointService() + .updateValues( + batteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + energyLevelPercentageForecast); + services + .getAssetPredictedDatapointService() + .updateValues( + batteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), + powerSetpointForecast); + + // System.out.println("UPDATED forecasts"); } - private void batteryCheckConnection(List electricityBatteryAssets, Services services) { - // This method checks if a battery is connected based on if the power and energyLevelPercentage attributes update within the active time interval - String connected = EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected.toString(); - String disconnected = EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected.toString(); - - long currentTimestampMillis = services.getTimerService().getCurrentTimeMillis(); - long activePeriodMillis = ACTIVE_PERIOD_MINUTES * 60000L; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - - Optional connectionStatusValue = electricityBatteryAsset.getConnectionStatus(); - String connectionStatusPrevious = ""; - - if (connectionStatusValue.isPresent()) { - connectionStatusPrevious = connectionStatusValue.get().toString(); - } + return batteryEnergyLevelPercentageTargets; + } + + private Map batteryCalculatePowerFlexibleAvailable( + List electricityBatteryAssets) { + HashMap powerFlexibleAvailable = new HashMap<>(); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); + boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + + double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); + double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + + if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { + chargePowerMaximum = 0.0; + } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { + chargePowerMaximum = 0.0; + } + + if (!allowDischarging + || energyLevelPercentageMinimum == null + || energyLevelPercentage == null) { + dischargePowerMaximum = 0.0; + } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { + dischargePowerMaximum = 0.0; + } + + powerFlexibleAvailable.put( + electricityBatteryAsset.getId(), + new double[] {chargePowerMaximum, dischargePowerMaximum}); + } - Double powerBattery = electricityBatteryAsset.getPower().orElse(null); - long powerTimestampMillisBattery = electricityBatteryAsset.getPowerTimestamp().orElse(0L); + return powerFlexibleAvailable; + } - Double energyLevelPercentageBattery = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - long energyLevelPercentageTimestampMillisBattery = electricityBatteryAsset.getEnergyLevelPercentageTimestamp().orElse(0L); + private Map batteryCalculatePowerSetpoints( + EmsEnergyOptimisationAsset energyOptimisationAsset, + List electricityBatteryAssets, + Services services, + String logPrefix) { + Map powerFlexibleAvailable = + batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); + Map powerSetpointsNew; - String connectionStatusCurrent = disconnected; + // Get latest power set-point across all batteries + long powerSetpointTimestampMillisLatest = 0L; - if ((currentTimestampMillis - powerTimestampMillisBattery) < activePeriodMillis && powerBattery != null && (currentTimestampMillis - energyLevelPercentageTimestampMillisBattery) < activePeriodMillis && energyLevelPercentageBattery != null) { - connectionStatusCurrent = connected; - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + long powerSetpointTimestampMillis = + electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); - if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.equals(disconnected)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(disconnected) && connectionStatusPrevious.equals(connected)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected), getClass().getSimpleName()); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE, null), getClass().getSimpleName()); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER, null), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.isEmpty()) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(disconnected) && connectionStatusPrevious.isEmpty()) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected), getClass().getSimpleName()); - } - } + if (powerSetpointTimestampMillis > powerSetpointTimestampMillisLatest) { + powerSetpointTimestampMillisLatest = powerSetpointTimestampMillis; + } } - private void batteryCheckSetup(List electricityBatteryAssets) { - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - // Check if the battery is enabled - boolean allowCharging = electricityBatteryAsset.getAllowDischarging().orElse(false); - boolean allowDischarging = electricityBatteryAsset.getAllowCharging().orElse(false); - - if (!allowCharging && !allowDischarging) { - continue; - } - - String logPrefixBattery = String.format("assetType='%s', assetId='%s', assetName='%s'", electricityBatteryAsset.getAssetType(), electricityBatteryAsset.getId(), electricityBatteryAsset.getAssetName()); - - // Check if the following attributes are connected - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Double power = electricityBatteryAsset.getPower().orElse(null); - StringBuilder logMessageBatteryConnect = new StringBuilder(); + // Check if power net is updated since last battery power set-points update + Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); + long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); - if (energyLevelPercentage == null) { - logMessageBatteryConnect.append(String.format(" '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName())); - } + // Turn off batteries that do not have flexible power available during main power meter + // disconnect + if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { + powerSetpointsNew = + batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); + return powerSetpointsNew; + } - if (power == null) { - logMessageBatteryConnect.append(String.format(" '%s',", EmsElectricityBatteryAsset.POWER.getName())); - } + // Check if power limits are present and calculate additional power limits + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - if (!logMessageBatteryConnect.isEmpty()) { - logMessageBatteryConnect.setLength(logMessageBatteryConnect.length() - 1); - logMessageBatteryConnect.insert(0, String.format("%s; Can't use battery for flexible power. The following attributes are not connected:", logPrefixBattery)); - LOG.warning(logMessageBatteryConnect.toString()); - } + Double powerLimitMaximumBatteries = null; - // Check if the following attributes are set - Double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(null); - Double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(null); - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - StringBuilder logMessageBatterySet = new StringBuilder(); - - if (chargePowerMaximum == null) { - logMessageBatterySet.append(String.format(" '%s',", EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName())); - } + if (powerLimitMaximumProfileTotal != null) { + powerLimitMaximumBatteries = + round( + powerLimitMaximumProfileTotal + * (1 - POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), + 3); + } - if (dischargePowerMaximum == null) { - logMessageBatterySet.append(String.format(" '%s',", EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName())); - } + Double powerLimitMinimumBatteries = null; - if (energyLevelPercentageMaximum == null) { - logMessageBatterySet.append(String.format(" '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName())); - } + if (powerLimitMinimumProfileTotal != null) { + powerLimitMinimumBatteries = + round( + powerLimitMinimumProfileTotal + * (1 - POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), + 3); + } - if (energyLevelPercentageMinimum == null) { - logMessageBatterySet.append(String.format(" '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName())); - } + // TODO: add battery limits cross-over warning + // TODO: add advanced settings - if (!logMessageBatterySet.isEmpty()) { - logMessageBatterySet.setLength(logMessageBatterySet.length() - 1); - logMessageBatterySet.insert(0, String.format("%s; Can't use battery for flexible power. The following attributes are not set:", logPrefixBattery)); - LOG.warning(logMessageBatterySet.toString()); - } + int currentMinute = LocalDateTime.now().getMinute(); + Map batteryEnergyLevelPercentageTargets = + batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); - if (energyLevelPercentageMaximum != null && energyLevelPercentageMinimum != null && (energyLevelPercentageMaximum - energyLevelPercentageMinimum) < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - int diff = energyLevelPercentageMaximum - energyLevelPercentageMinimum; - LOG.warning(String.format("%s; Can't use battery for flexible power. %s - %s = %s - %s = %s < %s%%)", logPrefixBattery, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), - energyLevelPercentageMaximum, energyLevelPercentageMinimum, diff, BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)); - } - } + // Calculate battery energy level forecast + // TODO: add 15 minute skip protection with forecast update timestamp for robust forecast + // updating + if ((currentMinute % 15) == 0) { + batteryEnergyLevelPercentageTargets = + batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); } - private Map batteryCheckPowerSetpointsCurrent(List electricityBatteryAssets, Map powerFlexibleAvailable) { - Map powerSetpointsNew = new HashMap<>(); + // Calculate virtual power consumption + Map powerSetpointsCurrent = new HashMap<>(); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + powerSetpointsCurrent.put(electricityBatteryAssetId, powerSetpoint); + } - if (energyLevelPercentage == null) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - continue; - } + double powerSetpointsCurrentSum = + powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + + // Calculate new power set-points + if (powerLimitMaximumProfileTotal != null + && powerConsumptionVirtual > powerLimitMaximumProfileTotal) { + Double powerLimitMaximumVirtual = + round( + powerLimitMaximumProfileTotal + * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), + 3); + powerSetpointsNew = + batteryCalculatePowerSetpointsOnLimitBreach( + powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMaximumVirtual, "max"); + } else if (powerLimitMinimumProfileTotal != null + && powerConsumptionVirtual < powerLimitMinimumProfileTotal) { + Double powerLimitMinimumVirtual = + round( + powerLimitMinimumProfileTotal + * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), + 3); + powerSetpointsNew = + batteryCalculatePowerSetpointsOnLimitBreach( + powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMinimumVirtual, "min"); + } else { + powerSetpointsNew = + batteryRestoreEnergyLevels( + energyOptimisationAsset, + electricityBatteryAssets, + powerFlexibleAvailable, + powerNet, + powerSetpointsCurrentSum, + powerLimitMinimumBatteries, + powerLimitMaximumBatteries, + batteryEnergyLevelPercentageTargets); + } - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; + // Create log messages on power limit breach + double powerSetpointsNewSum = + powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; + + if (powerLimitMaximumProfileTotal != null && powerNet > powerLimitMaximumProfileTotal) { + double powerExceededAmount = round((powerNet - powerLimitMaximumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Power net is %s kW above power limit maximum", logPrefix, powerExceededAmount)); + + if (powerNetNewVirtual > powerLimitMaximumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", + logPrefix, powerReductionShortage)); + } + } else if (powerLimitMinimumProfileTotal != null && powerNet < powerLimitMinimumProfileTotal) { + double powerExceededAmount = round((powerNet - powerLimitMinimumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Power net is %s kW below power limit minimum", logPrefix, powerExceededAmount)); + + if (powerNetNewVirtual < powerLimitMinimumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", + logPrefix, powerReductionShortage)); + } + } - // Set initial power set-point new - double powerSetpointNew = 0.0; + return powerSetpointsNew; + } - // Check if power set-point current is within available power limits - if (powerSetpointCurrent > 0.0) { - powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); - } else if (powerSetpointCurrent < 0.0) { - powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); - } + private Map batteryCalculatePowerSetpointsOnLimitBreach( + Map powerFlexibleAvailable, + Double powerConsumptionVirtual, + Double powerLimitVirtual, + String maxOrMin) { + Map powerSetpointsNew = new HashMap<>(); + boolean isMin = "min".equals(maxOrMin); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } + // Calculate the total power adjustment needed + double powerReduction = powerConsumptionVirtual - powerLimitVirtual; - return powerSetpointsNew; + if (isMin) { + powerReduction = -powerReduction; } - private Map batteryGetEnergyLevelPercentageTargetsCurrent(List electricityBatteryAssets, Services services) { - Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); - - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000) + (15 * 60000); + // Iterate over each battery asset + for (Map.Entry entry : powerFlexibleAvailable.entrySet()) { + String electricityBatteryAssetId = entry.getKey(); + double[] powerBatteryAvailableChargeDischarge = entry.getValue(); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String batteryAssetId = electricityBatteryAsset.getId(); - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); - List> energyLevelPercentagePredictedCurrent = services.getAssetPredictedDatapointService().queryDatapoints(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), assetDatapointQueryPredicted); + // Choose available power based on breach type + double powerBatteryAvailable = + isMin + ? -powerBatteryAvailableChargeDischarge[0] + : powerBatteryAvailableChargeDischarge[1]; - Integer energyLevelPercentageTarget = null; + if (powerReduction > 0) { + // Limit the power set-point to available battery power or remaining reduction needed + double setpoint = round(Math.max(-powerReduction, powerBatteryAvailable), 3); - if (!energyLevelPercentagePredictedCurrent.isEmpty()) { - Double value = ((Double) energyLevelPercentagePredictedCurrent.getLast().getValue()); - energyLevelPercentageTarget = (value != null) ? value.intValue() : null; - } - - batteryEnergyLevelPercentageTargetsCurrent.put(batteryAssetId, energyLevelPercentageTarget); + if (isMin) { + setpoint = -setpoint; } - return batteryEnergyLevelPercentageTargetsCurrent; + powerSetpointsNew.put(electricityBatteryAssetId, setpoint); + powerReduction += setpoint; + } else { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + } } - private Map batteryRestoreEnergyLevels(EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, Map powerFlexibleAvailable, Double powerNet, - Double powerSetpointsCurrentSum, Double powerLimitMinimumBatteries, Double powerLimitMaximumBatteries, Map batteryEnergyLevelPercentageTargets) { - Map powerSetpointsNew = new HashMap<>(); - - // Find battery power set-points for a system without power limits - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - - // Set initial power set-point of 0 - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - - if (energyLevelPercentage == null) { - continue; - } - - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + return powerSetpointsNew; + } + + private void batteryCheckConnection( + List electricityBatteryAssets, Services services) { + // This method checks if a battery is connected based on if the power and energyLevelPercentage + // attributes update within the active time interval + String connected = + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected + .toString(); + String disconnected = + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected + .toString(); + + long currentTimestampMillis = services.getTimerService().getCurrentTimeMillis(); + long activePeriodMillis = ACTIVE_PERIOD_MINUTES * 60000L; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + + Optional + connectionStatusValue = electricityBatteryAsset.getConnectionStatus(); + String connectionStatusPrevious = ""; + + if (connectionStatusValue.isPresent()) { + connectionStatusPrevious = connectionStatusValue.get().toString(); + } + + Double powerBattery = electricityBatteryAsset.getPower().orElse(null); + long powerTimestampMillisBattery = electricityBatteryAsset.getPowerTimestamp().orElse(0L); + + Double energyLevelPercentageBattery = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + long energyLevelPercentageTimestampMillisBattery = + electricityBatteryAsset.getEnergyLevelPercentageTimestamp().orElse(0L); + + String connectionStatusCurrent = disconnected; + + if ((currentTimestampMillis - powerTimestampMillisBattery) < activePeriodMillis + && powerBattery != null + && (currentTimestampMillis - energyLevelPercentageTimestampMillisBattery) + < activePeriodMillis + && energyLevelPercentageBattery != null) { + connectionStatusCurrent = connected; + } + + if (connectionStatusCurrent.equals(connected) + && connectionStatusPrevious.equals(disconnected)) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .connected), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(disconnected) + && connectionStatusPrevious.equals(connected)) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .disconnected), + getClass().getSimpleName()); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE, + null), + getClass().getSimpleName()); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER, null), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.isEmpty()) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .connected), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(disconnected) + && connectionStatusPrevious.isEmpty()) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .disconnected), + getClass().getSimpleName()); + } + } + } + + private void batteryCheckSetup(List electricityBatteryAssets) { + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + // Check if the battery is enabled + boolean allowCharging = electricityBatteryAsset.getAllowDischarging().orElse(false); + boolean allowDischarging = electricityBatteryAsset.getAllowCharging().orElse(false); + + if (!allowCharging && !allowDischarging) { + continue; + } + + String logPrefixBattery = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + electricityBatteryAsset.getAssetType(), + electricityBatteryAsset.getId(), + electricityBatteryAsset.getAssetName()); + + // Check if the following attributes are connected + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Double power = electricityBatteryAsset.getPower().orElse(null); + StringBuilder logMessageBatteryConnect = new StringBuilder(); + + if (energyLevelPercentage == null) { + logMessageBatteryConnect.append( + String.format(" '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName())); + } + + if (power == null) { + logMessageBatteryConnect.append( + String.format(" '%s',", EmsElectricityBatteryAsset.POWER.getName())); + } + + if (!logMessageBatteryConnect.isEmpty()) { + logMessageBatteryConnect.setLength(logMessageBatteryConnect.length() - 1); + logMessageBatteryConnect.insert( + 0, + String.format( + "%s; Can't use battery for flexible power. The following attributes are not connected:", + logPrefixBattery)); + LOG.warning(logMessageBatteryConnect.toString()); + } + + // Check if the following attributes are set + Double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(null); + Double dischargePowerMaximum = + electricityBatteryAsset.getDischargePowerMaximum().orElse(null); + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + StringBuilder logMessageBatterySet = new StringBuilder(); + + if (chargePowerMaximum == null) { + logMessageBatterySet.append( + String.format(" '%s',", EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName())); + } + + if (dischargePowerMaximum == null) { + logMessageBatterySet.append( + String.format(" '%s',", EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName())); + } + + if (energyLevelPercentageMaximum == null) { + logMessageBatterySet.append( + String.format( + " '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName())); + } + + if (energyLevelPercentageMinimum == null) { + logMessageBatterySet.append( + String.format( + " '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName())); + } + + if (!logMessageBatterySet.isEmpty()) { + logMessageBatterySet.setLength(logMessageBatterySet.length() - 1); + logMessageBatterySet.insert( + 0, + String.format( + "%s; Can't use battery for flexible power. The following attributes are not set:", + logPrefixBattery)); + LOG.warning(logMessageBatterySet.toString()); + } + + if (energyLevelPercentageMaximum != null + && energyLevelPercentageMinimum != null + && (energyLevelPercentageMaximum - energyLevelPercentageMinimum) + < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { + int diff = energyLevelPercentageMaximum - energyLevelPercentageMinimum; + LOG.warning( + String.format( + "%s; Can't use battery for flexible power. %s - %s = %s - %s = %s < %s%%)", + logPrefixBattery, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMaximum, + energyLevelPercentageMinimum, + diff, + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)); + } + } + } + + private Map batteryCheckPowerSetpointsCurrent( + List electricityBatteryAssets, + Map powerFlexibleAvailable) { + Map powerSetpointsNew = new HashMap<>(); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + if (energyLevelPercentage == null) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + continue; + } + + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; + double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; + + // Set initial power set-point new + double powerSetpointNew = 0.0; + + // Check if power set-point current is within available power limits + if (powerSetpointCurrent > 0.0) { + powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); + } else if (powerSetpointCurrent < 0.0) { + powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); + } + + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } - // Get battery energy level target - Integer energyLevelPercentageTarget = batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); + return powerSetpointsNew; + } - if (energyLevelPercentageTarget == null) { - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - energyLevelPercentageTarget = batteryCalculateEnergyLevelPercentageDefault(energyLevelPercentageMaximum, energyLevelPercentageMinimum, energyOptimisationAsset); - } + private Map batteryGetEnergyLevelPercentageTargetsCurrent( + List electricityBatteryAssets, Services services) { + Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); - if (energyLevelPercentage < (energyLevelPercentageTarget - BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { - // Start charging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (energyLevelPercentage > (energyLevelPercentageTarget + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { - // Start discharging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (powerSetpointCurrent > 0.0 && energyLevelPercentage < energyLevelPercentageTarget) { - // Continue charging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (powerSetpointCurrent < 0.0 && energyLevelPercentage > energyLevelPercentageTarget) { - // Continue discharging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } - } + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000) + (15 * 60000); - // Adjust battery power set-points in a system with power limits - double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String batteryAssetId = electricityBatteryAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); + List> energyLevelPercentagePredictedCurrent = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + batteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + assetDatapointQueryPredicted); - Double chargingSpace = null; - Double dischargingSpace = null; + Integer energyLevelPercentageTarget = null; - if (powerLimitMaximumBatteries != null) { - chargingSpace = powerLimitMaximumBatteries - powerConsumptionVirtual; - } + if (!energyLevelPercentagePredictedCurrent.isEmpty()) { + Double value = ((Double) energyLevelPercentagePredictedCurrent.getLast().getValue()); + energyLevelPercentageTarget = (value != null) ? value.intValue() : null; + } - if (powerLimitMinimumBatteries != null) { - dischargingSpace = powerLimitMinimumBatteries - powerConsumptionVirtual; - } + batteryEnergyLevelPercentageTargetsCurrent.put(batteryAssetId, energyLevelPercentageTarget); + } - if (chargingSpace != null && powerSetpointsNewSum > chargingSpace) { - // Adjust battery power set-points in case of charging space shortage - double powerReduction = powerSetpointsNewSum - chargingSpace; + return batteryEnergyLevelPercentageTargetsCurrent; + } + + private Map batteryRestoreEnergyLevels( + EmsEnergyOptimisationAsset energyOptimisationAsset, + List electricityBatteryAssets, + Map powerFlexibleAvailable, + Double powerNet, + Double powerSetpointsCurrentSum, + Double powerLimitMinimumBatteries, + Double powerLimitMaximumBatteries, + Map batteryEnergyLevelPercentageTargets) { + Map powerSetpointsNew = new HashMap<>(); + + // Find battery power set-points for a system without power limits + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + // Set initial power set-point of 0 + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + + if (energyLevelPercentage == null) { + continue; + } + + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + + // Get battery energy level target + Integer energyLevelPercentageTarget = + batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); + + if (energyLevelPercentageTarget == null) { + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + energyLevelPercentageTarget = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximum, + energyLevelPercentageMinimum, + energyOptimisationAsset); + } + + if (energyLevelPercentage + < (energyLevelPercentageTarget - BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { + // Start charging + double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } else if (energyLevelPercentage + > (energyLevelPercentageTarget + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { + // Start discharging + double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } else if (powerSetpointCurrent > 0.0 + && energyLevelPercentage < energyLevelPercentageTarget) { + // Continue charging + double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } else if (powerSetpointCurrent < 0.0 + && energyLevelPercentage > energyLevelPercentageTarget) { + // Continue discharging + double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } + } - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); + // Adjust battery power set-points in a system with power limits + double powerSetpointsNewSum = + powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; - // Only adjust battery power set-point for charging batteries - if (powerSetpointNew > 0.0) { - powerReduction = powerReduction - powerSetpointNew; + Double chargingSpace = null; + Double dischargingSpace = null; - if (powerReduction >= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = round(Math.min(-powerReduction, powerSetpointNew), 3); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } - } - } - } else if (dischargingSpace != null && powerSetpointsNewSum < dischargingSpace) { - // Adjust battery power set-points in case of discharging space shortage - double powerReduction = powerSetpointsNewSum - dischargingSpace; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - - // Only adjust battery power set-point for discharging batteries - if (powerSetpointNew < 0.0) { - powerReduction = powerReduction - powerSetpointNew; - - if (powerReduction <= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = Math.max(-powerReduction, powerSetpointNew); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } - } - } - } - - return powerSetpointsNew; + if (powerLimitMaximumBatteries != null) { + chargingSpace = powerLimitMaximumBatteries - powerConsumptionVirtual; } - private void batteryUpdatePowerSetpoints(List electricityBatteryAssets, Map powerSetpointsNew, Services services) { - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double powerSetpointNew = powerSetpointsNew.get(electricityBatteryAssetId); - - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT, powerSetpointNew), getClass().getSimpleName()); - } + if (powerLimitMinimumBatteries != null) { + dischargingSpace = powerLimitMinimumBatteries - powerConsumptionVirtual; } - private Map calculateTariffChargeAndDischargeZones(List> tariffImportDatapoints, List> tariffExportDatapoints, int window) { - Map chargeAndDischargeZonesMap = new HashMap<>(); + if (chargingSpace != null && powerSetpointsNewSum > chargingSpace) { + // Adjust battery power set-points in case of charging space shortage + double powerReduction = powerSetpointsNewSum - chargingSpace; - // Return empty map when there is no tariff forecast present - if (tariffImportDatapoints.size() < window || tariffExportDatapoints.size() < window) { - return chargeAndDischargeZonesMap; - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - // Find the best import price for each day - ZoneId zoneId = ZoneId.systemDefault(); - - List> tariffImportMovingAverage = movingAverage(tariffImportDatapoints, window); - List> tariffExportMovingAverage = movingAverage(tariffExportDatapoints, window); - - Map tariffImportDailyMinimumMap = - IntStream.range(0, tariffImportMovingAverage.size()) - .mapToObj(i -> { - ValueDatapoint dp = tariffImportMovingAverage.get(i); - return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); - }) - .collect(Collectors.toMap( - dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), - Function.identity(), - BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) - )); - - Map tariffExportDailyMinimumMap = - IntStream.range(0, tariffExportMovingAverage.size()) - .mapToObj(i -> { - ValueDatapoint dp = tariffExportMovingAverage.get(i); - return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); - }) - .collect(Collectors.toMap( - dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), - Function.identity(), - BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) - )); - - Map chargeZonesMap = new HashMap<>(); - Map dischargeZonesMap = new HashMap<>(); - - for (Map.Entry entry : tariffImportDailyMinimumMap.entrySet()) { - int startIndex = entry.getValue().index; - int endIndex = startIndex + window; - - for (int i = startIndex; i < endIndex; i++) { - long timeMillis = tariffImportDatapoints.get(i).getTimestamp(); - chargeZonesMap.put(timeMillis, 1); - } - } - - for (Map.Entry entry : tariffExportDailyMinimumMap.entrySet()) { - int startIndex = entry.getValue().index; - int endIndex = startIndex + window; + // Only adjust battery power set-point for charging batteries + if (powerSetpointNew > 0.0) { + powerReduction = powerReduction - powerSetpointNew; - for (int i = startIndex; i < endIndex; i++) { - long timeMillis = tariffExportDatapoints.get(i).getTimestamp(); - dischargeZonesMap.put(timeMillis, -1); - } + if (powerReduction >= 0.0) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + } else { + double powerSetpointAdjusted = round(Math.min(-powerReduction, powerSetpointNew), 3); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); + break; + } } + } + } else if (dischargingSpace != null && powerSetpointsNewSum < dischargingSpace) { + // Adjust battery power set-points in case of discharging space shortage + double powerReduction = powerSetpointsNewSum - dischargingSpace; - for (Map.Entry entry : chargeZonesMap.entrySet()) { - if (!dischargeZonesMap.containsKey(entry.getKey())) { - chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); - } - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - for (Map.Entry entry : dischargeZonesMap.entrySet()) { - if (!chargeZonesMap.containsKey(entry.getKey())) { - chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); - } + // Only adjust battery power set-point for discharging batteries + if (powerSetpointNew < 0.0) { + powerReduction = powerReduction - powerSetpointNew; + + if (powerReduction <= 0.0) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + } else { + double powerSetpointAdjusted = Math.max(-powerReduction, powerSetpointNew); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); + break; + } } + } + } - return chargeAndDischargeZonesMap; + return powerSetpointsNew; + } + + private void batteryUpdatePowerSetpoints( + List electricityBatteryAssets, + Map powerSetpointsNew, + Services services) { + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double powerSetpointNew = powerSetpointsNew.get(electricityBatteryAssetId); + + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT, + powerSetpointNew), + getClass().getSimpleName()); } + } - private EmsDayAheadAsset getDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - // Find assets in database - List assets = services.getAssetStorageService() - .findAll(new AssetQuery().parents(energyOptimisationAsset.getId()).types(EmsDayAheadAsset.class)) - .stream() - .map(asset -> (EmsDayAheadAsset) asset) - .toList(); - - EmsDayAheadAsset asset = null; - - if (assets.size() == 1) { - asset = assets.getFirst(); - } else if (assets.size() > 1) { - String logPrefixEnergyOptimisation = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAsset.getId(), energyOptimisationAsset.getAssetName()); - LOG.warning(String.format("%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", logPrefixEnergyOptimisation, assets.size(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName())); - } + private Map calculateTariffChargeAndDischargeZones( + List> tariffImportDatapoints, + List> tariffExportDatapoints, + int window) { + Map chargeAndDischargeZonesMap = new HashMap<>(); - return asset; + // Return empty map when there is no tariff forecast present + if (tariffImportDatapoints.size() < window || tariffExportDatapoints.size() < window) { + return chargeAndDischargeZonesMap; } - private List> getTariffDayAheadDatapoints(Asset asset, String attributeName, Services services) { - // Get the start of the day (00:00) in milliseconds - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + // Find the best import price for each day + ZoneId zoneId = ZoneId.systemDefault(); + + List> tariffImportMovingAverage = + movingAverage(tariffImportDatapoints, window); + List> tariffExportMovingAverage = + movingAverage(tariffExportDatapoints, window); + + Map tariffImportDailyMinimumMap = + IntStream.range(0, tariffImportMovingAverage.size()) + .mapToObj( + i -> { + ValueDatapoint dp = tariffImportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect( + Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)))); + + Map tariffExportDailyMinimumMap = + IntStream.range(0, tariffExportMovingAverage.size()) + .mapToObj( + i -> { + ValueDatapoint dp = tariffExportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect( + Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)))); + + Map chargeZonesMap = new HashMap<>(); + Map dischargeZonesMap = new HashMap<>(); + + for (Map.Entry entry : tariffImportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; + + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffImportDatapoints.get(i).getTimestamp(); + chargeZonesMap.put(timeMillis, 1); + } + } - // Get tariff data-points of yesterday, today and tomorrow - long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; - long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; - AssetDatapointAllQuery assetDatapointQueryHistoric = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> tariffHistoric = services.getAssetDatapointService().queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); + for (Map.Entry entry : tariffExportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; - return tariffHistoric; + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffExportDatapoints.get(i).getTimestamp(); + dischargeZonesMap.put(timeMillis, -1); + } } - private List> getTariffDatapoints(EmsEnergyOptimisationAsset energyOptimisationAsset, String attributeName, Services services) { - // Get the start of the day (00:00) in milliseconds - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); - - // Get tariff data-points of yesterday and 1 week into the future - long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; - long endTimeMillis = startOfCurrentDayMillis + 8 * 24 * 60 * 60000; - AssetDatapointAllQuery assetDatapointQueryHistoric = new AssetDatapointAllQuery(startTimeMillis, currentTimeMillis); - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); - List> tariffHistoric = services.getAssetDatapointService().queryDatapoints(energyOptimisationAsset.getId(), attributeName, assetDatapointQueryHistoric); - List> tariffPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), attributeName, assetDatapointQueryPredicted); - - // Combine historic and predicted data-points into one list - List> tariffCombined = new ArrayList<>(tariffHistoric); - tariffCombined.addAll(tariffPredicted); - - return tariffCombined; + for (Map.Entry entry : chargeZonesMap.entrySet()) { + if (!dischargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } } - public static List> intervalAverage(List> dataPoints, long intervalMillis) { - // Map - Map> valuesPerIntervalMap = new HashMap<>(); - - for (ValueDatapoint datapoint : dataPoints) { - long timestampMillis = datapoint.getTimestamp(); - Double value = (Double) datapoint.getValue(); - - if (value != null) { - // Calculate start of 15-minute interval - long intervalStartMillis = timestampMillis - timestampMillis % intervalMillis; + for (Map.Entry entry : dischargeZonesMap.entrySet()) { + if (!chargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } + } - // Add value to corresponding interval - valuesPerIntervalMap.computeIfAbsent(intervalStartMillis, key -> new ArrayList<>()).add(value); - } - } + return chargeAndDischargeZonesMap; + } + + private EmsDayAheadAsset getDayAheadAsset( + EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { + // Find assets in database + List assets = + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .parents(energyOptimisationAsset.getId()) + .types(EmsDayAheadAsset.class)) + .stream() + .map(asset -> (EmsDayAheadAsset) asset) + .toList(); + + EmsDayAheadAsset asset = null; + + if (assets.size() == 1) { + asset = assets.getFirst(); + } else if (assets.size() > 1) { + String logPrefixEnergyOptimisation = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAsset.getId(), + energyOptimisationAsset.getAssetName()); + LOG.warning( + String.format( + "%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", + logPrefixEnergyOptimisation, + assets.size(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName())); + } - // List with average values per interval - List> averageList = new ArrayList<>(); + return asset; + } + + private List> getTariffDayAheadDatapoints( + Asset asset, String attributeName, Services services) { + // Get the start of the day (00:00) in milliseconds + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + ZoneId zoneId = ZoneId.systemDefault(); + LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); + long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + + // Get tariff data-points of yesterday, today and tomorrow + long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; + long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; + AssetDatapointAllQuery assetDatapointQueryHistoric = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> tariffHistoric = + services + .getAssetDatapointService() + .queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); + + return tariffHistoric; + } + + private List> getTariffDatapoints( + EmsEnergyOptimisationAsset energyOptimisationAsset, String attributeName, Services services) { + // Get the start of the day (00:00) in milliseconds + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + ZoneId zoneId = ZoneId.systemDefault(); + LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); + long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + + // Get tariff data-points of yesterday and 1 week into the future + long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; + long endTimeMillis = startOfCurrentDayMillis + 8 * 24 * 60 * 60000; + AssetDatapointAllQuery assetDatapointQueryHistoric = + new AssetDatapointAllQuery(startTimeMillis, currentTimeMillis); + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); + List> tariffHistoric = + services + .getAssetDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), attributeName, assetDatapointQueryHistoric); + List> tariffPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), attributeName, assetDatapointQueryPredicted); + + // Combine historic and predicted data-points into one list + List> tariffCombined = new ArrayList<>(tariffHistoric); + tariffCombined.addAll(tariffPredicted); + + return tariffCombined; + } + + public static List> intervalAverage( + List> dataPoints, long intervalMillis) { + // Map + Map> valuesPerIntervalMap = new HashMap<>(); + + for (ValueDatapoint datapoint : dataPoints) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); + + if (value != null) { + // Calculate start of 15-minute interval + long intervalStartMillis = timestampMillis - timestampMillis % intervalMillis; + + // Add value to corresponding interval + valuesPerIntervalMap + .computeIfAbsent(intervalStartMillis, key -> new ArrayList<>()) + .add(value); + } + } - for (Map.Entry> entry : valuesPerIntervalMap.entrySet()) { - Long intervalStartMillis = entry.getKey(); - List values = entry.getValue(); + // List with average values per interval + List> averageList = new ArrayList<>(); - // Compute the average - double sum = 0; - for (double value : values) { - sum += value; - } - double average = sum / values.size(); + for (Map.Entry> entry : valuesPerIntervalMap.entrySet()) { + Long intervalStartMillis = entry.getKey(); + List values = entry.getValue(); - averageList.add(new ValueDatapoint<>(intervalStartMillis, average)); - } + // Compute the average + double sum = 0; + for (double value : values) { + sum += value; + } + double average = sum / values.size(); - return averageList; + averageList.add(new ValueDatapoint<>(intervalStartMillis, average)); } - private List> intervalInterpolate(List> dataPoints, long startTimeMillis, long endTimeMillis, long intervalMillis) { - List> interpolatedList = new ArrayList<>(); + return averageList; + } - if (dataPoints == null || dataPoints.size() < 2) { - return interpolatedList; - } + private List> intervalInterpolate( + List> dataPoints, + long startTimeMillis, + long endTimeMillis, + long intervalMillis) { + List> interpolatedList = new ArrayList<>(); - int idx1 = 0; + if (dataPoints == null || dataPoints.size() < 2) { + return interpolatedList; + } - for (long intervalTimeMillis = startTimeMillis; intervalTimeMillis <= endTimeMillis; intervalTimeMillis += intervalMillis) { - // Find data-point before and after interval - while (idx1 < (dataPoints.size() - 1) && dataPoints.get(idx1 + 1).getTimestamp() < intervalTimeMillis) { - idx1++; - } + int idx1 = 0; - long timeBeforeMillis = dataPoints.get(idx1).getTimestamp(); - long timeAfterMillis = dataPoints.get(idx1 + 1).getTimestamp(); + for (long intervalTimeMillis = startTimeMillis; + intervalTimeMillis <= endTimeMillis; + intervalTimeMillis += intervalMillis) { + // Find data-point before and after interval + while (idx1 < (dataPoints.size() - 1) + && dataPoints.get(idx1 + 1).getTimestamp() < intervalTimeMillis) { + idx1++; + } - // Interpolate value - if (intervalTimeMillis >= timeBeforeMillis && intervalTimeMillis <= timeAfterMillis) { - double valueBefore = (double) dataPoints.get(idx1).getValue(); - double valueAfter = (double) dataPoints.get(idx1 + 1).getValue(); + long timeBeforeMillis = dataPoints.get(idx1).getTimestamp(); + long timeAfterMillis = dataPoints.get(idx1 + 1).getTimestamp(); - double factor = (double) (intervalTimeMillis - timeBeforeMillis) / (timeAfterMillis - timeBeforeMillis); - double interpolatedValue = valueBefore + factor * (valueAfter - valueBefore); + // Interpolate value + if (intervalTimeMillis >= timeBeforeMillis && intervalTimeMillis <= timeAfterMillis) { + double valueBefore = (double) dataPoints.get(idx1).getValue(); + double valueAfter = (double) dataPoints.get(idx1 + 1).getValue(); - interpolatedList.add(new ValueDatapoint<>(intervalTimeMillis, interpolatedValue)); - } - } + double factor = + (double) (intervalTimeMillis - timeBeforeMillis) / (timeAfterMillis - timeBeforeMillis); + double interpolatedValue = valueBefore + factor * (valueAfter - valueBefore); - return interpolatedList; + interpolatedList.add(new ValueDatapoint<>(intervalTimeMillis, interpolatedValue)); + } } - public static List> movingAverage(List> dataPoints, int window) { - List> result = new ArrayList<>(); - if (window <= 0 || dataPoints.size() < window) { - return result; - } + return interpolatedList; + } - double sum = 0.0; + public static List> movingAverage( + List> dataPoints, int window) { + List> result = new ArrayList<>(); + if (window <= 0 || dataPoints.size() < window) { + return result; + } - for (int i = 0; i < dataPoints.size(); i++) { - // Extract numeric value - Number value = (Number) dataPoints.get(i).getValue(); - sum += value.doubleValue(); + double sum = 0.0; - // Remove value exiting the sliding window - if (i >= window) { - Number oldValue = (Number) dataPoints.get(i - window).getValue(); - sum -= oldValue.doubleValue(); - } + for (int i = 0; i < dataPoints.size(); i++) { + // Extract numeric value + Number value = (Number) dataPoints.get(i).getValue(); + sum += value.doubleValue(); - // When the window is "full", generate output datapoint - if (i >= window - 1) { - double avg = sum / window; + // Remove value exiting the sliding window + if (i >= window) { + Number oldValue = (Number) dataPoints.get(i - window).getValue(); + sum -= oldValue.doubleValue(); + } - // Use the timestamp of the window end (index i) - ValueDatapoint original = dataPoints.get(i); - ValueDatapoint averaged = - new ValueDatapoint<>(original.getTimestamp(), avg); + // When the window is "full", generate output datapoint + if (i >= window - 1) { + double avg = sum / window; - result.add(averaged); - } - } + // Use the timestamp of the window end (index i) + ValueDatapoint original = dataPoints.get(i); + ValueDatapoint averaged = new ValueDatapoint<>(original.getTimestamp(), avg); - return result; + result.add(averaged); + } } - private double round(double value, int precision) { - double scale = Math.pow(10, precision); - return Math.round(value * scale) / scale; + return result; + } + + private double round(double value, int precision) { + double scale = Math.pow(10, precision); + return Math.round(value * scale) / scale; + } + + private void updateDayAheadAsset( + EmsEnergyOptimisationAsset energyOptimisationAsset, + EmsDayAheadAsset dayAheadAsset, + Services services) { + String dayAheadAssetId = dayAheadAsset.getId(); + String logPrefixDayAhead = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); + + String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); + + if (collectTimeForecasts.isBlank()) { + LOG.warning( + String.format( + "%s, attributeName='%s'; Set time to collect day ahead forecasts", + logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); } - private void updateDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, EmsDayAheadAsset dayAheadAsset, Services services) { - String dayAheadAssetId = dayAheadAsset.getId(); - String logPrefixDayAhead = String.format("assetType='%s', assetId='%s', assetName='%s'", dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); - - String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); + // Parse the time string + LocalTime collectTime = null; + + if (!collectTimeForecasts.isBlank()) { + try { + collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); + } catch (Exception e) { + LOG.warning( + String.format( + "%s, attributeName='%s'; Error while parsing collect time; Exception: %s", + logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); + } + } - if (collectTimeForecasts.isBlank()) { - LOG.warning(String.format("%s, attributeName='%s'; Set time to collect day ahead forecasts", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); + // Collect the day ahead tariff forecasts at the desired collect time + if (collectTime != null) { + // Calculate the 15-minute interval for collecting the day ahead tariff forecasts + LocalDate currentDate = LocalDate.now(); + LocalDateTime collectDateTime = LocalDateTime.of(currentDate, collectTime); + long collectTimeMillisStart = + collectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long collectTimeMillisEnd = collectTimeMillisStart + 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + + if (currentTimeMillis >= collectTimeMillisStart && currentTimeMillis < collectTimeMillisEnd) { + // Create asset datapoint query + LocalDate tomorrowDate = currentDate.plusDays(1); + LocalDateTime startOfNextDay = tomorrowDate.atStartOfDay(); + long startTimeMillis = + startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get the tariffs number of data-points from day ahead asset + int tariffExportDayAheadSize = + services + .getAssetDatapointService() + .queryDatapoints( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), + assetDatapointQuery) + .size(); + int tariffImportDayAheadSize = + services + .getAssetDatapointService() + .queryDatapoints( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), + assetDatapointQuery) + .size(); + + // Only update the historic datapoint table if there are no day ahead tariffs present + if (tariffExportDayAheadSize == 0) { + List> tariffExport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), + tariffExport); } - // Parse the time string - LocalTime collectTime = null; - - if (!collectTimeForecasts.isBlank()) { - try { - collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); - } catch (Exception e) { - LOG.warning(String.format("%s, attributeName='%s'; Error while parsing collect time; Exception: %s", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); - } + if (tariffImportDayAheadSize == 0) { + List> tariffImport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), + tariffImport); } - // Collect the day ahead tariff forecasts at the desired collect time - if (collectTime != null) { - // Calculate the 15-minute interval for collecting the day ahead tariff forecasts - LocalDate currentDate = LocalDate.now(); - LocalDateTime collectDateTime = LocalDateTime.of(currentDate, collectTime); - long collectTimeMillisStart = collectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long collectTimeMillisEnd = collectTimeMillisStart + 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - - if (currentTimeMillis >= collectTimeMillisStart && currentTimeMillis < collectTimeMillisEnd) { - // Create asset datapoint query - LocalDate tomorrowDate = currentDate.plusDays(1); - LocalDateTime startOfNextDay = tomorrowDate.atStartOfDay(); - long startTimeMillis = startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - // Get the tariffs number of data-points from day ahead asset - int tariffExportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); - int tariffImportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); - - // Only update the historic datapoint table if there are no day ahead tariffs present - if (tariffExportDayAheadSize == 0) { - List> tariffExport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), assetDatapointQuery); - services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); - } - - if (tariffImportDayAheadSize == 0) { - List> tariffImport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), assetDatapointQuery); - services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), tariffImport); - } - - // Update the last update forecasts datetime field - String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); - String lastUpdateForecastsNew = collectDateTime.toString(); + // Update the last update forecasts datetime field + String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); + String lastUpdateForecastsNew = collectDateTime.toString(); - LocalDateTime lastUpdateDateTime = collectDateTime.plusDays(-1); + LocalDateTime lastUpdateDateTime = collectDateTime.plusDays(-1); - if (!lastUpdateForecasts.isEmpty()) { - lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); - } - - LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + if (!lastUpdateForecasts.isEmpty()) { + lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); + } - if (nextUpdateDate.isEqual(currentDate)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(dayAheadAssetId, EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), lastUpdateForecastsNew, collectTimeMillisStart)); - } - } + LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + + if (nextUpdateDate.isEqual(currentDate)) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + dayAheadAssetId, + EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), + lastUpdateForecastsNew, + collectTimeMillisStart)); } + } } + } - record IndexedDatapoint(long timestamp, double value, int index) { - } + record IndexedDatapoint(long timestamp, double value, int index) {} } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java index 008027a..88c45f8 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,23 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.optimisationMethods; +import static org.openremote.model.syslog.SyslogCategory.DATA; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import org.openremote.extension.ems.agent.EmsDayAheadAsset; import org.openremote.extension.ems.agent.EmsElectricityBatteryAsset; import org.openremote.extension.ems.agent.EmsEnergyOptimisationAsset; @@ -33,1730 +43,2522 @@ import org.openremote.model.syslog.SyslogCategory; import org.openremote.model.value.ValueType; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.openremote.model.syslog.SyslogCategory.DATA; - public class EmsOptimisationBeta implements OptimisationMethod { - protected static final Logger LOG = SyslogCategory.getLogger(DATA, EmsOptimisationBeta.class.getName()); - private final String optimisationMethodName = EmsOptimisationBeta.class.getSimpleName(); - - // Maximum interval between data-points send by the device to be considered connected - private final int ACTIVE_PERIOD_MINUTES = 5; + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, EmsOptimisationBeta.class.getName()); + private final String optimisationMethodName = EmsOptimisationBeta.class.getSimpleName(); + + // Maximum interval between data-points send by the device to be considered connected + private final int ACTIVE_PERIOD_MINUTES = 5; + + // Default EMS power limit settings + private final int POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT = 10; + + // Default battery settings + private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; + private final double BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT = 0.5; + private final int BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT = 8; + + // Advanced settings attribute names + private final String POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = + "powerLimitMaximumFluctuationMargin"; + private final String POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = + "powerLimitMinimumFluctuationMargin"; + + private final String[][] advancedSettingsAttributesInfo = { + {POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()}, + {POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()} + }; + + @Override + public void execute(String energyOptimisationAssetId, Services services) { + runOptimisationMethodEms(energyOptimisationAssetId, services); + } + + private void runOptimisationMethodEms(String energyOptimisationAssetId, Services services) { + // Get energy optimisation asset + EmsEnergyOptimisationAsset energyOptimisationAsset = + (EmsEnergyOptimisationAsset) + services.getAssetStorageService().find(energyOptimisationAssetId); + + if (energyOptimisationAsset == null) { + return; + } - // Default EMS power limit settings - private final int POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT = 10; + // Update Advanced settings attributes info + String advancedSettingsAttributes = + energyOptimisationAsset.getAdvancedSettingsAttributes().orElse(""); - // Default battery settings - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; - private final double BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT = 0.5; - private final int BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT = 8; + if (advancedSettingsAttributes.isBlank()) { + StringBuilder advancedSettingsAttributesBody = new StringBuilder(); - // Advanced settings attribute names - private final String POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = "powerLimitMaximumFluctuationMargin"; - private final String POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = "powerLimitMinimumFluctuationMargin"; + String firstRow = String.format("Optimisation method '%s':\n", optimisationMethodName); + advancedSettingsAttributesBody.append(firstRow); - private final String[][] advancedSettingsAttributesInfo = { - {POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()}, - {POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()} - }; + for (String[] row : advancedSettingsAttributesInfo) { + advancedSettingsAttributesBody.append(String.join(",", row)).append("\n"); + } - @Override - public void execute(String energyOptimisationAssetId, Services services) { - runOptimisationMethodEms(energyOptimisationAssetId, services); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, + advancedSettingsAttributesBody.toString()), + getClass().getSimpleName()); } - private void runOptimisationMethodEms(String energyOptimisationAssetId, Services services) { - // Get energy optimisation asset - EmsEnergyOptimisationAsset energyOptimisationAsset = (EmsEnergyOptimisationAsset) services.getAssetStorageService().find(energyOptimisationAssetId); - - if (energyOptimisationAsset == null) { - return; - } + String logPrefixEnergyOptimisation = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAssetId, + energyOptimisationAsset.getAssetName()); + + // Get all battery assets + List electricityBatteryAssets = + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .parents(energyOptimisationAssetId) + .types(EmsElectricityBatteryAsset.class)) + .stream() + .map(asset -> (EmsElectricityBatteryAsset) asset) + .toList(); + + if (energyOptimisationAsset.getEnableDetailedLogging().orElse(false)) { + int allowChargingSize = + electricityBatteryAssets.stream() + .filter( + electricityBatteryAsset -> + electricityBatteryAsset.getAllowCharging().orElse(false)) + .toList() + .size(); + + int allowDischargingSize = + electricityBatteryAssets.stream() + .filter( + electricityBatteryAsset -> + electricityBatteryAsset.getAllowDischarging().orElse(false)) + .toList() + .size(); + + LOG.info( + String.format( + "%s; Energy optimisation asset has %s battery asset(s). Number of batteries with '%s'= %s and '%s'= %s", + logPrefixEnergyOptimisation, + electricityBatteryAssets.size(), + EmsElectricityBatteryAsset.ALLOW_CHARGING.getName(), + allowChargingSize, + EmsElectricityBatteryAsset.ALLOW_DISCHARGING.getName(), + allowDischargingSize)); + } - // Update Advanced settings attributes info - String advancedSettingsAttributes = energyOptimisationAsset.getAdvancedSettingsAttributes().orElse(""); + // Check if power net is connected + Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); - if (advancedSettingsAttributes.isBlank()) { - StringBuilder advancedSettingsAttributesBody = new StringBuilder(); + if (powerNet == null) { + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. '%s' attribute is not connected", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_NET.getName())); + } - String firstRow = String.format("Optimisation method '%s':\n", optimisationMethodName); - advancedSettingsAttributesBody.append(firstRow); + // Validate power limits + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); + Long powerLimitMaximumProfileTimestampMillis = + energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); - for (String[] row : advancedSettingsAttributesInfo) { - advancedSettingsAttributesBody.append(String.join(",", row)).append("\n"); - } + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(energyOptimisationAssetId, EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, advancedSettingsAttributesBody.toString()), getClass().getSimpleName()); - } + String powerLimitMaximumProfileDateTime = ""; - String logPrefixEnergyOptimisation = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAssetId, energyOptimisationAsset.getAssetName()); - - // Get all battery assets - List electricityBatteryAssets = services.getAssetStorageService() - .findAll(new AssetQuery().parents(energyOptimisationAssetId).types(EmsElectricityBatteryAsset.class)) - .stream() - .map(asset -> (EmsElectricityBatteryAsset) asset) - .toList(); - - if (energyOptimisationAsset.getEnableDetailedLogging().orElse(false)) { - int allowChargingSize = electricityBatteryAssets - .stream() - .filter(electricityBatteryAsset -> electricityBatteryAsset.getAllowCharging().orElse(false)) - .toList() - .size(); - - int allowDischargingSize = electricityBatteryAssets - .stream() - .filter(electricityBatteryAsset -> electricityBatteryAsset.getAllowDischarging().orElse(false)) - .toList() - .size(); - - LOG.info(String.format("%s; Energy optimisation asset has %s battery asset(s). Number of batteries with '%s'= %s and '%s'= %s", - logPrefixEnergyOptimisation, electricityBatteryAssets.size(), EmsElectricityBatteryAsset.ALLOW_CHARGING.getName(), allowChargingSize, EmsElectricityBatteryAsset.ALLOW_DISCHARGING.getName(), allowDischargingSize)); - } + if (powerLimitMaximumProfileTimestampMillis != null) { + powerLimitMaximumProfileDateTime = + formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); + } - // Check if power net is connected - Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); + if (powerLimitMaximumProfileTotal != null + && powerLimitMinimumProfileTotal != null + && powerLimitMaximumProfileTotal <= powerLimitMinimumProfileTotal) { + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. The '%s'= %s kW is lower than or equal to '%s'= %s kW for timestamp='%s'", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + powerLimitMaximumProfileTotal, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + powerLimitMinimumProfileTotal, + powerLimitMaximumProfileDateTime)); + } - if (powerNet == null) { - LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. '%s' attribute is not connected", logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_NET.getName())); - } + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); - // Validate power limits - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - Long powerLimitMaximumProfileTimestampMillis = energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); + // Update day ahead asset at specific time of day + if (dayAheadAsset != null) { + updateDayAheadAsset(energyOptimisationAsset, dayAheadAsset, services); + } - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + // Check for battery assets + if (electricityBatteryAssets.isEmpty()) { + return; + } - String powerLimitMaximumProfileDateTime = ""; + // Order battery assets + electricityBatteryAssets = batteryOrder(electricityBatteryAssets); - if (powerLimitMaximumProfileTimestampMillis != null) { - powerLimitMaximumProfileDateTime = formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); - } + // Check connection status of battery assets + batteryCheckConnection(electricityBatteryAssets, services); - if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal != null && powerLimitMaximumProfileTotal <= powerLimitMinimumProfileTotal) { - LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. The '%s'= %s kW is lower than or equal to '%s'= %s kW for timestamp='%s'", - logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal, - EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal, powerLimitMaximumProfileDateTime)); - } + // Check if all required attributes are connected/set and create log messages + batteryCheckSetup(electricityBatteryAssets); - // Get day ahead asset - EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + // Find latest power set-point update across all batteries + long powerSetpointTimestampLatestMillis = + batteriesLatestPowerSetpointUpdate(electricityBatteryAssets); - // Update day ahead asset at specific time of day - if (dayAheadAsset != null) { - updateDayAheadAsset(energyOptimisationAsset, dayAheadAsset, services); - } + // Calculate battery energy level percentage targets + int intervalPeriodMinutes = 15; + int currentMinute = LocalDateTime.now().getMinute(); + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long forecastUpdateTimeMillis = + currentTimeMillis - currentTimeMillis % (intervalPeriodMinutes * 60 * 1000); - // Check for battery assets - if (electricityBatteryAssets.isEmpty()) { - return; - } + Map batteryEnergyLevelPercentageTargets; - // Order battery assets - electricityBatteryAssets = batteryOrder(electricityBatteryAssets); + if ((currentMinute % intervalPeriodMinutes) == 0 + || powerSetpointTimestampLatestMillis < forecastUpdateTimeMillis) { + batteryEnergyLevelPercentageTargets = + batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); + } else { + batteryEnergyLevelPercentageTargets = + batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); + } - // Check connection status of battery assets - batteryCheckConnection(electricityBatteryAssets, services); + // Calculate battery power set-points + Map powerSetpointsNew = + batteryCalculatePowerSetpoints( + energyOptimisationAsset, + electricityBatteryAssets, + powerSetpointTimestampLatestMillis, + batteryEnergyLevelPercentageTargets, + services, + logPrefixEnergyOptimisation); + + // Update battery power set-points + batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); + } + + private long batteriesLatestPowerSetpointUpdate( + List electricityBatteryAssets) { + // Find latest power set-point update across all batteries + long powerSetpointTimestampLatestMillis = 0L; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + long powerSetpointTimestampMillis = + electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); + + if (powerSetpointTimestampMillis > powerSetpointTimestampLatestMillis) { + powerSetpointTimestampLatestMillis = powerSetpointTimestampMillis; + } + } - // Check if all required attributes are connected/set and create log messages - batteryCheckSetup(electricityBatteryAssets); + return powerSetpointTimestampLatestMillis; + } - // Find latest power set-point update across all batteries - long powerSetpointTimestampLatestMillis = batteriesLatestPowerSetpointUpdate(electricityBatteryAssets); + private int batteryCalculateEnergyLevelPercentageDefault( + Integer energyLevelPercentageMaximumBattery, + Integer energyLevelPercentageMinimumBattery, + EmsEnergyOptimisationAsset energyOptimisationAsset) { + if (energyLevelPercentageMaximumBattery == null + || energyLevelPercentageMinimumBattery == null) { + return BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT; + } - // Calculate battery energy level percentage targets - int intervalPeriodMinutes = 15; - int currentMinute = LocalDateTime.now().getMinute(); - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long forecastUpdateTimeMillis = currentTimeMillis - currentTimeMillis % (intervalPeriodMinutes * 60 * 1000); + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); + + int energyLevelPercentageDefault; + + if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { + energyLevelPercentageDefault = energyLevelPercentageMinimumBattery; + } else if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { + energyLevelPercentageDefault = energyLevelPercentageMaximumBattery; + } else { + energyLevelPercentageDefault = + (int) + Math.round( + (double) + (energyLevelPercentageMaximumBattery + + energyLevelPercentageMinimumBattery) + / 2); + } - Map batteryEnergyLevelPercentageTargets; + return energyLevelPercentageDefault; + } + + private Map batteryCalculateForecasts( + List electricityBatteryAssets, + EmsEnergyOptimisationAsset energyOptimisationAsset, + Services services) { + Map batteryEnergyLevelPercentageTargets = new HashMap<>(); + + StringBuilder infoStr = new StringBuilder(); + infoStr.append("EMS forecast:\n"); + + // Get power consumption forecast for 1 week + long intervalMillis = 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeForecastPeriodMillis = currentTimeMillis - currentTimeMillis % intervalMillis; + long endTimeForecastPeriodMillis = + startTimeForecastPeriodMillis + - startTimeForecastPeriodMillis % (24 * 60 * 60000) + + (8 * 24 * 60 * 60000); + + String energyOptimisationAssetId = energyOptimisationAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = + new AssetDatapointAllQuery(startTimeForecastPeriodMillis, endTimeForecastPeriodMillis); + List> energyOptimisationPowerConsumptionPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName(), + assetDatapointQueryConsumptionPredicted); + + // System.out.println("energyOptimisationPowerConsumptionPredicted = " + + // energyOptimisationPowerConsumptionPredicted); + + if (energyOptimisationPowerConsumptionPredicted.isEmpty()) { + return batteryEnergyLevelPercentageTargets; + } - if ((currentMinute % intervalPeriodMinutes) == 0 || powerSetpointTimestampLatestMillis < forecastUpdateTimeMillis) { - batteryEnergyLevelPercentageTargets = batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); - } else { - batteryEnergyLevelPercentageTargets = batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); - } + // Calculate power average for each 15-minute interval + List> totalPowerConsumptionAveraged = + intervalAverage(energyOptimisationPowerConsumptionPredicted, intervalMillis); + totalPowerConsumptionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - // Calculate battery power set-points - Map powerSetpointsNew = batteryCalculatePowerSetpoints(energyOptimisationAsset, electricityBatteryAssets, powerSetpointTimestampLatestMillis, batteryEnergyLevelPercentageTargets, services, logPrefixEnergyOptimisation); + // System.out.println("totalPowerConsumptionAveraged = " + + // totalPowerConsumptionAveraged); - // Update battery power set-points - batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); - } + long startTimeForecastDataMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); + long endTimeForecastDataMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); - private long batteriesLatestPowerSetpointUpdate(List electricityBatteryAssets) { - // Find latest power set-point update across all batteries - long powerSetpointTimestampLatestMillis = 0L; + // Interpolate power average values for each 15-minute interval + List> totalPowerConsumptionInterpolated = + intervalInterpolate( + totalPowerConsumptionAveraged, + startTimeForecastDataMillis, + endTimeForecastDataMillis, + intervalMillis); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - long powerSetpointTimestampMillis = electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); + List timestampsMillisList = new ArrayList<>(); + List totalPowerConsumptionList = new ArrayList<>(); - if (powerSetpointTimestampMillis > powerSetpointTimestampLatestMillis) { - powerSetpointTimestampLatestMillis = powerSetpointTimestampMillis; - } - } + for (ValueDatapoint datapoint : totalPowerConsumptionInterpolated) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); - return powerSetpointTimestampLatestMillis; + timestampsMillisList.add(timestampMillis); + totalPowerConsumptionList.add(value); } - private int batteryCalculateEnergyLevelPercentageDefault(Integer energyLevelPercentageMaximumBattery, Integer energyLevelPercentageMinimumBattery, EmsEnergyOptimisationAsset energyOptimisationAsset) { - if (energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { - return BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT; - } - - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); + int numberOfTimestamps = timestampsMillisList.size(); - int energyLevelPercentageDefault; + infoStr.append(String.format("numberOfTimestamps = %s \n", numberOfTimestamps)); + infoStr.append(String.format("timestampsMillisList = %s \n", timestampsMillisList)); + infoStr.append(String.format("totalPowerConsumptionList = %s \n", totalPowerConsumptionList)); - if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { - energyLevelPercentageDefault = energyLevelPercentageMinimumBattery; - } else if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { - energyLevelPercentageDefault = energyLevelPercentageMaximumBattery; - } else { - energyLevelPercentageDefault = (int) Math.round((double) (energyLevelPercentageMaximumBattery + energyLevelPercentageMinimumBattery) / 2); - } + if (totalPowerConsumptionList.isEmpty()) { + return batteryEnergyLevelPercentageTargets; + } - return energyLevelPercentageDefault; + AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = + new AssetDatapointAllQuery(startTimeForecastDataMillis, endTimeForecastDataMillis); + List> energyOptimisationPowerProductionPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName(), + assetDatapointQueryPeriodPredicted); + + // System.out.println("energyOptimisationPowerProductionPredicted = " + + // energyOptimisationPowerProductionPredicted); + + Map totalPowerProductionMap = new HashMap<>(); + + if (!energyOptimisationPowerProductionPredicted.isEmpty()) { + // Calculate power average for each 15-minute interval + List> totalPowerProductionAveraged = + intervalAverage(energyOptimisationPowerProductionPredicted, intervalMillis); + totalPowerProductionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + + // Interpolate power average values for each 15-minute interval + List> totalPowerProductionInterpolated = + intervalInterpolate( + totalPowerProductionAveraged, + totalPowerProductionAveraged.getFirst().getTimestamp(), + totalPowerProductionAveraged.getLast().getTimestamp(), + intervalMillis); + + totalPowerProductionMap = + totalPowerProductionInterpolated.stream() + .collect( + Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); } - private Map batteryCalculateForecasts(List electricityBatteryAssets, EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - Map batteryEnergyLevelPercentageTargets = new HashMap<>(); + // List to store the sum of total power consumption, production and flexible + List totalPowerConsumptionProductionFlexibleList = new ArrayList<>(); - StringBuilder infoStr = new StringBuilder(); - infoStr.append("EMS forecast:\n"); + // Sum total power consumption and production + for (int i = 0; i < numberOfTimestamps; i++) { + Double powerConsumption = totalPowerConsumptionList.get(i); + Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); - // Get power consumption forecast for 1 week - long intervalMillis = 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long startTimeForecastPeriodMillis = currentTimeMillis - currentTimeMillis % intervalMillis; - long endTimeForecastPeriodMillis = startTimeForecastPeriodMillis - startTimeForecastPeriodMillis % (24 * 60 * 60000) + (8 * 24 * 60 * 60000); + if (powerProduction != null) { + double sum = powerConsumption + powerProduction; + totalPowerConsumptionProductionFlexibleList.add(sum); + } else { + totalPowerConsumptionProductionFlexibleList.add(powerConsumption); + } + } - String energyOptimisationAssetId = energyOptimisationAsset.getId(); - AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = new AssetDatapointAllQuery(startTimeForecastPeriodMillis, endTimeForecastPeriodMillis); - List> energyOptimisationPowerConsumptionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName(), assetDatapointQueryConsumptionPredicted); + infoStr.append( + String.format( + "totalPowerConsumptionProductionFlexibleList = %s \n", + totalPowerConsumptionProductionFlexibleList)); + + // Get power limit forecasts + List> energyOptimisationPowerLimitMaximumPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + assetDatapointQueryPeriodPredicted); + List> energyOptimisationPowerLimitMinimumPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + assetDatapointQueryPeriodPredicted); + + Map powerLimitMaximumMap = new HashMap<>(); + Map powerLimitMinimumMap = new HashMap<>(); + + for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { + powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + } -// System.out.println("energyOptimisationPowerConsumptionPredicted = " + energyOptimisationPowerConsumptionPredicted); + for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { + powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + } - if (energyOptimisationPowerConsumptionPredicted.isEmpty()) { - return batteryEnergyLevelPercentageTargets; + List chargePowerAvailableTotalList = new ArrayList<>(); + List dischargePowerAvailableTotalList = new ArrayList<>(); + + // Add current available charge and discharge power which is 0 + chargePowerAvailableTotalList.add(0.0); + dischargePowerAvailableTotalList.add(0.0); + + double intervalHour = round((double) intervalMillis / (60 * 60000), 5); + int batteryPercentageRoundingPrecision = 5; + + // Calculate forecast for each battery + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String batteryAssetId = electricityBatteryAsset.getId(); + Integer chargeEfficiencyBattery = electricityBatteryAsset.getChargeEfficiency().orElse(null); + Double chargePowerMaximumBattery = + electricityBatteryAsset.getChargePowerMaximum().orElse(null); + Integer dischargeEfficiencyBattery = + electricityBatteryAsset.getDischargeEfficiency().orElse(null); + Double dischargePowerMaximumBattery = + electricityBatteryAsset.getDischargePowerMaximum().orElse(null); + Double energyCapacityBattery = electricityBatteryAsset.getEnergyCapacity().orElse(null); + Double energyLevelPercentageBattery = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximumBattery = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimumBattery = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + + if (chargeEfficiencyBattery == null + || chargePowerMaximumBattery == null + || dischargeEfficiencyBattery == null + || dischargePowerMaximumBattery == null + || energyCapacityBattery == null + || energyLevelPercentageBattery == null + || energyLevelPercentageMaximumBattery == null + || energyLevelPercentageMinimumBattery == null) { + continue; + } + + // Check if the energy level percentage maximum and minimum are valid + if (energyLevelPercentageMaximumBattery <= energyLevelPercentageMinimumBattery) { + continue; + } + + // Check if the charge and discharge efficiency are valid + if (chargeEfficiencyBattery <= 0 || dischargeEfficiencyBattery <= 0) { + continue; + } + + List powerLimitMaximumVirtualList = new ArrayList<>(); + List powerLimitMinimumVirtualList = new ArrayList<>(); + + // Calculate total available charge and discharge power + for (int i = 0; i < numberOfTimestamps; i++) { + long timestampMillis = timestampsMillisList.get(i); + double totalPower = totalPowerConsumptionProductionFlexibleList.get(i); + + Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); + Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); + + Double powerLimitMaximumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); + Double powerLimitMinimumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); + + powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); + powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); + + if (powerLimitMaximumVirtual != null) { + double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; + chargePowerAvailableTotalList.add(chargePowerTotalAvailable); + } else { + chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); } - // Calculate power average for each 15-minute interval - List> totalPowerConsumptionAveraged = intervalAverage(energyOptimisationPowerConsumptionPredicted, intervalMillis); - totalPowerConsumptionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - -// System.out.println("totalPowerConsumptionAveraged = " + totalPowerConsumptionAveraged); - - long startTimeForecastDataMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); - long endTimeForecastDataMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); - - // Interpolate power average values for each 15-minute interval - List> totalPowerConsumptionInterpolated = intervalInterpolate(totalPowerConsumptionAveraged, startTimeForecastDataMillis, endTimeForecastDataMillis, intervalMillis); - - List timestampsMillisList = new ArrayList<>(); - List totalPowerConsumptionList = new ArrayList<>(); - - for (ValueDatapoint datapoint : totalPowerConsumptionInterpolated) { - long timestampMillis = datapoint.getTimestamp(); - Double value = (Double) datapoint.getValue(); - - timestampsMillisList.add(timestampMillis); - totalPowerConsumptionList.add(value); + if (powerLimitMinimumVirtual != null) { + double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; + dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); + } else { + dischargePowerAvailableTotalList.add(Double.NEGATIVE_INFINITY); } - - int numberOfTimestamps = timestampsMillisList.size(); - - infoStr.append(String.format("numberOfTimestamps = %s \n", numberOfTimestamps)); - infoStr.append(String.format("timestampsMillisList = %s \n", timestampsMillisList)); - infoStr.append(String.format("totalPowerConsumptionList = %s \n", totalPowerConsumptionList)); - - if (totalPowerConsumptionList.isEmpty()) { - return batteryEnergyLevelPercentageTargets; + } + + int numberOfDataPoints = chargePowerAvailableTotalList.size(); + + infoStr.append( + String.format("powerLimitMaximumVirtualList = %s \n", powerLimitMaximumVirtualList)); + infoStr.append( + String.format("powerLimitMinimumVirtualList = %s \n", powerLimitMinimumVirtualList)); + infoStr.append("\n"); + + // System.out.println("chargePowerAvailableTotalList = " + + // chargePowerAvailableTotalList); + // System.out.println("dischargePowerAvailableTotalList = " + + // dischargePowerAvailableTotalList); + + List chargeNeededTotalList = + dischargePowerAvailableTotalList.stream().map(x -> x > 0 ? x : 0).toList(); + List dischargeNeededTotalList = + chargePowerAvailableTotalList.stream().map(x -> x < 0 ? x : 0).toList(); + + // System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); + // System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); + + List chargePowerAvailableBatteryList = + chargePowerAvailableTotalList.stream() + .map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0) + .toList(); + List dischargePowerAvailableBatteryList = + dischargePowerAvailableTotalList.stream() + .map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0) + .toList(); + + // System.out.println("chargeAvailableBatteryList = " + + // chargePowerAvailableBatteryList); + // System.out.println("dischargeAvailableBatteryList = " + + // dischargePowerAvailableBatteryList); + // System.out.println(); + + // Convert power to energy level percentage + List chargePercentageNeededTotalList = new ArrayList<>(); + List dischargePercentageNeededTotalList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + double c = + intervalHour + * chargeNeededTotalList.get(i) + * chargeEfficiencyBattery + / energyCapacityBattery; + double d = + 10000 + * intervalHour + * dischargeNeededTotalList.get(i) + / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageNeededTotalList.add(round(c, batteryPercentageRoundingPrecision)); + dischargePercentageNeededTotalList.add(round(d, batteryPercentageRoundingPrecision)); + } + + // System.out.println("chargePercentageNeededTotalList = " + + // chargePercentageNeededTotalList); + // System.out.println("dischargePercentageNeededTotalList = " + + // dischargePercentageNeededTotalList); + + List chargePercentageAvailableBatteryList = new ArrayList<>(); + List dischargePercentageAvailableBatteryList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + double c = + intervalHour + * chargePowerAvailableBatteryList.get(i) + * chargeEfficiencyBattery + / energyCapacityBattery; + double d = + 10000 + * intervalHour + * dischargePowerAvailableBatteryList.get(i) + / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageAvailableBatteryList.add(round(c, batteryPercentageRoundingPrecision)); + dischargePercentageAvailableBatteryList.add(round(d, batteryPercentageRoundingPrecision)); + } + + // System.out.println("chargePercentageAvailableBatteryList = " + + // chargePercentageAvailableBatteryList); + // System.out.println("dischargePercentageAvailableBatteryList = " + + // dischargePercentageAvailableBatteryList); + // System.out.println(); + + // Calculate forecast when current energy level percentage is outside limits + int getToLimitIndex = 0; + double getToLimitPercentage = energyLevelPercentageBattery; + List getToLimitPercentageList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + getToLimitIndex = i; + + if (getToLimitPercentage > energyLevelPercentageMaximumBattery) { + double dischargePercentageNeeded = + energyLevelPercentageMaximumBattery - getToLimitPercentage; + double dischargePercentageAvailable = + Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; + dischargePercentageAvailableBatteryList.set( + i, + round( + dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable, + batteryPercentageRoundingPrecision)); + getToLimitPercentageList.add( + round(getToLimitPercentage, batteryPercentageRoundingPrecision)); + } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { + double chargePercentageNeeded = + energyLevelPercentageMinimumBattery - getToLimitPercentage; + double chargePercentageAvailable = + Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; + chargePercentageAvailableBatteryList.set( + i, + round( + chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable, + batteryPercentageRoundingPrecision)); + getToLimitPercentageList.add( + round(getToLimitPercentage, batteryPercentageRoundingPrecision)); + } else { + break; } - - AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = new AssetDatapointAllQuery(startTimeForecastDataMillis, endTimeForecastDataMillis); - List> energyOptimisationPowerProductionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName(), assetDatapointQueryPeriodPredicted); - -// System.out.println("energyOptimisationPowerProductionPredicted = " + energyOptimisationPowerProductionPredicted); - - Map totalPowerProductionMap = new HashMap<>(); - - if (!energyOptimisationPowerProductionPredicted.isEmpty()) { - // Calculate power average for each 15-minute interval - List> totalPowerProductionAveraged = intervalAverage(energyOptimisationPowerProductionPredicted, intervalMillis); - totalPowerProductionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - - // Interpolate power average values for each 15-minute interval - List> totalPowerProductionInterpolated = intervalInterpolate(totalPowerProductionAveraged, totalPowerProductionAveraged.getFirst().getTimestamp(), totalPowerProductionAveraged.getLast().getTimestamp(), intervalMillis); - - totalPowerProductionMap = totalPowerProductionInterpolated.stream().collect(Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); + } + + // System.out.println("getToLimitIndex = " + getToLimitIndex); + // System.out.println("getToLimitPercentage = " + getToLimitPercentage); + // System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); + // System.out.println("chargePercentageAvailableBatteryList2 = " + + // chargePercentageAvailableBatteryList); + // System.out.println("dischargePercentageAvailableBatteryList2 = " + + // dischargePercentageAvailableBatteryList); + + List energyLevelPredictionList = new ArrayList<>(); + + if (getToLimitPercentage > energyLevelPercentageMaximumBattery + || getToLimitPercentage < energyLevelPercentageMinimumBattery) { + // Energy level percentage forecast when entire forecast is outside of battery percentage + // limits + energyLevelPredictionList = getToLimitPercentageList; + } else { + // Calculate energy level percentage forecast starting from value within battery percentage + // limits + List chargePercentageWantBatteryList = new ArrayList<>(); + List dischargePercentageWantBatteryList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + chargePercentageWantBatteryList.add( + Math.min( + chargePercentageNeededTotalList.get(i), + chargePercentageAvailableBatteryList.get(i))); + dischargePercentageWantBatteryList.add( + Math.max( + dischargePercentageNeededTotalList.get(i), + dischargePercentageAvailableBatteryList.get(i))); } - // List to store the sum of total power consumption, production and flexible - List totalPowerConsumptionProductionFlexibleList = new ArrayList<>(); + // System.out.println("chargePercentageWantBatteryList = " + + // chargePercentageWantBatteryList); + // System.out.println("dischargePercentageWantBatteryList = " + + // dischargePercentageWantBatteryList); - // Sum total power consumption and production - for (int i = 0; i < numberOfTimestamps; i++) { - Double powerConsumption = totalPowerConsumptionList.get(i); - Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); + List chargeAndDischargePercentageWantBatteryList = new ArrayList<>(); - if (powerProduction != null) { - double sum = powerConsumption + powerProduction; - totalPowerConsumptionProductionFlexibleList.add(sum); - } else { - totalPowerConsumptionProductionFlexibleList.add(powerConsumption); - } + for (int i = 0; i < numberOfDataPoints; i++) { + chargeAndDischargePercentageWantBatteryList.add( + round( + chargePercentageWantBatteryList.get(i) + + dischargePercentageWantBatteryList.get(i), + batteryPercentageRoundingPrecision)); } - infoStr.append(String.format("totalPowerConsumptionProductionFlexibleList = %s \n", totalPowerConsumptionProductionFlexibleList)); - - // Get power limit forecasts - List> energyOptimisationPowerLimitMaximumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); - List> energyOptimisationPowerLimitMinimumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); + // System.out.println("chargeAndDischargePercentageWantBatteryList = " + + // chargeAndDischargePercentageWantBatteryList); + // System.out.println(); - Map powerLimitMaximumMap = new HashMap<>(); - Map powerLimitMinimumMap = new HashMap<>(); + // Calculate forecast without battery percentage limits + int startRunningSumIndex = getToLimitIndex > 0 ? getToLimitIndex - 1 : 0; + List runningSumList = new ArrayList<>(); + double sum = getToLimitPercentage; - for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { - powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { + sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); + runningSumList.add(round(sum, batteryPercentageRoundingPrecision)); } - for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { - powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); - } - - List chargePowerAvailableTotalList = new ArrayList<>(); - List dischargePowerAvailableTotalList = new ArrayList<>(); - - // Add current available charge and discharge power which is 0 - chargePowerAvailableTotalList.add(0.0); - dischargePowerAvailableTotalList.add(0.0); - - double intervalHour = round((double) intervalMillis / (60 * 60000), 5); - int batteryPercentageRoundingPrecision = 5; - - // Calculate forecast for each battery - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String batteryAssetId = electricityBatteryAsset.getId(); - Integer chargeEfficiencyBattery = electricityBatteryAsset.getChargeEfficiency().orElse(null); - Double chargePowerMaximumBattery = electricityBatteryAsset.getChargePowerMaximum().orElse(null); - Integer dischargeEfficiencyBattery = electricityBatteryAsset.getDischargeEfficiency().orElse(null); - Double dischargePowerMaximumBattery = electricityBatteryAsset.getDischargePowerMaximum().orElse(null); - Double energyCapacityBattery = electricityBatteryAsset.getEnergyCapacity().orElse(null); - Double energyLevelPercentageBattery = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Integer energyLevelPercentageMaximumBattery = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimumBattery = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - - if (chargeEfficiencyBattery == null || chargePowerMaximumBattery == null || dischargeEfficiencyBattery == null || dischargePowerMaximumBattery == null || energyCapacityBattery == null || - energyLevelPercentageBattery == null || energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { - continue; - } - - // Check if the energy level percentage maximum and minimum are valid - if (energyLevelPercentageMaximumBattery <= energyLevelPercentageMinimumBattery) { - continue; - } - - // Check if the charge and discharge efficiency are valid - if (chargeEfficiencyBattery <= 0 || dischargeEfficiencyBattery <= 0) { - continue; - } - - List powerLimitMaximumVirtualList = new ArrayList<>(); - List powerLimitMinimumVirtualList = new ArrayList<>(); - - // Calculate total available charge and discharge power - for (int i = 0; i < numberOfTimestamps; i++) { - long timestampMillis = timestampsMillisList.get(i); - double totalPower = totalPowerConsumptionProductionFlexibleList.get(i); - - Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); - Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); - - Double powerLimitMaximumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); - Double powerLimitMinimumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); - - powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); - powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); - - if (powerLimitMaximumVirtual != null) { - double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; - chargePowerAvailableTotalList.add(chargePowerTotalAvailable); - } else { - chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); - } - - if (powerLimitMinimumVirtual != null) { - double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; - dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); - } else { - dischargePowerAvailableTotalList.add(Double.NEGATIVE_INFINITY); - } - } - - int numberOfDataPoints = chargePowerAvailableTotalList.size(); - - infoStr.append(String.format("powerLimitMaximumVirtualList = %s \n", powerLimitMaximumVirtualList)); - infoStr.append(String.format("powerLimitMinimumVirtualList = %s \n", powerLimitMinimumVirtualList)); - infoStr.append("\n"); - -// System.out.println("chargePowerAvailableTotalList = " + chargePowerAvailableTotalList); -// System.out.println("dischargePowerAvailableTotalList = " + dischargePowerAvailableTotalList); - - List chargeNeededTotalList = dischargePowerAvailableTotalList.stream().map(x -> x > 0 ? x : 0).toList(); - List dischargeNeededTotalList = chargePowerAvailableTotalList.stream().map(x -> x < 0 ? x : 0).toList(); - -// System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); -// System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); - - List chargePowerAvailableBatteryList = chargePowerAvailableTotalList.stream().map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0).toList(); - List dischargePowerAvailableBatteryList = dischargePowerAvailableTotalList.stream().map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0).toList(); - -// System.out.println("chargeAvailableBatteryList = " + chargePowerAvailableBatteryList); -// System.out.println("dischargeAvailableBatteryList = " + dischargePowerAvailableBatteryList); -// System.out.println(); - - // Convert power to energy level percentage - List chargePercentageNeededTotalList = new ArrayList<>(); - List dischargePercentageNeededTotalList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - double c = intervalHour * chargeNeededTotalList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; - double d = 10000 * intervalHour * dischargeNeededTotalList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageNeededTotalList.add(round(c, batteryPercentageRoundingPrecision)); - dischargePercentageNeededTotalList.add(round(d, batteryPercentageRoundingPrecision)); - } - -// System.out.println("chargePercentageNeededTotalList = " + chargePercentageNeededTotalList); -// System.out.println("dischargePercentageNeededTotalList = " + dischargePercentageNeededTotalList); - - List chargePercentageAvailableBatteryList = new ArrayList<>(); - List dischargePercentageAvailableBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - double c = intervalHour * chargePowerAvailableBatteryList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; - double d = 10000 * intervalHour * dischargePowerAvailableBatteryList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageAvailableBatteryList.add(round(c, batteryPercentageRoundingPrecision)); - dischargePercentageAvailableBatteryList.add(round(d, batteryPercentageRoundingPrecision)); - } - -// System.out.println("chargePercentageAvailableBatteryList = " + chargePercentageAvailableBatteryList); -// System.out.println("dischargePercentageAvailableBatteryList = " + dischargePercentageAvailableBatteryList); -// System.out.println(); - - // Calculate forecast when current energy level percentage is outside limits - int getToLimitIndex = 0; - double getToLimitPercentage = energyLevelPercentageBattery; - List getToLimitPercentageList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - getToLimitIndex = i; - - if (getToLimitPercentage > energyLevelPercentageMaximumBattery) { - double dischargePercentageNeeded = energyLevelPercentageMaximumBattery - getToLimitPercentage; - double dischargePercentageAvailable = Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); - getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; - dischargePercentageAvailableBatteryList.set(i, round(dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable, batteryPercentageRoundingPrecision)); - getToLimitPercentageList.add(round(getToLimitPercentage, batteryPercentageRoundingPrecision)); - } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { - double chargePercentageNeeded = energyLevelPercentageMinimumBattery - getToLimitPercentage; - double chargePercentageAvailable = Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); - getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; - chargePercentageAvailableBatteryList.set(i, round(chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable, batteryPercentageRoundingPrecision)); - getToLimitPercentageList.add(round(getToLimitPercentage, batteryPercentageRoundingPrecision)); - } else { - break; - } - } - -// System.out.println("getToLimitIndex = " + getToLimitIndex); -// System.out.println("getToLimitPercentage = " + getToLimitPercentage); -// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); -// System.out.println("chargePercentageAvailableBatteryList2 = " + chargePercentageAvailableBatteryList); -// System.out.println("dischargePercentageAvailableBatteryList2 = " + dischargePercentageAvailableBatteryList); - - List energyLevelPredictionList = new ArrayList<>(); - - if (getToLimitPercentage > energyLevelPercentageMaximumBattery || getToLimitPercentage < energyLevelPercentageMinimumBattery) { - // Energy level percentage forecast when entire forecast is outside of battery percentage limits - energyLevelPredictionList = getToLimitPercentageList; - } else { - // Calculate energy level percentage forecast starting from value within battery percentage limits - List chargePercentageWantBatteryList = new ArrayList<>(); - List dischargePercentageWantBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - chargePercentageWantBatteryList.add(Math.min(chargePercentageNeededTotalList.get(i), chargePercentageAvailableBatteryList.get(i))); - dischargePercentageWantBatteryList.add(Math.max(dischargePercentageNeededTotalList.get(i), dischargePercentageAvailableBatteryList.get(i))); - } - -// System.out.println("chargePercentageWantBatteryList = " + chargePercentageWantBatteryList); -// System.out.println("dischargePercentageWantBatteryList = " + dischargePercentageWantBatteryList); - - List chargeAndDischargePercentageWantBatteryList = new ArrayList<>(); - - for (int i = 0; i < numberOfDataPoints; i++) { - chargeAndDischargePercentageWantBatteryList.add(round(chargePercentageWantBatteryList.get(i) + dischargePercentageWantBatteryList.get(i), batteryPercentageRoundingPrecision)); - } - -// System.out.println("chargeAndDischargePercentageWantBatteryList = " + chargeAndDischargePercentageWantBatteryList); -// System.out.println(); - - // Calculate forecast without battery percentage limits - int startRunningSumIndex = getToLimitIndex > 0 ? getToLimitIndex - 1 : 0; - List runningSumList = new ArrayList<>(); - double sum = getToLimitPercentage; - - for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { - sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); - runningSumList.add(round(sum, batteryPercentageRoundingPrecision)); - } - -// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); -// System.out.println("runningSumList = " + runningSumList); -// System.out.println(); - - // Combine outside limits and running sum energy level percentages - if (getToLimitPercentageList.size() > 1) { - energyLevelPredictionList.addAll(getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); - } - energyLevelPredictionList.addAll(runningSumList); - int energyLevelPredictionListSize = energyLevelPredictionList.size(); - - infoStr.append(String.format("energyLevelPredictionListOriginal = %s \n", energyLevelPredictionList)); - - // Calculate forecast starting within battery percentage limits - double energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionListSize)); - double energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionListSize)); + // System.out.println("getToLimitPercentageList = " + + // getToLimitPercentageList); + // System.out.println("runningSumList = " + runningSumList); + // System.out.println(); - long dtStart = services.getTimerService().getCurrentTimeMillis(); - long dt = 0; - long timeoutMillis = 10000; - - int intervalEndIndex = startRunningSumIndex; - int predictionListIndex = startRunningSumIndex; - - while (energyLevelPredictionMaximum > energyLevelPercentageMaximumBattery || energyLevelPredictionMinimum < energyLevelPercentageMinimumBattery) { - String chargeOrDischarge = ""; - int intervalStartIndex = startRunningSumIndex; - - for (int i = intervalEndIndex; i < energyLevelPredictionListSize; i++) { - predictionListIndex = i; - - if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { - intervalEndIndex = i; - - for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { - if (energyLevelPredictionList.get(j) >= energyLevelPercentageMaximumBattery) { - intervalStartIndex = j; - break; - } - } - - chargeOrDischarge = "charge"; - -// System.out.println("CHARGE"); -// System.out.println("intervalStartIndex = " + intervalStartIndex); -// System.out.println("intervalEndIndex = " + intervalEndIndex); - - break; - } else if (energyLevelPredictionList.get(i) > energyLevelPercentageMaximumBattery) { - intervalEndIndex = i; - - for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { - if (energyLevelPredictionList.get(j) <= energyLevelPercentageMinimumBattery) { - intervalStartIndex = j; - break; - } - } - - chargeOrDischarge = "discharge"; - -// System.out.println("DISCHARGE"); -// System.out.println("intervalStartIndex = " + intervalStartIndex); -// System.out.println("intervalEndIndex = " + intervalEndIndex); - - break; - } - } - - int changeIndex = -1; - double energyLevelPredictionValueBefore = energyLevelPredictionList.get(intervalEndIndex); - - if (chargeOrDischarge.equals("charge")) { - double chargeAvailable = 0; - double chargeAvailableInterval = round(energyLevelPercentageMaximumBattery - Collections.max(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)), batteryPercentageRoundingPrecision); - - if (chargeAvailableInterval > 0) { - for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { - if (i - 1 < 0) { - break; - } - - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = round(chargePercentageAvailableBatteryList.get(i) - chargeInUse, batteryPercentageRoundingPrecision); - } - - if (chargeAvailableLeft > 0) { - changeIndex = i; - chargeAvailable = Math.min(chargeAvailableInterval, chargeAvailableLeft); - break; - } - } - } - - double chargeNeeded = energyLevelPercentageMinimumBattery - energyLevelPredictionList.get(intervalEndIndex); - double changeValue = Math.min(chargeAvailable, chargeNeeded); - - if (changeIndex == -1) { - changeIndex = intervalEndIndex; - changeValue = chargeNeeded; - } - - for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { - energyLevelPredictionList.set(i, round(energyLevelPredictionList.get(i) + changeValue, batteryPercentageRoundingPrecision)); - } - } else if (chargeOrDischarge.equals("discharge")) { - double dischargeAvailable = 0; - double dischargeAvailableInterval = round(energyLevelPercentageMinimumBattery - Collections.min(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)), batteryPercentageRoundingPrecision); - - if (dischargeAvailableInterval > 0) { - for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { - if (i - 1 < 0) { - break; - } - - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = round(dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, batteryPercentageRoundingPrecision); - } - if (dischargeAvailableLeft < 0) { - changeIndex = i; - dischargeAvailable = Math.max(dischargeAvailableInterval, dischargeAvailableLeft); - break; - } - } - } - - double dischargeNeeded = energyLevelPercentageMaximumBattery - energyLevelPredictionList.get(intervalEndIndex); - double changeValue = Math.max(dischargeAvailable, dischargeNeeded); - - if (changeIndex == -1) { - changeIndex = intervalEndIndex; - changeValue = dischargeNeeded; - } - - for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { - energyLevelPredictionList.set(i, round(energyLevelPredictionList.get(i) + changeValue, batteryPercentageRoundingPrecision)); - } - } - - if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { - infoStr.insert(0, String.format("Battery energy level percentage calculation error at timestamp = %s", timestampsMillisList.get(predictionListIndex))); - infoStr.append(String.format("energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); - LOG.warning(infoStr.toString()); - break; - } else if (dt > timeoutMillis) { - infoStr.insert(0, String.format("Battery energy level percentage calculation timed out during power limit optimisation at timestamp = %s", timestampsMillisList.get(predictionListIndex))); - infoStr.append(String.format("energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); - LOG.warning(infoStr.toString()); - break; - } - - energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionListSize)); - energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionListSize)); - dt = services.getTimerService().getCurrentTimeMillis() - dtStart; - } - -// System.out.println("energyLevelPredictionListOptimisedLimits = " + energyLevelPredictionList + "\n"); - - // Get day ahead asset - EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); - boolean useDayAheadTariffs = false; - - if (dayAheadAsset != null) { - useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); + // Combine outside limits and running sum energy level percentages + if (getToLimitPercentageList.size() > 1) { + energyLevelPredictionList.addAll( + getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); + } + energyLevelPredictionList.addAll(runningSumList); + int energyLevelPredictionListSize = energyLevelPredictionList.size(); + + infoStr.append( + String.format("energyLevelPredictionListOriginal = %s \n", energyLevelPredictionList)); + + // Calculate forecast starting within battery percentage limits + double energyLevelPredictionMaximum = + Collections.max( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionListSize)); + double energyLevelPredictionMinimum = + Collections.min( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionListSize)); + + long dtStart = services.getTimerService().getCurrentTimeMillis(); + long dt = 0; + long timeoutMillis = 10000; + + int intervalEndIndex = startRunningSumIndex; + int predictionListIndex = startRunningSumIndex; + + while (energyLevelPredictionMaximum > energyLevelPercentageMaximumBattery + || energyLevelPredictionMinimum < energyLevelPercentageMinimumBattery) { + String chargeOrDischarge = ""; + int intervalStartIndex = startRunningSumIndex; + + for (int i = intervalEndIndex; i < energyLevelPredictionListSize; i++) { + predictionListIndex = i; + + if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { + intervalEndIndex = i; + + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) >= energyLevelPercentageMaximumBattery) { + intervalStartIndex = j; + break; } + } - // Get the tariff forecasts from energy optimisation asset for 1 week, timestamps are ordered from newest to oldest (descending order) - List> tariffExportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), services); - List> tariffImportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), services); - - // Overwrite the current tariff forecasts with the day ahead tariff forecasts - if (useDayAheadTariffs) { - List> tariffExportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); - List> tariffImportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); - - // Combine tariff export forecasts - if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); + chargeOrDischarge = "charge"; - for (ValueDatapoint dp : tariffExportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - tariffExportDatapoints = new ArrayList<>(mergedMap.values()); - tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - } + // System.out.println("CHARGE"); + // System.out.println("intervalStartIndex = " + + // intervalStartIndex); + // System.out.println("intervalEndIndex = " + + // intervalEndIndex); - // Combine tariff import forecasts - if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); + break; + } else if (energyLevelPredictionList.get(i) > energyLevelPercentageMaximumBattery) { + intervalEndIndex = i; - for (ValueDatapoint dp : tariffImportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); - } - - tariffImportDatapoints = new ArrayList<>(mergedMap.values()); - tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - } + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) <= energyLevelPercentageMinimumBattery) { + intervalStartIndex = j; + break; } + } - // Calculate optimal charge and discharge zone for each day based on tariffs - Map chargeAndDischargeZonesMap = calculateTariffChargeAndDischargeZones(tariffImportDatapoints, tariffExportDatapoints, BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT); - - // Charge zone = 1, discharge zone = -1 - List chargeAndDischargeZonesList = new ArrayList<>(Collections.nCopies(energyLevelPredictionListSize, 0)); - - for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { - long timestampMillis = entry.getKey(); - int value = entry.getValue(); + chargeOrDischarge = "discharge"; - int timestampIndex = timestampsMillisList.indexOf(timestampMillis); + // System.out.println("DISCHARGE"); + // System.out.println("intervalStartIndex = " + + // intervalStartIndex); + // System.out.println("intervalEndIndex = " + + // intervalEndIndex); - if (timestampIndex != -1) { - // Add 1 to align list indices - chargeAndDischargeZonesList.set(timestampIndex + 1, value); - } + break; + } + } + + int changeIndex = -1; + double energyLevelPredictionValueBefore = energyLevelPredictionList.get(intervalEndIndex); + + if (chargeOrDischarge.equals("charge")) { + double chargeAvailable = 0; + double chargeAvailableInterval = + round( + energyLevelPercentageMaximumBattery + - Collections.max( + energyLevelPredictionList.subList( + intervalStartIndex, intervalEndIndex)), + batteryPercentageRoundingPrecision); + + if (chargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; } -// System.out.println("chargeAndDischargeZonesList = " + chargeAndDischargeZonesList); - - // Calculate battery energy level percentage default list - double energyLevelPercentageDefault = batteryCalculateEnergyLevelPercentageDefault(energyLevelPercentageMaximumBattery, energyLevelPercentageMinimumBattery, energyOptimisationAsset); - List energyLevelPercentageDefaultList = new ArrayList<>(Collections.nCopies(energyLevelPredictionListSize, energyLevelPercentageDefault)); + double chargeAvailableLeft = 0; - for (int i = 0; i < energyLevelPredictionListSize; i++) { - if (chargeAndDischargeZonesList.get(i) == 1) { - energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMaximumBattery)); - } else if (chargeAndDischargeZonesList.get(i) == -1) { - energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMinimumBattery)); - } + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); } -// System.out.println("energyLevelPercentageDefaultList = " + energyLevelPercentageDefaultList); - - // Optimise forecast based on tariffs - for (int i = 1; i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionListSize; i++) { - if (chargeAndDischargeZonesList.get(i) == 1 && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { - // Interval from charge zone index till end of forecast - Double energyLevelIntervalMaximum = Collections.max(energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); - double chargeSpaceOverall = round(energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum, batteryPercentageRoundingPrecision); - - if (chargeSpaceOverall > 0) { - double chargeNeeded = round(energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), batteryPercentageRoundingPrecision); - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = round(chargePercentageAvailableBatteryList.get(i) - chargeInUse, batteryPercentageRoundingPrecision); - } - - if (chargeAvailableLeft > 0) { - chargeAvailableLeft = Math.min(Math.min(chargeSpaceOverall, chargeAvailableLeft), chargeNeeded); - chargeSpaceOverall = round(chargeSpaceOverall - chargeAvailableLeft, batteryPercentageRoundingPrecision); - - for (int j = i; j < energyLevelPredictionListSize; j++) { - energyLevelPredictionList.set(j, round(energyLevelPredictionList.get(j) + chargeAvailableLeft, batteryPercentageRoundingPrecision)); - } - } - } - - // Get the index of the first maximum - energyLevelIntervalMaximum = Collections.max(energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); - int energyLevelIntervalMaximumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); - - if (chargeSpaceOverall <= 0 && i < energyLevelIntervalMaximumIndex) { - double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - double chargeAvailableLeft = round(chargePercentageAvailableBatteryList.get(i) - chargeInUse, batteryPercentageRoundingPrecision); - - int energyLevelIntervalIndex = 0; - dt = 0; - dtStart = services.getTimerService().getCurrentTimeMillis(); - - while (chargeAvailableLeft > 0 && energyLevelIntervalIndex < energyLevelIntervalMaximumIndex) { - - for (int k = i + 1; k <= energyLevelIntervalMaximumIndex; k++) { - energyLevelIntervalIndex = k; - double chargeNeeded = round(energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), batteryPercentageRoundingPrecision); - - // Only allow moving charging moment into charge zone if no charge is wanted at index k - if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { - double energyLevelIntervalMaximum2 = Collections.max(energyLevelPredictionList.subList(i, k)); - double chargeSpaceInterval = round(energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2, batteryPercentageRoundingPrecision); - - if (chargeSpaceInterval > 0) { - double chargeChange = Math.min(Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); - chargeAvailableLeft = round(chargeAvailableLeft - chargeChange, batteryPercentageRoundingPrecision); - - for (int j = i; j < k; j++) { - energyLevelPredictionList.set(j, round(energyLevelPredictionList.get(j) + chargeChange, batteryPercentageRoundingPrecision)); - } - break; - } - } - } - - if (dt > timeoutMillis) { - infoStr.insert(0, String.format("Battery energy level percentage calculation timed out during tariff charge optimisation at timestamp = %s", timestampsMillisList.get(i))); - infoStr.append(String.format("energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); - LOG.warning(infoStr.toString()); - break; - } - - dt = services.getTimerService().getCurrentTimeMillis() - dtStart; - } - } - } else if (chargeAndDischargeZonesList.get(i) == -1 && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { - // Interval from discharge zone index till end of forecast - Double energyLevelIntervalMinimum = Collections.min(energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); - double dischargeSpaceOverall = round(energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum, batteryPercentageRoundingPrecision); - - if (dischargeSpaceOverall < 0) { - double dischargeNeeded = round(energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), batteryPercentageRoundingPrecision); - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = round(dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, batteryPercentageRoundingPrecision); - } - - if (dischargeAvailableLeft < 0) { - dischargeAvailableLeft = Math.max(Math.max(dischargeSpaceOverall, dischargeAvailableLeft), dischargeNeeded); - dischargeSpaceOverall = round(dischargeSpaceOverall - dischargeAvailableLeft, batteryPercentageRoundingPrecision); - - for (int j = i; j < energyLevelPredictionListSize; j++) { - energyLevelPredictionList.set(j, round(energyLevelPredictionList.get(j) + dischargeAvailableLeft, batteryPercentageRoundingPrecision)); - } - } - } - - // Get the index of the first minimum - energyLevelIntervalMinimum = Collections.min(energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); - int energyLevelIntervalMinimumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); - - if (dischargeSpaceOverall >= 0 && i < energyLevelIntervalMinimumIndex) { - double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - double dischargeAvailableLeft = round(dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, batteryPercentageRoundingPrecision); - - int energyLevelIntervalIndex = 0; - dt = 0; - dtStart = services.getTimerService().getCurrentTimeMillis(); - - while (dischargeAvailableLeft < 0 && energyLevelIntervalIndex < energyLevelIntervalMinimumIndex) { - - for (int k = i + 1; k <= energyLevelIntervalMinimumIndex; k++) { - energyLevelIntervalIndex = k; - double dischargeNeeded = round(energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), batteryPercentageRoundingPrecision); - - // Only allow moving discharging moment into discharge zone if no discharge is wanted at index k - if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { - double energyLevelIntervalMinimum2 = Collections.min(energyLevelPredictionList.subList(i, k)); - double dischargeSpaceInterval = round(energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2, batteryPercentageRoundingPrecision); - - if (dischargeSpaceInterval < 0) { - double dischargeChange = Math.max(Math.max(dischargeAvailableLeft, dischargeNeeded), dischargeSpaceInterval); - dischargeAvailableLeft = round(dischargeAvailableLeft - dischargeChange, batteryPercentageRoundingPrecision); - - for (int j = i; j < k; j++) { - energyLevelPredictionList.set(j, round(energyLevelPredictionList.get(j) + dischargeChange, batteryPercentageRoundingPrecision)); - } - break; - } - } - } - - if (dt > timeoutMillis) { - infoStr.insert(0, String.format("Battery energy level percentage calculation timed out during tariff discharge optimisation at timestamp = %s", timestampsMillisList.get(i))); - infoStr.append(String.format("energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); - LOG.warning(infoStr.toString()); - break; - } - - dt = services.getTimerService().getCurrentTimeMillis() - dtStart; - } - } - } + if (chargeAvailableLeft > 0) { + changeIndex = i; + chargeAvailable = Math.min(chargeAvailableInterval, chargeAvailableLeft); + break; } + } } -// System.out.println("energyLevelPredictionListOptimisedLimitsAndTariffs = " + energyLevelPredictionList + "\n"); - - // Get the current energy level percentage target - int batteryEnergyLevelPercentageTarget = (int) Math.round(energyLevelPredictionList.get(1)); - batteryEnergyLevelPercentageTargets.put(batteryAssetId, batteryEnergyLevelPercentageTarget); + double chargeNeeded = + energyLevelPercentageMinimumBattery + - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.min(chargeAvailable, chargeNeeded); - // Calculate energy level percentage change per interval - List percentageChangeBatteryList = new ArrayList<>(); - - for (int i = 0; i < energyLevelPredictionList.size() - 1; i++) { - double percentageChange = energyLevelPredictionList.get(i + 1) - energyLevelPredictionList.get(i); - percentageChangeBatteryList.add(percentageChange); + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = chargeNeeded; } -// System.out.println("percentageChangeBatteryList = " + percentageChangeBatteryList); - - // Calculate total power change for the EMS - List powerChangeTotalList = new ArrayList<>(); - - for (double percentageChange : percentageChangeBatteryList) { - double power = 0; - - if (percentageChange > 0) { - power = percentageChange * energyCapacityBattery / (intervalHour * chargeEfficiencyBattery); - } else if (percentageChange < 0) { - power = percentageChange * energyCapacityBattery * dischargeEfficiencyBattery / (10000 * intervalHour); - } - - powerChangeTotalList.add(round(power, 3)); + for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { + energyLevelPredictionList.set( + i, + round( + energyLevelPredictionList.get(i) + changeValue, + batteryPercentageRoundingPrecision)); } + } else if (chargeOrDischarge.equals("discharge")) { + double dischargeAvailable = 0; + double dischargeAvailableInterval = + round( + energyLevelPercentageMinimumBattery + - Collections.min( + energyLevelPredictionList.subList( + intervalStartIndex, intervalEndIndex)), + batteryPercentageRoundingPrecision); + + if (dischargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; + } -// System.out.println("powerChangeTotalList = " + powerChangeTotalList); + double dischargeAvailableLeft = 0; - // Update list with power flexible changes - for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { - totalPowerConsumptionProductionFlexibleList.set(i, totalPowerConsumptionProductionFlexibleList.get(i) - powerChangeTotalList.get(i)); + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); + } + if (dischargeAvailableLeft < 0) { + changeIndex = i; + dischargeAvailable = Math.max(dischargeAvailableInterval, dischargeAvailableLeft); + break; + } + } } -// System.out.println("totalPowerConsumptionProductionFlexibleList = " + totalPowerConsumptionProductionFlexibleList); - - List> energyLevelPercentageForecast = new ArrayList<>(); - List> powerSetpointForecast = new ArrayList<>(); + double dischargeNeeded = + energyLevelPercentageMaximumBattery + - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.max(dischargeAvailable, dischargeNeeded); - // Update energy level percentage forecast starting after current time - for (int i = 1; i < numberOfTimestamps; i++) { - energyLevelPercentageForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = dischargeNeeded; } - // Update power set-point starting from current power limit - for (int i = 0; i < numberOfTimestamps; i++) { - powerSetpointForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); + for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { + energyLevelPredictionList.set( + i, + round( + energyLevelPredictionList.get(i) + changeValue, + batteryPercentageRoundingPrecision)); } - - services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), energyLevelPercentageForecast); - services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), powerSetpointForecast); + } + + if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation error at timestamp = %s", + timestampsMillisList.get(predictionListIndex))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); + break; + } else if (dt > timeoutMillis) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation timed out during power limit optimisation at timestamp = %s", + timestampsMillisList.get(predictionListIndex))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); + break; + } + + energyLevelPredictionMaximum = + Collections.max( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionListSize)); + energyLevelPredictionMinimum = + Collections.min( + energyLevelPredictionList.subList( + startRunningSumIndex, energyLevelPredictionListSize)); + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; } - return batteryEnergyLevelPercentageTargets; - } - - private Map batteryCalculatePowerFlexibleAvailable(List electricityBatteryAssets) { - Map powerFlexibleAvailable = new HashMap<>(); + // System.out.println("energyLevelPredictionListOptimisedLimits = " + + // energyLevelPredictionList + "\n"); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); - boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + boolean useDayAheadTariffs = false; - double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); - double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + if (dayAheadAsset != null) { + useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); + } - if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { - chargePowerMaximum = 0.0; - } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { - chargePowerMaximum = 0.0; + // Get the tariff forecasts from energy optimisation asset for 1 week, timestamps are + // ordered from newest to oldest (descending order) + List> tariffExportDatapoints = + getTariffDatapoints( + energyOptimisationAsset, + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + services); + List> tariffImportDatapoints = + getTariffDatapoints( + energyOptimisationAsset, + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), + services); + + // Overwrite the current tariff forecasts with the day ahead tariff forecasts + if (useDayAheadTariffs) { + List> tariffExportDayAheadAssetDatapoints = + getTariffDayAheadDatapoints( + dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); + List> tariffImportDayAheadAssetDatapoints = + getTariffDayAheadDatapoints( + dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); + + // Combine tariff export forecasts + if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); + + for (ValueDatapoint dp : tariffExportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - if (!allowDischarging || energyLevelPercentageMinimum == null || energyLevelPercentage == null) { - dischargePowerMaximum = 0.0; - } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { - dischargePowerMaximum = 0.0; + for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - powerFlexibleAvailable.put(electricityBatteryAsset.getId(), new ChargeDischarge(chargePowerMaximum, dischargePowerMaximum)); - } - - return powerFlexibleAvailable; - } - - private Map batteryCalculatePowerSetpoints(EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, long powerSetpointTimestampMillisLatest, Map batteryEnergyLevelPercentageTargets, Services services, String logPrefixEnergyOptimisation) { - Map powerFlexibleAvailable = batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); - Map powerSetpointsNew; - - // Check if power net updated since last batteries power set-point update - Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); - long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); - - // Turn off batteries that do not have flexible power available during main power meter disconnect - if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { - powerSetpointsNew = batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); - return powerSetpointsNew; - } - - // Check if power limits are present - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - - // Calculate debounce power limits - Double powerLimitMaximumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); - Double powerLimitMinimumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); - - // Send warning LOG message when too large fluctuation margins are set - if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal != null && powerLimitMaximumVirtual != null && powerLimitMinimumVirtual != null && powerLimitMaximumVirtual < powerLimitMinimumVirtual) { - double diffPowerLimit = powerLimitMaximumProfileTotal - powerLimitMinimumProfileTotal; - double fluctuationMarginSum = Math.abs(powerLimitMaximumProfileTotal - powerLimitMaximumVirtual) + Math.abs(powerLimitMinimumProfileTotal - powerLimitMinimumVirtual); + tariffExportDatapoints = new ArrayList<>(mergedMap.values()); + tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + } - Long powerLimitMaximumProfileTimestampMillis = energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); - String powerLimitMaximumProfileDateTime = ""; + // Combine tariff import forecasts + if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); - if (powerLimitMaximumProfileTimestampMillis != null) { - powerLimitMaximumProfileDateTime = formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); + for (ValueDatapoint dp : tariffImportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); } - LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. The difference between '%s' - %s = %s kW is smaller than the fluctuation margin sum = %s kW for timestamp='%s'", - logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), - EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), diffPowerLimit, fluctuationMarginSum, powerLimitMaximumProfileDateTime)); - } - - // Calculate virtual power consumption - Map powerSetpointsCurrent = new HashMap<>(); + for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - powerSetpointsCurrent.put(electricityBatteryAsset.getId(), powerSetpoint); + tariffImportDatapoints = new ArrayList<>(mergedMap.values()); + tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + } } - double powerSetpointsCurrentSum = powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + // Calculate optimal charge and discharge zone for each day based on tariffs + Map chargeAndDischargeZonesMap = + calculateTariffChargeAndDischargeZones( + tariffImportDatapoints, + tariffExportDatapoints, + BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT); - // Calculate new power set-points without limits - Map powerSetpointsNewWithoutLimits = batteryCalculatePowerSetpointsWithoutLimits(electricityBatteryAssets, batteryEnergyLevelPercentageTargets, energyOptimisationAsset, powerFlexibleAvailable, services); + // Charge zone = 1, discharge zone = -1 + List chargeAndDischargeZonesList = + new ArrayList<>(Collections.nCopies(energyLevelPredictionListSize, 0)); - // Apply power limits and adjust new power set-points - double powerSetpointsNewWithoutLimitsSum = powerSetpointsNewWithoutLimits.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerNetVirtualNew = powerConsumptionVirtual + powerSetpointsNewWithoutLimitsSum; + for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { + long timestampMillis = entry.getKey(); + int value = entry.getValue(); - double powerLimitMaximumFluctuation = calculatePowerFluctuationMargin(energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); - double powerLimitMinimumFluctuation = calculatePowerFluctuationMargin(energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); + int timestampIndex = timestampsMillisList.indexOf(timestampMillis); - if (powerLimitMaximumVirtual != null && (powerNetVirtualNew + powerLimitMaximumFluctuation) > powerLimitMaximumVirtual) { - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerNetVirtualNew, powerLimitMaximumVirtual, electricityBatteryAssets, powerFlexibleAvailable, powerSetpointsNewWithoutLimits); - } else if (powerLimitMinimumVirtual != null && (powerNetVirtualNew - powerLimitMinimumFluctuation) < powerLimitMinimumVirtual) { - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerNetVirtualNew, powerLimitMinimumVirtual, electricityBatteryAssets, powerFlexibleAvailable, powerSetpointsNewWithoutLimits); - } else { - powerSetpointsNew = powerSetpointsNewWithoutLimits; + if (timestampIndex != -1) { + // Add 1 to align list indices + chargeAndDischargeZonesList.set(timestampIndex + 1, value); + } } - // Send warning LOG message on power limit breach - double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; - - if (powerLimitMaximumProfileTotal != null && powerNetNewVirtual > powerLimitMaximumProfileTotal) { - double powerReductionShortage = round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); - LOG.warning(String.format("%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", logPrefixEnergyOptimisation, powerReductionShortage)); - } else if (powerLimitMinimumProfileTotal != null && powerNetNewVirtual < powerLimitMinimumProfileTotal) { - double powerReductionShortage = round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); - LOG.warning(String.format("%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", logPrefixEnergyOptimisation, powerReductionShortage)); + // System.out.println("chargeAndDischargeZonesList = " + + // chargeAndDischargeZonesList); + + // Calculate battery energy level percentage default list + double energyLevelPercentageDefault = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximumBattery, + energyLevelPercentageMinimumBattery, + energyOptimisationAsset); + List energyLevelPercentageDefaultList = + new ArrayList<>( + Collections.nCopies(energyLevelPredictionListSize, energyLevelPercentageDefault)); + + for (int i = 0; i < energyLevelPredictionListSize; i++) { + if (chargeAndDischargeZonesList.get(i) == 1) { + energyLevelPercentageDefaultList.set( + i, Double.valueOf(energyLevelPercentageMaximumBattery)); + } else if (chargeAndDischargeZonesList.get(i) == -1) { + energyLevelPercentageDefaultList.set( + i, Double.valueOf(energyLevelPercentageMinimumBattery)); + } } - return powerSetpointsNew; - } - - private Map batteryCalculatePowerSetpointsOnLimitBreach(double power, double powerLimitVirtual, List electricityBatteryAssets, Map powerFlexibleAvailable, Map powerSetpointsNewWithoutLimits) { - Map powerSetpointsNew = new HashMap<>(); - double powerChangeNeeded = round(power - powerLimitVirtual, 3); - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - - double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; - double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; - double powerSetpointNewWithoutLimits = powerSetpointsNewWithoutLimits.getOrDefault(electricityBatteryAssetId, 0.0); - double chargePowerAvailableTotal = chargePowerAvailable - powerSetpointNewWithoutLimits; - double dischargePowerAvailableTotal = dischargePowerAvailable - powerSetpointNewWithoutLimits; - - double powerSetpointChange = 0.0; - - if (powerChangeNeeded > 0) { - powerSetpointChange = Math.max(-powerChangeNeeded, dischargePowerAvailableTotal); - } else if (powerChangeNeeded < 0) { - powerSetpointChange = Math.min(-powerChangeNeeded, chargePowerAvailableTotal); + // System.out.println("energyLevelPercentageDefaultList = " + + // energyLevelPercentageDefaultList); + + // Optimise forecast based on tariffs + for (int i = 1; + i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionListSize; + i++) { + if (chargeAndDischargeZonesList.get(i) == 1 + && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { + // Interval from charge zone index till end of forecast + Double energyLevelIntervalMaximum = + Collections.max( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + double chargeSpaceOverall = + round( + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum, + batteryPercentageRoundingPrecision); + + if (chargeSpaceOverall > 0) { + double chargeNeeded = + round( + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); + } + + if (chargeAvailableLeft > 0) { + chargeAvailableLeft = + Math.min(Math.min(chargeSpaceOverall, chargeAvailableLeft), chargeNeeded); + chargeSpaceOverall = + round( + chargeSpaceOverall - chargeAvailableLeft, + batteryPercentageRoundingPrecision); + + for (int j = i; j < energyLevelPredictionListSize; j++) { + energyLevelPredictionList.set( + j, + round( + energyLevelPredictionList.get(j) + chargeAvailableLeft, + batteryPercentageRoundingPrecision)); + } + } } - powerChangeNeeded = round(powerChangeNeeded + powerSetpointChange, 3); - - double powerSetpointNew = round(powerSetpointNewWithoutLimits + powerSetpointChange, 3); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } - - return powerSetpointsNew; - } - - private Map batteryCalculatePowerSetpointsWithoutLimits(List electricityBatteryAssets, Map batteryEnergyLevelPercentageTargets, EmsEnergyOptimisationAsset energyOptimisationAsset, Map powerFlexibleAvailable, Services services) { - // Find battery power set-points for a system without power limits - Map powerSetpointsNew = new HashMap<>(); - - long intervalMillis = 15 * 60000; - long endTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long startTimeMillis = endTimeMillis - endTimeMillis % intervalMillis; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + // Get the index of the first maximum + energyLevelIntervalMaximum = + Collections.max( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + int energyLevelIntervalMaximumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); + + if (chargeSpaceOverall <= 0 && i < energyLevelIntervalMaximumIndex) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + double chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); + + int energyLevelIntervalIndex = 0; + dt = 0; + dtStart = services.getTimerService().getCurrentTimeMillis(); + + while (chargeAvailableLeft > 0 + && energyLevelIntervalIndex < energyLevelIntervalMaximumIndex) { + + for (int k = i + 1; k <= energyLevelIntervalMaximumIndex; k++) { + energyLevelIntervalIndex = k; + double chargeNeeded = + round( + energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + + // Only allow moving charging moment into charge zone if no charge is wanted at + // index k + if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { + double energyLevelIntervalMaximum2 = + Collections.max(energyLevelPredictionList.subList(i, k)); + double chargeSpaceInterval = + round( + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2, + batteryPercentageRoundingPrecision); + + if (chargeSpaceInterval > 0) { + double chargeChange = + Math.min( + Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); + chargeAvailableLeft = + round( + chargeAvailableLeft - chargeChange, + batteryPercentageRoundingPrecision); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set( + j, + round( + energyLevelPredictionList.get(j) + chargeChange, + batteryPercentageRoundingPrecision)); + } + break; + } + } + } - // Set initial power set-point of 0 - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + if (dt > timeoutMillis) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation timed out during tariff charge optimisation at timestamp = %s", + timestampsMillisList.get(i))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); + break; + } - if (energyLevelPercentage == null) { - continue; + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; + } } - - double energyCapacity = electricityBatteryAsset.getEnergyCapacity().orElse(0.0); - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - - // Get battery energy level percentage target - Integer energyLevelPercentageTarget = batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); - - if (energyLevelPercentageTarget == null) { - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - energyLevelPercentageTarget = batteryCalculateEnergyLevelPercentageDefault(energyLevelPercentageMaximum, energyLevelPercentageMinimum, energyOptimisationAsset); + } else if (chargeAndDischargeZonesList.get(i) == -1 + && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { + // Interval from discharge zone index till end of forecast + Double energyLevelIntervalMinimum = + Collections.min( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + double dischargeSpaceOverall = + round( + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum, + batteryPercentageRoundingPrecision); + + if (dischargeSpaceOverall < 0) { + double dischargeNeeded = + round( + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); + } + + if (dischargeAvailableLeft < 0) { + dischargeAvailableLeft = + Math.max( + Math.max(dischargeSpaceOverall, dischargeAvailableLeft), dischargeNeeded); + dischargeSpaceOverall = + round( + dischargeSpaceOverall - dischargeAvailableLeft, + batteryPercentageRoundingPrecision); + + for (int j = i; j < energyLevelPredictionListSize; j++) { + energyLevelPredictionList.set( + j, + round( + energyLevelPredictionList.get(j) + dischargeAvailableLeft, + batteryPercentageRoundingPrecision)); + } + } } - // Get battery power set-point target - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> powerSetpointForecastList = services.getAssetPredictedDatapointService().queryDatapoints(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), assetDatapointQueryPredicted); - - // Calculate battery power set-point target - double energyLevelPercentageRounded = round(energyLevelPercentage, 1); - double powerNeeded = round((energyLevelPercentageTarget - energyLevelPercentageRounded) * 0.01 * energyCapacity, 3); - double powerSetpointNeeded = round(powerNeeded * 60 * BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT, 3); - - if (energyLevelPercentageRounded < energyLevelPercentageTarget || (powerSetpointCurrent > 0.0 && energyLevelPercentageRounded < energyLevelPercentageTarget)) { - // Start charging or continue charging - int chargeEfficiencyPercentage = electricityBatteryAsset.getChargeEfficiency().orElse(100); - - double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; - double powerSetpointEfficiency = round(powerSetpointNeeded / (chargeEfficiencyPercentage * 0.01), 3); - double powerSetpointNew = Math.min(powerSetpointEfficiency, chargePowerAvailable); - - if (!powerSetpointForecastList.isEmpty()) { - Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); - - if (powerSetpointForecast != null && powerSetpointForecast > 0) { - powerSetpointNew = Math.min(powerSetpointNew, powerSetpointForecast); + // Get the index of the first minimum + energyLevelIntervalMinimum = + Collections.min( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + int energyLevelIntervalMinimumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); + + if (dischargeSpaceOverall >= 0 && i < energyLevelIntervalMinimumIndex) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + double dischargeAvailableLeft = + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); + + int energyLevelIntervalIndex = 0; + dt = 0; + dtStart = services.getTimerService().getCurrentTimeMillis(); + + while (dischargeAvailableLeft < 0 + && energyLevelIntervalIndex < energyLevelIntervalMinimumIndex) { + + for (int k = i + 1; k <= energyLevelIntervalMinimumIndex; k++) { + energyLevelIntervalIndex = k; + double dischargeNeeded = + round( + energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + + // Only allow moving discharging moment into discharge zone if no discharge is + // wanted at index k + if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { + double energyLevelIntervalMinimum2 = + Collections.min(energyLevelPredictionList.subList(i, k)); + double dischargeSpaceInterval = + round( + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2, + batteryPercentageRoundingPrecision); + + if (dischargeSpaceInterval < 0) { + double dischargeChange = + Math.max( + Math.max(dischargeAvailableLeft, dischargeNeeded), + dischargeSpaceInterval); + dischargeAvailableLeft = + round( + dischargeAvailableLeft - dischargeChange, + batteryPercentageRoundingPrecision); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set( + j, + round( + energyLevelPredictionList.get(j) + dischargeChange, + batteryPercentageRoundingPrecision)); + } + break; } + } } - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (energyLevelPercentageRounded > energyLevelPercentageTarget || (powerSetpointCurrent < 0.0 && energyLevelPercentageRounded > energyLevelPercentageTarget)) { - // Start discharging or continue discharging - int dischargeEfficiencyPercentage = electricityBatteryAsset.getDischargeEfficiency().orElse(100); - - double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; - double powerSetpointEfficiency = round(powerSetpointNeeded / (dischargeEfficiencyPercentage * 0.01), 3); - double powerSetpointNew = Math.max(powerSetpointEfficiency, dischargePowerAvailable); - - if (!powerSetpointForecastList.isEmpty()) { - Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); - - if (powerSetpointForecast != null && powerSetpointForecast < 0) { - powerSetpointNew = Math.max(powerSetpointNew, powerSetpointForecast); - } + if (dt > timeoutMillis) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation timed out during tariff discharge optimisation at timestamp = %s", + timestampsMillisList.get(i))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); + break; } - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; + } } + } + } + } + + // System.out.println("energyLevelPredictionListOptimisedLimitsAndTariffs = " + + // energyLevelPredictionList + "\n"); + + // Get the current energy level percentage target + int batteryEnergyLevelPercentageTarget = (int) Math.round(energyLevelPredictionList.get(1)); + batteryEnergyLevelPercentageTargets.put(batteryAssetId, batteryEnergyLevelPercentageTarget); + + // Calculate energy level percentage change per interval + List percentageChangeBatteryList = new ArrayList<>(); + + for (int i = 0; i < energyLevelPredictionList.size() - 1; i++) { + double percentageChange = + energyLevelPredictionList.get(i + 1) - energyLevelPredictionList.get(i); + percentageChangeBatteryList.add(percentageChange); + } + + // System.out.println("percentageChangeBatteryList = " + + // percentageChangeBatteryList); + + // Calculate total power change for the EMS + List powerChangeTotalList = new ArrayList<>(); + + for (double percentageChange : percentageChangeBatteryList) { + double power = 0; + + if (percentageChange > 0) { + power = + percentageChange * energyCapacityBattery / (intervalHour * chargeEfficiencyBattery); + } else if (percentageChange < 0) { + power = + percentageChange + * energyCapacityBattery + * dischargeEfficiencyBattery + / (10000 * intervalHour); } - return powerSetpointsNew; + powerChangeTotalList.add(round(power, 3)); + } + + // System.out.println("powerChangeTotalList = " + powerChangeTotalList); + + // Update list with power flexible changes + for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { + totalPowerConsumptionProductionFlexibleList.set( + i, totalPowerConsumptionProductionFlexibleList.get(i) - powerChangeTotalList.get(i)); + } + + // System.out.println("totalPowerConsumptionProductionFlexibleList = " + + // totalPowerConsumptionProductionFlexibleList); + + List> energyLevelPercentageForecast = new ArrayList<>(); + List> powerSetpointForecast = new ArrayList<>(); + + // Update energy level percentage forecast starting after current time + for (int i = 1; i < numberOfTimestamps; i++) { + energyLevelPercentageForecast.add( + new ValueDatapoint<>( + timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); + } + + // Update power set-point starting from current power limit + for (int i = 0; i < numberOfTimestamps; i++) { + powerSetpointForecast.add( + new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); + } + + services + .getAssetPredictedDatapointService() + .updateValues( + batteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + energyLevelPercentageForecast); + services + .getAssetPredictedDatapointService() + .updateValues( + batteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), + powerSetpointForecast); } - private void batteryCheckConnection(List electricityBatteryAssets, Services services) { - // This method checks if a battery is connected based on if the 'power' and 'energyLevelPercentage' attributes update within the active time interval - String connected = EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected.toString(); - String disconnected = EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected.toString(); - - long currentTimestampMillis = services.getTimerService().getCurrentTimeMillis(); - long activePeriodMillis = ACTIVE_PERIOD_MINUTES * 60000L; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - - Optional connectionStatusValue = electricityBatteryAsset.getConnectionStatus(); - String connectionStatusPrevious = ""; - - if (connectionStatusValue.isPresent()) { - connectionStatusPrevious = connectionStatusValue.get().toString(); - } - - Double powerBattery = electricityBatteryAsset.getPower().orElse(null); - long powerTimestampMillisBattery = electricityBatteryAsset.getPowerTimestamp().orElse(0L); - - Double energyLevelPercentageBattery = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - long energyLevelPercentageTimestampMillisBattery = electricityBatteryAsset.getEnergyLevelPercentageTimestamp().orElse(0L); - - String connectionStatusCurrent = disconnected; - - if ((currentTimestampMillis - powerTimestampMillisBattery) < activePeriodMillis && powerBattery != null && (currentTimestampMillis - energyLevelPercentageTimestampMillisBattery) < activePeriodMillis && energyLevelPercentageBattery != null) { - connectionStatusCurrent = connected; - } - - if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.equals(disconnected)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(disconnected) && connectionStatusPrevious.equals(connected)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected), getClass().getSimpleName()); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE, null), getClass().getSimpleName()); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER, null), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.isEmpty()) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected), getClass().getSimpleName()); - } else if (connectionStatusCurrent.equals(disconnected) && connectionStatusPrevious.isEmpty()) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.CONNECTION_STATUS, EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected), getClass().getSimpleName()); - } - } + return batteryEnergyLevelPercentageTargets; + } + + private Map batteryCalculatePowerFlexibleAvailable( + List electricityBatteryAssets) { + Map powerFlexibleAvailable = new HashMap<>(); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); + boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + + double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); + double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + + if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { + chargePowerMaximum = 0.0; + } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { + chargePowerMaximum = 0.0; + } + + if (!allowDischarging + || energyLevelPercentageMinimum == null + || energyLevelPercentage == null) { + dischargePowerMaximum = 0.0; + } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { + dischargePowerMaximum = 0.0; + } + + powerFlexibleAvailable.put( + electricityBatteryAsset.getId(), + new ChargeDischarge(chargePowerMaximum, dischargePowerMaximum)); } - private Map batteryCheckPowerSetpointsCurrent(List electricityBatteryAssets, Map powerFlexibleAvailable) { - Map powerSetpointsNew = new HashMap<>(); - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - - if (energyLevelPercentage == null) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - continue; - } - - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; - double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; - - // Set initial power set-point new - double powerSetpointNew = 0.0; - - // Check if power set-point current is within available power limits - if (powerSetpointCurrent > 0.0) { - powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); - } else if (powerSetpointCurrent < 0.0) { - powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); - } - - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } - - return powerSetpointsNew; + return powerFlexibleAvailable; + } + + private Map batteryCalculatePowerSetpoints( + EmsEnergyOptimisationAsset energyOptimisationAsset, + List electricityBatteryAssets, + long powerSetpointTimestampMillisLatest, + Map batteryEnergyLevelPercentageTargets, + Services services, + String logPrefixEnergyOptimisation) { + Map powerFlexibleAvailable = + batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); + Map powerSetpointsNew; + + // Check if power net updated since last batteries power set-point update + Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); + long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); + + // Turn off batteries that do not have flexible power available during main power meter + // disconnect + if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { + powerSetpointsNew = + batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); + return powerSetpointsNew; } - private void batteryCheckSetup(List electricityBatteryAssets) { - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - // Check if the battery is enabled - boolean allowCharging = electricityBatteryAsset.getAllowDischarging().orElse(false); - boolean allowDischarging = electricityBatteryAsset.getAllowCharging().orElse(false); - - if (!allowCharging && !allowDischarging) { - continue; - } - - String logPrefixBattery = String.format("assetType='%s', assetId='%s', assetName='%s'", electricityBatteryAsset.getAssetType(), electricityBatteryAsset.getId(), electricityBatteryAsset.getAssetName()); - - // Check if the following attributes are connected - Map requiredFieldsConnection = new HashMap<>(); - - requiredFieldsConnection.put(EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), electricityBatteryAsset.getEnergyLevelPercentage().orElse(null)); - requiredFieldsConnection.put(EmsElectricityBatteryAsset.POWER.getName(), electricityBatteryAsset.getPower().orElse(null)); - - List missingFieldsConnection = requiredFieldsConnection.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(Map.Entry::getKey) - .toList(); - - if (!missingFieldsConnection.isEmpty()) { - LOG.warning(String.format("%s; Can't use battery for flexible power. The following attributes are not connected: %s", - logPrefixBattery, - String.join(", ", missingFieldsConnection.stream().map(attr -> "'" + attr + "'").toList()) - )); - } - - // Check if the following attributes are set - Map requiredFieldsSetup = new HashMap<>(); - - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - Integer chargeEfficiency = electricityBatteryAsset.getChargeEfficiency().orElse(null); - Integer dischargeEfficiency = electricityBatteryAsset.getDischargeEfficiency().orElse(null); - - requiredFieldsSetup.put(EmsElectricityBatteryAsset.CHARGE_EFFICIENCY.getName(), chargeEfficiency); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName(), electricityBatteryAsset.getChargePowerMaximum().orElse(null)); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.DISCHARGE_EFFICIENCY.getName(), dischargeEfficiency); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName(), electricityBatteryAsset.getDischargePowerMaximum().orElse(null)); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.ENERGY_CAPACITY.getName(), electricityBatteryAsset.getEnergyCapacity().orElse(null)); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), energyLevelPercentageMaximum); - requiredFieldsSetup.put(EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), energyLevelPercentageMinimum); - - List missingFieldsSetup = requiredFieldsSetup.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(Map.Entry::getKey) - .toList(); - - if (!missingFieldsSetup.isEmpty()) { - LOG.warning(String.format("%s; Can't use battery for flexible power. The following attributes are not set: %s", - logPrefixBattery, - String.join(", ", missingFieldsSetup.stream().map(attr -> "'" + attr + "'").toList()) - )); - } + // Check if power limits are present + Double powerLimitMaximumProfileTotal = + energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); + Double powerLimitMinimumProfileTotal = + energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); + + // Calculate debounce power limits + Double powerLimitMaximumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); + Double powerLimitMinimumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); + + // Send warning LOG message when too large fluctuation margins are set + if (powerLimitMaximumProfileTotal != null + && powerLimitMinimumProfileTotal != null + && powerLimitMaximumVirtual != null + && powerLimitMinimumVirtual != null + && powerLimitMaximumVirtual < powerLimitMinimumVirtual) { + double diffPowerLimit = powerLimitMaximumProfileTotal - powerLimitMinimumProfileTotal; + double fluctuationMarginSum = + Math.abs(powerLimitMaximumProfileTotal - powerLimitMaximumVirtual) + + Math.abs(powerLimitMinimumProfileTotal - powerLimitMinimumVirtual); + + Long powerLimitMaximumProfileTimestampMillis = + energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + String powerLimitMaximumProfileDateTime = ""; + + if (powerLimitMaximumProfileTimestampMillis != null) { + powerLimitMaximumProfileDateTime = + formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); + } + + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. The difference between '%s' - %s = %s kW is smaller than the fluctuation margin sum = %s kW for timestamp='%s'", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + diffPowerLimit, + fluctuationMarginSum, + powerLimitMaximumProfileDateTime)); + } - // Check if the following attributes are set correctly - if (energyLevelPercentageMaximum != null && energyLevelPercentageMinimum != null && energyLevelPercentageMaximum <= energyLevelPercentageMinimum) { - LOG.warning(String.format("%s; Can't use battery for flexible power. '%s' = %s%% is smaller than or equal to '%s'= %s%%)", logPrefixBattery, - EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), energyLevelPercentageMaximum, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), energyLevelPercentageMinimum)); - } + // Calculate virtual power consumption + Map powerSetpointsCurrent = new HashMap<>(); - if (chargeEfficiency != null && chargeEfficiency <= 0) { - LOG.warning(String.format("The charge efficiency = %s%% is smaller than or equal to 0", chargeEfficiency)); - } - - if (dischargeEfficiency != null && dischargeEfficiency <= 0) { - LOG.warning(String.format("The discharge efficiency = %s%% is smaller than or equal to 0", dischargeEfficiency)); - } - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + powerSetpointsCurrent.put(electricityBatteryAsset.getId(), powerSetpoint); } - private Map batteryGetEnergyLevelPercentageTargetsCurrent(List electricityBatteryAssets, Services services) { - Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); - - long intervalMillis = 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis + intervalMillis; + double powerSetpointsCurrentSum = + powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + + // Calculate new power set-points without limits + Map powerSetpointsNewWithoutLimits = + batteryCalculatePowerSetpointsWithoutLimits( + electricityBatteryAssets, + batteryEnergyLevelPercentageTargets, + energyOptimisationAsset, + powerFlexibleAvailable, + services); + + // Apply power limits and adjust new power set-points + double powerSetpointsNewWithoutLimitsSum = + powerSetpointsNewWithoutLimits.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerNetVirtualNew = powerConsumptionVirtual + powerSetpointsNewWithoutLimitsSum; + + double powerLimitMaximumFluctuation = + calculatePowerFluctuationMargin( + energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); + double powerLimitMinimumFluctuation = + calculatePowerFluctuationMargin( + energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); + + if (powerLimitMaximumVirtual != null + && (powerNetVirtualNew + powerLimitMaximumFluctuation) > powerLimitMaximumVirtual) { + powerSetpointsNew = + batteryCalculatePowerSetpointsOnLimitBreach( + powerNetVirtualNew, + powerLimitMaximumVirtual, + electricityBatteryAssets, + powerFlexibleAvailable, + powerSetpointsNewWithoutLimits); + } else if (powerLimitMinimumVirtual != null + && (powerNetVirtualNew - powerLimitMinimumFluctuation) < powerLimitMinimumVirtual) { + powerSetpointsNew = + batteryCalculatePowerSetpointsOnLimitBreach( + powerNetVirtualNew, + powerLimitMinimumVirtual, + electricityBatteryAssets, + powerFlexibleAvailable, + powerSetpointsNewWithoutLimits); + } else { + powerSetpointsNew = powerSetpointsNewWithoutLimits; + } - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String batteryAssetId = electricityBatteryAsset.getId(); - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); - List> energyLevelPercentagePredictedCurrent = services.getAssetPredictedDatapointService().queryDatapoints(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), assetDatapointQueryPredicted); + // Send warning LOG message on power limit breach + double powerSetpointsNewSum = + powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; + + if (powerLimitMaximumProfileTotal != null + && powerNetNewVirtual > powerLimitMaximumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", + logPrefixEnergyOptimisation, powerReductionShortage)); + } else if (powerLimitMinimumProfileTotal != null + && powerNetNewVirtual < powerLimitMinimumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); + LOG.warning( + String.format( + "%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", + logPrefixEnergyOptimisation, powerReductionShortage)); + } - Integer energyLevelPercentageTarget = null; + return powerSetpointsNew; + } + + private Map batteryCalculatePowerSetpointsOnLimitBreach( + double power, + double powerLimitVirtual, + List electricityBatteryAssets, + Map powerFlexibleAvailable, + Map powerSetpointsNewWithoutLimits) { + Map powerSetpointsNew = new HashMap<>(); + double powerChangeNeeded = round(power - powerLimitVirtual, 3); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + double powerSetpointNewWithoutLimits = + powerSetpointsNewWithoutLimits.getOrDefault(electricityBatteryAssetId, 0.0); + double chargePowerAvailableTotal = chargePowerAvailable - powerSetpointNewWithoutLimits; + double dischargePowerAvailableTotal = dischargePowerAvailable - powerSetpointNewWithoutLimits; + + double powerSetpointChange = 0.0; + + if (powerChangeNeeded > 0) { + powerSetpointChange = Math.max(-powerChangeNeeded, dischargePowerAvailableTotal); + } else if (powerChangeNeeded < 0) { + powerSetpointChange = Math.min(-powerChangeNeeded, chargePowerAvailableTotal); + } + + powerChangeNeeded = round(powerChangeNeeded + powerSetpointChange, 3); + + double powerSetpointNew = round(powerSetpointNewWithoutLimits + powerSetpointChange, 3); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } - if (!energyLevelPercentagePredictedCurrent.isEmpty()) { - Double value = ((Double) energyLevelPercentagePredictedCurrent.getLast().getValue()); - energyLevelPercentageTarget = (value != null) ? value.intValue() : null; - } + return powerSetpointsNew; + } + + private Map batteryCalculatePowerSetpointsWithoutLimits( + List electricityBatteryAssets, + Map batteryEnergyLevelPercentageTargets, + EmsEnergyOptimisationAsset energyOptimisationAsset, + Map powerFlexibleAvailable, + Services services) { + // Find battery power set-points for a system without power limits + Map powerSetpointsNew = new HashMap<>(); + + long intervalMillis = 15 * 60000; + long endTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeMillis = endTimeMillis - endTimeMillis % intervalMillis; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + // Set initial power set-point of 0 + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + + if (energyLevelPercentage == null) { + continue; + } + + double energyCapacity = electricityBatteryAsset.getEnergyCapacity().orElse(0.0); + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + + // Get battery energy level percentage target + Integer energyLevelPercentageTarget = + batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); + + if (energyLevelPercentageTarget == null) { + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + energyLevelPercentageTarget = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximum, + energyLevelPercentageMinimum, + energyOptimisationAsset); + } + + // Get battery power set-point target + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> powerSetpointForecastList = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), + assetDatapointQueryPredicted); + + // Calculate battery power set-point target + double energyLevelPercentageRounded = round(energyLevelPercentage, 1); + double powerNeeded = + round( + (energyLevelPercentageTarget - energyLevelPercentageRounded) * 0.01 * energyCapacity, + 3); + double powerSetpointNeeded = + round(powerNeeded * 60 * BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT, 3); + + if (energyLevelPercentageRounded < energyLevelPercentageTarget + || (powerSetpointCurrent > 0.0 + && energyLevelPercentageRounded < energyLevelPercentageTarget)) { + // Start charging or continue charging + int chargeEfficiencyPercentage = electricityBatteryAsset.getChargeEfficiency().orElse(100); + + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double powerSetpointEfficiency = + round(powerSetpointNeeded / (chargeEfficiencyPercentage * 0.01), 3); + double powerSetpointNew = Math.min(powerSetpointEfficiency, chargePowerAvailable); + + if (!powerSetpointForecastList.isEmpty()) { + Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); + + if (powerSetpointForecast != null && powerSetpointForecast > 0) { + powerSetpointNew = Math.min(powerSetpointNew, powerSetpointForecast); + } + } - batteryEnergyLevelPercentageTargetsCurrent.put(batteryAssetId, energyLevelPercentageTarget); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } else if (energyLevelPercentageRounded > energyLevelPercentageTarget + || (powerSetpointCurrent < 0.0 + && energyLevelPercentageRounded > energyLevelPercentageTarget)) { + // Start discharging or continue discharging + int dischargeEfficiencyPercentage = + electricityBatteryAsset.getDischargeEfficiency().orElse(100); + + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + double powerSetpointEfficiency = + round(powerSetpointNeeded / (dischargeEfficiencyPercentage * 0.01), 3); + double powerSetpointNew = Math.max(powerSetpointEfficiency, dischargePowerAvailable); + + if (!powerSetpointForecastList.isEmpty()) { + Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); + + if (powerSetpointForecast != null && powerSetpointForecast < 0) { + powerSetpointNew = Math.max(powerSetpointNew, powerSetpointForecast); + } } - return batteryEnergyLevelPercentageTargetsCurrent; + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } } - private List batteryOrder(List electricityBatteryAssets) { - List sorted = new ArrayList<>(electricityBatteryAssets); - - sorted.sort(Comparator - .comparing((EmsElectricityBatteryAsset b) -> b.getEnergyCapacity().isPresent() && b.getDischargePowerMaximum().orElse(0.0) != 0.0) - .reversed() - .thenComparing(b -> b.getDischargePowerMaximum().orElse(0.0) == 0 - ? null - : round(b.getEnergyCapacity().orElse(0.0) / b.getDischargePowerMaximum().orElse(0.0), 2), - Comparator.nullsLast(Comparator.naturalOrder()) - ) - .thenComparing(EmsElectricityBatteryAsset::getId) - ); - - return sorted; + return powerSetpointsNew; + } + + private void batteryCheckConnection( + List electricityBatteryAssets, Services services) { + // This method checks if a battery is connected based on if the 'power' and + // 'energyLevelPercentage' attributes update within the active time interval + String connected = + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected + .toString(); + String disconnected = + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.disconnected + .toString(); + + long currentTimestampMillis = services.getTimerService().getCurrentTimeMillis(); + long activePeriodMillis = ACTIVE_PERIOD_MINUTES * 60000L; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + + Optional + connectionStatusValue = electricityBatteryAsset.getConnectionStatus(); + String connectionStatusPrevious = ""; + + if (connectionStatusValue.isPresent()) { + connectionStatusPrevious = connectionStatusValue.get().toString(); + } + + Double powerBattery = electricityBatteryAsset.getPower().orElse(null); + long powerTimestampMillisBattery = electricityBatteryAsset.getPowerTimestamp().orElse(0L); + + Double energyLevelPercentageBattery = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + long energyLevelPercentageTimestampMillisBattery = + electricityBatteryAsset.getEnergyLevelPercentageTimestamp().orElse(0L); + + String connectionStatusCurrent = disconnected; + + if ((currentTimestampMillis - powerTimestampMillisBattery) < activePeriodMillis + && powerBattery != null + && (currentTimestampMillis - energyLevelPercentageTimestampMillisBattery) + < activePeriodMillis + && energyLevelPercentageBattery != null) { + connectionStatusCurrent = connected; + } + + if (connectionStatusCurrent.equals(connected) + && connectionStatusPrevious.equals(disconnected)) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .connected), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(disconnected) + && connectionStatusPrevious.equals(connected)) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .disconnected), + getClass().getSimpleName()); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE, + null), + getClass().getSimpleName()); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER, null), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(connected) && connectionStatusPrevious.isEmpty()) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .connected), + getClass().getSimpleName()); + } else if (connectionStatusCurrent.equals(disconnected) + && connectionStatusPrevious.isEmpty()) { + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.CONNECTION_STATUS, + EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType + .disconnected), + getClass().getSimpleName()); + } + } + } + + private Map batteryCheckPowerSetpointsCurrent( + List electricityBatteryAssets, + Map powerFlexibleAvailable) { + Map powerSetpointsNew = new HashMap<>(); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + if (energyLevelPercentage == null) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + continue; + } + + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + + // Set initial power set-point new + double powerSetpointNew = 0.0; + + // Check if power set-point current is within available power limits + if (powerSetpointCurrent > 0.0) { + powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); + } else if (powerSetpointCurrent < 0.0) { + powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); + } + + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); } - private void batteryUpdatePowerSetpoints(List electricityBatteryAssets, Map powerSetpointsNew, Services services) { - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double powerSetpointNew = powerSetpointsNew.get(electricityBatteryAssetId); - - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT, powerSetpointNew), getClass().getSimpleName()); - } + return powerSetpointsNew; + } + + private void batteryCheckSetup(List electricityBatteryAssets) { + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + // Check if the battery is enabled + boolean allowCharging = electricityBatteryAsset.getAllowDischarging().orElse(false); + boolean allowDischarging = electricityBatteryAsset.getAllowCharging().orElse(false); + + if (!allowCharging && !allowDischarging) { + continue; + } + + String logPrefixBattery = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + electricityBatteryAsset.getAssetType(), + electricityBatteryAsset.getId(), + electricityBatteryAsset.getAssetName()); + + // Check if the following attributes are connected + Map requiredFieldsConnection = new HashMap<>(); + + requiredFieldsConnection.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null)); + requiredFieldsConnection.put( + EmsElectricityBatteryAsset.POWER.getName(), + electricityBatteryAsset.getPower().orElse(null)); + + List missingFieldsConnection = + requiredFieldsConnection.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .toList(); + + if (!missingFieldsConnection.isEmpty()) { + LOG.warning( + String.format( + "%s; Can't use battery for flexible power. The following attributes are not connected: %s", + logPrefixBattery, + String.join( + ", ", + missingFieldsConnection.stream().map(attr -> "'" + attr + "'").toList()))); + } + + // Check if the following attributes are set + Map requiredFieldsSetup = new HashMap<>(); + + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + Integer chargeEfficiency = electricityBatteryAsset.getChargeEfficiency().orElse(null); + Integer dischargeEfficiency = electricityBatteryAsset.getDischargeEfficiency().orElse(null); + + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.CHARGE_EFFICIENCY.getName(), chargeEfficiency); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName(), + electricityBatteryAsset.getChargePowerMaximum().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.DISCHARGE_EFFICIENCY.getName(), dischargeEfficiency); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName(), + electricityBatteryAsset.getDischargePowerMaximum().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_CAPACITY.getName(), + electricityBatteryAsset.getEnergyCapacity().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), + energyLevelPercentageMaximum); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMinimum); + + List missingFieldsSetup = + requiredFieldsSetup.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .toList(); + + if (!missingFieldsSetup.isEmpty()) { + LOG.warning( + String.format( + "%s; Can't use battery for flexible power. The following attributes are not set: %s", + logPrefixBattery, + String.join( + ", ", missingFieldsSetup.stream().map(attr -> "'" + attr + "'").toList()))); + } + + // Check if the following attributes are set correctly + if (energyLevelPercentageMaximum != null + && energyLevelPercentageMinimum != null + && energyLevelPercentageMaximum <= energyLevelPercentageMinimum) { + LOG.warning( + String.format( + "%s; Can't use battery for flexible power. '%s' = %s%% is smaller than or equal to '%s'= %s%%)", + logPrefixBattery, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), + energyLevelPercentageMaximum, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMinimum)); + } + + if (chargeEfficiency != null && chargeEfficiency <= 0) { + LOG.warning( + String.format( + "The charge efficiency = %s%% is smaller than or equal to 0", chargeEfficiency)); + } + + if (dischargeEfficiency != null && dischargeEfficiency <= 0) { + LOG.warning( + String.format( + "The discharge efficiency = %s%% is smaller than or equal to 0", + dischargeEfficiency)); + } + } + } + + private Map batteryGetEnergyLevelPercentageTargetsCurrent( + List electricityBatteryAssets, Services services) { + Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); + + long intervalMillis = 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis + intervalMillis; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String batteryAssetId = electricityBatteryAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); + List> energyLevelPercentagePredictedCurrent = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + batteryAssetId, + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + assetDatapointQueryPredicted); + + Integer energyLevelPercentageTarget = null; + + if (!energyLevelPercentagePredictedCurrent.isEmpty()) { + Double value = ((Double) energyLevelPercentagePredictedCurrent.getLast().getValue()); + energyLevelPercentageTarget = (value != null) ? value.intValue() : null; + } + + batteryEnergyLevelPercentageTargetsCurrent.put(batteryAssetId, energyLevelPercentageTarget); } - private double calculatePowerFluctuationMargin(EmsEnergyOptimisationAsset energyOptimisationAsset, Double powerLimitProfileTotal, String maxOrMin) { - if (powerLimitProfileTotal == null) { - return 0.0; - } + return batteryEnergyLevelPercentageTargetsCurrent; + } + + private List batteryOrder( + List electricityBatteryAssets) { + List sorted = new ArrayList<>(electricityBatteryAssets); + + sorted.sort( + Comparator.comparing( + (EmsElectricityBatteryAsset b) -> + b.getEnergyCapacity().isPresent() + && b.getDischargePowerMaximum().orElse(0.0) != 0.0) + .reversed() + .thenComparing( + b -> + b.getDischargePowerMaximum().orElse(0.0) == 0 + ? null + : round( + b.getEnergyCapacity().orElse(0.0) + / b.getDischargePowerMaximum().orElse(0.0), + 2), + Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(EmsElectricityBatteryAsset::getId)); + + return sorted; + } + + private void batteryUpdatePowerSetpoints( + List electricityBatteryAssets, + Map powerSetpointsNew, + Services services) { + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double powerSetpointNew = powerSetpointsNew.get(electricityBatteryAssetId); + + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT, + powerSetpointNew), + getClass().getSimpleName()); + } + } + + private double calculatePowerFluctuationMargin( + EmsEnergyOptimisationAsset energyOptimisationAsset, + Double powerLimitProfileTotal, + String maxOrMin) { + if (powerLimitProfileTotal == null) { + return 0.0; + } - boolean isMin = "min".equals(maxOrMin); - String fluctuationMarginAttributeName = POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; + boolean isMin = "min".equals(maxOrMin); + String fluctuationMarginAttributeName = POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; - if (isMin) { - fluctuationMarginAttributeName = POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; - } + if (isMin) { + fluctuationMarginAttributeName = POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; + } - Double powerLimitFluctuationMargin = (Double) energyOptimisationAsset.getAttribute(fluctuationMarginAttributeName) + Double powerLimitFluctuationMargin = + (Double) + energyOptimisationAsset + .getAttribute(fluctuationMarginAttributeName) .flatMap(Attribute::getValue) .orElse(null); - if (powerLimitFluctuationMargin == null) { - powerLimitFluctuationMargin = powerLimitProfileTotal * POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT * 0.01; - } - - return round(Math.abs(powerLimitFluctuationMargin), 3); + if (powerLimitFluctuationMargin == null) { + powerLimitFluctuationMargin = + powerLimitProfileTotal * POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT * 0.01; } - private Double calculatePowerLimitVirtual(EmsEnergyOptimisationAsset energyOptimisationAsset, Double powerLimitProfileTotal, String maxOrMin) { - if (powerLimitProfileTotal == null) { - return null; - } - - double powerLimitFluctuationMargin = calculatePowerFluctuationMargin(energyOptimisationAsset, powerLimitProfileTotal, maxOrMin); - boolean isMin = "min".equals(maxOrMin); + return round(Math.abs(powerLimitFluctuationMargin), 3); + } - double powerLimitVirtual; - - if (isMin) { - powerLimitVirtual = powerLimitProfileTotal + powerLimitFluctuationMargin; - } else { - powerLimitVirtual = powerLimitProfileTotal - powerLimitFluctuationMargin; - } - - return round(powerLimitVirtual, 3); + private Double calculatePowerLimitVirtual( + EmsEnergyOptimisationAsset energyOptimisationAsset, + Double powerLimitProfileTotal, + String maxOrMin) { + if (powerLimitProfileTotal == null) { + return null; } - private Map calculateTariffChargeAndDischargeZones(List> tariffImportDatapoints, List> tariffExportDatapoints, int window) { - Map chargeAndDischargeZonesMap = new HashMap<>(); - - // Return empty map when there is no tariff forecast present - if (tariffImportDatapoints.size() < window || tariffExportDatapoints.size() < window) { - return chargeAndDischargeZonesMap; - } + double powerLimitFluctuationMargin = + calculatePowerFluctuationMargin(energyOptimisationAsset, powerLimitProfileTotal, maxOrMin); + boolean isMin = "min".equals(maxOrMin); - // Find the best import/export tariff window for each day - List> tariffImportMovingAverage = movingAverage(tariffImportDatapoints, window); - List> tariffExportMovingAverage = movingAverage(tariffExportDatapoints, window); - - ZoneId zoneId = ZoneId.systemDefault(); - - Map tariffImportDailyMinimumMap = - IntStream.range(0, tariffImportMovingAverage.size()) - .mapToObj(i -> { - ValueDatapoint dp = tariffImportMovingAverage.get(i); - return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); - }) - .collect(Collectors.toMap( - dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), - Function.identity(), - BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) - )); - - Map tariffExportDailyMinimumMap = - IntStream.range(0, tariffExportMovingAverage.size()) - .mapToObj(i -> { - ValueDatapoint dp = tariffExportMovingAverage.get(i); - return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); - }) - .collect(Collectors.toMap( - dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), - Function.identity(), - BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) - )); - - Map chargeZonesMap = new HashMap<>(); - Map dischargeZonesMap = new HashMap<>(); - - for (Map.Entry entry : tariffImportDailyMinimumMap.entrySet()) { - int startIndex = entry.getValue().index; - int endIndex = startIndex + window; - - for (int i = startIndex; i < endIndex; i++) { - long timeMillis = tariffImportDatapoints.get(i).getTimestamp(); - chargeZonesMap.put(timeMillis, 1); - } - } + double powerLimitVirtual; - for (Map.Entry entry : tariffExportDailyMinimumMap.entrySet()) { - int startIndex = entry.getValue().index; - int endIndex = startIndex + window; + if (isMin) { + powerLimitVirtual = powerLimitProfileTotal + powerLimitFluctuationMargin; + } else { + powerLimitVirtual = powerLimitProfileTotal - powerLimitFluctuationMargin; + } - for (int i = startIndex; i < endIndex; i++) { - long timeMillis = tariffExportDatapoints.get(i).getTimestamp(); - dischargeZonesMap.put(timeMillis, -1); - } - } + return round(powerLimitVirtual, 3); + } - for (Map.Entry entry : chargeZonesMap.entrySet()) { - if (!dischargeZonesMap.containsKey(entry.getKey())) { - chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); - } - } + private Map calculateTariffChargeAndDischargeZones( + List> tariffImportDatapoints, + List> tariffExportDatapoints, + int window) { + Map chargeAndDischargeZonesMap = new HashMap<>(); - for (Map.Entry entry : dischargeZonesMap.entrySet()) { - if (!chargeZonesMap.containsKey(entry.getKey())) { - chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); - } - } + // Return empty map when there is no tariff forecast present + if (tariffImportDatapoints.size() < window || tariffExportDatapoints.size() < window) { + return chargeAndDischargeZonesMap; + } - return chargeAndDischargeZonesMap; + // Find the best import/export tariff window for each day + List> tariffImportMovingAverage = + movingAverage(tariffImportDatapoints, window); + List> tariffExportMovingAverage = + movingAverage(tariffExportDatapoints, window); + + ZoneId zoneId = ZoneId.systemDefault(); + + Map tariffImportDailyMinimumMap = + IntStream.range(0, tariffImportMovingAverage.size()) + .mapToObj( + i -> { + ValueDatapoint dp = tariffImportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect( + Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)))); + + Map tariffExportDailyMinimumMap = + IntStream.range(0, tariffExportMovingAverage.size()) + .mapToObj( + i -> { + ValueDatapoint dp = tariffExportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect( + Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)))); + + Map chargeZonesMap = new HashMap<>(); + Map dischargeZonesMap = new HashMap<>(); + + for (Map.Entry entry : tariffImportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; + + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffImportDatapoints.get(i).getTimestamp(); + chargeZonesMap.put(timeMillis, 1); + } } - private EmsDayAheadAsset getDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - // Find assets in database - List assets = services.getAssetStorageService() - .findAll(new AssetQuery().parents(energyOptimisationAsset.getId()).types(EmsDayAheadAsset.class)) - .stream() - .map(asset -> (EmsDayAheadAsset) asset) - .toList(); - - EmsDayAheadAsset asset = null; - - if (assets.size() == 1) { - asset = assets.getFirst(); - } else if (assets.size() > 1) { - String logPrefixEnergyOptimisation = String.format("assetType='%s', assetId='%s', assetName='%s'", energyOptimisationAsset.getAssetType(), energyOptimisationAsset.getId(), energyOptimisationAsset.getAssetName()); - LOG.warning(String.format("%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", logPrefixEnergyOptimisation, assets.size(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName(), EmsGOPACSAsset.class.getSimpleName())); - } + for (Map.Entry entry : tariffExportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; - return asset; + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffExportDatapoints.get(i).getTimestamp(); + dischargeZonesMap.put(timeMillis, -1); + } } - private List> getTariffDatapoints(EmsEnergyOptimisationAsset energyOptimisationAsset, String attributeName, Services services) { - // Get the start of the day (00:00) in milliseconds - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); - - // Get tariff data-points of yesterday and 1 week into the future - long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; - long endTimeMillis = startOfCurrentDayMillis + 8 * 24 * 60 * 60000; - AssetDatapointAllQuery assetDatapointQueryHistoric = new AssetDatapointAllQuery(startTimeMillis, currentTimeMillis); - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); - List> tariffHistoric = services.getAssetDatapointService().queryDatapoints(energyOptimisationAsset.getId(), attributeName, assetDatapointQueryHistoric); - List> tariffPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), attributeName, assetDatapointQueryPredicted); - - // Combine historic and predicted data-points, timestamps are ordered from newest to oldest (descending order) - List> tariffCombined = new ArrayList<>(tariffPredicted); - tariffCombined.addAll(tariffHistoric); - - return tariffCombined; + for (Map.Entry entry : chargeZonesMap.entrySet()) { + if (!dischargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } } - private List> getTariffDayAheadDatapoints(Asset asset, String attributeName, Services services) { - // Get the start of the day (00:00) in milliseconds - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); - - // Get tariff data-points of yesterday, today and tomorrow - long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; - long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; - AssetDatapointAllQuery assetDatapointQueryHistoric = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> tariffHistoric = services.getAssetDatapointService().queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); - - return tariffHistoric; + for (Map.Entry entry : dischargeZonesMap.entrySet()) { + if (!chargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } } - public static List> intervalAverage(List> dataPoints, long intervalMillis) { - // Map - Map> valuesPerIntervalMap = new HashMap<>(); - - for (ValueDatapoint datapoint : dataPoints) { - long timestampMillis = datapoint.getTimestamp(); - Double value = (Double) datapoint.getValue(); - - if (value != null) { - // Calculate start of 15-minute interval - long intervalStartMillis = timestampMillis - timestampMillis % intervalMillis; - - // Add value to corresponding interval - valuesPerIntervalMap.computeIfAbsent(intervalStartMillis, key -> new ArrayList<>()).add(value); - } - } + return chargeAndDischargeZonesMap; + } + + private EmsDayAheadAsset getDayAheadAsset( + EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { + // Find assets in database + List assets = + services + .getAssetStorageService() + .findAll( + new AssetQuery() + .parents(energyOptimisationAsset.getId()) + .types(EmsDayAheadAsset.class)) + .stream() + .map(asset -> (EmsDayAheadAsset) asset) + .toList(); + + EmsDayAheadAsset asset = null; + + if (assets.size() == 1) { + asset = assets.getFirst(); + } else if (assets.size() > 1) { + String logPrefixEnergyOptimisation = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + energyOptimisationAsset.getAssetType(), + energyOptimisationAsset.getId(), + energyOptimisationAsset.getAssetName()); + LOG.warning( + String.format( + "%s; Found %s '%s' assets; Only 1 '%s' asset is allowed; Remove additional '%s' assets", + logPrefixEnergyOptimisation, + assets.size(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName(), + EmsGOPACSAsset.class.getSimpleName())); + } - // List with average values per interval - List> averageList = new ArrayList<>(); + return asset; + } + + private List> getTariffDatapoints( + EmsEnergyOptimisationAsset energyOptimisationAsset, String attributeName, Services services) { + // Get the start of the day (00:00) in milliseconds + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + ZoneId zoneId = ZoneId.systemDefault(); + LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); + long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + + // Get tariff data-points of yesterday and 1 week into the future + long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; + long endTimeMillis = startOfCurrentDayMillis + 8 * 24 * 60 * 60000; + AssetDatapointAllQuery assetDatapointQueryHistoric = + new AssetDatapointAllQuery(startTimeMillis, currentTimeMillis); + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); + List> tariffHistoric = + services + .getAssetDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), attributeName, assetDatapointQueryHistoric); + List> tariffPredicted = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), attributeName, assetDatapointQueryPredicted); + + // Combine historic and predicted data-points, timestamps are ordered from newest to oldest + // (descending order) + List> tariffCombined = new ArrayList<>(tariffPredicted); + tariffCombined.addAll(tariffHistoric); + + return tariffCombined; + } + + private List> getTariffDayAheadDatapoints( + Asset asset, String attributeName, Services services) { + // Get the start of the day (00:00) in milliseconds + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + ZoneId zoneId = ZoneId.systemDefault(); + LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); + long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + + // Get tariff data-points of yesterday, today and tomorrow + long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; + long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; + AssetDatapointAllQuery assetDatapointQueryHistoric = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> tariffHistoric = + services + .getAssetDatapointService() + .queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); + + return tariffHistoric; + } + + public static List> intervalAverage( + List> dataPoints, long intervalMillis) { + // Map + Map> valuesPerIntervalMap = new HashMap<>(); + + for (ValueDatapoint datapoint : dataPoints) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); + + if (value != null) { + // Calculate start of 15-minute interval + long intervalStartMillis = timestampMillis - timestampMillis % intervalMillis; + + // Add value to corresponding interval + valuesPerIntervalMap + .computeIfAbsent(intervalStartMillis, key -> new ArrayList<>()) + .add(value); + } + } - for (Map.Entry> entry : valuesPerIntervalMap.entrySet()) { - Long intervalStartMillis = entry.getKey(); - List values = entry.getValue(); + // List with average values per interval + List> averageList = new ArrayList<>(); - // Compute the average - double sum = 0; - for (double value : values) { - sum += value; - } - double average = sum / values.size(); + for (Map.Entry> entry : valuesPerIntervalMap.entrySet()) { + Long intervalStartMillis = entry.getKey(); + List values = entry.getValue(); - averageList.add(new ValueDatapoint<>(intervalStartMillis, average)); - } + // Compute the average + double sum = 0; + for (double value : values) { + sum += value; + } + double average = sum / values.size(); - return averageList; + averageList.add(new ValueDatapoint<>(intervalStartMillis, average)); } - private List> intervalInterpolate(List> dataPoints, long startTimeMillis, long endTimeMillis, long intervalMillis) { - List> interpolatedList = new ArrayList<>(); + return averageList; + } - if (dataPoints == null || dataPoints.size() < 2) { - return interpolatedList; - } + private List> intervalInterpolate( + List> dataPoints, + long startTimeMillis, + long endTimeMillis, + long intervalMillis) { + List> interpolatedList = new ArrayList<>(); - int idx1 = 0; + if (dataPoints == null || dataPoints.size() < 2) { + return interpolatedList; + } - for (long intervalTimeMillis = startTimeMillis; intervalTimeMillis <= endTimeMillis; intervalTimeMillis += intervalMillis) { - // Find data-point before and after interval - while (idx1 < (dataPoints.size() - 1) && dataPoints.get(idx1 + 1).getTimestamp() < intervalTimeMillis) { - idx1++; - } + int idx1 = 0; - long timeBeforeMillis = dataPoints.get(idx1).getTimestamp(); - long timeAfterMillis = dataPoints.get(idx1 + 1).getTimestamp(); + for (long intervalTimeMillis = startTimeMillis; + intervalTimeMillis <= endTimeMillis; + intervalTimeMillis += intervalMillis) { + // Find data-point before and after interval + while (idx1 < (dataPoints.size() - 1) + && dataPoints.get(idx1 + 1).getTimestamp() < intervalTimeMillis) { + idx1++; + } - // Interpolate value - if (intervalTimeMillis >= timeBeforeMillis && intervalTimeMillis <= timeAfterMillis) { - double valueBefore = (double) dataPoints.get(idx1).getValue(); - double valueAfter = (double) dataPoints.get(idx1 + 1).getValue(); + long timeBeforeMillis = dataPoints.get(idx1).getTimestamp(); + long timeAfterMillis = dataPoints.get(idx1 + 1).getTimestamp(); - double factor = (double) (intervalTimeMillis - timeBeforeMillis) / (timeAfterMillis - timeBeforeMillis); - double interpolatedValue = valueBefore + factor * (valueAfter - valueBefore); + // Interpolate value + if (intervalTimeMillis >= timeBeforeMillis && intervalTimeMillis <= timeAfterMillis) { + double valueBefore = (double) dataPoints.get(idx1).getValue(); + double valueAfter = (double) dataPoints.get(idx1 + 1).getValue(); - interpolatedList.add(new ValueDatapoint<>(intervalTimeMillis, interpolatedValue)); - } - } + double factor = + (double) (intervalTimeMillis - timeBeforeMillis) / (timeAfterMillis - timeBeforeMillis); + double interpolatedValue = valueBefore + factor * (valueAfter - valueBefore); - return interpolatedList; + interpolatedList.add(new ValueDatapoint<>(intervalTimeMillis, interpolatedValue)); + } } - public List> movingAverage(List> dataPoints, int window) { - List> result = new ArrayList<>(); - if (window <= 0 || dataPoints.size() < window) { - return result; - } + return interpolatedList; + } - double sum = 0.0; + public List> movingAverage(List> dataPoints, int window) { + List> result = new ArrayList<>(); + if (window <= 0 || dataPoints.size() < window) { + return result; + } - for (int i = 0; i < dataPoints.size(); i++) { - Double value = (Double) dataPoints.get(i).getValue(); - sum += value; + double sum = 0.0; - if (i >= window) { - Double valueOld = (Double) dataPoints.get(i - window).getValue(); - sum -= valueOld; - } + for (int i = 0; i < dataPoints.size(); i++) { + Double value = (Double) dataPoints.get(i).getValue(); + sum += value; - if (i >= window - 1) { - double avg = sum / window; + if (i >= window) { + Double valueOld = (Double) dataPoints.get(i - window).getValue(); + sum -= valueOld; + } - ValueDatapoint original = dataPoints.get(i); + if (i >= window - 1) { + double avg = sum / window; - ValueDatapoint averaged = new ValueDatapoint<>(original.getTimestamp(), round(avg, 7)); - result.add(averaged); - } - } + ValueDatapoint original = dataPoints.get(i); - return result; + ValueDatapoint averaged = + new ValueDatapoint<>(original.getTimestamp(), round(avg, 7)); + result.add(averaged); + } } - private double round(double value, int precision) { - double scale = Math.pow(10, precision); - return Math.round(value * scale) / scale; + return result; + } + + private double round(double value, int precision) { + double scale = Math.pow(10, precision); + return Math.round(value * scale) / scale; + } + + private void updateDayAheadAsset( + EmsEnergyOptimisationAsset energyOptimisationAsset, + EmsDayAheadAsset dayAheadAsset, + Services services) { + String dayAheadAssetId = dayAheadAsset.getId(); + String logPrefixDayAhead = + String.format( + "assetType='%s', assetId='%s', assetName='%s'", + dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); + + String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); + + if (collectTimeForecasts.isBlank()) { + LOG.warning( + String.format( + "%s, attributeName='%s'; Set time to collect day ahead forecasts", + logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); } - private void updateDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, EmsDayAheadAsset dayAheadAsset, Services services) { - String dayAheadAssetId = dayAheadAsset.getId(); - String logPrefixDayAhead = String.format("assetType='%s', assetId='%s', assetName='%s'", dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); - - String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); - - if (collectTimeForecasts.isBlank()) { - LOG.warning(String.format("%s, attributeName='%s'; Set time to collect day ahead forecasts", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); - } - - // Parse the time string - LocalTime collectTime = null; - - if (!collectTimeForecasts.isBlank()) { - try { - collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); - } catch (Exception e) { - LOG.warning(String.format("%s, attributeName='%s'; Error while parsing collect time; Exception: %s", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); - } - } - - Long lastUpdateForecastsTimestamp = dayAheadAsset.getLastUpdateForecastsTimestamp().orElse(null); - - if (collectTime == null || lastUpdateForecastsTimestamp == null) { - return; - } - - // Calculate the 15-minute interval for collecting the day ahead tariff forecasts - LocalDate currentDate = LocalDate.now(); - LocalDateTime currentCollectDateTime = LocalDateTime.of(currentDate, collectTime); - long collectTimeStartMillis = currentCollectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long collectTimeEndMillis = collectTimeStartMillis + 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - - String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); - LocalDateTime lastUpdateDateTime = currentCollectDateTime.plusDays(-1); - - if (!lastUpdateForecasts.isEmpty()) { - lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); - } + // Parse the time string + LocalTime collectTime = null; + + if (!collectTimeForecasts.isBlank()) { + try { + collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); + } catch (Exception e) { + LOG.warning( + String.format( + "%s, attributeName='%s'; Error while parsing collect time; Exception: %s", + logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); + } + } - LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + Long lastUpdateForecastsTimestamp = + dayAheadAsset.getLastUpdateForecastsTimestamp().orElse(null); - // Collect the day ahead tariff forecasts at the desired collect time - if (currentTimeMillis >= collectTimeStartMillis && currentTimeMillis < collectTimeEndMillis && collectTimeStartMillis > lastUpdateForecastsTimestamp && currentDate.isEqual(nextUpdateDate)) { - // Create asset datapoint query - LocalDateTime startOfNextDay = currentDate.plusDays(1).atStartOfDay(); - long startTimeMillis = startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + if (collectTime == null || lastUpdateForecastsTimestamp == null) { + return; + } - // Get the tariffs number of data-points from day ahead asset - int tariffExportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); - int tariffImportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); + // Calculate the 15-minute interval for collecting the day ahead tariff forecasts + LocalDate currentDate = LocalDate.now(); + LocalDateTime currentCollectDateTime = LocalDateTime.of(currentDate, collectTime); + long collectTimeStartMillis = + currentCollectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long collectTimeEndMillis = collectTimeStartMillis + 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - // Only update the historic data-point table if there are no day ahead tariffs present in the historic data-point table for current interval - if (tariffExportDayAheadSize == 0) { - List> tariffExport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), assetDatapointQuery); - services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); - } + String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); + LocalDateTime lastUpdateDateTime = currentCollectDateTime.plusDays(-1); - if (tariffImportDayAheadSize == 0) { - List> tariffImport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), assetDatapointQuery); - services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), tariffImport); - } - - // Update the 'last update forecasts' datetime field with current update datetime - String lastUpdateForecastsNew = currentCollectDateTime.toString(); - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(dayAheadAssetId, EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), lastUpdateForecastsNew, collectTimeStartMillis)); - } + if (!lastUpdateForecasts.isEmpty()) { + lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); } - private record ChargeDischarge(double charge, double discharge) { + LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + + // Collect the day ahead tariff forecasts at the desired collect time + if (currentTimeMillis >= collectTimeStartMillis + && currentTimeMillis < collectTimeEndMillis + && collectTimeStartMillis > lastUpdateForecastsTimestamp + && currentDate.isEqual(nextUpdateDate)) { + // Create asset datapoint query + LocalDateTime startOfNextDay = currentDate.plusDays(1).atStartOfDay(); + long startTimeMillis = + startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get the tariffs number of data-points from day ahead asset + int tariffExportDayAheadSize = + services + .getAssetDatapointService() + .queryDatapoints( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), + assetDatapointQuery) + .size(); + int tariffImportDayAheadSize = + services + .getAssetDatapointService() + .queryDatapoints( + dayAheadAssetId, + EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), + assetDatapointQuery) + .size(); + + // Only update the historic data-point table if there are no day ahead tariffs present in the + // historic data-point table for current interval + if (tariffExportDayAheadSize == 0) { + List> tariffExport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); + } + + if (tariffImportDayAheadSize == 0) { + List> tariffImport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), tariffImport); + } + + // Update the 'last update forecasts' datetime field with current update datetime + String lastUpdateForecastsNew = currentCollectDateTime.toString(); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + dayAheadAssetId, + EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), + lastUpdateForecastsNew, + collectTimeStartMillis)); } + } - private record IndexedDatapoint(long timestamp, double value, int index) { - } + private record ChargeDischarge(double charge, double discharge) {} + + private record IndexedDatapoint(long timestamp, double value, int index) {} } diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/OptimisationMethod.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/OptimisationMethod.java index 6f6d0fa..30c485e 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/OptimisationMethod.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/OptimisationMethod.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,12 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.ems.manager.optimisationMethods; import org.openremote.extension.ems.manager.Services; public interface OptimisationMethod { - void execute(String energyOptimisationAssetId, Services services); + void execute(String energyOptimisationAssetId, Services services); } diff --git a/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java index bd1e75d..c98e6da 100644 --- a/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java +++ b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,30 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.manager; +import static java.time.temporal.ChronoUnit.HOURS; +import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; +import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; +import static org.openremote.manager.gateway.GatewayService.isNotForGateway; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import org.apache.camel.builder.RouteBuilder; import org.openremote.container.message.MessageBrokerService; import org.openremote.container.timer.TimerService; @@ -55,929 +72,1369 @@ import org.openremote.model.util.ValueUtil; import org.openremote.model.value.MetaItemType; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static java.time.temporal.ChronoUnit.HOURS; -import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; -import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; -import static org.openremote.manager.gateway.GatewayService.isNotForGateway; - -/** - * Handles optimisation instances for {@link EnergyOptimisationAsset}. - */ +/** Handles optimisation instances for {@link EnergyOptimisationAsset}. */ public class EnergyOptimisationService extends RouteBuilder implements ContainerService { - protected static class OptimisationInstance { - EnergyOptimisationAsset optimisationAsset; - EnergyOptimiser energyOptimiser; - ScheduledFuture optimiserFuture; - - /** - * This keeps track of a theoretical energy level of storage assets. This is used to calculate - * the theoretical un-optimised costs. - */ - Map unoptimisedStorageAssetEnergyLevels = new HashMap<>(); - - public OptimisationInstance(EnergyOptimisationAsset optimisationAsset, EnergyOptimiser energyOptimiser, ScheduledFuture optimiserFuture) { - this.optimisationAsset = optimisationAsset; - this.energyOptimiser = energyOptimiser; - this.optimiserFuture = optimiserFuture; - } - } + protected static class OptimisationInstance { + EnergyOptimisationAsset optimisationAsset; + EnergyOptimiser energyOptimiser; + ScheduledFuture optimiserFuture; - protected static final Logger LOG = Logger.getLogger(EnergyOptimisationService.class.getName()); - protected static final int OPTIMISATION_TIMEOUT_MILLIS = 60000*10; // 10 mins - protected DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC)); - protected TimerService timerService; - protected AssetProcessingService assetProcessingService; - protected AssetStorageService assetStorageService; - protected AssetPredictedDatapointService assetPredictedDatapointService; - protected MessageBrokerService messageBrokerService; - protected ClientEventService clientEventService; - protected GatewayService gatewayService; - protected ExecutorService executorService; - protected ScheduledExecutorService scheduledExecutorService; - protected final Map assetOptimisationInstanceMap = new HashMap<>(); - protected List forceChargeAssetIds = new ArrayList<>(); - - @Override - public void init(Container container) throws Exception { - timerService = container.getService(TimerService.class); - assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); - assetProcessingService = container.getService(AssetProcessingService.class); - assetStorageService = container.getService(AssetStorageService.class); - messageBrokerService = container.getService(MessageBrokerService.class); - clientEventService = container.getService(ClientEventService.class); - gatewayService = container.getService(GatewayService.class); - executorService = container.getExecutor(); - scheduledExecutorService = container.getScheduledExecutor(); - } - - @Override - public void start(Container container) throws Exception { - container.getService(MessageBrokerService.class).getContext().addRoutes(this); - - // Load all enabled optimisation assets and instantiate an optimiser for each - LOG.fine("Loading optimisation assets..."); - - List energyOptimisationAssets = assetStorageService.findAll( - new AssetQuery() - .types(EnergyOptimisationAsset.class) - ) - .stream() + /** + * This keeps track of a theoretical energy level of storage assets. This is used to calculate + * the theoretical un-optimised costs. + */ + Map unoptimisedStorageAssetEnergyLevels = new HashMap<>(); + + public OptimisationInstance( + EnergyOptimisationAsset optimisationAsset, + EnergyOptimiser energyOptimiser, + ScheduledFuture optimiserFuture) { + this.optimisationAsset = optimisationAsset; + this.energyOptimiser = energyOptimiser; + this.optimiserFuture = optimiserFuture; + } + } + + protected static final Logger LOG = Logger.getLogger(EnergyOptimisationService.class.getName()); + protected static final int OPTIMISATION_TIMEOUT_MILLIS = 60000 * 10; // 10 mins + protected DateTimeFormatter formatter = + DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC)); + protected TimerService timerService; + protected AssetProcessingService assetProcessingService; + protected AssetStorageService assetStorageService; + protected AssetPredictedDatapointService assetPredictedDatapointService; + protected MessageBrokerService messageBrokerService; + protected ClientEventService clientEventService; + protected GatewayService gatewayService; + protected ExecutorService executorService; + protected ScheduledExecutorService scheduledExecutorService; + protected final Map assetOptimisationInstanceMap = new HashMap<>(); + protected List forceChargeAssetIds = new ArrayList<>(); + + @Override + public void init(Container container) throws Exception { + timerService = container.getService(TimerService.class); + assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); + assetProcessingService = container.getService(AssetProcessingService.class); + assetStorageService = container.getService(AssetStorageService.class); + messageBrokerService = container.getService(MessageBrokerService.class); + clientEventService = container.getService(ClientEventService.class); + gatewayService = container.getService(GatewayService.class); + executorService = container.getExecutor(); + scheduledExecutorService = container.getScheduledExecutor(); + } + + @Override + public void start(Container container) throws Exception { + container.getService(MessageBrokerService.class).getContext().addRoutes(this); + + // Load all enabled optimisation assets and instantiate an optimiser for each + LOG.fine("Loading optimisation assets..."); + + List energyOptimisationAssets = + assetStorageService.findAll(new AssetQuery().types(EnergyOptimisationAsset.class)).stream() .map(asset -> (EnergyOptimisationAsset) asset) .filter(optimisationAsset -> !optimisationAsset.isOptimisationDisabled().orElse(false)) .toList(); - LOG.fine("Found enabled optimisation asset count = " + energyOptimisationAssets.size()); - - energyOptimisationAssets.forEach(this::startOptimisation); - - clientEventService.addSubscription( - AttributeEvent.class, - null, - this::processAttributeEvent); + LOG.fine("Found enabled optimisation asset count = " + energyOptimisationAssets.size()); + + energyOptimisationAssets.forEach(this::startOptimisation); + + clientEventService.addSubscription(AttributeEvent.class, null, this::processAttributeEvent); + } + + @SuppressWarnings("unchecked") + @Override + public void configure() throws Exception { + from(PERSISTENCE_TOPIC) + .routeId("Persistence-EnergyOptimisation") + .filter(isPersistenceEventForEntityType(EnergyOptimisationAsset.class)) + .filter(isNotForGateway(gatewayService)) + .process( + exchange -> + processAssetChange( + (PersistenceEvent) + exchange.getIn().getBody(PersistenceEvent.class))); + } + + @Override + public void stop(Container container) throws Exception { + new ArrayList<>(assetOptimisationInstanceMap.keySet()).forEach(this::stopOptimisation); + } + + protected void processAssetChange(PersistenceEvent persistenceEvent) { + LOG.fine("Processing optimisation asset change: " + persistenceEvent); + stopOptimisation(persistenceEvent.getEntity().getId()); + + if (persistenceEvent.getCause() != PersistenceEvent.Cause.DELETE) { + if (!persistenceEvent.getEntity().isOptimisationDisabled().orElse(false)) { + startOptimisation(persistenceEvent.getEntity()); + } } + } - @SuppressWarnings("unchecked") - @Override - public void configure() throws Exception { - from(PERSISTENCE_TOPIC) - .routeId("Persistence-EnergyOptimisation") - .filter(isPersistenceEventForEntityType(EnergyOptimisationAsset.class)) - .filter(isNotForGateway(gatewayService)) - .process(exchange -> processAssetChange((PersistenceEvent) exchange.getIn().getBody(PersistenceEvent.class))); - } + protected void processAttributeEvent(AttributeEvent attributeEvent) { + OptimisationInstance optimisationInstance = + assetOptimisationInstanceMap.get(attributeEvent.getId()); - @Override - public void stop(Container container) throws Exception { - new ArrayList<>(assetOptimisationInstanceMap.keySet()) - .forEach(this::stopOptimisation); + if (optimisationInstance != null) { + processOptimisationAssetAttributeEvent(optimisationInstance, attributeEvent); + return; } - protected void processAssetChange(PersistenceEvent persistenceEvent) { - LOG.fine("Processing optimisation asset change: " + persistenceEvent); - stopOptimisation(persistenceEvent.getEntity().getId()); - - if (persistenceEvent.getCause() != PersistenceEvent.Cause.DELETE) { - if (!persistenceEvent.getEntity().isOptimisationDisabled().orElse(false)) { - startOptimisation(persistenceEvent.getEntity()); - } - } + String attributeName = attributeEvent.getName(); + + if ((attributeName.equals(ElectricityChargerAsset.VEHICLE_CONNECTED.getName()) + || attributeName.equals(ElectricVehicleAsset.CHARGER_CONNECTED.getName())) + && (Boolean) attributeEvent.getValue().orElse(false)) { + // Look for forced charge asset + if (forceChargeAssetIds.remove(attributeEvent.getId())) { + LOG.fine( + "Previously force charged asset has now been disconnected so clearing force charge flag: " + + attributeEvent.getId()); + } + return; } - protected void processAttributeEvent(AttributeEvent attributeEvent) { - OptimisationInstance optimisationInstance = assetOptimisationInstanceMap.get(attributeEvent.getId()); - - if (optimisationInstance != null) { - processOptimisationAssetAttributeEvent(optimisationInstance, attributeEvent); - return; - } - - String attributeName = attributeEvent.getName(); + // Check for request to force charge + if (attributeName.equals(ElectricityStorageAsset.FORCE_CHARGE.getName())) { + Asset asset = assetStorageService.find(attributeEvent.getId()); + if (!(asset instanceof ElectricityStorageAsset)) { + LOG.fine( + "Request to force charge asset will be ignored as asset not found or is not of type '" + + ElectricityStorageAsset.class.getSimpleName() + + "': " + + attributeEvent.getId()); + return; + } - if ((attributeName.equals(ElectricityChargerAsset.VEHICLE_CONNECTED.getName()) || attributeName.equals(ElectricVehicleAsset.CHARGER_CONNECTED.getName())) - && (Boolean)attributeEvent.getValue().orElse(false)) { - // Look for forced charge asset - if (forceChargeAssetIds.remove(attributeEvent.getId())) { - LOG.fine("Previously force charged asset has now been disconnected so clearing force charge flag: " + attributeEvent.getId()); - } - return; - } - - // Check for request to force charge - if (attributeName.equals(ElectricityStorageAsset.FORCE_CHARGE.getName())) { - Asset asset = assetStorageService.find(attributeEvent.getId()); - if (!(asset instanceof ElectricityStorageAsset)) { - LOG.fine("Request to force charge asset will be ignored as asset not found or is not of type '" + ElectricityStorageAsset.class.getSimpleName() + "': " + attributeEvent.getId()); - return; - } - - ElectricityStorageAsset storageAsset = (ElectricityStorageAsset) asset; - - if (attributeEvent.getValue().orElse(null) == AttributeExecuteStatus.REQUEST_START) { - - double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); - double maxEnergyLevel = getElectricityStorageAssetEnergyLevelMax(storageAsset); - double currentEnergyLevel = storageAsset.getEnergyLevel().orElse(0d); - LOG.fine("Request to force charge asset '" + attributeEvent.getId() + "': attempting to set powerSetpoint=" + powerImportMax); + ElectricityStorageAsset storageAsset = (ElectricityStorageAsset) asset; - if (forceChargeAssetIds.contains(attributeEvent.getId())) { - LOG.fine("Request to force charge asset will be ignored as force charge already requested for asset: " + storageAsset); - return; - } + if (attributeEvent.getValue().orElse(null) == AttributeExecuteStatus.REQUEST_START) { - if (currentEnergyLevel >= maxEnergyLevel) { - LOG.fine("Request to force charge asset will be ignored as asset is already at or above maxEnergyLevel: " + storageAsset); - return; - } - - forceChargeAssetIds.add(attributeEvent.getId()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(storageAsset.getId(), ElectricityAsset.POWER_SETPOINT, powerImportMax), getClass().getSimpleName()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(storageAsset.getId(), ElectricityStorageAsset.FORCE_CHARGE, AttributeExecuteStatus.RUNNING), getClass().getSimpleName()); - - } else if (attributeEvent.getValue().orElse(null) == AttributeExecuteStatus.REQUEST_CANCEL) { - - if (forceChargeAssetIds.remove(attributeEvent.getId())) { - LOG.info("Request to cancel force charge asset: " + storageAsset.getId()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(storageAsset.getId(), ElectricityAsset.POWER_SETPOINT, 0d), getClass().getSimpleName()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(storageAsset.getId(), ElectricityStorageAsset.FORCE_CHARGE, AttributeExecuteStatus.CANCELLED), getClass().getSimpleName()); - } - } + double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + double maxEnergyLevel = getElectricityStorageAssetEnergyLevelMax(storageAsset); + double currentEnergyLevel = storageAsset.getEnergyLevel().orElse(0d); + LOG.fine( + "Request to force charge asset '" + + attributeEvent.getId() + + "': attempting to set powerSetpoint=" + + powerImportMax); + + if (forceChargeAssetIds.contains(attributeEvent.getId())) { + LOG.fine( + "Request to force charge asset will be ignored as force charge already requested for asset: " + + storageAsset); + return; } - } - - protected double getElectricityStorageAssetEnergyLevelMax(ElectricityStorageAsset asset) { - double energyCapacity = asset.getEnergyCapacity().orElse(0d); - int maxEnergyLevelPercentage = asset.getEnergyLevelPercentageMax().orElse(100); - return energyCapacity * ((1d*maxEnergyLevelPercentage)/100d); - } - - protected synchronized void processOptimisationAssetAttributeEvent(OptimisationInstance optimisationInstance, AttributeEvent attributeEvent) { - if (EnergyOptimisationAsset.FINANCIAL_SAVING.getName().equals(attributeEvent.getName()) - || EnergyOptimisationAsset.CARBON_SAVING.getName().equals(attributeEvent.getName())) { - // These are updated by this service - return; + if (currentEnergyLevel >= maxEnergyLevel) { + LOG.fine( + "Request to force charge asset will be ignored as asset is already at or above maxEnergyLevel: " + + storageAsset); + return; } - - if (attributeEvent.getName().equals(EnergyOptimisationAsset.OPTIMISATION_DISABLED.getName())) { - boolean disabled = (Boolean)attributeEvent.getValue().orElse(false); - if (!disabled && assetOptimisationInstanceMap.containsKey(optimisationInstance.optimisationAsset.getId())) { - // Nothing to do here - return; - } else if (disabled && !assetOptimisationInstanceMap.containsKey(optimisationInstance.optimisationAsset.getId())) { - // Nothing to do here - return; - } + forceChargeAssetIds.add(attributeEvent.getId()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + storageAsset.getId(), ElectricityAsset.POWER_SETPOINT, powerImportMax), + getClass().getSimpleName()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + storageAsset.getId(), + ElectricityStorageAsset.FORCE_CHARGE, + AttributeExecuteStatus.RUNNING), + getClass().getSimpleName()); + + } else if (attributeEvent.getValue().orElse(null) + == AttributeExecuteStatus.REQUEST_CANCEL) { + + if (forceChargeAssetIds.remove(attributeEvent.getId())) { + LOG.info("Request to cancel force charge asset: " + storageAsset.getId()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent(storageAsset.getId(), ElectricityAsset.POWER_SETPOINT, 0d), + getClass().getSimpleName()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + storageAsset.getId(), + ElectricityStorageAsset.FORCE_CHARGE, + AttributeExecuteStatus.CANCELLED), + getClass().getSimpleName()); } + } + } + } - LOG.info("Processing optimisation asset attribute event: " + attributeEvent); - stopOptimisation(attributeEvent.getId()); + protected double getElectricityStorageAssetEnergyLevelMax(ElectricityStorageAsset asset) { + double energyCapacity = asset.getEnergyCapacity().orElse(0d); + int maxEnergyLevelPercentage = asset.getEnergyLevelPercentageMax().orElse(100); + return energyCapacity * ((1d * maxEnergyLevelPercentage) / 100d); + } - // Get latest asset from storage - EnergyOptimisationAsset asset = (EnergyOptimisationAsset) assetStorageService.find(attributeEvent.getId()); + protected synchronized void processOptimisationAssetAttributeEvent( + OptimisationInstance optimisationInstance, AttributeEvent attributeEvent) { - if (asset != null && !asset.isOptimisationDisabled().orElse(false)) { - startOptimisation(asset); - } + if (EnergyOptimisationAsset.FINANCIAL_SAVING.getName().equals(attributeEvent.getName()) + || EnergyOptimisationAsset.CARBON_SAVING.getName().equals(attributeEvent.getName())) { + // These are updated by this service + return; } - protected synchronized void startOptimisation(EnergyOptimisationAsset optimisationAsset) { - LOG.fine("Initialising optimiser for optimisation asset: " + optimisationAsset); - double intervalSize = optimisationAsset.getIntervalSize().orElse(0.25d); - int financialWeighting = optimisationAsset.getFinancialWeighting().orElse(100); + if (attributeEvent.getName().equals(EnergyOptimisationAsset.OPTIMISATION_DISABLED.getName())) { + boolean disabled = (Boolean) attributeEvent.getValue().orElse(false); + if (!disabled + && assetOptimisationInstanceMap.containsKey( + optimisationInstance.optimisationAsset.getId())) { + // Nothing to do here + return; + } else if (disabled + && !assetOptimisationInstanceMap.containsKey( + optimisationInstance.optimisationAsset.getId())) { + // Nothing to do here + return; + } + } - try { - EnergyOptimiser optimiser = new EnergyOptimiser(intervalSize, ((double) financialWeighting) / 100); + LOG.info("Processing optimisation asset attribute event: " + attributeEvent); + stopOptimisation(attributeEvent.getId()); - long periodSeconds = (long) (optimiser.intervalSize * 60 * 60); + // Get latest asset from storage + EnergyOptimisationAsset asset = + (EnergyOptimisationAsset) assetStorageService.find(attributeEvent.getId()); - if (periodSeconds < 300) { - throw new IllegalStateException("Optimiser interval size is too small (minimum is 5 mins) for asset: " + optimisationAsset.getId()); + if (asset != null && !asset.isOptimisationDisabled().orElse(false)) { + startOptimisation(asset); + } + } + + protected synchronized void startOptimisation(EnergyOptimisationAsset optimisationAsset) { + LOG.fine("Initialising optimiser for optimisation asset: " + optimisationAsset); + double intervalSize = optimisationAsset.getIntervalSize().orElse(0.25d); + int financialWeighting = optimisationAsset.getFinancialWeighting().orElse(100); + + try { + EnergyOptimiser optimiser = + new EnergyOptimiser(intervalSize, ((double) financialWeighting) / 100); + + long periodSeconds = (long) (optimiser.intervalSize * 60 * 60); + + if (periodSeconds < 300) { + throw new IllegalStateException( + "Optimiser interval size is too small (minimum is 5 mins) for asset: " + + optimisationAsset.getId()); + } + + long currentMillis = timerService.getCurrentTimeMillis(); + Instant optimisationStartTime = getOptimisationStartTime(currentMillis, periodSeconds); + + // Schedule subsequent runs + long offsetSeconds = (long) (Math.random() * 30) + periodSeconds; + Duration startDuration = + Duration.between( + Instant.ofEpochMilli(currentMillis), + optimisationStartTime.plus(offsetSeconds, ChronoUnit.SECONDS)); + + ScheduledFuture optimisationFuture = + scheduleOptimisation(optimisationAsset.getId(), optimiser, startDuration, periodSeconds); + assetOptimisationInstanceMap.put( + optimisationAsset.getId(), + new OptimisationInstance(optimisationAsset, optimiser, optimisationFuture)); + + // Execute first optimisation at the period that started previous to now + LOG.finest( + getLogPrefix(optimisationAsset.getId()) + + "Running first optimisation for time '" + + formatter.format(optimisationStartTime)); + + executorService.execute( + () -> { + try { + runOptimisation(optimisationAsset.getId(), optimisationStartTime); + } catch (Exception e) { + LOG.log( + Level.SEVERE, + "Failed to run energy optimiser for asset: " + optimisationAsset.getId(), + e); } - - long currentMillis = timerService.getCurrentTimeMillis(); - Instant optimisationStartTime = getOptimisationStartTime(currentMillis, periodSeconds); - - // Schedule subsequent runs - long offsetSeconds = (long) (Math.random() * 30) + periodSeconds; - Duration startDuration = Duration.between(Instant.ofEpochMilli(currentMillis), optimisationStartTime.plus(offsetSeconds, ChronoUnit.SECONDS)); - - ScheduledFuture optimisationFuture = scheduleOptimisation(optimisationAsset.getId(), optimiser, startDuration, periodSeconds); - assetOptimisationInstanceMap.put(optimisationAsset.getId(), new OptimisationInstance(optimisationAsset, optimiser, optimisationFuture)); - - // Execute first optimisation at the period that started previous to now - LOG.finest(getLogPrefix(optimisationAsset.getId()) + "Running first optimisation for time '" + formatter.format(optimisationStartTime)); - - executorService.execute(() -> { - try { - runOptimisation(optimisationAsset.getId(), optimisationStartTime); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to run energy optimiser for asset: " + optimisationAsset.getId(), e); - } - }); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to start energy optimiser for asset: " + optimisationAsset, e); - } + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to start energy optimiser for asset: " + optimisationAsset, e); } + } - protected synchronized void stopOptimisation(String optimisationAssetId) { - OptimisationInstance optimisationInstance = assetOptimisationInstanceMap.remove(optimisationAssetId); - - if (optimisationInstance == null || optimisationInstance.optimiserFuture == null) { - return; - } + protected synchronized void stopOptimisation(String optimisationAssetId) { + OptimisationInstance optimisationInstance = + assetOptimisationInstanceMap.remove(optimisationAssetId); - LOG.fine("Removing optimiser for optimisation asset: " + optimisationAssetId); - optimisationInstance.optimiserFuture.cancel(false); + if (optimisationInstance == null || optimisationInstance.optimiserFuture == null) { + return; } - - /** - * Schedules execution of the optimiser at the start of the interval window with up to 30s of offset randomness - * added so that multiple optimisers don't all run at exactly the same instance; the interval execution times are - * calculated relative to the hour. e.g. a 0.25h intervalSize (15min) would execute at NN:00+offset, NN:15+offset, - * NN:30+offset, NN:45+offset...It is important that intervals coincide with any change in supplier tariff so that - * the optimisation works effectively. - */ - protected ScheduledFuture scheduleOptimisation(String optimisationAssetId, EnergyOptimiser optimiser, Duration startDuration, long periodSeconds) throws IllegalStateException { - - if (optimiser == null) { - throw new IllegalStateException("Optimiser instance not found for asset: " + optimisationAssetId); - } - - return scheduledExecutorService.scheduleAtFixedRate(() -> { - try { - runOptimisation(optimisationAssetId, Instant.ofEpochMilli(timerService.getCurrentTimeMillis()).truncatedTo(ChronoUnit.MINUTES)); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to run energy optimiser for asset: " + optimisationAssetId, e); - } - }, - startDuration.getSeconds(), - periodSeconds, - TimeUnit.SECONDS); + LOG.fine("Removing optimiser for optimisation asset: " + optimisationAssetId); + optimisationInstance.optimiserFuture.cancel(false); + } + + /** + * Schedules execution of the optimiser at the start of the interval window with up to 30s of + * offset randomness added so that multiple optimisers don't all run at exactly the same instance; + * the interval execution times are calculated relative to the hour. e.g. a 0.25h intervalSize + * (15min) would execute at NN:00+offset, NN:15+offset, NN:30+offset, NN:45+offset...It is + * important that intervals coincide with any change in supplier tariff so that the optimisation + * works effectively. + */ + protected ScheduledFuture scheduleOptimisation( + String optimisationAssetId, + EnergyOptimiser optimiser, + Duration startDuration, + long periodSeconds) + throws IllegalStateException { + + if (optimiser == null) { + throw new IllegalStateException( + "Optimiser instance not found for asset: " + optimisationAssetId); } - /** - * Gets the start time of the interval that the currentMillis value is within - */ - protected static Instant getOptimisationStartTime(long currentMillis, long periodSeconds) { - Instant now = Instant.ofEpochMilli(currentMillis); - - Instant optimisationStartTime = now - .truncatedTo(ChronoUnit.DAYS); - - while (optimisationStartTime.isBefore(now)) { - optimisationStartTime = optimisationStartTime.plus(periodSeconds, ChronoUnit.SECONDS); - } - - // Move to one period before - return optimisationStartTime.minus(periodSeconds, ChronoUnit.SECONDS); + return scheduledExecutorService.scheduleAtFixedRate( + () -> { + try { + runOptimisation( + optimisationAssetId, + Instant.ofEpochMilli(timerService.getCurrentTimeMillis()) + .truncatedTo(ChronoUnit.MINUTES)); + } catch (Exception e) { + LOG.log( + Level.SEVERE, + "Failed to run energy optimiser for asset: " + optimisationAssetId, + e); + } + }, + startDuration.getSeconds(), + periodSeconds, + TimeUnit.SECONDS); + } + + /** Gets the start time of the interval that the currentMillis value is within */ + protected static Instant getOptimisationStartTime(long currentMillis, long periodSeconds) { + Instant now = Instant.ofEpochMilli(currentMillis); + + Instant optimisationStartTime = now.truncatedTo(ChronoUnit.DAYS); + + while (optimisationStartTime.isBefore(now)) { + optimisationStartTime = optimisationStartTime.plus(periodSeconds, ChronoUnit.SECONDS); } - protected String getLogPrefix(String optimisationAssetId) { - return "Optimisation '" + optimisationAssetId + "': "; + // Move to one period before + return optimisationStartTime.minus(periodSeconds, ChronoUnit.SECONDS); + } + + protected String getLogPrefix(String optimisationAssetId) { + return "Optimisation '" + optimisationAssetId + "': "; + } + + protected void checkTimeoutAndThrow(String optimisationAssetId, long startTimeMillis) + throws TimeoutException { + long runtime = timerService.getCurrentTimeMillis() - startTimeMillis; + if (runtime > OPTIMISATION_TIMEOUT_MILLIS) { + String logMsg = + getLogPrefix(optimisationAssetId) + + "Optimisation has been running for " + + runtime + + "ms, timeout is at " + + OPTIMISATION_TIMEOUT_MILLIS + + "ms"; + LOG.warning(logMsg); + throw new TimeoutException(logMsg); } - - protected void checkTimeoutAndThrow(String optimisationAssetId, long startTimeMillis) throws TimeoutException { - long runtime = timerService.getCurrentTimeMillis() - startTimeMillis; - if (runtime > OPTIMISATION_TIMEOUT_MILLIS) { - String logMsg = getLogPrefix(optimisationAssetId) + "Optimisation has been running for " + runtime + "ms, timeout is at " + OPTIMISATION_TIMEOUT_MILLIS + "ms"; - LOG.warning(logMsg); - throw new TimeoutException(logMsg); - } + } + + /** + * Runs the optimisation routine for the specified time; it is important that this method does not + * throw an exception as it will cancel the scheduled task thus stopping future optimisations. + */ + protected void runOptimisation(String optimisationAssetId, Instant optimisationTime) + throws Exception { + OptimisationInstance optimisationInstance = + assetOptimisationInstanceMap.get(optimisationAssetId); + + if (optimisationInstance == null) { + return; } - /** - * Runs the optimisation routine for the specified time; it is important that this method does not throw an - * exception as it will cancel the scheduled task thus stopping future optimisations. - */ - protected void runOptimisation(String optimisationAssetId, Instant optimisationTime) throws Exception { - OptimisationInstance optimisationInstance = assetOptimisationInstanceMap.get(optimisationAssetId); - - if (optimisationInstance == null) { - return; - } - - LOG.finest(getLogPrefix(optimisationAssetId) + "Running for time '" + formatter.format(optimisationTime)); - - long startTimeMillis = timerService.getCurrentTimeMillis(); - EnergyOptimiser optimiser = optimisationInstance.energyOptimiser; - int intervalCount = optimiser.get24HourIntervalCount(); - double intervalSize = optimiser.getIntervalSize(); - - LOG.finest(getLogPrefix(optimisationAssetId) + "Fetching child assets of type '" + ElectricitySupplierAsset.class.getSimpleName() + "'"); - - List supplierAssets = assetStorageService.findAll( + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Running for time '" + + formatter.format(optimisationTime)); + + long startTimeMillis = timerService.getCurrentTimeMillis(); + EnergyOptimiser optimiser = optimisationInstance.energyOptimiser; + int intervalCount = optimiser.get24HourIntervalCount(); + double intervalSize = optimiser.getIntervalSize(); + + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Fetching child assets of type '" + + ElectricitySupplierAsset.class.getSimpleName() + + "'"); + + List supplierAssets = + assetStorageService + .findAll( new AssetQuery() .types(ElectricitySupplierAsset.class) .recursive(true) - .parents(optimisationAssetId) - ).stream() + .parents(optimisationAssetId)) + .stream() .filter(asset -> asset.hasAttribute(ElectricitySupplierAsset.TARIFF_IMPORT)) - .map(asset -> (ElectricitySupplierAsset) asset).toList(); + .map(asset -> (ElectricitySupplierAsset) asset) + .toList(); - if (supplierAssets.size() != 1) { - LOG.warning(getLogPrefix(optimisationAssetId) + "Expected exactly one " + ElectricitySupplierAsset.class.getSimpleName() + " asset with a '" + ElectricitySupplierAsset.TARIFF_IMPORT.getName() + "' attribute but found: " + supplierAssets.size()); - return; - } + if (supplierAssets.size() != 1) { + LOG.warning( + getLogPrefix(optimisationAssetId) + + "Expected exactly one " + + ElectricitySupplierAsset.class.getSimpleName() + + " asset with a '" + + ElectricitySupplierAsset.TARIFF_IMPORT.getName() + + "' attribute but found: " + + supplierAssets.size()); + return; + } - double[] powerNets = new double[intervalCount]; - ElectricitySupplierAsset supplierAsset = supplierAssets.getFirst(); + double[] powerNets = new double[intervalCount]; + ElectricitySupplierAsset supplierAsset = supplierAssets.getFirst(); - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Found child asset of type '" + ElectricitySupplierAsset.class.getSimpleName() + "': " + supplierAsset); - } + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Found child asset of type '" + + ElectricitySupplierAsset.class.getSimpleName() + + "': " + + supplierAsset); + } - // Do some basic validation - if (supplierAsset.getTariffImport().isPresent()) { - LOG.warning(getLogPrefix(optimisationAssetId) + ElectricitySupplierAsset.class.getSimpleName() + " asset '" + ElectricitySupplierAsset.TARIFF_IMPORT.getName() + "' attribute has no value"); - } + // Do some basic validation + if (supplierAsset.getTariffImport().isPresent()) { + LOG.warning( + getLogPrefix(optimisationAssetId) + + ElectricitySupplierAsset.class.getSimpleName() + + " asset '" + + ElectricitySupplierAsset.TARIFF_IMPORT.getName() + + "' attribute has no value"); + } - LOG.finest(getLogPrefix(optimisationAssetId) + "Fetching optimisable child assets of type '" + ElectricityStorageAsset.class.getSimpleName() + "'"); + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Fetching optimisable child assets of type '" + + ElectricityStorageAsset.class.getSimpleName() + + "'"); - List optimisableStorageAssets = assetStorageService.findAll( - new AssetQuery() - .recursive(true) - .parents(optimisationAssetId) - .types(ElectricityStorageAsset.class) - .attributes( - new LogicGroup<>( - LogicGroup.Operator.AND, - Collections.singletonList(new LogicGroup<>( - LogicGroup.Operator.OR, - new AttributePredicate(ElectricityStorageAsset.SUPPORTS_IMPORT.getName(), new BooleanPredicate(true)), - new AttributePredicate(ElectricityStorageAsset.SUPPORTS_EXPORT.getName(), new BooleanPredicate(true)) - )), - new AttributePredicate().name(new StringPredicate(ElectricityAsset.POWER_SETPOINT.getName()))) - ) - ) + List optimisableStorageAssets = + assetStorageService + .findAll( + new AssetQuery() + .recursive(true) + .parents(optimisationAssetId) + .types(ElectricityStorageAsset.class) + .attributes( + new LogicGroup<>( + LogicGroup.Operator.AND, + Collections.singletonList( + new LogicGroup<>( + LogicGroup.Operator.OR, + new AttributePredicate( + ElectricityStorageAsset.SUPPORTS_IMPORT.getName(), + new BooleanPredicate(true)), + new AttributePredicate( + ElectricityStorageAsset.SUPPORTS_EXPORT.getName(), + new BooleanPredicate(true)))), + new AttributePredicate() + .name( + new StringPredicate( + ElectricityAsset.POWER_SETPOINT.getName()))))) .stream() - .map(asset -> (ElectricityStorageAsset)asset) + .map(asset -> (ElectricityStorageAsset) asset) .collect(Collectors.toList()); - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); - List finalOptimisableStorageAssets = optimisableStorageAssets; - optimisableStorageAssets = optimisableStorageAssets - .stream() - .filter(asset -> { + List finalOptimisableStorageAssets = optimisableStorageAssets; + optimisableStorageAssets = + optimisableStorageAssets.stream() + .filter( + asset -> { - // Exclude force charged assets (so we don't mess with the setpoint) - if (forceChargeAssetIds.contains(asset.getId())) { - LOG.finest("Optimisable asset was requested to force charge so it won't be optimised: " + asset.getId()); + // Exclude force charged assets (so we don't mess with the setpoint) + if (forceChargeAssetIds.contains(asset.getId())) { + LOG.finest( + "Optimisable asset was requested to force charge so it won't be optimised: " + + asset.getId()); @SuppressWarnings("OptionalGetWithoutIsPresent") - Attribute powerAttribute = asset.getAttribute(ElectricityAsset.POWER).get(); - double[] powerLevels = get24HAttributeValues(asset.getId(), powerAttribute, optimiser.getIntervalSize(), intervalCount, optimisationTime); + Attribute powerAttribute = + asset.getAttribute(ElectricityAsset.POWER).get(); + double[] powerLevels = + get24HAttributeValues( + asset.getId(), + powerAttribute, + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); IntStream.range(0, intervalCount).forEach(i -> powerNets[i] += powerLevels[i]); double currentEnergyLevel = asset.getEnergyLevel().orElse(0d); double maxEnergyLevel = getElectricityStorageAssetEnergyLevelMax(asset); if (currentEnergyLevel >= maxEnergyLevel) { - LOG.info("Force charged asset has reached maxEnergyLevelPercentage so stopping charging: " + asset.getId()); - forceChargeAssetIds.remove(asset.getId()); - assetProcessingService.sendAttributeEvent( - new AttributeEvent(asset.getId(), ElectricityStorageAsset.POWER_SETPOINT, 0d), - getClass().getSimpleName()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(asset.getId(), ElectricityStorageAsset.FORCE_CHARGE, AttributeExecuteStatus.COMPLETED), getClass().getSimpleName()); + LOG.info( + "Force charged asset has reached maxEnergyLevelPercentage so stopping charging: " + + asset.getId()); + forceChargeAssetIds.remove(asset.getId()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + asset.getId(), ElectricityStorageAsset.POWER_SETPOINT, 0d), + getClass().getSimpleName()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + asset.getId(), + ElectricityStorageAsset.FORCE_CHARGE, + AttributeExecuteStatus.COMPLETED), + getClass().getSimpleName()); } return false; - } + } - if (asset instanceof ElectricityChargerAsset) { + if (asset instanceof ElectricityChargerAsset) { // Check if it has a child vehicle asset return finalOptimisableStorageAssets.stream() - .noneMatch(a -> { - if (a instanceof ElectricVehicleAsset && a.getParentId().equals(asset.getId())) { + .noneMatch( + a -> { + if (a instanceof ElectricVehicleAsset + && a.getParentId().equals(asset.getId())) { // Take the lowest power max from vehicle or charger - double vehiclePowerImportMax = a.getPowerImportMax().orElse(Double.MAX_VALUE); - double vehiclePowerExportMax = a.getPowerExportMax().orElse(Double.MAX_VALUE); - double chargerPowerImportMax = asset.getPowerImportMax().orElse(Double.MAX_VALUE); - double chargerPowerExportMax = asset.getPowerExportMax().orElse(Double.MAX_VALUE); - double smallestPowerImportMax = Math.min(vehiclePowerImportMax, chargerPowerImportMax); - double smallestPowerExportMax = Math.min(vehiclePowerExportMax, chargerPowerExportMax); + double vehiclePowerImportMax = + a.getPowerImportMax().orElse(Double.MAX_VALUE); + double vehiclePowerExportMax = + a.getPowerExportMax().orElse(Double.MAX_VALUE); + double chargerPowerImportMax = + asset.getPowerImportMax().orElse(Double.MAX_VALUE); + double chargerPowerExportMax = + asset.getPowerExportMax().orElse(Double.MAX_VALUE); + double smallestPowerImportMax = + Math.min(vehiclePowerImportMax, chargerPowerImportMax); + double smallestPowerExportMax = + Math.min(vehiclePowerExportMax, chargerPowerExportMax); if (smallestPowerImportMax < vehiclePowerImportMax) { - LOG.fine("Reducing vehicle power import max due to connected charger limit: vehicle=" + a.getId() + ", oldPowerImportMax=" + vehiclePowerImportMax + ", newPowerImportMax=" + smallestPowerImportMax); - a.setPowerImportMax(smallestPowerImportMax); + LOG.fine( + "Reducing vehicle power import max due to connected charger limit: vehicle=" + + a.getId() + + ", oldPowerImportMax=" + + vehiclePowerImportMax + + ", newPowerImportMax=" + + smallestPowerImportMax); + a.setPowerImportMax(smallestPowerImportMax); } if (smallestPowerExportMax < vehiclePowerExportMax) { - LOG.fine("Reducing vehicle power Export max due to connected charger limit: vehicle=" + a.getId() + ", oldPowerExportMax=" + vehiclePowerExportMax + ", newPowerExportMax=" + smallestPowerExportMax); - a.setPowerExportMax(smallestPowerExportMax); + LOG.fine( + "Reducing vehicle power Export max due to connected charger limit: vehicle=" + + a.getId() + + ", oldPowerExportMax=" + + vehiclePowerExportMax + + ", newPowerExportMax=" + + smallestPowerExportMax); + a.setPowerExportMax(smallestPowerExportMax); } - LOG.finest("Excluding charger from optimisable assets and child vehicle will be used instead: " + asset.getId()); + LOG.finest( + "Excluding charger from optimisable assets and child vehicle will be used instead: " + + asset.getId()); return true; - } - return false; - }); - } - return true; - }) - .sorted(Comparator.comparingInt(asset -> asset.getEnergyLevelSchedule().map(schedule -> 0).orElse(1))) + } + return false; + }); + } + return true; + }) + .sorted( + Comparator.comparingInt( + asset -> asset.getEnergyLevelSchedule().map(schedule -> 0).orElse(1))) .collect(Collectors.toList()); - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); - - if (optimisableStorageAssets.isEmpty()) { - LOG.warning(getLogPrefix(optimisationAssetId) + "Expected at least one optimisable '" + ElectricityStorageAsset.class.getSimpleName() + " asset with a '" + ElectricityAsset.POWER_SETPOINT.getName() + "' attribute but found none"); - return; - } - - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Found optimisable child assets of type '" + ElectricityStorageAsset.class.getSimpleName() + "': " + optimisableStorageAssets.stream().map(Asset::getId).collect(Collectors.joining(", "))); - } + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + + if (optimisableStorageAssets.isEmpty()) { + LOG.warning( + getLogPrefix(optimisationAssetId) + + "Expected at least one optimisable '" + + ElectricityStorageAsset.class.getSimpleName() + + " asset with a '" + + ElectricityAsset.POWER_SETPOINT.getName() + + "' attribute but found none"); + return; + } - LOG.finest(getLogPrefix(optimisationAssetId) + "Fetching plain consumer and producer child assets of type '" + ElectricityProducerAsset.class.getSimpleName() + "', '" + ElectricityConsumerAsset.class.getSimpleName() + "', '" + ElectricityStorageAsset.class.getSimpleName() + "'"); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Found optimisable child assets of type '" + + ElectricityStorageAsset.class.getSimpleName() + + "': " + + optimisableStorageAssets.stream() + .map(Asset::getId) + .collect(Collectors.joining(", "))); + } - AtomicInteger count = new AtomicInteger(0); - assetStorageService.findAll( + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Fetching plain consumer and producer child assets of type '" + + ElectricityProducerAsset.class.getSimpleName() + + "', '" + + ElectricityConsumerAsset.class.getSimpleName() + + "', '" + + ElectricityStorageAsset.class.getSimpleName() + + "'"); + + AtomicInteger count = new AtomicInteger(0); + assetStorageService + .findAll( new AssetQuery() .recursive(true) .parents(optimisationAssetId) .types(ElectricityConsumerAsset.class, ElectricityProducerAsset.class) - .attributes(new AttributePredicate().name(new StringPredicate(ElectricityAsset.POWER.getName()))) - ) - //.stream() - //.filter(asset -> !(asset instanceof GroupAsset) || isElectricityGroupAsset(asset)) - .forEach(asset -> { - @SuppressWarnings("OptionalGetWithoutIsPresent") - Attribute powerAttribute = asset.getAttribute(ElectricityAsset.POWER).get(); - double[] powerLevels = get24HAttributeValues(asset.getId(), powerAttribute, optimiser.getIntervalSize(), intervalCount, optimisationTime); - IntStream.range(0, intervalCount).forEach(i -> powerNets[i] += powerLevels[i]); - count.incrementAndGet(); + .attributes( + new AttributePredicate() + .name(new StringPredicate(ElectricityAsset.POWER.getName())))) + // .stream() + // .filter(asset -> !(asset instanceof GroupAsset) || isElectricityGroupAsset(asset)) + .forEach( + asset -> { + @SuppressWarnings("OptionalGetWithoutIsPresent") + Attribute powerAttribute = asset.getAttribute(ElectricityAsset.POWER).get(); + double[] powerLevels = + get24HAttributeValues( + asset.getId(), + powerAttribute, + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + IntStream.range(0, intervalCount).forEach(i -> powerNets[i] += powerLevels[i]); + count.incrementAndGet(); }); - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); - // Get power of storage assets that don't support neither import or export (treat them as plain consumers/producers) - List plainStorageAssets = assetStorageService.findAll( + // Get power of storage assets that don't support neither import or export (treat them as plain + // consumers/producers) + List plainStorageAssets = + assetStorageService + .findAll( new AssetQuery() .recursive(true) .parents(optimisationAssetId) .types(ElectricityStorageAsset.class) .attributes( - new AttributePredicate().name(new StringPredicate(ElectricityAsset.POWER.getName())), - new AttributePredicate(ElectricityStorageAsset.SUPPORTS_IMPORT.getName(), new BooleanPredicate(true), true, null), - new AttributePredicate(ElectricityStorageAsset.SUPPORTS_EXPORT.getName(), new BooleanPredicate(true), true, null) - ) - ) + new AttributePredicate() + .name(new StringPredicate(ElectricityAsset.POWER.getName())), + new AttributePredicate( + ElectricityStorageAsset.SUPPORTS_IMPORT.getName(), + new BooleanPredicate(true), + true, + null), + new AttributePredicate( + ElectricityStorageAsset.SUPPORTS_EXPORT.getName(), + new BooleanPredicate(true), + true, + null))) .stream() - .map(asset -> (ElectricityStorageAsset) asset).toList(); - - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + .map(asset -> (ElectricityStorageAsset) asset) + .toList(); - // Exclude chargers with a power value != 0 and a child vehicle with a power value != 0 (avoid double counting - vehicle takes priority) - plainStorageAssets - .stream() - .filter(asset -> { - if (asset instanceof ElectricityChargerAsset) { - // Check if it has a child vehicle asset also check optimisable assets as child vehicle could be in there - return plainStorageAssets.stream() - .noneMatch(a -> { - if (a instanceof ElectricVehicleAsset && a.getParentId().equals(asset.getId())) { - LOG.finest("Excluding charger from plain consumer/producer calculations to avoid double counting power: " + asset.getId()); + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + + // Exclude chargers with a power value != 0 and a child vehicle with a power value != 0 (avoid + // double counting - vehicle takes priority) + plainStorageAssets.stream() + .filter( + asset -> { + if (asset instanceof ElectricityChargerAsset) { + // Check if it has a child vehicle asset also check optimisable assets as child + // vehicle could be in there + return plainStorageAssets.stream() + .noneMatch( + a -> { + if (a instanceof ElectricVehicleAsset + && a.getParentId().equals(asset.getId())) { + LOG.finest( + "Excluding charger from plain consumer/producer calculations to avoid double counting power: " + + asset.getId()); return true; - } - return false; - }) && finalOptimisableStorageAssets.stream() - .noneMatch(a -> { - if (a instanceof ElectricVehicleAsset && a.getParentId().equals(asset.getId())) { - LOG.finest("Excluding charger from plain consumer/producer calculations to avoid double counting power: " + asset.getId()); + } + return false; + }) + && finalOptimisableStorageAssets.stream() + .noneMatch( + a -> { + if (a instanceof ElectricVehicleAsset + && a.getParentId().equals(asset.getId())) { + LOG.finest( + "Excluding charger from plain consumer/producer calculations to avoid double counting power: " + + asset.getId()); return true; - } - return false; - }); - } - return true; + } + return false; + }); + } + return true; }) - .forEach(asset -> { - @SuppressWarnings("OptionalGetWithoutIsPresent") - Attribute powerAttribute = asset.getAttribute(ElectricityAsset.POWER).get(); - double[] powerLevels = get24HAttributeValues(asset.getId(), powerAttribute, optimiser.getIntervalSize(), intervalCount, optimisationTime); - IntStream.range(0, intervalCount).forEach(i -> powerNets[i] += powerLevels[i]); - count.incrementAndGet(); + .forEach( + asset -> { + @SuppressWarnings("OptionalGetWithoutIsPresent") + Attribute powerAttribute = asset.getAttribute(ElectricityAsset.POWER).get(); + double[] powerLevels = + get24HAttributeValues( + asset.getId(), + powerAttribute, + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + IntStream.range(0, intervalCount).forEach(i -> powerNets[i] += powerLevels[i]); + count.incrementAndGet(); }); - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Found plain consumer and producer child assets count=" + count.get()); - LOG.finest("Calculated net power of consumers and producers: " + Arrays.toString(powerNets)); - } - - // Get supplier costs for each interval - double financialWeightingImport = optimiser.getFinancialWeighting(); - double financialWeightingExport = optimiser.getFinancialWeighting(); - - if (financialWeightingImport < 1d && !supplierAsset.getCarbonImport().isPresent()) { - financialWeightingImport = 1d; - } + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Found plain consumer and producer child assets count=" + + count.get()); + LOG.finest("Calculated net power of consumers and producers: " + Arrays.toString(powerNets)); + } - if (financialWeightingExport < 1d && !supplierAsset.getCarbonExport().isPresent()) { - financialWeightingExport = 1d; - } + // Get supplier costs for each interval + double financialWeightingImport = optimiser.getFinancialWeighting(); + double financialWeightingExport = optimiser.getFinancialWeighting(); - double[] costsImport = get24HAttributeValues(supplierAsset.getId(), supplierAsset.getAttribute(ElectricitySupplierAsset.TARIFF_IMPORT).orElse(null), optimiser.getIntervalSize(), intervalCount, optimisationTime); - double[] costsExport = get24HAttributeValues(supplierAsset.getId(), supplierAsset.getAttribute(ElectricitySupplierAsset.TARIFF_EXPORT).orElse(null), optimiser.getIntervalSize(), intervalCount, optimisationTime); + if (financialWeightingImport < 1d && !supplierAsset.getCarbonImport().isPresent()) { + financialWeightingImport = 1d; + } - if (financialWeightingImport < 1d || financialWeightingExport < 1d) { - double[] carbonImport = get24HAttributeValues(supplierAsset.getId(), supplierAsset.getAttribute(ElectricitySupplierAsset.CARBON_IMPORT).orElse(null), optimiser.getIntervalSize(), intervalCount, optimisationTime); - double[] carbonExport = get24HAttributeValues(supplierAsset.getId(), supplierAsset.getAttribute(ElectricitySupplierAsset.CARBON_EXPORT).orElse(null), optimiser.getIntervalSize(), intervalCount, optimisationTime); + if (financialWeightingExport < 1d && !supplierAsset.getCarbonExport().isPresent()) { + financialWeightingExport = 1d; + } - LOG.finest(getLogPrefix(optimisationAssetId) + "Adjusting costs to include some carbon weighting, financialWeightingImport=" + financialWeightingImport + ", financialWeightingExport=" + financialWeightingExport); + double[] costsImport = + get24HAttributeValues( + supplierAsset.getId(), + supplierAsset.getAttribute(ElectricitySupplierAsset.TARIFF_IMPORT).orElse(null), + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + double[] costsExport = + get24HAttributeValues( + supplierAsset.getId(), + supplierAsset.getAttribute(ElectricitySupplierAsset.TARIFF_EXPORT).orElse(null), + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + + if (financialWeightingImport < 1d || financialWeightingExport < 1d) { + double[] carbonImport = + get24HAttributeValues( + supplierAsset.getId(), + supplierAsset.getAttribute(ElectricitySupplierAsset.CARBON_IMPORT).orElse(null), + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + double[] carbonExport = + get24HAttributeValues( + supplierAsset.getId(), + supplierAsset.getAttribute(ElectricitySupplierAsset.CARBON_EXPORT).orElse(null), + optimiser.getIntervalSize(), + intervalCount, + optimisationTime); + + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Adjusting costs to include some carbon weighting, financialWeightingImport=" + + financialWeightingImport + + ", financialWeightingExport=" + + financialWeightingExport); + + for (int i = 0; i < costsImport.length; i++) { + costsImport[i] = + (financialWeightingImport * costsImport[i]) + + ((1 - financialWeightingImport) * carbonImport[i]); + costsExport[i] = + (financialWeightingExport * costsExport[i]) + + ((1 - financialWeightingExport) * carbonExport[i]); + } + } - for (int i = 0; i < costsImport.length; i++) { - costsImport[i] = (financialWeightingImport * costsImport[i]) + ((1-financialWeightingImport) * carbonImport[i]); - costsExport[i] = (financialWeightingExport * costsExport[i]) + ((1-financialWeightingExport) * carbonExport[i]); - } - } + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest( + getLogPrefix(optimisationAssetId) + "Import costs: " + Arrays.toString(costsImport)); + LOG.finest( + getLogPrefix(optimisationAssetId) + "Export costs: " + Arrays.toString(costsExport)); + } + // Savings variables + List obsoleteUnoptimisedAssetIds = + new ArrayList<>(optimisationInstance.unoptimisedStorageAssetEnergyLevels.keySet()); + double unoptimisedPower = powerNets[0]; + double financialCost = 0d; + double carbonCost = 0d; + double unoptimisedFinancialCost = 0d; + double unoptimisedCarbonCost = 0d; + + // Optimise storage assets with priority on storage assets with an energy schedule (already + // sorted above) + double importPowerMax = supplierAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + double exportPowerMax = -1 * supplierAsset.getPowerExportMax().orElse(Double.MAX_VALUE); + double[] importPowerMaxes = new double[intervalCount]; + double[] exportPowerMaxes = new double[intervalCount]; + Arrays.fill(importPowerMaxes, importPowerMax); + Arrays.fill(exportPowerMaxes, exportPowerMax); + long periodSeconds = (long) (optimiser.getIntervalSize() * 60 * 60); + + for (ElectricityStorageAsset storageAsset : optimisableStorageAssets) { + boolean hasSetpoint = storageAsset.hasAttribute(ElectricityStorageAsset.POWER_SETPOINT); + boolean supportsExport = storageAsset.isSupportsExport().orElse(false); + boolean supportsImport = storageAsset.isSupportsImport().orElse(false); + + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Optimising power set points for storage asset: " + + storageAsset); + + if (!supportsExport && !supportsImport) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Storage asset doesn't support import or export: " + + storageAsset.getId()); + continue; + } + + if (!hasSetpoint) { + LOG.info( + getLogPrefix(optimisationAssetId) + + "Storage asset has no '" + + ElectricityStorageAsset.POWER_SETPOINT.getName() + + "' attribute so cannot be controlled: " + + storageAsset.getId()); + continue; + } + + double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); + double energyLevel = Math.min(energyCapacity, storageAsset.getEnergyLevel().orElse(-1d)); + + if (energyCapacity <= 0d || energyLevel < 0) { + LOG.info( + getLogPrefix(optimisationAssetId) + + "Storage asset has no capacity or energy level so cannot import or export energy: " + + storageAsset.getId()); + continue; + } + + double energyLevelMax = + Math.min( + energyCapacity, + ((double) storageAsset.getEnergyLevelPercentageMax().orElse(100) / 100) + * energyCapacity); + double energyLevelMin = + Math.min( + energyCapacity, + ((double) storageAsset.getEnergyLevelPercentageMin().orElse(0) / 100) + * energyCapacity); + double[] energyLevelMins = new double[intervalCount]; + double[] energyLevelMaxs = new double[intervalCount]; + Arrays.fill(energyLevelMins, energyLevelMin); + Arrays.fill(energyLevelMaxs, energyLevelMax); + + // Does the storage support import and have an energy level schedule + Optional energyLevelScheduleOptional = storageAsset.getEnergyLevelSchedule(); + boolean hasEnergyMinRequirement = + energyLevelMin > 0 || energyLevelScheduleOptional.isPresent(); + double powerExportMax = + storageAsset.getPowerExportMax().map(power -> -1 * power).orElse(Double.MIN_VALUE); + double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + int[][] energySchedule = + energyLevelScheduleOptional + .map( + dayArr -> + Arrays.stream(dayArr) + .map( + hourArr -> + Arrays.stream(hourArr).mapToInt(i -> i != null ? i : 0).toArray()) + .toArray(int[][]::new)) + .orElse(null); + + if (energySchedule != null) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Applying energy schedule for storage asset: " + + storageAsset.getId()); + optimiser.applyEnergySchedule( + energyLevelMins, + energyLevelMaxs, + energyCapacity, + energySchedule, + Instant.ofEpochMilli(timerService.getCurrentTimeMillis()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime()); + } + + double maxEnergyLevelMin = Arrays.stream(energyLevelMins).max().orElse(0); + boolean isConnected = storageAssetConnected(storageAsset); + + // TODO: Make these a function of energy level + Function powerImportMaxCalculator = + interval -> interval == 0 && !isConnected ? 0 : powerImportMax; + Function powerExportMaxCalculator = + interval -> interval == 0 && !isConnected ? 0 : powerExportMax; + + if (hasEnergyMinRequirement) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Normalising min energy requirements for storage asset: " + + storageAsset.getId()); + optimiser.normaliseEnergyMinRequirements( + energyLevelMins, powerImportMaxCalculator, powerExportMaxCalculator, energyLevel); if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Import costs: " + Arrays.toString(costsImport)); - LOG.finest(getLogPrefix(optimisationAssetId) + "Export costs: " + Arrays.toString(costsExport)); + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Min energy requirements for storage asset '" + + storageAsset.getId() + + "': " + + Arrays.toString(energyLevelMins)); } - - // Savings variables - List obsoleteUnoptimisedAssetIds = new ArrayList<>(optimisationInstance.unoptimisedStorageAssetEnergyLevels.keySet()); - double unoptimisedPower = powerNets[0]; - double financialCost = 0d; - double carbonCost = 0d; - double unoptimisedFinancialCost = 0d; - double unoptimisedCarbonCost = 0d; - - // Optimise storage assets with priority on storage assets with an energy schedule (already sorted above) - double importPowerMax = supplierAsset.getPowerImportMax().orElse(Double.MAX_VALUE); - double exportPowerMax = -1 * supplierAsset.getPowerExportMax().orElse(Double.MAX_VALUE); - double[] importPowerMaxes = new double[intervalCount]; - double[] exportPowerMaxes = new double[intervalCount]; - Arrays.fill(importPowerMaxes, importPowerMax); - Arrays.fill(exportPowerMaxes, exportPowerMax); - long periodSeconds = (long)(optimiser.getIntervalSize()*60*60); - - for (ElectricityStorageAsset storageAsset : optimisableStorageAssets) { - boolean hasSetpoint = storageAsset.hasAttribute(ElectricityStorageAsset.POWER_SETPOINT); - boolean supportsExport = storageAsset.isSupportsExport().orElse(false); - boolean supportsImport = storageAsset.isSupportsImport().orElse(false); - - checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); - - LOG.finest(getLogPrefix(optimisationAssetId) + "Optimising power set points for storage asset: " + storageAsset); - - if (!supportsExport && !supportsImport) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Storage asset doesn't support import or export: " + storageAsset.getId()); - continue; - } - - if (!hasSetpoint) { - LOG.info(getLogPrefix(optimisationAssetId) + "Storage asset has no '" + ElectricityStorageAsset.POWER_SETPOINT.getName() + "' attribute so cannot be controlled: " + storageAsset.getId()); - continue; - } - - double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); - double energyLevel = Math.min(energyCapacity, storageAsset.getEnergyLevel().orElse(-1d)); - - if (energyCapacity <= 0d || energyLevel < 0) { - LOG.info(getLogPrefix(optimisationAssetId) + "Storage asset has no capacity or energy level so cannot import or export energy: " + storageAsset.getId()); - continue; - } - - double energyLevelMax = Math.min(energyCapacity, ((double) storageAsset.getEnergyLevelPercentageMax().orElse(100) / 100) * energyCapacity); - double energyLevelMin = Math.min(energyCapacity, ((double) storageAsset.getEnergyLevelPercentageMin().orElse(0) / 100) * energyCapacity); - double[] energyLevelMins = new double[intervalCount]; - double[] energyLevelMaxs = new double[intervalCount]; - Arrays.fill(energyLevelMins, energyLevelMin); - Arrays.fill(energyLevelMaxs, energyLevelMax); - - // Does the storage support import and have an energy level schedule - Optional energyLevelScheduleOptional = storageAsset.getEnergyLevelSchedule(); - boolean hasEnergyMinRequirement = energyLevelMin > 0 || energyLevelScheduleOptional.isPresent(); - double powerExportMax = storageAsset.getPowerExportMax().map(power -> -1*power).orElse(Double.MIN_VALUE); - double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); - int[][] energySchedule = energyLevelScheduleOptional.map(dayArr -> Arrays.stream(dayArr).map(hourArr -> Arrays.stream(hourArr).mapToInt(i -> i != null ? i : 0).toArray()).toArray(int[][]::new)).orElse(null); - - if (energySchedule != null) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Applying energy schedule for storage asset: " + storageAsset.getId()); - optimiser.applyEnergySchedule(energyLevelMins, energyLevelMaxs, energyCapacity, energySchedule, Instant.ofEpochMilli(timerService.getCurrentTimeMillis()).atZone(ZoneId.systemDefault()).toLocalDateTime()); - } - - double maxEnergyLevelMin = Arrays.stream(energyLevelMins).max().orElse(0); - boolean isConnected = storageAssetConnected(storageAsset); - - // TODO: Make these a function of energy level - Function powerImportMaxCalculator = interval -> interval == 0 && !isConnected ? 0 : powerImportMax; - Function powerExportMaxCalculator = interval -> interval == 0 && !isConnected ? 0 : powerExportMax; - - if (hasEnergyMinRequirement) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Normalising min energy requirements for storage asset: " + storageAsset.getId()); - optimiser.normaliseEnergyMinRequirements(energyLevelMins, powerImportMaxCalculator, powerExportMaxCalculator, energyLevel); - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Min energy requirements for storage asset '" + storageAsset.getId() + "': " + Arrays.toString(energyLevelMins)); - } + } + + // Calculate the power setpoints for this asset and update power net values for each interval + double[] setpoints = + getStoragePowerSetpoints( + optimisationInstance, + storageAsset, + energyLevelMins, + energyLevelMaxs, + powerNets, + importPowerMaxes, + exportPowerMaxes, + costsImport, + costsExport); + + if (setpoints != null) { + + // Assume these setpoints will be applied so update the power net values with these + for (int i = 0; i < powerNets.length; i++) { + + if (i == 0) { + if (!storageAssetConnected(storageAsset)) { + LOG.finest( + "Optimised storage asset not connected so interval 0 will not be counted or actioned: " + + storageAsset.getId()); + setpoints[i] = 0; + continue; } - // Calculate the power setpoints for this asset and update power net values for each interval - double[] setpoints = getStoragePowerSetpoints(optimisationInstance, storageAsset, energyLevelMins, energyLevelMaxs, powerNets, importPowerMaxes, exportPowerMaxes, costsImport, costsExport); - - if (setpoints != null) { - - // Assume these setpoints will be applied so update the power net values with these - for (int i = 0; i < powerNets.length; i++) { - - if (i == 0) { - if (!storageAssetConnected(storageAsset)) { - LOG.finest("Optimised storage asset not connected so interval 0 will not be counted or actioned: " + storageAsset.getId()); - setpoints[i] = 0; - continue; - } - - // Update savings/cost data with costs specific to this asset - if (setpoints[i] > 0) { - financialCost += storageAsset.getTariffImport().orElse(0d) * setpoints[i] * intervalSize; - } else { - financialCost += storageAsset.getTariffExport().orElse(0d) * -1 * setpoints[i] * intervalSize; - } - } - - powerNets[i] += setpoints[i]; - } - - // Push the setpoints into the prediction service for the storage asset's setpoint attribute and set current setpoint - List> valuesAndTimestamps = IntStream.range(1, setpoints.length).mapToObj(i -> - new ValueDatapoint<>(optimisationTime.plus(periodSeconds * i, ChronoUnit.SECONDS).toEpochMilli(), setpoints[i]) - ).collect(Collectors.toList()); - - assetPredictedDatapointService.updateValues(storageAsset.getId(), ElectricityAsset.POWER_SETPOINT.getName(), valuesAndTimestamps); + // Update savings/cost data with costs specific to this asset + if (setpoints[i] > 0) { + financialCost += + storageAsset.getTariffImport().orElse(0d) * setpoints[i] * intervalSize; + } else { + financialCost += + storageAsset.getTariffExport().orElse(0d) * -1 * setpoints[i] * intervalSize; } + } - assetProcessingService.sendAttributeEvent(new AttributeEvent(storageAsset.getId(), ElectricityAsset.POWER_SETPOINT, setpoints != null ? setpoints[0] : null), getClass().getSimpleName()); - - // Update unoptimised power for this asset - obsoleteUnoptimisedAssetIds.remove(storageAsset.getId()); - double assetUnoptimisedPower = getStorageUnoptimisedImportPower(optimisationInstance, optimisationAssetId, storageAsset, maxEnergyLevelMin, Math.max(0, powerImportMax - unoptimisedPower)); - unoptimisedPower += assetUnoptimisedPower; - unoptimisedFinancialCost += storageAsset.getTariffImport().orElse(0d) * assetUnoptimisedPower * intervalSize; + powerNets[i] += setpoints[i]; } - // Clear out un-optimised data for not found assets - obsoleteUnoptimisedAssetIds.forEach(optimisationInstance.unoptimisedStorageAssetEnergyLevels.keySet()::remove); - - // Calculate and store savings data - carbonCost = (powerNets[0] >= 0 ? supplierAsset.getCarbonImport().orElse(0d) : -1 * supplierAsset.getCarbonExport().orElse(0d)) * powerNets[0] * intervalSize; - financialCost += (powerNets[0] >= 0 ? supplierAsset.getTariffImport().orElse(0d) : -1 * supplierAsset.getTariffExport().orElse(0d)) * powerNets[0] * intervalSize; - unoptimisedCarbonCost = (unoptimisedPower >= 0 ? supplierAsset.getCarbonImport().orElse(0d) : -1 * supplierAsset.getCarbonExport().orElse(0d)) * unoptimisedPower * intervalSize; - unoptimisedFinancialCost += (unoptimisedPower >= 0 ? supplierAsset.getTariffImport().orElse(0d) : -1 * supplierAsset.getTariffExport().orElse(0d)) * unoptimisedPower * intervalSize; - - double financialSaving = unoptimisedFinancialCost - financialCost; - double carbonSaving = unoptimisedCarbonCost - carbonCost; - - LOG.info(getLogPrefix(optimisationAssetId) + "Current interval financial saving = " + financialSaving); - LOG.info(getLogPrefix(optimisationAssetId) + "Current interval carbon saving = " + carbonSaving); - - financialSaving += optimisationInstance.optimisationAsset.getFinancialSaving().orElse(0d); - carbonSaving += optimisationInstance.optimisationAsset.getCarbonSaving().orElse(0d); - - // Update in memory asset - optimisationInstance.optimisationAsset.setFinancialSaving(financialSaving); - optimisationInstance.optimisationAsset.setCarbonSaving(carbonSaving); - - // Push new values into the DB - assetProcessingService.sendAttributeEvent(new AttributeEvent(optimisationAssetId, EnergyOptimisationAsset.FINANCIAL_SAVING, financialSaving), getClass().getSimpleName()); - assetProcessingService.sendAttributeEvent(new AttributeEvent(optimisationAssetId, EnergyOptimisationAsset.CARBON_SAVING, carbonSaving), getClass().getSimpleName()); + // Push the setpoints into the prediction service for the storage asset's setpoint attribute + // and set current setpoint + List> valuesAndTimestamps = + IntStream.range(1, setpoints.length) + .mapToObj( + i -> + new ValueDatapoint<>( + optimisationTime + .plus(periodSeconds * i, ChronoUnit.SECONDS) + .toEpochMilli(), + setpoints[i])) + .collect(Collectors.toList()); + + assetPredictedDatapointService.updateValues( + storageAsset.getId(), ElectricityAsset.POWER_SETPOINT.getName(), valuesAndTimestamps); + } + + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + storageAsset.getId(), + ElectricityAsset.POWER_SETPOINT, + setpoints != null ? setpoints[0] : null), + getClass().getSimpleName()); + + // Update unoptimised power for this asset + obsoleteUnoptimisedAssetIds.remove(storageAsset.getId()); + double assetUnoptimisedPower = + getStorageUnoptimisedImportPower( + optimisationInstance, + optimisationAssetId, + storageAsset, + maxEnergyLevelMin, + Math.max(0, powerImportMax - unoptimisedPower)); + unoptimisedPower += assetUnoptimisedPower; + unoptimisedFinancialCost += + storageAsset.getTariffImport().orElse(0d) * assetUnoptimisedPower * intervalSize; } - protected boolean isElectricityGroupAsset(Asset asset) { - if (!(asset instanceof GroupAsset)) { - return false; - } + // Clear out un-optimised data for not found assets + obsoleteUnoptimisedAssetIds.forEach( + optimisationInstance.unoptimisedStorageAssetEnergyLevels.keySet()::remove); + + // Calculate and store savings data + carbonCost = + (powerNets[0] >= 0 + ? supplierAsset.getCarbonImport().orElse(0d) + : -1 * supplierAsset.getCarbonExport().orElse(0d)) + * powerNets[0] + * intervalSize; + financialCost += + (powerNets[0] >= 0 + ? supplierAsset.getTariffImport().orElse(0d) + : -1 * supplierAsset.getTariffExport().orElse(0d)) + * powerNets[0] + * intervalSize; + unoptimisedCarbonCost = + (unoptimisedPower >= 0 + ? supplierAsset.getCarbonImport().orElse(0d) + : -1 * supplierAsset.getCarbonExport().orElse(0d)) + * unoptimisedPower + * intervalSize; + unoptimisedFinancialCost += + (unoptimisedPower >= 0 + ? supplierAsset.getTariffImport().orElse(0d) + : -1 * supplierAsset.getTariffExport().orElse(0d)) + * unoptimisedPower + * intervalSize; + + double financialSaving = unoptimisedFinancialCost - financialCost; + double carbonSaving = unoptimisedCarbonCost - carbonCost; + + LOG.info( + getLogPrefix(optimisationAssetId) + + "Current interval financial saving = " + + financialSaving); + LOG.info( + getLogPrefix(optimisationAssetId) + "Current interval carbon saving = " + carbonSaving); + + financialSaving += optimisationInstance.optimisationAsset.getFinancialSaving().orElse(0d); + carbonSaving += optimisationInstance.optimisationAsset.getCarbonSaving().orElse(0d); + + // Update in memory asset + optimisationInstance.optimisationAsset.setFinancialSaving(financialSaving); + optimisationInstance.optimisationAsset.setCarbonSaving(carbonSaving); + + // Push new values into the DB + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + optimisationAssetId, EnergyOptimisationAsset.FINANCIAL_SAVING, financialSaving), + getClass().getSimpleName()); + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + optimisationAssetId, EnergyOptimisationAsset.CARBON_SAVING, carbonSaving), + getClass().getSimpleName()); + } + + protected boolean isElectricityGroupAsset(Asset asset) { + if (!(asset instanceof GroupAsset)) { + return false; + } - Class assetClass = ValueUtil - .getAssetDescriptor(((GroupAsset)asset).getChildAssetType().orElse(null)) + Class assetClass = + ValueUtil.getAssetDescriptor(((GroupAsset) asset).getChildAssetType().orElse(null)) .map(AssetDescriptor::getType) .orElse(null); - return assetClass != null && - ElectricityAsset.class.isAssignableFrom(assetClass); - } - - protected double[] get24HAttributeValues(String assetId, Attribute attribute, double intervalSize, int intervalCount, Instant optimisationTime) { + return assetClass != null && ElectricityAsset.class.isAssignableFrom(assetClass); + } - double[] values = new double[intervalCount]; + protected double[] get24HAttributeValues( + String assetId, + Attribute attribute, + double intervalSize, + int intervalCount, + Instant optimisationTime) { - if (attribute == null) { - return values; - } + double[] values = new double[intervalCount]; - AttributeRef ref = new AttributeRef(assetId, attribute.getName()); - - if (attribute.hasMeta(MetaItemType.HAS_PREDICTED_DATA_POINTS)) { - LocalDateTime timestamp = optimisationTime.atZone(ZoneId.systemDefault()).toLocalDateTime(); - List> predictedData = assetPredictedDatapointService.queryDatapoints( - ref.getId(), - ref.getName(), - new AssetDatapointIntervalQuery( - timestamp, - timestamp.plus(24, HOURS).minus((long)(intervalSize * 60), ChronoUnit.MINUTES), - (intervalSize * 60) + " minutes", - AssetDatapointIntervalQuery.Formula.AVG, - true - ) - ); - if (predictedData.size() != values.length) { - LOG.warning("Returned predicted data point count does not match interval count: Ref=" + ref + ", expected=" + values.length + ", actual=" + predictedData.size()); - } else { + if (attribute == null) { + return values; + } - IntStream.range(0, predictedData.size()).forEach(i -> { - if (predictedData.get(i).getValue() != null) { - values[i] = (double) (Object) predictedData.get(i).getValue(); - } else { - // Average previous and next values to fill in gaps (goes up to 5 back and forward) - this fixes - // issues with resolution differences between stored predicted data and optimisation interval - Double previous = null; - Double next = null; - int j = i-1; - while (previous == null && j >= 0) { - previous = (Double) predictedData.get(j).getValue(); - j--; - } - j = i+1; - while (next == null && j < predictedData.size()) { - next = (Double) predictedData.get(j).getValue(); - j++; - } - if (next == null) { - next = previous; - } - if (previous == null) { - previous = next; - } - if (next != null) { - values[i] = (previous + next) / 2; - } + AttributeRef ref = new AttributeRef(assetId, attribute.getName()); + + if (attribute.hasMeta(MetaItemType.HAS_PREDICTED_DATA_POINTS)) { + LocalDateTime timestamp = optimisationTime.atZone(ZoneId.systemDefault()).toLocalDateTime(); + List> predictedData = + assetPredictedDatapointService.queryDatapoints( + ref.getId(), + ref.getName(), + new AssetDatapointIntervalQuery( + timestamp, + timestamp.plus(24, HOURS).minus((long) (intervalSize * 60), ChronoUnit.MINUTES), + (intervalSize * 60) + " minutes", + AssetDatapointIntervalQuery.Formula.AVG, + true)); + if (predictedData.size() != values.length) { + LOG.warning( + "Returned predicted data point count does not match interval count: Ref=" + + ref + + ", expected=" + + values.length + + ", actual=" + + predictedData.size()); + } else { + + IntStream.range(0, predictedData.size()) + .forEach( + i -> { + if (predictedData.get(i).getValue() != null) { + values[i] = (double) (Object) predictedData.get(i).getValue(); + } else { + // Average previous and next values to fill in gaps (goes up to 5 back and + // forward) - this fixes + // issues with resolution differences between stored predicted data and + // optimisation interval + Double previous = null; + Double next = null; + int j = i - 1; + while (previous == null && j >= 0) { + previous = (Double) predictedData.get(j).getValue(); + j--; + } + j = i + 1; + while (next == null && j < predictedData.size()) { + next = (Double) predictedData.get(j).getValue(); + j++; + } + if (next == null) { + next = previous; + } + if (previous == null) { + previous = next; + } + if (next != null) { + values[i] = (previous + next) / 2; } + } }); - } - } - - values[0] = attribute.getValue().orElse(0d); - return values; + } } - /** - * Returns the power setpoint calculator for the specified asset (for producers power demand will only ever be - * negative, for consumers it will only ever be positive and for storage assets that support export (i.e. supports - * producer and consumer) it can be positive or negative at a given interval. For this to work the supplied - * parameters should be updated when the system changes and not replaced so that references maintained by the - * calculator are valid and up to date. - */ - protected double[] getStoragePowerSetpoints( - OptimisationInstance optimisationInstance, - ElectricityStorageAsset storageAsset, - double[] normalisedEnergyLevelMins, - double[] energyLevelMaxs, - double[] powerNets, - double[] importPowerLimits, - double[] exportPowerLimits, - double[] costImports, - double[] costExports) { - - EnergyOptimiser optimiser = optimisationInstance.energyOptimiser; - String optimisationAssetId = optimisationInstance.optimisationAsset.getId(); - int intervalCount = optimiser.get24HourIntervalCount(); - boolean supportsExport = storageAsset.isSupportsExport().orElse(false); - boolean supportsImport = storageAsset.isSupportsImport().orElse(false); - - LOG.finest(getLogPrefix(optimisationAssetId) + "Optimising storage asset: " + storageAsset); - - double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); - double energyLevel = Math.min(energyCapacity, storageAsset.getEnergyLevel().orElse(-1d)); - double powerExportMax = storageAsset.getPowerExportMax().map(power -> -1*power).orElse(Double.MIN_VALUE); - double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); - boolean isConnected = storageAssetConnected(storageAsset); - - // TODO: Make these a function of energy level - Function powerImportMaxCalculator = interval -> interval == 0 && !isConnected ? 0 : powerImportMax; - Function powerExportMaxCalculator = interval -> interval == 0 && !isConnected ? 0 : powerExportMax; - - double[][] exportCostAndPower = null; - double[][] importCostAndPower = null; - double[] powerSetpoints = new double[intervalCount]; - - Function energyLevelCalculator = interval -> - energyLevel + IntStream.range(0, interval).mapToDouble(j -> powerSetpoints[j] * optimiser.getIntervalSize()).sum(); - - // If asset supports exporting energy (V2G, battery storage, etc.) then need to determine if there are - // opportunities to export energy to save/earn, taking into consideration the cost of exporting from this asset - if (supportsExport) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Storage asset supports export so calculating export cost and power levels for each interval: " + storageAsset.getId()); - // Find intervals that save/earn by exporting energy from this storage asset by looking at power levels - BiFunction exportOptimiser = optimiser.getExportOptimiser(powerNets, exportPowerLimits, costImports, costExports, storageAsset.getTariffExport().orElse(0d)); - exportCostAndPower = IntStream.range(0, intervalCount).mapToObj(it -> exportOptimiser.apply(it, powerExportMax)) - .toArray(double[][]::new); - } - - // If asset supports importing energy then need to determine if there are opportunities to import energy to - // save/earn, taking into consideration the cost of importing to this asset, also need to ensure that min - // energy demands are met. - if (supportsImport) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Storage asset supports import so calculating export cost and power levels for each interval: " + storageAsset.getId()); - BiFunction importOptimiser = optimiser.getImportOptimiser(powerNets, importPowerLimits, costImports, costExports, storageAsset.getTariffImport().orElse(0d)); - importCostAndPower = IntStream.range(0, intervalCount).mapToObj(it -> importOptimiser.apply(it, new double[]{0d, powerImportMax})) - .toArray(double[][]::new); - - boolean hasEnergyMinRequirement = Arrays.stream(normalisedEnergyLevelMins).anyMatch(el -> el > 0); - - if (hasEnergyMinRequirement) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Applying imports to achieve min energy level requirements for storage asset: " + storageAsset.getId()); - optimiser.applyEnergyMinImports(importCostAndPower, normalisedEnergyLevelMins, powerSetpoints, energyLevelCalculator, importOptimiser, powerImportMaxCalculator); - if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Setpoints to achieve min energy level requirements for storage asset '" + storageAsset.getId() + "': " + Arrays.toString(powerSetpoints)); - } - } - } - - optimiser.applyEarningOpportunities(importCostAndPower, exportCostAndPower, normalisedEnergyLevelMins, energyLevelMaxs, powerSetpoints, energyLevelCalculator, powerImportMaxCalculator, powerExportMaxCalculator); + values[0] = attribute.getValue().orElse(0d); + return values; + } + + /** + * Returns the power setpoint calculator for the specified asset (for producers power demand will + * only ever be negative, for consumers it will only ever be positive and for storage assets that + * support export (i.e. supports producer and consumer) it can be positive or negative at a given + * interval. For this to work the supplied parameters should be updated when the system changes + * and not replaced so that references maintained by the calculator are valid and up to date. + */ + protected double[] getStoragePowerSetpoints( + OptimisationInstance optimisationInstance, + ElectricityStorageAsset storageAsset, + double[] normalisedEnergyLevelMins, + double[] energyLevelMaxs, + double[] powerNets, + double[] importPowerLimits, + double[] exportPowerLimits, + double[] costImports, + double[] costExports) { + + EnergyOptimiser optimiser = optimisationInstance.energyOptimiser; + String optimisationAssetId = optimisationInstance.optimisationAsset.getId(); + int intervalCount = optimiser.get24HourIntervalCount(); + boolean supportsExport = storageAsset.isSupportsExport().orElse(false); + boolean supportsImport = storageAsset.isSupportsImport().orElse(false); + + LOG.finest(getLogPrefix(optimisationAssetId) + "Optimising storage asset: " + storageAsset); + + double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); + double energyLevel = Math.min(energyCapacity, storageAsset.getEnergyLevel().orElse(-1d)); + double powerExportMax = + storageAsset.getPowerExportMax().map(power -> -1 * power).orElse(Double.MIN_VALUE); + double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + boolean isConnected = storageAssetConnected(storageAsset); + + // TODO: Make these a function of energy level + Function powerImportMaxCalculator = + interval -> interval == 0 && !isConnected ? 0 : powerImportMax; + Function powerExportMaxCalculator = + interval -> interval == 0 && !isConnected ? 0 : powerExportMax; + + double[][] exportCostAndPower = null; + double[][] importCostAndPower = null; + double[] powerSetpoints = new double[intervalCount]; + + Function energyLevelCalculator = + interval -> + energyLevel + + IntStream.range(0, interval) + .mapToDouble(j -> powerSetpoints[j] * optimiser.getIntervalSize()) + .sum(); + + // If asset supports exporting energy (V2G, battery storage, etc.) then need to determine if + // there are + // opportunities to export energy to save/earn, taking into consideration the cost of exporting + // from this asset + if (supportsExport) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Storage asset supports export so calculating export cost and power levels for each interval: " + + storageAsset.getId()); + // Find intervals that save/earn by exporting energy from this storage asset by looking at + // power levels + BiFunction exportOptimiser = + optimiser.getExportOptimiser( + powerNets, + exportPowerLimits, + costImports, + costExports, + storageAsset.getTariffExport().orElse(0d)); + exportCostAndPower = + IntStream.range(0, intervalCount) + .mapToObj(it -> exportOptimiser.apply(it, powerExportMax)) + .toArray(double[][]::new); + } + // If asset supports importing energy then need to determine if there are opportunities to + // import energy to + // save/earn, taking into consideration the cost of importing to this asset, also need to ensure + // that min + // energy demands are met. + if (supportsImport) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Storage asset supports import so calculating export cost and power levels for each interval: " + + storageAsset.getId()); + BiFunction importOptimiser = + optimiser.getImportOptimiser( + powerNets, + importPowerLimits, + costImports, + costExports, + storageAsset.getTariffImport().orElse(0d)); + importCostAndPower = + IntStream.range(0, intervalCount) + .mapToObj(it -> importOptimiser.apply(it, new double[] {0d, powerImportMax})) + .toArray(double[][]::new); + + boolean hasEnergyMinRequirement = + Arrays.stream(normalisedEnergyLevelMins).anyMatch(el -> el > 0); + + if (hasEnergyMinRequirement) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Applying imports to achieve min energy level requirements for storage asset: " + + storageAsset.getId()); + optimiser.applyEnergyMinImports( + importCostAndPower, + normalisedEnergyLevelMins, + powerSetpoints, + energyLevelCalculator, + importOptimiser, + powerImportMaxCalculator); if (LOG.isLoggable(Level.FINEST)) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Calculated earning opportunity power set points for storage asset '" + storageAsset.getId() + "': " + Arrays.toString(powerSetpoints)); + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Setpoints to achieve min energy level requirements for storage asset '" + + storageAsset.getId() + + "': " + + Arrays.toString(powerSetpoints)); } - - return powerSetpoints; + } } - protected boolean storageAssetConnected(ElectricityStorageAsset storageAsset) { - if (storageAsset instanceof ElectricVehicleAsset) { - return ((ElectricVehicleAsset)storageAsset).getChargerConnected().orElse(false); - } - if (storageAsset instanceof ElectricityChargerAsset) { - return ((ElectricityChargerAsset)storageAsset).getVehicleConnected().orElse(false); - } - return true; + optimiser.applyEarningOpportunities( + importCostAndPower, + exportCostAndPower, + normalisedEnergyLevelMins, + energyLevelMaxs, + powerSetpoints, + energyLevelCalculator, + powerImportMaxCalculator, + powerExportMaxCalculator); + + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Calculated earning opportunity power set points for storage asset '" + + storageAsset.getId() + + "': " + + Arrays.toString(powerSetpoints)); } - /** - * Gets the un-optimised import power for the first (current) interval for the supplied storage asset - */ - protected double getStorageUnoptimisedImportPower(OptimisationInstance optimisationInstance, String optimisationAssetId, ElectricityStorageAsset storageAsset, double energyLevelTarget, double remainingPowerCapacity) { - - double intervalSize = optimisationInstance.energyOptimiser.getIntervalSize(); - boolean isConnected = storageAssetConnected(storageAsset); + return powerSetpoints; + } - if (!isConnected) { - optimisationInstance.unoptimisedStorageAssetEnergyLevels.remove(storageAsset.getId()); - return 0; - } - - // Get current energy level from map or directly from the asset - double energyLevel = optimisationInstance.unoptimisedStorageAssetEnergyLevels.get(storageAsset.getId()) != null ? optimisationInstance.unoptimisedStorageAssetEnergyLevels.get(storageAsset.getId()) : storageAsset.getEnergyLevel().orElse(-1d); + protected boolean storageAssetConnected(ElectricityStorageAsset storageAsset) { + if (storageAsset instanceof ElectricVehicleAsset) { + return ((ElectricVehicleAsset) storageAsset).getChargerConnected().orElse(false); + } + if (storageAsset instanceof ElectricityChargerAsset) { + return ((ElectricityChargerAsset) storageAsset).getVehicleConnected().orElse(false); + } + return true; + } + + /** + * Gets the un-optimised import power for the first (current) interval for the supplied storage + * asset + */ + protected double getStorageUnoptimisedImportPower( + OptimisationInstance optimisationInstance, + String optimisationAssetId, + ElectricityStorageAsset storageAsset, + double energyLevelTarget, + double remainingPowerCapacity) { + + double intervalSize = optimisationInstance.energyOptimiser.getIntervalSize(); + boolean isConnected = storageAssetConnected(storageAsset); + + if (!isConnected) { + optimisationInstance.unoptimisedStorageAssetEnergyLevels.remove(storageAsset.getId()); + return 0; + } - if (energyLevel < 0) { - LOG.finest(getLogPrefix(optimisationAssetId) + "Storage asset has no energy level so cannot calculate un-optimised power demand: " + storageAsset.getId()); - return 0; - } + // Get current energy level from map or directly from the asset + double energyLevel = + optimisationInstance.unoptimisedStorageAssetEnergyLevels.get(storageAsset.getId()) != null + ? optimisationInstance.unoptimisedStorageAssetEnergyLevels.get(storageAsset.getId()) + : storageAsset.getEnergyLevel().orElse(-1d); + + if (energyLevel < 0) { + LOG.finest( + getLogPrefix(optimisationAssetId) + + "Storage asset has no energy level so cannot calculate un-optimised power demand: " + + storageAsset.getId()); + return 0; + } - // Calculate power - double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); - double remainingEnergy = Math.max(0, energyLevelTarget - energyLevel); - double toFillPower = remainingEnergy / intervalSize; - double power = Math.min(Math.min(toFillPower, powerImportMax), remainingPowerCapacity); + // Calculate power + double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + double remainingEnergy = Math.max(0, energyLevelTarget - energyLevel); + double toFillPower = remainingEnergy / intervalSize; + double power = Math.min(Math.min(toFillPower, powerImportMax), remainingPowerCapacity); - // Update energy level using previous interval power - energyLevel += power * intervalSize; - optimisationInstance.unoptimisedStorageAssetEnergyLevels.put(storageAsset.getId(), energyLevel); + // Update energy level using previous interval power + energyLevel += power * intervalSize; + optimisationInstance.unoptimisedStorageAssetEnergyLevels.put(storageAsset.getId(), energyLevel); - return power; - } + return power; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java index 920e4d6..1dcbef2 100644 --- a/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java +++ b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,11 +12,13 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.manager; -import org.openremote.model.util.Pair; +import static org.openremote.extension.energy.manager.EnergyOptimisationService.LOG; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -36,751 +35,972 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.openremote.extension.energy.manager.EnergyOptimisationService.LOG; +import org.openremote.model.util.Pair; public class EnergyOptimiser { - protected double intervalSize; - protected double financialWeighting; + protected double intervalSize; + protected double financialWeighting; - /** - * 24 divided by intervalSize must be a whole number - */ - public EnergyOptimiser(double intervalSize, double financialWeighting) throws IllegalArgumentException { - if ((24d / intervalSize) != (int) (24d / intervalSize)) { - throw new IllegalArgumentException("24 divided by intervalSizeHours must be whole number"); - } - this.intervalSize = intervalSize; - this.financialWeighting = Math.max(0, Math.min(1d, financialWeighting)); + /** 24 divided by intervalSize must be a whole number */ + public EnergyOptimiser(double intervalSize, double financialWeighting) + throws IllegalArgumentException { + if ((24d / intervalSize) != (int) (24d / intervalSize)) { + throw new IllegalArgumentException("24 divided by intervalSizeHours must be whole number"); } - - public double getIntervalSize() { - return intervalSize; + this.intervalSize = intervalSize; + this.financialWeighting = Math.max(0, Math.min(1d, financialWeighting)); + } + + public double getIntervalSize() { + return intervalSize; + } + + public double getFinancialWeighting() { + return financialWeighting; + } + + public int get24HourIntervalCount() { + return (int) (24d / intervalSize); + } + + // /** + // * Function to calculate the power requirements for the given storage asset at each interval + // in the window + // * based on the provided power demand requirements and potential cost saving at each + // interval so for each interval + // * a an array of [powerDemand, gridCost] is required. For a given interval a positive value + // means importing + // * power and negative means exporting power: + // *

      + // *
    • powerDemand = The net demand of suppliers - consumers - higher priority supplier + // capabilities
    • + // *
    • gridCost = Grid cost / kWh (positive means expense, negative means income)
    • + // *
    + // */ + // // TODO: powerImportMax should be a function of energy level + // public Function getStoragePowerCalculator(ElectricityStorageAsset + // storageAsset) { + // + // return (powerExportsAndSavings) -> { + // + // double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); + // double storedEnergy = storageAsset.getEnergyLevel().orElse(0d); + // double tariffExport = storageAsset.getTariffExport().orElse(0d); + // double tariffImport = storageAsset.getTariffExport().orElse(0d); + // double carbonExport = storageAsset.getCarbonExport().orElse(0d); + // double carbonImport = storageAsset.getCarbonImport().orElse(0d); + // double costExport = financialWeighting * tariffExport + (1d-financialWeighting) * + // carbonExport; + // double costImport = financialWeighting * tariffImport + (1d-financialWeighting) * + // carbonImport; + // double powerEfficiencyImport = storageAsset.getEfficiencyImport().orElse(100); + // double powerEfficiencyExport = storageAsset.getEfficiencyExport().orElse(100); + // double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + // double powerExportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); + // int energyLevelMax = storageAsset.getEnergyLevelPercentageMax().orElse(100); + // + // + // double[] powerImport = new double[intervalCount]; + // double[] powerExport = new double[intervalCount]; + // double[] energyLevel = new double[intervalCount]; + // Arrays.fill(energyLevel, storedEnergy); + // + //// // Calculate total energy requirement + //// double totalEnergyRequired = Arrays.stream(powerExportsAndSavings) + //// .map(interval -> interval[2] * intervalSize) + //// .reduce(0d, Double::sum); + // + //// // If we have enough energy stored then just consume that (no need to import more) + //// if (totalEnergyRequired < storedEnergy) { + //// Arrays.fill(powerImport, 0d); + //// return powerImport; + //// } + // + // /* Look for storage import income opportunities (i.e. when grid pays us to import) + // */ + // + // // Order intervals based on potential income [gridCost+storageImportCost] < 0 + // (smallest first) + // Integer[] indexesSortedIncome = IntStream.range(0, + // powerExportsAndSavings.length).boxed().toArray(Integer[]::new); + // Arrays.sort( + // indexesSortedIncome, + // Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1] + costExport) + // ); + // + // for (int i=0; i= 0) { + // // No income opportunity + // break; + // } + // + // // TODO: Ensure we don't exceed power demand limits + // // Find an earlier interval + // } + // + // + // // Order intervals based on potential cost saving [gridCost-storageExportCost] + // (largest first) + // Integer[] indexesSortedSaving = IntStream.range(0, + // powerExportsAndSavings.length).boxed().toArray(Integer[]::new); + // Arrays.sort( + // indexesSortedSaving, + // Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1] - + // costExport).reversed() + // ); + // + // + // // Find an interval earlier than each ordered export interval that costs less than + // the potential saving + // Arrays.stream(indexesSortedSaving).forEach(powerExportIndex -> { + // double[] powerExportDemand = powerExportsAndSavings[powerExportIndex]; + // double powerDemand = powerExportDemand[0]; + // double saving = powerExportDemand[1] - costExport; + // + // // Order grid costs up to this interval (smallest first) + // Integer[] indexesSortedGridCost = IntStream.range(0, + // powerExportIndex).boxed().toArray(Integer[]::new); + // Arrays.sort( + // indexesSortedGridCost, + // Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1]) + // ); + // + // // Go through intervals allocating required power demand until interval reaches + // powerImportMax or storage is full + // for (int i=0; i 0 && i powerDemand) { + // // This interval can fulfill entire demand + // used += powerDemand; + // powerDemand = 0; + // } else { + // // This interval can only partially fulfill the demand + // powerDemand -= (powerImportMax - used); + // used = powerImportMax; + // } + // } + // powerImport[i] = used; + // i++; + // } + // }); + // }; + // } + + /** + * Will take the supplied 24x7 energy schedule percentages and energy level min/max values and + * apply them to the supplied energyLevelMins also adjusting for any intervalSize difference. The + * energy schedule should be in UTC time. + */ + public void applyEnergySchedule( + double[] energyLevelMins, + double[] energyLevelMaxs, + double energyCapacity, + int[][] energyLevelSchedule, + LocalDateTime currentTime) { + + if (energyLevelSchedule == null) { + return; } - public double getFinancialWeighting() { - return financialWeighting; + // Extract the schedule for the next 24 hour period starting at current hour plus 1 (need to + // attain energy level by the time the hour starts) + OffsetDateTime date = currentTime.plus(1, ChronoUnit.HOURS).atOffset(ZoneOffset.UTC); + int dayIndex = date.getDayOfWeek().getValue(); + int hourIndex = date.get(ChronoField.HOUR_OF_DAY); + int i = 0; + double[] schedule = new double[24]; + + while (i < 24) { + // Convert from % to absolute value + schedule[i] = energyCapacity * energyLevelSchedule[dayIndex][hourIndex] * 0.01; + hourIndex++; + if (hourIndex > 23) { + hourIndex = 0; + dayIndex = (dayIndex + 1) % 7; + } + i++; } - public int get24HourIntervalCount() { - return (int) (24d / intervalSize); + // Convert schedule intervals to match optimisation intervals - need to look at schedule for + if (intervalSize <= 1d) { + int hourIntervals = (int) (1d / intervalSize); + + for (i = 0; i < schedule.length; i++) { + // Put energy level schedule value into first interval for the hour + energyLevelMins[(hourIntervals * i)] = + Math.min( + energyLevelMaxs[hourIntervals * i], + Math.max(energyLevelMins[hourIntervals * i], schedule[i])); + } + } else { + int takeSize = (int) intervalSize; + int hourIntervals = (int) (24d / intervalSize); + + for (i = 0; i < hourIntervals; i++) { + // Take largest energy level for the intervals + energyLevelMins[i] = + Math.min( + energyLevelMaxs[i], + Math.max( + energyLevelMins[i], + java.util.Arrays.stream(schedule, (i * takeSize), (i * takeSize) + takeSize) + .max() + .orElse(0))); + } } - -// /** -// * Function to calculate the power requirements for the given storage asset at each interval in the window -// * based on the provided power demand requirements and potential cost saving at each interval so for each interval -// * a an array of [powerDemand, gridCost] is required. For a given interval a positive value means importing -// * power and negative means exporting power: -// *
      -// *
    • powerDemand = The net demand of suppliers - consumers - higher priority supplier capabilities
    • -// *
    • gridCost = Grid cost / kWh (positive means expense, negative means income)
    • -// *
    -// */ -// // TODO: powerImportMax should be a function of energy level -// public Function getStoragePowerCalculator(ElectricityStorageAsset storageAsset) { -// -// return (powerExportsAndSavings) -> { -// -// double energyCapacity = storageAsset.getEnergyCapacity().orElse(0d); -// double storedEnergy = storageAsset.getEnergyLevel().orElse(0d); -// double tariffExport = storageAsset.getTariffExport().orElse(0d); -// double tariffImport = storageAsset.getTariffExport().orElse(0d); -// double carbonExport = storageAsset.getCarbonExport().orElse(0d); -// double carbonImport = storageAsset.getCarbonImport().orElse(0d); -// double costExport = financialWeighting * tariffExport + (1d-financialWeighting) * carbonExport; -// double costImport = financialWeighting * tariffImport + (1d-financialWeighting) * carbonImport; -// double powerEfficiencyImport = storageAsset.getEfficiencyImport().orElse(100); -// double powerEfficiencyExport = storageAsset.getEfficiencyExport().orElse(100); -// double powerImportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); -// double powerExportMax = storageAsset.getPowerImportMax().orElse(Double.MAX_VALUE); -// int energyLevelMax = storageAsset.getEnergyLevelPercentageMax().orElse(100); -// -// -// double[] powerImport = new double[intervalCount]; -// double[] powerExport = new double[intervalCount]; -// double[] energyLevel = new double[intervalCount]; -// Arrays.fill(energyLevel, storedEnergy); -// -//// // Calculate total energy requirement -//// double totalEnergyRequired = Arrays.stream(powerExportsAndSavings) -//// .map(interval -> interval[2] * intervalSize) -//// .reduce(0d, Double::sum); -// -//// // If we have enough energy stored then just consume that (no need to import more) -//// if (totalEnergyRequired < storedEnergy) { -//// Arrays.fill(powerImport, 0d); -//// return powerImport; -//// } -// -// /* Look for storage import income opportunities (i.e. when grid pays us to import) */ -// -// // Order intervals based on potential income [gridCost+storageImportCost] < 0 (smallest first) -// Integer[] indexesSortedIncome = IntStream.range(0, powerExportsAndSavings.length).boxed().toArray(Integer[]::new); -// Arrays.sort( -// indexesSortedIncome, -// Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1] + costExport) -// ); -// -// for (int i=0; i= 0) { -// // No income opportunity -// break; -// } -// -// // TODO: Ensure we don't exceed power demand limits -// // Find an earlier interval -// } -// -// -// // Order intervals based on potential cost saving [gridCost-storageExportCost] (largest first) -// Integer[] indexesSortedSaving = IntStream.range(0, powerExportsAndSavings.length).boxed().toArray(Integer[]::new); -// Arrays.sort( -// indexesSortedSaving, -// Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1] - costExport).reversed() -// ); -// -// -// // Find an interval earlier than each ordered export interval that costs less than the potential saving -// Arrays.stream(indexesSortedSaving).forEach(powerExportIndex -> { -// double[] powerExportDemand = powerExportsAndSavings[powerExportIndex]; -// double powerDemand = powerExportDemand[0]; -// double saving = powerExportDemand[1] - costExport; -// -// // Order grid costs up to this interval (smallest first) -// Integer[] indexesSortedGridCost = IntStream.range(0, powerExportIndex).boxed().toArray(Integer[]::new); -// Arrays.sort( -// indexesSortedGridCost, -// Comparator.comparingDouble((i) -> powerExportsAndSavings[i][1]) -// ); -// -// // Go through intervals allocating required power demand until interval reaches powerImportMax or storage is full -// for (int i=0; i 0 && i powerDemand) { -// // This interval can fulfill entire demand -// used += powerDemand; -// powerDemand = 0; -// } else { -// // This interval can only partially fulfill the demand -// powerDemand -= (powerImportMax - used); -// used = powerImportMax; -// } -// } -// powerImport[i] = used; -// i++; -// } -// }); -// }; -// } - - /** - * Will take the supplied 24x7 energy schedule percentages and energy level min/max values and apply them to the - * supplied energyLevelMins also adjusting for any intervalSize difference. The energy schedule should be in UTC - * time. - */ - public void applyEnergySchedule(double[] energyLevelMins, double[] energyLevelMaxs, double energyCapacity, int[][] energyLevelSchedule, LocalDateTime currentTime) { - - if (energyLevelSchedule == null) { - return; - } - - // Extract the schedule for the next 24 hour period starting at current hour plus 1 (need to attain energy level by the time the hour starts) - OffsetDateTime date = currentTime.plus(1, ChronoUnit.HOURS).atOffset(ZoneOffset.UTC); - int dayIndex = date.getDayOfWeek().getValue(); - int hourIndex = date.get(ChronoField.HOUR_OF_DAY); - int i = 0; - double[] schedule = new double[24]; - - while (i < 24) { - // Convert from % to absolute value - schedule[i] = energyCapacity * energyLevelSchedule[dayIndex][hourIndex] * 0.01; - hourIndex++; - if (hourIndex > 23) { - hourIndex = 0; - dayIndex = (dayIndex + 1) % 7; - } - i++; - } - - // Convert schedule intervals to match optimisation intervals - need to look at schedule for - if (intervalSize <= 1d) { - int hourIntervals = (int) (1d / intervalSize); - - for (i = 0; i < schedule.length; i++) { - // Put energy level schedule value into first interval for the hour - energyLevelMins[(hourIntervals * i)] = Math.min(energyLevelMaxs[hourIntervals * i], Math.max(energyLevelMins[hourIntervals * i], schedule[i])); - } - } else { - int takeSize = (int) intervalSize; - int hourIntervals = (int) (24d / intervalSize); - - for (i = 0; i < hourIntervals; i++) { - // Take largest energy level for the intervals - energyLevelMins[i] = Math.min(energyLevelMaxs[i], Math.max(energyLevelMins[i], java.util.Arrays.stream(schedule, (i * takeSize), (i * takeSize) + takeSize).max().orElse(0))); - } - } - } - - /** - * Adjusts the supplied energyLevelMin values to match the physical characteristics (i.e. the charge and discharge - * rates). - */ - public void normaliseEnergyMinRequirements(double[] energyLevelMins, Function powerImportMaxCalculator, Function powerExportMaxCalculator, double energyLevel) { - - int intervalCount = get24HourIntervalCount(); - Function previousEnergyLevelCalculator = i -> (i == 0 ? energyLevel : energyLevelMins[i - 1]); - - // Adjust energy min requirements to match physical characteristics (charge/discharge rate) - IntStream.range(0, intervalCount).forEach(i -> { - double energyDelta = energyLevelMins[i] - previousEnergyLevelCalculator.apply(i); - - if (energyDelta > 0) { - - // May need to increase earlier min values until there is no energy deficit with previous interval - // If we reach interval 0 and there is still a deficit then need to reduce this energy level + } + + /** + * Adjusts the supplied energyLevelMin values to match the physical characteristics (i.e. the + * charge and discharge rates). + */ + public void normaliseEnergyMinRequirements( + double[] energyLevelMins, + Function powerImportMaxCalculator, + Function powerExportMaxCalculator, + double energyLevel) { + + int intervalCount = get24HourIntervalCount(); + Function previousEnergyLevelCalculator = + i -> (i == 0 ? energyLevel : energyLevelMins[i - 1]); + + // Adjust energy min requirements to match physical characteristics (charge/discharge rate) + IntStream.range(0, intervalCount) + .forEach( + i -> { + double energyDelta = energyLevelMins[i] - previousEnergyLevelCalculator.apply(i); + + if (energyDelta > 0) { + + // May need to increase earlier min values until there is no energy deficit with + // previous interval + // If we reach interval 0 and there is still a deficit then need to reduce this + // energy level for (int j = i; j >= 0; j--) { - double previousMin = energyLevelMins[j] - (powerImportMaxCalculator.apply(j) * intervalSize); - double previous = previousEnergyLevelCalculator.apply(j); - - if (previous < previousMin) { - if (j == 0) { - // Can't attain so shift all min values down - double shift = previous - previousMin; - for (int k = 0; k <= i; k++) { - energyLevelMins[k] += shift; - } - } else { - // Increase the previous min value - energyLevelMins[j - 1] = previousMin; - } + double previousMin = + energyLevelMins[j] - (powerImportMaxCalculator.apply(j) * intervalSize); + double previous = previousEnergyLevelCalculator.apply(j); + + if (previous < previousMin) { + if (j == 0) { + // Can't attain so shift all min values down + double shift = previous - previousMin; + for (int k = 0; k <= i; k++) { + energyLevelMins[k] += shift; + } } else { - // Already at or above min requirement - break; + // Increase the previous min value + energyLevelMins[j - 1] = previousMin; } + } else { + // Already at or above min requirement + break; + } } - } else if (energyDelta < 0) { + } else if (energyDelta < 0) { // May need to spread discharge over this and later intervals for (int j = i; j < intervalCount; j++) { - double min = previousEnergyLevelCalculator.apply(j) + (powerExportMaxCalculator.apply(j) * intervalSize); + double min = + previousEnergyLevelCalculator.apply(j) + + (powerExportMaxCalculator.apply(j) * intervalSize); - if (min > energyLevelMins[j]) { - energyLevelMins[j] = min; - } else { - // Already at or above min requirement - break; - } + if (min > energyLevelMins[j]) { + energyLevelMins[j] = min; + } else { + // Already at or above min requirement + break; + } } - } - }); - } - - /** - * Will update the powerSetpoints in order to achieve the energyLevelMin values supplied. - */ - public void applyEnergyMinImports(double[][] importCostAndPower, double[] energyLevelMins, double[] powerSetpoints, Function energyLevelCalculator, BiFunction importOptimiser, Function powerImportMaxCalculator) { - // Ensure min energy levels are attained by the end of the interval as these have priority - AtomicInteger fromInterval = new AtomicInteger(0); - - IntStream.range(0, get24HourIntervalCount()).forEach(i -> { - double intervalEnergyLevel = energyLevelCalculator.apply(i); - double energyDeficit = energyLevelMins[i] - intervalEnergyLevel; - - if (energyDeficit > 0) { + } + }); + } + + /** Will update the powerSetpoints in order to achieve the energyLevelMin values supplied. */ + public void applyEnergyMinImports( + double[][] importCostAndPower, + double[] energyLevelMins, + double[] powerSetpoints, + Function energyLevelCalculator, + BiFunction importOptimiser, + Function powerImportMaxCalculator) { + // Ensure min energy levels are attained by the end of the interval as these have priority + AtomicInteger fromInterval = new AtomicInteger(0); + + IntStream.range(0, get24HourIntervalCount()) + .forEach( + i -> { + double intervalEnergyLevel = energyLevelCalculator.apply(i); + double energyDeficit = energyLevelMins[i] - intervalEnergyLevel; + + if (energyDeficit > 0) { double energyAttainable = powerImportMaxCalculator.apply(i) * intervalSize; energyAttainable = Math.min(energyDeficit, energyAttainable); powerSetpoints[i] = energyAttainable / intervalSize; energyDeficit -= energyAttainable; if (energyDeficit > 0) { - retrospectiveEnergyAllocator(importCostAndPower, energyLevelMins, powerSetpoints, importOptimiser, powerImportMaxCalculator, energyDeficit, fromInterval.getAndSet(i), i); + retrospectiveEnergyAllocator( + importCostAndPower, + energyLevelMins, + powerSetpoints, + importOptimiser, + powerImportMaxCalculator, + energyDeficit, + fromInterval.getAndSet(i), + i); } - } - }); + } + }); + } + + /** + * Creates earlier imports between fromInterval (inclusive) and toInterval (exclusive) in order to + * meet min energy level requirement at the specified interval based on the provided energy level + * at the start of fromInterval. + */ + public void retrospectiveEnergyAllocator( + double[][] importCostAndPower, + double[] energyLevelMins, + double[] powerSetpoints, + BiFunction importOptimiser, + Function powerImportMaxCalculator, + double energyLevel, + int fromInterval, + int toInterval) { + + double energyDeficit = energyLevelMins[toInterval] - energyLevel; + + if (energyDeficit <= 0) { + return; } - /** - * Creates earlier imports between fromInterval (inclusive) and toInterval (exclusive) in order to meet min energy - * level requirement at the specified interval based on the provided energy level at the start of fromInterval. - */ - public void retrospectiveEnergyAllocator(double[][] importCostAndPower, double[] energyLevelMins, double[] powerSetpoints, BiFunction importOptimiser, Function powerImportMaxCalculator, double energyLevel, int fromInterval, int toInterval) { - - double energyDeficit = energyLevelMins[toInterval] - energyLevel; - - if (energyDeficit <= 0) { - return; - } - - // Do import until energy deficit reaches 0 or there are no more intervals - boolean canMeetDeficit = IntStream.range(fromInterval, toInterval).mapToDouble(i -> - Math.min(powerImportMaxCalculator.apply(i), importCostAndPower[i][2]) - ).sum() >= energyDeficit; - boolean morePowerAvailable = !canMeetDeficit && IntStream.range(fromInterval, toInterval).mapToObj(i -> importCostAndPower[i][2] < powerImportMaxCalculator.apply(i)).anyMatch(b -> b); - - if (!canMeetDeficit && morePowerAvailable) { - // Need to push imports beyond optimum to fulfill energy deficit - IntStream.range(fromInterval, toInterval).forEach(i -> { + // Do import until energy deficit reaches 0 or there are no more intervals + boolean canMeetDeficit = + IntStream.range(fromInterval, toInterval) + .mapToDouble( + i -> Math.min(powerImportMaxCalculator.apply(i), importCostAndPower[i][2])) + .sum() + >= energyDeficit; + boolean morePowerAvailable = + !canMeetDeficit + && IntStream.range(fromInterval, toInterval) + .mapToObj(i -> importCostAndPower[i][2] < powerImportMaxCalculator.apply(i)) + .anyMatch(b -> b); + + if (!canMeetDeficit && morePowerAvailable) { + // Need to push imports beyond optimum to fulfill energy deficit + IntStream.range(fromInterval, toInterval) + .forEach( + i -> { double powerImportMax = powerImportMaxCalculator.apply(i); if (importCostAndPower[i][2] < powerImportMax) { - importCostAndPower[i] = importOptimiser.apply(i, new double[]{0d, powerImportMax}); + importCostAndPower[i] = + importOptimiser.apply(i, new double[] {0d, powerImportMax}); } - }); - } - - // Sort import intervals by cost (lowest to highest) - List> sortedImportCostAndPower = IntStream.range(fromInterval, toInterval) - .mapToObj(i -> new Pair<>(i, importCostAndPower[i])).sorted( - Comparator.comparingDouble(pair -> pair.value[0]) - ).collect(Collectors.toList()); - - int i = 0; - while (energyDeficit > 0 && i < sortedImportCostAndPower.size()) { - double importPower = Math.min(powerImportMaxCalculator.apply(i), importCostAndPower[i][2]); - double requiredPower = energyDeficit / intervalSize; - // If we earn by importing then take the maximum power - importPower = importCostAndPower[i][0] < 0 ? importPower : Math.min(importPower, requiredPower); - powerSetpoints[i] = importPower; - energyDeficit -= importPower; - i++; - } + }); } - /** - * Will find the best earning opportunity for each interval (import or export) and will then try to apply them in - * chronological order (reallocating earlier import/exports if it cost beneficial). The powerSetpoints will be - * updated as a result. - */ - public void applyEarningOpportunities(double[][] importCostAndPower, double[][] exportCostAndPower, double[] energyLevelMins, double[] energyLevelMaxs, double[] powerSetpoints, Function energyLevelCalculator, Function powerImportMaxCalculator, Function powerExportMaxCalculator) { - LOG.finest("Applying earning opportunities"); - - // Look for import and export earning opportunities - double[][] primary = importCostAndPower != null ? importCostAndPower : exportCostAndPower; // Never null - double[][] secondary = importCostAndPower != null ? exportCostAndPower : null; // Could be null - - List> earningOpportunities = IntStream.range(0, primary.length).mapToObj(i -> { - if (secondary == null) { - return new Pair<>(i, primary[i]); - } - // Return whichever has the lowest cost - if (primary[i][0] < secondary[i][0]) { - return new Pair<>(i, primary[i]); - } - return new Pair<>(i, secondary[i]); - }) + // Sort import intervals by cost (lowest to highest) + List> sortedImportCostAndPower = + IntStream.range(fromInterval, toInterval) + .mapToObj(i -> new Pair<>(i, importCostAndPower[i])) + .sorted(Comparator.comparingDouble(pair -> pair.value[0])) + .collect(Collectors.toList()); + + int i = 0; + while (energyDeficit > 0 && i < sortedImportCostAndPower.size()) { + double importPower = Math.min(powerImportMaxCalculator.apply(i), importCostAndPower[i][2]); + double requiredPower = energyDeficit / intervalSize; + // If we earn by importing then take the maximum power + importPower = + importCostAndPower[i][0] < 0 ? importPower : Math.min(importPower, requiredPower); + powerSetpoints[i] = importPower; + energyDeficit -= importPower; + i++; + } + } + + /** + * Will find the best earning opportunity for each interval (import or export) and will then try + * to apply them in chronological order (reallocating earlier import/exports if it cost + * beneficial). The powerSetpoints will be updated as a result. + */ + public void applyEarningOpportunities( + double[][] importCostAndPower, + double[][] exportCostAndPower, + double[] energyLevelMins, + double[] energyLevelMaxs, + double[] powerSetpoints, + Function energyLevelCalculator, + Function powerImportMaxCalculator, + Function powerExportMaxCalculator) { + LOG.finest("Applying earning opportunities"); + + // Look for import and export earning opportunities + double[][] primary = + importCostAndPower != null ? importCostAndPower : exportCostAndPower; // Never null + double[][] secondary = importCostAndPower != null ? exportCostAndPower : null; // Could be null + + List> earningOpportunities = + IntStream.range(0, primary.length) + .mapToObj( + i -> { + if (secondary == null) { + return new Pair<>(i, primary[i]); + } + // Return whichever has the lowest cost + if (primary[i][0] < secondary[i][0]) { + return new Pair<>(i, primary[i]); + } + return new Pair<>(i, secondary[i]); + }) .filter(intervalCostAndPowerBand -> intervalCostAndPowerBand.value[0] < 0) .sorted(Comparator.comparingDouble(optimisedInterval -> optimisedInterval.value[0])) .collect(Collectors.toList()); - if (earningOpportunities.isEmpty()) { - LOG.finest("No earning opportunities found"); - } - - if (LOG.isLoggable(Level.FINEST)) { - earningOpportunities.forEach(op -> LOG.finest("Earning opportunity: interval=" + op.key + ", cost=" + op.value[0] + ", powerMin=" + op.value[1] + ", powerMax=" + op.value[2])); - } - - // Go through each earning opportunity and determine if it can be utilised without breaching the energy min - // levels - for (Pair earningOpportunity : earningOpportunities) { - int interval = earningOpportunity.key; - double[] costAndPower = earningOpportunity.value; - assert importCostAndPower != null; - assert exportCostAndPower != null; - - if (isImportOpportunity(costAndPower, powerSetpoints[interval], interval, powerImportMaxCalculator)) { - // import opportunity and interval still available to import power - applyImportOpportunity(importCostAndPower, exportCostAndPower, energyLevelMins, energyLevelMaxs, powerSetpoints, energyLevelCalculator, powerImportMaxCalculator, powerExportMaxCalculator, interval); - } else if (isExportOpportunity(costAndPower, powerSetpoints[interval], interval, powerExportMaxCalculator)) { - // export opportunity and interval still available to export power - applyExportOpportunity(importCostAndPower, exportCostAndPower, energyLevelMins, energyLevelMaxs, powerSetpoints, energyLevelCalculator, powerImportMaxCalculator, powerExportMaxCalculator, interval); - } - } + if (earningOpportunities.isEmpty()) { + LOG.finest("No earning opportunities found"); } - protected boolean isImportOpportunity(double[] costAndPower, double powerSetpoint, int interval, Function powerImportMaxCalculator) { - return costAndPower[2] > 0 && powerSetpoint >= 0 && powerSetpoint < Math.min(powerImportMaxCalculator.apply(interval), costAndPower[2]); + if (LOG.isLoggable(Level.FINEST)) { + earningOpportunities.forEach( + op -> + LOG.finest( + "Earning opportunity: interval=" + + op.key + + ", cost=" + + op.value[0] + + ", powerMin=" + + op.value[1] + + ", powerMax=" + + op.value[2])); } - protected boolean isExportOpportunity(double[] costAndPower, double powerSetpoint, int interval, Function powerExportMaxCalculator) { - return costAndPower[1] < 0 && powerSetpoint <= 0 && powerSetpoint > Math.max(powerExportMaxCalculator.apply(interval), costAndPower[1]); + // Go through each earning opportunity and determine if it can be utilised without breaching the + // energy min + // levels + for (Pair earningOpportunity : earningOpportunities) { + int interval = earningOpportunity.key; + double[] costAndPower = earningOpportunity.value; + assert importCostAndPower != null; + assert exportCostAndPower != null; + + if (isImportOpportunity( + costAndPower, powerSetpoints[interval], interval, powerImportMaxCalculator)) { + // import opportunity and interval still available to import power + applyImportOpportunity( + importCostAndPower, + exportCostAndPower, + energyLevelMins, + energyLevelMaxs, + powerSetpoints, + energyLevelCalculator, + powerImportMaxCalculator, + powerExportMaxCalculator, + interval); + } else if (isExportOpportunity( + costAndPower, powerSetpoints[interval], interval, powerExportMaxCalculator)) { + // export opportunity and interval still available to export power + applyExportOpportunity( + importCostAndPower, + exportCostAndPower, + energyLevelMins, + energyLevelMaxs, + powerSetpoints, + energyLevelCalculator, + powerImportMaxCalculator, + powerExportMaxCalculator, + interval); + } + } + } + + protected boolean isImportOpportunity( + double[] costAndPower, + double powerSetpoint, + int interval, + Function powerImportMaxCalculator) { + return costAndPower[2] > 0 + && powerSetpoint >= 0 + && powerSetpoint < Math.min(powerImportMaxCalculator.apply(interval), costAndPower[2]); + } + + protected boolean isExportOpportunity( + double[] costAndPower, + double powerSetpoint, + int interval, + Function powerExportMaxCalculator) { + return costAndPower[1] < 0 + && powerSetpoint <= 0 + && powerSetpoint > Math.max(powerExportMaxCalculator.apply(interval), costAndPower[1]); + } + + /** + * Tries to apply the maximum import power as defined in the importCostAndPower at the specified + * interval taking into consideration the maximum power and energy levels; if there is + * insufficient power or energy capacity at the interval then an earlier cost effective export + * opportunity will be attempted to offset the requirement. The powerSetpoints will be updated as + * a result. + */ + public void applyImportOpportunity( + double[][] importCostAndPower, + double[][] exportCostAndPower, + double[] energyLevelMins, + double[] energyLevelMaxs, + double[] powerSetpoints, + Function energyLevelCalculator, + Function powerImportMaxCalculator, + Function powerExportMaxCalculator, + int interval) { + LOG.finest("Applying import earning opportunity: interval=" + interval); + double[] costAndPower = importCostAndPower[interval]; + double impPowerMin = costAndPower[1]; + double impPowerMax = Math.min(powerImportMaxCalculator.apply(interval), costAndPower[2]); + double powerCapacity = impPowerMax - powerSetpoints[interval]; + + if (impPowerMin > powerCapacity) { + LOG.finest("Can't attain min power level to make use of this opportunity"); + return; } - /** - * Tries to apply the maximum import power as defined in the importCostAndPower at the specified interval taking - * into consideration the maximum power and energy levels; if there is insufficient power or energy capacity at the - * interval then an earlier cost effective export opportunity will be attempted to offset the requirement. The - * powerSetpoints will be updated as a result. - */ - public void applyImportOpportunity(double[][] importCostAndPower, double[][] exportCostAndPower, double[] energyLevelMins, double[] energyLevelMaxs, double[] powerSetpoints, Function energyLevelCalculator, Function powerImportMaxCalculator, Function powerExportMaxCalculator, int interval) { - LOG.finest("Applying import earning opportunity: interval=" + interval); - double[] costAndPower = importCostAndPower[interval]; - double impPowerMin = costAndPower[1]; - double impPowerMax = Math.min(powerImportMaxCalculator.apply(interval), costAndPower[2]); - double powerCapacity = impPowerMax - powerSetpoints[interval]; - - if (impPowerMin > powerCapacity) { - LOG.finest("Can't attain min power level to make use of this opportunity"); - return; - } - - double energySpace = energyLevelMaxs[interval] - energyLevelCalculator.apply(interval); - double energySpaceMax = powerCapacity * intervalSize; - double energySpaceMin = impPowerMin * intervalSize; - List> pastIntervalPowerDeltas = new ArrayList<>(); - - int k = interval; - while (k < powerSetpoints.length && energySpace > 0 && energySpace >= energySpaceMin) { - double futureEnergySpace = energyLevelMaxs[k] - energyLevelCalculator.apply(k); - energySpace = Math.min(energySpace, futureEnergySpace); - k++; - } - - if (energySpace < energySpaceMax && exportCostAndPower != null) { - // Can't maximise on opportunity without exporting earlier on so can this be done - // in a cost effective way - LOG.finest("Looking for earlier export opportunities to maximise on this import opportunity: space=" + energySpace + ", max=" + energySpaceMax); - int i = interval - 1; - List> pastOpportunities = new ArrayList<>(); - - while (i >= 0) { - if (costAndPower[0] + exportCostAndPower[i][0] < 0 && powerSetpoints[i] <= 0) { - // We can afford to export earlier and still earn from this import - pastOpportunities.add(new Pair<>(i, exportCostAndPower[i][0])); - } - i--; - } - - pastOpportunities.sort(Comparator.comparingDouble(op -> op.value)); - int j = 0; - - if (pastOpportunities.isEmpty()) { - LOG.finest("No earlier export opportunities identified"); - } - - while (energySpace < energySpaceMax && j < pastOpportunities.size()) { - // Energy level at this interval must be above energy min to consider exporting - Pair opportunity = pastOpportunities.get(j); - int pastInterval = opportunity.key; - - // Power capacity must be within the optimum power band - double[] pastCostAndPower = exportCostAndPower[pastInterval]; - double expPowerMax = Math.max(powerExportMaxCalculator.apply(pastInterval), pastCostAndPower[1]); - double expPowerCapacity = expPowerMax - powerSetpoints[pastInterval]; - - if (expPowerCapacity >= 0 || expPowerCapacity > pastCostAndPower[2]) { - LOG.finest("Power capacity is outside optimum power band so cannot use this opportunity"); - j++; - continue; - } - - double energySurplusMin = pastCostAndPower[2] * intervalSize; - double energySurplus = energyLevelMins[pastInterval] - energyLevelCalculator.apply(pastInterval); - energySurplus = Math.max(energySurplus, energySpace - energySpaceMax); - - // We have spare energy capacity and power check if we don't violate energy min for any future exports - k = pastInterval; - while (k < powerSetpoints.length && energySurplus < 0 && energySurplus <= energySurplusMin) { - double futureEnergySurplus = energyLevelCalculator.apply(k) - energyLevelMins[k]; - energySurplus = Math.max(energySurplus, -futureEnergySurplus); - if (energySurplus <= 0) { - LOG.finest("Earlier export opportunity would violate future energy min level: interval=" + j + ", futureInterval=" + k); - } - k++; - } - - expPowerCapacity = Math.max(expPowerCapacity, energySurplus / intervalSize); + double energySpace = energyLevelMaxs[interval] - energyLevelCalculator.apply(interval); + double energySpaceMax = powerCapacity * intervalSize; + double energySpaceMin = impPowerMin * intervalSize; + List> pastIntervalPowerDeltas = new ArrayList<>(); - if (expPowerCapacity < 0 && expPowerCapacity < pastCostAndPower[2]) { - // We can export in the optimum range - energySpace += (-1d * expPowerCapacity * intervalSize); - pastIntervalPowerDeltas.add(new Pair<>(pastInterval, expPowerCapacity)); - LOG.finest("Earlier export opportunity identified: interval=" + pastInterval + ", power=" + expPowerCapacity); - } + int k = interval; + while (k < powerSetpoints.length && energySpace > 0 && energySpace >= energySpaceMin) { + double futureEnergySpace = energyLevelMaxs[k] - energyLevelCalculator.apply(k); + energySpace = Math.min(energySpace, futureEnergySpace); + k++; + } - j++; - } + if (energySpace < energySpaceMax && exportCostAndPower != null) { + // Can't maximise on opportunity without exporting earlier on so can this be done + // in a cost effective way + LOG.finest( + "Looking for earlier export opportunities to maximise on this import opportunity: space=" + + energySpace + + ", max=" + + energySpaceMax); + int i = interval - 1; + List> pastOpportunities = new ArrayList<>(); + + while (i >= 0) { + if (costAndPower[0] + exportCostAndPower[i][0] < 0 && powerSetpoints[i] <= 0) { + // We can afford to export earlier and still earn from this import + pastOpportunities.add(new Pair<>(i, exportCostAndPower[i][0])); } - - // Do original import if there is enough energy space - if (energySpace > 0 && energySpace >= energySpaceMin) { - - // Adjust past interval set points as required - pastIntervalPowerDeltas.forEach(intervalAndDelta -> powerSetpoints[intervalAndDelta.key] += intervalAndDelta.value); - - energySpaceMax = Math.min(energySpaceMax, energySpace); - powerCapacity = Math.min(impPowerMax - powerSetpoints[interval], (energySpaceMax / intervalSize)); - powerSetpoints[interval] = powerSetpoints[interval] + powerCapacity; - LOG.finest("Applied import earning opportunity: set point=" + powerSetpoints[interval] + " (delta: " + powerCapacity + ")"); + i--; + } + + pastOpportunities.sort(Comparator.comparingDouble(op -> op.value)); + int j = 0; + + if (pastOpportunities.isEmpty()) { + LOG.finest("No earlier export opportunities identified"); + } + + while (energySpace < energySpaceMax && j < pastOpportunities.size()) { + // Energy level at this interval must be above energy min to consider exporting + Pair opportunity = pastOpportunities.get(j); + int pastInterval = opportunity.key; + + // Power capacity must be within the optimum power band + double[] pastCostAndPower = exportCostAndPower[pastInterval]; + double expPowerMax = + Math.max(powerExportMaxCalculator.apply(pastInterval), pastCostAndPower[1]); + double expPowerCapacity = expPowerMax - powerSetpoints[pastInterval]; + + if (expPowerCapacity >= 0 || expPowerCapacity > pastCostAndPower[2]) { + LOG.finest("Power capacity is outside optimum power band so cannot use this opportunity"); + j++; + continue; } - } - /** - * Tries to apply the maximum export power as defined in the exportCostAndPower at the specified interval taking - * into consideration the maximum power and energy levels; if there is insufficient power or energy capacity at the - * interval then an earlier cost effective import opportunity will be attempted to offset the requirement. The - * powerSetpoints will be updated as a result. - */ - public void applyExportOpportunity(double[][] importCostAndPower, double[][] exportCostAndPower, double[] energyLevelMins, double[] energyLevelMaxs, double[] powerSetpoints, Function energyLevelCalculator, Function powerImportMaxCalculator, Function powerExportMaxCalculator, int interval) { - LOG.finest("Applying export earning opportunity: interval=" + interval); - double[] costAndPower = exportCostAndPower[interval]; - double expPowerMin = costAndPower[2]; - double expPowerMax = Math.max(powerExportMaxCalculator.apply(interval), costAndPower[1]); - double powerCapacity = expPowerMax - powerSetpoints[interval]; - - if (expPowerMin < powerCapacity) { - LOG.finest("Can't attain min power level to make use of this opportunity"); - return; + double energySurplusMin = pastCostAndPower[2] * intervalSize; + double energySurplus = + energyLevelMins[pastInterval] - energyLevelCalculator.apply(pastInterval); + energySurplus = Math.max(energySurplus, energySpace - energySpaceMax); + + // We have spare energy capacity and power check if we don't violate energy min for any + // future exports + k = pastInterval; + while (k < powerSetpoints.length + && energySurplus < 0 + && energySurplus <= energySurplusMin) { + double futureEnergySurplus = energyLevelCalculator.apply(k) - energyLevelMins[k]; + energySurplus = Math.max(energySurplus, -futureEnergySurplus); + if (energySurplus <= 0) { + LOG.finest( + "Earlier export opportunity would violate future energy min level: interval=" + + j + + ", futureInterval=" + + k); + } + k++; } - double energySurplus = energyLevelCalculator.apply(interval) - energyLevelMins[interval]; - double energySurplusMin = -1d * expPowerMin * intervalSize; - double energySurplusMax = -1d * powerCapacity * intervalSize; - List> pastAndFutureIntervalPowerDeltas = new ArrayList<>(); - - int k = interval; - while (k < powerSetpoints.length && energySurplus > 0 && energySurplus >= energySurplusMin) { - - double futureEnergySurplus = energyLevelCalculator.apply(k) - energyLevelMins[k]; - - // The following is an attempt to make use of an earning opportunity that would violate future energy limits - // by allocating extra imports between 'now' and 'then' - this needs more work -// if (futureEnergySurplus < energySurplusMin || futureEnergySurplus <= 0) { -// // Try and allocate an import between this future point and the earning opportunity to prevent this deficit -// int l = k - 1; -// while (futureEnergySurplus < energySurplusMax && l > interval) { -// int finalL = l; -// if (powerSetpoints[l] >= 0 && pastAndFutureIntervalPowerDeltas.stream().noneMatch(delta -> delta.key.equals(finalL))) { -// -// double surplusDeficit = energySurplusMax - futureEnergySurplus; -// double intermediateEnergyCapacity = energyLevelMax - energyLevelCalculator.apply(l); -// double intermediatePowerCapacity = powerImportMaxCalculator.apply(l) - powerSetpoints[l]; -// intermediatePowerCapacity = Math.min(intermediatePowerCapacity, surplusDeficit / intervalSize); -// intermediatePowerCapacity = Math.min(intermediatePowerCapacity, intermediateEnergyCapacity / intervalSize); -// -// if (intermediatePowerCapacity > 0 && powerSetpoints[l] + intermediatePowerCapacity > importCostAndPower[l][1] && costAndPower[0] + importCostAndPower[l][0] < 0) { -// // There is capacity and it is cost effective to use it -// futureEnergySurplus = intermediatePowerCapacity * intervalSize; -// pastAndFutureIntervalPowerDeltas.add(new Pair<>(l, intermediatePowerCapacity)); -// } -// } -// l--; -// } -// } - - energySurplus = Math.min(energySurplus, futureEnergySurplus); - k++; - + expPowerCapacity = Math.max(expPowerCapacity, energySurplus / intervalSize); + + if (expPowerCapacity < 0 && expPowerCapacity < pastCostAndPower[2]) { + // We can export in the optimum range + energySpace += (-1d * expPowerCapacity * intervalSize); + pastIntervalPowerDeltas.add(new Pair<>(pastInterval, expPowerCapacity)); + LOG.finest( + "Earlier export opportunity identified: interval=" + + pastInterval + + ", power=" + + expPowerCapacity); } - if (energySurplus < energySurplusMax && importCostAndPower != null) { - // Can't maximise on opportunity without importing earlier on so can this be done - // in a cost effective way - LOG.finest("Looking for earlier import opportunities to maximise on this export opportunity: surplus=" + energySurplus + ", max=" + energySurplusMax); - int i = interval - 1; - List> pastOpportunities = new ArrayList<>(); - - while (i >= 0) { - - if (costAndPower[0] + importCostAndPower[i][0] < 0 && powerSetpoints[i] >= 0) { - // We can afford to import and still earn using original export - pastOpportunities.add(new Pair<>(i, importCostAndPower[i][0])); - } - - i--; - } - - pastOpportunities.sort(Comparator.comparingDouble(op -> op.value)); - int j = 0; - - if (pastOpportunities.isEmpty()) { - LOG.finest("No earlier import opportunities identified"); - } - - while (energySurplus < energySurplusMax && j < pastOpportunities.size()) { - Pair opportunity = pastOpportunities.get(j); - int pastInterval = opportunity.key; + j++; + } + } - // Power capacity must be within the optimum power band - double[] pastCostAndPower = importCostAndPower[pastInterval]; - double impPowerMax = Math.min(powerImportMaxCalculator.apply(interval), pastCostAndPower[2]); - double impPowerCapacity = impPowerMax - powerSetpoints[pastInterval]; + // Do original import if there is enough energy space + if (energySpace > 0 && energySpace >= energySpaceMin) { + + // Adjust past interval set points as required + pastIntervalPowerDeltas.forEach( + intervalAndDelta -> powerSetpoints[intervalAndDelta.key] += intervalAndDelta.value); + + energySpaceMax = Math.min(energySpaceMax, energySpace); + powerCapacity = + Math.min(impPowerMax - powerSetpoints[interval], (energySpaceMax / intervalSize)); + powerSetpoints[interval] = powerSetpoints[interval] + powerCapacity; + LOG.finest( + "Applied import earning opportunity: set point=" + + powerSetpoints[interval] + + " (delta: " + + powerCapacity + + ")"); + } + } + + /** + * Tries to apply the maximum export power as defined in the exportCostAndPower at the specified + * interval taking into consideration the maximum power and energy levels; if there is + * insufficient power or energy capacity at the interval then an earlier cost effective import + * opportunity will be attempted to offset the requirement. The powerSetpoints will be updated as + * a result. + */ + public void applyExportOpportunity( + double[][] importCostAndPower, + double[][] exportCostAndPower, + double[] energyLevelMins, + double[] energyLevelMaxs, + double[] powerSetpoints, + Function energyLevelCalculator, + Function powerImportMaxCalculator, + Function powerExportMaxCalculator, + int interval) { + LOG.finest("Applying export earning opportunity: interval=" + interval); + double[] costAndPower = exportCostAndPower[interval]; + double expPowerMin = costAndPower[2]; + double expPowerMax = Math.max(powerExportMaxCalculator.apply(interval), costAndPower[1]); + double powerCapacity = expPowerMax - powerSetpoints[interval]; + + if (expPowerMin < powerCapacity) { + LOG.finest("Can't attain min power level to make use of this opportunity"); + return; + } - if (impPowerCapacity <= 0 || impPowerCapacity < pastCostAndPower[1]) { - LOG.finest("Power capacity is outside optimum power band so cannot use this opportunity"); - j++; - continue; - } + double energySurplus = energyLevelCalculator.apply(interval) - energyLevelMins[interval]; + double energySurplusMin = -1d * expPowerMin * intervalSize; + double energySurplusMax = -1d * powerCapacity * intervalSize; + List> pastAndFutureIntervalPowerDeltas = new ArrayList<>(); + + int k = interval; + while (k < powerSetpoints.length && energySurplus > 0 && energySurplus >= energySurplusMin) { + + double futureEnergySurplus = energyLevelCalculator.apply(k) - energyLevelMins[k]; + + // The following is an attempt to make use of an earning opportunity that would violate future + // energy limits + // by allocating extra imports between 'now' and 'then' - this needs more work + // if (futureEnergySurplus < energySurplusMin || futureEnergySurplus <= 0) { + // // Try and allocate an import between this future point and the earning + // opportunity to prevent this deficit + // int l = k - 1; + // while (futureEnergySurplus < energySurplusMax && l > interval) { + // int finalL = l; + // if (powerSetpoints[l] >= 0 && + // pastAndFutureIntervalPowerDeltas.stream().noneMatch(delta -> delta.key.equals(finalL))) { + // + // double surplusDeficit = energySurplusMax - futureEnergySurplus; + // double intermediateEnergyCapacity = energyLevelMax - + // energyLevelCalculator.apply(l); + // double intermediatePowerCapacity = powerImportMaxCalculator.apply(l) + // - powerSetpoints[l]; + // intermediatePowerCapacity = Math.min(intermediatePowerCapacity, + // surplusDeficit / intervalSize); + // intermediatePowerCapacity = Math.min(intermediatePowerCapacity, + // intermediateEnergyCapacity / intervalSize); + // + // if (intermediatePowerCapacity > 0 && powerSetpoints[l] + + // intermediatePowerCapacity > importCostAndPower[l][1] && costAndPower[0] + + // importCostAndPower[l][0] < 0) { + // // There is capacity and it is cost effective to use it + // futureEnergySurplus = intermediatePowerCapacity * intervalSize; + // pastAndFutureIntervalPowerDeltas.add(new Pair<>(l, + // intermediatePowerCapacity)); + // } + // } + // l--; + // } + // } + + energySurplus = Math.min(energySurplus, futureEnergySurplus); + k++; + } - double energySpaceMin = pastCostAndPower[1] * intervalSize; - double energySpace = energyLevelMaxs[interval] - energyLevelCalculator.apply(pastInterval); - energySpace = Math.max(energySpace, energySpace - energySurplusMax); + if (energySurplus < energySurplusMax && importCostAndPower != null) { + // Can't maximise on opportunity without importing earlier on so can this be done + // in a cost effective way + LOG.finest( + "Looking for earlier import opportunities to maximise on this export opportunity: surplus=" + + energySurplus + + ", max=" + + energySurplusMax); + int i = interval - 1; + List> pastOpportunities = new ArrayList<>(); + + while (i >= 0) { + + if (costAndPower[0] + importCostAndPower[i][0] < 0 && powerSetpoints[i] >= 0) { + // We can afford to import and still earn using original export + pastOpportunities.add(new Pair<>(i, importCostAndPower[i][0])); + } - // We have spare energy capacity and power check if we don't violate energy max for any future imports - k = pastInterval; - while (k < powerSetpoints.length && energySpace > 0 && energySpace >= energySpaceMin) { + i--; + } - double futureEnergySpace = energyLevelMaxs[k] - energyLevelCalculator.apply(k); - energySpace = Math.min(energySpace, futureEnergySpace); - if (energySpace <= 0) { - LOG.finest("Earlier import opportunity would violate future energy max level: interval=" + j + ", futureInterval=" + k); - } - k++; + pastOpportunities.sort(Comparator.comparingDouble(op -> op.value)); + int j = 0; - } + if (pastOpportunities.isEmpty()) { + LOG.finest("No earlier import opportunities identified"); + } - impPowerCapacity = Math.min(impPowerCapacity, energySpace / intervalSize); + while (energySurplus < energySurplusMax && j < pastOpportunities.size()) { + Pair opportunity = pastOpportunities.get(j); + int pastInterval = opportunity.key; - if (impPowerCapacity > 0 && impPowerCapacity > pastCostAndPower[1]) { - // We can import in the optimum range - energySurplus += (impPowerCapacity * intervalSize); - pastAndFutureIntervalPowerDeltas.add(new Pair<>(pastInterval, impPowerCapacity)); - LOG.finest("Earlier import opportunity identified: interval=" + pastInterval + ", power=" + impPowerCapacity); - } + // Power capacity must be within the optimum power band + double[] pastCostAndPower = importCostAndPower[pastInterval]; + double impPowerMax = + Math.min(powerImportMaxCalculator.apply(interval), pastCostAndPower[2]); + double impPowerCapacity = impPowerMax - powerSetpoints[pastInterval]; - j++; - } + if (impPowerCapacity <= 0 || impPowerCapacity < pastCostAndPower[1]) { + LOG.finest("Power capacity is outside optimum power band so cannot use this opportunity"); + j++; + continue; } - // Do original export if there is any energy surplus - if (energySurplus > 0 && energySurplus >= energySurplusMin) { + double energySpaceMin = pastCostAndPower[1] * intervalSize; + double energySpace = energyLevelMaxs[interval] - energyLevelCalculator.apply(pastInterval); + energySpace = Math.max(energySpace, energySpace - energySurplusMax); - // Adjust past interval set points as required - pastAndFutureIntervalPowerDeltas.forEach(intervalAndDelta -> powerSetpoints[intervalAndDelta.key] += intervalAndDelta.value); + // We have spare energy capacity and power check if we don't violate energy max for any + // future imports + k = pastInterval; + while (k < powerSetpoints.length && energySpace > 0 && energySpace >= energySpaceMin) { - energySurplusMax = Math.min(energySurplusMax, energySurplus); - powerCapacity = Math.max(expPowerMax - powerSetpoints[interval], -1d * (energySurplusMax / intervalSize)); - powerSetpoints[interval] = powerSetpoints[interval] + powerCapacity; - LOG.finest("Applied export earning opportunity: interval=" + interval + ", set point=" + powerSetpoints[interval] + " (delta: " + powerCapacity + ")"); + double futureEnergySpace = energyLevelMaxs[k] - energyLevelCalculator.apply(k); + energySpace = Math.min(energySpace, futureEnergySpace); + if (energySpace <= 0) { + LOG.finest( + "Earlier import opportunity would violate future energy max level: interval=" + + j + + ", futureInterval=" + + k); + } + k++; } - } - - /** - * Returns a function that can be used to calculate any export saving (per kWh) and power band needed to achieve it - * based on requested interval index and power export max value (negative as this is for export). This is used to - * determine whether there are export opportunities for earning/saving rather than using the grid. - */ - public BiFunction getExportOptimiser(double[] powerNets, double[] powerNetLimits, double[] tariffImports, double[] tariffExports, double assetExportCost) { - - // Power max should be negative as this is export - return (interval, powerMax) -> { - double powerNet = powerNets[interval]; - double powerNetLimit = powerNetLimits[interval]; - double tariffImport = tariffImports[interval]; - double tariffExport = tariffExports[interval]; - powerMax = Math.max(powerMax, powerNetLimit - powerNet); - - if (powerMax >= 0) { - // No capacity to export - return new double[]{Double.MAX_VALUE, 0d, 0d}; - } - - if (powerNet <= 0) { - // Already net exporting so tariff will not change if we export more - return new double[]{tariffExport + assetExportCost, powerMax, 0d}; - } - - if (powerNet + powerMax > 0d) { - // Can't make tariff flip (we're reducing import hence the -1d) - return new double[]{(-1d * tariffImport) + assetExportCost, powerMax, 0d}; - } - - // We can flip tariffs if we export enough power - double powerStart = 0d; - double powerEnd = 0d - powerNet; // Inflection point where switch to export instead of import - - // If import was paying then reducing import is a loss in earnings - double cost = powerEnd * (tariffImport - assetExportCost); - - // Is it beneficial to include remaining (-ve export power) - if (tariffExport + assetExportCost < 0d || tariffExport <= (-1d * tariffImport)) { - if (Math.abs((-1d * tariffImport) - tariffExport) > Double.MIN_VALUE) { - // We need to be at power max to achieve the optimum cost - powerStart = powerMax; - } - cost += -1d * (powerMax - powerEnd) * (tariffExport + assetExportCost); - powerEnd = powerMax; - } - // Normalise the cost - cost = cost / (-1d * powerEnd); + impPowerCapacity = Math.min(impPowerCapacity, energySpace / intervalSize); + + if (impPowerCapacity > 0 && impPowerCapacity > pastCostAndPower[1]) { + // We can import in the optimum range + energySurplus += (impPowerCapacity * intervalSize); + pastAndFutureIntervalPowerDeltas.add(new Pair<>(pastInterval, impPowerCapacity)); + LOG.finest( + "Earlier import opportunity identified: interval=" + + pastInterval + + ", power=" + + impPowerCapacity); + } - return new double[]{cost, powerEnd, powerStart}; - }; + j++; + } } - /** - * Returns a function that can be used to calculate the optimum cost (per kWh) and power band needed to achieve it - * based on requested interval index, power min and power max values. The returned power band will satisfy the - * requested power min value but this could mean that cost is not optimum for that interval if a lower power could - * be used. If possible a 0 power min should be tried first for all applicable intervals and if more energy is - * required then another pass can be made with a high enough min power to allow desired energy levels to be reached. - * This is used to determine the best times and power values for importing energy to meet the requirements. - */ - public BiFunction getImportOptimiser(double[] powerNets, double[] powerNetLimits, double[] tariffImports, double[] tariffExports, double assetImportCost) { - - return (interval, powerRequiredMinMax) -> { - - double powerNet = powerNets[interval]; - double powerNetLimit = powerNetLimits[interval]; - double tariffImport = tariffImports[interval]; - double tariffExport = tariffExports[interval]; - double powerMin = powerRequiredMinMax[0]; - double powerMax = Math.min(powerRequiredMinMax[1], powerNetLimit - powerNet); - - if (powerMax <= 0d) { - // No capacity to import - return new double[]{Double.MAX_VALUE, 0d, 0d}; - } - - if (powerNet >= 0d) { - // Already net importing so tariff will not change if we import more - return new double[]{tariffImport + assetImportCost, powerMin, powerMax}; - } - - if (powerNet + powerMax < 0d) { - // Can't make tariff flip (we're reducing import hence the -1d) - return new double[]{(-1d * tariffExport) + assetImportCost, powerMin, powerMax}; - } - - // We can flip tariffs if we take enough power - double powerStart = powerMin; - double powerEnd = 0d - powerNet; // Inflection point where switch to import instead of export - - // If export was paying then reducing export is a loss in earnings i.e. a cost and vice versa hence the -1d - double cost = -1d * powerEnd * (tariffExport - assetImportCost); - - if (powerMin > powerEnd) { - // We have to flip to meet power req - cost += (powerMin - powerEnd) * (tariffImport + assetImportCost); - powerEnd = powerMin; - } - - if (powerEnd < powerMax) { - // Is it beneficial to include remaining (+ve import power) - if (tariffImport + assetImportCost < 0d || tariffImport <= (-1d * tariffExport)) { - if (Math.abs((-1d * tariffExport) - tariffImport) > Double.MIN_VALUE) { - // We need to be at power max to achieve the optimum cost - powerStart = powerMax; - } - cost += (powerMax - powerEnd) * (tariffImport + assetImportCost); - powerEnd = powerMax; - } - } + // Do original export if there is any energy surplus + if (energySurplus > 0 && energySurplus >= energySurplusMin) { + + // Adjust past interval set points as required + pastAndFutureIntervalPowerDeltas.forEach( + intervalAndDelta -> powerSetpoints[intervalAndDelta.key] += intervalAndDelta.value); + + energySurplusMax = Math.min(energySurplusMax, energySurplus); + powerCapacity = + Math.max(expPowerMax - powerSetpoints[interval], -1d * (energySurplusMax / intervalSize)); + powerSetpoints[interval] = powerSetpoints[interval] + powerCapacity; + LOG.finest( + "Applied export earning opportunity: interval=" + + interval + + ", set point=" + + powerSetpoints[interval] + + " (delta: " + + powerCapacity + + ")"); + } + } + + /** + * Returns a function that can be used to calculate any export saving (per kWh) and power band + * needed to achieve it based on requested interval index and power export max value (negative as + * this is for export). This is used to determine whether there are export opportunities for + * earning/saving rather than using the grid. + */ + public BiFunction getExportOptimiser( + double[] powerNets, + double[] powerNetLimits, + double[] tariffImports, + double[] tariffExports, + double assetExportCost) { + + // Power max should be negative as this is export + return (interval, powerMax) -> { + double powerNet = powerNets[interval]; + double powerNetLimit = powerNetLimits[interval]; + double tariffImport = tariffImports[interval]; + double tariffExport = tariffExports[interval]; + powerMax = Math.max(powerMax, powerNetLimit - powerNet); + + if (powerMax >= 0) { + // No capacity to export + return new double[] {Double.MAX_VALUE, 0d, 0d}; + } + + if (powerNet <= 0) { + // Already net exporting so tariff will not change if we export more + return new double[] {tariffExport + assetExportCost, powerMax, 0d}; + } + + if (powerNet + powerMax > 0d) { + // Can't make tariff flip (we're reducing import hence the -1d) + return new double[] {(-1d * tariffImport) + assetExportCost, powerMax, 0d}; + } + + // We can flip tariffs if we export enough power + double powerStart = 0d; + double powerEnd = 0d - powerNet; // Inflection point where switch to export instead of import + + // If import was paying then reducing import is a loss in earnings + double cost = powerEnd * (tariffImport - assetExportCost); + + // Is it beneficial to include remaining (-ve export power) + if (tariffExport + assetExportCost < 0d || tariffExport <= (-1d * tariffImport)) { + if (Math.abs((-1d * tariffImport) - tariffExport) > Double.MIN_VALUE) { + // We need to be at power max to achieve the optimum cost + powerStart = powerMax; + } + cost += -1d * (powerMax - powerEnd) * (tariffExport + assetExportCost); + powerEnd = powerMax; + } + + // Normalise the cost + cost = cost / (-1d * powerEnd); + + return new double[] {cost, powerEnd, powerStart}; + }; + } + + /** + * Returns a function that can be used to calculate the optimum cost (per kWh) and power band + * needed to achieve it based on requested interval index, power min and power max values. The + * returned power band will satisfy the requested power min value but this could mean that cost is + * not optimum for that interval if a lower power could be used. If possible a 0 power min should + * be tried first for all applicable intervals and if more energy is required then another pass + * can be made with a high enough min power to allow desired energy levels to be reached. This is + * used to determine the best times and power values for importing energy to meet the + * requirements. + */ + public BiFunction getImportOptimiser( + double[] powerNets, + double[] powerNetLimits, + double[] tariffImports, + double[] tariffExports, + double assetImportCost) { + + return (interval, powerRequiredMinMax) -> { + double powerNet = powerNets[interval]; + double powerNetLimit = powerNetLimits[interval]; + double tariffImport = tariffImports[interval]; + double tariffExport = tariffExports[interval]; + double powerMin = powerRequiredMinMax[0]; + double powerMax = Math.min(powerRequiredMinMax[1], powerNetLimit - powerNet); + + if (powerMax <= 0d) { + // No capacity to import + return new double[] {Double.MAX_VALUE, 0d, 0d}; + } + + if (powerNet >= 0d) { + // Already net importing so tariff will not change if we import more + return new double[] {tariffImport + assetImportCost, powerMin, powerMax}; + } + + if (powerNet + powerMax < 0d) { + // Can't make tariff flip (we're reducing import hence the -1d) + return new double[] {(-1d * tariffExport) + assetImportCost, powerMin, powerMax}; + } + + // We can flip tariffs if we take enough power + double powerStart = powerMin; + double powerEnd = 0d - powerNet; // Inflection point where switch to import instead of export + + // If export was paying then reducing export is a loss in earnings i.e. a cost and vice versa + // hence the -1d + double cost = -1d * powerEnd * (tariffExport - assetImportCost); + + if (powerMin > powerEnd) { + // We have to flip to meet power req + cost += (powerMin - powerEnd) * (tariffImport + assetImportCost); + powerEnd = powerMin; + } + + if (powerEnd < powerMax) { + // Is it beneficial to include remaining (+ve import power) + if (tariffImport + assetImportCost < 0d || tariffImport <= (-1d * tariffExport)) { + if (Math.abs((-1d * tariffExport) - tariffImport) > Double.MIN_VALUE) { + // We need to be at power max to achieve the optimum cost + powerStart = powerMax; + } + cost += (powerMax - powerEnd) * (tariffImport + assetImportCost); + powerEnd = powerMax; + } + } - // Normalise the cost - cost = cost / powerEnd; + // Normalise the cost + cost = cost / powerEnd; - return new double[]{cost, powerStart, powerEnd}; - }; - } + return new double[] {cost, powerStart, powerEnd}; + }; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java index 07a1d1a..9352424 100644 --- a/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java +++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java @@ -1,7 +1,47 @@ +/* + * Copyright 2026, OpenRemote Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package org.openremote.extension.energy.manager; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME; +import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; +import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; +import static org.openremote.container.web.WebTargetBuilder.getClient; +import static org.openremote.manager.gateway.GatewayService.isNotForGateway; +import static org.openremote.model.syslog.SyslogCategory.DATA; +import static org.openremote.model.util.MapAccess.getString; + +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.*; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.ws.rs.core.Response; + import org.apache.camel.builder.RouteBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.openremote.container.message.MessageBrokerService; @@ -28,395 +68,503 @@ import org.openremote.model.query.AssetQuery; import org.openremote.model.syslog.SyslogCategory; -import java.io.IOException; -import java.net.URI; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.*; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; -import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME; -import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; -import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; -import static org.openremote.container.web.WebTargetBuilder.getClient; -import static org.openremote.manager.gateway.GatewayService.isNotForGateway; -import static org.openremote.model.syslog.SyslogCategory.DATA; -import static org.openremote.model.util.MapAccess.getString; +import jakarta.ws.rs.core.Response; /** - * Fills in power forecast from ForecastSolar (https://forecast.solar) for {@link ElectricityProducerSolarAsset}. + * Fills in power forecast from ForecastSolar (https://forecast.solar) for {@link + * ElectricityProducerSolarAsset}. */ public class ForecastSolarService extends RouteBuilder implements ContainerService { - protected static class EstimateResponse { - @JsonProperty - protected Result result; + protected static class EstimateResponse { + @JsonProperty protected Result result; + } + + protected static class Result { + @JsonProperty protected Map wattHours; + @JsonProperty protected Map wattHoursDay; + @JsonProperty protected Map watts; + } + + public static final String OR_FORECAST_SOLAR_API_KEY = "OR_FORECAST_SOLAR_API_KEY"; + + protected static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITHOUT_T = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral(' ') + .append(ISO_LOCAL_TIME) + .toFormatter(); + + protected ScheduledExecutorService scheduledExecutorService; + protected AssetStorageService assetStorageService; + protected AssetProcessingService assetProcessingService; + protected AssetPredictedDatapointService assetPredictedDatapointService; + protected GatewayService gatewayService; + protected ClientEventService clientEventService; + protected RulesService rulesService; + protected TimerService timerService; + + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, ForecastSolarService.class.getName()); + protected ResteasyWebTarget forecastSolarTarget; + private String forecastSolarApiKey; + private final Map electricityProducerSolarAssetMap = + new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public void configure() throws Exception { + from(PERSISTENCE_TOPIC) + .routeId("Persistence-ForecastSolar") + .filter(isPersistenceEventForEntityType(ElectricityProducerSolarAsset.class)) + .filter(isNotForGateway(gatewayService)) + .process( + exchange -> + processAssetChange( + (PersistenceEvent) + exchange.getIn().getBody(PersistenceEvent.class))); + } + + @Override + public void init(Container container) throws Exception { + assetStorageService = container.getService(AssetStorageService.class); + assetProcessingService = container.getService(AssetProcessingService.class); + gatewayService = container.getService(GatewayService.class); + assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); + clientEventService = container.getService(ClientEventService.class); + scheduledExecutorService = container.getScheduledExecutor(); + rulesService = container.getService(RulesService.class); + timerService = container.getService(TimerService.class); + + forecastSolarApiKey = getString(container.getConfig(), OR_FORECAST_SOLAR_API_KEY, null); + } + + @Override + public void start(Container container) throws Exception { + if (forecastSolarApiKey == null) { + LOG.fine("No value found for OR_FORECAST_SOLAR_API_KEY, ForecastSolarService won't start"); + return; } - protected static class Result { - @JsonProperty - protected Map wattHours; - @JsonProperty - protected Map wattHoursDay; - @JsonProperty - protected Map watts; + forecastSolarTarget = + new WebTargetBuilder( + getClient(), + URI.create("https://api.forecast.solar/" + forecastSolarApiKey + "/estimate")) + .build(); + + container.getService(MessageBrokerService.class).getContext().addRoutes(this); + + // Load all enabled producer solar assets + LOG.fine("Loading electricity producer solar assets..."); + + List electricityProducerSolarAssets = + assetStorageService + .findAll(new AssetQuery().types(ElectricityProducerSolarAsset.class)) + .stream() + .map(asset -> (ElectricityProducerSolarAsset) asset) + .filter( + electricityProducerSolarAsset -> + electricityProducerSolarAsset.isIncludeForecastSolarService().orElse(false)) + .toList(); + + LOG.fine( + "Number of electricity producer solar assets with forecast enabled = " + + electricityProducerSolarAssets.size()); + + for (ElectricityProducerSolarAsset electricityProducerSolarAsset : + electricityProducerSolarAssets) { + electricityProducerSolarAssetMap.put( + electricityProducerSolarAsset.getId(), electricityProducerSolarAsset); + getSolarForecast(electricityProducerSolarAsset); + updateSolarForecastAttribute(electricityProducerSolarAsset); } - public static final String OR_FORECAST_SOLAR_API_KEY = "OR_FORECAST_SOLAR_API_KEY"; - - protected static final DateTimeFormatter ISO_LOCAL_DATE_TIME_WITHOUT_T = new DateTimeFormatterBuilder() - .parseCaseInsensitive() - .append(ISO_LOCAL_DATE) - .appendLiteral(' ') - .append(ISO_LOCAL_TIME) - .toFormatter(); - - protected ScheduledExecutorService scheduledExecutorService; - protected AssetStorageService assetStorageService; - protected AssetProcessingService assetProcessingService; - protected AssetPredictedDatapointService assetPredictedDatapointService; - protected GatewayService gatewayService; - protected ClientEventService clientEventService; - protected RulesService rulesService; - protected TimerService timerService; - - protected static final Logger LOG = SyslogCategory.getLogger(DATA, ForecastSolarService.class.getName()); - protected ResteasyWebTarget forecastSolarTarget; - private String forecastSolarApiKey; - private final Map electricityProducerSolarAssetMap = new HashMap<>(); - - @SuppressWarnings("unchecked") - @Override - public void configure() throws Exception { - from(PERSISTENCE_TOPIC) - .routeId("Persistence-ForecastSolar") - .filter(isPersistenceEventForEntityType(ElectricityProducerSolarAsset.class)) - .filter(isNotForGateway(gatewayService)) - .process(exchange -> processAssetChange((PersistenceEvent) exchange.getIn().getBody(PersistenceEvent.class))); + // Start forecast solar thread + scheduledExecutorService.scheduleAtFixedRate(this::processSolarData, 1, 1, TimeUnit.MINUTES); + + clientEventService.addSubscription( + AttributeEvent.class, + new AssetFilter() + .setAssetClasses(Collections.singletonList(ElectricityProducerSolarAsset.class)), + this::processElectricityProducerSolarAssetAttributeEvent); + } + + @Override + public void stop(Container container) throws Exception { + // empty + } + + protected synchronized void processElectricityProducerSolarAssetAttributeEvent( + AttributeEvent attributeEvent) { + String attributeName = attributeEvent.getName(); + + // These are updated by this service + if (ElectricityProducerSolarAsset.POWER.getName().equals(attributeName) + || ElectricityProducerSolarAsset.POWER_FORECAST.getName().equals(attributeName)) { + return; } - @Override - public void init(Container container) throws Exception { - assetStorageService = container.getService(AssetStorageService.class); - assetProcessingService = container.getService(AssetProcessingService.class); - gatewayService = container.getService(GatewayService.class); - assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); - clientEventService = container.getService(ClientEventService.class); - scheduledExecutorService = container.getScheduledExecutor(); - rulesService = container.getService(RulesService.class); - timerService = container.getService(TimerService.class); - - forecastSolarApiKey = getString(container.getConfig(), OR_FORECAST_SOLAR_API_KEY, null); + // Set power attribute value with power forecast attribute value + if (attributeName.equals( + ElectricityProducerSolarAsset.SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST.getName())) { + boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); + + // Get latest asset from storage + ElectricityProducerSolarAsset asset = + (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); + + if (asset != null && enabled) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + asset.getId(), + ElectricityProducerSolarAsset.POWER, + asset.getPowerForecast().orElse(null)), + getClass().getSimpleName()); + } else if (asset != null) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent(asset.getId(), ElectricityProducerSolarAsset.POWER, null), + getClass().getSimpleName()); + } + return; } - @Override - public void start(Container container) throws Exception { - if (forecastSolarApiKey == null) { - LOG.fine("No value found for OR_FORECAST_SOLAR_API_KEY, ForecastSolarService won't start"); - return; + // Enable solar forecast + if (attributeName.equals( + ElectricityProducerSolarAsset.INCLUDE_FORECAST_SOLAR_SERVICE.getName())) { + boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); + + if (enabled && !electricityProducerSolarAssetMap.containsKey(attributeEvent.getId())) { + LOG.info( + String.format( + "Enabled solar forecast for ElectricityProducerSolarAsset: name='%s', ID='%s';", + attributeEvent.getAssetName(), attributeEvent.getId())); + + // Get latest asset from storage + ElectricityProducerSolarAsset asset = + (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); + + if (asset != null) { + electricityProducerSolarAssetMap.put(asset.getId(), asset); + getSolarForecast(asset); + updateSolarForecastAttribute(asset); } + } else if (!enabled && electricityProducerSolarAssetMap.containsKey(attributeEvent.getId())) { + LOG.info( + String.format( + "Disabled solar forecast for ElectricityProducerSolarAsset: name='%s', ID='%s';", + attributeEvent.getAssetName(), attributeEvent.getId())); + electricityProducerSolarAssetMap.remove(attributeEvent.getId()); + } + } - forecastSolarTarget = new WebTargetBuilder(getClient(), URI.create("https://api.forecast.solar/" + forecastSolarApiKey + "/estimate")).build(); - - container.getService(MessageBrokerService.class).getContext().addRoutes(this); - - // Load all enabled producer solar assets - LOG.fine("Loading electricity producer solar assets..."); - - List electricityProducerSolarAssets = assetStorageService.findAll( - new AssetQuery() - .types(ElectricityProducerSolarAsset.class) - ) - .stream() - .map(asset -> (ElectricityProducerSolarAsset) asset) - .filter(electricityProducerSolarAsset -> electricityProducerSolarAsset.isIncludeForecastSolarService().orElse(false)) - .toList(); - - LOG.fine("Number of electricity producer solar assets with forecast enabled = " + electricityProducerSolarAssets.size()); - - for (ElectricityProducerSolarAsset electricityProducerSolarAsset : electricityProducerSolarAssets) { - electricityProducerSolarAssetMap.put(electricityProducerSolarAsset.getId(), electricityProducerSolarAsset); - getSolarForecast(electricityProducerSolarAsset); - updateSolarForecastAttribute(electricityProducerSolarAsset); + // Update solar forecast + if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_AZIMUTH.getName()) + || attributeName.equals(ElectricityProducerSolarAsset.PANEL_PITCH.getName()) + || attributeName.equals(ElectricityProducerSolarAsset.POWER_EXPORT_MAX.getName()) + || attributeName.equals(ElectricityProducerSolarAsset.LOCATION.getName())) { + // Get latest asset from storage + ElectricityProducerSolarAsset asset = + (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); + + if (asset != null && asset.isIncludeForecastSolarService().orElse(false)) { + ElectricityProducerSolarAsset assetPrevious = + electricityProducerSolarAssetMap.get(asset.getId()); + + String valueStr = attributeEvent.getValue().toString(); + String valuePreviousStr = + assetPrevious + .getAttributes() + .get(attributeEvent.getName()) + .flatMap(Attribute::getValue) + .toString(); + + // Only update solar forecast on attribute value change + if (!valueStr.equals(valuePreviousStr)) { + Object value = attributeEvent.getValue().orElse(null); + + if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_AZIMUTH.getName())) { + asset.setPanelAzimuth((Integer) value); + } else if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_PITCH.getName())) { + asset.setPanelPitch((Integer) value); + } else if (attributeName.equals( + ElectricityProducerSolarAsset.POWER_EXPORT_MAX.getName())) { + asset.setPowerExportMax((Double) value); + } else if (attributeName.equals(ElectricityProducerSolarAsset.LOCATION.getName())) { + asset.setLocation((GeoJSONPoint) value); + } + + getSolarForecast(asset); + updateSolarForecastAttribute(asset); } - - // Start forecast solar thread - scheduledExecutorService.scheduleAtFixedRate(this::processSolarData, 1, 1, TimeUnit.MINUTES); - - clientEventService.addSubscription( - AttributeEvent.class, - new AssetFilter().setAssetClasses(Collections.singletonList(ElectricityProducerSolarAsset.class)), - this::processElectricityProducerSolarAssetAttributeEvent); + electricityProducerSolarAssetMap.put(asset.getId(), asset); + } } - - @Override - public void stop(Container container) throws Exception { - // empty + } + + protected void processAssetChange( + PersistenceEvent persistenceEvent) { + LOG.fine("Processing producer solar asset change: " + persistenceEvent); + + if (persistenceEvent.getCause() == PersistenceEvent.Cause.CREATE + && persistenceEvent.getEntity().isIncludeForecastSolarService().orElse(false)) { + electricityProducerSolarAssetMap.put( + persistenceEvent.getEntity().getId(), persistenceEvent.getEntity()); + getSolarForecast(persistenceEvent.getEntity()); + updateSolarForecastAttribute(persistenceEvent.getEntity()); + } else if (persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE) { + electricityProducerSolarAssetMap.remove(persistenceEvent.getEntity().getId()); } + } - protected synchronized void processElectricityProducerSolarAssetAttributeEvent(AttributeEvent attributeEvent) { - String attributeName = attributeEvent.getName(); - - // These are updated by this service - if (ElectricityProducerSolarAsset.POWER.getName().equals(attributeName) - || ElectricityProducerSolarAsset.POWER_FORECAST.getName().equals(attributeName)) { - return; - } - - // Set power attribute value with power forecast attribute value - if (attributeName.equals(ElectricityProducerSolarAsset.SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST.getName())) { - boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); - - // Get latest asset from storage - ElectricityProducerSolarAsset asset = (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); - - if (asset != null && enabled) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(asset.getId(), ElectricityProducerSolarAsset.POWER, asset.getPowerForecast().orElse(null)), getClass().getSimpleName()); - } else if (asset != null) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(asset.getId(), ElectricityProducerSolarAsset.POWER, null), getClass().getSimpleName()); - } - return; - } + protected void processSolarData() { + // Check if there are any electricity producer solar assets to process + if (electricityProducerSolarAssetMap.isEmpty()) { + return; + } - // Enable solar forecast - if (attributeName.equals(ElectricityProducerSolarAsset.INCLUDE_FORECAST_SOLAR_SERVICE.getName())) { - boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); + int currentMinute = LocalDateTime.now().getMinute(); - if (enabled && !electricityProducerSolarAssetMap.containsKey(attributeEvent.getId())) { - LOG.info(String.format("Enabled solar forecast for ElectricityProducerSolarAsset: name='%s', ID='%s';", attributeEvent.getAssetName(), attributeEvent.getId())); + // Update solar forecast every hour + if (currentMinute == 0) { + electricityProducerSolarAssetMap.forEach( + (assetId, electricityProducerSolarAsset) -> + getSolarForecast(electricityProducerSolarAsset)); + } - // Get latest asset from storage - ElectricityProducerSolarAsset asset = (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); + // Update solar power forecast attribute every 15 minutes + if ((currentMinute % 15) == 0) { + electricityProducerSolarAssetMap.forEach( + (assetId, electricityProducerSolarAsset) -> + updateSolarForecastAttribute(electricityProducerSolarAsset)); + } + } + + protected void getSolarForecast(ElectricityProducerSolarAsset electricityProducerSolarAsset) { + Optional lat = + electricityProducerSolarAsset + .getAttribute(Asset.LOCATION) + .flatMap(attr -> attr.getValue().map(GeoJSONPoint::getY)); + Optional lon = + electricityProducerSolarAsset + .getAttribute(Asset.LOCATION) + .flatMap(attr -> attr.getValue().map(GeoJSONPoint::getX)); + Optional pitch = electricityProducerSolarAsset.getPanelPitch(); + Optional azimuth = electricityProducerSolarAsset.getPanelAzimuth(); + Optional kwp = electricityProducerSolarAsset.getPowerExportMax(); + + if (lat.isEmpty() || lon.isEmpty() || pitch.isEmpty() || azimuth.isEmpty() || kwp.isEmpty()) { + LOG.warning( + String.format( + "ElectricityProducerSolarAsset: name='%s', ID='%s' doesn't have all needed attributes filled in;" + + " latitude='%s', longitude='%s', panelAzimuth='%s', panelPitch='%s', powerExportMax='%s'", + electricityProducerSolarAsset.getName(), + electricityProducerSolarAsset.getId(), + lat, + lon, + azimuth, + pitch, + kwp)); + return; + } - if (asset != null) { - electricityProducerSolarAssetMap.put(asset.getId(), asset); - getSolarForecast(asset); - updateSolarForecastAttribute(asset); + try (Response response = + forecastSolarTarget + .path( + String.format( + "%f/%f/%d/%d/%f", lat.get(), lon.get(), pitch.get(), azimuth.get(), kwp.get())) + .request() + .build("GET") + .invoke()) { + if (response != null && response.getStatus() == 200) { + EstimateResponse responseModel = response.readEntity(EstimateResponse.class); + + if (responseModel != null) { + HashMap solarForecast = new HashMap<>(); + HashMap solarForecastPrevious = new HashMap<>(); + + // Get previous solar forecast from database + List solarForecastListPrevious = + assetPredictedDatapointService.getDatapoints( + new AttributeRef( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER_FORECAST.getName())); + + for (ValueDatapoint datapoint : solarForecastListPrevious) { + LocalDateTime dateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(datapoint.getTimestamp()), ZoneId.systemDefault()); + Double powerKiloWatt = (Double) datapoint.getValue(); + solarForecastPrevious.put(dateTime, powerKiloWatt); + } + + // Get start and end dateTime of solar forecast + String minKey = + responseModel.result.watts.keySet().stream().min(String::compareTo).orElse(""); + String maxKey = + responseModel.result.watts.keySet().stream().max(String::compareTo).orElse(""); + LocalDateTime startDateTime = + LocalDateTime.parse(minKey, ISO_LOCAL_DATE_TIME_WITHOUT_T) + .toLocalDate() + .atStartOfDay(); + LocalDateTime endDateTime = + LocalDateTime.parse(maxKey, ISO_LOCAL_DATE_TIME_WITHOUT_T) + .toLocalDate() + .plusDays(1) + .atStartOfDay(); + + // Prepopulate solar forecast map with 15-minute intervals + for (LocalDateTime dateTime = startDateTime; + !dateTime.isAfter(endDateTime); + dateTime = dateTime.plusMinutes(15)) { + solarForecast.put(dateTime, 0.0); + } + + // Add solar forecast to solar forecast map + for (Map.Entry wattItem : responseModel.result.watts.entrySet()) { + LocalDateTime dateTime = + LocalDateTime.parse(wattItem.getKey(), ISO_LOCAL_DATE_TIME_WITHOUT_T); + Double powerKiloWatt = -wattItem.getValue() / 1000; + solarForecast.put(dateTime, powerKiloWatt); + } + + LocalDateTime currentDateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(timerService.getCurrentTimeMillis()), + ZoneId.systemDefault()); + + // Update solar forecast in database + solarForecast.forEach( + (dateTime, powerKiloWatt) -> { + if (dateTime.isAfter(currentDateTime) + || solarForecastPrevious.get(dateTime) == null) { + assetPredictedDatapointService.updateValue( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER_FORECAST.getName(), + powerKiloWatt, + dateTime); + assetPredictedDatapointService.updateValue( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER.getName(), + powerKiloWatt, + dateTime); } - } else if (!enabled && electricityProducerSolarAssetMap.containsKey(attributeEvent.getId())) { - LOG.info(String.format("Disabled solar forecast for ElectricityProducerSolarAsset: name='%s', ID='%s';", attributeEvent.getAssetName(), attributeEvent.getId())); - electricityProducerSolarAssetMap.remove(attributeEvent.getId()); - } + }); } - - // Update solar forecast - if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_AZIMUTH.getName()) || - attributeName.equals(ElectricityProducerSolarAsset.PANEL_PITCH.getName()) || - attributeName.equals(ElectricityProducerSolarAsset.POWER_EXPORT_MAX.getName()) || - attributeName.equals(ElectricityProducerSolarAsset.LOCATION.getName())) { - // Get latest asset from storage - ElectricityProducerSolarAsset asset = (ElectricityProducerSolarAsset) assetStorageService.find(attributeEvent.getId()); - - if (asset != null && asset.isIncludeForecastSolarService().orElse(false)) { - ElectricityProducerSolarAsset assetPrevious = electricityProducerSolarAssetMap.get(asset.getId()); - - String valueStr = attributeEvent.getValue().toString(); - String valuePreviousStr = assetPrevious.getAttributes().get(attributeEvent.getName()).flatMap(Attribute::getValue).toString(); - - // Only update solar forecast on attribute value change - if (!valueStr.equals(valuePreviousStr)) { - Object value = attributeEvent.getValue().orElse(null); - - if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_AZIMUTH.getName())) { - asset.setPanelAzimuth((Integer) value); - } else if (attributeName.equals(ElectricityProducerSolarAsset.PANEL_PITCH.getName())) { - asset.setPanelPitch((Integer) value); - } else if (attributeName.equals(ElectricityProducerSolarAsset.POWER_EXPORT_MAX.getName())) { - asset.setPowerExportMax((Double) value); - } else if (attributeName.equals(ElectricityProducerSolarAsset.LOCATION.getName())) { - asset.setLocation((GeoJSONPoint) value); - } - - getSolarForecast(asset); - updateSolarForecastAttribute(asset); - } - electricityProducerSolarAssetMap.put(asset.getId(), asset); - } + rulesService.fireDeploymentsWithPredictedDataForAsset( + electricityProducerSolarAsset.getId()); + } else { + StringBuilder message = new StringBuilder("Unknown"); + if (response != null) { + message.setLength(0); + message.append("Status "); + message.append(response.getStatus()); + message.append(" - "); + message.append(response.readEntity(String.class)); } + LOG.warning("Request failed: " + message); + } + } catch (Throwable e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + LOG.log(Level.SEVERE, "Exception when requesting forecast solar data", e.getCause()); + } else { + LOG.log(Level.SEVERE, "Exception when requesting forecast solar data", e); + } } - - protected void processAssetChange(PersistenceEvent persistenceEvent) { - LOG.fine("Processing producer solar asset change: " + persistenceEvent); - - if (persistenceEvent.getCause() == PersistenceEvent.Cause.CREATE && persistenceEvent.getEntity().isIncludeForecastSolarService().orElse(false)) { - electricityProducerSolarAssetMap.put(persistenceEvent.getEntity().getId(), persistenceEvent.getEntity()); - getSolarForecast(persistenceEvent.getEntity()); - updateSolarForecastAttribute(persistenceEvent.getEntity()); - } else if (persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE) { - electricityProducerSolarAssetMap.remove(persistenceEvent.getEntity().getId()); - } + } + + protected void updateSolarForecastAttribute( + ElectricityProducerSolarAsset electricityProducerSolarAsset) { + Optional lat = + electricityProducerSolarAsset + .getAttribute(Asset.LOCATION) + .flatMap(attr -> attr.getValue().map(GeoJSONPoint::getY)); + Optional lon = + electricityProducerSolarAsset + .getAttribute(Asset.LOCATION) + .flatMap(attr -> attr.getValue().map(GeoJSONPoint::getX)); + Optional pitch = electricityProducerSolarAsset.getPanelPitch(); + Optional azimuth = electricityProducerSolarAsset.getPanelAzimuth(); + Optional kwp = electricityProducerSolarAsset.getPowerExportMax(); + + if (lat.isEmpty() || lon.isEmpty() || pitch.isEmpty() || azimuth.isEmpty() || kwp.isEmpty()) { + return; } - - protected void processSolarData() { - // Check if there are any electricity producer solar assets to process - if (electricityProducerSolarAssetMap.isEmpty()) { - return; - } - - int currentMinute = LocalDateTime.now().getMinute(); - - // Update solar forecast every hour - if (currentMinute == 0) { - electricityProducerSolarAssetMap.forEach((assetId, electricityProducerSolarAsset) -> getSolarForecast(electricityProducerSolarAsset)); - } - - // Update solar power forecast attribute every 15 minutes - if ((currentMinute % 15) == 0) { - electricityProducerSolarAssetMap.forEach((assetId, electricityProducerSolarAsset) -> updateSolarForecastAttribute(electricityProducerSolarAsset)); - } + // Get solar forecast data-points for current 15 minute interval + long currentTimeMillis = timerService.getCurrentTimeMillis(); + long startTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000); + long endTimeMillis = startTimeMillis + 15 * 60000; + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> solarForecastDatapoints = + assetPredictedDatapointService.queryDatapoints( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER_FORECAST.getName(), + assetDatapointQuery); + + if (solarForecastDatapoints == null || solarForecastDatapoints.size() < 2) { + LOG.warning( + String.format( + "ElectricityProducerSolarAsset: name='%s', ID='%s' doesn't have a solar forecast", + electricityProducerSolarAsset.getName(), electricityProducerSolarAsset.getId())); + return; } - protected void getSolarForecast(ElectricityProducerSolarAsset electricityProducerSolarAsset) { - Optional lat = electricityProducerSolarAsset.getAttribute(Asset.LOCATION).flatMap(attr -> attr.getValue().map(GeoJSONPoint::getY)); - Optional lon = electricityProducerSolarAsset.getAttribute(Asset.LOCATION).flatMap(attr -> attr.getValue().map(GeoJSONPoint::getX)); - Optional pitch = electricityProducerSolarAsset.getPanelPitch(); - Optional azimuth = electricityProducerSolarAsset.getPanelAzimuth(); - Optional kwp = electricityProducerSolarAsset.getPowerExportMax(); - - if (lat.isEmpty() || lon.isEmpty() || pitch.isEmpty() || azimuth.isEmpty() || kwp.isEmpty()) { - LOG.warning(String.format("ElectricityProducerSolarAsset: name='%s', ID='%s' doesn't have all needed attributes filled in;" + - " latitude='%s', longitude='%s', panelAzimuth='%s', panelPitch='%s', powerExportMax='%s'", - electricityProducerSolarAsset.getName(), electricityProducerSolarAsset.getId(), lat, lon, azimuth, pitch, kwp)); - return; - } - - try (Response response = forecastSolarTarget - .path(String.format("%f/%f/%d/%d/%f", lat.get(), lon.get(), pitch.get(), azimuth.get(), kwp.get())) - .request() - .build("GET") - .invoke()) { - if (response != null && response.getStatus() == 200) { - EstimateResponse responseModel = response.readEntity(EstimateResponse.class); - - if (responseModel != null) { - HashMap solarForecast = new HashMap<>(); - HashMap solarForecastPrevious = new HashMap<>(); - - // Get previous solar forecast from database - List solarForecastListPrevious = assetPredictedDatapointService.getDatapoints(new AttributeRef(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName())); - - for (ValueDatapoint datapoint : solarForecastListPrevious) { - LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(datapoint.getTimestamp()), ZoneId.systemDefault()); - Double powerKiloWatt = (Double) datapoint.getValue(); - solarForecastPrevious.put(dateTime, powerKiloWatt); - } - - // Get start and end dateTime of solar forecast - String minKey = responseModel.result.watts.keySet().stream().min(String::compareTo).orElse(""); - String maxKey = responseModel.result.watts.keySet().stream().max(String::compareTo).orElse(""); - LocalDateTime startDateTime = LocalDateTime.parse(minKey, ISO_LOCAL_DATE_TIME_WITHOUT_T).toLocalDate().atStartOfDay(); - LocalDateTime endDateTime = LocalDateTime.parse(maxKey, ISO_LOCAL_DATE_TIME_WITHOUT_T).toLocalDate().plusDays(1).atStartOfDay(); - - // Prepopulate solar forecast map with 15-minute intervals - for (LocalDateTime dateTime = startDateTime; !dateTime.isAfter(endDateTime); dateTime = dateTime.plusMinutes(15)) { - solarForecast.put(dateTime, 0.0); - } - - // Add solar forecast to solar forecast map - for (Map.Entry wattItem : responseModel.result.watts.entrySet()) { - LocalDateTime dateTime = LocalDateTime.parse(wattItem.getKey(), ISO_LOCAL_DATE_TIME_WITHOUT_T); - Double powerKiloWatt = -wattItem.getValue() / 1000; - solarForecast.put(dateTime, powerKiloWatt); - } - - LocalDateTime currentDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timerService.getCurrentTimeMillis()), ZoneId.systemDefault()); - - // Update solar forecast in database - solarForecast.forEach((dateTime, powerKiloWatt) -> { - if (dateTime.isAfter(currentDateTime) || solarForecastPrevious.get(dateTime) == null) { - assetPredictedDatapointService.updateValue(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName(), powerKiloWatt, dateTime); - assetPredictedDatapointService.updateValue(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER.getName(), powerKiloWatt, dateTime); - } - }); - } - rulesService.fireDeploymentsWithPredictedDataForAsset(electricityProducerSolarAsset.getId()); - } else { - StringBuilder message = new StringBuilder("Unknown"); - if (response != null) { - message.setLength(0); - message.append("Status "); - message.append(response.getStatus()); - message.append(" - "); - message.append(response.readEntity(String.class)); - } - LOG.warning("Request failed: " + message); - } - } catch (Throwable e) { - if (e.getCause() != null && e.getCause() instanceof IOException) { - LOG.log(Level.SEVERE, "Exception when requesting forecast solar data", e.getCause()); - } else { - LOG.log(Level.SEVERE, "Exception when requesting forecast solar data", e); - } - } + ValueDatapoint solarForecastDatapointMax = solarForecastDatapoints.getFirst(); + ValueDatapoint solarForecastDatapointMin = solarForecastDatapoints.getLast(); + + // Get current timestamp of power forecast attribute + ElectricityProducerSolarAsset asset = + (ElectricityProducerSolarAsset) + assetStorageService.find(electricityProducerSolarAsset.getId()); + long powerForecastAttributeTimeMillis = + asset + .getAttributes() + .get(ElectricityProducerSolarAsset.POWER_FORECAST) + .flatMap(Attribute::getTimestamp) + .orElse(0L); + + // Update power forecast attribute value + Double powerKiloWatt; + long timeMillis; + + if (solarForecastDatapointMin.getTimestamp() > powerForecastAttributeTimeMillis) { + powerKiloWatt = (Double) solarForecastDatapointMin.getValue(); + timeMillis = solarForecastDatapointMin.getTimestamp(); + } else { + long upperTimestamp = solarForecastDatapointMax.getTimestamp(); + long lowerTimestamp = solarForecastDatapointMin.getTimestamp(); + Double upperValue = (Double) solarForecastDatapointMax.getValue(); + Double lowerValue = (Double) solarForecastDatapointMin.getValue(); + + if (upperValue == null || lowerValue == null) { + return; + } + + // Interpolate value + double factor = + (double) (currentTimeMillis - lowerTimestamp) / (upperTimestamp - lowerTimestamp); + double interpolatedValue = lowerValue + factor * (upperValue - lowerValue); + powerKiloWatt = Math.round(interpolatedValue * 1000.0) / 1000.0; + timeMillis = currentTimeMillis; } - protected void updateSolarForecastAttribute(ElectricityProducerSolarAsset electricityProducerSolarAsset) { - Optional lat = electricityProducerSolarAsset.getAttribute(Asset.LOCATION).flatMap(attr -> attr.getValue().map(GeoJSONPoint::getY)); - Optional lon = electricityProducerSolarAsset.getAttribute(Asset.LOCATION).flatMap(attr -> attr.getValue().map(GeoJSONPoint::getX)); - Optional pitch = electricityProducerSolarAsset.getPanelPitch(); - Optional azimuth = electricityProducerSolarAsset.getPanelAzimuth(); - Optional kwp = electricityProducerSolarAsset.getPowerExportMax(); - - if (lat.isEmpty() || lon.isEmpty() || pitch.isEmpty() || azimuth.isEmpty() || kwp.isEmpty()) { - return; - } - - // Get solar forecast data-points for current 15 minute interval - long currentTimeMillis = timerService.getCurrentTimeMillis(); - long startTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000); - long endTimeMillis = startTimeMillis + 15 * 60000; - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> solarForecastDatapoints = assetPredictedDatapointService.queryDatapoints(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName(), assetDatapointQuery); - - if (solarForecastDatapoints == null || solarForecastDatapoints.size() < 2) { - LOG.warning(String.format("ElectricityProducerSolarAsset: name='%s', ID='%s' doesn't have a solar forecast", electricityProducerSolarAsset.getName(), electricityProducerSolarAsset.getId())); - return; - } - - ValueDatapoint solarForecastDatapointMax = solarForecastDatapoints.getFirst(); - ValueDatapoint solarForecastDatapointMin = solarForecastDatapoints.getLast(); - - // Get current timestamp of power forecast attribute - ElectricityProducerSolarAsset asset = (ElectricityProducerSolarAsset) assetStorageService.find(electricityProducerSolarAsset.getId()); - long powerForecastAttributeTimeMillis = asset.getAttributes().get(ElectricityProducerSolarAsset.POWER_FORECAST).flatMap(Attribute::getTimestamp).orElse(0L); - - // Update power forecast attribute value - Double powerKiloWatt; - long timeMillis; - - if (solarForecastDatapointMin.getTimestamp() > powerForecastAttributeTimeMillis) { - powerKiloWatt = (Double) solarForecastDatapointMin.getValue(); - timeMillis = solarForecastDatapointMin.getTimestamp(); - } else { - long upperTimestamp = solarForecastDatapointMax.getTimestamp(); - long lowerTimestamp = solarForecastDatapointMin.getTimestamp(); - Double upperValue = (Double) solarForecastDatapointMax.getValue(); - Double lowerValue = (Double) solarForecastDatapointMin.getValue(); - - if (upperValue == null || lowerValue == null) { - return; - } - - // Interpolate value - double factor = (double) (currentTimeMillis - lowerTimestamp) / (upperTimestamp - lowerTimestamp); - double interpolatedValue = lowerValue + factor * (upperValue - lowerValue); - powerKiloWatt = Math.round(interpolatedValue * 1000.0) / 1000.0; - timeMillis = currentTimeMillis; - } - - // Update attributes - assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName(), powerKiloWatt, timeMillis)); - - if (electricityProducerSolarAsset.isSetActualSolarValueWithForecast().orElse(false)) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerSolarAsset.getId(), ElectricityProducerSolarAsset.POWER.getName(), powerKiloWatt, timeMillis)); - } + // Update attributes + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER_FORECAST.getName(), + powerKiloWatt, + timeMillis)); + + if (electricityProducerSolarAsset.isSetActualSolarValueWithForecast().orElse(false)) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + electricityProducerSolarAsset.getId(), + ElectricityProducerSolarAsset.POWER.getName(), + powerKiloWatt, + timeMillis)); } + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java index c002699..6194c71 100644 --- a/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java +++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java @@ -1,8 +1,46 @@ +/* + * Copyright 2026, OpenRemote Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ package org.openremote.extension.energy.manager; +import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; +import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; +import static org.openremote.container.web.WebTargetBuilder.getClient; +import static org.openremote.manager.gateway.GatewayService.isNotForGateway; +import static org.openremote.model.syslog.SyslogCategory.DATA; +import static org.openremote.model.util.MapAccess.getString; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriBuilder; + import org.apache.camel.builder.RouteBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.openremote.container.message.MessageBrokerService; @@ -22,323 +60,362 @@ import org.openremote.model.query.AssetQuery; import org.openremote.model.syslog.SyslogCategory; -import java.io.IOException; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC; -import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType; -import static org.openremote.container.web.WebTargetBuilder.getClient; -import static org.openremote.manager.gateway.GatewayService.isNotForGateway; -import static org.openremote.model.syslog.SyslogCategory.DATA; -import static org.openremote.model.util.MapAccess.getString; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; -/** - * Calculates power generation for {@link ElectricityProducerWindAsset}. - */ +/** Calculates power generation for {@link ElectricityProducerWindAsset}. */ public class ForecastWindService extends RouteBuilder implements ContainerService { - protected static class WeatherForecastResponseModel { + protected static class WeatherForecastResponseModel { - protected WeatherForecastModel current; + protected WeatherForecastModel current; - @JsonProperty("hourly") - protected WeatherForecastModel[] list; + @JsonProperty("hourly") + protected WeatherForecastModel[] list; - public WeatherForecastModel[] getList() { - return list; - } + public WeatherForecastModel[] getList() { + return list; } + } - protected static class WeatherForecastModel { - - /** - * Seconds - */ - @JsonProperty("dt") - protected long timestamp; + protected static class WeatherForecastModel { - @JsonProperty("temp") - protected double tempature; + /** Seconds */ + @JsonProperty("dt") + protected long timestamp; - @JsonProperty("humidity") - protected int humidity; + @JsonProperty("temp") + protected double tempature; - @JsonProperty("wind_speed") - protected double windSpeed; + @JsonProperty("humidity") + protected int humidity; - @JsonProperty("wind_deg") - protected int windDirection; + @JsonProperty("wind_speed") + protected double windSpeed; - @JsonProperty("uvi") - protected double uv; + @JsonProperty("wind_deg") + protected int windDirection; - public long getTimestamp() { - return timestamp * 1000; - } - - public double getTempature() { - return tempature; - } - - public int getHumidity() { - return humidity; - } + @JsonProperty("uvi") + protected double uv; - public double getWindSpeed() { - return windSpeed; - } + public long getTimestamp() { + return timestamp * 1000; + } - public int getWindDirection() { - return windDirection; - } + public double getTempature() { + return tempature; + } - public double getUv() { - return uv; - } + public int getHumidity() { + return humidity; } - public static final String OR_OPEN_WEATHER_API_APP_ID = "OR_OPEN_WEATHER_API_APP_ID"; - - protected static final Logger LOG = SyslogCategory.getLogger(DATA, ForecastWindService.class.getName()); - protected AssetStorageService assetStorageService; - protected AssetProcessingService assetProcessingService; - protected GatewayService gatewayService; - protected AssetPredictedDatapointService assetPredictedDatapointService; - protected ClientEventService clientEventService; - protected ScheduledExecutorService scheduledExecutorService; - protected RulesService rulesService; - private ResteasyWebTarget weatherForecastWebTarget; - private String openWeatherAppId; - - private final Map> calculationFutures = new HashMap<>(); - - @SuppressWarnings("unchecked") - @Override - public void configure() throws Exception { - from(PERSISTENCE_TOPIC) - .routeId("Persistence-ForecastWind") - .filter(isPersistenceEventForEntityType(ElectricityProducerWindAsset.class)) - .filter(isNotForGateway(gatewayService)) - .process(exchange -> processAssetChange((PersistenceEvent) exchange.getIn().getBody(PersistenceEvent.class))); + public double getWindSpeed() { + return windSpeed; } - @Override - public void init(Container container) throws Exception { - assetStorageService = container.getService(AssetStorageService.class); - assetProcessingService = container.getService(AssetProcessingService.class); - gatewayService = container.getService(GatewayService.class); - assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); - clientEventService = container.getService(ClientEventService.class); - scheduledExecutorService = container.getScheduledExecutor(); - rulesService = container.getService(RulesService.class); - - openWeatherAppId = getString(container.getConfig(), OR_OPEN_WEATHER_API_APP_ID, null); + public int getWindDirection() { + return windDirection; } - @Override - public void start(Container container) throws Exception { - if (openWeatherAppId == null) { - LOG.fine("No value found for OR_OPEN_WEATHER_API_APP_ID, ForecastWindService won't start"); - return; - } + public double getUv() { + return uv; + } + } + + public static final String OR_OPEN_WEATHER_API_APP_ID = "OR_OPEN_WEATHER_API_APP_ID"; + + protected static final Logger LOG = + SyslogCategory.getLogger(DATA, ForecastWindService.class.getName()); + protected AssetStorageService assetStorageService; + protected AssetProcessingService assetProcessingService; + protected GatewayService gatewayService; + protected AssetPredictedDatapointService assetPredictedDatapointService; + protected ClientEventService clientEventService; + protected ScheduledExecutorService scheduledExecutorService; + protected RulesService rulesService; + private ResteasyWebTarget weatherForecastWebTarget; + private String openWeatherAppId; + + private final Map> calculationFutures = new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public void configure() throws Exception { + from(PERSISTENCE_TOPIC) + .routeId("Persistence-ForecastWind") + .filter(isPersistenceEventForEntityType(ElectricityProducerWindAsset.class)) + .filter(isNotForGateway(gatewayService)) + .process( + exchange -> + processAssetChange( + (PersistenceEvent) + exchange.getIn().getBody(PersistenceEvent.class))); + } + + @Override + public void init(Container container) throws Exception { + assetStorageService = container.getService(AssetStorageService.class); + assetProcessingService = container.getService(AssetProcessingService.class); + gatewayService = container.getService(GatewayService.class); + assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class); + clientEventService = container.getService(ClientEventService.class); + scheduledExecutorService = container.getScheduledExecutor(); + rulesService = container.getService(RulesService.class); + + openWeatherAppId = getString(container.getConfig(), OR_OPEN_WEATHER_API_APP_ID, null); + } + + @Override + public void start(Container container) throws Exception { + if (openWeatherAppId == null) { + LOG.fine("No value found for OR_OPEN_WEATHER_API_APP_ID, ForecastWindService won't start"); + return; + } - weatherForecastWebTarget = new WebTargetBuilder( + weatherForecastWebTarget = + new WebTargetBuilder( getClient(), UriBuilder.fromUri("https://api.openweathermap.org/data/2.5") - .queryParam("units", "metric") - .queryParam("exclude", "minutely,daily,alerts") - .queryParam("appid", openWeatherAppId).build()).build(); - - container.getService(MessageBrokerService.class).getContext().addRoutes(this); - - // Load all enabled producer wind assets - LOG.fine("Loading producer wind assets..."); - - List electricityProducerWindAssets = assetStorageService.findAll( - new AssetQuery() - .types(ElectricityProducerWindAsset.class) - ) - .stream() - .map(asset -> (ElectricityProducerWindAsset) asset) - .filter(electricityProducerWindAsset -> electricityProducerWindAsset.isIncludeForecastWindService().orElse(false) + .queryParam("units", "metric") + .queryParam("exclude", "minutely,daily,alerts") + .queryParam("appid", openWeatherAppId) + .build()) + .build(); + + container.getService(MessageBrokerService.class).getContext().addRoutes(this); + + // Load all enabled producer wind assets + LOG.fine("Loading producer wind assets..."); + + List electricityProducerWindAssets = + assetStorageService + .findAll(new AssetQuery().types(ElectricityProducerWindAsset.class)) + .stream() + .map(asset -> (ElectricityProducerWindAsset) asset) + .filter( + electricityProducerWindAsset -> + electricityProducerWindAsset.isIncludeForecastWindService().orElse(false) && electricityProducerWindAsset.getLocation().isPresent()) - .toList(); + .toList(); - LOG.fine("Found includes producer wind asset count = " + electricityProducerWindAssets.size()); + LOG.fine("Found includes producer wind asset count = " + electricityProducerWindAssets.size()); - electricityProducerWindAssets.forEach(this::startCalculation); + electricityProducerWindAssets.forEach(this::startCalculation); - clientEventService.addSubscription( - AttributeEvent.class, - null, - this::processAttributeEvent); - } + clientEventService.addSubscription(AttributeEvent.class, null, this::processAttributeEvent); + } - @Override - public void stop(Container container) throws Exception { - new ArrayList<>(calculationFutures.keySet()).forEach(this::stopCalculation); - } + @Override + public void stop(Container container) throws Exception { + new ArrayList<>(calculationFutures.keySet()).forEach(this::stopCalculation); + } + + protected void processAttributeEvent(AttributeEvent attributeEvent) { + processElectricityProducerWindAssetAttributeEvent(attributeEvent); + } - protected void processAttributeEvent(AttributeEvent attributeEvent) { - processElectricityProducerWindAssetAttributeEvent(attributeEvent); + protected synchronized void processElectricityProducerWindAssetAttributeEvent( + AttributeEvent attributeEvent) { + + if (ElectricityProducerWindAsset.POWER.getName().equals(attributeEvent.getName()) + || ElectricityProducerWindAsset.POWER_FORECAST.getName().equals(attributeEvent.getName())) { + // These are updated by this service + return; } - protected synchronized void processElectricityProducerWindAssetAttributeEvent(AttributeEvent attributeEvent) { + if (attributeEvent + .getName() + .equals(ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.getName())) { + boolean enabled = (Boolean) attributeEvent.getValue().orElse(false); + if (enabled && calculationFutures.containsKey(attributeEvent.getId())) { + // Nothing to do here + return; + } else if (!enabled && !calculationFutures.containsKey(attributeEvent.getId())) { + // Nothing to do here + return; + } + + LOG.fine("Processing producer wind asset attribute event: " + attributeEvent); + stopCalculation(attributeEvent.getId()); + + // Get latest asset from storage + ElectricityProducerWindAsset asset = + (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId()); + + if (asset != null + && asset.isIncludeForecastWindService().orElse(false) + && asset.getLocation().isPresent()) { + startCalculation(asset); + } + } - if (ElectricityProducerWindAsset.POWER.getName().equals(attributeEvent.getName()) - || ElectricityProducerWindAsset.POWER_FORECAST.getName().equals(attributeEvent.getName())) { - // These are updated by this service - return; + if (attributeEvent + .getName() + .equals(ElectricityProducerWindAsset.SET_ACTUAL_WIND_VALUE_WITH_FORECAST.getName())) { + // Get latest asset from storage + ElectricityProducerWindAsset asset = + (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId()); + + // Check if power is currently zero and set it if power forecast has an value + if (asset.getPower().orElse(0d) == 0d && asset.getPowerForecast().orElse(0d) != 0d) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + asset.getId(), + ElectricityProducerWindAsset.POWER, + asset.getPowerForecast().orElse(0d)), + getClass().getSimpleName()); + } + } + } + + protected void processAssetChange( + PersistenceEvent persistenceEvent) { + LOG.fine("Processing producer wind asset change: " + persistenceEvent); + stopCalculation(persistenceEvent.getEntity().getId()); + + if (persistenceEvent.getCause() != PersistenceEvent.Cause.DELETE) { + if (persistenceEvent.getEntity().isIncludeForecastWindService().orElse(false) + && persistenceEvent.getEntity().getLocation().isPresent()) { + startCalculation(persistenceEvent.getEntity()); + } + } + } + + protected void startCalculation(ElectricityProducerWindAsset electricityProducerWindAsset) { + LOG.fine("Starting calculation for producer wind asset: " + electricityProducerWindAsset); + calculationFutures.put( + electricityProducerWindAsset.getId(), + scheduledExecutorService.scheduleAtFixedRate( + () -> { + processWeatherData(electricityProducerWindAsset); + }, + 0, + 1, + TimeUnit.HOURS)); + } + + protected void stopCalculation(String electricityProducerWindAssetId) { + ScheduledFuture scheduledFuture = calculationFutures.remove(electricityProducerWindAssetId); + if (scheduledFuture != null) { + LOG.fine("Stopping calculation for producer wind asset: " + electricityProducerWindAssetId); + scheduledFuture.cancel(false); + } + } + + protected void processWeatherData(ElectricityProducerWindAsset electricityProducerWindAsset) { + try (Response response = + weatherForecastWebTarget + .path("onecall") + .queryParam("lat", electricityProducerWindAsset.getLocation().get().getY()) + .queryParam("lon", electricityProducerWindAsset.getLocation().get().getX()) + .request() + .build("GET") + .invoke()) { + if (response != null && response.getStatus() == 200) { + + WeatherForecastResponseModel weatherForecastResponseModel = + response.readEntity(WeatherForecastResponseModel.class); + + double currentPower = + calculatePower(electricityProducerWindAsset, weatherForecastResponseModel.current); + + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER_FORECAST.getName(), + -currentPower), + getClass().getSimpleName()); + + if (electricityProducerWindAsset.isSetActualWindValueWithForecast().orElse(false)) { + assetProcessingService.sendAttributeEvent( + new AttributeEvent( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER.getName(), + -currentPower), + getClass().getSimpleName()); } - if (attributeEvent.getName().equals(ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.getName())) { - boolean enabled = (Boolean)attributeEvent.getValue().orElse(false); - if (enabled && calculationFutures.containsKey(attributeEvent.getId())) { - // Nothing to do here - return; - } else if (!enabled && !calculationFutures.containsKey(attributeEvent.getId())) { - // Nothing to do here - return; - } - - LOG.fine("Processing producer wind asset attribute event: " + attributeEvent); - stopCalculation(attributeEvent.getId()); - - // Get latest asset from storage - ElectricityProducerWindAsset asset = (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId()); - - if (asset != null && asset.isIncludeForecastWindService().orElse(false) && asset.getLocation().isPresent()) { - startCalculation(asset); - } + for (WeatherForecastModel weatherForecastModel : weatherForecastResponseModel.getList()) { + double powerForecast = calculatePower(electricityProducerWindAsset, weatherForecastModel); + + LocalDateTime timestamp = + Instant.ofEpochMilli(weatherForecastModel.getTimestamp()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + assetPredictedDatapointService.updateValue( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER_FORECAST.getName(), + -powerForecast, + timestamp); + assetPredictedDatapointService.updateValue( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER.getName(), + -powerForecast, + timestamp); + + for (int i = 0; i < 3; i++) { + timestamp = timestamp.plusMinutes(15); + assetPredictedDatapointService.updateValue( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER_FORECAST.getName(), + -powerForecast, + timestamp); + assetPredictedDatapointService.updateValue( + electricityProducerWindAsset.getId(), + ElectricityProducerAsset.POWER.getName(), + -powerForecast, + timestamp); + } } - if (attributeEvent.getName().equals(ElectricityProducerWindAsset.SET_ACTUAL_WIND_VALUE_WITH_FORECAST.getName())) { - // Get latest asset from storage - ElectricityProducerWindAsset asset = (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId()); - - // Check if power is currently zero and set it if power forecast has an value - if (asset.getPower().orElse(0d) == 0d && asset.getPowerForecast().orElse(0d) != 0d) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(asset.getId(), ElectricityProducerWindAsset.POWER, asset.getPowerForecast().orElse(0d)), getClass().getSimpleName()); - } + rulesService.fireDeploymentsWithPredictedDataForAsset(electricityProducerWindAsset.getId()); + } else { + StringBuilder message = new StringBuilder("Unknown"); + if (response != null) { + message.setLength(0); + message.append("Status "); + message.append(response.getStatus()); + message.append(" - "); + message.append(response.readEntity(String.class)); } + LOG.warning("Request failed: " + message); + } + } catch (Throwable e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e.getCause()); + } else { + LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e); + } } - - protected void processAssetChange(PersistenceEvent persistenceEvent) { - LOG.fine("Processing producer wind asset change: " + persistenceEvent); - stopCalculation(persistenceEvent.getEntity().getId()); - - if (persistenceEvent.getCause() != PersistenceEvent.Cause.DELETE) { - if (persistenceEvent.getEntity().isIncludeForecastWindService().orElse(false) - && persistenceEvent.getEntity().getLocation().isPresent()) { - startCalculation(persistenceEvent.getEntity()); - } - } + } + + protected double calculatePower( + ElectricityProducerWindAsset electricityProducerWindAsset, + WeatherForecastModel weatherForecastModel) { + double windSpeed = weatherForecastModel.getWindSpeed(); + double powerForecast = 0; + double windSpeedMin = electricityProducerWindAsset.getWindSpeedMin().orElse(0d); + double windSpeedMax = electricityProducerWindAsset.getWindSpeedMax().orElse(0d); + double windSpeedReference = electricityProducerWindAsset.getWindSpeedReference().orElse(0d); + double energyExportMax = electricityProducerWindAsset.getPowerExportMax().orElse(0d); + + if (windSpeed <= 0 || windSpeed < windSpeedMin) { + powerForecast = 0; } - - protected void startCalculation(ElectricityProducerWindAsset electricityProducerWindAsset) { - LOG.fine("Starting calculation for producer wind asset: " + electricityProducerWindAsset); - calculationFutures.put(electricityProducerWindAsset.getId(), scheduledExecutorService.scheduleAtFixedRate(() -> { - processWeatherData(electricityProducerWindAsset); - }, 0, 1, TimeUnit.HOURS)); + if (windSpeedMin <= windSpeed && windSpeed <= windSpeedReference) { + powerForecast = Math.pow((windSpeed / windSpeedReference), 2) * energyExportMax; } - - protected void stopCalculation(String electricityProducerWindAssetId) { - ScheduledFuture scheduledFuture = calculationFutures.remove(electricityProducerWindAssetId); - if (scheduledFuture != null) { - LOG.fine("Stopping calculation for producer wind asset: " + electricityProducerWindAssetId); - scheduledFuture.cancel(false); - } + if (windSpeedReference < windSpeed && windSpeed <= windSpeedMax) { + powerForecast = energyExportMax; } - - protected void processWeatherData(ElectricityProducerWindAsset electricityProducerWindAsset) { - try (Response response = weatherForecastWebTarget - .path("onecall") - .queryParam("lat", electricityProducerWindAsset.getLocation().get().getY()) - .queryParam("lon", electricityProducerWindAsset.getLocation().get().getX()) - .request() - .build("GET") - .invoke()) { - if (response != null && response.getStatus() == 200) { - - WeatherForecastResponseModel weatherForecastResponseModel = response.readEntity(WeatherForecastResponseModel.class); - - double currentPower = calculatePower(electricityProducerWindAsset, weatherForecastResponseModel.current); - - assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -currentPower), getClass().getSimpleName()); - - if (electricityProducerWindAsset.isSetActualWindValueWithForecast().orElse(false)) { - assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -currentPower), getClass().getSimpleName()); - } - - for (WeatherForecastModel weatherForecastModel : weatherForecastResponseModel.getList()) { - double powerForecast = calculatePower(electricityProducerWindAsset, weatherForecastModel); - - LocalDateTime timestamp = Instant.ofEpochMilli(weatherForecastModel.getTimestamp()).atZone(ZoneId.systemDefault()).toLocalDateTime(); - assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -powerForecast, timestamp); - assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -powerForecast, timestamp); - - for (int i = 0; i < 3; i++) { - timestamp = timestamp.plusMinutes(15); - assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -powerForecast, timestamp); - assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -powerForecast, timestamp); - } - } - - rulesService.fireDeploymentsWithPredictedDataForAsset(electricityProducerWindAsset.getId()); - } else { - StringBuilder message = new StringBuilder("Unknown"); - if (response != null) { - message.setLength(0); - message.append("Status "); - message.append(response.getStatus()); - message.append(" - "); - message.append(response.readEntity(String.class)); - } - LOG.warning("Request failed: " + message); - } - } catch (Throwable e) { - if (e.getCause() != null && e.getCause() instanceof IOException) { - LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e.getCause()); - } else { - LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e); - } - } + if (windSpeed > windSpeedMax) { + powerForecast = 0; } - - protected double calculatePower(ElectricityProducerWindAsset electricityProducerWindAsset, WeatherForecastModel weatherForecastModel) { - double windSpeed = weatherForecastModel.getWindSpeed(); - double powerForecast = 0; - double windSpeedMin = electricityProducerWindAsset.getWindSpeedMin().orElse(0d); - double windSpeedMax = electricityProducerWindAsset.getWindSpeedMax().orElse(0d); - double windSpeedReference = electricityProducerWindAsset.getWindSpeedReference().orElse(0d); - double energyExportMax = electricityProducerWindAsset.getPowerExportMax().orElse(0d); - - if (windSpeed <= 0 || windSpeed < windSpeedMin) { - powerForecast = 0; - } - if (windSpeedMin <= windSpeed && windSpeed <= windSpeedReference) { - powerForecast = Math.pow((windSpeed / windSpeedReference), 2) * energyExportMax; - } - if (windSpeedReference < windSpeed && windSpeed <= windSpeedMax) { - powerForecast = energyExportMax; - } - if (windSpeed > windSpeedMax) { - powerForecast = 0; - } - if (powerForecast < 0) { - powerForecast = 0; - } - return powerForecast; + if (powerForecast < 0) { + powerForecast = 0; } + return powerForecast; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java index f312f3b..9910d7d 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,17 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; + +import java.util.Collection; +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; @@ -31,321 +35,325 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Collection; -import java.util.Optional; - -import static org.openremote.model.Constants.*; @Entity public class ElectricVehicleAsset extends ElectricityBatteryAsset { - public enum EnergyType { - EV, - PHEV - } - - public static final ValueDescriptor ENERGY_TYPE_VALUE = new ValueDescriptor<>("energyType", EnergyType.class); - - public static final AttributeDescriptor ENERGY_TYPE = new AttributeDescriptor<>("energyType", ENERGY_TYPE_VALUE); - public static final AttributeDescriptor CONNECTOR_TYPE = new AttributeDescriptor<>("connectorType", ElectricityChargerAsset.CONNECTOR_TYPE_VALUE); - public static final AttributeDescriptor ODOMETER = new AttributeDescriptor<>("odometer", ValueType.POSITIVE_INTEGER, - new MetaItem<>(MetaItemType.READ_ONLY)) - .withUnits(UNITS_KILO, UNITS_METRE); - public static final AttributeDescriptor CHARGER_CONNECTED = new AttributeDescriptor<>("chargerConnected", ValueType.BOOLEAN, - new MetaItem<>(MetaItemType.READ_ONLY)); - public static final AttributeDescriptor CHARGER_ID = new AttributeDescriptor<>("chargerID", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY)); - public static final AttributeDescriptor MILEAGE_CAPACITY = new AttributeDescriptor<>("mileageCapacity", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_METRE); - public static final AttributeDescriptor MILEAGE_CHARGED = new AttributeDescriptor<>("mileageCharged", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_METRE); - public static final AttributeDescriptor MILEAGE_MIN = new AttributeDescriptor<>("mileageMin", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_METRE); - public static final AttributeDescriptor VEHICLE_CATEGORY = new AttributeDescriptor<>("vehicleCategory", ValueType.TEXT); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("car-electric", "49B0D8", ElectricVehicleAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricVehicleAsset() { - } - - public ElectricVehicleAsset(String name) { - super(name); - } - - public Optional getEnergyType() { - return getAttributes().getValue(ENERGY_TYPE); - } - - public ElectricVehicleAsset setEnergyType(EnergyType value) { - getAttributes().getOrCreate(ENERGY_TYPE).setValue(value); - return this; - } - - public Optional getConnectorType() { - return getAttributes().getValue(CONNECTOR_TYPE); - } - - public ElectricVehicleAsset setConnectorType(ElectricityChargerAsset.ConnectorType value) { - getAttributes().getOrCreate(CONNECTOR_TYPE).setValue(value); - return this; - } - - public Optional getOdometer() { - return getAttributes().getValue(ODOMETER); - } - - public ElectricVehicleAsset setOdometer(Integer value) { - getAttributes().getOrCreate(ODOMETER).setValue(value); - return this; - } - - public Optional getChargerConnected() { - return getAttributes().getValue(CHARGER_CONNECTED); - } - - public ElectricVehicleAsset setChargerConnected(Boolean value) { - getAttributes().getOrCreate(CHARGER_CONNECTED).setValue(value); - return this; - } - - public Optional getChargerId() { - return getAttributes().getValue(CHARGER_ID); - } - - public ElectricVehicleAsset setChargerId(String value) { - getAttributes().getOrCreate(CHARGER_ID).setValue(value); - return this; - } - - public Optional getMileageCapacity() { - return getAttributes().getValue(MILEAGE_CAPACITY); - } - - public ElectricVehicleAsset setMileageCapacity(Integer value) { - getAttributes().getOrCreate(MILEAGE_CAPACITY).setValue(value); - return this; - } - - public Optional getMileageCharged() { - return getAttributes().getValue(MILEAGE_CHARGED); - } - - public ElectricVehicleAsset setMileageCharged(Double value) { - getAttributes().getOrCreate(MILEAGE_CHARGED).setValue(value); - return this; - } - - public Optional getMileageMin() { - return getAttributes().getValue(MILEAGE_MIN); - } - - public ElectricVehicleAsset setMileageMin(Integer value) { - getAttributes().getOrCreate(MILEAGE_MIN).setValue(value); - return this; - } - - public Optional getVehicleCategory() { - return getAttributes().getValue(VEHICLE_CATEGORY); - } - - public ElectricVehicleAsset setVehicleCategory(String value) { - getAttributes().getOrCreate(VEHICLE_CATEGORY).setValue(value); - return this; - } - - @Override - public ElectricVehicleAsset setPower(Double value) { - super.setPower(value); - return this; - } - - @Override - public ElectricVehicleAsset setPowerSetpoint(Double value) { - super.setPowerSetpoint(value); - return this; - } - - @Override - public ElectricVehicleAsset setPowerImportMin(Double value) { - super.setPowerImportMin(value); - return this; - } - - @Override - public ElectricVehicleAsset setPowerImportMax(Double value) { - super.setPowerImportMax(value); - return this; - } - - @Override - public ElectricVehicleAsset setPowerExportMin(Double value) { - super.setPowerExportMin(value); - return this; - } - - @Override - public ElectricVehicleAsset setPowerExportMax(Double value) { - super.setPowerExportMax(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyImportTotal(Double value) { - super.setEnergyImportTotal(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyExportTotal(Double value) { - super.setEnergyExportTotal(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyCapacity(Double value) { - super.setEnergyCapacity(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyLevel(Double value) { - super.setEnergyLevel(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyLevelPercentage(Integer value) { - super.setEnergyLevelPercentage(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyLevelPercentageMin(Integer value) { - super.setEnergyLevelPercentageMin(value); - return this; - } - - @Override - public ElectricVehicleAsset setEnergyLevelPercentageMax(Integer value) { - super.setEnergyLevelPercentageMax(value); - return this; - } - - @Override - public ElectricVehicleAsset setEfficiencyImport(Integer value) { - super.setEfficiencyImport(value); - return this; - } - - @Override - public ElectricVehicleAsset setEfficiencyExport(Integer value) { - super.setEfficiencyExport(value); - return this; - } - - @Override - public ElectricVehicleAsset setId(String id) { - super.setId(id); - return this; - } - - @Override - public ElectricVehicleAsset setName(String name) throws IllegalArgumentException { - super.setName(name); - return this; - } - - @Override - public ElectricVehicleAsset setAccessPublicRead(boolean accessPublicRead) { - super.setAccessPublicRead(accessPublicRead); - return this; - } - - @Override - public ElectricVehicleAsset setParent(Asset parent) { - super.setParent(parent); - return this; - } - - @Override - public ElectricVehicleAsset setParentId(String parentId) { - super.setParentId(parentId); - return this; - } - - @Override - public ElectricVehicleAsset setRealm(String realm) { - super.setRealm(realm); - return this; - } - - @Override - public ElectricVehicleAsset setAttributes(AttributeMap attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public Asset setAttributes(Attribute... attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleAsset setAttributes(Collection> attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleAsset setLocation(GeoJSONPoint location) { - super.setLocation(location); - return this; - } - - @Override - public ElectricVehicleAsset setEmail(String email) { - super.setEmail(email); - return this; - } - - @Override - public ElectricVehicleAsset setNotes(String notes) { - super.setNotes(notes); - return this; - } - - @Override - public ElectricVehicleAsset setManufacturer(String manufacturer) { - super.setManufacturer(manufacturer); - return this; - } - - @Override - public ElectricVehicleAsset setModel(String model) { - super.setModel(model); - return this; - } - - @Override - public ElectricVehicleAsset addAttributes(Attribute... attributes) { - super.addAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleAsset addOrReplaceAttributes(Attribute... attributes) { - super.addOrReplaceAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleAsset setTags(String[] tags) { - super.setTags(tags); - return this; - } + public enum EnergyType { + EV, + PHEV + } + + public static final ValueDescriptor ENERGY_TYPE_VALUE = + new ValueDescriptor<>("energyType", EnergyType.class); + + public static final AttributeDescriptor ENERGY_TYPE = + new AttributeDescriptor<>("energyType", ENERGY_TYPE_VALUE); + public static final AttributeDescriptor CONNECTOR_TYPE = + new AttributeDescriptor<>("connectorType", ElectricityChargerAsset.CONNECTOR_TYPE_VALUE); + public static final AttributeDescriptor ODOMETER = + new AttributeDescriptor<>( + "odometer", ValueType.POSITIVE_INTEGER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_METRE); + public static final AttributeDescriptor CHARGER_CONNECTED = + new AttributeDescriptor<>( + "chargerConnected", ValueType.BOOLEAN, new MetaItem<>(MetaItemType.READ_ONLY)); + public static final AttributeDescriptor CHARGER_ID = + new AttributeDescriptor<>( + "chargerID", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY)); + public static final AttributeDescriptor MILEAGE_CAPACITY = + new AttributeDescriptor<>("mileageCapacity", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_METRE); + public static final AttributeDescriptor MILEAGE_CHARGED = + new AttributeDescriptor<>( + "mileageCharged", ValueType.POSITIVE_NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_METRE); + public static final AttributeDescriptor MILEAGE_MIN = + new AttributeDescriptor<>("mileageMin", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_METRE); + public static final AttributeDescriptor VEHICLE_CATEGORY = + new AttributeDescriptor<>("vehicleCategory", ValueType.TEXT); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("car-electric", "49B0D8", ElectricVehicleAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricVehicleAsset() {} + + public ElectricVehicleAsset(String name) { + super(name); + } + + public Optional getEnergyType() { + return getAttributes().getValue(ENERGY_TYPE); + } + + public ElectricVehicleAsset setEnergyType(EnergyType value) { + getAttributes().getOrCreate(ENERGY_TYPE).setValue(value); + return this; + } + + public Optional getConnectorType() { + return getAttributes().getValue(CONNECTOR_TYPE); + } + + public ElectricVehicleAsset setConnectorType(ElectricityChargerAsset.ConnectorType value) { + getAttributes().getOrCreate(CONNECTOR_TYPE).setValue(value); + return this; + } + + public Optional getOdometer() { + return getAttributes().getValue(ODOMETER); + } + + public ElectricVehicleAsset setOdometer(Integer value) { + getAttributes().getOrCreate(ODOMETER).setValue(value); + return this; + } + + public Optional getChargerConnected() { + return getAttributes().getValue(CHARGER_CONNECTED); + } + + public ElectricVehicleAsset setChargerConnected(Boolean value) { + getAttributes().getOrCreate(CHARGER_CONNECTED).setValue(value); + return this; + } + + public Optional getChargerId() { + return getAttributes().getValue(CHARGER_ID); + } + + public ElectricVehicleAsset setChargerId(String value) { + getAttributes().getOrCreate(CHARGER_ID).setValue(value); + return this; + } + + public Optional getMileageCapacity() { + return getAttributes().getValue(MILEAGE_CAPACITY); + } + + public ElectricVehicleAsset setMileageCapacity(Integer value) { + getAttributes().getOrCreate(MILEAGE_CAPACITY).setValue(value); + return this; + } + + public Optional getMileageCharged() { + return getAttributes().getValue(MILEAGE_CHARGED); + } + + public ElectricVehicleAsset setMileageCharged(Double value) { + getAttributes().getOrCreate(MILEAGE_CHARGED).setValue(value); + return this; + } + + public Optional getMileageMin() { + return getAttributes().getValue(MILEAGE_MIN); + } + + public ElectricVehicleAsset setMileageMin(Integer value) { + getAttributes().getOrCreate(MILEAGE_MIN).setValue(value); + return this; + } + + public Optional getVehicleCategory() { + return getAttributes().getValue(VEHICLE_CATEGORY); + } + + public ElectricVehicleAsset setVehicleCategory(String value) { + getAttributes().getOrCreate(VEHICLE_CATEGORY).setValue(value); + return this; + } + + @Override + public ElectricVehicleAsset setPower(Double value) { + super.setPower(value); + return this; + } + + @Override + public ElectricVehicleAsset setPowerSetpoint(Double value) { + super.setPowerSetpoint(value); + return this; + } + + @Override + public ElectricVehicleAsset setPowerImportMin(Double value) { + super.setPowerImportMin(value); + return this; + } + + @Override + public ElectricVehicleAsset setPowerImportMax(Double value) { + super.setPowerImportMax(value); + return this; + } + + @Override + public ElectricVehicleAsset setPowerExportMin(Double value) { + super.setPowerExportMin(value); + return this; + } + + @Override + public ElectricVehicleAsset setPowerExportMax(Double value) { + super.setPowerExportMax(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyImportTotal(Double value) { + super.setEnergyImportTotal(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyExportTotal(Double value) { + super.setEnergyExportTotal(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyCapacity(Double value) { + super.setEnergyCapacity(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyLevel(Double value) { + super.setEnergyLevel(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyLevelPercentage(Integer value) { + super.setEnergyLevelPercentage(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyLevelPercentageMin(Integer value) { + super.setEnergyLevelPercentageMin(value); + return this; + } + + @Override + public ElectricVehicleAsset setEnergyLevelPercentageMax(Integer value) { + super.setEnergyLevelPercentageMax(value); + return this; + } + + @Override + public ElectricVehicleAsset setEfficiencyImport(Integer value) { + super.setEfficiencyImport(value); + return this; + } + + @Override + public ElectricVehicleAsset setEfficiencyExport(Integer value) { + super.setEfficiencyExport(value); + return this; + } + + @Override + public ElectricVehicleAsset setId(String id) { + super.setId(id); + return this; + } + + @Override + public ElectricVehicleAsset setName(String name) throws IllegalArgumentException { + super.setName(name); + return this; + } + + @Override + public ElectricVehicleAsset setAccessPublicRead(boolean accessPublicRead) { + super.setAccessPublicRead(accessPublicRead); + return this; + } + + @Override + public ElectricVehicleAsset setParent(Asset parent) { + super.setParent(parent); + return this; + } + + @Override + public ElectricVehicleAsset setParentId(String parentId) { + super.setParentId(parentId); + return this; + } + + @Override + public ElectricVehicleAsset setRealm(String realm) { + super.setRealm(realm); + return this; + } + + @Override + public ElectricVehicleAsset setAttributes(AttributeMap attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public Asset setAttributes(Attribute... attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleAsset setAttributes(Collection> attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleAsset setLocation(GeoJSONPoint location) { + super.setLocation(location); + return this; + } + + @Override + public ElectricVehicleAsset setEmail(String email) { + super.setEmail(email); + return this; + } + + @Override + public ElectricVehicleAsset setNotes(String notes) { + super.setNotes(notes); + return this; + } + + @Override + public ElectricVehicleAsset setManufacturer(String manufacturer) { + super.setManufacturer(manufacturer); + return this; + } + + @Override + public ElectricVehicleAsset setModel(String model) { + super.setModel(model); + return this; + } + + @Override + public ElectricVehicleAsset addAttributes(Attribute... attributes) { + super.addAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleAsset addOrReplaceAttributes(Attribute... attributes) { + super.addOrReplaceAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleAsset setTags(String[] tags) { + super.setTags(tags); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java index 3182dfd..4c0bf84 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,17 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; + +import java.util.Collection; +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.asset.impl.GroupAsset; @@ -29,165 +33,169 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Collection; -import java.util.Optional; - -import static org.openremote.model.Constants.*; @Entity public class ElectricVehicleFleetGroupAsset extends GroupAsset { - public static final AttributeDescriptor FLEET_CATEGORY = new AttributeDescriptor<>("fleetCategory", ValueType.TEXT); - public static final AttributeDescriptor AVAILABLE_CHARGING_SPACES = new AttributeDescriptor<>("availableChargingSpaces", ValueType.POSITIVE_INTEGER); - public static final AttributeDescriptor AVAILABLE_DISCHARGING_SPACES = new AttributeDescriptor<>("availableDischargingSpaces", ValueType.POSITIVE_INTEGER); - public static final AttributeDescriptor POWER_IMPORT_MAX = new AttributeDescriptor<>("powerImportMax", ValueType.POSITIVE_INTEGER); - public static final AttributeDescriptor POWER_EXPORT_MAX = new AttributeDescriptor<>("powerExportMax", ValueType.POSITIVE_INTEGER); - - public static final AttributeDescriptor MILEAGE_MINIMUM = new AttributeDescriptor<>("mileageMinimum", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_KILO, UNITS_METRE); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("car-multiple", "49B0D8", ElectricVehicleFleetGroupAsset.class); - - protected ElectricVehicleFleetGroupAsset() { - } - - public ElectricVehicleFleetGroupAsset(String name) { - super(name, ElectricVehicleAsset.class); - } - - public Optional getFleetCategory() { - return getAttributes().getValue(FLEET_CATEGORY); - } - - public Optional getAvailableChargingSpaces() { - return getAttributes().getValue(AVAILABLE_CHARGING_SPACES); - } - - public Optional getAvailableDischargingSpaces() { - return getAttributes().getValue(AVAILABLE_DISCHARGING_SPACES); - } - - public Optional getPowerImportMax() { - return getAttributes().getValue(POWER_IMPORT_MAX); - } - - public Optional getPowerExportMax() { - return getAttributes().getValue(POWER_EXPORT_MAX); - } - - public Optional getMileageMinimum() { return getAttributes().getValue(MILEAGE_MINIMUM); } - - @Override - public ElectricVehicleFleetGroupAsset setId(String id) { - super.setId(id); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setVersion(long version) { - super.setVersion(version); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setName(String name) throws IllegalArgumentException { - super.setName(name); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setAccessPublicRead(boolean accessPublicRead) { - super.setAccessPublicRead(accessPublicRead); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setParent(Asset parent) { - super.setParent(parent); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setParentId(String parentId) { - super.setParentId(parentId); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setRealm(String realm) { - super.setRealm(realm); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setAttributes(AttributeMap attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public Asset setAttributes(Attribute... attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setAttributes(Collection> attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset addAttributes(Attribute... attributes) { - super.addAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset addOrReplaceAttributes(Attribute... attributes) { - super.addOrReplaceAttributes(attributes); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setLocation(GeoJSONPoint location) { - super.setLocation(location); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setTags(String[] tags) { - super.setTags(tags); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setEmail(String email) { - super.setEmail(email); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setNotes(String notes) { - super.setNotes(notes); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setManufacturer(String manufacturer) { - super.setManufacturer(manufacturer); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setModel(String model) { - super.setModel(model); - return this; - } - - @Override - public ElectricVehicleFleetGroupAsset setChildAssetType(String childAssetType) { - super.setChildAssetType(childAssetType); - return this; - } + public static final AttributeDescriptor FLEET_CATEGORY = + new AttributeDescriptor<>("fleetCategory", ValueType.TEXT); + public static final AttributeDescriptor AVAILABLE_CHARGING_SPACES = + new AttributeDescriptor<>("availableChargingSpaces", ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor AVAILABLE_DISCHARGING_SPACES = + new AttributeDescriptor<>("availableDischargingSpaces", ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor POWER_IMPORT_MAX = + new AttributeDescriptor<>("powerImportMax", ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor POWER_EXPORT_MAX = + new AttributeDescriptor<>("powerExportMax", ValueType.POSITIVE_INTEGER); + + public static final AttributeDescriptor MILEAGE_MINIMUM = + new AttributeDescriptor<>("mileageMinimum", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_KILO, UNITS_METRE); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("car-multiple", "49B0D8", ElectricVehicleFleetGroupAsset.class); + + protected ElectricVehicleFleetGroupAsset() {} + + public ElectricVehicleFleetGroupAsset(String name) { + super(name, ElectricVehicleAsset.class); + } + + public Optional getFleetCategory() { + return getAttributes().getValue(FLEET_CATEGORY); + } + + public Optional getAvailableChargingSpaces() { + return getAttributes().getValue(AVAILABLE_CHARGING_SPACES); + } + + public Optional getAvailableDischargingSpaces() { + return getAttributes().getValue(AVAILABLE_DISCHARGING_SPACES); + } + + public Optional getPowerImportMax() { + return getAttributes().getValue(POWER_IMPORT_MAX); + } + + public Optional getPowerExportMax() { + return getAttributes().getValue(POWER_EXPORT_MAX); + } + + public Optional getMileageMinimum() { + return getAttributes().getValue(MILEAGE_MINIMUM); + } + + @Override + public ElectricVehicleFleetGroupAsset setId(String id) { + super.setId(id); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setVersion(long version) { + super.setVersion(version); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setName(String name) throws IllegalArgumentException { + super.setName(name); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setAccessPublicRead(boolean accessPublicRead) { + super.setAccessPublicRead(accessPublicRead); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setParent(Asset parent) { + super.setParent(parent); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setParentId(String parentId) { + super.setParentId(parentId); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setRealm(String realm) { + super.setRealm(realm); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setAttributes(AttributeMap attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public Asset setAttributes(Attribute... attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setAttributes(Collection> attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset addAttributes(Attribute... attributes) { + super.addAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset addOrReplaceAttributes(Attribute... attributes) { + super.addOrReplaceAttributes(attributes); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setLocation(GeoJSONPoint location) { + super.setLocation(location); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setTags(String[] tags) { + super.setTags(tags); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setEmail(String email) { + super.setEmail(email); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setNotes(String notes) { + super.setNotes(notes); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setManufacturer(String manufacturer) { + super.setManufacturer(manufacturer); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setModel(String model) { + super.setModel(model); + return this; + } + + @Override + public ElectricVehicleFleetGroupAsset setChildAssetType(String childAssetType) { + super.setChildAssetType(childAssetType); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java index 6785876..3920e6e 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,167 +12,185 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; -import org.openremote.model.asset.Asset; -import org.openremote.model.attribute.MetaItem; -import org.openremote.model.value.*; +import static org.openremote.model.Constants.*; import java.util.Optional; -import static org.openremote.model.Constants.*; +import org.openremote.model.asset.Asset; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.value.*; @SuppressWarnings("unchecked") public abstract class ElectricityAsset> extends Asset { - public static final AttributeDescriptor POWER = new AttributeDescriptor<>("power", ValueType.NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT); - public static final AttributeDescriptor POWER_SETPOINT = new AttributeDescriptor<>("powerSetpoint", ValueType.NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT).withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MIN = new AttributeDescriptor<>("powerImportMin", ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT); - public static final AttributeDescriptor POWER_IMPORT_MAX = new AttributeDescriptor<>("powerImportMax", ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT); - public static final AttributeDescriptor POWER_EXPORT_MIN = new AttributeDescriptor<>("powerExportMin", ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT); - public static final AttributeDescriptor POWER_EXPORT_MAX = new AttributeDescriptor<>("powerExportMax", ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT); - - - public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = new AttributeDescriptor<>("energyImportTotal", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = new AttributeDescriptor<>("energyExportTotal", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor EFFICIENCY_IMPORT = new AttributeDescriptor<>("efficiencyImport", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - public static final AttributeDescriptor EFFICIENCY_EXPORT = new AttributeDescriptor<>("efficiencyExport", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - - public static final AttributeDescriptor TARIFF_IMPORT = new AttributeDescriptor<>("tariffImport", ValueType.NUMBER) - .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR).withOptional(true); - public static final AttributeDescriptor TARIFF_EXPORT = new AttributeDescriptor<>("tariffExport", ValueType.NUMBER) - .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR).withOptional(true); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityAsset() { - } - - public ElectricityAsset(String name) { - super(name); - } - - public Optional getPower() { - return getAttributes().getValue(POWER); - } - - public T setPower(Double value) { - getAttributes().getOrCreate(POWER).setValue(value); - return (T)this; - } - - public Optional getPowerSetpoint() { - return getAttributes().getValue(POWER_SETPOINT); - } - - public T setPowerSetpoint(Double value) { - getAttributes().getOrCreate(POWER_SETPOINT).setValue(value); - return (T)this; - } - - public Optional getPowerImportMin() { - return getAttributes().getValue(POWER_IMPORT_MIN); - } - - public T setPowerImportMin(Double value) { - getAttributes().getOrCreate(POWER_IMPORT_MIN).setValue(value); - return (T)this; - } - - public Optional getPowerImportMax() { - return getAttributes().getValue(POWER_IMPORT_MAX); - } - - public T setPowerImportMax(Double value) { - getAttributes().getOrCreate(POWER_IMPORT_MAX).setValue(value); - return (T)this; - } - - public Optional getPowerExportMin() { - return getAttributes().getValue(POWER_EXPORT_MIN); - } - - public T setPowerExportMin(Double value) { - getAttributes().getOrCreate(POWER_EXPORT_MIN).setValue(value); - return (T)this; - } - - public Optional getPowerExportMax() { - return getAttributes().getValue(POWER_EXPORT_MAX); - } - - public T setPowerExportMax(Double value) { - getAttributes().getOrCreate(POWER_EXPORT_MAX).setValue(value); - return (T)this; - } - - public Optional getEnergyImportTotal() { - return getAttributes().getValue(ENERGY_IMPORT_TOTAL); - } - - public T setEnergyImportTotal(Double value) { - getAttributes().getOrCreate(ENERGY_IMPORT_TOTAL).setValue(value); - return (T)this; - } - - public Optional getEnergyExportTotal() { - return getAttributes().getValue(ENERGY_EXPORT_TOTAL); - } - - public T setEnergyExportTotal(Double value) { - getAttributes().getOrCreate(ENERGY_EXPORT_TOTAL).setValue(value); - return (T)this; - } - - public Optional getEfficiencyImport() { - return getAttributes().getValue(EFFICIENCY_IMPORT); - } - - public T setEfficiencyImport(Integer value) { - getAttributes().getOrCreate(EFFICIENCY_IMPORT).setValue(value); - return (T)this; - } - - public Optional getEfficiencyExport() { - return getAttributes().getValue(EFFICIENCY_EXPORT); - } - - public T setEfficiencyExport(Integer value) { - getAttributes().getOrCreate(EFFICIENCY_EXPORT).setValue(value); - return (T)this; - } - - public Optional getTariffImport() { - return getAttributes().getValue(TARIFF_IMPORT); - } - - public T setTariffImport(Double value) { - getAttributes().getOrCreate(TARIFF_IMPORT).setValue(value); - return (T)this; - } - - public Optional getTariffExport() { - return getAttributes().getValue(TARIFF_EXPORT); - } - - public T setTariffExport(Double value) { - getAttributes().getOrCreate(TARIFF_EXPORT).setValue(value); - return (T)this; - } + public static final AttributeDescriptor POWER = + new AttributeDescriptor<>("power", ValueType.NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT); + public static final AttributeDescriptor POWER_SETPOINT = + new AttributeDescriptor<>("powerSetpoint", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT) + .withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MIN = + new AttributeDescriptor<>("powerImportMin", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + public static final AttributeDescriptor POWER_IMPORT_MAX = + new AttributeDescriptor<>("powerImportMax", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + public static final AttributeDescriptor POWER_EXPORT_MIN = + new AttributeDescriptor<>("powerExportMin", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + public static final AttributeDescriptor POWER_EXPORT_MAX = + new AttributeDescriptor<>("powerExportMax", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = + new AttributeDescriptor<>( + "energyImportTotal", + ValueType.POSITIVE_NUMBER, + new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = + new AttributeDescriptor<>( + "energyExportTotal", + ValueType.POSITIVE_NUMBER, + new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + public static final AttributeDescriptor EFFICIENCY_IMPORT = + new AttributeDescriptor<>("efficiencyImport", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + public static final AttributeDescriptor EFFICIENCY_EXPORT = + new AttributeDescriptor<>("efficiencyExport", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + + public static final AttributeDescriptor TARIFF_IMPORT = + new AttributeDescriptor<>("tariffImport", ValueType.NUMBER) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR) + .withOptional(true); + public static final AttributeDescriptor TARIFF_EXPORT = + new AttributeDescriptor<>("tariffExport", ValueType.NUMBER) + .withUnits("EUR", UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR) + .withOptional(true); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityAsset() {} + + public ElectricityAsset(String name) { + super(name); + } + + public Optional getPower() { + return getAttributes().getValue(POWER); + } + + public T setPower(Double value) { + getAttributes().getOrCreate(POWER).setValue(value); + return (T) this; + } + + public Optional getPowerSetpoint() { + return getAttributes().getValue(POWER_SETPOINT); + } + + public T setPowerSetpoint(Double value) { + getAttributes().getOrCreate(POWER_SETPOINT).setValue(value); + return (T) this; + } + + public Optional getPowerImportMin() { + return getAttributes().getValue(POWER_IMPORT_MIN); + } + + public T setPowerImportMin(Double value) { + getAttributes().getOrCreate(POWER_IMPORT_MIN).setValue(value); + return (T) this; + } + + public Optional getPowerImportMax() { + return getAttributes().getValue(POWER_IMPORT_MAX); + } + + public T setPowerImportMax(Double value) { + getAttributes().getOrCreate(POWER_IMPORT_MAX).setValue(value); + return (T) this; + } + + public Optional getPowerExportMin() { + return getAttributes().getValue(POWER_EXPORT_MIN); + } + + public T setPowerExportMin(Double value) { + getAttributes().getOrCreate(POWER_EXPORT_MIN).setValue(value); + return (T) this; + } + + public Optional getPowerExportMax() { + return getAttributes().getValue(POWER_EXPORT_MAX); + } + + public T setPowerExportMax(Double value) { + getAttributes().getOrCreate(POWER_EXPORT_MAX).setValue(value); + return (T) this; + } + + public Optional getEnergyImportTotal() { + return getAttributes().getValue(ENERGY_IMPORT_TOTAL); + } + + public T setEnergyImportTotal(Double value) { + getAttributes().getOrCreate(ENERGY_IMPORT_TOTAL).setValue(value); + return (T) this; + } + + public Optional getEnergyExportTotal() { + return getAttributes().getValue(ENERGY_EXPORT_TOTAL); + } + + public T setEnergyExportTotal(Double value) { + getAttributes().getOrCreate(ENERGY_EXPORT_TOTAL).setValue(value); + return (T) this; + } + + public Optional getEfficiencyImport() { + return getAttributes().getValue(EFFICIENCY_IMPORT); + } + + public T setEfficiencyImport(Integer value) { + getAttributes().getOrCreate(EFFICIENCY_IMPORT).setValue(value); + return (T) this; + } + + public Optional getEfficiencyExport() { + return getAttributes().getValue(EFFICIENCY_EXPORT); + } + + public T setEfficiencyExport(Integer value) { + getAttributes().getOrCreate(EFFICIENCY_EXPORT).setValue(value); + return (T) this; + } + + public Optional getTariffImport() { + return getAttributes().getValue(TARIFF_IMPORT); + } + + public T setTariffImport(Double value) { + getAttributes().getOrCreate(TARIFF_IMPORT).setValue(value); + return (T) this; + } + + public Optional getTariffExport() { + return getAttributes().getValue(TARIFF_EXPORT); + } + + public T setTariffExport(Double value) { + getAttributes().getOrCreate(TARIFF_EXPORT).setValue(value); + return (T) this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java index f725a40..94a81f1 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import java.util.Optional; + import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AttributeDescriptor; @@ -26,30 +27,30 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Optional; @Entity public class ElectricityBatteryAsset extends ElectricityStorageAsset { - public static final AttributeDescriptor CHARGE_CYCLES = new AttributeDescriptor<>("chargeCycles", ValueType.POSITIVE_INTEGER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withOptional(true); + public static final AttributeDescriptor CHARGE_CYCLES = + new AttributeDescriptor<>( + "chargeCycles", ValueType.POSITIVE_INTEGER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withOptional(true); - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("battery-charging", "1B7C89", ElectricityBatteryAsset.class); + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("battery-charging", "1B7C89", ElectricityBatteryAsset.class); - protected ElectricityBatteryAsset() { - } + protected ElectricityBatteryAsset() {} - public ElectricityBatteryAsset(String name) { - super(name); - } + public ElectricityBatteryAsset(String name) { + super(name); + } - public Optional getChargeCycles() { - return getAttributes().getValue(CHARGE_CYCLES); - } + public Optional getChargeCycles() { + return getAttributes().getValue(CHARGE_CYCLES); + } - public ElectricityStorageAsset setChargeCycles(Integer value) { - getAttributes().getOrCreate(CHARGE_CYCLES).setValue(value); - return this; - } + public ElectricityStorageAsset setChargeCycles(Integer value) { + getAttributes().getOrCreate(CHARGE_CYCLES).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java index 88690d8..200eed2 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import java.util.Optional; + import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AttributeDescriptor; @@ -27,65 +28,66 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Optional; @Entity public class ElectricityChargerAsset extends ElectricityStorageAsset { - public enum ConnectorType { - YAZAKI, - MENNEKES, - LE_GRAND, - CHADEMO, - COMBO, - SCHUKO, - ENERGYLOCK - } - - public static final ValueDescriptor CONNECTOR_TYPE_VALUE = new ValueDescriptor<>("connectorType", ConnectorType.class); - - public static final AttributeDescriptor CONNECTOR_TYPE = new AttributeDescriptor<>("connectorType", CONNECTOR_TYPE_VALUE); - public static final AttributeDescriptor VEHICLE_CONNECTED = new AttributeDescriptor<>("vehicleConnected", ValueType.BOOLEAN, - new MetaItem<>(MetaItemType.READ_ONLY)); - public static final AttributeDescriptor VEHICLE_ID = new AttributeDescriptor<>("vehicleID", ValueType.TEXT, - new MetaItem<>(MetaItemType.READ_ONLY, true)); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("ev-station", "8A293D", ElectricityChargerAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityChargerAsset() { - } - - public ElectricityChargerAsset(String name) { - super(name); - } - - public Optional getConnectorType() { - return getAttributes().getValue(CONNECTOR_TYPE); - } - - public ElectricityChargerAsset setConnectorType(ConnectorType value) { - getAttributes().getOrCreate(CONNECTOR_TYPE).setValue(value); - return this; - } - - public Optional getVehicleConnected() { - return getAttributes().getValue(VEHICLE_CONNECTED); - } - - public ElectricityChargerAsset setVehicleConnected(boolean value) { - getAttributes().getOrCreate(VEHICLE_CONNECTED).setValue(value); - return this; - } - - public Optional getVehicleID() { - return getAttributes().getValue(VEHICLE_ID); - } - - public ElectricityChargerAsset setVehicleID(String value) { - getAttributes().getOrCreate(VEHICLE_ID).setValue(value); - return this; - } + public enum ConnectorType { + YAZAKI, + MENNEKES, + LE_GRAND, + CHADEMO, + COMBO, + SCHUKO, + ENERGYLOCK + } + + public static final ValueDescriptor CONNECTOR_TYPE_VALUE = + new ValueDescriptor<>("connectorType", ConnectorType.class); + + public static final AttributeDescriptor CONNECTOR_TYPE = + new AttributeDescriptor<>("connectorType", CONNECTOR_TYPE_VALUE); + public static final AttributeDescriptor VEHICLE_CONNECTED = + new AttributeDescriptor<>( + "vehicleConnected", ValueType.BOOLEAN, new MetaItem<>(MetaItemType.READ_ONLY)); + public static final AttributeDescriptor VEHICLE_ID = + new AttributeDescriptor<>( + "vehicleID", ValueType.TEXT, new MetaItem<>(MetaItemType.READ_ONLY, true)); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("ev-station", "8A293D", ElectricityChargerAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityChargerAsset() {} + + public ElectricityChargerAsset(String name) { + super(name); + } + + public Optional getConnectorType() { + return getAttributes().getValue(CONNECTOR_TYPE); + } + + public ElectricityChargerAsset setConnectorType(ConnectorType value) { + getAttributes().getOrCreate(CONNECTOR_TYPE).setValue(value); + return this; + } + + public Optional getVehicleConnected() { + return getAttributes().getValue(VEHICLE_CONNECTED); + } + + public ElectricityChargerAsset setVehicleConnected(boolean value) { + getAttributes().getOrCreate(VEHICLE_CONNECTED).setValue(value); + return this; + } + + public Optional getVehicleID() { + return getAttributes().getValue(VEHICLE_ID); + } + + public ElectricityChargerAsset setVehicleID(String value) { + getAttributes().getOrCreate(VEHICLE_ID).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java index 8a3d4f8..e57e8ac 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,45 +12,57 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.UNITS_KILO; +import static org.openremote.model.Constants.UNITS_WATT; + import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.value.*; import jakarta.persistence.Entity; -import static org.openremote.model.Constants.UNITS_KILO; -import static org.openremote.model.Constants.UNITS_WATT; - @Entity public class ElectricityConsumerAsset extends ElectricityAsset { - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("power-plug", "8A293D", ElectricityConsumerAsset.class); - - public static final AttributeDescriptor POWER_SETPOINT = ElectricityAsset.POWER_SETPOINT.withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MIN = ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MAX = ElectricityAsset.POWER_IMPORT_MAX.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MIN = ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MAX = ElectricityAsset.POWER_EXPORT_MAX.withOptional(true); - public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = ElectricityAsset.ENERGY_EXPORT_TOTAL.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_IMPORT = ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_EXPORT = ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); - public static final AttributeDescriptor TARIFF_IMPORT = ElectricitySupplierAsset.TARIFF_IMPORT.withOptional(true); - public static final AttributeDescriptor TARIFF_EXPORT = ElectricitySupplierAsset.TARIFF_EXPORT.withOptional(true); - public static final AttributeDescriptor CARBON_IMPORT = ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); - - public static final AttributeDescriptor POWER_FORECAST = new AttributeDescriptor<>("powerForecast", ValueType.NUMBER - ).withUnits(UNITS_KILO, UNITS_WATT); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityConsumerAsset() { - } - - public ElectricityConsumerAsset(String name) { - super(name); - } + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("power-plug", "8A293D", ElectricityConsumerAsset.class); + + public static final AttributeDescriptor POWER_SETPOINT = + ElectricityAsset.POWER_SETPOINT.withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MIN = + ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MAX = + ElectricityAsset.POWER_IMPORT_MAX.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MIN = + ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MAX = + ElectricityAsset.POWER_EXPORT_MAX.withOptional(true); + public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = + ElectricityAsset.ENERGY_EXPORT_TOTAL.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_IMPORT = + ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_EXPORT = + ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); + public static final AttributeDescriptor TARIFF_IMPORT = + ElectricitySupplierAsset.TARIFF_IMPORT.withOptional(true); + public static final AttributeDescriptor TARIFF_EXPORT = + ElectricitySupplierAsset.TARIFF_EXPORT.withOptional(true); + public static final AttributeDescriptor CARBON_IMPORT = + ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); + + public static final AttributeDescriptor POWER_FORECAST = + new AttributeDescriptor<>("powerForecast", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityConsumerAsset() {} + + public ElectricityConsumerAsset(String name) { + super(name); + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java index aef906d..85e5929 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,17 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.UNITS_KILO; +import static org.openremote.model.Constants.UNITS_WATT; + +import java.util.Optional; + import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AttributeDescriptor; @@ -26,42 +30,47 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Optional; - -import static org.openremote.model.Constants.UNITS_KILO; -import static org.openremote.model.Constants.UNITS_WATT; @Entity public class ElectricityProducerAsset extends ElectricityAsset { - public static final AttributeDescriptor POWER_SETPOINT = ElectricityAsset.POWER_SETPOINT.withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MIN = ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MAX = ElectricityAsset.POWER_IMPORT_MAX.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MIN = ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); - public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = ElectricityAsset.ENERGY_IMPORT_TOTAL.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_IMPORT = ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_EXPORT = ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); - public static final AttributeDescriptor TARIFF_IMPORT = ElectricitySupplierAsset.TARIFF_IMPORT.withOptional(true); - public static final AttributeDescriptor TARIFF_EXPORT = ElectricitySupplierAsset.TARIFF_EXPORT.withOptional(true); - public static final AttributeDescriptor CARBON_IMPORT = ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); + public static final AttributeDescriptor POWER_SETPOINT = + ElectricityAsset.POWER_SETPOINT.withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MIN = + ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MAX = + ElectricityAsset.POWER_IMPORT_MAX.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MIN = + ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); + public static final AttributeDescriptor ENERGY_IMPORT_TOTAL = + ElectricityAsset.ENERGY_IMPORT_TOTAL.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_IMPORT = + ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_EXPORT = + ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); + public static final AttributeDescriptor TARIFF_IMPORT = + ElectricitySupplierAsset.TARIFF_IMPORT.withOptional(true); + public static final AttributeDescriptor TARIFF_EXPORT = + ElectricitySupplierAsset.TARIFF_EXPORT.withOptional(true); + public static final AttributeDescriptor CARBON_IMPORT = + ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); - public static final AttributeDescriptor POWER_FORECAST = new AttributeDescriptor<>("powerForecast", ValueType.NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT); + public static final AttributeDescriptor POWER_FORECAST = + new AttributeDescriptor<>( + "powerForecast", ValueType.NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT); - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("flash", "EABB4D", ElectricityProducerAsset.class); + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("flash", "EABB4D", ElectricityProducerAsset.class); - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityProducerAsset() { - } + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityProducerAsset() {} - public ElectricityProducerAsset(String name) { - super(name); - } + public ElectricityProducerAsset(String name) { + super(name); + } - public Optional getPowerForecast() { - return getAttributes().getOrCreate(POWER_FORECAST).getValue(); - } + public Optional getPowerForecast() { + return getAttributes().getOrCreate(POWER_FORECAST).getValue(); + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java index 136d3ba..1fe00ce 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,18 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.UNITS_DEGREE; +import static org.openremote.model.value.ValueType.BOOLEAN; + +import java.util.Collection; +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; @@ -27,248 +32,246 @@ import org.openremote.model.value.*; import jakarta.persistence.Entity; -import java.util.Collection; -import java.util.Optional; - -import static org.openremote.model.Constants.UNITS_DEGREE; -import static org.openremote.model.value.ValueType.BOOLEAN; @Entity public class ElectricityProducerSolarAsset extends ElectricityProducerAsset { - public enum PanelOrientation { - SOUTH, - EAST_WEST - } - - public static final ValueDescriptor PANEL_ORIENTATION_VALUE = new ValueDescriptor<>("panelOrientation", PanelOrientation.class); - - public static final AttributeDescriptor PANEL_ORIENTATION = new AttributeDescriptor<>("panelOrientation", PANEL_ORIENTATION_VALUE); - public static final AttributeDescriptor PANEL_AZIMUTH = new AttributeDescriptor<>("panelAzimuth", ValueType.INTEGER - ).withUnits(UNITS_DEGREE); - public static final AttributeDescriptor PANEL_PITCH = new AttributeDescriptor<>("panelPitch", ValueType.POSITIVE_INTEGER - ).withUnits(UNITS_DEGREE); - - public static final AttributeDescriptor INCLUDE_FORECAST_SOLAR_SERVICE = new AttributeDescriptor<>("includeForecastSolarService", BOOLEAN); - - public static final AttributeDescriptor SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST = new AttributeDescriptor<>("setActualSolarValueWithForecast", BOOLEAN); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("white-balance-sunny", "EABB4D", ElectricityProducerSolarAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityProducerSolarAsset() { - } - - public ElectricityProducerSolarAsset(String name) { - super(name); - } - - public Optional getPanelOrientation() { - return getAttributes().getValue(PANEL_ORIENTATION); - } - - public Optional isIncludeForecastSolarService() { - return getAttribute(INCLUDE_FORECAST_SOLAR_SERVICE).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional isSetActualSolarValueWithForecast() { - return getAttribute(SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST).flatMap(AbstractNameValueHolder::getValue); - } - - public ElectricityProducerSolarAsset setPanelOrientation(PanelOrientation value) { - getAttributes().getOrCreate(PANEL_ORIENTATION).setValue(value); - return this; - } - - public Optional getPanelAzimuth() { - return getAttributes().getValue(PANEL_AZIMUTH); - } - - public ElectricityProducerSolarAsset setPanelAzimuth(Integer value) { - getAttributes().getOrCreate(PANEL_AZIMUTH).setValue(value); - return this; - } - - public Optional getPanelPitch() { - return getAttributes().getValue(PANEL_PITCH); - } - - public ElectricityProducerSolarAsset setPanelPitch(Integer value) { - getAttributes().getOrCreate(PANEL_PITCH).setValue(value); - return this; - } - - public ElectricityProducerSolarAsset setIncludeForecastSolarService(boolean value) { - getAttributes().getOrCreate(INCLUDE_FORECAST_SOLAR_SERVICE).setValue(value); - return this; - } - - public ElectricityProducerSolarAsset setSetActualSolarValueWithForecast(boolean value) { - getAttributes().getOrCreate(SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST).setValue(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPower(Double value) { - super.setPower(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPowerSetpoint(Double value) { - super.setPowerSetpoint(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPowerImportMin(Double value) { - super.setPowerImportMin(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPowerImportMax(Double value) { - super.setPowerImportMax(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPowerExportMin(Double value) { - super.setPowerExportMin(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setPowerExportMax(Double value) { - super.setPowerExportMax(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setEnergyImportTotal(Double value) { - super.setEnergyImportTotal(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setEnergyExportTotal(Double value) { - super.setEnergyExportTotal(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setEfficiencyImport(Integer value) { - super.setEfficiencyImport(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setEfficiencyExport(Integer value) { - super.setEfficiencyExport(value); - return this; - } - - @Override - public ElectricityProducerSolarAsset setId(String id) { - super.setId(id); - return this; - } - - @Override - public ElectricityProducerSolarAsset setName(String name) throws IllegalArgumentException { - super.setName(name); - return this; - } - - @Override - public ElectricityProducerSolarAsset setAccessPublicRead(boolean accessPublicRead) { - super.setAccessPublicRead(accessPublicRead); - return this; - } - - @Override - public ElectricityProducerSolarAsset setParent(Asset parent) { - super.setParent(parent); - return this; - } - - @Override - public ElectricityProducerSolarAsset setParentId(String parentId) { - super.setParentId(parentId); - return this; - } - - @Override - public ElectricityProducerSolarAsset setRealm(String realm) { - super.setRealm(realm); - return this; - } - - @Override - public ElectricityProducerSolarAsset setAttributes(AttributeMap attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public Asset setAttributes(Attribute... attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerSolarAsset setAttributes(Collection> attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerSolarAsset addAttributes(Attribute... attributes) { - super.addAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerSolarAsset addOrReplaceAttributes(Attribute... attributes) { - super.addOrReplaceAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerSolarAsset setLocation(GeoJSONPoint location) { - super.setLocation(location); - return this; - } - - @Override - public ElectricityProducerSolarAsset setTags(String[] tags) { - super.setTags(tags); - return this; - } - - @Override - public ElectricityProducerSolarAsset setEmail(String email) { - super.setEmail(email); - return this; - } - - @Override - public ElectricityProducerSolarAsset setNotes(String notes) { - super.setNotes(notes); - return this; - } - - @Override - public ElectricityProducerSolarAsset setManufacturer(String manufacturer) { - super.setManufacturer(manufacturer); - return this; - } - - @Override - public ElectricityProducerSolarAsset setModel(String model) { - super.setModel(model); - return this; - } + public enum PanelOrientation { + SOUTH, + EAST_WEST + } + + public static final ValueDescriptor PANEL_ORIENTATION_VALUE = + new ValueDescriptor<>("panelOrientation", PanelOrientation.class); + + public static final AttributeDescriptor PANEL_ORIENTATION = + new AttributeDescriptor<>("panelOrientation", PANEL_ORIENTATION_VALUE); + public static final AttributeDescriptor PANEL_AZIMUTH = + new AttributeDescriptor<>("panelAzimuth", ValueType.INTEGER).withUnits(UNITS_DEGREE); + public static final AttributeDescriptor PANEL_PITCH = + new AttributeDescriptor<>("panelPitch", ValueType.POSITIVE_INTEGER).withUnits(UNITS_DEGREE); + + public static final AttributeDescriptor INCLUDE_FORECAST_SOLAR_SERVICE = + new AttributeDescriptor<>("includeForecastSolarService", BOOLEAN); + + public static final AttributeDescriptor SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST = + new AttributeDescriptor<>("setActualSolarValueWithForecast", BOOLEAN); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("white-balance-sunny", "EABB4D", ElectricityProducerSolarAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityProducerSolarAsset() {} + + public ElectricityProducerSolarAsset(String name) { + super(name); + } + + public Optional getPanelOrientation() { + return getAttributes().getValue(PANEL_ORIENTATION); + } + + public Optional isIncludeForecastSolarService() { + return getAttribute(INCLUDE_FORECAST_SOLAR_SERVICE).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional isSetActualSolarValueWithForecast() { + return getAttribute(SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST) + .flatMap(AbstractNameValueHolder::getValue); + } + + public ElectricityProducerSolarAsset setPanelOrientation(PanelOrientation value) { + getAttributes().getOrCreate(PANEL_ORIENTATION).setValue(value); + return this; + } + + public Optional getPanelAzimuth() { + return getAttributes().getValue(PANEL_AZIMUTH); + } + + public ElectricityProducerSolarAsset setPanelAzimuth(Integer value) { + getAttributes().getOrCreate(PANEL_AZIMUTH).setValue(value); + return this; + } + + public Optional getPanelPitch() { + return getAttributes().getValue(PANEL_PITCH); + } + + public ElectricityProducerSolarAsset setPanelPitch(Integer value) { + getAttributes().getOrCreate(PANEL_PITCH).setValue(value); + return this; + } + + public ElectricityProducerSolarAsset setIncludeForecastSolarService(boolean value) { + getAttributes().getOrCreate(INCLUDE_FORECAST_SOLAR_SERVICE).setValue(value); + return this; + } + + public ElectricityProducerSolarAsset setSetActualSolarValueWithForecast(boolean value) { + getAttributes().getOrCreate(SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST).setValue(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPower(Double value) { + super.setPower(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPowerSetpoint(Double value) { + super.setPowerSetpoint(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPowerImportMin(Double value) { + super.setPowerImportMin(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPowerImportMax(Double value) { + super.setPowerImportMax(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPowerExportMin(Double value) { + super.setPowerExportMin(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setPowerExportMax(Double value) { + super.setPowerExportMax(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setEnergyImportTotal(Double value) { + super.setEnergyImportTotal(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setEnergyExportTotal(Double value) { + super.setEnergyExportTotal(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setEfficiencyImport(Integer value) { + super.setEfficiencyImport(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setEfficiencyExport(Integer value) { + super.setEfficiencyExport(value); + return this; + } + + @Override + public ElectricityProducerSolarAsset setId(String id) { + super.setId(id); + return this; + } + + @Override + public ElectricityProducerSolarAsset setName(String name) throws IllegalArgumentException { + super.setName(name); + return this; + } + + @Override + public ElectricityProducerSolarAsset setAccessPublicRead(boolean accessPublicRead) { + super.setAccessPublicRead(accessPublicRead); + return this; + } + + @Override + public ElectricityProducerSolarAsset setParent(Asset parent) { + super.setParent(parent); + return this; + } + + @Override + public ElectricityProducerSolarAsset setParentId(String parentId) { + super.setParentId(parentId); + return this; + } + + @Override + public ElectricityProducerSolarAsset setRealm(String realm) { + super.setRealm(realm); + return this; + } + + @Override + public ElectricityProducerSolarAsset setAttributes(AttributeMap attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public Asset setAttributes(Attribute... attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerSolarAsset setAttributes(Collection> attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerSolarAsset addAttributes(Attribute... attributes) { + super.addAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerSolarAsset addOrReplaceAttributes(Attribute... attributes) { + super.addOrReplaceAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerSolarAsset setLocation(GeoJSONPoint location) { + super.setLocation(location); + return this; + } + + @Override + public ElectricityProducerSolarAsset setTags(String[] tags) { + super.setTags(tags); + return this; + } + + @Override + public ElectricityProducerSolarAsset setEmail(String email) { + super.setEmail(email); + return this; + } + + @Override + public ElectricityProducerSolarAsset setNotes(String notes) { + super.setNotes(notes); + return this; + } + + @Override + public ElectricityProducerSolarAsset setManufacturer(String manufacturer) { + super.setManufacturer(manufacturer); + return this; + } + + @Override + public ElectricityProducerSolarAsset setModel(String model) { + super.setModel(model); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java index e5d414d..e2ffbc1 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,18 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; +import static org.openremote.model.value.ValueType.BOOLEAN; + +import java.util.Collection; +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.Attribute; @@ -29,242 +34,241 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Collection; -import java.util.Optional; - -import static org.openremote.model.Constants.*; -import static org.openremote.model.value.ValueType.BOOLEAN; @Entity public class ElectricityProducerWindAsset extends ElectricityProducerAsset { - public static final AttributeDescriptor WIND_SPEED_REFERENCE = new AttributeDescriptor<>("windSpeedReference", ValueType.POSITIVE_NUMBER - ).withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); - public static final AttributeDescriptor WIND_SPEED_MIN = new AttributeDescriptor<>("windSpeedMin", ValueType.POSITIVE_NUMBER - ).withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); - public static final AttributeDescriptor WIND_SPEED_MAX = new AttributeDescriptor<>("windSpeedMax", ValueType.POSITIVE_NUMBER - ).withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); - - public static final AttributeDescriptor INCLUDE_FORECAST_WIND_SERVICE = new AttributeDescriptor<>("includeForecastWindService", BOOLEAN); - - public static final AttributeDescriptor SET_ACTUAL_WIND_VALUE_WITH_FORECAST = new AttributeDescriptor<>("setWindActualValueWithForecast", BOOLEAN); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("wind-turbine", "4B87EA", ElectricityProducerWindAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityProducerWindAsset() { - } - - public ElectricityProducerWindAsset(String name) { - super(name); - } - - public Optional getWindSpeedReference() { - return getAttributes().getValue(WIND_SPEED_REFERENCE); - } - - public Optional getWindSpeedMin() { - return getAttributes().getValue(WIND_SPEED_MIN); - } - - public Optional getWindSpeedMax() { - return getAttributes().getValue(WIND_SPEED_MAX); - } - - public Optional isIncludeForecastWindService() { - return getAttribute(INCLUDE_FORECAST_WIND_SERVICE).flatMap(AbstractNameValueHolder::getValue); - } - - public Optional isSetActualWindValueWithForecast() { - return getAttribute(SET_ACTUAL_WIND_VALUE_WITH_FORECAST).flatMap(AbstractNameValueHolder::getValue); - } - - @Override - public ElectricityProducerWindAsset setPower(Double value) { - super.setPower(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setPowerSetpoint(Double value) { - super.setPowerSetpoint(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setPowerImportMin(Double value) { - super.setPowerImportMin(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setPowerImportMax(Double value) { - super.setPowerImportMax(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setPowerExportMin(Double value) { - super.setPowerExportMin(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setPowerExportMax(Double value) { - super.setPowerExportMax(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setEnergyImportTotal(Double value) { - super.setEnergyImportTotal(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setEnergyExportTotal(Double value) { - super.setEnergyExportTotal(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setEfficiencyImport(Integer value) { - super.setEfficiencyImport(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setEfficiencyExport(Integer value) { - super.setEfficiencyExport(value); - return this; - } - - @Override - public ElectricityProducerWindAsset setId(String id) { - super.setId(id); - return this; - } - - @Override - public ElectricityProducerWindAsset setName(String name) throws IllegalArgumentException { - super.setName(name); - return this; - } - - @Override - public ElectricityProducerWindAsset setAccessPublicRead(boolean accessPublicRead) { - super.setAccessPublicRead(accessPublicRead); - return this; - } - - @Override - public ElectricityProducerWindAsset setParent(Asset parent) { - super.setParent(parent); - return this; - } - - @Override - public ElectricityProducerWindAsset setParentId(String parentId) { - super.setParentId(parentId); - return this; - } - - @Override - public ElectricityProducerWindAsset setRealm(String realm) { - super.setRealm(realm); - return this; - } - - @Override - public ElectricityProducerWindAsset setAttributes(AttributeMap attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public Asset setAttributes(Attribute... attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerWindAsset setAttributes(Collection> attributes) { - super.setAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerWindAsset addAttributes(Attribute... attributes) { - super.addAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerWindAsset addOrReplaceAttributes(Attribute... attributes) { - super.addOrReplaceAttributes(attributes); - return this; - } - - @Override - public ElectricityProducerWindAsset setLocation(GeoJSONPoint location) { - super.setLocation(location); - return this; - } - - @Override - public ElectricityProducerWindAsset setTags(String[] tags) { - super.setTags(tags); - return this; - } - - @Override - public ElectricityProducerWindAsset setEmail(String email) { - super.setEmail(email); - return this; - } - - @Override - public ElectricityProducerWindAsset setNotes(String notes) { - super.setNotes(notes); - return this; - } - - @Override - public ElectricityProducerWindAsset setManufacturer(String manufacturer) { - super.setManufacturer(manufacturer); - return this; - } - - @Override - public ElectricityProducerWindAsset setModel(String model) { - super.setModel(model); - return this; - } - - public ElectricityProducerWindAsset setWindSpeedMin(double value) { - getAttributes().getOrCreate(WIND_SPEED_MIN).setValue(value); - return this; - } - - public ElectricityProducerWindAsset setWindSpeedMax(double value) { - getAttributes().getOrCreate(WIND_SPEED_MAX).setValue(value); - return this; - } - - public ElectricityProducerWindAsset setWindSpeedReference(double value) { - getAttributes().getOrCreate(WIND_SPEED_REFERENCE).setValue(value); - return this; - } - - public ElectricityProducerWindAsset setIncludeForecastWindService(boolean value) { - getAttributes().getOrCreate(INCLUDE_FORECAST_WIND_SERVICE).setValue(value); - return this; - } - - public ElectricityProducerWindAsset setSetActualWindValueWithForecast(boolean value) { - getAttributes().getOrCreate(SET_ACTUAL_WIND_VALUE_WITH_FORECAST).setValue(value); - return this; - } + public static final AttributeDescriptor WIND_SPEED_REFERENCE = + new AttributeDescriptor<>("windSpeedReference", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); + public static final AttributeDescriptor WIND_SPEED_MIN = + new AttributeDescriptor<>("windSpeedMin", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); + public static final AttributeDescriptor WIND_SPEED_MAX = + new AttributeDescriptor<>("windSpeedMax", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_METRE, UNITS_PER, UNITS_SECOND); + + public static final AttributeDescriptor INCLUDE_FORECAST_WIND_SERVICE = + new AttributeDescriptor<>("includeForecastWindService", BOOLEAN); + + public static final AttributeDescriptor SET_ACTUAL_WIND_VALUE_WITH_FORECAST = + new AttributeDescriptor<>("setWindActualValueWithForecast", BOOLEAN); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("wind-turbine", "4B87EA", ElectricityProducerWindAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityProducerWindAsset() {} + + public ElectricityProducerWindAsset(String name) { + super(name); + } + + public Optional getWindSpeedReference() { + return getAttributes().getValue(WIND_SPEED_REFERENCE); + } + + public Optional getWindSpeedMin() { + return getAttributes().getValue(WIND_SPEED_MIN); + } + + public Optional getWindSpeedMax() { + return getAttributes().getValue(WIND_SPEED_MAX); + } + + public Optional isIncludeForecastWindService() { + return getAttribute(INCLUDE_FORECAST_WIND_SERVICE).flatMap(AbstractNameValueHolder::getValue); + } + + public Optional isSetActualWindValueWithForecast() { + return getAttribute(SET_ACTUAL_WIND_VALUE_WITH_FORECAST) + .flatMap(AbstractNameValueHolder::getValue); + } + + @Override + public ElectricityProducerWindAsset setPower(Double value) { + super.setPower(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setPowerSetpoint(Double value) { + super.setPowerSetpoint(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setPowerImportMin(Double value) { + super.setPowerImportMin(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setPowerImportMax(Double value) { + super.setPowerImportMax(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setPowerExportMin(Double value) { + super.setPowerExportMin(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setPowerExportMax(Double value) { + super.setPowerExportMax(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setEnergyImportTotal(Double value) { + super.setEnergyImportTotal(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setEnergyExportTotal(Double value) { + super.setEnergyExportTotal(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setEfficiencyImport(Integer value) { + super.setEfficiencyImport(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setEfficiencyExport(Integer value) { + super.setEfficiencyExport(value); + return this; + } + + @Override + public ElectricityProducerWindAsset setId(String id) { + super.setId(id); + return this; + } + + @Override + public ElectricityProducerWindAsset setName(String name) throws IllegalArgumentException { + super.setName(name); + return this; + } + + @Override + public ElectricityProducerWindAsset setAccessPublicRead(boolean accessPublicRead) { + super.setAccessPublicRead(accessPublicRead); + return this; + } + + @Override + public ElectricityProducerWindAsset setParent(Asset parent) { + super.setParent(parent); + return this; + } + + @Override + public ElectricityProducerWindAsset setParentId(String parentId) { + super.setParentId(parentId); + return this; + } + + @Override + public ElectricityProducerWindAsset setRealm(String realm) { + super.setRealm(realm); + return this; + } + + @Override + public ElectricityProducerWindAsset setAttributes(AttributeMap attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public Asset setAttributes(Attribute... attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerWindAsset setAttributes(Collection> attributes) { + super.setAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerWindAsset addAttributes(Attribute... attributes) { + super.addAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerWindAsset addOrReplaceAttributes(Attribute... attributes) { + super.addOrReplaceAttributes(attributes); + return this; + } + + @Override + public ElectricityProducerWindAsset setLocation(GeoJSONPoint location) { + super.setLocation(location); + return this; + } + + @Override + public ElectricityProducerWindAsset setTags(String[] tags) { + super.setTags(tags); + return this; + } + + @Override + public ElectricityProducerWindAsset setEmail(String email) { + super.setEmail(email); + return this; + } + + @Override + public ElectricityProducerWindAsset setNotes(String notes) { + super.setNotes(notes); + return this; + } + + @Override + public ElectricityProducerWindAsset setManufacturer(String manufacturer) { + super.setManufacturer(manufacturer); + return this; + } + + @Override + public ElectricityProducerWindAsset setModel(String model) { + super.setModel(model); + return this; + } + + public ElectricityProducerWindAsset setWindSpeedMin(double value) { + getAttributes().getOrCreate(WIND_SPEED_MIN).setValue(value); + return this; + } + + public ElectricityProducerWindAsset setWindSpeedMax(double value) { + getAttributes().getOrCreate(WIND_SPEED_MAX).setValue(value); + return this; + } + + public ElectricityProducerWindAsset setWindSpeedReference(double value) { + getAttributes().getOrCreate(WIND_SPEED_REFERENCE).setValue(value); + return this; + } + + public ElectricityProducerWindAsset setIncludeForecastWindService(boolean value) { + getAttributes().getOrCreate(INCLUDE_FORECAST_WIND_SERVICE).setValue(value); + return this; + } + + public ElectricityProducerWindAsset setSetActualWindValueWithForecast(boolean value) { + getAttributes().getOrCreate(SET_ACTUAL_WIND_VALUE_WITH_FORECAST).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java index 9fc17b9..27ce0dc 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,16 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; + +import java.util.Optional; + import org.openremote.model.attribute.AttributeExecuteStatus; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AttributeDescriptor; @@ -26,115 +29,126 @@ import org.openremote.model.value.ValueConstraint; import org.openremote.model.value.ValueType; -import java.util.Optional; - -import static org.openremote.model.Constants.*; - public abstract class ElectricityStorageAsset extends ElectricityAsset { - - public static final AttributeDescriptor SUPPORTS_EXPORT = new AttributeDescriptor<>("supportsExport", ValueType.BOOLEAN); - public static final AttributeDescriptor SUPPORTS_IMPORT = new AttributeDescriptor<>("supportsImport", ValueType.BOOLEAN); - public static final AttributeDescriptor ENERGY_LEVEL = new AttributeDescriptor<>("energyLevel", ValueType.POSITIVE_NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor ENERGY_CAPACITY = new AttributeDescriptor<>("energyCapacity", ValueType.POSITIVE_NUMBER) - .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE = new AttributeDescriptor<>("energyLevelPercentage", ValueType.POSITIVE_INTEGER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MAX = new AttributeDescriptor<>("energyLevelPercentageMax", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MIN = new AttributeDescriptor<>("energyLevelPercentageMin", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - public static final AttributeDescriptor ENERGY_LEVEL_SCHEDULE = new AttributeDescriptor<>("energyLevelSchedule", ValueType.POSITIVE_INTEGER.asArray().asArray()) - .withOptional(true); - public static final AttributeDescriptor FORCE_CHARGE = new AttributeDescriptor<>("forceCharge", ValueType.EXECUTION_STATUS); - - public static final AttributeDescriptor POWER_SETPOINT = ElectricityAsset.POWER_SETPOINT.withOptional(false); - public static final AttributeDescriptor POWER_IMPORT_MIN = ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MIN = ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); - public static final AttributeDescriptor CARBON_IMPORT = ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricityStorageAsset() { - } - - public ElectricityStorageAsset(String name) { - super(name); - } - - public Optional isSupportsExport() { - return getAttributes().getValue(SUPPORTS_EXPORT); - } - - public ElectricityStorageAsset setSupportsExport(Boolean value) { - getAttributes().getOrCreate(SUPPORTS_EXPORT).setValue(value); - return this; - } - - public Optional isSupportsImport() { - return getAttributes().getValue(SUPPORTS_IMPORT); - } - - public ElectricityStorageAsset setSupportsImport(Boolean value) { - getAttributes().getOrCreate(SUPPORTS_IMPORT).setValue(value); - return this; - } - - public Optional getEnergyCapacity() { - return getAttributes().getValue(ENERGY_CAPACITY); - } - - public ElectricityStorageAsset setEnergyCapacity(Double value) { - getAttributes().getOrCreate(ENERGY_CAPACITY).setValue(value); - return this; - } - - public Optional getEnergyLevel() { - return getAttributes().getValue(ENERGY_LEVEL); - } - - public ElectricityStorageAsset setEnergyLevel(Double value) { - getAttributes().getOrCreate(ENERGY_LEVEL).setValue(value); - return this; - } - - public Optional getEnergyLevelPercentage() { - return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE); - } - - public ElectricityStorageAsset setEnergyLevelPercentage(Integer value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE).setValue(value); - return this; - } - - public Optional getEnergyLevelPercentageMin() { - return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE_MIN); - } - - public ElectricityStorageAsset setEnergyLevelPercentageMin(Integer value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MIN).setValue(value); - return this; - } - - public Optional getEnergyLevelPercentageMax() { - return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE_MAX); - } - - public ElectricityStorageAsset setEnergyLevelPercentageMax(Integer value) { - getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MAX).setValue(value); - return this; - } - - public Optional getEnergyLevelSchedule() { - return getAttributes().getValue(ENERGY_LEVEL_SCHEDULE); - } - - public ElectricityStorageAsset setEnergyLevelSchedule(Integer[][] value) { - getAttributes().getOrCreate(ENERGY_LEVEL_SCHEDULE).setValue(value); - return this; - } + public static final AttributeDescriptor SUPPORTS_EXPORT = + new AttributeDescriptor<>("supportsExport", ValueType.BOOLEAN); + public static final AttributeDescriptor SUPPORTS_IMPORT = + new AttributeDescriptor<>("supportsImport", ValueType.BOOLEAN); + public static final AttributeDescriptor ENERGY_LEVEL = + new AttributeDescriptor<>( + "energyLevel", ValueType.POSITIVE_NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + public static final AttributeDescriptor ENERGY_CAPACITY = + new AttributeDescriptor<>("energyCapacity", ValueType.POSITIVE_NUMBER) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE = + new AttributeDescriptor<>( + "energyLevelPercentage", + ValueType.POSITIVE_INTEGER, + new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MAX = + new AttributeDescriptor<>("energyLevelPercentageMax", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + public static final AttributeDescriptor ENERGY_LEVEL_PERCENTAGE_MIN = + new AttributeDescriptor<>("energyLevelPercentageMin", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + public static final AttributeDescriptor ENERGY_LEVEL_SCHEDULE = + new AttributeDescriptor<>( + "energyLevelSchedule", ValueType.POSITIVE_INTEGER.asArray().asArray()) + .withOptional(true); + public static final AttributeDescriptor FORCE_CHARGE = + new AttributeDescriptor<>("forceCharge", ValueType.EXECUTION_STATUS); + + public static final AttributeDescriptor POWER_SETPOINT = + ElectricityAsset.POWER_SETPOINT.withOptional(false); + public static final AttributeDescriptor POWER_IMPORT_MIN = + ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MIN = + ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); + public static final AttributeDescriptor CARBON_IMPORT = + ElectricitySupplierAsset.CARBON_IMPORT.withOptional(true); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricityStorageAsset() {} + + public ElectricityStorageAsset(String name) { + super(name); + } + + public Optional isSupportsExport() { + return getAttributes().getValue(SUPPORTS_EXPORT); + } + + public ElectricityStorageAsset setSupportsExport(Boolean value) { + getAttributes().getOrCreate(SUPPORTS_EXPORT).setValue(value); + return this; + } + + public Optional isSupportsImport() { + return getAttributes().getValue(SUPPORTS_IMPORT); + } + + public ElectricityStorageAsset setSupportsImport(Boolean value) { + getAttributes().getOrCreate(SUPPORTS_IMPORT).setValue(value); + return this; + } + + public Optional getEnergyCapacity() { + return getAttributes().getValue(ENERGY_CAPACITY); + } + + public ElectricityStorageAsset setEnergyCapacity(Double value) { + getAttributes().getOrCreate(ENERGY_CAPACITY).setValue(value); + return this; + } + + public Optional getEnergyLevel() { + return getAttributes().getValue(ENERGY_LEVEL); + } + + public ElectricityStorageAsset setEnergyLevel(Double value) { + getAttributes().getOrCreate(ENERGY_LEVEL).setValue(value); + return this; + } + + public Optional getEnergyLevelPercentage() { + return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE); + } + + public ElectricityStorageAsset setEnergyLevelPercentage(Integer value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE).setValue(value); + return this; + } + + public Optional getEnergyLevelPercentageMin() { + return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE_MIN); + } + + public ElectricityStorageAsset setEnergyLevelPercentageMin(Integer value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MIN).setValue(value); + return this; + } + + public Optional getEnergyLevelPercentageMax() { + return getAttributes().getValue(ENERGY_LEVEL_PERCENTAGE_MAX); + } + + public ElectricityStorageAsset setEnergyLevelPercentageMax(Integer value) { + getAttributes().getOrCreate(ENERGY_LEVEL_PERCENTAGE_MAX).setValue(value); + return this; + } + + public Optional getEnergyLevelSchedule() { + return getAttributes().getValue(ENERGY_LEVEL_SCHEDULE); + } + + public ElectricityStorageAsset setEnergyLevelSchedule(Integer[][] value) { + getAttributes().getOrCreate(ENERGY_LEVEL_SCHEDULE).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java index 802dacb..29e0c0e 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2020, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,17 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; +import static org.openremote.model.value.ValueType.NUMBER; + +import java.util.Optional; + import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; import org.openremote.model.value.AbstractNameValueHolder; @@ -27,125 +31,135 @@ import org.openremote.model.value.ValueType; import jakarta.persistence.Entity; -import java.util.Optional; - -import static org.openremote.model.Constants.*; -import static org.openremote.model.value.ValueType.NUMBER; @Entity public class ElectricitySupplierAsset extends ElectricityAsset { - public static final AttributeDescriptor FINANCIAL_COST = new AttributeDescriptor<>("financialCost", ValueType.NUMBER).withUnits("EUR"); - public static final AttributeDescriptor CARBON_COST = new AttributeDescriptor<>("carbonCost", ValueType.NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_GRAM); - public static final AttributeDescriptor ENERGY_LOCAL = new AttributeDescriptor<>("energyLocal", NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor ENERGY_RENEWABLE_SHARE = new AttributeDescriptor<>("energyRenewableShare", NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_PERCENTAGE); - public static final AttributeDescriptor ENERGY_SELF_CONSUMPTION = new AttributeDescriptor<>("energySelfConsumption", NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_PERCENTAGE); - public static final AttributeDescriptor ENERGY_AUTARKY = new AttributeDescriptor<>("energyAutarky", NUMBER, - new MetaItem<>(MetaItemType.READ_ONLY) - ).withUnits(UNITS_PERCENTAGE); - - public static final AttributeDescriptor POWER_SETPOINT = ElectricityAsset.POWER_SETPOINT.withOptional(true); - public static final AttributeDescriptor POWER_IMPORT_MIN = ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MAX = ElectricityAsset.POWER_EXPORT_MAX.withOptional(true); - public static final AttributeDescriptor POWER_EXPORT_MIN = ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_IMPORT = ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); - public static final AttributeDescriptor EFFICIENCY_EXPORT = ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); - public static final AttributeDescriptor TARIFF_IMPORT = ElectricityAsset.TARIFF_IMPORT.withOptional(false); - public static final AttributeDescriptor TARIFF_EXPORT = ElectricityAsset.TARIFF_EXPORT.withOptional(false); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("upload-network", "9257A9", ElectricitySupplierAsset.class); - public static final AttributeDescriptor CARBON_IMPORT = new AttributeDescriptor<>("carbonImport", ValueType.NUMBER) - .withUnits(UNITS_KILO, UNITS_GRAM, UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR).withOptional(false); - public static final AttributeDescriptor CARBON_EXPORT = new AttributeDescriptor<>("carbonExport", ValueType.NUMBER) - .withUnits(UNITS_KILO, UNITS_GRAM, UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR).withOptional(false); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected ElectricitySupplierAsset() { - } - - public ElectricitySupplierAsset(String name) { - super(name); - } - - public Optional getFinancialCost() { - return getAttributes().getValue(FINANCIAL_COST); - } - - public ElectricitySupplierAsset setFinancialCost(Double value) { - getAttributes().getOrCreate(FINANCIAL_COST).setValue(value); - return this; - } - - public Optional getCarbonCost() { - return getAttributes().getValue(CARBON_COST); - } - - public ElectricitySupplierAsset setCarbonCost(Double value) { - getAttributes().getOrCreate(CARBON_COST).setValue(value); - return this; - } - - public Optional getCarbonImport() { - return getAttributes().getValue(CARBON_IMPORT); - } - - public ElectricitySupplierAsset setCarbonImport(Double value) { - getAttributes().getOrCreate(CARBON_IMPORT).setValue(value); - return this; - } - - public Optional getCarbonExport() { - return getAttributes().getValue(CARBON_EXPORT); - } - - public ElectricitySupplierAsset setCarbonExport(Double value) { - getAttributes().getOrCreate(CARBON_EXPORT).setValue(value); - return this; - } - - public Optional getEnergyLocal() { - return getAttribute(ENERGY_LOCAL).flatMap(AbstractNameValueHolder::getValue); - } - - public ElectricitySupplierAsset setEnergyLocal(Double value) { - getAttributes().getOrCreate(ENERGY_LOCAL).setValue(value); - return this; - } - - public Optional getEnergyRenewableShare() { - return getAttribute(ENERGY_RENEWABLE_SHARE).flatMap(AbstractNameValueHolder::getValue); - } - - public ElectricitySupplierAsset setEnergyRenewableShare(Double value) { - getAttributes().getOrCreate(ENERGY_RENEWABLE_SHARE).setValue(value); - return this; - } - - public Optional getEnergySelfConsumption() { - return getAttribute(ENERGY_SELF_CONSUMPTION).flatMap(AbstractNameValueHolder::getValue); - } - - public ElectricitySupplierAsset setEnergySelfConsumption(Double value) { - getAttributes().getOrCreate(ENERGY_SELF_CONSUMPTION).setValue(value); - return this; - } - - public Optional getEnergyAutarky() { - return getAttribute(ENERGY_AUTARKY).flatMap(AbstractNameValueHolder::getValue); - } - - public ElectricitySupplierAsset setEnergyAutarky(Double value) { - getAttributes().getOrCreate(ENERGY_AUTARKY).setValue(value); - return this; - } + public static final AttributeDescriptor FINANCIAL_COST = + new AttributeDescriptor<>("financialCost", ValueType.NUMBER).withUnits("EUR"); + public static final AttributeDescriptor CARBON_COST = + new AttributeDescriptor<>( + "carbonCost", ValueType.NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_GRAM); + public static final AttributeDescriptor ENERGY_LOCAL = + new AttributeDescriptor<>("energyLocal", NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); + public static final AttributeDescriptor ENERGY_RENEWABLE_SHARE = + new AttributeDescriptor<>( + "energyRenewableShare", NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_PERCENTAGE); + public static final AttributeDescriptor ENERGY_SELF_CONSUMPTION = + new AttributeDescriptor<>( + "energySelfConsumption", NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_PERCENTAGE); + public static final AttributeDescriptor ENERGY_AUTARKY = + new AttributeDescriptor<>("energyAutarky", NUMBER, new MetaItem<>(MetaItemType.READ_ONLY)) + .withUnits(UNITS_PERCENTAGE); + + public static final AttributeDescriptor POWER_SETPOINT = + ElectricityAsset.POWER_SETPOINT.withOptional(true); + public static final AttributeDescriptor POWER_IMPORT_MIN = + ElectricityAsset.POWER_IMPORT_MIN.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MAX = + ElectricityAsset.POWER_EXPORT_MAX.withOptional(true); + public static final AttributeDescriptor POWER_EXPORT_MIN = + ElectricityAsset.POWER_EXPORT_MIN.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_IMPORT = + ElectricityAsset.EFFICIENCY_IMPORT.withOptional(true); + public static final AttributeDescriptor EFFICIENCY_EXPORT = + ElectricityAsset.EFFICIENCY_EXPORT.withOptional(true); + public static final AttributeDescriptor TARIFF_IMPORT = + ElectricityAsset.TARIFF_IMPORT.withOptional(false); + public static final AttributeDescriptor TARIFF_EXPORT = + ElectricityAsset.TARIFF_EXPORT.withOptional(false); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("upload-network", "9257A9", ElectricitySupplierAsset.class); + public static final AttributeDescriptor CARBON_IMPORT = + new AttributeDescriptor<>("carbonImport", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_GRAM, UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR) + .withOptional(false); + public static final AttributeDescriptor CARBON_EXPORT = + new AttributeDescriptor<>("carbonExport", ValueType.NUMBER) + .withUnits(UNITS_KILO, UNITS_GRAM, UNITS_PER, UNITS_KILO, UNITS_WATT, UNITS_HOUR) + .withOptional(false); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected ElectricitySupplierAsset() {} + + public ElectricitySupplierAsset(String name) { + super(name); + } + + public Optional getFinancialCost() { + return getAttributes().getValue(FINANCIAL_COST); + } + + public ElectricitySupplierAsset setFinancialCost(Double value) { + getAttributes().getOrCreate(FINANCIAL_COST).setValue(value); + return this; + } + + public Optional getCarbonCost() { + return getAttributes().getValue(CARBON_COST); + } + + public ElectricitySupplierAsset setCarbonCost(Double value) { + getAttributes().getOrCreate(CARBON_COST).setValue(value); + return this; + } + + public Optional getCarbonImport() { + return getAttributes().getValue(CARBON_IMPORT); + } + + public ElectricitySupplierAsset setCarbonImport(Double value) { + getAttributes().getOrCreate(CARBON_IMPORT).setValue(value); + return this; + } + + public Optional getCarbonExport() { + return getAttributes().getValue(CARBON_EXPORT); + } + + public ElectricitySupplierAsset setCarbonExport(Double value) { + getAttributes().getOrCreate(CARBON_EXPORT).setValue(value); + return this; + } + + public Optional getEnergyLocal() { + return getAttribute(ENERGY_LOCAL).flatMap(AbstractNameValueHolder::getValue); + } + + public ElectricitySupplierAsset setEnergyLocal(Double value) { + getAttributes().getOrCreate(ENERGY_LOCAL).setValue(value); + return this; + } + + public Optional getEnergyRenewableShare() { + return getAttribute(ENERGY_RENEWABLE_SHARE).flatMap(AbstractNameValueHolder::getValue); + } + + public ElectricitySupplierAsset setEnergyRenewableShare(Double value) { + getAttributes().getOrCreate(ENERGY_RENEWABLE_SHARE).setValue(value); + return this; + } + + public Optional getEnergySelfConsumption() { + return getAttribute(ENERGY_SELF_CONSUMPTION).flatMap(AbstractNameValueHolder::getValue); + } + + public ElectricitySupplierAsset setEnergySelfConsumption(Double value) { + getAttributes().getOrCreate(ENERGY_SELF_CONSUMPTION).setValue(value); + return this; + } + + public Optional getEnergyAutarky() { + return getAttribute(ENERGY_AUTARKY).flatMap(AbstractNameValueHolder::getValue); + } + + public ElectricitySupplierAsset setEnergyAutarky(Double value) { + getAttributes().getOrCreate(ENERGY_AUTARKY).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java b/energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java index 8640a44..e9873c4 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java @@ -1,9 +1,6 @@ /* * Copyright 2025, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,19 +12,19 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; import org.openremote.model.AssetModelProvider; -/** - * Provides the energy asset classes. - */ +/** Provides the energy asset classes. */ public class EnergyModelProvider implements AssetModelProvider { - @Override - public boolean useAutoScan() { - return true; - } + @Override + public boolean useAutoScan() { + return true; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java b/energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java index 70124ad..415aa93 100644 --- a/energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java +++ b/energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,10 +12,18 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.model; +import static org.openremote.model.Constants.*; +import static org.openremote.model.value.MetaItemType.READ_ONLY; +import static org.openremote.model.value.ValueType.*; + +import java.util.Optional; + import org.openremote.model.asset.Asset; import org.openremote.model.asset.AssetDescriptor; import org.openremote.model.attribute.MetaItem; @@ -26,76 +31,76 @@ import jakarta.persistence.Entity; -import java.util.Optional; - -import static org.openremote.model.Constants.*; -import static org.openremote.model.value.MetaItemType.READ_ONLY; -import static org.openremote.model.value.ValueType.*; - @Entity public class EnergyOptimisationAsset extends Asset { - public static final AttributeDescriptor FINANCIAL_WEIGHTING = new AttributeDescriptor<>("financialWeighting", ValueType.POSITIVE_INTEGER) - .withUnits(UNITS_PERCENTAGE).withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); - public static final AttributeDescriptor INTERVAL_SIZE = new AttributeDescriptor<>("intervalSize", POSITIVE_NUMBER); - public static final AttributeDescriptor OPTIMISATION_DISABLED = new AttributeDescriptor<>("optimisationDisabled", BOOLEAN, new MetaItem<>(READ_ONLY)); - public static final AttributeDescriptor FINANCIAL_SAVING = new AttributeDescriptor<>("financialSaving", NUMBER, new MetaItem<>(READ_ONLY)).withUnits("EUR"); - public static final AttributeDescriptor CARBON_SAVING = new AttributeDescriptor<>("carbonSaving", NUMBER, new MetaItem<>(READ_ONLY)).withUnits(UNITS_KILO, UNITS_GRAM); - - public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("flash", "C4DB0D", EnergyOptimisationAsset.class); - - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected EnergyOptimisationAsset() { - } - - public EnergyOptimisationAsset(String name) { - super(name); - } - - public Optional getFinancialWeighting() { - return getAttribute(FINANCIAL_WEIGHTING).flatMap(AbstractNameValueHolder::getValue); - } - - public EnergyOptimisationAsset setFinancialWeighting(Integer value) { - getAttributes().getOrCreate(FINANCIAL_WEIGHTING).setValue(value); - return this; - } - - public Optional getIntervalSize() { - return getAttribute(INTERVAL_SIZE).flatMap(AbstractNameValueHolder::getValue); - } - - public EnergyOptimisationAsset setIntervalSize(Double value) { - getAttributes().getOrCreate(INTERVAL_SIZE).setValue(value); - return this; - } - - public Optional isOptimisationDisabled() { - return getAttribute(OPTIMISATION_DISABLED).flatMap(AbstractNameValueHolder::getValue); - } - - public EnergyOptimisationAsset setOptimisationDisabled(Boolean value) { - getAttributes().getOrCreate(OPTIMISATION_DISABLED).setValue(value); - return this; - } - - public Optional getFinancialSaving() { - return getAttribute(FINANCIAL_SAVING).flatMap(AbstractNameValueHolder::getValue); - } - - public EnergyOptimisationAsset setFinancialSaving(Double value) { - getAttributes().getOrCreate(FINANCIAL_SAVING).setValue(value); - return this; - } - - public Optional getCarbonSaving() { - return getAttribute(CARBON_SAVING).flatMap(AbstractNameValueHolder::getValue); - } - - public EnergyOptimisationAsset setCarbonSaving(Double value) { - getAttributes().getOrCreate(CARBON_SAVING).setValue(value); - return this; - } + public static final AttributeDescriptor FINANCIAL_WEIGHTING = + new AttributeDescriptor<>("financialWeighting", ValueType.POSITIVE_INTEGER) + .withUnits(UNITS_PERCENTAGE) + .withConstraints(new ValueConstraint.Min(0), new ValueConstraint.Max(100)); + public static final AttributeDescriptor INTERVAL_SIZE = + new AttributeDescriptor<>("intervalSize", POSITIVE_NUMBER); + public static final AttributeDescriptor OPTIMISATION_DISABLED = + new AttributeDescriptor<>("optimisationDisabled", BOOLEAN, new MetaItem<>(READ_ONLY)); + public static final AttributeDescriptor FINANCIAL_SAVING = + new AttributeDescriptor<>("financialSaving", NUMBER, new MetaItem<>(READ_ONLY)) + .withUnits("EUR"); + public static final AttributeDescriptor CARBON_SAVING = + new AttributeDescriptor<>("carbonSaving", NUMBER, new MetaItem<>(READ_ONLY)) + .withUnits(UNITS_KILO, UNITS_GRAM); + + public static final AssetDescriptor DESCRIPTOR = + new AssetDescriptor<>("flash", "C4DB0D", EnergyOptimisationAsset.class); + + /** For use by hydrators (i.e. JPA/Jackson) */ + protected EnergyOptimisationAsset() {} + + public EnergyOptimisationAsset(String name) { + super(name); + } + + public Optional getFinancialWeighting() { + return getAttribute(FINANCIAL_WEIGHTING).flatMap(AbstractNameValueHolder::getValue); + } + + public EnergyOptimisationAsset setFinancialWeighting(Integer value) { + getAttributes().getOrCreate(FINANCIAL_WEIGHTING).setValue(value); + return this; + } + + public Optional getIntervalSize() { + return getAttribute(INTERVAL_SIZE).flatMap(AbstractNameValueHolder::getValue); + } + + public EnergyOptimisationAsset setIntervalSize(Double value) { + getAttributes().getOrCreate(INTERVAL_SIZE).setValue(value); + return this; + } + + public Optional isOptimisationDisabled() { + return getAttribute(OPTIMISATION_DISABLED).flatMap(AbstractNameValueHolder::getValue); + } + + public EnergyOptimisationAsset setOptimisationDisabled(Boolean value) { + getAttributes().getOrCreate(OPTIMISATION_DISABLED).setValue(value); + return this; + } + + public Optional getFinancialSaving() { + return getAttribute(FINANCIAL_SAVING).flatMap(AbstractNameValueHolder::getValue); + } + + public EnergyOptimisationAsset setFinancialSaving(Double value) { + getAttributes().getOrCreate(FINANCIAL_SAVING).setValue(value); + return this; + } + + public Optional getCarbonSaving() { + return getAttribute(CARBON_SAVING).flatMap(AbstractNameValueHolder::getValue); + } + + public EnergyOptimisationAsset setCarbonSaving(Double value) { + getAttributes().getOrCreate(CARBON_SAVING).setValue(value); + return this; + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java index 8f85771..b08a9e2 100644 --- a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java @@ -1,9 +1,6 @@ /* * Copyright 2017, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.storage; @@ -25,24 +24,27 @@ import jakarta.persistence.Entity; @Entity -public class StorageSimulatorAgent extends Agent { +public class StorageSimulatorAgent + extends Agent { - public static AgentDescriptor DESCRIPTOR = new AgentDescriptor<>( - StorageSimulatorAgent.class, StorageSimulatorProtocol.class, StorageSimulatorAgentLink.class, null - ); + public static AgentDescriptor< + StorageSimulatorAgent, StorageSimulatorProtocol, StorageSimulatorAgentLink> + DESCRIPTOR = + new AgentDescriptor<>( + StorageSimulatorAgent.class, + StorageSimulatorProtocol.class, + StorageSimulatorAgentLink.class, + null); - /** - * For use by hydrators (i.e. JPA/Jackson) - */ - protected StorageSimulatorAgent() { - } + /** For use by hydrators (i.e. JPA/Jackson) */ + protected StorageSimulatorAgent() {} - public StorageSimulatorAgent(String name) { - super(name); - } + public StorageSimulatorAgent(String name) { + super(name); + } - @Override - public StorageSimulatorProtocol getProtocolInstance() { - return new StorageSimulatorProtocol(this); - } + @Override + public StorageSimulatorProtocol getProtocolInstance() { + return new StorageSimulatorProtocol(this); + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java index 92d6975..7590cd4 100644 --- a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java @@ -1,9 +1,6 @@ /* * Copyright 2021, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,18 +12,19 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.storage; import org.openremote.model.asset.agent.AgentLink; public class StorageSimulatorAgentLink extends AgentLink { - // For Hydrators - protected StorageSimulatorAgentLink() { - } + // For Hydrators + protected StorageSimulatorAgentLink() {} - public StorageSimulatorAgentLink(String id) { - super(id); - } + public StorageSimulatorAgentLink(String id) { + super(id); + } } diff --git a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java index b7e2e0f..4b8b4a6 100644 --- a/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java @@ -1,9 +1,6 @@ /* * Copyright 2017, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,19 +12,14 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.energy.storage; -import org.openremote.agent.protocol.AbstractProtocol; -import org.openremote.model.Container; -import org.openremote.model.asset.Asset; -import org.openremote.model.asset.agent.ConnectionStatus; -import org.openremote.extension.energy.model.ElectricityStorageAsset; -import org.openremote.model.attribute.Attribute; -import org.openremote.model.attribute.AttributeEvent; -import org.openremote.model.attribute.AttributeRef; -import org.openremote.model.syslog.SyslogCategory; +import static org.openremote.extension.energy.model.ElectricityStorageAsset.*; +import static org.openremote.model.syslog.SyslogCategory.PROTOCOL; import java.time.Duration; import java.time.Instant; @@ -40,196 +32,230 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static org.openremote.extension.energy.model.ElectricityStorageAsset.*; -import static org.openremote.model.syslog.SyslogCategory.PROTOCOL; - -public class StorageSimulatorProtocol extends AbstractProtocol { - - public static final String PROTOCOL_DISPLAY_NAME = "StorageSimulator"; - private static final Logger LOG = SyslogCategory.getLogger(PROTOCOL, StorageSimulatorProtocol.class); - protected final Map lastUpdateMap = new HashMap<>(); - protected final Map> simulationMap = new HashMap<>(); - protected final ReentrantLock lock = new ReentrantLock(); - - public StorageSimulatorProtocol(StorageSimulatorAgent agent) { - super(agent); - } - - @Override - public String getProtocolName() { - return PROTOCOL_DISPLAY_NAME; - } - - @Override - public String getProtocolInstanceUri() { - return "storagesimulator://" + agent.getId(); +import org.openremote.agent.protocol.AbstractProtocol; +import org.openremote.extension.energy.model.ElectricityStorageAsset; +import org.openremote.model.Container; +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.agent.ConnectionStatus; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeEvent; +import org.openremote.model.attribute.AttributeRef; +import org.openremote.model.syslog.SyslogCategory; +public class StorageSimulatorProtocol + extends AbstractProtocol { + + public static final String PROTOCOL_DISPLAY_NAME = "StorageSimulator"; + private static final Logger LOG = + SyslogCategory.getLogger(PROTOCOL, StorageSimulatorProtocol.class); + protected final Map lastUpdateMap = new HashMap<>(); + protected final Map> simulationMap = new HashMap<>(); + protected final ReentrantLock lock = new ReentrantLock(); + + public StorageSimulatorProtocol(StorageSimulatorAgent agent) { + super(agent); + } + + @Override + public String getProtocolName() { + return PROTOCOL_DISPLAY_NAME; + } + + @Override + public String getProtocolInstanceUri() { + return "storagesimulator://" + agent.getId(); + } + + @Override + protected void doStart(Container container) throws Exception { + setConnectionStatus(ConnectionStatus.CONNECTED); + } + + @Override + protected void doStop(Container container) throws Exception { + lock.lock(); + try { + new HashSet<>(simulationMap.keySet()).forEach(assetId -> stopSimulation(assetId)); + } finally { + lock.unlock(); } - - @Override - protected void doStart(Container container) throws Exception { - setConnectionStatus(ConnectionStatus.CONNECTED); + } + + @Override + protected void doLinkAttribute( + String assetId, Attribute attribute, StorageSimulatorAgentLink agentLink) + throws RuntimeException { + lock.lock(); + try { + if (checkLinkedAttributes(assetId) && !isSimulationStarted(assetId)) { + startSimulation(assetId); + } + } finally { + lock.unlock(); } - - @Override - protected void doStop(Container container) throws Exception { - lock.lock(); - try { - new HashSet<>(simulationMap.keySet()).forEach(assetId -> stopSimulation(assetId)); - } finally { - lock.unlock(); - } + } + + @Override + protected void doUnlinkAttribute( + String assetId, Attribute attribute, StorageSimulatorAgentLink agentLink) { + lock.lock(); + try { + if (!checkLinkedAttributes(assetId) && isSimulationStarted(assetId)) { + stopSimulation(assetId); + LOG.info("Stopped storage simulation for asset id: " + assetId); + } + } finally { + lock.unlock(); } + } - @Override - protected void doLinkAttribute(String assetId, Attribute attribute, StorageSimulatorAgentLink agentLink) throws RuntimeException { - lock.lock(); - try { - if (checkLinkedAttributes(assetId) && !isSimulationStarted(assetId)) { - startSimulation(assetId); - } - } finally { - lock.unlock(); - } - } + @Override + protected void doLinkedAttributeWrite( + StorageSimulatorAgentLink agentLink, AttributeEvent event, Object processedValue) { - @Override - protected void doUnlinkAttribute(String assetId, Attribute attribute, StorageSimulatorAgentLink agentLink) { - lock.lock(); - try { - if (!checkLinkedAttributes(assetId) && isSimulationStarted(assetId)) { - stopSimulation(assetId); - LOG.info("Stopped storage simulation for asset id: " + assetId); - } - } finally { - lock.unlock(); - } + // Power attribute is updated only by this protocol not by clients + if (event.getName().equals(POWER.getName())) { + return; } - @Override - protected void doLinkedAttributeWrite(StorageSimulatorAgentLink agentLink, AttributeEvent event, Object processedValue) { + updateLinkedAttribute(event.getRef(), processedValue); + } - // Power attribute is updated only by this protocol not by clients - if (event.getName().equals(POWER.getName())) { - return; - } + protected void updateStorageAsset(String assetId) { - updateLinkedAttribute(event.getRef(), processedValue); + Asset asset = assetService.findAsset(assetId); + ElectricityStorageAsset storageAsset = + asset instanceof ElectricityStorageAsset ? (ElectricityStorageAsset) asset : null; + if (storageAsset == null) { + LOG.finest("Storage asset not set so skipping update"); + return; } - protected void updateStorageAsset(String assetId) { - - Asset asset = assetService.findAsset(assetId); - ElectricityStorageAsset storageAsset = asset instanceof ElectricityStorageAsset ? (ElectricityStorageAsset)asset : null; - if (storageAsset == null) { - LOG.finest("Storage asset not set so skipping update"); - return; - } - - Instant now = Instant.now(); - Instant previousTimestamp; - try { - lock.lockInterruptibly(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - try { - previousTimestamp = lastUpdateMap.put(assetId, now); - } finally { - lock.unlock(); - } - - double setpoint = storageAsset.getPowerSetpoint().orElse(0d); - double capacity = storageAsset.getEnergyCapacity().orElse(0d); - double power = storageAsset.getPower().orElse(0d); - double level = storageAsset.getEnergyLevel().orElse(0d); - int minPercentage = storageAsset.getEnergyLevelPercentageMin().orElse(0); - int maxPercentage = storageAsset.getEnergyLevelPercentageMax().orElse(100); - capacity = Math.max(0d, capacity); - level = Math.max(0d, Math.min(capacity, level)); - double maxLevel = (((double) maxPercentage) / 100d) * capacity; - double minLevel = (((double) minPercentage) / 100d) * capacity; - - if (capacity <= 0d) { - LOG.info("Storage asset capacity is 0 so not usable: " + assetId); - level = 0d; - setpoint = 0d; - } - - if (capacity > 0 && power != 0d && previousTimestamp != null) { - - // Calculate energy delta since last execution - Duration duration = Duration.between(previousTimestamp, now); - long seconds = duration.getSeconds(); - - if (seconds > 0) { - - double deltaHours = seconds / 3600d; - double efficiency = power > 0 ? ((double) storageAsset.getEfficiencyImport().orElse(100)) / 100d : (1d / (((double) storageAsset.getEfficiencyExport().orElse(100)) / 100d)); // Export efficiency < 1 means more energy is consumed to produce requested power - double energyDelta = power * deltaHours * efficiency; - double newLevel = Math.max(0d, Math.min(capacity, level + energyDelta)); - energyDelta = newLevel - level; - level = newLevel; - - if (energyDelta > 0) { - updateLinkedAttribute(new AttributeRef(storageAsset.getId(), ElectricityStorageAsset.ENERGY_IMPORT_TOTAL.getName()), storageAsset.getEnergyImportTotal().orElse(0d) + energyDelta); - } else { - updateLinkedAttribute(new AttributeRef(storageAsset.getId(), ElectricityStorageAsset.ENERGY_EXPORT_TOTAL.getName()), storageAsset.getEnergyExportTotal().orElse(0d) - energyDelta); - } - } - } - - power = (setpoint < 0 && level <= minLevel) || (setpoint > 0 && level >= maxLevel) ? 0d : setpoint; - - if (power > 0d && !storageAsset.isSupportsImport().orElse(false)) { - LOG.fine("Setpoint is requesting power import but asset does not support it: " + storageAsset); - power = 0d; - } else if (power < 0d && !storageAsset.isSupportsExport().orElse(false)) { - LOG.fine("Setpoint is requesting power export but asset does not support it: " + storageAsset); - power = 0d; - } - - updateLinkedAttribute(new AttributeRef(assetId, POWER.getName()), power); - updateLinkedAttribute(new AttributeRef(assetId, ENERGY_LEVEL.getName()), level); - updateLinkedAttribute(new AttributeRef(assetId, ENERGY_LEVEL_PERCENTAGE.getName()), capacity <= 0d ? 0 : (int) ((level / capacity) * 100)); + Instant now = Instant.now(); + Instant previousTimestamp; + try { + lock.lockInterruptibly(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; } - - protected ScheduledFuture scheduleUpdate(String assetId) { - return scheduledExecutorService.scheduleAtFixedRate(() -> { - try { - updateStorageAsset(assetId); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Exception in " + getProtocolName(), e); - setConnectionStatus(ConnectionStatus.ERROR); - } - - }, 0, 1, TimeUnit.MINUTES); + try { + previousTimestamp = lastUpdateMap.put(assetId, now); + } finally { + lock.unlock(); } - protected boolean checkLinkedAttributes(String assetId) { - return getLinkedAttributes().containsKey(new AttributeRef(assetId, POWER_SETPOINT.getName())) && - getLinkedAttributes().containsKey(new AttributeRef(assetId, POWER.getName())) && - getLinkedAttributes().containsKey(new AttributeRef(assetId, ENERGY_LEVEL.getName()))&& - getLinkedAttributes().containsKey(new AttributeRef(assetId, ENERGY_LEVEL_PERCENTAGE.getName())); + double setpoint = storageAsset.getPowerSetpoint().orElse(0d); + double capacity = storageAsset.getEnergyCapacity().orElse(0d); + double power = storageAsset.getPower().orElse(0d); + double level = storageAsset.getEnergyLevel().orElse(0d); + int minPercentage = storageAsset.getEnergyLevelPercentageMin().orElse(0); + int maxPercentage = storageAsset.getEnergyLevelPercentageMax().orElse(100); + capacity = Math.max(0d, capacity); + level = Math.max(0d, Math.min(capacity, level)); + double maxLevel = (((double) maxPercentage) / 100d) * capacity; + double minLevel = (((double) minPercentage) / 100d) * capacity; + + if (capacity <= 0d) { + LOG.info("Storage asset capacity is 0 so not usable: " + assetId); + level = 0d; + setpoint = 0d; } - protected boolean isSimulationStarted(String assetId) { - return simulationMap.containsKey(assetId); + if (capacity > 0 && power != 0d && previousTimestamp != null) { + + // Calculate energy delta since last execution + Duration duration = Duration.between(previousTimestamp, now); + long seconds = duration.getSeconds(); + + if (seconds > 0) { + + double deltaHours = seconds / 3600d; + double efficiency = + power > 0 + ? ((double) storageAsset.getEfficiencyImport().orElse(100)) / 100d + : (1d + / (((double) storageAsset.getEfficiencyExport().orElse(100)) + / 100d)); // Export efficiency < 1 means more energy is consumed to produce + // requested power + double energyDelta = power * deltaHours * efficiency; + double newLevel = Math.max(0d, Math.min(capacity, level + energyDelta)); + energyDelta = newLevel - level; + level = newLevel; + + if (energyDelta > 0) { + updateLinkedAttribute( + new AttributeRef( + storageAsset.getId(), ElectricityStorageAsset.ENERGY_IMPORT_TOTAL.getName()), + storageAsset.getEnergyImportTotal().orElse(0d) + energyDelta); + } else { + updateLinkedAttribute( + new AttributeRef( + storageAsset.getId(), ElectricityStorageAsset.ENERGY_EXPORT_TOTAL.getName()), + storageAsset.getEnergyExportTotal().orElse(0d) - energyDelta); + } + } } - protected void startSimulation(String assetId) { - LOG.info("Started storage simulation for asset id: " + assetId); - simulationMap.put(assetId, scheduleUpdate(assetId)); + power = + (setpoint < 0 && level <= minLevel) || (setpoint > 0 && level >= maxLevel) ? 0d : setpoint; + + if (power > 0d && !storageAsset.isSupportsImport().orElse(false)) { + LOG.fine( + "Setpoint is requesting power import but asset does not support it: " + storageAsset); + power = 0d; + } else if (power < 0d && !storageAsset.isSupportsExport().orElse(false)) { + LOG.fine( + "Setpoint is requesting power export but asset does not support it: " + storageAsset); + power = 0d; } - protected void stopSimulation(String assetId) { - if (isSimulationStarted(assetId)) { - lastUpdateMap.remove(assetId); - ScheduledFuture scheduledFuture = simulationMap.remove(assetId); - if (scheduledFuture != null) { - scheduledFuture.cancel(true); - } - } + updateLinkedAttribute(new AttributeRef(assetId, POWER.getName()), power); + updateLinkedAttribute(new AttributeRef(assetId, ENERGY_LEVEL.getName()), level); + updateLinkedAttribute( + new AttributeRef(assetId, ENERGY_LEVEL_PERCENTAGE.getName()), + capacity <= 0d ? 0 : (int) ((level / capacity) * 100)); + } + + protected ScheduledFuture scheduleUpdate(String assetId) { + return scheduledExecutorService.scheduleAtFixedRate( + () -> { + try { + updateStorageAsset(assetId); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Exception in " + getProtocolName(), e); + setConnectionStatus(ConnectionStatus.ERROR); + } + }, + 0, + 1, + TimeUnit.MINUTES); + } + + protected boolean checkLinkedAttributes(String assetId) { + return getLinkedAttributes().containsKey(new AttributeRef(assetId, POWER_SETPOINT.getName())) + && getLinkedAttributes().containsKey(new AttributeRef(assetId, POWER.getName())) + && getLinkedAttributes().containsKey(new AttributeRef(assetId, ENERGY_LEVEL.getName())) + && getLinkedAttributes() + .containsKey(new AttributeRef(assetId, ENERGY_LEVEL_PERCENTAGE.getName())); + } + + protected boolean isSimulationStarted(String assetId) { + return simulationMap.containsKey(assetId); + } + + protected void startSimulation(String assetId) { + LOG.info("Started storage simulation for asset id: " + assetId); + simulationMap.put(assetId, scheduleUpdate(assetId)); + } + + protected void stopSimulation(String assetId) { + if (isSimulationStarted(assetId)) { + lastUpdateMap.remove(assetId); + ScheduledFuture scheduledFuture = simulationMap.remove(assetId); + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + } } + } } diff --git a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgent.java b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgent.java index b763253..2977800 100644 --- a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgent.java +++ b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgent.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,55 +12,59 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.entsoe.agent.protocol; -import jakarta.persistence.Entity; +import java.util.Optional; + import org.openremote.model.asset.agent.Agent; import org.openremote.model.asset.agent.AgentDescriptor; import org.openremote.model.value.AttributeDescriptor; import org.openremote.model.value.ValueType; -import java.util.Optional; +import jakarta.persistence.Entity; @Entity public class EntsoeAgent extends Agent { - public static final AgentDescriptor DESCRIPTOR = new AgentDescriptor<>( - EntsoeAgent.class, EntsoeProtocol.class, EntsoeAgentLink.class); + public static final AgentDescriptor DESCRIPTOR = + new AgentDescriptor<>(EntsoeAgent.class, EntsoeProtocol.class, EntsoeAgentLink.class); - public static final AttributeDescriptor SECURITY_TOKEN = new AttributeDescriptor<>("securityToken", ValueType.TEXT); + public static final AttributeDescriptor SECURITY_TOKEN = + new AttributeDescriptor<>("securityToken", ValueType.TEXT); - public static final AttributeDescriptor BASE_URL = new AttributeDescriptor<>("baseURL", ValueType.TEXT).withOptional(true); + public static final AttributeDescriptor BASE_URL = + new AttributeDescriptor<>("baseURL", ValueType.TEXT).withOptional(true); - public EntsoeAgent() { - } + public EntsoeAgent() {} - public EntsoeAgent(String name) { - super(name); - } + public EntsoeAgent(String name) { + super(name); + } - @Override - public EntsoeProtocol getProtocolInstance() { - return new EntsoeProtocol(this); - } + @Override + public EntsoeProtocol getProtocolInstance() { + return new EntsoeProtocol(this); + } - public Optional getSecurityToken() { - return getAttributes().getValue(SECURITY_TOKEN); - } + public Optional getSecurityToken() { + return getAttributes().getValue(SECURITY_TOKEN); + } - public EntsoeAgent setSecurityToken(String value) { - getAttributes().getOrCreate(SECURITY_TOKEN).setValue(value); - return this; - } + public EntsoeAgent setSecurityToken(String value) { + getAttributes().getOrCreate(SECURITY_TOKEN).setValue(value); + return this; + } - public Optional getBaseURL() { - return getAttributes().getValue(BASE_URL); - } + public Optional getBaseURL() { + return getAttributes().getValue(BASE_URL); + } - public EntsoeAgent setBaseURL(String value) { - getAttributes().getOrCreate(BASE_URL).setValue(value); - return this; - } + public EntsoeAgent setBaseURL(String value) { + getAttributes().getOrCreate(BASE_URL).setValue(value); + return this; + } } diff --git a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentLink.java b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentLink.java index 32ff175..aef4419 100644 --- a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentLink.java +++ b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentLink.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,36 +12,37 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.entsoe.agent.protocol; import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import org.openremote.model.asset.agent.AgentLink; + import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; -import org.openremote.model.asset.agent.AgentLink; public class EntsoeAgentLink extends AgentLink { - @NotNull - @JsonPropertyDescription("Energy Identification Code of zone to fetch data for") - @Pattern(regexp = "^\\d{2}[A-Z][A-Z0-9-]{12}[A-Z0-9]$") - private String zone; + @NotNull @JsonPropertyDescription("Energy Identification Code of zone to fetch data for") + @Pattern(regexp = "^\\d{2}[A-Z][A-Z0-9-]{12}[A-Z0-9]$") private String zone; - // For Hydrators - public EntsoeAgentLink() { - } + // For Hydrators + public EntsoeAgentLink() {} - public EntsoeAgentLink(String id) { - super(id); - } + public EntsoeAgentLink(String id) { + super(id); + } - public String getZone() { - return zone; - } + public String getZone() { + return zone; + } - public EntsoeAgentLink setZone(String zone) { - this.zone = zone; - return this; - } + public EntsoeAgentLink setZone(String zone) { + this.zone = zone; + return this; + } } diff --git a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentModelProvider.java b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentModelProvider.java index a6d3c7d..9a9f800 100644 --- a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentModelProvider.java +++ b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeAgentModelProvider.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,7 +12,9 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.entsoe.agent.protocol; @@ -23,8 +22,8 @@ public class EntsoeAgentModelProvider implements AssetModelProvider { - @Override - public boolean useAutoScan() { - return true; - } + @Override + public boolean useAutoScan() { + return true; + } } diff --git a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocol.java b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocol.java index 5b41433..4783ba6 100644 --- a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocol.java +++ b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocol.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,27 +12,15 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.entsoe.agent.protocol; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriBuilder; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.openremote.agent.protocol.AbstractProtocol; -import org.openremote.model.Container; -import org.openremote.model.asset.agent.ConnectionStatus; -import org.openremote.model.attribute.Attribute; -import org.openremote.model.attribute.AttributeEvent; -import org.openremote.model.attribute.AttributeRef; -import org.openremote.model.datapoint.ValueDatapoint; -import org.openremote.model.syslog.SyslogCategory; +import static org.openremote.container.web.WebTargetBuilder.createClient; +import static org.openremote.model.syslog.SyslogCategory.PROTOCOL; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamConstants; -import javax.xml.stream.XMLStreamReader; import java.io.StringReader; import java.math.BigDecimal; import java.time.Duration; @@ -56,405 +41,476 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static org.openremote.container.web.WebTargetBuilder.createClient; -import static org.openremote.model.syslog.SyslogCategory.PROTOCOL; - -public class EntsoeProtocol extends AbstractProtocol { - - private static final Logger LOG = SyslogCategory.getLogger(PROTOCOL, EntsoeProtocol.class); - private static final DateTimeFormatter PERIOD_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmm").withZone(ZoneId.of("UTC")); - private static final DateTimeFormatter ENTSOE_DATETIME_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm") - .optionalStart().appendPattern(":ss").optionalEnd() - .appendOffsetId() - .toFormatter(); - public static final String PROTOCOL_DISPLAY_NAME = "ENTSO-E"; - private static final AtomicReference client = new AtomicReference<>(); - private static final JAXBContext PUBLICATION_MARKET_DOCUMENT_CONTEXT = createPublicationMarketDocumentContext(); - private static final ThreadLocal PUBLICATION_UNMARSHALLER = ThreadLocal.withInitial(EntsoeProtocol::createPublicationUnmarshaller); - private static final ThreadLocal XML_INPUT_FACTORY = ThreadLocal.withInitial(EntsoeProtocol::createSecureXmlInputFactory); - - // Initial delay to allow system to populate agent links - private static final int INITIAL_POLLING_DELAY_MILLIS = 3000; // 3 seconds - private static final int DEFAULT_POLLING_MILLIS = 3600000; // 1 hour - - protected ScheduledFuture pollingFuture; - - public EntsoeProtocol(EntsoeAgent agent) { - super(agent); - initClient(); - } - - @Override - protected void doStart(Container container) throws Exception { - setConnectionStatus(ConnectionStatus.CONNECTING); - - if (agent.getSecurityToken().isEmpty()) { - setConnectionStatus(ConnectionStatus.ERROR); - LOG.warning("Security token is not set"); - return; - } - - if (!healthCheck()) { - setConnectionStatus(ConnectionStatus.ERROR); - LOG.warning("Could not reach ENTSO-E API, either API is unavailable or security token is invalid"); - return; - } +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamReader; - restartPollingWithInitialDelay(); +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.openremote.agent.protocol.AbstractProtocol; +import org.openremote.model.Container; +import org.openremote.model.asset.agent.ConnectionStatus; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeEvent; +import org.openremote.model.attribute.AttributeRef; +import org.openremote.model.datapoint.ValueDatapoint; +import org.openremote.model.syslog.SyslogCategory; - setConnectionStatus(ConnectionStatus.CONNECTED); - } +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; - @Override - protected void doStop(Container container) throws Exception { - // Cancel the polling task - if (pollingFuture != null) { - pollingFuture.cancel(true); - } - } +public class EntsoeProtocol extends AbstractProtocol { - @Override - protected void doLinkAttribute(String assetId, Attribute attribute, EntsoeAgentLink agentLink) throws RuntimeException { - if (!attribute.getType().getType().isAssignableFrom(BigDecimal.class) && !attribute.getType().getType().isAssignableFrom(Double.class)) { - LOG.warning("Linked attribute " + attribute.getName() + " of asset " + assetId + " not of supported type. Predicted data points will still be generated but inconsistent behaviour could occur."); - } - restartPollingWithInitialDelay(); + private static final Logger LOG = SyslogCategory.getLogger(PROTOCOL, EntsoeProtocol.class); + private static final DateTimeFormatter PERIOD_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMddHHmm").withZone(ZoneId.of("UTC")); + private static final DateTimeFormatter ENTSOE_DATETIME_FORMATTER = + new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm") + .optionalStart() + .appendPattern(":ss") + .optionalEnd() + .appendOffsetId() + .toFormatter(); + public static final String PROTOCOL_DISPLAY_NAME = "ENTSO-E"; + private static final AtomicReference client = new AtomicReference<>(); + private static final JAXBContext PUBLICATION_MARKET_DOCUMENT_CONTEXT = + createPublicationMarketDocumentContext(); + private static final ThreadLocal PUBLICATION_UNMARSHALLER = + ThreadLocal.withInitial(EntsoeProtocol::createPublicationUnmarshaller); + private static final ThreadLocal XML_INPUT_FACTORY = + ThreadLocal.withInitial(EntsoeProtocol::createSecureXmlInputFactory); + + // Initial delay to allow system to populate agent links + private static final int INITIAL_POLLING_DELAY_MILLIS = 3000; // 3 seconds + private static final int DEFAULT_POLLING_MILLIS = 3600000; // 1 hour + + protected ScheduledFuture pollingFuture; + + public EntsoeProtocol(EntsoeAgent agent) { + super(agent); + initClient(); + } + + @Override + protected void doStart(Container container) throws Exception { + setConnectionStatus(ConnectionStatus.CONNECTING); + + if (agent.getSecurityToken().isEmpty()) { + setConnectionStatus(ConnectionStatus.ERROR); + LOG.warning("Security token is not set"); + return; } - @Override - protected void doUnlinkAttribute(String assetId, Attribute attribute, EntsoeAgentLink agentLink) { - // Do nothing, attributes that have been unlinked will not be processed anymore when we next trigger + if (!healthCheck()) { + setConnectionStatus(ConnectionStatus.ERROR); + LOG.warning( + "Could not reach ENTSO-E API, either API is unavailable or security token is invalid"); + return; } - protected synchronized void restartPollingWithInitialDelay() { - if (scheduledExecutorService == null) { - return; - } + restartPollingWithInitialDelay(); - if (pollingFuture != null) { - pollingFuture.cancel(false); - } + setConnectionStatus(ConnectionStatus.CONNECTED); + } - int pollingMillis = agent.getPollingMillis().orElse(DEFAULT_POLLING_MILLIS); - pollingFuture = scheduledExecutorService.scheduleAtFixedRate( - this::runScheduledUpdate, - INITIAL_POLLING_DELAY_MILLIS, - pollingMillis, - TimeUnit.MILLISECONDS - ); + @Override + protected void doStop(Container container) throws Exception { + // Cancel the polling task + if (pollingFuture != null) { + pollingFuture.cancel(true); } - - protected void runScheduledUpdate() { - try { - updateAllLinkedAttributes(); - } catch (RuntimeException e) { - LOG.log(Level.WARNING, e, () -> "Scheduled ENTSO-E polling failed; keeping schedule active"); - } + } + + @Override + protected void doLinkAttribute(String assetId, Attribute attribute, EntsoeAgentLink agentLink) + throws RuntimeException { + if (!attribute.getType().getType().isAssignableFrom(BigDecimal.class) + && !attribute.getType().getType().isAssignableFrom(Double.class)) { + LOG.warning( + "Linked attribute " + + attribute.getName() + + " of asset " + + assetId + + " not of supported type. Predicted data points will still be generated but inconsistent behaviour could occur."); } - - @Override - protected void doLinkedAttributeWrite(EntsoeAgentLink agentLink, AttributeEvent event, Object processedValue) { - // If some external source wants to write the current value of the attribute, we're OK with that - // and relay the event with AgentService as source so it goes through. - assetService.sendAttributeEvent(event); + restartPollingWithInitialDelay(); + } + + @Override + protected void doUnlinkAttribute( + String assetId, Attribute attribute, EntsoeAgentLink agentLink) { + // Do nothing, attributes that have been unlinked will not be processed anymore when we next + // trigger + } + + protected synchronized void restartPollingWithInitialDelay() { + if (scheduledExecutorService == null) { + return; } - @Override - public String getProtocolName() { - return PROTOCOL_DISPLAY_NAME; + if (pollingFuture != null) { + pollingFuture.cancel(false); } - @Override - public String getProtocolInstanceUri() { - return "https://transparency.entsoe.eu/"; + int pollingMillis = agent.getPollingMillis().orElse(DEFAULT_POLLING_MILLIS); + pollingFuture = + scheduledExecutorService.scheduleAtFixedRate( + this::runScheduledUpdate, + INITIAL_POLLING_DELAY_MILLIS, + pollingMillis, + TimeUnit.MILLISECONDS); + } + + protected void runScheduledUpdate() { + try { + updateAllLinkedAttributes(); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, e, () -> "Scheduled ENTSO-E polling failed; keeping schedule active"); + } + } + + @Override + protected void doLinkedAttributeWrite( + EntsoeAgentLink agentLink, AttributeEvent event, Object processedValue) { + // If some external source wants to write the current value of the attribute, we're OK with that + // and relay the event with AgentService as source so it goes through. + assetService.sendAttributeEvent(event); + } + + @Override + public String getProtocolName() { + return PROTOCOL_DISPLAY_NAME; + } + + @Override + public String getProtocolInstanceUri() { + return "https://transparency.entsoe.eu/"; + } + + protected void updateAllLinkedAttributes() { + LOG.fine("Updating all linked attributes with pricing information from ENTSO-E"); + + if (getLinkedAttributes().isEmpty()) { + LOG.fine("No linked attributes found, skipping pricing data update"); + return; } - protected void updateAllLinkedAttributes() { - LOG.fine("Updating all linked attributes with pricing information from ENTSO-E"); - - if (getLinkedAttributes().isEmpty()) { - LOG.fine("No linked attributes found, skipping pricing data update"); - return; - } - - Map>> attributesByZone = collectLinkedAttributesByZone(); + Map>> attributesByZone = + collectLinkedAttributesByZone(); - attributesByZone.forEach((zone, linkedAttributesForZone) -> { - PublicationMarketDocument document = fetchPricingInformation(buildApiUrl(zone)); - applyPricingInformation(zone, document, linkedAttributesForZone); + attributesByZone.forEach( + (zone, linkedAttributesForZone) -> { + PublicationMarketDocument document = fetchPricingInformation(buildApiUrl(zone)); + applyPricingInformation(zone, document, linkedAttributesForZone); }); + } + + protected Map>> collectLinkedAttributesByZone() { + Map>> attributesByZone = new LinkedHashMap<>(); + + getLinkedAttributes() + .forEach( + (attributeRef, attribute) -> { + EntsoeAgentLink agentLink = agent.getAgentLink(attribute); + attributesByZone + .computeIfAbsent(agentLink.getZone(), ignored -> new ArrayList<>()) + .add(Map.entry(attributeRef, attribute)); + }); + + return attributesByZone; + } + + protected void applyPricingInformation( + String zone, + PublicationMarketDocument document, + List> linkedAttributesForZone) { + if (document == null) { + linkedAttributesForZone.forEach( + entry -> + LOG.warning( + () -> + "No ENTSO-E publication document returned for attribute: " + + entry.getKey() + + " in zone: " + + zone)); + return; } - protected Map>> collectLinkedAttributesByZone() { - Map>> attributesByZone = new LinkedHashMap<>(); + List> predictedDatapoints = buildPredictedDatapoints(document); + if (predictedDatapoints.isEmpty()) { + linkedAttributesForZone.forEach( + entry -> + LOG.warning( + () -> + "No datapoints built from ENTSO-E publication document for attribute: " + + entry.getKey() + + " in zone: " + + zone)); + return; + } - getLinkedAttributes().forEach((attributeRef, attribute) -> { - EntsoeAgentLink agentLink = agent.getAgentLink(attribute); - attributesByZone.computeIfAbsent(agentLink.getZone(), ignored -> new ArrayList<>()) - .add(Map.entry(attributeRef, attribute)); + linkedAttributesForZone.forEach( + entry -> { + AttributeRef attributeRef = entry.getKey(); + Attribute attribute = entry.getValue(); + LOG.fine("Updating pricing information data for attribute " + attribute.getName()); + predictedDatapointService.updateValues( + attributeRef.getId(), attributeRef.getName(), predictedDatapoints); }); - - return attributesByZone; + } + + protected String buildApiUrl(String zone) { + String securityToken = + agent + .getSecurityToken() + .orElseThrow(() -> new IllegalStateException("Security token is not set")); + String baseUrl = agent.getBaseURL().orElse("https://web-api.tp.entsoe.eu/api"); + Instant start = timerService.getNow(); + Instant end = start.plus(1, ChronoUnit.DAYS); + + return UriBuilder.fromUri(baseUrl) + .queryParam("documentType", "A44") + .queryParam("contract_MarketAgreement.type", "A01") + .queryParam("periodStart", PERIOD_FORMATTER.format(start)) + .queryParam("periodEnd", PERIOD_FORMATTER.format(end)) + .queryParam("in_Domain", zone) + .queryParam("out_Domain", zone) + .queryParam("securityToken", securityToken) + .build() + .toString(); + } + + /** + * Perform a health check by sending a request to the ENTSO-E API + * + * @return true if the health check is successful, false otherwise + */ + protected boolean healthCheck() { + String apiUrl = buildApiUrl("10YBE----------2"); + try (Response response = + client.get().target(apiUrl).request(jakarta.ws.rs.core.MediaType.APPLICATION_XML).get()) { + if (response.getStatus() != 200) { + LOG.warning("Health check failed with status: " + response.getStatus()); + return false; + } else { + return true; + } + } catch (Exception e) { + LOG.log(Level.WARNING, e, () -> "Failed to perform health check"); + return false; } - - protected void applyPricingInformation(String zone, PublicationMarketDocument document, List> linkedAttributesForZone) { - if (document == null) { - linkedAttributesForZone.forEach(entry -> - LOG.warning(() -> "No ENTSO-E publication document returned for attribute: " + entry.getKey() + " in zone: " + zone)); - return; + } + + /** + * Fetch the pricing data from the ENTSO-E API for the given API URL + * + * @param apiUrl the API URL + * @return the PublicationMarketDocument from the API + */ + protected PublicationMarketDocument fetchPricingInformation(String apiUrl) { + try (Response response = + client.get().target(apiUrl).request(javax.ws.rs.core.MediaType.APPLICATION_XML).get()) { + if (response.getStatus() == 200) { + String responseXml = response.readEntity(String.class); + EntsoeXmlMeta xmlMeta = parseEntsoeXmlMeta(responseXml); + + if ("Publication_MarketDocument".equals(xmlMeta.rootElement)) { + return unmarshalPublicationMarketDocument(responseXml); } - List> predictedDatapoints = buildPredictedDatapoints(document); - if (predictedDatapoints.isEmpty()) { - linkedAttributesForZone.forEach(entry -> - LOG.warning(() -> "No datapoints built from ENTSO-E publication document for attribute: " + entry.getKey() + " in zone: " + zone)); - return; + if ("Acknowledgement_MarketDocument".equals(xmlMeta.rootElement)) { + String reason = xmlMeta.reasonText != null ? xmlMeta.reasonText : "no reason provided"; + LOG.info("No ENTSO-E pricing data available: " + reason); + return null; } - linkedAttributesForZone.forEach(entry -> { - AttributeRef attributeRef = entry.getKey(); - Attribute attribute = entry.getValue(); - LOG.fine("Updating pricing information data for attribute " + attribute.getName()); - predictedDatapointService.updateValues(attributeRef.getId(), attributeRef.getName(), predictedDatapoints); - }); + LOG.warning("Unsupported ENTSO-E response XML root element: " + xmlMeta.rootElement); + return null; + } else if (response.getStatus() == 401) { + LOG.warning( + "API request was unauthorized, either the security token is invalid or does not provide access to the API"); + return null; + } else { + LOG.warning("API request failed with status: " + response.getStatus()); + return null; + } + } catch (Exception e) { + LOG.log(Level.WARNING, e, () -> "Failed to fetch pricing data"); + return null; } - - protected String buildApiUrl(String zone) { - String securityToken = agent.getSecurityToken().orElseThrow(() -> new IllegalStateException("Security token is not set")); - String baseUrl = agent.getBaseURL().orElse("https://web-api.tp.entsoe.eu/api"); - Instant start = timerService.getNow(); - Instant end = start.plus(1, ChronoUnit.DAYS); - - return UriBuilder.fromUri(baseUrl) - .queryParam("documentType", "A44") - .queryParam("contract_MarketAgreement.type", "A01") - .queryParam("periodStart", PERIOD_FORMATTER.format(start)) - .queryParam("periodEnd", PERIOD_FORMATTER.format(end)) - .queryParam("in_Domain", zone) - .queryParam("out_Domain", zone) - .queryParam("securityToken", securityToken) - .build() - .toString(); + } + + protected static XMLInputFactory createSecureXmlInputFactory() { + try { + XMLInputFactory factory = XMLInputFactory.newFactory(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + factory.setXMLResolver((publicId, systemId, baseUri, namespace) -> null); + return factory; + } catch (Exception e) { + throw new IllegalStateException("Failed to initialise secure XMLInputFactory", e); } - - /** - * Perform a health check by sending a request to the ENTSO-E API - * - * @return true if the health check is successful, false otherwise - */ - protected boolean healthCheck() { - String apiUrl = buildApiUrl("10YBE----------2"); - try (Response response = client.get().target(apiUrl).request(jakarta.ws.rs.core.MediaType.APPLICATION_XML).get()) { - if (response.getStatus() != 200) { - LOG.warning("Health check failed with status: " + response.getStatus()); - return false; - } else { - return true; - } - } catch (Exception e) { - LOG.log(Level.WARNING, e, () -> "Failed to perform health check"); - return false; - } + } + + protected static JAXBContext createPublicationMarketDocumentContext() { + try { + return JAXBContext.newInstance(PublicationMarketDocument.class); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to initialise PublicationMarketDocument JAXB context", e); } - - /** - * Fetch the pricing data from the ENTSO-E API for the given API URL - * - * @param apiUrl the API URL - * @return the PublicationMarketDocument from the API - */ - protected PublicationMarketDocument fetchPricingInformation(String apiUrl) { - try (Response response = client.get().target(apiUrl).request(javax.ws.rs.core.MediaType.APPLICATION_XML).get()) { - if (response.getStatus() == 200) { - String responseXml = response.readEntity(String.class); - EntsoeXmlMeta xmlMeta = parseEntsoeXmlMeta(responseXml); - - if ("Publication_MarketDocument".equals(xmlMeta.rootElement)) { - return unmarshalPublicationMarketDocument(responseXml); - } - - if ("Acknowledgement_MarketDocument".equals(xmlMeta.rootElement)) { - String reason = xmlMeta.reasonText != null ? xmlMeta.reasonText : "no reason provided"; - LOG.info("No ENTSO-E pricing data available: " + reason); - return null; - } - - LOG.warning("Unsupported ENTSO-E response XML root element: " + xmlMeta.rootElement); - return null; - } else if (response.getStatus() == 401) { - LOG.warning("API request was unauthorized, either the security token is invalid or does not provide access to the API"); - return null; - } else { - LOG.warning("API request failed with status: " + response.getStatus()); - return null; - } - } catch (Exception e) { - LOG.log(Level.WARNING, e, () -> "Failed to fetch pricing data"); - return null; - } + } + + protected static Unmarshaller createPublicationUnmarshaller() { + try { + return PUBLICATION_MARKET_DOCUMENT_CONTEXT.createUnmarshaller(); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to initialise PublicationMarketDocument unmarshaller", e); } - - protected static XMLInputFactory createSecureXmlInputFactory() { - try { - XMLInputFactory factory = XMLInputFactory.newFactory(); - factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); - factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); - factory.setXMLResolver((publicId, systemId, baseUri, namespace) -> null); - return factory; - } catch (Exception e) { - throw new IllegalStateException("Failed to initialise secure XMLInputFactory", e); - } - } - - protected static JAXBContext createPublicationMarketDocumentContext() { - try { - return JAXBContext.newInstance(PublicationMarketDocument.class); - } catch (Exception e) { - throw new IllegalStateException("Failed to initialise PublicationMarketDocument JAXB context", e); - } + } + + protected PublicationMarketDocument unmarshalPublicationMarketDocument(String xml) + throws Exception { + XMLStreamReader reader = XML_INPUT_FACTORY.get().createXMLStreamReader(new StringReader(xml)); + try { + return (PublicationMarketDocument) PUBLICATION_UNMARSHALLER.get().unmarshal(reader); + } finally { + reader.close(); } - - protected static Unmarshaller createPublicationUnmarshaller() { - try { - return PUBLICATION_MARKET_DOCUMENT_CONTEXT.createUnmarshaller(); - } catch (Exception e) { - throw new IllegalStateException("Failed to initialise PublicationMarketDocument unmarshaller", e); + } + + protected EntsoeXmlMeta parseEntsoeXmlMeta(String xml) throws Exception { + XMLStreamReader reader = XML_INPUT_FACTORY.get().createXMLStreamReader(new StringReader(xml)); + String rootElement = null; + StringBuilder reasonTextBuilder = new StringBuilder(); + boolean inReason = false; + boolean inReasonText = false; + + try { + while (reader.hasNext()) { + int event = reader.next(); + + if (event == XMLStreamConstants.START_ELEMENT) { + String localName = reader.getLocalName(); + if (rootElement == null) { + rootElement = localName; + } + if ("Reason".equals(localName)) { + inReason = true; + } else if (inReason && "text".equals(localName)) { + inReasonText = true; + } + } else if (event == XMLStreamConstants.CHARACTERS && inReasonText) { + String text = reader.getText(); + if (text != null) { + reasonTextBuilder.append(text); + } + } else if (event == XMLStreamConstants.END_ELEMENT) { + String localName = reader.getLocalName(); + if ("text".equals(localName)) { + inReasonText = false; + } else if ("Reason".equals(localName)) { + inReason = false; + } } + } + } finally { + reader.close(); } - protected PublicationMarketDocument unmarshalPublicationMarketDocument(String xml) throws Exception { - XMLStreamReader reader = XML_INPUT_FACTORY.get().createXMLStreamReader(new StringReader(xml)); - try { - return (PublicationMarketDocument) PUBLICATION_UNMARSHALLER.get().unmarshal(reader); - } finally { - reader.close(); - } + String reasonText = reasonTextBuilder.toString().replaceAll("\\s+", " ").trim(); + if (reasonText.isEmpty()) { + reasonText = null; } - protected EntsoeXmlMeta parseEntsoeXmlMeta(String xml) throws Exception { - XMLStreamReader reader = XML_INPUT_FACTORY.get().createXMLStreamReader(new StringReader(xml)); - String rootElement = null; - StringBuilder reasonTextBuilder = new StringBuilder(); - boolean inReason = false; - boolean inReasonText = false; + return new EntsoeXmlMeta(rootElement, reasonText); + } - try { - while (reader.hasNext()) { - int event = reader.next(); - - if (event == XMLStreamConstants.START_ELEMENT) { - String localName = reader.getLocalName(); - if (rootElement == null) { - rootElement = localName; - } - if ("Reason".equals(localName)) { - inReason = true; - } else if (inReason && "text".equals(localName)) { - inReasonText = true; - } - } else if (event == XMLStreamConstants.CHARACTERS && inReasonText) { - String text = reader.getText(); - if (text != null) { - reasonTextBuilder.append(text); - } - } else if (event == XMLStreamConstants.END_ELEMENT) { - String localName = reader.getLocalName(); - if ("text".equals(localName)) { - inReasonText = false; - } else if ("Reason".equals(localName)) { - inReason = false; - } - } - } - } finally { - reader.close(); - } + protected static class EntsoeXmlMeta { + protected final String rootElement; + protected final String reasonText; - String reasonText = reasonTextBuilder.toString().replaceAll("\\s+", " ").trim(); - if (reasonText.isEmpty()) { - reasonText = null; - } + protected EntsoeXmlMeta(String rootElement, String reasonText) { + this.rootElement = rootElement; + this.reasonText = reasonText; + } + } - return new EntsoeXmlMeta(rootElement, reasonText); + protected List> buildPredictedDatapoints(PublicationMarketDocument document) { + List> values = new ArrayList<>(); + long nowMillis = timerService.getCurrentTimeMillis(); + if (document.getTimeSeries() == null || document.getTimeSeries().isEmpty()) { + return values; } - protected static class EntsoeXmlMeta { - protected final String rootElement; - protected final String reasonText; + for (PublicationMarketDocument.TimeSeries timeSeries : document.getTimeSeries()) { + if (timeSeries.getPeriods() == null || timeSeries.getPeriods().isEmpty()) { + continue; + } - protected EntsoeXmlMeta(String rootElement, String reasonText) { - this.rootElement = rootElement; - this.reasonText = reasonText; + for (PublicationMarketDocument.Period period : timeSeries.getPeriods()) { + if (period == null || period.getPoints() == null || period.getPoints().isEmpty()) { + continue; } - } - protected List> buildPredictedDatapoints(PublicationMarketDocument document) { - List> values = new ArrayList<>(); - long nowMillis = timerService.getCurrentTimeMillis(); - if (document.getTimeSeries() == null || document.getTimeSeries().isEmpty()) { - return values; + PublicationMarketDocument.PeriodTimeInterval timeInterval = + period.getTimeInterval() != null + ? period.getTimeInterval() + : document.getPeriodTimeInterval(); + if (timeInterval == null + || timeInterval.getStart() == null + || period.getResolution() == null) { + continue; } - for (PublicationMarketDocument.TimeSeries timeSeries : document.getTimeSeries()) { - if (timeSeries.getPeriods() == null || timeSeries.getPeriods().isEmpty()) { - continue; - } - - for (PublicationMarketDocument.Period period : timeSeries.getPeriods()) { - if (period == null || period.getPoints() == null || period.getPoints().isEmpty()) { - continue; - } - - PublicationMarketDocument.PeriodTimeInterval timeInterval = period.getTimeInterval() != null - ? period.getTimeInterval() - : document.getPeriodTimeInterval(); - if (timeInterval == null || timeInterval.getStart() == null || period.getResolution() == null) { - continue; - } - - final Instant start; - final Duration resolution; - try { - start = parseEntsoeInstant(timeInterval.getStart()); - resolution = Duration.parse(period.getResolution()); - } catch (Exception e) { - LOG.log(Level.WARNING, e, () -> "Could not parse ENTSO-E timeseries time data"); - continue; - } - - period.getPoints().stream() - .filter(point -> point.getPosition() != null && point.getPosition() > 0 && point.getPriceAmount() != null) - .sorted(Comparator.comparingInt(PublicationMarketDocument.Point::getPosition)) - .forEach(point -> { - long timestamp = start - .plus(resolution.multipliedBy(point.getPosition() - 1L)) - .toEpochMilli(); - if (timestamp >= nowMillis) { - values.add(new ValueDatapoint<>(timestamp, point.getPriceAmount())); - } - }); - } + final Instant start; + final Duration resolution; + try { + start = parseEntsoeInstant(timeInterval.getStart()); + resolution = Duration.parse(period.getResolution()); + } catch (Exception e) { + LOG.log(Level.WARNING, e, () -> "Could not parse ENTSO-E timeseries time data"); + continue; } - values.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - return values; + period.getPoints().stream() + .filter( + point -> + point.getPosition() != null + && point.getPosition() > 0 + && point.getPriceAmount() != null) + .sorted(Comparator.comparingInt(PublicationMarketDocument.Point::getPosition)) + .forEach( + point -> { + long timestamp = + start.plus(resolution.multipliedBy(point.getPosition() - 1L)).toEpochMilli(); + if (timestamp >= nowMillis) { + values.add(new ValueDatapoint<>(timestamp, point.getPriceAmount())); + } + }); + } } - protected Instant parseEntsoeInstant(String value) { - try { - return Instant.parse(value); - } catch (Exception ignored) { - return OffsetDateTime.parse(value, ENTSOE_DATETIME_FORMATTER).toInstant(); - } - } + values.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + return values; + } - protected static void initClient() { - synchronized (client) { - if (client.get() == null) { - client.set(createClient(org.openremote.container.Container.SCHEDULED_EXECUTOR)); - } - } + protected Instant parseEntsoeInstant(String value) { + try { + return Instant.parse(value); + } catch (Exception ignored) { + return OffsetDateTime.parse(value, ENTSOE_DATETIME_FORMATTER).toInstant(); } + } + protected static void initClient() { + synchronized (client) { + if (client.get() == null) { + client.set(createClient(org.openremote.container.Container.SCHEDULED_EXECUTOR)); + } + } + } } diff --git a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/PublicationMarketDocument.java b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/PublicationMarketDocument.java index e555427..f2042c8 100644 --- a/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/PublicationMarketDocument.java +++ b/entsoe/src/main/java/org/openremote/extension/entsoe/agent/protocol/PublicationMarketDocument.java @@ -1,9 +1,6 @@ /* * Copyright 2026, OpenRemote Inc. * - * See the CONTRIBUTORS.txt file in the distribution for a - * full listing of individual contributors. - * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -15,82 +12,108 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later */ package org.openremote.extension.entsoe.agent.protocol; +import java.math.BigDecimal; +import java.util.List; + import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlRootElement; -import java.math.BigDecimal; -import java.util.List; - - @XmlRootElement(name = "Publication_MarketDocument", namespace = PublicationMarketDocument.NS) @XmlAccessorType(XmlAccessType.FIELD) public class PublicationMarketDocument { - public static final String NS = "urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3"; + public static final String NS = "urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3"; - @XmlElement(name = "period.timeInterval", namespace = NS) - private PeriodTimeInterval periodTimeInterval; + @XmlElement(name = "period.timeInterval", namespace = NS) + private PeriodTimeInterval periodTimeInterval; - @XmlElement(name = "TimeSeries", namespace = NS) - private List timeSeries; + @XmlElement(name = "TimeSeries", namespace = NS) + private List timeSeries; - @XmlAccessorType(XmlAccessType.FIELD) - public static class PeriodTimeInterval { - @XmlElement(name = "start", namespace = NS) - private String start; + @XmlAccessorType(XmlAccessType.FIELD) + public static class PeriodTimeInterval { + @XmlElement(name = "start", namespace = NS) + private String start; - @XmlElement(name = "end", namespace = NS) - private String end; + @XmlElement(name = "end", namespace = NS) + private String end; - public String getStart() { return start; } - public String getEnd() { return end; } + public String getStart() { + return start; } - @XmlAccessorType(XmlAccessType.FIELD) - public static class TimeSeries { + public String getEnd() { + return end; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + public static class TimeSeries { - @XmlElement(name = "Period", namespace = NS) - private List periods; + @XmlElement(name = "Period", namespace = NS) + private List periods; - public List getPeriods() { return periods; } + public List getPeriods() { + return periods; } + } - @XmlAccessorType(XmlAccessType.FIELD) - public static class Period { + @XmlAccessorType(XmlAccessType.FIELD) + public static class Period { - @XmlElement(name = "timeInterval", namespace = NS) - private PeriodTimeInterval timeInterval; + @XmlElement(name = "timeInterval", namespace = NS) + private PeriodTimeInterval timeInterval; - @XmlElement(name = "resolution", namespace = NS) - private String resolution; // e.g. PT15M + @XmlElement(name = "resolution", namespace = NS) + private String resolution; // e.g. PT15M - @XmlElement(name = "Point", namespace = NS) - private List points; + @XmlElement(name = "Point", namespace = NS) + private List points; - public PeriodTimeInterval getTimeInterval() { return timeInterval; } - public String getResolution() { return resolution; } - public List getPoints() { return points; } + public PeriodTimeInterval getTimeInterval() { + return timeInterval; } - @XmlAccessorType(XmlAccessType.FIELD) - public static class Point { + public String getResolution() { + return resolution; + } + + public List getPoints() { + return points; + } + } - @XmlElement(name = "position", namespace = NS) - private Integer position; + @XmlAccessorType(XmlAccessType.FIELD) + public static class Point { - @XmlElement(name = "price.amount", namespace = NS) - private BigDecimal priceAmount; + @XmlElement(name = "position", namespace = NS) + private Integer position; - public Integer getPosition() { return position; } - public BigDecimal getPriceAmount() { return priceAmount; } + @XmlElement(name = "price.amount", namespace = NS) + private BigDecimal priceAmount; + + public Integer getPosition() { + return position; + } + + public BigDecimal getPriceAmount() { + return priceAmount; } + } + + public List getTimeSeries() { + return timeSeries; + } - public List getTimeSeries() { return timeSeries; } - public PeriodTimeInterval getPeriodTimeInterval() { return periodTimeInterval; } + public PeriodTimeInterval getPeriodTimeInterval() { + return periodTimeInterval; + } } diff --git a/project.gradle b/project.gradle index 8e2f64d..5265ba9 100644 --- a/project.gradle +++ b/project.gradle @@ -83,6 +83,9 @@ repositories { maven { url = 'https://jitpack.io' } + maven { + url = 'https://plugins.gradle.org/m2' + } } // Eclipse needs help @@ -191,4 +194,20 @@ plugins.withType(JavaPlugin).whenPluginAdded { } } +plugins.apply('com.diffplug.spotless') + +spotless { + java { + target '**/*.java' + + googleJavaFormat() + licenseHeaderFile rootProject.file('tools/spotless/openremote-license-header.txt') + importOrder('java', 'javax', 'com', 'org') + removeUnusedImports() + formatAnnotations() + trimTrailingWhitespace() + endWithNewline() + } +} + // POM generator diff --git a/settings.gradle b/settings.gradle index 3a3c7c1..7793ea8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} + plugins { id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' apply false id "org.jetbrains.gradle.plugin.idea-ext" version "1.3" apply false diff --git a/tools/spotless/openremote-license-header.txt b/tools/spotless/openremote-license-header.txt new file mode 100644 index 0000000..2a336e1 --- /dev/null +++ b/tools/spotless/openremote-license-header.txt @@ -0,0 +1,18 @@ +/* + * Copyright $YEAR, OpenRemote Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */