From da7a7f35fc9f4a8e051c0bd607d8ef4bb5f0d33f Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Mon, 5 Oct 2026 09:38:59 +0200 Subject: [PATCH 1/3] Advanced EMS setting: Add margin to power consumption and production forecasts --- .../EmsOptimisationBeta.java | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java index 9f00b6f..b74e585 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java @@ -52,6 +52,7 @@ public class EmsOptimisationBeta implements OptimisationMethod { // Default EMS power limit settings private final int POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT = 10; + private final int POWER_UNCERTAINTY_MARGIN_PERCENTAGE_DEFAULT = 2; // Default battery settings private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; @@ -63,10 +64,16 @@ public class EmsOptimisationBeta implements OptimisationMethod { "powerLimitMaximumFluctuationMargin"; private final String POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = "powerLimitMinimumFluctuationMargin"; + private final String POWER_CONSUMPTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME = + "powerConsumptionUncertaintyMargin"; + private final String POWER_PRODUCTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME = + "powerProductionUncertaintyMargin"; private final String[][] advancedSettingsAttributesInfo = { {POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()}, - {POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()} + {POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()}, + {POWER_CONSUMPTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME, ValueType.NUMBER.getName()}, + {POWER_PRODUCTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME, ValueType.NUMBER.getName()} }; @Override @@ -419,8 +426,8 @@ private Map batteryCalculateForecasts( Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); } - // List to store the sum of total power consumption, production and flexible - List totalPowerConsumptionProductionFlexibleList = new ArrayList<>(); + // List to store the sum of total power consumption and production + List totalPowerConsumptionProductionList = new ArrayList<>(); // Sum total power consumption and production for (int i = 0; i < numberOfTimestamps; i++) { @@ -429,16 +436,27 @@ private Map batteryCalculateForecasts( if (powerProduction != null) { double sum = powerConsumption + powerProduction; - totalPowerConsumptionProductionFlexibleList.add(sum); + totalPowerConsumptionProductionList.add(sum); } else { - totalPowerConsumptionProductionFlexibleList.add(powerConsumption); + totalPowerConsumptionProductionList.add(powerConsumption); } } infoStr.append( String.format( - "totalPowerConsumptionProductionFlexibleList = %s \n", - totalPowerConsumptionProductionFlexibleList)); + "totalPowerConsumptionProductionList = %s \n", totalPowerConsumptionProductionList)); + + // List to store the sum of total power consumption, production and flexible + List totalPowerConsumptionProductionFlexibleList = new ArrayList<>(); + + // Add a margin to the summed consumption-production forecast. This accounts for possible + // underestimation of the forecast, which could lead to premature battery depletion + for (Double power : totalPowerConsumptionProductionList) { + double uncertaintyMargin = calculateForecastUncertaintyMargin(energyOptimisationAsset, power); + double powerPlusMargin = power + uncertaintyMargin; + + totalPowerConsumptionProductionFlexibleList.add(powerPlusMargin); + } // Get power limit forecasts List> energyOptimisationPowerLimitMaximumPredicted = @@ -456,15 +474,24 @@ private Map batteryCalculateForecasts( EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); - Map powerLimitMaximumMap = new HashMap<>(); - Map powerLimitMinimumMap = new HashMap<>(); + // Calculate virtual power limit forecast + List powerLimitMaximumVirtualList = new ArrayList<>(); + List powerLimitMinimumVirtualList = new ArrayList<>(); for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { - powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + Double powerLimitMaximum = (Double) dp.getValue(); + Double powerLimitMaximumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); + + powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); } for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { - powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + Double powerLimitMinimum = (Double) dp.getValue(); + Double powerLimitMinimumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); + + powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); } List chargePowerAvailableTotalList = new ArrayList<>(); @@ -516,24 +543,12 @@ private Map batteryCalculateForecasts( continue; } - List powerLimitMaximumVirtualList = new ArrayList<>(); - List powerLimitMinimumVirtualList = new ArrayList<>(); - // Calculate total available charge and discharge power for (int i = 0; i < numberOfTimestamps; i++) { - long timestampMillis = timestampsMillisList.get(i); double totalPower = totalPowerConsumptionProductionFlexibleList.get(i); - Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); - Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); - - Double powerLimitMaximumVirtual = - calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); - Double powerLimitMinimumVirtual = - calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); - - powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); - powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); + Double powerLimitMaximumVirtual = powerLimitMaximumVirtualList.get(i); + Double powerLimitMinimumVirtual = powerLimitMinimumVirtualList.get(i); if (powerLimitMaximumVirtual != null) { double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; @@ -571,21 +586,6 @@ private Map batteryCalculateForecasts( // System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); // System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); - List chargePowerAvailableBatteryList = - chargePowerAvailableTotalList.stream() - .map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0) - .toList(); - List dischargePowerAvailableBatteryList = - dischargePowerAvailableTotalList.stream() - .map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0) - .toList(); - - // System.out.println("chargeAvailableBatteryList = " + - // chargePowerAvailableBatteryList); - // System.out.println("dischargeAvailableBatteryList = " + - // dischargePowerAvailableBatteryList); - // System.out.println(); - // Convert power to energy level percentage List chargePercentageNeededTotalList = new ArrayList<>(); List dischargePercentageNeededTotalList = new ArrayList<>(); @@ -610,6 +610,21 @@ private Map batteryCalculateForecasts( // System.out.println("dischargePercentageNeededTotalList = " + // dischargePercentageNeededTotalList); + List chargePowerAvailableBatteryList = + chargePowerAvailableTotalList.stream() + .map(x -> x > 0 ? Math.min(x, chargePowerMaximumBattery) : 0) + .toList(); + List dischargePowerAvailableBatteryList = + dischargePowerAvailableTotalList.stream() + .map(x -> x < 0 ? Math.max(x, dischargePowerMaximumBattery) : 0) + .toList(); + + // System.out.println("chargeAvailableBatteryList = " + + // chargePowerAvailableBatteryList); + // System.out.println("dischargeAvailableBatteryList = " + + // dischargePowerAvailableBatteryList); + // System.out.println(); + List chargePercentageAvailableBatteryList = new ArrayList<>(); List dischargePercentageAvailableBatteryList = new ArrayList<>(); @@ -2080,6 +2095,36 @@ private void batteryUpdatePowerSetpoints( } } + private double calculateForecastUncertaintyMargin( + EmsEnergyOptimisationAsset energyOptimisationAsset, Double power) { + if (power == null) { + return 0.0; + } + + String uncertaintyMarginAttributeName = POWER_CONSUMPTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME; + + if (power < 0.0) { + uncertaintyMarginAttributeName = POWER_PRODUCTION_UNCERTAINTY_MARGIN_ATTRIBUTE_NAME; + } + + Double uncertaintyMargin = + (Double) + energyOptimisationAsset + .getAttribute(uncertaintyMarginAttributeName) + .flatMap(Attribute::getValue) + .orElse(null); + + if (uncertaintyMargin == null) { + uncertaintyMargin = power * POWER_UNCERTAINTY_MARGIN_PERCENTAGE_DEFAULT * 0.01; + + if (uncertaintyMargin < 0.0) { + uncertaintyMargin = -uncertaintyMargin; + } + } + + return round(Math.abs(uncertaintyMargin), 3); + } + private double calculatePowerFluctuationMargin( EmsEnergyOptimisationAsset energyOptimisationAsset, Double powerLimitProfileTotal, From 38b07178c6502b97518b66ebe4797e2c14148893 Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Mon, 5 Oct 2026 10:00:52 +0200 Subject: [PATCH 2/3] Apply spotless --- .../ems/manager/optimisationMethods/EmsOptimisationBeta.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java index b74e585..7cf225e 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java @@ -482,7 +482,7 @@ private Map batteryCalculateForecasts( Double powerLimitMaximum = (Double) dp.getValue(); Double powerLimitMaximumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); - + powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); } From 20f5fa8f191de25bc5d9b242c50b40f2afee1c1e Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Tue, 6 Oct 2026 12:55:29 +0200 Subject: [PATCH 3/3] Fixed virtual power limits and advanced settings attribute refresh --- .../EmsOptimisationBeta.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java index 7cf225e..783c960 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisationBeta.java @@ -95,23 +95,25 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services String advancedSettingsAttributes = energyOptimisationAsset.getAdvancedSettingsAttributes().orElse(""); - if (advancedSettingsAttributes.isBlank()) { - StringBuilder advancedSettingsAttributesBody = new StringBuilder(); + StringBuilder advancedSettingsAttributesBody = new StringBuilder(); - String firstRow = String.format("Optimisation method '%s':\n", optimisationMethodName); - advancedSettingsAttributesBody.append(firstRow); + String firstRow = String.format("Optimisation method '%s':\n", optimisationMethodName); + advancedSettingsAttributesBody.append(firstRow); - for (String[] row : advancedSettingsAttributesInfo) { - advancedSettingsAttributesBody.append(String.join(",", row)).append("\n"); - } + for (String[] row : advancedSettingsAttributesInfo) { + advancedSettingsAttributesBody.append(String.join(",", row)).append("\n"); + } + + String advancedSettingsAttributesNew = advancedSettingsAttributesBody.toString(); + if (!advancedSettingsAttributes.equals(advancedSettingsAttributesNew)) { services .getAssetProcessingService() .sendAttributeEvent( new AttributeEvent( energyOptimisationAssetId, EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, - advancedSettingsAttributesBody.toString()), + advancedSettingsAttributesNew), getClass().getSimpleName()); } @@ -475,15 +477,15 @@ private Map batteryCalculateForecasts( assetDatapointQueryPeriodPredicted); // Calculate virtual power limit forecast - List powerLimitMaximumVirtualList = new ArrayList<>(); - List powerLimitMinimumVirtualList = new ArrayList<>(); + Map powerLimitMaximumVirtualList = new HashMap<>(); + Map powerLimitMinimumVirtualList = new HashMap<>(); for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { Double powerLimitMaximum = (Double) dp.getValue(); Double powerLimitMaximumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); - powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); + powerLimitMaximumVirtualList.put(dp.getTimestamp(), powerLimitMaximumVirtual); } for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { @@ -491,7 +493,7 @@ private Map batteryCalculateForecasts( Double powerLimitMinimumVirtual = calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); - powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); + powerLimitMinimumVirtualList.put(dp.getTimestamp(), powerLimitMinimumVirtual); } List chargePowerAvailableTotalList = new ArrayList<>(); @@ -545,10 +547,13 @@ private Map batteryCalculateForecasts( // Calculate total available charge and discharge power for (int i = 0; i < numberOfTimestamps; i++) { + long timestampMillis = timestampsMillisList.get(i); double totalPower = totalPowerConsumptionProductionFlexibleList.get(i); - Double powerLimitMaximumVirtual = powerLimitMaximumVirtualList.get(i); - Double powerLimitMinimumVirtual = powerLimitMinimumVirtualList.get(i); + Double powerLimitMaximumVirtual = + powerLimitMaximumVirtualList.getOrDefault(timestampMillis, null); + Double powerLimitMinimumVirtual = + powerLimitMinimumVirtualList.getOrDefault(timestampMillis, null); if (powerLimitMaximumVirtual != null) { double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower;