diff --git a/demo-setup/build.gradle b/demo-setup/build.gradle
new file mode 100644
index 0000000..3a02a8d
--- /dev/null
+++ b/demo-setup/build.gradle
@@ -0,0 +1,82 @@
+apply plugin: "java-library"
+apply plugin: "maven-publish"
+apply plugin: "signing"
+
+base {
+ archivesName = "openremote-${project.name}"
+}
+
+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}"
+ from components.java
+ pom {
+ name = 'OpenRemote Demo setup'
+ description = 'Adds the OpenRemote Demo setup'
+ url = 'https://github.com/openremote/extensions'
+ 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/extensions.git'
+ developerConnection = 'scm:git:ssh://github.com:openremote/extensions.git'
+ url = 'https://github.com/openremote/extensions/tree/main'
+ }
+ }
+ }
+ }
+
+ 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
+ }
+}
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..ec88bfc
--- /dev/null
+++ b/demo-setup/src/main/java/org/openremote/extension/demosetup/ManagerDemoSetup.java
@@ -0,0 +1,2388 @@
+/*
+ * 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.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 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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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).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/energy/build.gradle b/energy/build.gradle
new file mode 100644
index 0000000..69e2ca9
--- /dev/null
+++ b/energy/build.gradle
@@ -0,0 +1,86 @@
+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/extensions'
+ 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/extensions.git'
+ developerConnection = 'scm:git:ssh://github.com:openremote/extensions.git'
+ url = 'https://github.com/openremote/extensions/tree/main'
+ }
+ }
+ }
+ }
+
+ 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
+ }
+}
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..3fa9861
--- /dev/null
+++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastSolarService.java
@@ -0,0 +1,434 @@
+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.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
+import org.openremote.container.message.MessageBrokerService;
+import org.openremote.container.timer.TimerService;
+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.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.concurrent.atomic.AtomicReference;
+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.model.util.MapAccess.getString;
+import static org.openremote.container.web.WebTargetBuilder.createClient;
+import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
+import static org.openremote.model.syslog.SyslogCategory.DATA;
+
+/**
+ * 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 static final AtomicReference resteasyClient = new AtomicReference<>();
+ 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;
+ }
+
+ initClient();
+
+ forecastSolarTarget = resteasyClient.get()
+ .target("https://api.forecast.solar/" + forecastSolarApiKey + "/estimate");
+
+ 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 {
+ scheduledExecutorService.shutdown();
+ }
+
+ protected static void initClient() {
+ synchronized (resteasyClient) {
+ if (resteasyClient.get() == null) {
+ resteasyClient.set(createClient(org.openremote.container.Container.EXECUTOR));
+ }
+ }
+ }
+
+ 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..e578e57
--- /dev/null
+++ b/energy/src/main/java/org/openremote/extension/energy/manager/ForecastWindService.java
@@ -0,0 +1,354 @@
+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.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
+import org.openremote.container.message.MessageBrokerService;
+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.concurrent.atomic.AtomicReference;
+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.model.util.MapAccess.getString;
+import static org.openremote.container.web.WebTargetBuilder.createClient;
+import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
+import static org.openremote.model.syslog.SyslogCategory.DATA;
+
+/**
+ * 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 static final AtomicReference resteasyClient = new AtomicReference<>();
+ 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;
+ }
+
+ initClient();
+
+ weatherForecastWebTarget = resteasyClient.get()
+ .target("https://api.openweathermap.org/data/2.5")
+ .queryParam("units", "metric")
+ .queryParam("exclude", "minutely,daily,alerts")
+ .queryParam("appid", openWeatherAppId);
+
+ 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 static void initClient() {
+ synchronized (resteasyClient) {
+ if (resteasyClient.get() == null) {
+ resteasyClient.set(createClient(org.openremote.container.Container.SCHEDULED_EXECUTOR));
+ }
+ }
+ }
+
+ 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