From 87a95e1685ec15a244629cc03c098a53322c3a6c Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 11:58:52 +0200 Subject: [PATCH 01/35] Add Energy and Demo-Setup extension / update CI/CD to create manager image with all extensions --- .github/workflows/ci_cd.yml | 28 +- demo-setup/build.gradle | 90 + .../extension/demosetup/DemoSetupTasks.java | 52 + .../demosetup/KeycloakDemoSetup.java | 84 + .../demosetup/ManagerDemoAgentSetup.java | 95 + .../demosetup/ManagerDemoDashboardSetup.java | 56 + .../extension/demosetup/ManagerDemoSetup.java | 2393 +++++++++++++++++ .../extension/demosetup/RulesDemoSetup.java | 223 ++ .../demosetup/model/HarvestRobotAsset.java | 80 + .../demosetup/model/IrrigationAsset.java | 55 + .../model/ManufacturerAssetModelProvider.java | 30 + .../demosetup/model/SoilSensorAsset.java | 64 + .../org.openremote.model.AssetModelProvider | 1 + .../org.openremote.model.setup.SetupTasks | 1 + .../dashboards/manufacturer/harvesting.json | 425 +++ .../demo/dashboards/smartcity/parking.json | 274 ++ .../demo/rules/manufacturer/FlowPerMeter.flow | 1 + .../rules/manufacturer/IrrigationTankLow.json | 123 + .../manufacturer/SalinityBetween20And25.json | 94 + .../manufacturer/SalinityGreaterThan25.json | 93 + .../rules/manufacturer/SalinityLessThan3.json | 93 + .../demo/rules/manufacturer/TotalFlow.flow | 1 + .../demo/rules/smartcity/DeKuip.json | 76 + .../rules/smartcity/EnvironmentAlerts.json | 86 + .../demo/rules/smartcity/Euromast.json | 76 + .../demo/rules/smartcity/LightGroupOnOff.flow | 270 ++ .../demo/rules/smartcity/Markthal.json | 76 + .../smartcity/MarkthalChargersInUse.json | 139 + .../smartcity/OnsParkBrightStrongWinds.json | 138 + .../rules/smartcity/OnsParkDimLightWinds.json | 138 + .../demo/rules/smartcity/ParkingFull.json | 61 + .../smartcity/ParkingOccupiedPercentage.flow | 551 ++++ .../rules/smartcity/RotterdamBatteryUse.json | 63 + .../smartcity/RotterdamPowerBalance.flow | 167 ++ .../demo/rules/smartcity/StationCrowded.json | 150 ++ .../smartcity/TotalPowerConsumption.flow | 317 +++ .../rules/smartcity/TotalSolarProduction.flow | 317 +++ deployment/Dockerfile | 4 + deployment/build.gradle | 4 + ems/build.gradle | 9 + energy/build.gradle | 96 + .../manager/EnergyOptimisationService.java | 983 +++++++ .../energy/manager/EnergyOptimiser.java | 786 ++++++ .../energy/manager/ForecastSolarService.java | 422 +++ .../energy/manager/ForecastWindService.java | 344 +++ .../energy/model/ElectricVehicleAsset.java | 351 +++ .../model/ElectricVehicleFleetGroupAsset.java | 193 ++ .../energy/model/ElectricityAsset.java | 181 ++ .../energy/model/ElectricityBatteryAsset.java | 55 + .../energy/model/ElectricityChargerAsset.java | 91 + .../model/ElectricityConsumerAsset.java | 59 + .../model/ElectricityProducerAsset.java | 67 + .../model/ElectricityProducerSolarAsset.java | 274 ++ .../model/ElectricityProducerWindAsset.java | 270 ++ .../energy/model/ElectricityStorageAsset.java | 140 + .../model/ElectricitySupplierAsset.java | 151 ++ .../energy/model/EnergyModelProvider.java | 33 + .../energy/model/EnergyOptimisationAsset.java | 101 + .../energy/storage/StorageSimulatorAgent.java | 48 + .../storage/StorageSimulatorAgentLink.java | 32 + .../storage/StorageSimulatorProtocol.java | 235 ++ .../org.openremote.model.AssetModelProvider | 1 + .../org.openremote.model.ContainerService | 3 + .../energy/EnergyOptimisationAssetTest.groovy | 401 +++ .../energy/EnergyOptimisationTest.groovy | 367 +++ .../energy/ForecastSolarServiceTest.groovy | 214 ++ .../energy/ForecastWindServiceTest.groovy | 778 ++++++ .../extension/energy/KeycloakTestSetup.groovy | 20 + .../extension/energy/ManagerTestSetup.groovy | 118 + .../extension/energy/TestSetupTasks.groovy | 13 + .../org.openremote.model.setup.SetupTasks | 1 + entsoe/build.gradle | 9 + gradle.properties | 3 +- 73 files changed, 13834 insertions(+), 4 deletions(-) create mode 100644 demo-setup/build.gradle create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java create mode 100644 demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java create mode 100644 demo-setup/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider create mode 100644 demo-setup/src/main/resources/META-INF/services/org.openremote.model.setup.SetupTasks create mode 100644 demo-setup/src/main/resources/demo/dashboards/manufacturer/harvesting.json create mode 100644 demo-setup/src/main/resources/demo/dashboards/smartcity/parking.json create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/FlowPerMeter.flow create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/IrrigationTankLow.json create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/SalinityBetween20And25.json create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/SalinityGreaterThan25.json create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/SalinityLessThan3.json create mode 100644 demo-setup/src/main/resources/demo/rules/manufacturer/TotalFlow.flow create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/DeKuip.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/EnvironmentAlerts.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/Euromast.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/LightGroupOnOff.flow create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/Markthal.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/MarkthalChargersInUse.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/OnsParkBrightStrongWinds.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/OnsParkDimLightWinds.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/ParkingFull.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/ParkingOccupiedPercentage.flow create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/RotterdamBatteryUse.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/RotterdamPowerBalance.flow create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/StationCrowded.json create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/TotalPowerConsumption.flow create mode 100644 demo-setup/src/main/resources/demo/rules/smartcity/TotalSolarProduction.flow create mode 100644 deployment/Dockerfile create mode 100644 deployment/build.gradle create mode 100644 energy/build.gradle create mode 100644 energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java create mode 100644 energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java create mode 100644 energy/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider create mode 100644 energy/src/main/resources/META-INF/services/org.openremote.model.ContainerService create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationTest.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/KeycloakTestSetup.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/ManagerTestSetup.groovy create mode 100644 energy/src/test/groovy/org/openremote/extension/energy/TestSetupTasks.groovy create mode 100644 energy/src/test/resources/META-INF/services/org.openremote.model.setup.SetupTasks diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 9efa40c..a57df02 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -34,9 +34,9 @@ jobs: id: java uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: - distribution: 'temurin' - java-version: '21' - cache: 'gradle' + distribution: "temurin" + java-version: "21" + cache: "gradle" - name: Check if main repo id: is_main_repo @@ -51,6 +51,28 @@ jobs: ./gradlew build timeout-minutes: 20 + - name: Install QEMU + #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + with: + platforms: linux/amd64,linux/aarch64 + + - name: Install Buildx + #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Build Manager image + #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: deployment/build + platforms: linux/amd64,linux/aarch64 + load: false + push: false + tags: openremote/manager:${{ inputs.tag }} + build-args: | + GIT_COMMIT=${{ github.sha }} + - name: Run build and publish if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} run: | diff --git a/demo-setup/build.gradle b/demo-setup/build.gradle new file mode 100644 index 0000000..53b8ca5 --- /dev/null +++ b/demo-setup/build.gradle @@ -0,0 +1,90 @@ +apply plugin: "java-library" +apply plugin: "maven-publish" +apply plugin: "signing" + +base { + archivesName = "openremote-${project.name}-extension" +} + +dependencies { + api "io.openremote:openremote-manager:$openremoteVersion" + implementation project(":energy") +} + +jar { + from sourceSets.main.allJava +} + +javadoc { + failOnError = false +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + maven(MavenPublication) { + group = "io.openremote.extension" + artifactId = "openremote-${project.name}-extension" + from components.java + pom { + name = 'OpenRemote Demo setup' + description = 'Adds the OpenRemote Demo setup' + url = 'https://github.com/openremote/openremote' + licenses { + license { + name = 'GNU Affero General Public License v3.0' + url = 'https://www.gnu.org/licenses/agpl-3.0.en.html' + } + } + developers { + developer { + id = 'developers' + name = 'Developers' + email = 'developers@openremote.io' + organization = 'OpenRemote' + organizationUrl = 'https://openremote.io' + } + } + scm { + connection = 'scm:git:git://github.com/openremote/openremote.git' + developerConnection = 'scm:git:ssh://github.com:openremote/openremote.git' + url = 'https://github.com/openremote/openremote/tree/master' + } + } + } + } + + repositories { + maven { + if (!version.endsWith('-LOCAL')) { + credentials { + username = findProperty("publishUsername") + password = findProperty("publishPassword") + } + } + url = version.endsWith('-LOCAL') ? layout.buildDirectory.dir('repo') : version.endsWith('-SNAPSHOT') ? findProperty("snapshotsRepoUrl") : findProperty("releasesRepoUrl") + } + } +} + +signing { + def signingKey = findProperty("signingKey") + def signingPassword = findProperty("signingPassword") + if (signingKey && signingPassword) { + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications.maven + } +} + +tasks.register("copyExtension", Copy) { + from jar.archiveFile + into project(":deployment").layout.buildDirectory.dir("extensions") +} + +tasks.named('build') { + dependsOn('copyExtension') +} \ No newline at end of file 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 new file mode 100644 index 0000000..633046a --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/DemoSetupTasks.java @@ -0,0 +1,52 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +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. + */ +public class DemoSetupTasks implements SetupTasks { + + public static final String DEMO_SETUP_TYPE = "demo"; + + @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; + } +} 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 new file mode 100644 index 0000000..2566a8c --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/KeycloakDemoSetup.java @@ -0,0 +1,84 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +import org.openremote.manager.setup.AbstractKeycloakSetup; +import org.openremote.model.Constants; +import org.openremote.model.Container; +import org.openremote.model.security.ClientRole; +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
  • + * + *
+ */ +public class KeycloakDemoSetup extends AbstractKeycloakSetup { + + 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 KeycloakDemoSetup(Container container) { + super(container); + } + + @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"); + + // 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 + } +} 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 new file mode 100644 index 0000000..de20ca6 --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoAgentSetup.java @@ -0,0 +1,95 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +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; + +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); + } + + @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); + + VelbusTCPAgent agent = new VelbusTCPAgent("Demo VELBUS agent") + .setRealm(realmMasterName) + .setHost(velbusHost) + .setPort(velbusPort); + + 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 new file mode 100644 index 0000000..91773da --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoDashboardSetup.java @@ -0,0 +1,56 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +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); + } + + @Override + public void onStart() throws Exception { + super.onStart(); + + // 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 new file mode 100644 index 0000000..a395c04 --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java @@ -0,0 +1,2393 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +import org.openremote.agent.protocol.http.HTTPAgent; +import org.openremote.agent.protocol.http.HTTPAgentLink; +import org.openremote.agent.protocol.simulator.SimulatorAgent; +import org.openremote.agent.protocol.simulator.SimulatorAgentLink; +import org.openremote.agent.protocol.simulator.SimulatorProtocol; +import org.openremote.extension.demosetup.model.HarvestRobotAsset; +import org.openremote.extension.demosetup.model.HarvestRobotAsset.OperationMode; +import org.openremote.extension.demosetup.model.HarvestRobotAsset.VegetableType; +import org.openremote.extension.demosetup.model.IrrigationAsset; +import org.openremote.extension.demosetup.model.SoilSensorAsset; +import org.openremote.extension.energy.model.ElectricityBatteryAsset; +import org.openremote.extension.energy.model.ElectricityChargerAsset; +import org.openremote.extension.energy.model.ElectricityConsumerAsset; +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; +import org.openremote.model.Container; +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.UserAssetLink; +import org.openremote.model.asset.agent.AgentLink; +import org.openremote.model.asset.impl.*; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeLink; +import org.openremote.model.attribute.AttributeRef; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.geo.GeoJSONPoint; +import org.openremote.model.security.Realm; +import org.openremote.model.simulator.SimulatorReplayDatapoint; +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; + +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 class ManagerDemoSetup extends ManagerSetup { + + 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 static SimulatorProtocol.Schedule createDailySchedule() { + return new SimulatorProtocol.Schedule(null, null, "FREQ=DAILY"); + } + + 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; + } + + // ################################ 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; + } + + 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; + } + + @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) + }) + ) + ); + }); + + 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), -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.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)) + } + ) + ) + ); + }); + 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) + } + ) + ) + ); + }); + 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)) + } + ) + ) + ); + }); + 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)); + }); + 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), + } + ); + } + + /** + * 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())); + } + } + + midnight = midnight.plusDays(1); + } + + 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)); + + 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)); + + 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)); + + 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)); + + 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 new file mode 100644 index 0000000..6379b67 --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/RulesDemoSetup.java @@ -0,0 +1,223 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup; + +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(); + } + } +} 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 new file mode 100644 index 0000000..04a28c5 --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/HarvestRobotAsset.java @@ -0,0 +1,80 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup.model; + +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 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 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 AttributeDescriptor HARVESTED_SESSION = new AttributeDescriptor<>("harvestedSession", + ValueType.POSITIVE_INTEGER); + public static final AttributeDescriptor HARVESTED_TOTAL = new AttributeDescriptor<>("harvestedTotal", + ValueType.POSITIVE_INTEGER); + + public static final AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("robot-industrial", "38761d", HarvestRobotAsset.class); + + /** + * 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 new file mode 100644 index 0000000..a0f9b05 --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/IrrigationAsset.java @@ -0,0 +1,55 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup.model; + +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); + } + +} 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 new file mode 100644 index 0000000..8e99a6a --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/ManufacturerAssetModelProvider.java @@ -0,0 +1,30 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup.model; + +import org.openremote.model.AssetModelProvider; + +public class ManufacturerAssetModelProvider implements AssetModelProvider { + + @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 new file mode 100644 index 0000000..0d05adf --- /dev/null +++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/model/SoilSensorAsset.java @@ -0,0 +1,64 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.demosetup.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.value.AttributeDescriptor; +import org.openremote.model.value.ValueConstraint; +import org.openremote.model.value.ValueType; + +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); + } + +} diff --git a/demo-setup/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider b/demo-setup/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider new file mode 100644 index 0000000..03fd358 --- /dev/null +++ b/demo-setup/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider @@ -0,0 +1 @@ +org.openremote.extension.demosetup.model.ManufacturerAssetModelProvider diff --git a/demo-setup/src/main/resources/META-INF/services/org.openremote.model.setup.SetupTasks b/demo-setup/src/main/resources/META-INF/services/org.openremote.model.setup.SetupTasks new file mode 100644 index 0000000..6f4ff5b --- /dev/null +++ b/demo-setup/src/main/resources/META-INF/services/org.openremote.model.setup.SetupTasks @@ -0,0 +1 @@ +org.openremote.extension.demosetup.DemoSetupTasks diff --git a/demo-setup/src/main/resources/demo/dashboards/manufacturer/harvesting.json b/demo-setup/src/main/resources/demo/dashboards/manufacturer/harvesting.json new file mode 100644 index 0000000..5751732 --- /dev/null +++ b/demo-setup/src/main/resources/demo/dashboards/manufacturer/harvesting.json @@ -0,0 +1,425 @@ +{ + "createdOn": 1711029945937, + "realm": "manufacturer", + "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "access": "SHARED", + "displayName": "Harvesting dashboard", + "template": { + "id": "vmkw9nr0i4", + "columns": 12, + "maxScreenWidth": 4000, + "refreshInterval": "OFF", + "screenPresets": [ + { + "id": "mobile", + "displayName": "dashboard.size.mobile", + "breakpoint": 640, + "scalingPreset": "WRAP_TO_SINGLE_COLUMN" + } + ], + "widgets": [ + { + "id": "slipo02jjs", + "displayName": "Total flow", + "gridItem": { + "id": "slipo02jjs", + "x": 0, + "y": 0, + "w": 6, + "h": 3, + "minH": 2, + "minW": 2, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "linechart", + "widgetConfig": { + "showLegend": true, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "72f37OCsEYkVPdzgcKDlRL", + "name": "flowTotal" + }, + { + "id": "7jSsUaNvM3ZQGfRqBTsOpJ", + "name": "flowTotal" + }, + { + "id": "7atRfYJf83HuiHB6ZdM3ko", + "name": "flowTotal" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1711029955635, + "fromTimestamp": 1710943555635, + "amountOfPoints": 100 + }, + "defaultTimePresetKey": "last24Hours", + "showTimestampControls": false + } + }, + { + "id": "4vsnq4odw8", + "displayName": "Harvest Robot 1 - speed", + "gridItem": { + "id": "4vsnq4odw8", + "x": 6, + "y": 0, + "w": 3, + "h": 3, + "minH": 1, + "minW": 1, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "gauge", + "widgetConfig": { + "max": 100, + "min": 0, + "decimals": 0, + "valueType": "number", + "thresholds": [ + [ + 0, + "#4caf50" + ], + [ + 75, + "#ff9800" + ], + [ + 90, + "#ef5350" + ] + ], + "attributeRefs": [ + { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "speed" + } + ] + } + }, + { + "id": "0z3bfyv8fw", + "displayName": "Harvest Robot 1 - harvested Session", + "gridItem": { + "id": "0z3bfyv8fw", + "x": 9, + "y": 0, + "w": 3, + "h": 3, + "minH": 1, + "minW": 1, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "kpi", + "widgetConfig": { + "period": "hour", + "decimals": 0, + "deltaFormat": "absolute", + "attributeRefs": [ + { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "harvestedSession" + } + ], + "showTimestampControls": true + } + }, + { + "id": "r0w96d6mi1l", + "displayName": "Harvest and irrigation - summary", + "gridItem": { + "id": "r0w96d6mi1l", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "minH": 1, + "minW": 1, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "image", + "widgetConfig": { + "markers": [ + { + "coordinates": [ + 45, + 80 + ], + "attributeRef": { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "harvestedSession" + } + }, + { + "coordinates": [ + 75, + 80 + ], + "attributeRef": { + "id": "2XOXVoMmDkOyLjY1JEnuoF", + "name": "soilTensionMeasured" + } + }, + { + "coordinates": [ + 12, + 80 + ], + "attributeRef": { + "id": "2XOXVoMmDkOyLjY1JEnuoF", + "name": "temperature" + } + } + ], + "imagePath": "https://openremote.io/wp-content/uploads/2024/03/organifarms2.jpeg", + "attributeRefs": [ + { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "harvestedSession" + }, + { + "id": "2XOXVoMmDkOyLjY1JEnuoF", + "name": "soilTensionMeasured" + }, + { + "id": "2XOXVoMmDkOyLjY1JEnuoF", + "name": "temperature" + } + ], + "showTimestampControls": false + } + }, + { + "id": "l2a718n1xp", + "displayName": "Harvest Robot 1", + "gridItem": { + "id": "l2a718n1xp", + "x": 4, + "y": 3, + "w": 2, + "h": 1, + "minH": 0, + "minW": 0, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "attributeinput", + "widgetConfig": { + "readonly": false, + "attributeRefs": [ + { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "operationMode" + } + ], + "showHelperText": false + } + }, + { + "id": "7cnhy57jkd", + "displayName": "Harvest Robot 1", + "gridItem": { + "id": "7cnhy57jkd", + "x": 4, + "y": 4, + "w": 2, + "h": 1, + "minH": 0, + "minW": 0, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "attributeinput", + "widgetConfig": { + "readonly": true, + "attributeRefs": [ + { + "id": "6ZQkcWjwZjL8d04t9mvVvW", + "name": "speed" + } + ], + "showHelperText": false + } + }, + { + "id": "ovhykndppih", + "displayName": "Irrigation 1", + "gridItem": { + "id": "ovhykndppih", + "x": 4, + "y": 5, + "w": 2, + "h": 1, + "minH": 0, + "minW": 0, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "attributeinput", + "widgetConfig": { + "readonly": false, + "attributeRefs": [ + { + "id": "72f37OCsEYkVPdzgcKDlRL", + "name": "flowNutrients" + } + ], + "showHelperText": false + } + }, + { + "id": "wd1oa93jrw", + "displayName": "Irrigation assets", + "gridItem": { + "id": "wd1oa93jrw", + "x": 6, + "y": 3, + "w": 6, + "h": 3, + "minH": 2, + "minW": 2, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "map", + "widgetConfig": { + "zoom": 12, + "center": { + "lat": 51.97, + "lng": 4.295 + }, + "assetIds": [ + "5xl65BMWEKtRiEchVBkANS", + "6PH7Ti0yKO9iQG8xeRMtSZ", + "3cCtcUEyVRs9V1X1JZdpPy", + "7R2GiUXxqKquYpsSERhDA5", + "4yYoOPOlnUrOVlE8Ym50Pw", + "72f37OCsEYkVPdzgcKDlRL", + "7jSsUaNvM3ZQGfRqBTsOpJ", + "7atRfYJf83HuiHB6ZdM3ko", + "4zpw0K9DYH4ejSXqBKetJJ", + "7XusRNMIB1z2R77Uyr5sfP", + "7jEekZEXNaX15k6lvwas13" + ], + "assetType": "IrrigationAsset", + "showUnits": true, + "valueType": "positiveNumber", + "assetTypes": [], + "attributes": [], + "boolColors": { + "true": "#4caf50", + "type": "boolean", + "false": "#ef5350" + }, + "showLabels": true, + "textColors": [ + [ + "example", + "#4caf50" + ], + [ + "example2", + "#ff9800" + ] + ], + "thresholds": [ + [ + 90, + "#ef5350" + ], + [ + 75, + "#ff9800" + ], + [ + 0, + "#4caf50" + ] + ], + "showGeoJson": true, + "attributeName": "flowTotal", + "attributeRefs": [] + } + }, + { + "id": "ku2zv5v9hj", + "displayName": "Summary harvest robots", + "gridItem": { + "id": "ku2zv5v9hj", + "x": 0, + "y": 6, + "w": 12, + "h": 4, + "minH": 0, + "minW": 0, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "table", + "widgetConfig": { + "assetIds": [ + "6ZQkcWjwZjL8d04t9mvVvW", + "3mha9bCluTsqybsGXt5vPb", + "4shhSXQ69KQZY3kJEK9uOH", + "6LDbUPVtOVzI961EMJNnpD", + "5EQdLwkCfhJdWtYyVqvuOI" + ], + "assetType": "HarvestRobotAsset", + "tableSize": 10, + "tableOptions": [ + 10, + 25, + 100 + ], + "attributeNames": [ + "harvestedTotal", + "vegetableType", + "speed", + "harvestedSession" + ] + } + } + ] + } +} diff --git a/demo-setup/src/main/resources/demo/dashboards/smartcity/parking.json b/demo-setup/src/main/resources/demo/dashboards/smartcity/parking.json new file mode 100644 index 0000000..6b84123 --- /dev/null +++ b/demo-setup/src/main/resources/demo/dashboards/smartcity/parking.json @@ -0,0 +1,274 @@ +{ + "createdOn": 1711029229921, + "realm": "smartcity", + "ownerId": "e5ca8a39-5193-478d-ba86-57e7095679cf", + "access": "SHARED", + "displayName": "Parking dashboard", + "template": { + "id": "szpfxnrzpg", + "columns": 12, + "maxScreenWidth": 4000, + "refreshInterval": "OFF", + "screenPresets": [ + { + "id": "mobile", + "displayName": "dashboard.size.mobile", + "breakpoint": 640, + "scalingPreset": "WRAP_TO_SINGLE_COLUMN" + } + ], + "widgets": [ + { + "id": "b4qzd9haajg", + "displayName": "Spaces occupied", + "gridItem": { + "id": "b4qzd9haajg", + "x": 0, + "y": 0, + "w": 8, + "h": 4, + "minH": 2, + "minW": 2, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "linechart", + "widgetConfig": { + "showLegend": true, + "chartOptions": { + "options": { + "scales": { + "y": {}, + "y1": {} + } + } + }, + "attributeRefs": [ + { + "id": "2tLAEBGjmRCu1KrdJn9T2Z", + "name": "spacesOccupied" + }, + { + "id": "3D49AxXerrIycM8gNd0zlK", + "name": "spacesOccupied" + }, + { + "id": "1yuaW634x1LqumPTwGxvyg", + "name": "spacesOccupied" + } + ], + "datapointQuery": { + "type": "lttb", + "toTimestamp": 1711029263421, + "fromTimestamp": 1710942863421, + "amountOfPoints": 100 + }, + "defaultTimePresetKey": "last24Hours", + "showTimestampControls": false + } + }, + { + "id": "he1n3khghci", + "displayName": "Parking space occupation", + "gridItem": { + "id": "he1n3khghci", + "x": 8, + "y": 0, + "w": 4, + "h": 4, + "minH": 2, + "minW": 2, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "map", + "widgetConfig": { + "zoom": 14, + "assetIds": [ + "1yuaW634x1LqumPTwGxvyg", + "3D49AxXerrIycM8gNd0zlK", + "2tLAEBGjmRCu1KrdJn9T2Z" + ], + "assetType": "ParkingAsset", + "showUnits": false, + "valueType": "positiveInteger", + "assetTypes": [], + "attributes": [], + "boolColors": { + "true": "#4caf50", + "type": "boolean", + "false": "#ef5350" + }, + "showLabels": true, + "textColors": [ + [ + "example", + "#4caf50" + ], + [ + "example2", + "#ff9800" + ] + ], + "thresholds": [ + [ + 450, + "#ef5350" + ], + [ + 250, + "#ff9800" + ], + [ + 0, + "#4caf50" + ] + ], + "showGeoJson": true, + "attributeName": "spacesOccupied", + "attributeRefs": [] + } + }, + { + "id": "atxb3vf2zc", + "displayName": "Parking total Occupancy (%)", + "gridItem": { + "id": "atxb3vf2zc", + "x": 0, + "y": 4, + "w": 3, + "h": 3, + "minH": 1, + "minW": 1, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "gauge", + "widgetConfig": { + "max": 100, + "min": 0, + "decimals": 0, + "valueType": "number", + "thresholds": [ + [ + 0, + "#4caf50" + ], + [ + 75, + "#ff9800" + ], + [ + 90, + "#ef5350" + ] + ], + "attributeRefs": [ + { + "id": "7UUzmvnTuLdjVpTb8MnjSX", + "name": "totalOccupancy" + } + ] + } + }, + { + "id": "va9swrnq9ig", + "displayName": "Boompjes parking", + "gridItem": { + "id": "va9swrnq9ig", + "x": 3, + "y": 4, + "w": 4, + "h": 3, + "minH": 1, + "minW": 1, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "image", + "widgetConfig": { + "markers": [ + { + "coordinates": [ + 10, + 50 + ], + "attributeRef": { + "id": "52kOB7c1Rs7WUxgfoCKf5B", + "name": "onOff" + } + }, + { + "coordinates": [ + 9, + 10 + ], + "attributeRef": { + "id": "52kOB7c1Rs7WUxgfoCKf5B", + "name": "colourRGB" + } + } + ], + "imagePath": "https://openremote.io/wp-content/uploads/2024/03/Parking-boompjes.png", + "attributeRefs": [ + { + "id": "52kOB7c1Rs7WUxgfoCKf5B", + "name": "colourRGB" + } + ], + "showTimestampControls": false + } + }, + { + "id": "c2jlie8wj8", + "displayName": "Parking", + "gridItem": { + "id": "c2jlie8wj8", + "x": 7, + "y": 4, + "w": 5, + "h": 3, + "minH": 0, + "minW": 0, + "minPixelH": 0, + "minPixelW": 0, + "noResize": false, + "noMove": false, + "locked": false + }, + "widgetTypeId": "table", + "widgetConfig": { + "assetIds": [ + "2tLAEBGjmRCu1KrdJn9T2Z", + "3D49AxXerrIycM8gNd0zlK", + "1yuaW634x1LqumPTwGxvyg" + ], + "assetType": "ParkingAsset", + "tableSize": 10, + "tableOptions": [ + 10, + 25, + 100 + ], + "attributeNames": [ + "spacesOccupied", + "spacesOpen", + "spacesTotal" + ] + } + } + ] + } +} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/FlowPerMeter.flow b/demo-setup/src/main/resources/demo/rules/manufacturer/FlowPerMeter.flow new file mode 100644 index 0000000..6b53c10 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/FlowPerMeter.flow @@ -0,0 +1 @@ +{"name":"Exported on Wed Feb 08 2023 17:10:42 GMT+0100 (Central European Standard Time)","description":"","connections":[{"from":"fp95mFIcmY","to":"8nUBiX3tsf"},{"from":"UPOOjAjSf5","to":"6SprBC23_T"},{"from":"LdPPsVZ4g0","to":"jqpCePr99D"},{"from":"SzR23bYYWI","to":"hg6y2V016F"},{"from":"V07QwqrOjM","to":"37akuaTT4x"},{"from":"22Ea-H9rJt","to":"jxPmidZ4hp"},{"from":"l4u19raJj7","to":"7je2GMKIsy"}],"nodes":[{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"4w1CElB2Olsn17uoKA1ktn","attributeName":"area"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"sMZ2jkMHG","id":"V07QwqrOjM"}],"type":"INPUT","position":{"x":-491.69140625,"y":255.88019801980184},"size":{"x":141.875,"y":32},"id":"sMZ2jkMHG"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"72f37OCsEYkVPdzgcKDlRL","attributeName":"flowTotal"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"XUpkP5vJC","id":"fp95mFIcmY"}],"type":"INPUT","position":{"x":-493.69140625,"y":-66.5},"size":{"x":119.64099999999999,"y":32},"id":"XUpkP5vJC"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7jSsUaNvM3ZQGfRqBTsOpJ","attributeName":"flowTotal"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"u6eskO3wX","id":"UPOOjAjSf5"}],"type":"INPUT","position":{"x":-491.69140625,"y":38.5},"size":{"x":119.64099999999999,"y":32},"id":"u6eskO3wX"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7atRfYJf83HuiHB6ZdM3ko","attributeName":"flowTotal"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"u9V8msEJi","id":"LdPPsVZ4g0"}],"type":"INPUT","position":{"x":-492.69140625,"y":147.5},"size":{"x":119.64099999999999,"y":32},"id":"u9V8msEJi"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"ZauEuvdOH","id":"8nUBiX3tsf"},{"name":"b","type":"NUMBER","nodeId":"ZauEuvdOH","id":"6SprBC23_T"}],"internals":[],"name":"ADD_OPERATOR","displayCharacter":"+","outputs":[{"name":"c","type":"NUMBER","nodeId":"ZauEuvdOH","id":"SzR23bYYWI"}],"type":"PROCESSOR","position":{"x":-225.5,"y":17.5},"size":{"x":0,"y":0},"id":"ZauEuvdOH"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"WuAb9VWyu","id":"hg6y2V016F"},{"name":"b","type":"NUMBER","nodeId":"WuAb9VWyu","id":"jqpCePr99D"}],"internals":[],"name":"ADD_OPERATOR","displayCharacter":"+","outputs":[{"name":"c","type":"NUMBER","nodeId":"WuAb9VWyu","id":"22Ea-H9rJt"}],"type":"PROCESSOR","position":{"x":-117.5,"y":65.5},"size":{"x":0,"y":0},"id":"WuAb9VWyu"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"VS2XPRCIs","id":"jxPmidZ4hp"},{"name":"b","type":"NUMBER","nodeId":"VS2XPRCIs","id":"37akuaTT4x"}],"internals":[],"name":"DIVIDE_OPERATOR","displayCharacter":"÷","outputs":[{"name":"c","type":"NUMBER","nodeId":"VS2XPRCIs","id":"l4u19raJj7"}],"type":"PROCESSOR","position":{"x":-1.5990099009900973,"y":138.83069306930693},"size":{"x":0,"y":0},"id":"VS2XPRCIs"},{"inputs":[{"name":"value","type":"NUMBER","nodeId":"1APuVTGET","id":"7je2GMKIsy"}],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"4w1CElB2Olsn17uoKA1ktn","attributeName":"flowPerMeter"}}],"name":"WRITE_ATTRIBUTE","outputs":[],"type":"OUTPUT","position":{"x":127.42740563118812,"y":109.26732673267327},"size":{"x":169.188,"y":32},"id":"1APuVTGET"}]} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/IrrigationTankLow.json b/demo-setup/src/main/resources/demo/rules/manufacturer/IrrigationTankLow.json new file mode 100644 index 0000000..2d8d803 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/IrrigationTankLow.json @@ -0,0 +1,123 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "IrrigationAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "tankLevel" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": 900 + } + } + ] + }, + "ids": [ + "7atRfYJf83HuiHB6ZdM3ko" + ] + } + } + ] + }, + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "IrrigationAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "tankLevel" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": 900 + } + } + ] + }, + "ids": [ + "7jSsUaNvM3ZQGfRqBTsOpJ" + ] + } + } + ] + }, + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "IrrigationAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "tankLevel" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": 900 + } + } + ] + }, + "ids": [ + "72f37OCsEYkVPdzgcKDlRL" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "target": { + "custom": "test@test12345.com" + }, + "notification": { + "message": { + "type": "email", + "subject": "%RULESET_NAME%", + "html": "%TRIGGER_ASSETS%" + } + } + } + ], + "name": "Irrigation tank low" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityBetween20And25.json b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityBetween20And25.json new file mode 100644 index 0000000..2bd44b1 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityBetween20And25.json @@ -0,0 +1,94 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "SoilSensorAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "salinity" + }, + "value": { + "predicateType": "number", + "operator": "BETWEEN", + "value": 20, + "rangeValue": 25 + } + } + ] + }, + "ids": [ + "2XOXVoMmDkOyLjY1JEnuoF" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "72f37OCsEYkVPdzgcKDlRL" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 1, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7jSsUaNvM3ZQGfRqBTsOpJ" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 1.2, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7atRfYJf83HuiHB6ZdM3ko" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 1.4, + "attributeName": "flowNutrients" + } + ], + "name": "Salinity 20 < 25" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityGreaterThan25.json b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityGreaterThan25.json new file mode 100644 index 0000000..fbda54d --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityGreaterThan25.json @@ -0,0 +1,93 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "SoilSensorAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "salinity" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 25 + } + } + ] + }, + "ids": [ + "2XOXVoMmDkOyLjY1JEnuoF" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "72f37OCsEYkVPdzgcKDlRL" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 0.5, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7jSsUaNvM3ZQGfRqBTsOpJ" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 0.7, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7atRfYJf83HuiHB6ZdM3ko" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 0.9, + "attributeName": "flowNutrients" + } + ], + "name": "Salinity > 25" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityLessThan3.json b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityLessThan3.json new file mode 100644 index 0000000..a24bee5 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/SalinityLessThan3.json @@ -0,0 +1,93 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "SoilSensorAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "salinity" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": 3 + } + } + ] + }, + "ids": [ + "2XOXVoMmDkOyLjY1JEnuoF" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "72f37OCsEYkVPdzgcKDlRL" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 2, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7jSsUaNvM3ZQGfRqBTsOpJ" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 2.5, + "attributeName": "flowNutrients" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "7atRfYJf83HuiHB6ZdM3ko" + ], + "types": [ + "IrrigationAsset" + ] + } + }, + "value": 2.8, + "attributeName": "flowNutrients" + } + ], + "name": "Salinity < 3" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/manufacturer/TotalFlow.flow b/demo-setup/src/main/resources/demo/rules/manufacturer/TotalFlow.flow new file mode 100644 index 0000000..c7e2a5a --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/manufacturer/TotalFlow.flow @@ -0,0 +1 @@ +{"name":"Irrigation flow total","description":null,"connections":[{"from":"dEwbPuj8iM","to":"GOfh30lHQP"},{"from":"mJ_zJ7BAJL","to":"qs34J1LvHw"},{"from":"eb_Xl0VerR","to":"tVQmiCA97N"},{"from":"7iPfyFrqD2","to":"0U5yYfNQyW"},{"from":"FBWY9ciFvx","to":"I5TW2VSJ-P"},{"from":"Ku9TlCgFWm","to":"eY3ylR9A8g"},{"from":"DUNs7of8dJ","to":"NNYSD2qe7D"},{"from":"nUmYLnvrVT","to":"O4FTYmsLN2"},{"from":"0rxE2Az6pb","to":"B0RUK5UTvx"}],"nodes":[{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"72f37OCsEYkVPdzgcKDlRL","attributeName":"flowNutrients"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"OLkjOAU0Y","id":"mJ_zJ7BAJL"}],"type":"INPUT","position":{"x":-964.222729722535,"y":-171.25475630281085},"size":{"x":146.219,"y":32},"id":"OLkjOAU0Y"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"72f37OCsEYkVPdzgcKDlRL","attributeName":"flowWater"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"6gE-TDvFt","id":"dEwbPuj8iM"}],"type":"INPUT","position":{"x":-964.222729722535,"y":-64.5380333694229},"size":{"x":125.60900000000001,"y":32},"id":"6gE-TDvFt"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7jSsUaNvM3ZQGfRqBTsOpJ","attributeName":"flowNutrients"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"QJc-hbAqP","id":"FBWY9ciFvx"}],"type":"INPUT","position":{"x":-962.5288134854973,"y":72.66918183064736},"size":{"x":146.219,"y":32},"id":"QJc-hbAqP"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7jSsUaNvM3ZQGfRqBTsOpJ","attributeName":"flowWater"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"cURX8iyae","id":"Ku9TlCgFWm"}],"type":"INPUT","position":{"x":-962.5288134854968,"y":186.1615697121867},"size":{"x":125.60900000000001,"y":32},"id":"cURX8iyae"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7atRfYJf83HuiHB6ZdM3ko","attributeName":"flowNutrients"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"TAwn5NRH2","id":"nUmYLnvrVT"}],"type":"INPUT","position":{"x":-960.8348972484591,"y":321.67486867521944},"size":{"x":146.219,"y":32},"id":"TAwn5NRH2"},{"inputs":[],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7atRfYJf83HuiHB6ZdM3ko","attributeName":"flowWater"}}],"name":"READ_ATTRIBUTE","outputs":[{"name":"value","type":"NUMBER","nodeId":"_i1YC9li0","id":"0rxE2Az6pb"}],"type":"INPUT","position":{"x":-959.1409810114219,"y":431.77942408268353},"size":{"x":125.60900000000001,"y":32},"id":"_i1YC9li0"},{"inputs":[{"name":"value","type":"NUMBER","nodeId":"EannnlCyI","id":"tVQmiCA97N"}],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"72f37OCsEYkVPdzgcKDlRL","attributeName":"flowTotal"}}],"name":"WRITE_ATTRIBUTE","outputs":[],"type":"OUTPUT","position":{"x":-492.1847038718263,"y":-114.03802433017361},"size":{"x":119.64099999999999,"y":32},"id":"EannnlCyI"},{"inputs":[{"name":"value","type":"NUMBER","nodeId":"ref6QzgL8","id":"0U5yYfNQyW"}],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7jSsUaNvM3ZQGfRqBTsOpJ","attributeName":"flowTotal"}}],"name":"WRITE_ATTRIBUTE","outputs":[],"type":"OUTPUT","position":{"x":-492.18470387182674,"y":135.90872709053048},"size":{"x":119.64099999999999,"y":32},"id":"ref6QzgL8"},{"inputs":[{"name":"value","type":"NUMBER","nodeId":"R-Qn__JHZ","id":"NNYSD2qe7D"}],"internals":[{"name":"Attribute","picker":{"type":"ASSET_ATTRIBUTE"},"value":{"assetId":"7atRfYJf83HuiHB6ZdM3ko","attributeName":"flowTotal"}}],"name":"WRITE_ATTRIBUTE","outputs":[],"type":"OUTPUT","position":{"x":-490.67900055001365,"y":379.83266522399094},"size":{"x":119.64099999999999,"y":32},"id":"R-Qn__JHZ"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"TNkBOgC8Z","id":"qs34J1LvHw"},{"name":"b","type":"NUMBER","nodeId":"TNkBOgC8Z","id":"GOfh30lHQP"}],"internals":[],"name":"ADD_OPERATOR","displayCharacter":"+","outputs":[{"name":"c","type":"NUMBER","nodeId":"TNkBOgC8Z","id":"eb_Xl0VerR"}],"type":"PROCESSOR","position":{"x":-645.3277999991461,"y":-85.96957863614404},"size":{"x":0,"y":0},"id":"TNkBOgC8Z"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"qie94v39d","id":"I5TW2VSJ-P"},{"name":"b","type":"NUMBER","nodeId":"qie94v39d","id":"eY3ylR9A8g"}],"internals":[],"name":"ADD_OPERATOR","displayCharacter":"+","outputs":[{"name":"c","type":"NUMBER","nodeId":"qie94v39d","id":"7iPfyFrqD2"}],"type":"PROCESSOR","position":{"x":-639.3049867118997,"y":163.97717278456005},"size":{"x":0,"y":0},"id":"qie94v39d"},{"inputs":[{"name":"a","type":"NUMBER","nodeId":"ILSzcBney","id":"O4FTYmsLN2"},{"name":"b","type":"NUMBER","nodeId":"ILSzcBney","id":"B0RUK5UTvx"}],"internals":[],"name":"ADD_OPERATOR","displayCharacter":"+","outputs":[{"name":"c","type":"NUMBER","nodeId":"ILSzcBney","id":"DUNs7of8dJ"}],"type":"PROCESSOR","position":{"x":-636.2935800682768,"y":410.9125175616437},"size":{"x":0,"y":0},"id":"ILSzcBney"}]} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/DeKuip.json b/demo-setup/src/main/resources/demo/rules/smartcity/DeKuip.json new file mode 100644 index 0000000..242ddae --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/DeKuip.json @@ -0,0 +1,76 @@ +{ + "rules": [ + { + "name": "De Kuip", + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "ConsoleAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "location" + }, + "value": { + "predicateType": "radial", + "negated": false, + "radius": 100, + "lat": 51.8938569, + "lng": 4.5219983 + } + } + ] + } + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "notification": { + "name": "De Kuip", + "message": { + "type": "push", + "title": "De Kuip", + "body": "De thuis van Feyenoord", + "action": { + "url": "https://www.feyenoord.nl" + }, + "buttons": [ + { + "title": "Open in app", + "action": { + "url": "https://www.feyenoord.nl" + } + }, + { + "title": "Open in browser", + "action": { + "url": "https://www.feyenoord.nl", + "openInBrowser": true + } + } + ] + } + } + } + ], + "reset": { + "timer": "1m" + } + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/EnvironmentAlerts.json b/demo-setup/src/main/resources/demo/rules/smartcity/EnvironmentAlerts.json new file mode 100644 index 0000000..78aa615 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/EnvironmentAlerts.json @@ -0,0 +1,86 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 10080 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "GroundwaterSensorAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "waterLevel" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": 100 + } + } + ] + } + } + } + ] + }, + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "EnvironmentSensorAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "ozoneLevel" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 120 + } + } + ] + } + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "target": { + "custom": "test@testemail.com" + }, + "notification": { + "message": { + "type": "email", + "subject": "%RULESET_NAME%", + "html": "%TRIGGER_ASSETS%" + } + } + } + ], + "name": "Environment monitoring: Alerts" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/Euromast.json b/demo-setup/src/main/resources/demo/rules/smartcity/Euromast.json new file mode 100644 index 0000000..0b0b86a --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/Euromast.json @@ -0,0 +1,76 @@ +{ + "rules": [ + { + "name": "Euromast", + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "ConsoleAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "location" + }, + "value": { + "predicateType": "radial", + "negated": false, + "radius": 100, + "lat": 51.9069373, + "lng": 4.4633775 + } + } + ] + } + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "notification": { + "name": "Euromast", + "message": { + "type": "push", + "title": "Euromast", + "body": "Euromast, sinds 1960", + "action": { + "url": "https://euromast.nl" + }, + "buttons": [ + { + "title": "Open in app", + "action": { + "url": "https://euromast.nl" + } + }, + { + "title": "Open in browser", + "action": { + "url": "https://euromast.nl", + "openInBrowser": true + } + } + ] + } + } + } + ], + "reset": { + "timer": "1m" + } + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/LightGroupOnOff.flow b/demo-setup/src/main/resources/demo/rules/smartcity/LightGroupOnOff.flow new file mode 100644 index 0000000..3cc9f75 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/LightGroupOnOff.flow @@ -0,0 +1,270 @@ +{ + "name": "Light group: On/Off", + "description": null, + "connections": [ + { + "from": "ukpONuy_1l", + "to": "B3dY_taYNK" + }, + { + "from": "ukpONuy_1l", + "to": "68LWcne-67" + }, + { + "from": "ukpONuy_1l", + "to": "ktRR2NfCIz" + }, + { + "from": "ukpONuy_1l", + "to": "yeg_2mKTYs" + }, + { + "from": "ukpONuy_1l", + "to": "IZLWvgKE53" + }, + { + "from": "ukpONuy_1l", + "to": "jhuPjbsPKE" + } + ], + "nodes": [ + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "52kOB7c1Rs7WUxgfoCKf5B", + "attributeName": "onOff" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "06QKgajTZ", + "id": "ukpONuy_1l" + } + ], + "type": "INPUT", + "position": { + "x": -543.9625396728516, + "y": -112.80001831054688 + }, + "size": { + "x": 227.5, + "y": 65 + }, + "id": "06QKgajTZ" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "B5WJbAESc", + "id": "B3dY_taYNK" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3QlH8nQWvnevcyxat6tQKJ", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -17.587539672851562, + "y": -464.4250183105469 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "B5WJbAESc" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "-RIbDrogC", + "id": "68LWcne-67" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "4Y8sldGAezGSxqSSW3bly5", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -17.212539672851562, + "y": -323.1750183105469 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "-RIbDrogC" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "pfENJ9_NZ", + "id": "ktRR2NfCIz" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "4RYkKKuM1wOw21PNOgHShZ", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -16.962539672851562, + "y": -183.30001831054688 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "pfENJ9_NZ" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "w11vs1M8S", + "id": "yeg_2mKTYs" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "43F9MlyRcnGhY2m2hHlOdz", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -17.712539672851562, + "y": -41.675018310546875 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "w11vs1M8S" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "80FuLcHW-", + "id": "IZLWvgKE53" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "5aID6iE9exaVNirT2flago", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -16.587539672851562, + "y": 94.57498168945312 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "80FuLcHW-" + }, + { + "inputs": [ + { + "name": "value", + "type": "BOOLEAN", + "nodeId": "aJz5lyWMD", + "id": "jhuPjbsPKE" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3wauEi0ol2th6hXULHV2N4", + "attributeName": "onOff" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -17.462539672851562, + "y": 233.94998168945312 + }, + "size": { + "x": 113, + "y": 65 + }, + "id": "aJz5lyWMD" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/Markthal.json b/demo-setup/src/main/resources/demo/rules/smartcity/Markthal.json new file mode 100644 index 0000000..d430586 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/Markthal.json @@ -0,0 +1,76 @@ +{ + "rules": [ + { + "name": "Markthal", + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "ConsoleAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "location" + }, + "value": { + "predicateType": "radial", + "negated": false, + "radius": 100, + "lat": 51.9202494, + "lng": 4.4851372 + } + } + ] + } + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "notification": { + "name": "Markthal", + "message": { + "type": "push", + "title": "Markthal", + "body": "Happy Food Shopping", + "action": { + "url": "https://www.markthal.nl" + }, + "buttons": [ + { + "title": "Open in app", + "action": { + "url": "https://www.markthal.nl" + } + }, + { + "title": "Open in browser", + "action": { + "url": "https://www.markthal.nl", + "openInBrowser": true + } + } + ] + } + } + } + ], + "reset": { + "timer": "1m" + } + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/MarkthalChargersInUse.json b/demo-setup/src/main/resources/demo/rules/smartcity/MarkthalChargersInUse.json new file mode 100644 index 0000000..833a2c5 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/MarkthalChargersInUse.json @@ -0,0 +1,139 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 10080 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "ElectricityChargerAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "power" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_EQUALS", + "value": 0 + } + } + ] + }, + "ids": [ + "2QEKoczoTiOfLKh5UHqEf5" + ] + } + }, + { + "assets": { + "types": [ + "ElectricityChargerAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "power" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_EQUALS", + "value": 0 + } + } + ] + }, + "ids": [ + "6Pgv4HQGmC0EjXg7GmNM6l" + ] + } + }, + { + "assets": { + "types": [ + "ElectricityChargerAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "power" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_EQUALS", + "value": 0 + } + } + ] + }, + "ids": [ + "3KUCNnuNcS2zFAURMpAq0u" + ] + } + }, + { + "assets": { + "types": [ + "ElectricityChargerAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "power" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_EQUALS", + "value": 0 + } + } + ] + }, + "ids": [ + "6EaYX7DEAFwxOJr3de4GFf" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "target": { + "custom": "test@testemail.com" + }, + "notification": { + "message": { + "type": "email", + "subject": "%RULESET_NAME%", + "html": "%TRIGGER_ASSETS%" + } + } + } + ], + "name": "Markthal: All chargers in use" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkBrightStrongWinds.json b/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkBrightStrongWinds.json new file mode 100644 index 0000000..ee06d23 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkBrightStrongWinds.json @@ -0,0 +1,138 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "WeatherAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "windSpeed" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 2 + } + } + ] + }, + "ids": [ + "2bMjSx0iy1usC9KKhT24h9" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "3QlH8nQWvnevcyxat6tQKJ" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "4Y8sldGAezGSxqSSW3bly5" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "4RYkKKuM1wOw21PNOgHShZ" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "43F9MlyRcnGhY2m2hHlOdz" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "5aID6iE9exaVNirT2flago" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "3wauEi0ol2th6hXULHV2N4" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 90, + "attributeName": "brightness" + } + ], + "name": "Ons Park: Brighten lights" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkDimLightWinds.json b/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkDimLightWinds.json new file mode 100644 index 0000000..dd59a4c --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/OnsParkDimLightWinds.json @@ -0,0 +1,138 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "WeatherAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "windSpeed" + }, + "value": { + "predicateType": "number", + "operator": "LESS_EQUALS", + "value": 2 + } + } + ] + }, + "ids": [ + "2bMjSx0iy1usC9KKhT24h9" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "3QlH8nQWvnevcyxat6tQKJ" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "4Y8sldGAezGSxqSSW3bly5" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "4RYkKKuM1wOw21PNOgHShZ" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "43F9MlyRcnGhY2m2hHlOdz" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "5aID6iE9exaVNirT2flago" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "3wauEi0ol2th6hXULHV2N4" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + } + ], + "name": "Ons Park: Dim lights" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/ParkingFull.json b/demo-setup/src/main/resources/demo/rules/smartcity/ParkingFull.json new file mode 100644 index 0000000..1593d38 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/ParkingFull.json @@ -0,0 +1,61 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 10080 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "GroupAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "totalOccupancy" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_EQUALS", + "value": 90 + } + } + ] + }, + "ids": [ + "7UUzmvnTuLdjVpTb8MnjSX" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "notification", + "target": { + "custom": "test@testemail.com" + }, + "notification": { + "message": { + "type": "email", + "subject": "%RULESET_NAME%", + "html": "%TRIGGER_ASSETS%" + } + } + } + ], + "name": "Parking: Almost full" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/ParkingOccupiedPercentage.flow b/demo-setup/src/main/resources/demo/rules/smartcity/ParkingOccupiedPercentage.flow new file mode 100644 index 0000000..38c5958 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/ParkingOccupiedPercentage.flow @@ -0,0 +1,551 @@ +{ + "name": "Parking: Occupied spaces", + "description": null, + "connections": [ + { + "from": "5D-B3ktG0G", + "to": "vw097tfr81" + }, + { + "from": "hsSgzfNE-E", + "to": "bQs4E9qCN2" + }, + { + "from": "vCKMzAmLNx", + "to": "hz9f6Q4ESn" + }, + { + "from": "lasR8uUoUR", + "to": "KqxwaOcGIP" + }, + { + "from": "cOmTxKOpO0", + "to": "mJMXOPeGZd" + }, + { + "from": "SOy3l0IR80", + "to": "vVdwGz2y8k" + }, + { + "from": "-GPWnMsaGi", + "to": "OwI5iFzFMS" + }, + { + "from": "z3-Iw-kaOH", + "to": "IEc8KwEhvK" + }, + { + "from": "AKbiPAtB8t", + "to": "UVDHhIKihX" + }, + { + "from": "C1DIYbdOhv", + "to": "6S19cFPyNd" + }, + { + "from": "LI2D3VNWB6", + "to": "TPkxQ4HKOE" + }, + { + "from": "Rw4MA6Afzc", + "to": "iTcEE2Mgzd" + }, + { + "from": "yxflB92whn", + "to": "mfesEa2D7N" + } + ], + "nodes": [ + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "2tLAEBGjmRCu1KrdJn9T2Z", + "attributeName": "spacesOccupied" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "EXZ-b6ute", + "id": "5D-B3ktG0G" + } + ], + "type": "INPUT", + "position": { + "x": -749.5, + "y": -501.5 + }, + "size": { + "x": 136.234, + "y": 65 + }, + "id": "EXZ-b6ute" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "1yuaW634x1LqumPTwGxvyg", + "attributeName": "spacesOccupied" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "4K3EqIsY6", + "id": "hsSgzfNE-E" + } + ], + "type": "INPUT", + "position": { + "x": -748.5, + "y": -348.5 + }, + "size": { + "x": 121, + "y": 65 + }, + "id": "4K3EqIsY6" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3D49AxXerrIycM8gNd0zlK", + "attributeName": "spacesOccupied" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "fsaLpxzdx", + "id": "lasR8uUoUR" + } + ], + "type": "INPUT", + "position": { + "x": -744.5, + "y": -197.5 + }, + "size": { + "x": 121, + "y": 65 + }, + "id": "fsaLpxzdx" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "1yuaW634x1LqumPTwGxvyg", + "attributeName": "spacesTotal" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "cUasODe-H", + "id": "SOy3l0IR80" + } + ], + "type": "INPUT", + "position": { + "x": -745.5, + "y": 133.5 + }, + "size": { + "x": 121, + "y": 65 + }, + "id": "cUasODe-H" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "2tLAEBGjmRCu1KrdJn9T2Z", + "attributeName": "spacesTotal" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "SNDES5qeX", + "id": "cOmTxKOpO0" + } + ], + "type": "INPUT", + "position": { + "x": -744.5, + "y": -12.5 + }, + "size": { + "x": 136.234, + "y": 65 + }, + "id": "SNDES5qeX" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3D49AxXerrIycM8gNd0zlK", + "attributeName": "spacesTotal" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "qZnj0EbDZ", + "id": "-GPWnMsaGi" + } + ], + "type": "INPUT", + "position": { + "x": -749.5, + "y": 278.5 + }, + "size": { + "x": 121, + "y": 65 + }, + "id": "qZnj0EbDZ" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "EN5SNd71j", + "id": "vw097tfr81" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "EN5SNd71j", + "id": "bQs4E9qCN2" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "EN5SNd71j", + "id": "vCKMzAmLNx" + } + ], + "type": "PROCESSOR", + "position": { + "x": -446.5, + "y": -387.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "EN5SNd71j" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "4PR4GPJ-N", + "id": "hz9f6Q4ESn" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "4PR4GPJ-N", + "id": "KqxwaOcGIP" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "4PR4GPJ-N", + "id": "AKbiPAtB8t" + } + ], + "type": "PROCESSOR", + "position": { + "x": -328.5, + "y": -251.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "4PR4GPJ-N" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "XoUjUmfs9", + "id": "mJMXOPeGZd" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "XoUjUmfs9", + "id": "vVdwGz2y8k" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "XoUjUmfs9", + "id": "z3-Iw-kaOH" + } + ], + "type": "PROCESSOR", + "position": { + "x": -434.5, + "y": 104.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "XoUjUmfs9" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "dlsfNuzaj", + "id": "IEc8KwEhvK" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "dlsfNuzaj", + "id": "OwI5iFzFMS" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "dlsfNuzaj", + "id": "C1DIYbdOhv" + } + ], + "type": "PROCESSOR", + "position": { + "x": -301.5, + "y": 194.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "dlsfNuzaj" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "INNVamTqd", + "id": "UVDHhIKihX" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "INNVamTqd", + "id": "6S19cFPyNd" + } + ], + "internals": [], + "name": "DIVIDE_OPERATOR", + "displayCharacter": "÷", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "INNVamTqd", + "id": "LI2D3VNWB6" + } + ], + "type": "PROCESSOR", + "position": { + "x": -141.5, + "y": -113.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "INNVamTqd" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "WsFkoOv-R", + "id": "TPkxQ4HKOE" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "WsFkoOv-R", + "id": "iTcEE2Mgzd" + } + ], + "internals": [], + "name": "MULTIPLY_OPERATOR", + "displayCharacter": "×", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "WsFkoOv-R", + "id": "yxflB92whn" + } + ], + "type": "PROCESSOR", + "position": { + "x": 136.5, + "y": -51.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "WsFkoOv-R" + }, + { + "inputs": [], + "internals": [ + { + "name": "value", + "picker": { + "type": "NUMBER" + }, + "value": 100 + } + ], + "name": "NUMBER_INPUT", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "_H7TZoQqw", + "id": "Rw4MA6Afzc" + } + ], + "type": "INPUT", + "position": { + "x": -149.734375, + "y": 50.5 + }, + "size": { + "x": 148, + "y": 17 + }, + "id": "_H7TZoQqw" + }, + { + "inputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "jeli6TQY1", + "id": "mfesEa2D7N" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "7UUzmvnTuLdjVpTb8MnjSX", + "attributeName": "totalOccupancy" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": 281.9921875, + "y": -98.5 + }, + "size": { + "x": 263, + "y": 65 + }, + "id": "jeli6TQY1" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamBatteryUse.json b/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamBatteryUse.json new file mode 100644 index 0000000..878497c --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamBatteryUse.json @@ -0,0 +1,63 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "BuildingAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "powerBalance" + }, + "value": { + "predicateType": "number", + "operator": "LESS_THAN", + "value": -25 + } + } + ] + }, + "ids": [ + "2wzKB2j39144oTzAJnHpfs" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "6bzhox7vxOKpKQ5yX5Ysoh" + ], + "types": [ + "ElectricityBatteryAsset" + ] + } + }, + "value": 25, + "attributeName": "powerSetpoint" + } + ], + "name": "De Rotterdam: Battery use" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamPowerBalance.flow b/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamPowerBalance.flow new file mode 100644 index 0000000..367a119 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/RotterdamPowerBalance.flow @@ -0,0 +1,167 @@ +{ +"name": "De Rotterdam: Power balance", +"description": "", + "connections": [ + { + "from": "mtdBqMHEXV", + "to": "HdQFYHsvcv" + }, + { + "from": "LvwzHkg9-A", + "to": "q-nDnOD6dF" + }, + { + "from": "NsjVJrTtFW", + "to": "Ni1QTWS6Cr" + } + ], + "nodes": [ + { + "inputs": [ + + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "6DW7WWYVtOybr7W1BWREc3", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "sroPWsTkE", + "id": "LvwzHkg9-A" + } + ], + "type": "INPUT", + "position": { + "x": -689.5, + "y": -341.5 + }, + "size": { + "x": 193.766, + "y": 65 + }, + "id": "sroPWsTkE" + }, + { + "inputs": [ + + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3Mj9yiC6bcH4MoIYDY2T35", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "f4jr9m-j7", + "id": "mtdBqMHEXV" + } + ], + "type": "INPUT", + "position": { + "x": -687.5, + "y": -165.5 + }, + "size": { + "x": 256.844, + "y": 65 + }, + "id": "f4jr9m-j7" + }, + { + "inputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "pR1MxgR1S", + "id": "Ni1QTWS6Cr" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "2wzKB2j39144oTzAJnHpfs", + "attributeName": "powerBalance" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [ + + ], + "type": "OUTPUT", + "position": { + "x": 25.9921875, + "y": -245.5 + }, + "size": { + "x": 139.453, + "y": 65 + }, + "id": "pR1MxgR1S" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "WdW_DByTa", + "id": "q-nDnOD6dF" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "WdW_DByTa", + "id": "HdQFYHsvcv" + } + ], + "internals": [ + + ], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "WdW_DByTa", + "id": "NsjVJrTtFW" + } + ], + "type": "PROCESSOR", + "position": { + "x": -187.5, + "y": -197.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "WdW_DByTa" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/StationCrowded.json b/demo-setup/src/main/resources/demo/rules/smartcity/StationCrowded.json new file mode 100644 index 0000000..0f02935 --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/StationCrowded.json @@ -0,0 +1,150 @@ +{ + "rules": [ + { + "recurrence": { + "mins": 0 + }, + "when": { + "operator": "OR", + "groups": [ + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "MicrophoneAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "soundLevel" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 60 + } + } + ] + }, + "ids": [ + "2bdD9xZlveIOabucPi9Iur" + ] + } + } + ] + }, + { + "operator": "AND", + "items": [ + { + "assets": { + "types": [ + "MicrophoneAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "soundLevel" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 60 + } + } + ] + }, + "ids": [ + "5WKcxVxZqFq1GYd9b2YVGD" + ] + } + }, + { + "assets": { + "types": [ + "PeopleCounterAsset" + ], + "attributes": { + "items": [ + { + "name": { + "predicateType": "string", + "match": "EXACT", + "value": "countGrowthMinute" + }, + "value": { + "predicateType": "number", + "operator": "GREATER_THAN", + "value": 2 + } + } + ] + }, + "ids": [ + "4vD8XpKSR6iieaSLJ35nKl" + ] + } + } + ] + } + ] + }, + "then": [ + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "2JA9oLQDHY0pjVHnGJMS35" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 60, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "3BSvaTREZX2MLjuE8bkM16" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 75, + "attributeName": "brightness" + }, + { + "action": "write-attribute", + "target": { + "assets": { + "ids": [ + "2motXSzult66RsYrImPTdf" + ], + "types": [ + "LightAsset" + ] + } + }, + "value": 70, + "attributeName": "brightness" + } + ], + "name": "Station: Crowded square" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/TotalPowerConsumption.flow b/demo-setup/src/main/resources/demo/rules/smartcity/TotalPowerConsumption.flow new file mode 100644 index 0000000..332a9bf --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/TotalPowerConsumption.flow @@ -0,0 +1,317 @@ +{ + "name": "Total power consumption", + "description": null, + "connections": [ + { + "from": "k7xzWpPmOU", + "to": "mOv6cwMsN6" + }, + { + "from": "0GKNcY239I", + "to": "TlmXpsOXUQ" + }, + { + "from": "h9ZwYflt4Z", + "to": "rDjJ1NRPSV" + }, + { + "from": "lzZfpdD4S0", + "to": "N2_UIZs3xI" + }, + { + "from": "quH3sV5fSp", + "to": "Rsco32_whA" + }, + { + "from": "cTWyTa7iqz", + "to": "DZdxsbYtlW" + }, + { + "from": "74xUsnTGZY", + "to": "kW3nIIzmKM" + } + ], + "nodes": [ + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "3Mj9yiC6bcH4MoIYDY2T35", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "b_5QIrcpn", + "id": "k7xzWpPmOU" + } + ], + "type": "INPUT", + "position": { + "x": -855.0078125, + "y": -453.5 + }, + "size": { + "x": 257.781, + "y": 65 + }, + "id": "b_5QIrcpn" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "4jBtdIZOtA7pLxPNSwWCVa", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "5zCZbiVkM", + "id": "0GKNcY239I" + } + ], + "type": "INPUT", + "position": { + "x": -858.0078125, + "y": -301.5 + }, + "size": { + "x": 243.969, + "y": 65 + }, + "id": "5zCZbiVkM" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "6L8hVlM1fQ4dDHFM6HYA6O", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "TQ9PC2hLm", + "id": "h9ZwYflt4Z" + } + ], + "type": "INPUT", + "position": { + "x": -861.0078125, + "y": -151.5 + }, + "size": { + "x": 217.031, + "y": 65 + }, + "id": "TQ9PC2hLm" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "4oio0T4DX9lxeBsZVNIpEN", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "EA8rltWHy", + "id": "lzZfpdD4S0" + } + ], + "type": "INPUT", + "position": { + "x": -865.0078125, + "y": 2.5 + }, + "size": { + "x": 216.141, + "y": 65 + }, + "id": "EA8rltWHy" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "VTzqeMTRY", + "id": "mOv6cwMsN6" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "VTzqeMTRY", + "id": "TlmXpsOXUQ" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "VTzqeMTRY", + "id": "quH3sV5fSp" + } + ], + "type": "PROCESSOR", + "position": { + "x": -336.5, + "y": -327.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "VTzqeMTRY" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "zy-tW7hST", + "id": "rDjJ1NRPSV" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "zy-tW7hST", + "id": "N2_UIZs3xI" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "zy-tW7hST", + "id": "cTWyTa7iqz" + } + ], + "type": "PROCESSOR", + "position": { + "x": -362.5, + "y": -31.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "zy-tW7hST" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "4_h4BFrUr", + "id": "Rsco32_whA" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "4_h4BFrUr", + "id": "DZdxsbYtlW" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "4_h4BFrUr", + "id": "74xUsnTGZY" + } + ], + "type": "PROCESSOR", + "position": { + "x": -167.5, + "y": -186.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "4_h4BFrUr" + }, + { + "inputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "3CV-fcqbr", + "id": "kW3nIIzmKM" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "44ORIhkDVAlT97dYGUD9n5", + "attributeName": "powerTotalConsumers" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": 25.9921875, + "y": -232.5 + }, + "size": { + "x": 247, + "y": 65 + }, + "id": "3CV-fcqbr" + } + ] +} diff --git a/demo-setup/src/main/resources/demo/rules/smartcity/TotalSolarProduction.flow b/demo-setup/src/main/resources/demo/rules/smartcity/TotalSolarProduction.flow new file mode 100644 index 0000000..8d1effe --- /dev/null +++ b/demo-setup/src/main/resources/demo/rules/smartcity/TotalSolarProduction.flow @@ -0,0 +1,317 @@ +{ + "name": "Total power production", + "description": null, + "connections": [ + { + "from": "C-QaWZEeBs", + "to": "dxvU9Xz2IM" + }, + { + "from": "mNS9-GZPMK", + "to": "VIEnlWRV_a" + }, + { + "from": "nYeDEdHMK3", + "to": "r0o8xVQT3N" + }, + { + "from": "oCML5aGaOI", + "to": "bONtkmO85l" + }, + { + "from": "IV-unZRsFH", + "to": "PJIlVVDNQw" + }, + { + "from": "UCNOY-HrzI", + "to": "zW2K73tjyv" + }, + { + "from": "lEI0uY0L1o", + "to": "2zo6HaMm88" + } + ], + "nodes": [ + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "6DW7WWYVtOybr7W1BWREc3", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "CynzKmqQe", + "id": "C-QaWZEeBs" + } + ], + "type": "INPUT", + "position": { + "x": -753.0078125, + "y": -514.5 + }, + "size": { + "x": 194.563, + "y": 65 + }, + "id": "CynzKmqQe" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "4Fnz8pxxdtDobuDGWYwRNp", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "NEAaAZEMD", + "id": "mNS9-GZPMK" + } + ], + "type": "INPUT", + "position": { + "x": -751.0078125, + "y": -353.5 + }, + "size": { + "x": 161.578, + "y": 65 + }, + "id": "NEAaAZEMD" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "2RIt5xuSj1BSyaRhwU3z3I", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "DlwsVTeKD", + "id": "nYeDEdHMK3" + } + ], + "type": "INPUT", + "position": { + "x": -754.5, + "y": -188.5 + }, + "size": { + "x": 153.828, + "y": 65 + }, + "id": "DlwsVTeKD" + }, + { + "inputs": [], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "5xgMdcrVWORYWpUKuP2Mo0", + "attributeName": "power" + } + } + ], + "name": "READ_ATTRIBUTE", + "outputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "0_2NG2WHi", + "id": "oCML5aGaOI" + } + ], + "type": "INPUT", + "position": { + "x": -749.5, + "y": -32.5 + }, + "size": { + "x": 152.922, + "y": 65 + }, + "id": "0_2NG2WHi" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "-Citw31Em", + "id": "dxvU9Xz2IM" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "-Citw31Em", + "id": "VIEnlWRV_a" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "-Citw31Em", + "id": "UCNOY-HrzI" + } + ], + "type": "PROCESSOR", + "position": { + "x": -339.5, + "y": -348.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "-Citw31Em" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "2O2rRgaWI", + "id": "r0o8xVQT3N" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "2O2rRgaWI", + "id": "bONtkmO85l" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "2O2rRgaWI", + "id": "IV-unZRsFH" + } + ], + "type": "PROCESSOR", + "position": { + "x": -344.5, + "y": -94.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "2O2rRgaWI" + }, + { + "inputs": [ + { + "name": "a", + "type": "NUMBER", + "nodeId": "Gvoh2qclw", + "id": "zW2K73tjyv" + }, + { + "name": "b", + "type": "NUMBER", + "nodeId": "Gvoh2qclw", + "id": "PJIlVVDNQw" + } + ], + "internals": [], + "name": "ADD_OPERATOR", + "displayCharacter": "+", + "outputs": [ + { + "name": "c", + "type": "NUMBER", + "nodeId": "Gvoh2qclw", + "id": "lEI0uY0L1o" + } + ], + "type": "PROCESSOR", + "position": { + "x": -179.5, + "y": -215.5 + }, + "size": { + "x": 0, + "y": 0 + }, + "id": "Gvoh2qclw" + }, + { + "inputs": [ + { + "name": "value", + "type": "NUMBER", + "nodeId": "XVFP1kzlH", + "id": "2zo6HaMm88" + } + ], + "internals": [ + { + "name": "Attribute", + "picker": { + "type": "ASSET_ATTRIBUTE" + }, + "value": { + "assetId": "44ORIhkDVAlT97dYGUD9n5", + "attributeName": "powerTotalProducers" + } + } + ], + "name": "WRITE_ATTRIBUTE", + "outputs": [], + "type": "OUTPUT", + "position": { + "x": -14.5, + "y": -262.5 + }, + "size": { + "x": 247, + "y": 65 + }, + "id": "XVFP1kzlH" + } + ] +} diff --git a/deployment/Dockerfile b/deployment/Dockerfile new file mode 100644 index 0000000..0764ed8 --- /dev/null +++ b/deployment/Dockerfile @@ -0,0 +1,4 @@ +FROM openremote/manager:latest + +RUN mkdir -p /deployment/manager/extensions +ADD extensions /deployment \ No newline at end of file diff --git a/deployment/build.gradle b/deployment/build.gradle new file mode 100644 index 0000000..9f21d98 --- /dev/null +++ b/deployment/build.gradle @@ -0,0 +1,4 @@ +copy { + into layout.buildDirectory + from "Dockerfile" +} \ No newline at end of file diff --git a/ems/build.gradle b/ems/build.gradle index 5557af3..8ca4397 100644 --- a/ems/build.gradle +++ b/ems/build.gradle @@ -84,3 +84,12 @@ signing { sign publishing.publications.maven } } + +tasks.register("copyExtension", Copy) { + from jar.archiveFile + into project(":deployment").layout.buildDirectory.dir("extensions") +} + +tasks.named('build') { + dependsOn('copyExtension') +} \ No newline at end of file diff --git a/energy/build.gradle b/energy/build.gradle new file mode 100644 index 0000000..9e08907 --- /dev/null +++ b/energy/build.gradle @@ -0,0 +1,96 @@ +apply plugin: "groovy" +apply plugin: "java-library" +apply plugin: "maven-publish" +apply plugin: "signing" + +base { + archivesName = "openremote-${project.name}-extension" +} + +dependencies { + api "org.apache.camel:camel-core-model:$camelVersion" + api "org.jboss.resteasy:resteasy-client-api:$resteasyVersion" + + api "io.openremote:openremote-manager:$openremoteVersion" + api "io.openremote:openremote-model:$openremoteVersion" + + testImplementation "io.openremote:openremote-test:$openremoteVersion" +} + +jar { + from sourceSets.main.allJava +} + +javadoc { + failOnError = false +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + maven(MavenPublication) { + group = "io.openremote.extension" + artifactId = "openremote-${project.name}-extension" + from components.java + pom { + name = 'OpenRemote energy extension' + description = 'Adds the energy domain extension' + url = 'https://github.com/openremote/openremote' + licenses { + license { + name = 'GNU Affero General Public License v3.0' + url = 'https://www.gnu.org/licenses/agpl-3.0.en.html' + } + } + developers { + developer { + id = 'developers' + name = 'Developers' + email = 'developers@openremote.io' + organization = 'OpenRemote' + organizationUrl = 'https://openremote.io' + } + } + scm { + connection = 'scm:git:git://github.com/openremote/openremote.git' + developerConnection = 'scm:git:ssh://github.com:openremote/openremote.git' + url = 'https://github.com/openremote/openremote/tree/master' + } + } + } + } + + repositories { + maven { + if (!version.endsWith('-LOCAL')) { + credentials { + username = findProperty("publishUsername") + password = findProperty("publishPassword") + } + } + url = version.endsWith('-LOCAL') ? layout.buildDirectory.dir('repo') : version.endsWith('-SNAPSHOT') ? findProperty("snapshotsRepoUrl") : findProperty("releasesRepoUrl") + } + } +} + +signing { + def signingKey = findProperty("signingKey") + def signingPassword = findProperty("signingPassword") + if (signingKey && signingPassword) { + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications.maven + } +} + +tasks.register("copyExtension", Copy) { + from jar.archiveFile + into project(":deployment").layout.buildDirectory.dir("extensions") +} + +tasks.named('build') { + dependsOn('copyExtension') +} 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 new file mode 100644 index 0000000..bd1e75d --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimisationService.java @@ -0,0 +1,983 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.manager; + +import org.apache.camel.builder.RouteBuilder; +import org.openremote.container.message.MessageBrokerService; +import org.openremote.container.timer.TimerService; +import org.openremote.extension.energy.model.ElectricVehicleAsset; +import org.openremote.extension.energy.model.ElectricityAsset; +import org.openremote.extension.energy.model.ElectricityChargerAsset; +import org.openremote.extension.energy.model.ElectricityConsumerAsset; +import org.openremote.extension.energy.model.ElectricityProducerAsset; +import org.openremote.extension.energy.model.ElectricityStorageAsset; +import org.openremote.extension.energy.model.ElectricitySupplierAsset; +import org.openremote.extension.energy.model.EnergyOptimisationAsset; +import org.openremote.manager.asset.AssetProcessingService; +import org.openremote.manager.asset.AssetStorageService; +import org.openremote.manager.datapoint.AssetPredictedDatapointService; +import org.openremote.manager.event.ClientEventService; +import org.openremote.manager.gateway.GatewayService; +import org.openremote.model.Container; +import org.openremote.model.ContainerService; +import org.openremote.model.PersistenceEvent; +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.asset.impl.*; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeEvent; +import org.openremote.model.attribute.AttributeExecuteStatus; +import org.openremote.model.attribute.AttributeRef; +import org.openremote.model.datapoint.ValueDatapoint; +import org.openremote.model.datapoint.query.AssetDatapointIntervalQuery; +import org.openremote.model.query.AssetQuery; +import org.openremote.model.query.LogicGroup; +import org.openremote.model.query.filter.AttributePredicate; +import org.openremote.model.query.filter.BooleanPredicate; +import org.openremote.model.query.filter.StringPredicate; +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}. + */ +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 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); + } + + @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()); + } + } + } + + protected void processAttributeEvent(AttributeEvent attributeEvent) { + OptimisationInstance optimisationInstance = assetOptimisationInstanceMap.get(attributeEvent.getId()); + + if (optimisationInstance != null) { + processOptimisationAssetAttributeEvent(optimisationInstance, attributeEvent); + return; + } + + 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; + } + + // 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); + + 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 (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()); + } + } + } + } + + 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 (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; + } + } + + LOG.info("Processing optimisation asset attribute event: " + attributeEvent); + stopOptimisation(attributeEvent.getId()); + + // Get latest asset from storage + EnergyOptimisationAsset asset = (EnergyOptimisationAsset) assetStorageService.find(attributeEvent.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); + } + }); + } 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; + } + + 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); + } + + 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); + } + + // 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); + } + } + + /** + * 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( + new AssetQuery() + .types(ElectricitySupplierAsset.class) + .recursive(true) + .parents(optimisationAssetId) + ).stream() + .filter(asset -> asset.hasAttribute(ElectricitySupplierAsset.TARIFF_IMPORT)) + .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; + } + + 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); + } + + // 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() + "'"); + + 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) + .collect(Collectors.toList()); + + checkTimeoutAndThrow(optimisationAssetId, startTimeMillis); + + 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()); + @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]); + + 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()); + } + return false; + } + + 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())) { + // 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); + + 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); + } + 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.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))) + .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(", "))); + } + + 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(); + }); + + 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( + 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) + ) + ) + .stream() + .map(asset -> (ElectricityStorageAsset) asset).toList(); + + 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 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(); + }); + + 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 (financialWeightingExport < 1d && !supplierAsset.getCarbonExport().isPresent()) { + financialWeightingExport = 1d; + } + + 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]); + } + } + + 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) + "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; + } + + // 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); + } + + 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; + } + + // 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)) + .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) { + + double[] values = new double[intervalCount]; + + if (attribute == null) { + return values; + } + + 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); + + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest(getLogPrefix(optimisationAssetId) + "Calculated earning opportunity power set points 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; + } + + /** + * 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; + } + + // 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); + + // Update energy level using previous interval power + energyLevel += power * intervalSize; + optimisationInstance.unoptimisedStorageAssetEnergyLevels.put(storageAsset.getId(), energyLevel); + + 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 new file mode 100644 index 0000000..920e4d6 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/manager/EnergyOptimiser.java @@ -0,0 +1,786 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.manager; + +import org.openremote.model.util.Pair; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.openremote.extension.energy.manager.EnergyOptimisationService.LOG; + +public class EnergyOptimiser { + + 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)); + } + + 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; + } + + // 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 + 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; + } + } else { + // Already at or above min requirement + break; + } + } + + } 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); + + 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) { + 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); + } + } + }); + } + + /** + * 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 -> { + double powerImportMax = powerImportMaxCalculator.apply(i); + if (importCostAndPower[i][2] < 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]); + }) + .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); + } + } + } + + 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; + } + + 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); + + 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); + } + + j++; + } + } + + // 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; + } + + 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++; + + } + + 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; + + // 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]; + + if (impPowerCapacity <= 0 || impPowerCapacity < pastCostAndPower[1]) { + LOG.finest("Power capacity is outside optimum power band so cannot use this opportunity"); + j++; + continue; + } + + double energySpaceMin = pastCostAndPower[1] * intervalSize; + double energySpace = energyLevelMaxs[interval] - energyLevelCalculator.apply(pastInterval); + energySpace = Math.max(energySpace, energySpace - energySurplusMax); + + // 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) { + + 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++; + + } + + 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); + } + + j++; + } + } + + // 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; + + 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 new file mode 100644 index 0000000..07a1d1a --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java @@ -0,0 +1,422 @@ +package org.openremote.extension.energy.manager; + +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; +import org.openremote.container.timer.TimerService; +import org.openremote.container.web.WebTargetBuilder; +import org.openremote.extension.energy.model.ElectricityProducerSolarAsset; +import org.openremote.manager.asset.AssetProcessingService; +import org.openremote.manager.asset.AssetStorageService; +import org.openremote.manager.datapoint.AssetPredictedDatapointService; +import org.openremote.manager.event.ClientEventService; +import org.openremote.manager.gateway.GatewayService; +import org.openremote.manager.rules.RulesService; +import org.openremote.model.Container; +import org.openremote.model.ContainerService; +import org.openremote.model.PersistenceEvent; +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetFilter; +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.datapoint.query.AssetDatapointAllQuery; +import org.openremote.model.geo.GeoJSONPoint; +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; + +/** + * 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 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; + } + + 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); + } + + // 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; + } + + // 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; + } + + // 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()); + } + } + + // 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); + } + } + } + + 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 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)); + } + } + + 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); + } + } + } + + 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)); + } + } +} 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 new file mode 100644 index 0000000..c002699 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java @@ -0,0 +1,344 @@ +package org.openremote.extension.energy.manager; + +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; +import org.openremote.container.web.WebTargetBuilder; +import org.openremote.extension.energy.model.ElectricityProducerAsset; +import org.openremote.extension.energy.model.ElectricityProducerWindAsset; +import org.openremote.manager.asset.AssetProcessingService; +import org.openremote.manager.asset.AssetStorageService; +import org.openremote.manager.datapoint.AssetPredictedDatapointService; +import org.openremote.manager.event.ClientEventService; +import org.openremote.manager.gateway.GatewayService; +import org.openremote.manager.rules.RulesService; +import org.openremote.model.Container; +import org.openremote.model.ContainerService; +import org.openremote.model.PersistenceEvent; +import org.openremote.model.attribute.AttributeEvent; +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; + +/** + * Calculates power generation for {@link ElectricityProducerWindAsset}. + */ +public class ForecastWindService extends RouteBuilder implements ContainerService { + + protected static class WeatherForecastResponseModel { + + protected WeatherForecastModel current; + + @JsonProperty("hourly") + protected WeatherForecastModel[] list; + + public WeatherForecastModel[] getList() { + return list; + } + } + + protected static class WeatherForecastModel { + + /** + * Seconds + */ + @JsonProperty("dt") + protected long timestamp; + + @JsonProperty("temp") + protected double tempature; + + @JsonProperty("humidity") + protected int humidity; + + @JsonProperty("wind_speed") + protected double windSpeed; + + @JsonProperty("wind_deg") + protected int windDirection; + + @JsonProperty("uvi") + protected double uv; + + public long getTimestamp() { + return timestamp * 1000; + } + + public double getTempature() { + return tempature; + } + + public int getHumidity() { + return humidity; + } + + public double getWindSpeed() { + return windSpeed; + } + + public int getWindDirection() { + return windDirection; + } + + 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( + 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) + && electricityProducerWindAsset.getLocation().isPresent()) + .toList(); + + LOG.fine("Found includes producer wind asset count = " + electricityProducerWindAssets.size()); + + electricityProducerWindAssets.forEach(this::startCalculation); + + clientEventService.addSubscription( + AttributeEvent.class, + null, + this::processAttributeEvent); + } + + @Override + public void stop(Container container) throws Exception { + new ArrayList<>(calculationFutures.keySet()).forEach(this::stopCalculation); + } + + 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; + } + + 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 (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()); + } + + 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); + } + } + } + + 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; + } +} 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 new file mode 100644 index 0000000..f312f3b --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleAsset.java @@ -0,0 +1,351 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeMap; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.geo.GeoJSONPoint; +import org.openremote.model.value.AttributeDescriptor; +import org.openremote.model.value.MetaItemType; +import org.openremote.model.value.ValueDescriptor; +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; + } +} 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 new file mode 100644 index 0000000..3182dfd --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricVehicleFleetGroupAsset.java @@ -0,0 +1,193 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.asset.impl.GroupAsset; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeMap; +import org.openremote.model.geo.GeoJSONPoint; +import org.openremote.model.value.AttributeDescriptor; +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; + } +} 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 new file mode 100644 index 0000000..6785876 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityAsset.java @@ -0,0 +1,181 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.value.*; + +import java.util.Optional; + +import static org.openremote.model.Constants.*; + +@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; + } +} 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 new file mode 100644 index 0000000..f725a40 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityBatteryAsset.java @@ -0,0 +1,55 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +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 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 AssetDescriptor DESCRIPTOR = new AssetDescriptor<>("battery-charging", "1B7C89", ElectricityBatteryAsset.class); + + protected ElectricityBatteryAsset() { + } + + public ElectricityBatteryAsset(String name) { + super(name); + } + + public Optional getChargeCycles() { + return getAttributes().getValue(CHARGE_CYCLES); + } + + 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 new file mode 100644 index 0000000..88690d8 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityChargerAsset.java @@ -0,0 +1,91 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +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.ValueDescriptor; +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; + } +} 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 new file mode 100644 index 0000000..8a3d4f8 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityConsumerAsset.java @@ -0,0 +1,59 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +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); + } +} 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 new file mode 100644 index 0000000..aef906d --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerAsset.java @@ -0,0 +1,67 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +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 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_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); + + /** + * For use by hydrators (i.e. JPA/Jackson) + */ + protected ElectricityProducerAsset() { + } + + public ElectricityProducerAsset(String name) { + super(name); + } + + 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 new file mode 100644 index 0000000..136d3ba --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerSolarAsset.java @@ -0,0 +1,274 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeMap; +import org.openremote.model.geo.GeoJSONPoint; +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; + } +} 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 new file mode 100644 index 0000000..e5d414d --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityProducerWindAsset.java @@ -0,0 +1,270 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.attribute.Attribute; +import org.openremote.model.attribute.AttributeMap; +import org.openremote.model.geo.GeoJSONPoint; +import org.openremote.model.value.AbstractNameValueHolder; +import org.openremote.model.value.AttributeDescriptor; +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; + } +} 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 new file mode 100644 index 0000000..9fc17b9 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricityStorageAsset.java @@ -0,0 +1,140 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.attribute.AttributeExecuteStatus; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.value.AttributeDescriptor; +import org.openremote.model.value.MetaItemType; +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; + } +} 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 new file mode 100644 index 0000000..802dacb --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/ElectricitySupplierAsset.java @@ -0,0 +1,151 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.value.AbstractNameValueHolder; +import org.openremote.model.value.AttributeDescriptor; +import org.openremote.model.value.MetaItemType; +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; + } +} 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 new file mode 100644 index 0000000..8640a44 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/EnergyModelProvider.java @@ -0,0 +1,33 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.AssetModelProvider; + +/** + * Provides the energy asset classes. + */ +public class EnergyModelProvider implements AssetModelProvider { + + @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 new file mode 100644 index 0000000..70124ad --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/model/EnergyOptimisationAsset.java @@ -0,0 +1,101 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.model; + +import org.openremote.model.asset.Asset; +import org.openremote.model.asset.AssetDescriptor; +import org.openremote.model.attribute.MetaItem; +import org.openremote.model.value.*; + +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; + } +} 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 new file mode 100644 index 0000000..8f85771 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgent.java @@ -0,0 +1,48 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.storage; + +import org.openremote.model.asset.agent.Agent; +import org.openremote.model.asset.agent.AgentDescriptor; + +import jakarta.persistence.Entity; + +@Entity +public class StorageSimulatorAgent extends Agent { + + public static AgentDescriptor DESCRIPTOR = new AgentDescriptor<>( + StorageSimulatorAgent.class, StorageSimulatorProtocol.class, StorageSimulatorAgentLink.class, null + ); + + /** + * For use by hydrators (i.e. JPA/Jackson) + */ + protected StorageSimulatorAgent() { + } + + public StorageSimulatorAgent(String name) { + super(name); + } + + @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 new file mode 100644 index 0000000..92d6975 --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorAgentLink.java @@ -0,0 +1,32 @@ +/* + * 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 + * 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 . + */ +package org.openremote.extension.energy.storage; + +import org.openremote.model.asset.agent.AgentLink; + +public class StorageSimulatorAgentLink extends AgentLink { + // For Hydrators + protected StorageSimulatorAgentLink() { + } + + 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 new file mode 100644 index 0000000..b7e2e0f --- /dev/null +++ b/energy/src/main/java/org/openremote/extension/energy/storage/StorageSimulatorProtocol.java @@ -0,0 +1,235 @@ +/* + * 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 + * 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 . + */ +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 java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +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(); + + } + + @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 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 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 doLinkedAttributeWrite(StorageSimulatorAgentLink agentLink, AttributeEvent event, Object processedValue) { + + // Power attribute is updated only by this protocol not by clients + if (event.getName().equals(POWER.getName())) { + return; + } + + updateLinkedAttribute(event.getRef(), processedValue); + } + + 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)); + } + + 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/energy/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider b/energy/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider new file mode 100644 index 0000000..fb222e2 --- /dev/null +++ b/energy/src/main/resources/META-INF/services/org.openremote.model.AssetModelProvider @@ -0,0 +1 @@ +org.openremote.extension.energy.model.EnergyModelProvider diff --git a/energy/src/main/resources/META-INF/services/org.openremote.model.ContainerService b/energy/src/main/resources/META-INF/services/org.openremote.model.ContainerService new file mode 100644 index 0000000..028baa2 --- /dev/null +++ b/energy/src/main/resources/META-INF/services/org.openremote.model.ContainerService @@ -0,0 +1,3 @@ +org.openremote.extension.energy.manager.EnergyOptimisationService +org.openremote.extension.energy.manager.ForecastWindService +org.openremote.extension.energy.manager.ForecastSolarService diff --git a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy new file mode 100644 index 0000000..55c41d9 --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy @@ -0,0 +1,401 @@ +package org.openremote.extension.energy + +import com.google.common.collect.Lists +import org.openremote.container.timer.TimerService +import org.openremote.extension.energy.model.ElectricityAsset +import org.openremote.extension.energy.model.ElectricityBatteryAsset +import org.openremote.extension.energy.model.ElectricityConsumerAsset +import org.openremote.extension.energy.model.ElectricityProducerSolarAsset +import org.openremote.extension.energy.model.ElectricityStorageAsset +import org.openremote.extension.energy.model.ElectricitySupplierAsset +import org.openremote.extension.energy.model.EnergyOptimisationAsset +import org.openremote.extension.energy.manager.EnergyOptimisationService +import org.openremote.extension.energy.manager.EnergyOptimiser +import org.openremote.manager.asset.AssetProcessingService +import org.openremote.manager.asset.AssetStorageService +import org.openremote.manager.datapoint.AssetPredictedDatapointService +import org.openremote.manager.setup.SetupService +import org.openremote.model.attribute.AttributeEvent +import org.openremote.model.attribute.AttributeRef +import org.openremote.model.datapoint.query.AssetDatapointIntervalQuery +import org.openremote.model.util.ValueUtil +import org.openremote.test.ManagerContainerTrait +import spock.lang.Ignore +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeUnit + +import static spock.util.matcher.HamcrestMatchers.closeTo + +/* + * 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 + * 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 . + */ +@Ignore +class EnergyOptimisationAssetTest extends Specification implements ManagerContainerTrait { + def "Test storage asset with consumer and producer"() { + + given: "the container environment is started" + def conditions = new PollingConditions(timeout: 10, delay: 0.2) + def services = Lists.newArrayList(defaultServices()) + def spyOptimisationService = Spy(EnergyOptimisationService) { + scheduleOptimisation(_ as String, _ as EnergyOptimiser, _ as Duration, _ as Long) >> { + // Don't use the scheduler as we will manually trigger the optimisation for testing + optimisationAssetId, optimiser, startDuration, periodSeconds -> + return null + } + } + + services.replaceAll { it instanceof EnergyOptimisationService ? spyOptimisationService : it } + def container = startContainer(defaultConfig(), services) + def managerTestSetup = container.getService(SetupService.class).getTaskOfType(ManagerTestSetup.class) + def optimisationService = container.getService(EnergyOptimisationService.class) + def assetStorageService = container.getService(AssetStorageService.class) + def assetProcessingService = container.getService(AssetProcessingService.class) + def assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class) + def timerService = container.getService(TimerService.class) + + expect: "an optimisation instance should exist" + conditions.eventually { + assert !optimisationService.assetOptimisationInstanceMap.isEmpty() + assert optimisationService.assetOptimisationInstanceMap.get(managerTestSetup.electricityOptimisationAssetId) != null + } + + when: "the pseudo clock is stopped and the system time is set to midnight of next day" + stopPseudoClock() + def now = Instant.ofEpochMilli(timerService.getCurrentTimeMillis()) + now = now.truncatedTo(ChronoUnit.DAYS).plus(1, ChronoUnit.DAYS) + advancePseudoClock(now.toEpochMilli()-timerService.getCurrentTimeMillis(), TimeUnit.MILLISECONDS, container) + + then: "the optimisation start time should be correctly calculated" + def optimiser = optimisationService.assetOptimisationInstanceMap.get(managerTestSetup.electricityOptimisationAssetId).energyOptimiser + def optimisationTime = optimisationService.getOptimisationStartTime(now.toEpochMilli(), (long)optimiser.intervalSize * 60 * 60) + def optimisationDateTime = optimisationTime.atZone(ZoneId.systemDefault()).toLocalDateTime() + assert optimisationTime.isBefore(now) + assert optimisationTime.plus((long)optimiser.intervalSize*60, ChronoUnit.MINUTES).equals(now) + + when: "supplier tariff values are set for the next 24hrs" + def tariffExports = [-5, -2, -8, 2, 2, 5, -2, -2] + def tariffImports = [3, -5, 10, 1, 3, -5, 7, 8] + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_IMPORT.name, tariffImports.get(0))) + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_EXPORT.name, tariffExports.get(0))) + + for (int i = 1; i < tariffExports.size(); i++) { + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_IMPORT.name), tariffImports.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES)) + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_EXPORT.name), tariffExports.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES)) + } + + and: "consumer and producer prediction values are set for the next 24hrs" + def consumerPower = [0, 5, 20, 0, 0, 10, 5, 0] + def producerPower = [-5, -30, -5, 0, 0, -15, -10, -2] + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricityConsumerAssetId, ElectricityAsset.POWER.name, consumerPower.get(0))) + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySolarAssetId, ElectricityAsset.POWER.name, producerPower.get(0))) + + for (int i = 1; i < consumerPower.size(); i++) { + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricityConsumerAssetId, ElectricityAsset.POWER.name), consumerPower.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES)) + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySolarAssetId, ElectricityAsset.POWER.name), producerPower.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES)) + } + + then: "the current values of each attribute should have reached the DB" + conditions.eventually { + assert (assetStorageService.find(managerTestSetup.electricitySupplierAssetId) as ElectricitySupplierAsset).getTariffImport().orElse(0d) == tariffImports.get(0) + assert (assetStorageService.find(managerTestSetup.electricitySupplierAssetId) as ElectricitySupplierAsset).getTariffExport().orElse(0d) == tariffExports.get(0) + assert (assetStorageService.find(managerTestSetup.electricityConsumerAssetId) as ElectricityConsumerAsset).getPower().orElse(-1d) == consumerPower.get(0) + assert (assetStorageService.find(managerTestSetup.electricitySolarAssetId) as ElectricityProducerSolarAsset).getPower().orElse(-1d) == producerPower.get(0) + } + + when: "the optimisation runs" + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime) + + then: "the setpoints of the storage asset for the next 24hrs should be correctly optimised" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 0d + def setpoints = assetPredictedDatapointService.queryDatapoints( + managerTestSetup.electricityBatteryAssetId, + ElectricityAsset.POWER_SETPOINT.name, + new AssetDatapointIntervalQuery( + optimisationTime.atZone(ZoneId.systemDefault()).plus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + optimisationTime.atZone(ZoneId.systemDefault()).plus(1, ChronoUnit.DAYS).minus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + (optimiser.intervalSize * 60) + " minutes", + AssetDatapointIntervalQuery.Formula.AVG, + true + ) + ) + + assert setpoints.size() == 7 + assert setpoints[0].value == 0d + assert setpoints[1].value == -20d + assert setpoints[2].value == 7d //that(setpoints[2].value, closeTo(2.33333, 0.0001)) + assert setpoints[3].value == 0d + assert setpoints[4].value == 7d + assert setpoints[5].value == -14d + assert setpoints[6].value == 0d + } + + when: "storage asset import and export tariffs are added to make storage un-viable and optimisation is run" + def batteryAsset = ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)) + batteryAsset.setTariffExport(10) + batteryAsset.setTariffImport(10) + batteryAsset = assetStorageService.merge(batteryAsset) + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime) + + then: "the battery should not be used in any interval" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 0d + def setpoints = assetPredictedDatapointService.queryDatapoints( + managerTestSetup.electricityBatteryAssetId, + ElectricityAsset.POWER_SETPOINT.name, + new AssetDatapointIntervalQuery( + optimisationTime.atZone(ZoneId.systemDefault()).plus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + optimisationTime.atZone(ZoneId.systemDefault()).plus(1, ChronoUnit.DAYS).minus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + (optimiser.intervalSize * 60) + " minutes", + AssetDatapointIntervalQuery.Formula.AVG, + true + ) + ) + + assert setpoints.size() == 7 + assert setpoints[0].value == 0d + assert setpoints[1].value == 0d + assert setpoints[2].value == 0d + assert setpoints[3].value == 0d + assert setpoints[4].value == 0d + assert setpoints[5].value == 0d + assert setpoints[6].value == 0d + } + + when: "storage asset import and export tariffs are modified to make storage viable in limited intervals and optimisation is run" + batteryAsset = ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)) + batteryAsset.setTariffExport(5) + batteryAsset.setTariffImport(5) + batteryAsset = assetStorageService.merge(batteryAsset) + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime) + + then: "the battery should only be used in the correct intervals" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 0d + def setpoints = assetPredictedDatapointService.queryDatapoints( + managerTestSetup.electricityBatteryAssetId, + ElectricityAsset.POWER_SETPOINT.name, + new AssetDatapointIntervalQuery( + optimisationTime.atZone(ZoneId.systemDefault()).plus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + optimisationTime.atZone(ZoneId.systemDefault()).plus(1, ChronoUnit.DAYS).minus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + (optimiser.intervalSize * 60) + " minutes", + AssetDatapointIntervalQuery.Formula.AVG, + true + ) + ) + + assert setpoints.size() == 7 + assert setpoints[0].value == 0d + assert setpoints[1].value == -20d + assert setpoints[2].value == 0d + assert setpoints[3].value == 0d + assert setpoints[4].value == 0d + assert setpoints[5].value == 0d + assert setpoints[6].value == 0d + } + } + + def "Test savings calculations"() { + + given: "the container environment is started" + def conditions = new PollingConditions(timeout: 10, delay: 0.2) + def services = Lists.newArrayList(defaultServices()) + def spyOptimisationService = Spy(EnergyOptimisationService) { + scheduleOptimisation(_ as String, _ as EnergyOptimiser, _ as Duration, _ as Long) >> { + // Don't use the scheduler as we will manually trigger the optimisation for testing + optimisationAssetId, optimiser, startDuration, periodSeconds -> + return null + } + } + + services.replaceAll { it instanceof EnergyOptimisationService ? spyOptimisationService : it } + def container = startContainer(defaultConfig(), services) + def managerTestSetup = container.getService(SetupService.class).getTaskOfType(ManagerTestSetup.class) + def optimisationService = container.getService(EnergyOptimisationService.class) + def assetStorageService = container.getService(AssetStorageService.class) + def assetProcessingService = container.getService(AssetProcessingService.class) + def assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class) + def timerService = container.getService(TimerService.class) + + expect: "an optimisation instance should exist" + conditions.eventually { + assert !optimisationService.assetOptimisationInstanceMap.isEmpty() + assert optimisationService.assetOptimisationInstanceMap.get(managerTestSetup.electricityOptimisationAssetId) != null + } + + when: "the battery energy level is set to lower limit for ease of testing" + def batteryAsset = assetStorageService.find(managerTestSetup.electricityBatteryAssetId) as ElectricityBatteryAsset + batteryAsset.setEnergyLevel(40) + batteryAsset.setPowerImportMax(10) + batteryAsset.setPowerExportMax(10) + batteryAsset = assetStorageService.merge(batteryAsset) + + and: "the pseudo clock is stopped and the system time is set to midnight of two days time" + stopPseudoClock() + def now = Instant.ofEpochMilli(timerService.getCurrentTimeMillis()) + now = now.truncatedTo(ChronoUnit.DAYS).plus(2, ChronoUnit.DAYS) + advancePseudoClock(now.toEpochMilli()-timerService.getCurrentTimeMillis(), TimeUnit.MILLISECONDS, container) + + then: "the optimisation start time should be correctly calculated" + def optimiser = optimisationService.assetOptimisationInstanceMap.get(managerTestSetup.electricityOptimisationAssetId).energyOptimiser + def optimisationTime = optimisationService.getOptimisationStartTime(now.toEpochMilli(), (long)optimiser.intervalSize * 60 * 60) + def optimisationDateTime = optimisationTime.atZone(ZoneId.systemDefault()) + assert optimisationTime.isBefore(now) + assert optimisationTime.plus((long)optimiser.intervalSize*60, ChronoUnit.MINUTES).equals(now) + + and: "consumer and producer prediction values are set for the next 24hrs" + def consumerPower = [0, 0, 0, 0, 10, 10, 10, 10] + def producerPower = [-50, -50, -50, -50, 0, 0, 0, 0] + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricityConsumerAssetId, ElectricityAsset.POWER.name, consumerPower.get(0))) + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySolarAssetId, ElectricityAsset.POWER.name, producerPower.get(0))) + + for (int i = 1; i < consumerPower.size(); i++) { + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricityConsumerAssetId, ElectricityAsset.POWER.name), consumerPower.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES).toLocalDateTime()) + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySolarAssetId, ElectricityAsset.POWER.name), producerPower.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES).toLocalDateTime()) + } + + and: "supplier tariff values are set for the next 24hrs" + def tariffExports = [-0.05, -0.05, -0.05, -0.05, -0.05, -0.05, -0.05, -0.05] + def tariffImports = [0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08] + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_IMPORT.name, tariffImports.get(0))) + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_EXPORT.name, tariffExports.get(0))) + + for (int i = 1; i < tariffExports.size(); i++) { + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_IMPORT.name), tariffImports.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES).toLocalDateTime()) + assetPredictedDatapointService.updateValue(new AttributeRef(managerTestSetup.electricitySupplierAssetId, ElectricityAsset.TARIFF_EXPORT.name), tariffExports.get(i), optimisationDateTime.plus((long)(optimiser.intervalSize * 60)*i, ChronoUnit.MINUTES).toLocalDateTime()) + } + + then: "the current values of each attribute should have reached the DB" + conditions.eventually { + assert (assetStorageService.find(managerTestSetup.electricityConsumerAssetId) as ElectricityConsumerAsset).getPower().orElse(-1d) == consumerPower.get(0) + assert (assetStorageService.find(managerTestSetup.electricitySolarAssetId) as ElectricityProducerSolarAsset).getPower().orElse(-1d) == producerPower.get(0) + } + + when: "the optimisation asset is reset which will cause optimisation to run" + def optimisationAsset = assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset + optimisationAsset.setOptimisationDisabled(true) + optimisationAsset.setFinancialSaving(0d) + optimisationAsset = ValueUtil.clone(assetStorageService.merge(optimisationAsset)) + optimisationAsset.setOptimisationDisabled(false) + optimisationAsset = assetStorageService.merge(optimisationAsset) + + then: "the battery setpoint should be set to max to store produced energy for future deficit" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 10d + + def setpoints = assetPredictedDatapointService.queryDatapoints( + managerTestSetup.electricityBatteryAssetId, + ElectricityAsset.POWER_SETPOINT.name, + new AssetDatapointIntervalQuery( + optimisationTime.atZone(ZoneId.systemDefault()).plus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + optimisationTime.atZone(ZoneId.systemDefault()).plus(1, ChronoUnit.DAYS).minus((long)(optimiser.intervalSize * 60), ChronoUnit.MINUTES).toLocalDateTime(), + (optimiser.intervalSize * 60) + " minutes", + AssetDatapointIntervalQuery.Formula.AVG, + true + ) + ) + + assert setpoints.size() == 7 + assert setpoints[0].value == 10d + assert setpoints[1].value == 10d + assert setpoints[2].value == 10d + assert setpoints[3].value == -10d + assert setpoints[4].value == -10d + assert setpoints[5].value == -10d + assert setpoints[6].value == -10d + } + + and: "the optimisation saving should be inverse of earning from lost production export -(10x3x0.05)" + conditions.eventually { + assert (assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset).getFinancialSaving().orElse(0d) == -1.5 + } + + when: "the battery energy level changes and time advances" + batteryAsset.setEnergyLevel(70d) + batteryAsset = assetStorageService.merge(batteryAsset) + advancePseudoClock(1, TimeUnit.SECONDS, container) // This prevents attribute timestamp issues + + and: "another optimisation run occurs" + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime.plus((long)optimiser.intervalSize*60, ChronoUnit.MINUTES)) + + then: "the optimisation saving should have decreased by the same amount (i.e. cost)" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 10d + assert (assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset).getFinancialSaving().orElse(0d) == -3.0 + } + + when: "the battery energy level changes and time advances" + batteryAsset.setEnergyLevel(100d) + batteryAsset = assetStorageService.merge(batteryAsset) + advancePseudoClock(1, TimeUnit.SECONDS, container) // This prevents attribute timestamp issues + + and: "another optimisation run occurs" + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime.plus((long)optimiser.intervalSize*60*2, ChronoUnit.MINUTES)) + + then: "the optimisation saving should have decreased by the same amount (i.e. cost)" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 10d + assert (assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset).getFinancialSaving().orElse(0d) == -4.5 + } + + when: "the battery energy level changes and time advances" + batteryAsset.setEnergyLevel(130d) + batteryAsset = assetStorageService.merge(batteryAsset) + advancePseudoClock(1, TimeUnit.SECONDS, container) // This prevents attribute timestamp issues + + and: "another optimisation run occurs" + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime.plus((long)optimiser.intervalSize*60*3, ChronoUnit.MINUTES)) + + then: "the optimisation saving should have decreased by the same amount (i.e. cost)" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == 10d + assert (assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset).getFinancialSaving().orElse(0d) == -6.0 + } + + when: "the consumer and producer power attributes are updated (as they have changed according to the predicted data)" + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricityConsumerAssetId, ElectricityAsset.POWER.name, consumerPower.get(4))) + assetProcessingService.sendAttributeEvent(new AttributeEvent(managerTestSetup.electricitySolarAssetId, ElectricityAsset.POWER.name, producerPower.get(4))) + + then: "the values should have reached the DB" + conditions.eventually { + assert (assetStorageService.find(managerTestSetup.electricityConsumerAssetId) as ElectricityConsumerAsset).getPower().orElse(-1d) == consumerPower.get(4) + assert (assetStorageService.find(managerTestSetup.electricitySolarAssetId) as ElectricityProducerSolarAsset).getPower().orElse(-1d) == producerPower.get(4) + } + + when: "another optimisation run occurs (this should now be exporting from storage)" + batteryAsset.setEnergyLevel(160d) + batteryAsset = assetStorageService.merge(batteryAsset) + advancePseudoClock(1, TimeUnit.SECONDS, container) // This prevents attribute timestamp issues + + and: "another optimisation run occurs" + optimisationService.runOptimisation(managerTestSetup.electricityOptimisationAssetId, optimisationTime.plus((long)optimiser.intervalSize*60*4, ChronoUnit.MINUTES)) + + then: "the optimisation saving should have increased by the cost to import 30kWh (as battery will now be exporting)" + conditions.eventually { + assert ((ElectricityStorageAsset)assetStorageService.find(managerTestSetup.electricityBatteryAssetId)).getPowerSetpoint().orElse(0d) == -10d + assert (assetStorageService.find(managerTestSetup.electricityOptimisationAssetId) as EnergyOptimisationAsset).getFinancialSaving().orElse(0d), closeTo(-6.0+2.4, 0.1) + } + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationTest.groovy new file mode 100644 index 0000000..5666b1f --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationTest.groovy @@ -0,0 +1,367 @@ +package org.openremote.extension.energy + +import org.openremote.extension.energy.manager.EnergyOptimiser +import org.openremote.model.util.UniqueIdentifierGenerator +import org.openremote.model.util.Pair +import spock.lang.Specification + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import java.util.function.Function +import java.util.stream.IntStream + +import static spock.util.matcher.HamcrestMatchers.closeTo + +/* + * 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 + * 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 . + */ + +class EnergyOptimisationTest extends Specification { + + def gridId = UniqueIdentifierGenerator.generateId("grid") + def intervalCount = 8 + def intervalSize = 24 / intervalCount + def currentInterval = 0 + def tariffExports = [-5, -2, -8, 2, 2, 5, -2, -2] + def tariffImports = [3, -5, 10, 1, 3, -5, 7, 8] + def powerNets = [-5, -25, 15, 0, 0, -5, -5, -2] + + def "Check basic import optimiser"() { + + given: "an energy optimisation instance" + def optimisation = new EnergyOptimiser(intervalSize, 1d) + + and: "some import parameters" + def powerImportMax = 7d + + when: "we request the optimised import power values for the input parameters" + def powerNetLimits = new double[intervalCount] + Arrays.fill(powerNetLimits, 30d) + def costCalculator = optimisation.getImportOptimiser(powerNets as double[], powerNetLimits, tariffImports as double[], tariffExports as double[], 0d) + List> optimisedPower = IntStream.range(0, intervalCount).mapToObj{new Pair<>(it, costCalculator.apply(it, [0d, powerImportMax] as double[]))}.collect() + Collections.sort(optimisedPower, Comparator.comparingDouble({Pair optimisedInterval -> optimisedInterval.value[0]})) + + then: "the optimised import values should be correct" + optimisedPower.get(0).key == 5 + optimisedPower.get(0).value[0] == -5d + optimisedPower.get(0).value[1] == 0d + optimisedPower.get(0).value[2] == 7d + optimisedPower.get(1).key == 3 + optimisedPower.get(1).value[0] == 1d + optimisedPower.get(1).value[1] == 0d + optimisedPower.get(1).value[2] == 7d + optimisedPower.get(2).key == 1 + optimisedPower.get(2).value[0] == 2d + optimisedPower.get(2).value[1] == 0d + optimisedPower.get(2).value[2] == 7d + optimisedPower.get(3).key == 6 + optimisedPower.get(3).value[0] == 2d + optimisedPower.get(3).value[1] == 0d + optimisedPower.get(3).value[2] == 5d + optimisedPower.get(4).key == 7 + optimisedPower.get(4).value[0] == 2d + optimisedPower.get(4).value[1] == 0d + optimisedPower.get(4).value[2] == 2d + optimisedPower.get(5).key == 4 + optimisedPower.get(5).value[0] == 3d + optimisedPower.get(5).value[1] == 0d + optimisedPower.get(5).value[2] == 7d + optimisedPower.get(6).key == 0 + optimisedPower.get(6).value[0] closeTo(4.428, 0.001) + optimisedPower.get(6).value[1] == 7d + optimisedPower.get(6).value[2] == 7d + optimisedPower.get(7).key == 2 + optimisedPower.get(7).value[0] == 10d + optimisedPower.get(7).value[1] == 0d + optimisedPower.get(7).value[2] == 7d + + when: "we request the optimised consumption cost values for the EV asset with a minimum power requirement of 6" + optimisedPower = IntStream.range(0, intervalCount).mapToObj{new Pair<>(it, costCalculator.apply(it, [6d, powerImportMax] as double[]))}.collect() + Collections.sort(optimisedPower, Comparator.comparingDouble({Pair optimisedInterval -> optimisedInterval.value[0]})) + + then: "the optimised import values should be correct" + optimisedPower.get(0).key == 5 + optimisedPower.get(0).value[0] == -5d + optimisedPower.get(0).value[1] == 6d + optimisedPower.get(0).value[2] == 7d + optimisedPower.get(1).key == 3 + optimisedPower.get(1).value[0] == 1d + optimisedPower.get(1).value[1] == 6d + optimisedPower.get(1).value[2] == 7d + optimisedPower.get(2).key == 1 + optimisedPower.get(2).value[0] == 2d + optimisedPower.get(2).value[1] == 6d + optimisedPower.get(2).value[2] == 7d + optimisedPower.get(3).key == 6 + optimisedPower.get(3).value[0] closeTo(2.8333, 0.001) + optimisedPower.get(3).value[1] == 6d + optimisedPower.get(3).value[2] == 6d + optimisedPower.get(4).key == 4 + optimisedPower.get(4).value[0] == 3d + optimisedPower.get(4).value[1] == 6d + optimisedPower.get(4).value[2] == 7d + optimisedPower.get(5).key == 0 + optimisedPower.get(5).value[0] closeTo(4.428, 0.001) + optimisedPower.get(5).value[1] == 7d + optimisedPower.get(5).value[2] == 7d + optimisedPower.get(6).key == 7 + optimisedPower.get(6).value[0] == 6d + optimisedPower.get(6).value[1] == 6d + optimisedPower.get(6).value[2] == 6d + optimisedPower.get(7).key == 2 + optimisedPower.get(7).value[0] == 10d + optimisedPower.get(7).value[1] == 6d + optimisedPower.get(7).value[2] == 7d + } + + def "Check basic export optimiser"() { + + given: "an energy optimisation instance" + currentInterval = 0 + def optimisation = new EnergyOptimiser(intervalSize, 1d) + + and: "some storage export parameters" + def powerExportMax = -20d + + when: "we request the optimised export power values for the input parameters" + def powerNetLimits = new double[intervalCount] + Arrays.fill(powerNetLimits, -30d) + def costCalculator = optimisation.getExportOptimiser(powerNets as double[], powerNetLimits, tariffImports as double[], tariffExports as double[], 0d) + List> optimisedPower = IntStream.range(0, intervalCount).mapToObj{new Pair<>(it, costCalculator.apply(it, powerExportMax))}.collect() + Collections.sort(optimisedPower, Comparator.comparingDouble({Pair optimisedInterval -> optimisedInterval.value[0]})) + + then: "the optimised export values should be correct" + optimisedPower.get(0).key == 2 + optimisedPower.get(0).value[0] == -9.5d + optimisedPower.get(0).value[1] == -20d + optimisedPower.get(0).value[2] == -20d + optimisedPower.get(1).key == 0 + optimisedPower.get(1).value[0] == -5d + optimisedPower.get(1).value[1] == -20d + optimisedPower.get(1).value[2] == 0d + optimisedPower.get(2).key == 1 + optimisedPower.get(2).value[0] == -2d + optimisedPower.get(2).value[1] == -5d + optimisedPower.get(2).value[2] == 0d + optimisedPower.get(3).key == 6 + optimisedPower.get(3).value[0] == -2d + optimisedPower.get(3).value[1] == -20d + optimisedPower.get(3).value[2] == 0d + optimisedPower.get(4).key == 7 + optimisedPower.get(4).value[0] == -2d + optimisedPower.get(4).value[1] == -20d + optimisedPower.get(4).value[2] == 0d + optimisedPower.get(5).key == 3 + optimisedPower.get(5).value[0] == 2d + optimisedPower.get(5).value[1] == -20d + optimisedPower.get(5).value[2] == 0d + optimisedPower.get(6).key == 4 + optimisedPower.get(6).value[0] == 2d + optimisedPower.get(6).value[1] == -20d + optimisedPower.get(6).value[2] == 0d + optimisedPower.get(7).key == 5 + optimisedPower.get(7).value[0] == 5d + optimisedPower.get(7).value[1] == -20d + optimisedPower.get(7).value[2] == 0d + } + + def "Check energy schedule extraction"() { + + given: "an energy optimisation instance" + currentInterval = 0 + def startOfDay = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS) + def currentTime = startOfDay.plus(23, ChronoUnit.HOURS) + def optimisation = new EnergyOptimiser(intervalSize, 1d) + + and: "some input parameters" + double energyCapacity = 200d + double energyLevelMin = 40d + double energyLevelMax = 160d + double[] energyMinLevels = new double[intervalCount] + double[] energyMaxLevels = new double[intervalCount] + Arrays.fill(energyMinLevels, energyLevelMin) + Arrays.fill(energyMaxLevels, energyLevelMax) + + when: "an energy schedule is defined and the energy min levels generated from this" + int[] energyScheduleDay = [ + 0, // 00:00 + 0, + 0, + 0, + 0, + 0, + 0, + 80, + 0, + 0, + 0, + 0, + 0, // 12:00 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 // 23:00 + ] + int[][] energyScheduleWeek = new int[24][7] + Arrays.fill(energyScheduleWeek, energyScheduleDay) + optimisation.applyEnergySchedule(energyMinLevels, energyMaxLevels, energyCapacity, energyScheduleWeek, currentTime) + + then: "the energy min levels should be correct" + energyMinLevels == [40d, 40d, 160d, 40d, 40d, 40d, 40d, 40d] as double[] + + when: "the energy min levels are regenerated for a different time" + currentTime = currentTime.plus(7, ChronoUnit.HOURS) + Arrays.fill(energyMinLevels, energyLevelMin) + optimisation.applyEnergySchedule(energyMinLevels, energyMaxLevels, energyCapacity, energyScheduleWeek, currentTime) + + then: "the energy min levels should be correct" + energyMinLevels == [160d, 40d, 40d, 40d, 40d, 40d, 40d, 40d] as double[] + + when: "the optimisation is changed to have an interval size less than 1 hour" + energyMinLevels = new double[24*4] + energyMaxLevels = new double[24*4] + Arrays.fill(energyMinLevels, energyLevelMin) + Arrays.fill(energyMaxLevels, energyLevelMax) + optimisation = new EnergyOptimiser(0.25d, 1d) + optimisation.applyEnergySchedule(energyMinLevels, energyMaxLevels, energyCapacity, energyScheduleWeek, currentTime) + + then: "the energy min levels should be correct" + energyMinLevels[0] == 160d + IntStream.range(1, energyMinLevels.length).mapToDouble{energyMinLevels[it]}.allMatch{it == 40d} + } + + def "Check full storage optimisation functionality"() { + + given: "an energy optimisation instance" + currentInterval = 0 + def startOfDay = LocalDateTime.now().truncatedTo(ChronoUnit.DAYS) + def currentTime = startOfDay.plus(23, ChronoUnit.HOURS) + def optimisation = new EnergyOptimiser(intervalSize, 1d) + + and: "some input parameters" + double energyCapacity = 200d + double energyLevelMin = 40d + double energyLevelMax = 160d + double currentEnergyLevel = 100d + double[] powerSetpoints = new double[intervalCount] + double[] energyMinLevels = new double[intervalCount] + double[] energyMaxLevels = new double[intervalCount] + Arrays.fill(energyMinLevels, energyLevelMin) + Arrays.fill(energyMaxLevels, energyLevelMax) + Function powerImportMaxCalculator = {interval -> 7d} + Function powerExportMaxCalculator = {interval -> -20d} + + when: "an energy schedule is defined and the energy min levels generated from this" + int[] energyScheduleDay = [ + 0, // 00:00 + 0, + 0, + 0, + 0, + 0, + 0, + 80, + 0, + 0, + 0, + 0, + 0, // 12:00 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 // 23:00 + ] + int[][] energyScheduleWeek = new int[24][7] + Arrays.fill(energyScheduleWeek, energyScheduleDay) + optimisation.applyEnergySchedule(energyMinLevels, energyMaxLevels, energyCapacity, energyScheduleWeek, currentTime) + + then: "the energy min levels should be correct" + energyMinLevels == [40d, 40d, 160d, 40d, 40d, 40d, 40d, 40d] as double[] + + when: "the energy min levels are normalised" + optimisation.normaliseEnergyMinRequirements(energyMinLevels, powerImportMaxCalculator, powerExportMaxCalculator, currentEnergyLevel) + + then: "the energy min levels should be correctly normalised" + energyMinLevels == [118d, 139d, 160d, 100d, 40d, 40d, 40d, 40d] as double[] + + when: "the energy min levels are modified to improve code coverage in this test" + Arrays.fill(energyMaxLevels, 170d) + energyMinLevels = [133d, 150d, 166d, 130d, 130d, 100d, 10d, 100d] as double[] + + and: "the energy min levels are normalised" + optimisation.normaliseEnergyMinRequirements(energyMinLevels, powerImportMaxCalculator, powerExportMaxCalculator, currentEnergyLevel) + + then: "the energy min levels should be correctly normalised" + energyMinLevels == [121d, 142d, 163d, 130d, 130d, 100d, 79d, 100d] as double[] + + when: "the import requirements are optimised" + def powerNetLimits = new double[intervalCount] + Arrays.fill(powerNetLimits, 30d) + def importCostCalculator = optimisation.getImportOptimiser(powerNets as double[], powerNetLimits, tariffImports as double[], tariffExports as double[], 0d) + double[][] optimisedImport = IntStream.range(0, intervalCount).mapToObj{importCostCalculator.apply(it, [0d, powerImportMaxCalculator.apply(it)] as double[])}.toArray({new double[it][1]}) + + and: "the export requirements are optimised" + Arrays.fill(powerNetLimits, -30d) + def exportCostCalculator = optimisation.getExportOptimiser(powerNets as double[], powerNetLimits, tariffImports as double[], tariffExports as double[], 0d) + double[][] optimisedExport = IntStream.range(0, intervalCount).mapToObj{exportCostCalculator.apply(it, powerExportMaxCalculator.apply(it))}.toArray({new double[it][1]}) + + and: "the applyEnergyMinImports routine is run on the input parameters" + Function energyLevelCalculator = {int interval -> + currentEnergyLevel + IntStream.range(0, interval).mapToDouble({j -> powerSetpoints[j] * intervalSize}).sum() + } + optimisation.applyEnergyMinImports(optimisedImport, energyMinLevels, powerSetpoints, energyLevelCalculator, importCostCalculator, powerImportMaxCalculator) + + then: "the power setpoints should have been updated to meet energy min requirements in the most cost effective way" + powerSetpoints[0] == 7d + powerSetpoints[1] == 7d + powerSetpoints[2] == 7d + powerSetpoints[3] == 0d + powerSetpoints[4] == 0d + powerSetpoints[5] == 0d + powerSetpoints[6] == 0d + powerSetpoints[7] == 0d + + when: "the earning opportunities are applied to the power setpoints" + optimisation.applyEarningOpportunities(optimisedImport, optimisedExport, energyMinLevels, energyMaxLevels, powerSetpoints, energyLevelCalculator, powerImportMaxCalculator, powerExportMaxCalculator) + + then: "the power setpoints should have been updated to reflect utilisation of earning opportunities" + powerSetpoints[0] == 7d + powerSetpoints[1] == 7d + powerSetpoints[2] == 7d + powerSetpoints[3] == 0d + powerSetpoints[4] closeTo(-4.66666, 0.0001) + powerSetpoints[5] == 7d + powerSetpoints[6] == -20d + powerSetpoints[7] closeTo(-3.33333, 0.0001) + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy new file mode 100644 index 0000000..74077dd --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy @@ -0,0 +1,214 @@ +package org.openremote.extension.energy + +import org.openremote.container.web.WebTargetBuilder +import org.openremote.extension.energy.model.ElectricityProducerSolarAsset +import org.openremote.extension.energy.manager.ForecastSolarService +import org.openremote.manager.asset.AssetProcessingService +import org.openremote.manager.asset.AssetStorageService +import org.openremote.manager.datapoint.AssetPredictedDatapointService +import org.openremote.manager.setup.SetupService +import org.openremote.model.attribute.AttributeEvent +import org.openremote.model.attribute.AttributeRef +import org.openremote.model.geo.GeoJSONPoint +import org.openremote.model.util.ValueUtil +import org.openremote.test.ManagerContainerTrait +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import jakarta.ws.rs.client.ClientRequestContext +import jakarta.ws.rs.client.ClientRequestFilter +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +import static org.openremote.extension.energy.manager.ForecastSolarService.OR_FORECAST_SOLAR_API_KEY + +@Ignore +class ForecastSolarServiceTest extends Specification implements ManagerContainerTrait { + + @Shared + def mockServer = new ClientRequestFilter() { + + boolean finished = false + + @Override + void filter(ClientRequestContext requestContext) throws IOException { + if (finished) return + + def requestUri = requestContext.uri + + switch (requestUri.host) { + case "api.forecast.solar": + def now = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) + def content = "{\n" + + " \"result\": {\n" + + " \"watts\": {\n"+ + " \"${now.toLocalDate().toString()} ${now.toLocalTime().toString()}\": 0,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(15).toLocalTime().toString()}\": 780000,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(30).toLocalTime().toString()}\": 3904036,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(45).toLocalTime().toString()}\": 3854598,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(60).toLocalTime().toString()}\": 3746874,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(75).toLocalTime().toString()}\": 3675780,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(90).toLocalTime().toString()}\": 3553500,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(105).toLocalTime().toString()}\": 3439679,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(120).toLocalTime().toString()}\": 3278604,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(135).toLocalTime().toString()}\": 3137450,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(150).toLocalTime().toString()}\": 2954064,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(165).toLocalTime().toString()}\": 2739056,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(180).toLocalTime().toString()}\": 2493609,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(195).toLocalTime().toString()}\": 2254420,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(210).toLocalTime().toString()}\": 1949976,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(225).toLocalTime().toString()}\": 1697104,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(240).toLocalTime().toString()}\": 1452142,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(255).toLocalTime().toString()}\": 1194060,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(270).toLocalTime().toString()}\": 925642,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(285).toLocalTime().toString()}\": 761949,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(300).toLocalTime().toString()}\": 594217,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(315).toLocalTime().toString()}\": 423624,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(330).toLocalTime().toString()}\": 80000,\n"+ + " \"${now.plusDays(1).toLocalDate().toString()} ${now.toLocalTime().toString()}\": 0\n"+ + " },\n"+ + " \"watt_hours\": {\n"+ + " \"${now.toLocalDate().toString()} ${now.toLocalTime().toString()}\": 0,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(15).toLocalTime().toString()}\": 3471000,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(30).toLocalTime().toString()}\": 20778893,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(45).toLocalTime().toString()}\": 21742542,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(60).toLocalTime().toString()}\": 22679261,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(75).toLocalTime().toString()}\": 23598206,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(90).toLocalTime().toString()}\": 24486581,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(105).toLocalTime().toString()}\": 25346501,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(120).toLocalTime().toString()}\": 26166152,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(135).toLocalTime().toString()}\": 26950514,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(150).toLocalTime().toString()}\": 27689030,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(165).toLocalTime().toString()}\": 28373794,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(180).toLocalTime().toString()}\": 28997196,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(195).toLocalTime().toString()}\": 29560801,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(210).toLocalTime().toString()}\": 30048295,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(225).toLocalTime().toString()}\": 30472571,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(240).toLocalTime().toString()}\": 30835607,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(255).toLocalTime().toString()}\": 31134122,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(270).toLocalTime().toString()}\": 31365532,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(285).toLocalTime().toString()}\": 31556020,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(300).toLocalTime().toString()}\": 31704574,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(315).toLocalTime().toString()}\": 31810480,\n"+ + " \"${now.toLocalDate().toString()} ${now.plusMinutes(330).toLocalTime().toString()}\": 31817147,\n"+ + " \"${now.plusDays(1).toLocalDate().toString()} ${now.toLocalTime().toString()}\": 0\n"+ + " },\n"+ + " \"watt_hours_day\": {\n"+ + " \"${now.toLocalDate().toString()}\": 31817147\n"+ + " }\n"+ + " },\n"+ + " \"message\": {\n"+ + " \"code\": 0,\n"+ + " \"type\": \"success\",\n"+ + " \"text\": \"\",\n"+ + " \"info\": {\n"+ + " \"latitude\": 51.4969,\n"+ + " \"longitude\": -0.2773,\n"+ + " \"place\": \"W3 8BZ Turnham Green, Hounslow London Boro, England, GB\",\n"+ + " \"timezone\": \"Europe/London\"\n"+ + " },\n"+ + " \"ratelimit\": {\n"+ + " \"period\": 60,\n"+ + " \"limit\": 5,\n"+ + " \"remaining\": 4\n"+ + " }\n"+ + " }\n"+ + "}" + def responseBody = ValueUtil.JSON.readValue(content, ForecastSolarService.EstimateResponse.class) + requestContext.abortWith( + Response.ok(responseBody, MediaType.APPLICATION_JSON_TYPE).build() + ) + return + } + + requestContext.abortWith(Response.serverError().build()) + } + } + + def "Test adding and removing asset with enabled attributes"() { + given: "the container environment is started" + def conditions = new PollingConditions(timeout: 10, delay: 0.2) + def config = defaultConfig() + config << [(OR_FORECAST_SOLAR_API_KEY): "test-key"] + WebTargetBuilder.client.register(mockServer, Integer.MAX_VALUE) + + def container = startContainer(config, defaultServices()) + def managerTestSetup = container.getService(SetupService.class).getTaskOfType(ManagerTestSetup.class) + def assetStorageService = container.getService(AssetStorageService.class) + def assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class) + def assetProcessingService = container.getService(AssetProcessingService.class) + def forecastSolarService = container.getService(ForecastSolarService.class) + + expect: "the asset should have filled in value for power and powerForecast" + conditions.eventually { + def solarAsset = assetStorageService.find(managerTestSetup.electricitySolarAssetId) + assert solarAsset.getAttribute(ElectricityProducerSolarAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + assert solarAsset.getAttribute(ElectricityProducerSolarAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(solarAsset.getId(), ElectricityProducerSolarAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(solarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset is added with includeForecastSolarService set to true" + def newSolarAsset = new ElectricityProducerSolarAsset("SolarAsset") + newSolarAsset.setParentId(managerTestSetup.electricityOptimisationAssetId) + newSolarAsset.setRealm(managerTestSetup.realmEnergyName) + newSolarAsset.setPanelAzimuth(0) + newSolarAsset.setPanelPitch(30) + newSolarAsset.setEfficiencyExport(100) + newSolarAsset.setPowerExportMax(2.5) + newSolarAsset.setLocation(new GeoJSONPoint(9.195295, 48.787418)) + newSolarAsset.setSetActualSolarValueWithForecast(true) + newSolarAsset.setIncludeForecastSolarService(true) + newSolarAsset = assetStorageService.merge(newSolarAsset) + + then: "the asset attributes and datapoints should be updated" + conditions.eventually { + newSolarAsset = assetStorageService.find(newSolarAsset.getId()) + assert newSolarAsset.getAttribute(ElectricityProducerSolarAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + assert newSolarAsset.getAttribute(ElectricityProducerSolarAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newSolarAsset.getId(), ElectricityProducerSolarAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newSolarAsset.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset is added with includeForecastSolarService set to false" + def newSolarAsset2 = new ElectricityProducerSolarAsset("SolarAsset2") + newSolarAsset2.setParentId(managerTestSetup.electricityOptimisationAssetId) + newSolarAsset2.setRealm(managerTestSetup.realmEnergyName) + newSolarAsset2.setPanelAzimuth(0); + newSolarAsset2.setPanelPitch(30); + newSolarAsset2.setEfficiencyExport(100); + newSolarAsset2.setPowerExportMax(2.5); + newSolarAsset2.setLocation(new GeoJSONPoint(9.195275, 48.787418)); + newSolarAsset2.setSetActualSolarValueWithForecast(false); + newSolarAsset2.setIncludeForecastSolarService(false); + newSolarAsset2 = assetStorageService.merge(newSolarAsset2) + + and: "an asset updated its includeForecastSolarService to true" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newSolarAsset2.getId(), ElectricityProducerSolarAsset.INCLUDE_FORECAST_SOLAR_SERVICE.name, true)) + + then: "the asset attributes and datapoints should be updated" + conditions.eventually { + newSolarAsset2 = assetStorageService.find(newSolarAsset2.getId()) + assert newSolarAsset2.getAttribute(ElectricityProducerSolarAsset.POWER).flatMap { it.value }.orElse(0d) == 0d + assert newSolarAsset2.getAttribute(ElectricityProducerSolarAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newSolarAsset2.getId(), ElectricityProducerSolarAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newSolarAsset2.getId(), ElectricityProducerSolarAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset updated it's setActualValueWithForecast to true" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newSolarAsset2.getId(), ElectricityProducerSolarAsset.SET_ACTUAL_SOLAR_VALUE_WITH_FORECAST.name, true)) + + then: "it power should be updated too" + conditions.eventually { + newSolarAsset2 = assetStorageService.find(newSolarAsset2.getId()) + assert newSolarAsset2.getAttribute(ElectricityProducerSolarAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + } + + cleanup: "disable mock filter" + mockServer.finished = true + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy new file mode 100644 index 0000000..fd4ec0c --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy @@ -0,0 +1,778 @@ +package org.openremote.extension.energy + +import jakarta.ws.rs.client.ClientRequestContext +import jakarta.ws.rs.client.ClientRequestFilter +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import org.openremote.container.web.WebTargetBuilder +import org.openremote.extension.energy.manager.ForecastWindService +import org.openremote.extension.energy.model.ElectricityProducerWindAsset +import org.openremote.manager.asset.AssetProcessingService +import org.openremote.manager.asset.AssetStorageService +import org.openremote.manager.datapoint.AssetPredictedDatapointService +import org.openremote.manager.setup.SetupService +import org.openremote.model.asset.Asset +import org.openremote.model.attribute.AttributeEvent +import org.openremote.model.attribute.AttributeRef +import org.openremote.model.geo.GeoJSONPoint +import org.openremote.model.util.ValueUtil +import org.openremote.test.ManagerContainerTrait +import spock.lang.Ignore +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit + +import static org.openremote.extension.energy.manager.ForecastWindService.OR_OPEN_WEATHER_API_APP_ID + +@Ignore +class ForecastWindServiceTest extends Specification implements ManagerContainerTrait { + + @Shared + def mockServer = new ClientRequestFilter() { + boolean finished = false + + @Override + void filter(ClientRequestContext requestContext) throws IOException { + if (finished) { + return + } + def requestUri = requestContext.uri + + switch (requestUri.path) { + case "/data/2.5/onecall": + def now = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) + def content = "{\n" + + " \"lat\": 51.97,\n" + + " \"lon\": 5.9,\n" + + " \"timezone\": \"Europe/Amsterdam\",\n" + + " \"timezone_offset\": 3600,\n" + + " \"current\": {\n" + + " \"dt\": ${now.toEpochSecond(ZoneOffset.UTC)},\n" + + " \"sunrise\": 1637045887,\n" + + " \"sunset\": 1637077471,\n" + + " \"temp\": 5.37,\n" + + " \"feels_like\": 5.37,\n" + + " \"pressure\": 1022,\n" + + " \"humidity\": 93,\n" + + " \"dew_point\": 4.33,\n" + + " \"uvi\": 0.51,\n" + + " \"clouds\": 90,\n" + + " \"visibility\": 6000,\n" + + " \"wind_speed\": 3.45,\n" + + " \"wind_deg\": 170,\n" + + " \"wind_gust\": 0,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 701,\n" + + " \"main\": \"Mist\",\n" + + " \"description\": \"mist\",\n" + + " \"icon\": \"50d\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"hourly\": [\n" + + " {\n" + + " \"dt\": ${now.toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.37,\n" + + " \"feels_like\": 5.37,\n" + + " \"pressure\": 1022,\n" + + " \"humidity\": 93,\n" + + " \"dew_point\": 4.33,\n" + + " \"uvi\": 0.51,\n" + + " \"clouds\": 90,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 3.19,\n" + + " \"wind_deg\": 230,\n" + + " \"wind_gust\": 1.4,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(1).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.63,\n" + + " \"feels_like\": 4.53,\n" + + " \"pressure\": 1022,\n" + + " \"humidity\": 89,\n" + + " \"dew_point\": 3.96,\n" + + " \"uvi\": 0.3,\n" + + " \"clouds\": 92,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 2.61,\n" + + " \"wind_deg\": 232,\n" + + " \"wind_gust\": 2.04,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(2).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.79,\n" + + " \"feels_like\": 4.46,\n" + + " \"pressure\": 1022,\n" + + " \"humidity\": 86,\n" + + " \"dew_point\": 3.63,\n" + + " \"uvi\": 0.11,\n" + + " \"clouds\": 94,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.83,\n" + + " \"wind_deg\": 239,\n" + + " \"wind_gust\": 2.56,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(3).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.83,\n" + + " \"feels_like\": 4.51,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 83,\n" + + " \"dew_point\": 3.17,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 96,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.83,\n" + + " \"wind_deg\": 248,\n" + + " \"wind_gust\": 2.92,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(4).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.89,\n" + + " \"feels_like\": 4.64,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 81,\n" + + " \"dew_point\": 2.88,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 98,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.77,\n" + + " \"wind_deg\": 254,\n" + + " \"wind_gust\": 3.22,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(5).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.99,\n" + + " \"feels_like\": 4.72,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 78,\n" + + " \"dew_point\": 2.31,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.81,\n" + + " \"wind_deg\": 251,\n" + + " \"wind_gust\": 3.57,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(6).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6,\n" + + " \"feels_like\": 4.55,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 77,\n" + + " \"dew_point\": 2.15,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.98,\n" + + " \"wind_deg\": 254,\n" + + " \"wind_gust\": 3.76,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(7).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.95,\n" + + " \"feels_like\": 4.68,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 77,\n" + + " \"dew_point\": 2.15,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.8,\n" + + " \"wind_deg\": 242,\n" + + " \"wind_gust\": 3.37,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(8).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.93,\n" + + " \"feels_like\": 4.53,\n" + + " \"pressure\": 1021,\n" + + " \"humidity\": 77,\n" + + " \"dew_point\": 2.18,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 1.92,\n" + + " \"wind_deg\": 213,\n" + + " \"wind_gust\": 3.31,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(9).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 5.94,\n" + + " \"feels_like\": 4.27,\n" + + " \"pressure\": 1020,\n" + + " \"humidity\": 77,\n" + + " \"dew_point\": 2.24,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 2.2,\n" + + " \"wind_deg\": 195,\n" + + " \"wind_gust\": 3.58,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(10).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.05,\n" + + " \"feels_like\": 4.19,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 77,\n" + + " \"dew_point\": 2.35,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 2.45,\n" + + " \"wind_deg\": 192,\n" + + " \"wind_gust\": 4.3,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(11).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.16,\n" + + " \"feels_like\": 4.12,\n" + + " \"pressure\": 1020,\n" + + " \"humidity\": 78,\n" + + " \"dew_point\": 2.52,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 2.7,\n" + + " \"wind_deg\": 228,\n" + + " \"wind_gust\": 6.04,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(12).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.24,\n" + + " \"feels_like\": 4.04,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 78,\n" + + " \"dew_point\": 2.66,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 2.93,\n" + + " \"wind_deg\": 221,\n" + + " \"wind_gust\": 7.28,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(13).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.33,\n" + + " \"feels_like\": 3.88,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 79,\n" + + " \"dew_point\": 2.84,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 3.34,\n" + + " \"wind_deg\": 208,\n" + + " \"wind_gust\": 7.93,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(14).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.49,\n" + + " \"feels_like\": 3.7,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 79,\n" + + " \"dew_point\": 2.97,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 3.98,\n" + + " \"wind_deg\": 202,\n" + + " \"wind_gust\": 8.55,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(15).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.52,\n" + + " \"feels_like\": 3.53,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 79,\n" + + " \"dew_point\": 3.15,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 98,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.37,\n" + + " \"wind_deg\": 214,\n" + + " \"wind_gust\": 9.76,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(16).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.69,\n" + + " \"feels_like\": 3.65,\n" + + " \"pressure\": 1018,\n" + + " \"humidity\": 81,\n" + + " \"dew_point\": 3.62,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 98,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.57,\n" + + " \"wind_deg\": 211,\n" + + " \"wind_gust\": 9.8,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(17).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.72,\n" + + " \"feels_like\": 3.71,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 84,\n" + + " \"dew_point\": 4.1,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 99,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.52,\n" + + " \"wind_deg\": 218,\n" + + " \"wind_gust\": 10.41,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04n\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(18).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 6.85,\n" + + " \"feels_like\": 3.86,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 86,\n" + + " \"dew_point\": 4.58,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 100,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.54,\n" + + " \"wind_deg\": 225,\n" + + " \"wind_gust\": 10.91,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.01\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(19).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 7.15,\n" + + " \"feels_like\": 4.2,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 89,\n" + + " \"dew_point\": 5.39,\n" + + " \"uvi\": 0,\n" + + " \"clouds\": 98,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.6,\n" + + " \"wind_deg\": 228,\n" + + " \"wind_gust\": 10.3,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.05\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(20).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 7.85,\n" + + " \"feels_like\": 5.02,\n" + + " \"pressure\": 1019,\n" + + " \"humidity\": 91,\n" + + " \"dew_point\": 6.35,\n" + + " \"uvi\": 0.1,\n" + + " \"clouds\": 91,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.72,\n" + + " \"wind_deg\": 229,\n" + + " \"wind_gust\": 10.31,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.05\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(21).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 8.52,\n" + + " \"feels_like\": 5.91,\n" + + " \"pressure\": 1020,\n" + + " \"humidity\": 93,\n" + + " \"dew_point\": 7.31,\n" + + " \"uvi\": 0.26,\n" + + " \"clouds\": 94,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 4.6,\n" + + " \"wind_deg\": 258,\n" + + " \"wind_gust\": 9.79,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.05\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(22).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 10.23,\n" + + " \"feels_like\": 9.5,\n" + + " \"pressure\": 1020,\n" + + " \"humidity\": 84,\n" + + " \"dew_point\": 7.47,\n" + + " \"uvi\": 0.36,\n" + + " \"clouds\": 94,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 3.92,\n" + + " \"wind_deg\": 287,\n" + + " \"wind_gust\": 8.04,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.05\n" + + " },\n" + + " {\n" + + " \"dt\": ${now.plusHours(23).toEpochSecond(ZoneOffset.UTC)},\n" + + " \"temp\": 10.03,\n" + + " \"feels_like\": 9.05,\n" + + " \"pressure\": 1020,\n" + + " \"humidity\": 75,\n" + + " \"dew_point\": 5.78,\n" + + " \"uvi\": 0.37,\n" + + " \"clouds\": 95,\n" + + " \"visibility\": 10000,\n" + + " \"wind_speed\": 3.91,\n" + + " \"wind_deg\": 294,\n" + + " \"wind_gust\": 8.34,\n" + + " \"weather\": [\n" + + " {\n" + + " \"id\": 804,\n" + + " \"main\": \"Clouds\",\n" + + " \"description\": \"overcast clouds\",\n" + + " \"icon\": \"04d\"\n" + + " }\n" + + " ],\n" + + " \"pop\": 0.05\n" + + " }\n" + + " ]\n" + + "}" + def responseBody = ValueUtil.JSON.readValue(content, ForecastWindService.WeatherForecastResponseModel.class) + requestContext.abortWith( + Response.ok(responseBody, MediaType.APPLICATION_JSON_TYPE).build() + ) + return + } + + requestContext.abortWith(Response.serverError().build()) + } + } + + def "Test adding and removing asset with enabled attributes"() { + given: "the container environment is started" + def conditions = new PollingConditions(timeout: 10, delay: 0.2) + def config = defaultConfig() + config << [(OR_OPEN_WEATHER_API_APP_ID): "test-key"] + WebTargetBuilder.client.register(mockServer, Integer.MAX_VALUE) + + def container = startContainer(config, defaultServices()) + def managerTestSetup = container.getService(SetupService.class).getTaskOfType(ManagerTestSetup.class) + def assetStorageService = container.getService(AssetStorageService.class) + def assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class) + def assetProcessingService = container.getService(AssetProcessingService.class) + def forecastWindService = container.getService(ForecastWindService.class) + + expect: "a Future for calculation should exist and the asset should have filled in value for power and powerForecast" + conditions.eventually { + assert !forecastWindService.calculationFutures.isEmpty() + assert forecastWindService.calculationFutures.get(managerTestSetup.electricityWindAssetId) != null + def windAsset = assetStorageService.find(managerTestSetup.electricityWindAssetId) + assert windAsset.getAttribute(ElectricityProducerWindAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + assert windAsset.getAttribute(ElectricityProducerWindAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(windAsset.getId(), ElectricityProducerWindAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(windAsset.getId(), ElectricityProducerWindAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset is added with includeForecastWindService set to true" + def newWindAsset = new ElectricityProducerWindAsset("WindAsset") + newWindAsset.setParentId(managerTestSetup.electricityOptimisationAssetId) + newWindAsset.setRealm(managerTestSetup.realmEnergyName) + newWindAsset.setWindSpeedMax(18d); + newWindAsset.setWindSpeedMin(2d); + newWindAsset.setWindSpeedReference(12d); + newWindAsset.setEfficiencyExport(100); + newWindAsset.setPowerExportMax(2.5); + newWindAsset.setLocation(new GeoJSONPoint(9.195295, 48.787418)); + newWindAsset.setSetActualWindValueWithForecast(true); + newWindAsset.setIncludeForecastWindService(true); + newWindAsset = assetStorageService.merge(newWindAsset) + + then: "the assetId should be present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset.getId()) != null + newWindAsset = assetStorageService.find(newWindAsset.getId()) + assert newWindAsset.getAttribute(ElectricityProducerWindAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + assert newWindAsset.getAttribute(ElectricityProducerWindAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset.getId(), ElectricityProducerWindAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset.getId(), ElectricityProducerWindAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset is added with includeForecastWindService set to false" + def newWindAsset2 = new ElectricityProducerWindAsset("WindAsset2") + newWindAsset2.setParentId(managerTestSetup.electricityOptimisationAssetId) + newWindAsset2.setRealm(managerTestSetup.realmEnergyName) + newWindAsset2.setWindSpeedMax(18d); + newWindAsset2.setWindSpeedMin(2d); + newWindAsset2.setWindSpeedReference(12d); + newWindAsset2.setEfficiencyExport(100); + newWindAsset2.setPowerExportMax(2.5); + newWindAsset2.setLocation(new GeoJSONPoint(9.195275, 48.787418)); + newWindAsset2.setSetActualWindValueWithForecast(false); + newWindAsset2.setIncludeForecastWindService(false); + newWindAsset2 = assetStorageService.merge(newWindAsset2) + + then: "the assetId shouldn't be present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset2.getId()) == null + } + + when: "an asset updated it's includeForecastWindService to true" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newWindAsset2.getId(), ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.name, true)) + + then: "it should be present present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset2.getId()) != null + newWindAsset2 = assetStorageService.find(newWindAsset2.getId()) + assert newWindAsset2.getAttribute(ElectricityProducerWindAsset.POWER).flatMap { it.value }.orElse(0d) == 0d + assert newWindAsset2.getAttribute(ElectricityProducerWindAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset2.getId(), ElectricityProducerWindAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset2.getId(), ElectricityProducerWindAsset.POWER_FORECAST.getName())).size() > 0 + } + + when: "an asset updated it's setActualValueWithForecast to true" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newWindAsset2.getId(), ElectricityProducerWindAsset.SET_ACTUAL_WIND_VALUE_WITH_FORECAST.name, true)) + + then: "it power should be updated too" + conditions.eventually { + newWindAsset2 = assetStorageService.find(newWindAsset2.getId()) + assert newWindAsset2.getAttribute(ElectricityProducerWindAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + } + + when: "an asset updated it's includeForecastWindService to false" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newWindAsset2.getId(), ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.name, false)) + + then: "it shouldn't be present present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset2.getId()) == null + } + + when: "an asset is added with includeForecastWindService set to true but no location" + def newWindAsset3 = new ElectricityProducerWindAsset("WindAsset3") + newWindAsset3.setParentId(managerTestSetup.electricityOptimisationAssetId) + newWindAsset3.setRealm(managerTestSetup.realmEnergyName) + newWindAsset3.setWindSpeedMax(18d); + newWindAsset3.setWindSpeedMin(2d); + newWindAsset3.setWindSpeedReference(12d); + newWindAsset3.setEfficiencyExport(100); + newWindAsset3.setPowerExportMax(2.5); + newWindAsset3.setSetActualWindValueWithForecast(true); + newWindAsset3.setIncludeForecastWindService(true); + newWindAsset3 = assetStorageService.merge(newWindAsset3) + + then: "the assetId shouldn't be present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset3.getId()) == null + } + + when: "an asset updated it's location to have a value" + assetProcessingService.sendAttributeEvent(new AttributeEvent(newWindAsset3.getId(), Asset.LOCATION.name, new GeoJSONPoint(9.195275, 48.787418))) + + and: "trigger the service by setting includeForecastWindService to true again " + assetProcessingService.sendAttributeEvent(new AttributeEvent(newWindAsset3.getId(), ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.name, true)) + + then: "it should be present present in the calculationFutures" + conditions.eventually { + assert forecastWindService.calculationFutures.get(newWindAsset3.getId()) != null + newWindAsset3 = assetStorageService.find(newWindAsset3.getId()) + assert newWindAsset3.getAttribute(ElectricityProducerWindAsset.POWER).flatMap { it.value }.orElse(0d) != 0d + assert newWindAsset3.getAttribute(ElectricityProducerWindAsset.POWER_FORECAST).flatMap { it.value }.orElse(0d) != 0d + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset3.getId(), ElectricityProducerWindAsset.POWER.getName())).size() > 0 + assert assetPredictedDatapointService.getDatapoints(new AttributeRef(newWindAsset3.getId(), ElectricityProducerWindAsset.POWER_FORECAST.getName())).size() > 0 + } + + cleanup: "disable mock filter" + mockServer.finished = true + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/KeycloakTestSetup.groovy b/energy/src/test/groovy/org/openremote/extension/energy/KeycloakTestSetup.groovy new file mode 100644 index 0000000..2f94b58 --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/KeycloakTestSetup.groovy @@ -0,0 +1,20 @@ +package org.openremote.extension.energy + +import org.openremote.manager.setup.AbstractKeycloakSetup +import org.openremote.model.Container +import org.openremote.model.security.Realm + +class KeycloakTestSetup extends AbstractKeycloakSetup { + + Realm realmEnergy + + KeycloakTestSetup(Container container) { + super(container) + } + + @Override + void onStart() throws Exception { + super.onStart() + realmEnergy = createRealm("energy", "Energy Test", true) + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ManagerTestSetup.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ManagerTestSetup.groovy new file mode 100644 index 0000000..c083bda --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/ManagerTestSetup.groovy @@ -0,0 +1,118 @@ +package org.openremote.extension.energy + +import static org.openremote.model.value.MetaItemType.* + +import org.openremote.extension.energy.model.ElectricityAsset +import org.openremote.extension.energy.model.ElectricityBatteryAsset +import org.openremote.extension.energy.model.ElectricityConsumerAsset +import org.openremote.extension.energy.model.ElectricityProducerSolarAsset +import org.openremote.extension.energy.model.ElectricityProducerWindAsset +import org.openremote.extension.energy.model.ElectricitySupplierAsset +import org.openremote.extension.energy.model.EnergyOptimisationAsset +import org.openremote.manager.setup.ManagerSetup +import org.openremote.model.Container +import org.openremote.model.attribute.MetaItem +import org.openremote.model.geo.GeoJSONPoint + +class ManagerTestSetup extends ManagerSetup { + + String realmEnergyName + String electricityOptimisationAssetId + String electricityConsumerAssetId + String electricitySolarAssetId + String electricityWindAssetId + String electricitySupplierAssetId + String electricityBatteryAssetId + + ManagerTestSetup(Container container) { + super(container) + } + + @Override + void onStart() throws Exception { + super.onStart() + + KeycloakTestSetup keycloakTestSetup = setupService.getTaskOfType(KeycloakTestSetup.class) + realmEnergyName = keycloakTestSetup.realmEnergy.getName() + + EnergyOptimisationAsset electricityOptimisationAsset = new EnergyOptimisationAsset("Optimisation") + electricityOptimisationAsset.setIntervalSize(3d) + electricityOptimisationAsset.setRealm(keycloakTestSetup.realmEnergy.getName()) + electricityOptimisationAsset.setFinancialWeighting(100) + electricityOptimisationAsset = assetStorageService.merge(electricityOptimisationAsset) + electricityOptimisationAssetId = electricityOptimisationAsset.getId() + + ElectricityConsumerAsset electricityConsumerAsset = new ElectricityConsumerAsset("Consumer") + electricityConsumerAsset.setParent(electricityOptimisationAsset) + electricityConsumerAsset.getAttribute(ElectricityAsset.POWER).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricityConsumerAsset = assetStorageService.merge(electricityConsumerAsset) + electricityConsumerAssetId = electricityConsumerAsset.getId() + + ElectricityProducerSolarAsset electricitySolarAsset = new ElectricityProducerSolarAsset("Producer") + electricitySolarAsset.setParent(electricityOptimisationAsset) + electricitySolarAsset.getAttribute(ElectricityAsset.POWER).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricitySolarAsset.setPanelOrientation(ElectricityProducerSolarAsset.PanelOrientation.SOUTH) + electricitySolarAsset.setPanelAzimuth(0) + electricitySolarAsset.setPanelPitch(30) + electricitySolarAsset.setEfficiencyExport(100) + electricitySolarAsset.setPowerExportMax(2.5) + electricitySolarAsset.setLocation(new GeoJSONPoint(9.195285, 48.787418)) + electricitySolarAsset.setSetActualSolarValueWithForecast(true) + electricitySolarAsset.setIncludeForecastSolarService(true) + electricitySolarAsset = assetStorageService.merge(electricitySolarAsset) + electricitySolarAssetId = electricitySolarAsset.getId() + + ElectricityProducerWindAsset electricityWindAsset = new ElectricityProducerWindAsset("Wind Turbine") + electricityWindAsset.setParent(electricityOptimisationAsset) + electricityWindAsset.getAttribute(ElectricityAsset.POWER).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricityWindAsset.setWindSpeedMax(18d) + electricityWindAsset.setWindSpeedMin(2d) + electricityWindAsset.setWindSpeedReference(12d) + electricityWindAsset.setPowerExportMax(9000d) + electricityWindAsset.setEfficiencyExport(100) + electricityWindAsset.setPowerExportMax(2.5) + electricityWindAsset.setLocation(new GeoJSONPoint(9.195285, 48.787418)) + electricityWindAsset.setSetActualWindValueWithForecast(true) + electricityWindAsset.setIncludeForecastWindService(true) + electricityWindAsset = assetStorageService.merge(electricityWindAsset) + electricityWindAssetId = electricityWindAsset.getId() + + ElectricityBatteryAsset electricityBatteryAsset = new ElectricityBatteryAsset("Battery") + electricityBatteryAsset.setParent(electricityOptimisationAsset) + electricityBatteryAsset.setEnergyCapacity(200d) + electricityBatteryAsset.setEnergyLevelPercentageMin(20) + electricityBatteryAsset.setEnergyLevelPercentageMax(80) + electricityBatteryAsset.setEnergyLevel(100d) + electricityBatteryAsset.setPowerImportMax(7d) + electricityBatteryAsset.setPowerExportMax(20d) + electricityBatteryAsset.setPowerSetpoint(0d) + electricityBatteryAsset.setEfficiencyImport(95) + electricityBatteryAsset.setEfficiencyExport(98) + electricityBatteryAsset.setSupportsExport(true) + electricityBatteryAsset.setSupportsImport(true) + electricityBatteryAsset.getAttribute(ElectricityAsset.POWER_SETPOINT).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricityBatteryAsset = assetStorageService.merge(electricityBatteryAsset) + electricityBatteryAssetId = electricityBatteryAsset.getId() + + ElectricitySupplierAsset electricitySupplierAsset = new ElectricitySupplierAsset("Supplier") + electricitySupplierAsset.setParent(electricityOptimisationAsset) + electricitySupplierAsset.setTariffExport(-0.05) + electricitySupplierAsset.setTariffImport(0.08) + electricitySupplierAsset.getAttribute(ElectricityAsset.TARIFF_IMPORT).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricitySupplierAsset.getAttribute(ElectricityAsset.TARIFF_EXPORT).ifPresent(attr -> + attr.addMeta(new MetaItem<>(HAS_PREDICTED_DATA_POINTS)) + ) + electricitySupplierAsset = assetStorageService.merge(electricitySupplierAsset) + electricitySupplierAssetId = electricitySupplierAsset.getId() + } +} diff --git a/energy/src/test/groovy/org/openremote/extension/energy/TestSetupTasks.groovy b/energy/src/test/groovy/org/openremote/extension/energy/TestSetupTasks.groovy new file mode 100644 index 0000000..da8e1de --- /dev/null +++ b/energy/src/test/groovy/org/openremote/extension/energy/TestSetupTasks.groovy @@ -0,0 +1,13 @@ +package org.openremote.extension.energy + +import org.openremote.model.Container +import org.openremote.model.setup.Setup +import org.openremote.model.setup.SetupTasks + +class TestSetupTasks implements SetupTasks { + + @Override + List createTasks(Container container, String setupType, boolean keycloakEnabled) { + return List.of(new KeycloakTestSetup(container), new ManagerTestSetup(container)) + } +} diff --git a/energy/src/test/resources/META-INF/services/org.openremote.model.setup.SetupTasks b/energy/src/test/resources/META-INF/services/org.openremote.model.setup.SetupTasks new file mode 100644 index 0000000..f97023d --- /dev/null +++ b/energy/src/test/resources/META-INF/services/org.openremote.model.setup.SetupTasks @@ -0,0 +1 @@ +org.openremote.extension.energy.TestSetupTasks diff --git a/entsoe/build.gradle b/entsoe/build.gradle index d86e579..4e20ad8 100644 --- a/entsoe/build.gradle +++ b/entsoe/build.gradle @@ -85,3 +85,12 @@ signing { sign publishing.publications.maven } } + +tasks.register("copyExtension", Copy) { + from jar.archiveFile + into project(":deployment").layout.buildDirectory.dir("extensions") +} + +tasks.named('build') { + dependsOn('copyExtension') +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index db4133a..860b552 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,10 +13,11 @@ releasesRepoUrl=https://ossrh-staging-api.central.sonatype.com/service/local/ projectName = openremote-extensions -openremoteVersion = 1.22.1 +openremoteVersion = 1.23.1 bouncyCastleVersion = 1.81 jacksonVersion = 2.21.2 resteasyVersion = 6.2.15.Final shapeshifterVersion = 3.2.2 testLoggerVersion = 4.0.0 +camelVersion= 4.20.0 From d311435205df281c03a727360d617e31a2714130 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 12:03:20 +0200 Subject: [PATCH 02/35] wip --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index a57df02..2abbe7d 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -69,7 +69,7 @@ jobs: platforms: linux/amd64,linux/aarch64 load: false push: false - tags: openremote/manager:${{ inputs.tag }} + tags: openremote/manager:extensions build-args: | GIT_COMMIT=${{ github.sha }} From 01c8948b1eff2aefe0b15b8f42789de159c83431 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 13:27:05 +0200 Subject: [PATCH 03/35] wip --- .github/workflows/ci_cd.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 2abbe7d..dd195fa 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -47,9 +47,8 @@ jobs: - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} - run: | - ./gradlew build timeout-minutes: 20 + run: ./gradlew build - name: Install QEMU #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} @@ -61,17 +60,28 @@ jobs: #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build Manager image + - name: Build manager image #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + timeout-minutes: 20 with: context: deployment/build - platforms: linux/amd64,linux/aarch64 - load: false + platforms: linux/aarch64 + load: true push: false tags: openremote/manager:extensions + outputs: type=docker,dest=or_manager.tar build-args: | GIT_COMMIT=${{ github.sha }} + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false + + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: or_manager + path: or_manager.tar - name: Run build and publish if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} From 938e98a185742ca132c834c9f98e3c760f8819e9 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 13:43:38 +0200 Subject: [PATCH 04/35] wip --- deployment/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile b/deployment/Dockerfile index 0764ed8..6c15542 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -1,4 +1,4 @@ FROM openremote/manager:latest RUN mkdir -p /deployment/manager/extensions -ADD extensions /deployment \ No newline at end of file +ADD extensions /deployment/manager/extensions \ No newline at end of file From fedf5766ac513011c04c9abbfb8c12adeef26b1f Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 13:54:13 +0200 Subject: [PATCH 05/35] wip --- .github/workflows/ci_cd.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index dd195fa..67ce6d3 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -50,39 +50,39 @@ jobs: timeout-minutes: 20 run: ./gradlew build + - name: Login to DockerHub + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + #if: ${{ success() && steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Install QEMU - #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: linux/amd64,linux/aarch64 - name: Install Buildx - #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build manager image - #if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 timeout-minutes: 20 with: context: deployment/build - platforms: linux/aarch64 - load: true - push: false - tags: openremote/manager:extensions - outputs: type=docker,dest=or_manager.tar + platforms: linux/amd64,linux/aarch64 + load: false + push: true + tags: openremote/manager:${{ inputs.tag }} build-args: | GIT_COMMIT=${{ github.sha }} env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false - - name: Upload artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: or_manager - path: or_manager.tar - - name: Run build and publish if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} run: | From 3f0e0fb5b0b6586deada274b62d55367efba6f93 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 13:58:26 +0200 Subject: [PATCH 06/35] wip --- .github/workflows/ci_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 67ce6d3..bda8b2f 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -54,8 +54,8 @@ jobs: uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 #if: ${{ success() && steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + username: ${{ secrets._TEMP_DOCKERHUB_USER }} + password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }} - name: Install QEMU if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} From 7382fe1a4007205bc64e20aa0f54c12d9b305eb8 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:10:20 +0200 Subject: [PATCH 07/35] Update build and push step --- .github/workflows/ci_cd.yml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index bda8b2f..d10988a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -52,7 +52,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - #if: ${{ success() && steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ success() && steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} with: username: ${{ secrets._TEMP_DOCKERHUB_USER }} password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }} @@ -85,22 +85,13 @@ jobs: - name: Run build and publish if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} - run: | - echo "$KEYSTORE_BASE64" | base64 -d > keystore - ./gradlew build publishToSonatype closeAndReleaseSonatypeStagingRepository \ - -PsigningKey=$MAVEN_SIGNING_KEY -PsigningPassword=$MAVEN_SIGNING_PASSWORD -PpublishUsername=$MAVEN_USERNAME -PpublishPassword=$MAVEN_PASSWORD \ - -PkeystoreKeyAlias=$KEYSTORE_KEY_ALIAS -PkeystoreKeyPassword=$KEYSTORE_KEY_PASSWORD -PkeystoreFile=$PWD/keystore -PkeystorePassword=$KEYSTORE_PASSWORD - env: - MAVEN_SIGNING_KEY: ${{ secrets._TEMP_MAVEN_SIGNING_KEY || secrets.MAVEN_SIGNING_KEY }} - MAVEN_SIGNING_PASSWORD: ${{ secrets._TEMP_MAVEN_SIGNING_PASSWORD || secrets.MAVEN_SIGNING_PASSWORD }} - MAVEN_USERNAME: ${{ secrets._TEMP_MAVEN_USERNAME || secrets.MAVEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets._TEMP_MAVEN_PASSWORD || secrets.MAVEN_PASSWORD }} - KEYSTORE_BASE64: | - ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_KEYSTORE_KEY_ALIAS }} - KEYSTORE_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }} timeout-minutes: 20 + run: ./gradlew build publishToSonatype closeAndReleaseSonatypeStagingRepository -PsigningKey=$MAVEN_SIGNING_KEY -PsigningPassword=$MAVEN_SIGNING_PASSWORD -PpublishUsername=$MAVEN_USERNAME -PpublishPassword=$MAVEN_PASSWORD + env: + MAVEN_SIGNING_KEY: ${{ secrets._TEMP_MAVEN_SIGNING_KEY }} + MAVEN_SIGNING_PASSWORD: ${{ secrets._TEMP_MAVEN_SIGNING_PASSWORD }} + MAVEN_USERNAME: ${{ secrets._TEMP_MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets._TEMP_MAVEN_PASSWORD }} - name: Archive backend test results if: always() From 7a4760c23e86db6e637913f8d3dd928a630720df Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:13:30 +0200 Subject: [PATCH 08/35] enable entsoe-e tests --- .../extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy index 531fc4c..1945cd5 100644 --- a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy +++ b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy @@ -50,7 +50,6 @@ import static org.openremote.model.Constants.MASTER_REALM import static org.openremote.model.value.MetaItemType.AGENT_LINK import static org.openremote.model.value.ValueType.NUMBER -@IgnoreIf({ System.getenv("GITHUB_ACTIONS") == "true" }) @Issue("https://github.com/openremote/openremote/issues/2599") class EntsoeProtocolTest extends Specification implements ManagerContainerTrait { private static final String DATASET_START = "2026-02-16T23:00:00.000Z" From 9927b734b578deabfeee0336dbaabc68033c0219 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:29:41 +0200 Subject: [PATCH 09/35] enable energy tests --- .../extension/energy/EnergyOptimisationAssetTest.groovy | 1 - .../openremote/extension/energy/ForecastSolarServiceTest.groovy | 1 - .../openremote/extension/energy/ForecastWindServiceTest.groovy | 1 - 3 files changed, 3 deletions(-) diff --git a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy index 55c41d9..75cc56a 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy @@ -51,7 +51,6 @@ import static spock.util.matcher.HamcrestMatchers.closeTo * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -@Ignore class EnergyOptimisationAssetTest extends Specification implements ManagerContainerTrait { def "Test storage asset with consumer and producer"() { diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy index 74077dd..f608a3b 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy @@ -26,7 +26,6 @@ import java.time.temporal.ChronoUnit import static org.openremote.extension.energy.manager.ForecastSolarService.OR_FORECAST_SOLAR_API_KEY -@Ignore class ForecastSolarServiceTest extends Specification implements ManagerContainerTrait { @Shared diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy index fd4ec0c..87ea952 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy @@ -28,7 +28,6 @@ import java.time.temporal.ChronoUnit import static org.openremote.extension.energy.manager.ForecastWindService.OR_OPEN_WEATHER_API_APP_ID -@Ignore class ForecastWindServiceTest extends Specification implements ManagerContainerTrait { @Shared From 3cc3642421f5f1b15a2c6a455e3f9a61627038ee Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:47:31 +0200 Subject: [PATCH 10/35] add dev-testing --- .github/workflows/ci_cd.yml | 13 +++++++++++++ .../energy/ForecastSolarServiceTest.groovy | 1 - .../extension/energy/ForecastWindServiceTest.groovy | 1 - 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d10988a..e37362c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -45,6 +45,19 @@ jobs: echo "value=true" >> $GITHUB_OUTPUT fi + - name: Run dev-testing for tests + timeout-minutes: 20 + run: | + # Run dev-testing for tests + # Make temp dir with 777 mask as docker seems to run as root + mkdir -pm 777 tmp + + ## Download dev-testing docker-compose file + curl -o dev-testing.yml https://raw.githubusercontent.com/openremote/openremote/refs/heads/master/profile/dev-testing.yml + + # Start the stack + docker compose -f profile/dev-testing.yml up -d --no-build + - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} timeout-minutes: 20 diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy index f608a3b..3d5949a 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy @@ -12,7 +12,6 @@ import org.openremote.model.attribute.AttributeRef import org.openremote.model.geo.GeoJSONPoint import org.openremote.model.util.ValueUtil import org.openremote.test.ManagerContainerTrait -import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy index 87ea952..1d11ba2 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy @@ -17,7 +17,6 @@ import org.openremote.model.attribute.AttributeRef import org.openremote.model.geo.GeoJSONPoint import org.openremote.model.util.ValueUtil import org.openremote.test.ManagerContainerTrait -import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions From ae57ca9e164caa31ce7789a6267672b2d4a6d03b Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:49:00 +0200 Subject: [PATCH 11/35] add dev-testing --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e37362c..00b469c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -56,7 +56,7 @@ jobs: curl -o dev-testing.yml https://raw.githubusercontent.com/openremote/openremote/refs/heads/master/profile/dev-testing.yml # Start the stack - docker compose -f profile/dev-testing.yml up -d --no-build + docker compose -f dev-testing.yml up -d --no-build - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} From e52bc63ee1f3e5f569547c897a96f9073cdbe078 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:57:41 +0200 Subject: [PATCH 12/35] add sparse checkout to main --- .github/workflows/ci_cd.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 00b469c..40542ad 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -45,6 +45,16 @@ jobs: echo "value=true" >> $GITHUB_OUTPUT fi + - name: Checkout main repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: openremote + path: openremote + ref: master + sparse-checkout: profile + sparse-checkout-cone-mode: false + + - name: Run dev-testing for tests timeout-minutes: 20 run: | @@ -52,11 +62,8 @@ jobs: # Make temp dir with 777 mask as docker seems to run as root mkdir -pm 777 tmp - ## Download dev-testing docker-compose file - curl -o dev-testing.yml https://raw.githubusercontent.com/openremote/openremote/refs/heads/master/profile/dev-testing.yml - # Start the stack - docker compose -f dev-testing.yml up -d --no-build + docker compose -f profile/dev-testing.yml up -d --no-build - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} From 3817dde69e0b946c81aa84f2c8686538d91af299 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 14:58:35 +0200 Subject: [PATCH 13/35] add sparse checkout to main --- .github/workflows/ci_cd.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 40542ad..04c298c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -30,6 +30,15 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout main repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: openremote/openremote + path: openremote + ref: master + sparse-checkout: profile + sparse-checkout-cone-mode: false + - name: Set up JDK 21 and gradle cache id: java uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -45,16 +54,6 @@ jobs: echo "value=true" >> $GITHUB_OUTPUT fi - - name: Checkout main repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: openremote - path: openremote - ref: master - sparse-checkout: profile - sparse-checkout-cone-mode: false - - - name: Run dev-testing for tests timeout-minutes: 20 run: | From fe6148b36140f9d615534e860b67282fb9296cf8 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 15:00:24 +0200 Subject: [PATCH 14/35] add sparse checkout to main --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 04c298c..9120165 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: openremote/openremote - path: openremote + path: . ref: master sparse-checkout: profile sparse-checkout-cone-mode: false From 4ba74e6985e14e92b4cba756c5683331513c6f5a Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 15:01:36 +0200 Subject: [PATCH 15/35] add sparse checkout to main --- .github/workflows/ci_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 9120165..d043b9b 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: openremote/openremote - path: . + path: openremote ref: master sparse-checkout: profile sparse-checkout-cone-mode: false @@ -62,7 +62,7 @@ jobs: mkdir -pm 777 tmp # Start the stack - docker compose -f profile/dev-testing.yml up -d --no-build + docker compose -f openremote/profile/dev-testing.yml up -d --no-build - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} From 54de549f0a1cff8d2e81e63f742cd3ca79ffd06a Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 15:16:40 +0200 Subject: [PATCH 16/35] add sparse checkout to main --- .github/workflows/ci_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d043b9b..b9c5c4a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -54,10 +54,10 @@ jobs: echo "value=true" >> $GITHUB_OUTPUT fi - - name: Run dev-testing for tests + - name: Run dev-testing profile timeout-minutes: 20 run: | - # Run dev-testing for tests + # Run dev-testing profile # Make temp dir with 777 mask as docker seems to run as root mkdir -pm 777 tmp From 79eea888e41cdbe66d7b0e7ca7e87a25fe43a514 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 15:33:04 +0200 Subject: [PATCH 17/35] wip --- .github/workflows/ci_cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index b9c5c4a..d866b23 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -63,6 +63,8 @@ jobs: # Start the stack docker compose -f openremote/profile/dev-testing.yml up -d --no-build + + ./gradlew entsoe:test - name: Run build if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} From d50771aa41a28f121a5d9a9105ab621f619131ff Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 15:37:18 +0200 Subject: [PATCH 18/35] wip --- .github/workflows/ci_cd.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d866b23..75f721e 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -54,22 +54,18 @@ jobs: echo "value=true" >> $GITHUB_OUTPUT fi - - name: Run dev-testing profile + - name: Run build + if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} timeout-minutes: 20 run: | - # Run dev-testing profile + # Run build # Make temp dir with 777 mask as docker seems to run as root mkdir -pm 777 tmp - # Start the stack + # Start the dev-testing stack docker compose -f openremote/profile/dev-testing.yml up -d --no-build - ./gradlew entsoe:test - - - name: Run build - if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} - timeout-minutes: 20 - run: ./gradlew build + ./gradlew build - name: Login to DockerHub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 From 258324a46a0a623011d12ba83dab23bdee8d839e Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:08:01 +0200 Subject: [PATCH 19/35] disable paralism --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 860b552..12930a6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx3g -org.gradle.parallel=true +org.gradle.parallel=false # Set the socket timeout to 5 minutes (good for proxies) systemProp.org.gradle.internal.http.socketTimeout=300000 From 9d44e12b7017756394fbf6a4868ff0273d0c5cec Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:14:05 +0200 Subject: [PATCH 20/35] ignore forecast tests --- .../openremote/extension/energy/ForecastSolarServiceTest.groovy | 2 ++ .../openremote/extension/energy/ForecastWindServiceTest.groovy | 2 ++ 2 files changed, 4 insertions(+) diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy index 3d5949a..74077dd 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastSolarServiceTest.groovy @@ -12,6 +12,7 @@ import org.openremote.model.attribute.AttributeRef import org.openremote.model.geo.GeoJSONPoint import org.openremote.model.util.ValueUtil import org.openremote.test.ManagerContainerTrait +import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions @@ -25,6 +26,7 @@ import java.time.temporal.ChronoUnit import static org.openremote.extension.energy.manager.ForecastSolarService.OR_FORECAST_SOLAR_API_KEY +@Ignore class ForecastSolarServiceTest extends Specification implements ManagerContainerTrait { @Shared diff --git a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy index 1d11ba2..d665082 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/ForecastWindServiceTest.groovy @@ -18,6 +18,7 @@ import org.openremote.model.geo.GeoJSONPoint import org.openremote.model.util.ValueUtil import org.openremote.test.ManagerContainerTrait import spock.lang.Shared +import spock.lang.Ignore import spock.lang.Specification import spock.util.concurrent.PollingConditions @@ -27,6 +28,7 @@ import java.time.temporal.ChronoUnit import static org.openremote.extension.energy.manager.ForecastWindService.OR_OPEN_WEATHER_API_APP_ID +@Ignore class ForecastWindServiceTest extends Specification implements ManagerContainerTrait { @Shared From 589f8f980224a3ed9ce237da416db111eada0500 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:17:39 +0200 Subject: [PATCH 21/35] ignore forecast tests --- .../extension/energy/EnergyOptimisationAssetTest.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy index 75cc56a..55c41d9 100644 --- a/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy +++ b/energy/src/test/groovy/org/openremote/extension/energy/EnergyOptimisationAssetTest.groovy @@ -51,6 +51,7 @@ import static spock.util.matcher.HamcrestMatchers.closeTo * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +@Ignore class EnergyOptimisationAssetTest extends Specification implements ManagerContainerTrait { def "Test storage asset with consumer and producer"() { From d077aa39dbe049fd709a193b074ca18f645aca5d Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:19:52 +0200 Subject: [PATCH 22/35] wip --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 75f721e..6367eff 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 20 run: | # Run build - # Make temp dir with 777 mask as docker seems to run as root + # Make temp dir with 777 maesk as docker seems to run as root mkdir -pm 777 tmp # Start the dev-testing stack From f8db27ba8c157d62aa4dee1f474af536b76daa5d Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:27:27 +0200 Subject: [PATCH 23/35] wip --- .../extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy index 1945cd5..885fba4 100644 --- a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy +++ b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy @@ -34,6 +34,7 @@ import org.openremote.model.attribute.AttributeRef import org.openremote.model.attribute.MetaItem import org.openremote.model.datapoint.ValueDatapoint import org.openremote.test.ManagerContainerTrait +import spock.lang.Ignore import spock.lang.IgnoreIf import spock.lang.Issue import spock.lang.Shared @@ -983,6 +984,7 @@ class EntsoeProtocolTest extends Specification implements ManagerContainerTrait closeClient() } + @Ignore def "ENTSO-E integration test supports multiple periods in a single timeseries"() { given: "the container environment is started" requestCountByZone.clear() From 94314bcaa16327276766e3e7cd9202e45b9fffbe Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Tue, 19 May 2026 16:36:37 +0200 Subject: [PATCH 24/35] wip --- .github/workflows/ci_cd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 6367eff..3794ab3 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 20 run: | # Run build - # Make temp dir with 777 maesk as docker seems to run as root + # Make temp dir with 777 mask as docker seems to run as root mkdir -pm 777 tmp # Start the dev-testing stack @@ -100,10 +100,10 @@ jobs: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false - - name: Run build and publish + - name: Publish to Maven Sonatype if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} timeout-minutes: 20 - run: ./gradlew build publishToSonatype closeAndReleaseSonatypeStagingRepository -PsigningKey=$MAVEN_SIGNING_KEY -PsigningPassword=$MAVEN_SIGNING_PASSWORD -PpublishUsername=$MAVEN_USERNAME -PpublishPassword=$MAVEN_PASSWORD + run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -PsigningKey=$MAVEN_SIGNING_KEY -PsigningPassword=$MAVEN_SIGNING_PASSWORD -PpublishUsername=$MAVEN_USERNAME -PpublishPassword=$MAVEN_PASSWORD env: MAVEN_SIGNING_KEY: ${{ secrets._TEMP_MAVEN_SIGNING_KEY }} MAVEN_SIGNING_PASSWORD: ${{ secrets._TEMP_MAVEN_SIGNING_PASSWORD }} From 4841e684b9e8462b6bb361567431d722862a6fb9 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 12:14:10 +0200 Subject: [PATCH 25/35] add support for develop image --- .github/workflows/ci_cd.yml | 22 ++++++++++++++++++++-- .github/workflows/release.yml | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 3794ab3..e11598d 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -84,8 +84,24 @@ jobs: if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build develop image + if: ${{ steps.is_main_repo.outputs.value && github.event_name == 'push' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + timeout-minutes: 20 + with: + context: deployment/build + platforms: linux/amd64,linux/aarch64 + load: false + push: true + tags: openremote/manager:develop + build-args: | + GIT_COMMIT=${{ github.sha }} + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false + - name: Build manager image - if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name == 'release' }} uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 timeout-minutes: 20 with: @@ -93,7 +109,9 @@ jobs: platforms: linux/amd64,linux/aarch64 load: false push: true - tags: openremote/manager:${{ inputs.tag }} + tags: | + openremote/manager:${{ inputs.tag }} + openremote/manager:latest build-args: | GIT_COMMIT=${{ github.sha }} env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd75784..2e0d293 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,9 +37,9 @@ jobs: - name: Set up JDK 21 and gradle cache uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: - distribution: 'temurin' - java-version: '21' - cache: 'gradle' + distribution: "temurin" + java-version: "21" + cache: "gradle" - name: Run release id: release From 001e5b7b7cd9e9bb6c1d332e1ede8283941c7087 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 13:26:44 +0200 Subject: [PATCH 26/35] several fixes --- .github/workflows/ci_cd.yml | 9 +++++---- .../entsoe/agent/protocol/EntsoeProtocolTest.groovy | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e11598d..3d0202b 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -16,6 +16,9 @@ on: # Manual trigger workflow_dispatch: + # Release + release: + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -55,7 +58,6 @@ jobs: fi - name: Run build - if: ${{ !steps.is_main_repo.outputs.value || github.event_name == 'pull_request' }} timeout-minutes: 20 run: | # Run build @@ -75,13 +77,11 @@ jobs: password: ${{ secrets._TEMP_DOCKERHUB_PASSWORD }} - name: Install QEMU - if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: linux/amd64,linux/aarch64 - name: Install Buildx - if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build develop image @@ -119,7 +119,7 @@ jobs: DOCKER_BUILD_RECORD_UPLOAD: false - name: Publish to Maven Sonatype - if: ${{ steps.is_main_repo.outputs.value && github.event_name != 'pull_request' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name == 'release' }} timeout-minutes: 20 run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -PsigningKey=$MAVEN_SIGNING_KEY -PsigningPassword=$MAVEN_SIGNING_PASSWORD -PpublishUsername=$MAVEN_USERNAME -PpublishPassword=$MAVEN_PASSWORD env: @@ -136,6 +136,7 @@ jobs: path: test/build/reports/tests - name: Archive coverage report + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-report diff --git a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy index 885fba4..9ad983c 100644 --- a/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy +++ b/entsoe/src/test/groovy/org/openremote/extension/entsoe/agent/protocol/EntsoeProtocolTest.groovy @@ -35,7 +35,6 @@ import org.openremote.model.attribute.MetaItem import org.openremote.model.datapoint.ValueDatapoint import org.openremote.test.ManagerContainerTrait import spock.lang.Ignore -import spock.lang.IgnoreIf import spock.lang.Issue import spock.lang.Shared import spock.lang.Specification From e44e6738f8821a45357e974ad0e094e1ba35b0ad Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 13:34:52 +0200 Subject: [PATCH 27/35] fix test results --- .github/workflows/ci_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 3d0202b..ac29bb6 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -133,7 +133,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: backend-test-results - path: test/build/reports/tests + path: '**/build/reports/tests' - name: Archive coverage report if: always() From 06fef51161f2ff4642d865a28c8b68d9c95d657a Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 14:49:18 +0200 Subject: [PATCH 28/35] wip --- .github/workflows/ci_cd.yml | 2 +- build.gradle | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index ac29bb6..1ccc3ba 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -140,4 +140,4 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-report - path: test/build/reports/jacoco/test/html + path: '**/build/reports/jacoco/test/html' diff --git a/build.gradle b/build.gradle index 30e0dc7..cbe69f0 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,26 @@ allprojects { version = scmVersion.version } +subprojects { + pluginManager.withPlugin('java') { + apply plugin: 'jacoco' + jacoco { toolVersion = "0.8.14" } + test { + finalizedBy jacocoTestReport // report is always generated after tests run + } + jacocoTestReport { + dependsOn test // tests are required to run before generating the report + } + } + + tasks.withType(JacocoReport) { + reports { + xml.required = true + html.required = true + } + } +} + apply plugin: 'io.github.gradle-nexus.publish-plugin' nexusPublishing { repositories { From dd16f0f1398778779ce28ad8fa276d6c52b90517 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 15:41:37 +0200 Subject: [PATCH 29/35] several fixes --- .github/workflows/ci_cd.yml | 14 ++-- .github/workflows/release.yml | 65 ------------------- demo-setup/build.gradle | 8 +-- energy/build.gradle | 8 +-- .../energy/manager/ForecastWindService.java | 6 +- 5 files changed, 18 insertions(+), 83 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 1ccc3ba..ec460f6 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -79,18 +79,18 @@ jobs: - name: Install QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: - platforms: linux/amd64,linux/aarch64 + platforms: linux/amd64,linux/arm64 - name: Install Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build develop image - if: ${{ steps.is_main_repo.outputs.value && github.event_name == 'push' }} + if: ${{ steps.is_main_repo.outputs.value && github.event_name == 'push' && github.ref == 'refs/heads/main' }} uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 timeout-minutes: 20 with: context: deployment/build - platforms: linux/amd64,linux/aarch64 + platforms: linux/amd64,linux/arm64 load: false push: true tags: openremote/manager:develop @@ -106,11 +106,11 @@ jobs: timeout-minutes: 20 with: context: deployment/build - platforms: linux/amd64,linux/aarch64 + platforms: linux/amd64,linux/arm64 load: false push: true tags: | - openremote/manager:${{ inputs.tag }} + openremote/manager:${{ github.ref_name }} openremote/manager:latest build-args: | GIT_COMMIT=${{ github.sha }} @@ -128,11 +128,11 @@ jobs: MAVEN_USERNAME: ${{ secrets._TEMP_MAVEN_USERNAME }} MAVEN_PASSWORD: ${{ secrets._TEMP_MAVEN_PASSWORD }} - - name: Archive backend test results + - name: Archive test results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: backend-test-results + name: test-results path: '**/build/reports/tests' - name: Archive coverage report diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 2e0d293..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Release - -on: - # Manual trigger - workflow_dispatch: - inputs: - VERSION_INCREMENT: - description: 'The version number part to increment (major.minor.patch)' - default: Minor - type: choice - options: - - Major - - Minor - - Patch - required: false - VERSION_OVERRIDE: - description: 'Version override (when not incrementing the previous version)' - type: string - required: false - - -permissions: - actions: write - contents: write - -jobs: - - build: - name: Release - runs-on: ubuntu-latest - - steps: - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 21 and gradle cache - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: "temurin" - java-version: "21" - cache: "gradle" - - - name: Run release - id: release - run: | - if [ -n "$VERSION_OVERRIDE" ]; then - gradle release -Prelease.forceVersion=${VERSION_OVERRIDE} - else - gradle release -Prelease.versionIncrementer=increment${VERSION_INCREMENT} - fi - env: - VERSION_INCREMENT: ${{ github.event.inputs.VERSION_INCREMENT }} - VERSION_OVERRIDE: ${{ github.event.inputs.VERSION_OVERRIDE }} - - # When the 'github.token' is used events are not generated to prevent users from accidentally creating recursive workflow runs. - # See: https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow - - name: Create release - run: | - git push --follow-tags - gh workflow run ci_cd.yml --ref $RELEASED_VERSION - gh release create $RELEASED_VERSION --generate-notes - env: - GH_TOKEN: ${{ github.token }} - RELEASED_VERSION: ${{ steps.release.outputs.released-version }} diff --git a/demo-setup/build.gradle b/demo-setup/build.gradle index 53b8ca5..8eb7e28 100644 --- a/demo-setup/build.gradle +++ b/demo-setup/build.gradle @@ -33,7 +33,7 @@ publishing { pom { name = 'OpenRemote Demo setup' description = 'Adds the OpenRemote Demo setup' - url = 'https://github.com/openremote/openremote' + url = 'https://github.com/openremote/extensions' licenses { license { name = 'GNU Affero General Public License v3.0' @@ -50,9 +50,9 @@ publishing { } } scm { - connection = 'scm:git:git://github.com/openremote/openremote.git' - developerConnection = 'scm:git:ssh://github.com:openremote/openremote.git' - url = 'https://github.com/openremote/openremote/tree/master' + connection = 'scm:git:git://github.com/openremote/extensions.git' + developerConnection = 'scm:git:ssh://github.com:openremote/extensions.git' + url = 'https://github.com/openremote/extensions/tree/master' } } } diff --git a/energy/build.gradle b/energy/build.gradle index 9e08907..2cfa60f 100644 --- a/energy/build.gradle +++ b/energy/build.gradle @@ -39,7 +39,7 @@ publishing { pom { name = 'OpenRemote energy extension' description = 'Adds the energy domain extension' - url = 'https://github.com/openremote/openremote' + url = 'https://github.com/openremote/extensions' licenses { license { name = 'GNU Affero General Public License v3.0' @@ -56,9 +56,9 @@ publishing { } } scm { - connection = 'scm:git:git://github.com/openremote/openremote.git' - developerConnection = 'scm:git:ssh://github.com:openremote/openremote.git' - url = 'https://github.com/openremote/openremote/tree/master' + connection = 'scm:git:git://github.com/openremote/extensions.git' + developerConnection = 'scm:git:ssh://github.com:openremote/extensions.git' + url = 'https://github.com/openremote/extensions/tree/master' } } } 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..9265b10 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 @@ -69,7 +69,7 @@ protected static class WeatherForecastModel { protected long timestamp; @JsonProperty("temp") - protected double tempature; + protected double temperature; @JsonProperty("humidity") protected int humidity; @@ -87,8 +87,8 @@ public long getTimestamp() { return timestamp * 1000; } - public double getTempature() { - return tempature; + public double getTemperature() { + return temperature; } public int getHumidity() { From e9b718d738993aea4ec9b6b1c66dd8d11ce1b6e8 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 16:32:30 +0200 Subject: [PATCH 30/35] fix --- .github/workflows/ci_cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index ec460f6..f6a7006 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -18,6 +18,8 @@ on: # Release release: + types: + - published concurrency: group: ${{ github.workflow }}-${{ github.ref }} From ea41afe5febd5b7b034fe78530dd801cfb434b3d Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 16:42:18 +0200 Subject: [PATCH 31/35] fix --- .github/workflows/ci_cd.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index f6a7006..ed6c075 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -16,11 +16,6 @@ on: # Manual trigger workflow_dispatch: - # Release - release: - types: - - published - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From da795c1dc0f228eec7a473a239fffce464db4a9c Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 16:43:55 +0200 Subject: [PATCH 32/35] fix --- deployment/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile b/deployment/Dockerfile index 6c15542..6375779 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -1,4 +1,4 @@ -FROM openremote/manager:latest +FROM openremote/manager:1.23.1-core RUN mkdir -p /deployment/manager/extensions ADD extensions /deployment/manager/extensions \ No newline at end of file From f88da4827d0cc0eff5d2efe0ac29fcbb1b4131e9 Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 20 May 2026 16:46:30 +0200 Subject: [PATCH 33/35] fix --- deployment/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile b/deployment/Dockerfile index 6375779..31af98a 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -1,4 +1,4 @@ -FROM openremote/manager:1.23.1-core +FROM openremote/manager:latest-core RUN mkdir -p /deployment/manager/extensions ADD extensions /deployment/manager/extensions \ No newline at end of file From 3c4ec9d6f6274b721f38cab494ac814867fd590c Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 27 May 2026 09:57:46 +0200 Subject: [PATCH 34/35] Exclude EMS extension from image --- ems/build.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ems/build.gradle b/ems/build.gradle index 8ca4397..6ad789a 100644 --- a/ems/build.gradle +++ b/ems/build.gradle @@ -83,13 +83,4 @@ signing { useInMemoryPgpKeys(signingKey, signingPassword) sign publishing.publications.maven } -} - -tasks.register("copyExtension", Copy) { - from jar.archiveFile - into project(":deployment").layout.buildDirectory.dir("extensions") -} - -tasks.named('build') { - dependsOn('copyExtension') } \ No newline at end of file From 3c9e7838d5c2ff83e009dec8e31157b5b3c5b98b Mon Sep 17 00:00:00 2001 From: Dennis Kuijs Date: Wed, 27 May 2026 09:58:46 +0200 Subject: [PATCH 35/35] Don't push images to DockerHub --- .github/workflows/ci_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index ed6c075..7b948aa 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -89,7 +89,7 @@ jobs: context: deployment/build platforms: linux/amd64,linux/arm64 load: false - push: true + push: false tags: openremote/manager:develop build-args: | GIT_COMMIT=${{ github.sha }} @@ -105,7 +105,7 @@ jobs: context: deployment/build platforms: linux/amd64,linux/arm64 load: false - push: true + push: false tags: | openremote/manager:${{ github.ref_name }} openremote/manager:latest