diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java index 5e857f6..c3310c5 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/optimisationMethods/EmsOptimisation.java @@ -34,11 +34,13 @@ import org.openremote.extension.ems.agent.EmsGOPACSAsset; import org.openremote.extension.ems.manager.Services; import org.openremote.model.asset.Asset; +import org.openremote.model.attribute.Attribute; import org.openremote.model.attribute.AttributeEvent; import org.openremote.model.datapoint.ValueDatapoint; import org.openremote.model.datapoint.query.AssetDatapointAllQuery; import org.openremote.model.query.AssetQuery; import org.openremote.model.syslog.SyslogCategory; +import org.openremote.model.value.ValueType; public class EmsOptimisation implements OptimisationMethod { protected static final Logger LOG = @@ -48,16 +50,24 @@ public class EmsOptimisation implements OptimisationMethod { // Maximum interval between data-points send by the device to be considered connected private final int ACTIVE_PERIOD_MINUTES = 5; - // EMS power limits settings - private final int POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE = 5; - private final int POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE = 5; - private final int POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; - private final int POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE = 10; + // Default EMS power limit settings + private final int POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT = 10; - // Battery settings + // Default battery settings private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL = 3; - private final int BATTERY_EFFICIENCY_BUFFER_PERCENTAGE = 3; + private final double BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT = 0.5; + private final int BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT = 8; + + // Advanced settings attribute names + private final String POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = + "powerLimitMaximumFluctuationMargin"; + private final String POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME = + "powerLimitMinimumFluctuationMargin"; + + private final String[][] advancedSettingsAttributesInfo = { + {POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()}, + {POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME, ValueType.POSITIVE_NUMBER.getName()} + }; @Override public void execute(String energyOptimisationAssetId, Services services) { @@ -74,6 +84,30 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services return; } + // Update Advanced settings attributes info + String advancedSettingsAttributes = + energyOptimisationAsset.getAdvancedSettingsAttributes().orElse(""); + + if (advancedSettingsAttributes.isBlank()) { + StringBuilder advancedSettingsAttributesBody = new StringBuilder(); + + String firstRow = String.format("Optimisation method '%s':\n", optimisationMethodName); + advancedSettingsAttributesBody.append(firstRow); + + for (String[] row : advancedSettingsAttributesInfo) { + advancedSettingsAttributesBody.append(String.join(",", row)).append("\n"); + } + + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + energyOptimisationAssetId, + EmsEnergyOptimisationAsset.ADVANCED_SETTINGS_ATTRIBUTES, + advancedSettingsAttributesBody.toString()), + getClass().getSimpleName()); + } + String logPrefixEnergyOptimisation = String.format( "assetType='%s', assetId='%s', assetName='%s'", @@ -93,8 +127,6 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services .map(asset -> (EmsElectricityBatteryAsset) asset) .toList(); - // TODO: Add order to battery assets from longest to smallest discharge duration - if (energyOptimisationAsset.getEnableDetailedLogging().orElse(false)) { int allowChargingSize = electricityBatteryAssets.stream() @@ -181,17 +213,44 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services return; } + // Order battery assets + electricityBatteryAssets = batteryOrder(electricityBatteryAssets); + // Check connection status of battery assets batteryCheckConnection(electricityBatteryAssets, services); // Check if all required attributes are connected/set and create log messages batteryCheckSetup(electricityBatteryAssets); - // Calculate new battery power set-points + // Find latest power set-point update across all batteries + long powerSetpointTimestampLatestMillis = + batteriesLatestPowerSetpointUpdate(electricityBatteryAssets); + + // Calculate battery energy level percentage targets + int intervalPeriodMinutes = 15; + int currentMinute = LocalDateTime.now().getMinute(); + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long forecastUpdateTimeMillis = + currentTimeMillis - currentTimeMillis % (intervalPeriodMinutes * 60 * 1000); + + Map batteryEnergyLevelPercentageTargets; + + if ((currentMinute % intervalPeriodMinutes) == 0 + || powerSetpointTimestampLatestMillis < forecastUpdateTimeMillis) { + batteryEnergyLevelPercentageTargets = + batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); + } else { + batteryEnergyLevelPercentageTargets = + batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); + } + + // Calculate battery power set-points Map powerSetpointsNew = batteryCalculatePowerSetpoints( energyOptimisationAsset, electricityBatteryAssets, + powerSetpointTimestampLatestMillis, + batteryEnergyLevelPercentageTargets, services, logPrefixEnergyOptimisation); @@ -199,6 +258,23 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); } + private long batteriesLatestPowerSetpointUpdate( + List electricityBatteryAssets) { + // Find latest power set-point update across all batteries + long powerSetpointTimestampLatestMillis = 0L; + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + long powerSetpointTimestampMillis = + electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); + + if (powerSetpointTimestampMillis > powerSetpointTimestampLatestMillis) { + powerSetpointTimestampLatestMillis = powerSetpointTimestampMillis; + } + } + + return powerSetpointTimestampLatestMillis; + } + private int batteryCalculateEnergyLevelPercentageDefault( Integer energyLevelPercentageMaximumBattery, Integer energyLevelPercentageMinimumBattery, @@ -224,10 +300,9 @@ private int batteryCalculateEnergyLevelPercentageDefault( (int) Math.round( (double) - (energyLevelPercentageMaximumBattery - - energyLevelPercentageMinimumBattery) - / 2 - + energyLevelPercentageMinimumBattery); + (energyLevelPercentageMaximumBattery + + energyLevelPercentageMinimumBattery) + / 2); } return energyLevelPercentageDefault; @@ -239,15 +314,21 @@ private Map batteryCalculateForecasts( Services services) { Map batteryEnergyLevelPercentageTargets = new HashMap<>(); + StringBuilder infoStr = new StringBuilder(); + infoStr.append("EMS forecast:\n"); + // Get power consumption forecast for 1 week long intervalMillis = 15 * 60000; long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long startTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis; - long endTimeMillis = startTimeMillis + 7 * 24 * 60 * 60000; + long startTimeForecastPeriodMillis = currentTimeMillis - currentTimeMillis % intervalMillis; + long endTimeForecastPeriodMillis = + startTimeForecastPeriodMillis + - startTimeForecastPeriodMillis % (24 * 60 * 60000) + + (8 * 24 * 60 * 60000); String energyOptimisationAssetId = energyOptimisationAsset.getId(); AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = - new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + new AssetDatapointAllQuery(startTimeForecastPeriodMillis, endTimeForecastPeriodMillis); List> energyOptimisationPowerConsumptionPredicted = services .getAssetPredictedDatapointService() @@ -271,15 +352,15 @@ private Map batteryCalculateForecasts( // System.out.println("totalPowerConsumptionAveraged = " + // totalPowerConsumptionAveraged); - long startTimestampMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); - long endTimestampMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); + long startTimeForecastDataMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); + long endTimeForecastDataMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); // Interpolate power average values for each 15-minute interval List> totalPowerConsumptionInterpolated = intervalInterpolate( totalPowerConsumptionAveraged, - startTimestampMillis, - endTimestampMillis, + startTimeForecastDataMillis, + endTimeForecastDataMillis, intervalMillis); List timestampsMillisList = new ArrayList<>(); @@ -293,15 +374,18 @@ private Map batteryCalculateForecasts( totalPowerConsumptionList.add(value); } - // System.out.println("timestampsMillisList = " + timestampsMillisList); - // System.out.println("totalPowerConsumptionList = " + totalPowerConsumptionList); + int numberOfTimestamps = timestampsMillisList.size(); + + infoStr.append(String.format("numberOfTimestamps = %s \n", numberOfTimestamps)); + infoStr.append(String.format("timestampsMillisList = %s \n", timestampsMillisList)); + infoStr.append(String.format("totalPowerConsumptionList = %s \n", totalPowerConsumptionList)); if (totalPowerConsumptionList.isEmpty()) { return batteryEnergyLevelPercentageTargets; } AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = - new AssetDatapointAllQuery(startTimestampMillis, endTimestampMillis); + new AssetDatapointAllQuery(startTimeForecastDataMillis, endTimeForecastDataMillis); List> energyOptimisationPowerProductionPredicted = services .getAssetPredictedDatapointService() @@ -335,27 +419,28 @@ private Map batteryCalculateForecasts( Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); } - // List to store the sum of total power consumption, production and flexible power - List totalPowerConsumptionProductionFlexList = new ArrayList<>(); + // List to store the sum of total power consumption, production and flexible + List totalPowerConsumptionProductionFlexibleList = new ArrayList<>(); // Sum total power consumption and production - for (int i = 0; i < timestampsMillisList.size(); i++) { + for (int i = 0; i < numberOfTimestamps; i++) { Double powerConsumption = totalPowerConsumptionList.get(i); Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); if (powerProduction != null) { double sum = powerConsumption + powerProduction; - totalPowerConsumptionProductionFlexList.add(sum); + totalPowerConsumptionProductionFlexibleList.add(sum); } else { - totalPowerConsumptionProductionFlexList.add(powerConsumption); + totalPowerConsumptionProductionFlexibleList.add(powerConsumption); } } - // System.out.println("totalPowerConsumptionProductionFlexList = " + - // totalPowerConsumptionProductionFlexList); - // System.out.println(); + infoStr.append( + String.format( + "totalPowerConsumptionProductionFlexibleList = %s \n", + totalPowerConsumptionProductionFlexibleList)); - // Get power limits forecasts + // Get power limit forecasts List> energyOptimisationPowerLimitMaximumPredicted = services .getAssetPredictedDatapointService() @@ -385,11 +470,12 @@ private Map batteryCalculateForecasts( List chargePowerAvailableTotalList = new ArrayList<>(); List dischargePowerAvailableTotalList = new ArrayList<>(); - // Add current available charge and discharge power + // Add current available charge and discharge power which is 0 chargePowerAvailableTotalList.add(0.0); dischargePowerAvailableTotalList.add(0.0); - double intervalHour = (double) intervalMillis / (60 * 60000); + double intervalHour = round((double) intervalMillis / (60 * 60000), 5); + int batteryPercentageRoundingPrecision = 5; // Calculate forecast for each battery for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { @@ -421,31 +507,42 @@ private Map batteryCalculateForecasts( } // Check if the energy level percentage maximum and minimum are valid - if ((energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) - < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { + if (energyLevelPercentageMaximumBattery <= energyLevelPercentageMinimumBattery) { + continue; + } + + // Check if the charge and discharge efficiency are valid + if (chargeEfficiencyBattery <= 0 || dischargeEfficiencyBattery <= 0) { continue; } + List powerLimitMaximumVirtualList = new ArrayList<>(); + List powerLimitMinimumVirtualList = new ArrayList<>(); + // Calculate total available charge and discharge power - for (int i = 0; i < timestampsMillisList.size(); i++) { + for (int i = 0; i < numberOfTimestamps; i++) { long timestampMillis = timestampsMillisList.get(i); - double totalPower = totalPowerConsumptionProductionFlexList.get(i); + double totalPower = totalPowerConsumptionProductionFlexibleList.get(i); Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); - if (powerLimitMaximum != null) { - double powerLimitMaximumVirtual = - powerLimitMaximum * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + Double powerLimitMaximumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximum, "max"); + Double powerLimitMinimumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimum, "min"); + + powerLimitMaximumVirtualList.add(powerLimitMaximumVirtual); + powerLimitMinimumVirtualList.add(powerLimitMinimumVirtual); + + if (powerLimitMaximumVirtual != null) { double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; chargePowerAvailableTotalList.add(chargePowerTotalAvailable); } else { chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); } - if (powerLimitMinimum != null) { - double powerLimitMinimumVirtual = - powerLimitMinimum * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + if (powerLimitMinimumVirtual != null) { double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); } else { @@ -455,7 +552,12 @@ private Map batteryCalculateForecasts( int numberOfDataPoints = chargePowerAvailableTotalList.size(); - // System.out.println("numberOfDataPoints = " + numberOfDataPoints); + infoStr.append( + String.format("powerLimitMaximumVirtualList = %s \n", powerLimitMaximumVirtualList)); + infoStr.append( + String.format("powerLimitMinimumVirtualList = %s \n", powerLimitMinimumVirtualList)); + infoStr.append("\n"); + // System.out.println("chargePowerAvailableTotalList = " + // chargePowerAvailableTotalList); // System.out.println("dischargePowerAvailableTotalList = " + @@ -488,10 +590,6 @@ private Map batteryCalculateForecasts( List chargePercentageNeededTotalList = new ArrayList<>(); List dischargePercentageNeededTotalList = new ArrayList<>(); - chargeEfficiencyBattery = chargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; - dischargeEfficiencyBattery = - dischargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; - for (int i = 0; i < numberOfDataPoints; i++) { double c = intervalHour @@ -503,8 +601,8 @@ private Map batteryCalculateForecasts( * intervalHour * dischargeNeededTotalList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageNeededTotalList.add(c); - dischargePercentageNeededTotalList.add(d); + chargePercentageNeededTotalList.add(round(c, batteryPercentageRoundingPrecision)); + dischargePercentageNeededTotalList.add(round(d, batteryPercentageRoundingPrecision)); } // System.out.println("chargePercentageNeededTotalList = " + @@ -526,8 +624,8 @@ private Map batteryCalculateForecasts( * intervalHour * dischargePowerAvailableBatteryList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); - chargePercentageAvailableBatteryList.add(c); - dischargePercentageAvailableBatteryList.add(d); + chargePercentageAvailableBatteryList.add(round(c, batteryPercentageRoundingPrecision)); + dischargePercentageAvailableBatteryList.add(round(d, batteryPercentageRoundingPrecision)); } // System.out.println("chargePercentageAvailableBatteryList = " + @@ -551,8 +649,12 @@ private Map batteryCalculateForecasts( Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; dischargePercentageAvailableBatteryList.set( - i, dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable); - getToLimitPercentageList.add(getToLimitPercentage); + i, + round( + dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable, + batteryPercentageRoundingPrecision)); + getToLimitPercentageList.add( + round(getToLimitPercentage, batteryPercentageRoundingPrecision)); } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { double chargePercentageNeeded = energyLevelPercentageMinimumBattery - getToLimitPercentage; @@ -560,8 +662,12 @@ private Map batteryCalculateForecasts( Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; chargePercentageAvailableBatteryList.set( - i, chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable); - getToLimitPercentageList.add(getToLimitPercentage); + i, + round( + chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable, + batteryPercentageRoundingPrecision)); + getToLimitPercentageList.add( + round(getToLimitPercentage, batteryPercentageRoundingPrecision)); } else { break; } @@ -570,16 +676,16 @@ private Map batteryCalculateForecasts( // System.out.println("getToLimitIndex = " + getToLimitIndex); // System.out.println("getToLimitPercentage = " + getToLimitPercentage); // System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); - // System.out.println("chargePercentageAvailableBatteryList = " + + // System.out.println("chargePercentageAvailableBatteryList2 = " + // chargePercentageAvailableBatteryList); - // System.out.println("dischargePercentageAvailableBatteryList = " + + // System.out.println("dischargePercentageAvailableBatteryList2 = " + // dischargePercentageAvailableBatteryList); List energyLevelPredictionList = new ArrayList<>(); if (getToLimitPercentage > energyLevelPercentageMaximumBattery || getToLimitPercentage < energyLevelPercentageMinimumBattery) { - // Energy level percentage forecast when whole forecast is outside of battery percentage + // Energy level percentage forecast when entire forecast is outside of battery percentage // limits energyLevelPredictionList = getToLimitPercentageList; } else { @@ -608,7 +714,10 @@ private Map batteryCalculateForecasts( for (int i = 0; i < numberOfDataPoints; i++) { chargeAndDischargePercentageWantBatteryList.add( - chargePercentageWantBatteryList.get(i) + dischargePercentageWantBatteryList.get(i)); + round( + chargePercentageWantBatteryList.get(i) + + dischargePercentageWantBatteryList.get(i), + batteryPercentageRoundingPrecision)); } // System.out.println("chargeAndDischargePercentageWantBatteryList = " + @@ -622,7 +731,7 @@ private Map batteryCalculateForecasts( for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); - runningSumList.add(sum); + runningSumList.add(round(sum, batteryPercentageRoundingPrecision)); } // System.out.println("getToLimitPercentageList = " + @@ -636,20 +745,20 @@ private Map batteryCalculateForecasts( getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); } energyLevelPredictionList.addAll(runningSumList); + int energyLevelPredictionListSize = energyLevelPredictionList.size(); - // System.out.println("energyLevelPredictionList = " + - // energyLevelPredictionList); - // System.out.println(); + infoStr.append( + String.format("energyLevelPredictionListOriginal = %s \n", energyLevelPredictionList)); // Calculate forecast starting within battery percentage limits double energyLevelPredictionMaximum = Collections.max( energyLevelPredictionList.subList( - startRunningSumIndex, energyLevelPredictionList.size())); + startRunningSumIndex, energyLevelPredictionListSize)); double energyLevelPredictionMinimum = Collections.min( energyLevelPredictionList.subList( - startRunningSumIndex, energyLevelPredictionList.size())); + startRunningSumIndex, energyLevelPredictionListSize)); long dtStart = services.getTimerService().getCurrentTimeMillis(); long dt = 0; @@ -663,7 +772,7 @@ private Map batteryCalculateForecasts( String chargeOrDischarge = ""; int intervalStartIndex = startRunningSumIndex; - for (int i = intervalEndIndex; i < energyLevelPredictionList.size(); i++) { + for (int i = intervalEndIndex; i < energyLevelPredictionListSize; i++) { predictionListIndex = i; if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { @@ -713,9 +822,12 @@ private Map batteryCalculateForecasts( if (chargeOrDischarge.equals("charge")) { double chargeAvailable = 0; double chargeAvailableInterval = - energyLevelPercentageMaximumBattery - - Collections.max( - energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + round( + energyLevelPercentageMaximumBattery + - Collections.max( + energyLevelPredictionList.subList( + intervalStartIndex, intervalEndIndex)), + batteryPercentageRoundingPrecision); if (chargeAvailableInterval > 0) { for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { @@ -728,7 +840,10 @@ private Map batteryCalculateForecasts( if (dischargePercentageWantBatteryList.get(i) >= 0) { double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); } if (chargeAvailableLeft > 0) { @@ -749,15 +864,22 @@ private Map batteryCalculateForecasts( changeValue = chargeNeeded; } - for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { - energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); + for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { + energyLevelPredictionList.set( + i, + round( + energyLevelPredictionList.get(i) + changeValue, + batteryPercentageRoundingPrecision)); } } else if (chargeOrDischarge.equals("discharge")) { double dischargeAvailable = 0; double dischargeAvailableInterval = - energyLevelPercentageMinimumBattery - - Collections.min( - energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + round( + energyLevelPercentageMinimumBattery + - Collections.min( + energyLevelPredictionList.subList( + intervalStartIndex, intervalEndIndex)), + batteryPercentageRoundingPrecision); if (dischargeAvailableInterval > 0) { for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { @@ -771,7 +893,9 @@ private Map batteryCalculateForecasts( double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); dischargeAvailableLeft = - dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); } if (dischargeAvailableLeft < 0) { changeIndex = i; @@ -791,41 +915,51 @@ private Map batteryCalculateForecasts( changeValue = dischargeNeeded; } - for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { - energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); + for (int i = changeIndex; i < energyLevelPredictionListSize; i++) { + energyLevelPredictionList.set( + i, + round( + energyLevelPredictionList.get(i) + changeValue, + batteryPercentageRoundingPrecision)); } } if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { - LOG.warning( + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation error at timestamp = %s", + timestampsMillisList.get(predictionListIndex))); + infoStr.append( String.format( - "Battery energy level percentage calculation error at index = %s", - predictionListIndex)); - // System.out.println("ERROR: calculation error at index = " + - // predictionListIndex); + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); break; } else if (dt > timeoutMillis) { - LOG.warning( + infoStr.insert( + 0, String.format( - "Battery energy level percentage calculation timed out at index = %s", - predictionListIndex)); - // System.out.println("ERROR: calculation timeout at index = " + - // predictionListIndex); + "Battery energy level percentage calculation timed out during power limit optimisation at timestamp = %s", + timestampsMillisList.get(predictionListIndex))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); break; } energyLevelPredictionMaximum = Collections.max( energyLevelPredictionList.subList( - startRunningSumIndex, energyLevelPredictionList.size())); + startRunningSumIndex, energyLevelPredictionListSize)); energyLevelPredictionMinimum = Collections.min( energyLevelPredictionList.subList( - startRunningSumIndex, energyLevelPredictionList.size())); + startRunningSumIndex, energyLevelPredictionListSize)); dt = services.getTimerService().getCurrentTimeMillis() - dtStart; } - // System.out.println("After limits: energyLevelPredictionList = " + + // System.out.println("energyLevelPredictionListOptimisedLimits = " + // energyLevelPredictionList + "\n"); // Get day ahead asset @@ -836,7 +970,8 @@ private Map batteryCalculateForecasts( useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); } - // Get the tariff forecasts from energy optimisation asset for 1 week + // Get the tariff forecasts from energy optimisation asset for 1 week, timestamps are + // ordered from newest to oldest (descending order) List> tariffExportDatapoints = getTariffDatapoints( energyOptimisationAsset, @@ -890,22 +1025,16 @@ private Map batteryCalculateForecasts( } } - // Calculate battery energy level percentage default - double energyLevelPercentageDefault = - batteryCalculateEnergyLevelPercentageDefault( - energyLevelPercentageMaximumBattery, - energyLevelPercentageMinimumBattery, - energyOptimisationAsset); - - // TODO: make window dynamic based on time needed to reach energyLevelPercentageDefault // Calculate optimal charge and discharge zone for each day based on tariffs Map chargeAndDischargeZonesMap = calculateTariffChargeAndDischargeZones( - tariffImportDatapoints, tariffExportDatapoints, 4); + tariffImportDatapoints, + tariffExportDatapoints, + BATTERY_TARIFF_OPTIMISATION_WINDOW_DEFAULT); // Charge zone = 1, discharge zone = -1 List chargeAndDischargeZonesList = - new ArrayList<>(Collections.nCopies(energyLevelPredictionList.size(), 0)); + new ArrayList<>(Collections.nCopies(energyLevelPredictionListSize, 0)); for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { long timestampMillis = entry.getKey(); @@ -922,13 +1051,17 @@ private Map batteryCalculateForecasts( // System.out.println("chargeAndDischargeZonesList = " + // chargeAndDischargeZonesList); - // Default energy level percentage for each interval + // Calculate battery energy level percentage default list + double energyLevelPercentageDefault = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximumBattery, + energyLevelPercentageMinimumBattery, + energyOptimisationAsset); List energyLevelPercentageDefaultList = new ArrayList<>( - Collections.nCopies( - chargeAndDischargeZonesList.size(), energyLevelPercentageDefault)); + Collections.nCopies(energyLevelPredictionListSize, energyLevelPercentageDefault)); - for (int i = 0; i < chargeAndDischargeZonesList.size(); i++) { + for (int i = 0; i < energyLevelPredictionListSize; i++) { if (chargeAndDischargeZonesList.get(i) == 1) { energyLevelPercentageDefaultList.set( i, Double.valueOf(energyLevelPercentageMaximumBattery)); @@ -941,173 +1074,259 @@ private Map batteryCalculateForecasts( // System.out.println("energyLevelPercentageDefaultList = " + // energyLevelPercentageDefaultList); - // TODO: handle debounce interval in forecast - // Optimise forecast based on optimal tariffs + // Optimise forecast based on tariffs for (int i = 1; - i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionList.size(); + i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionListSize; i++) { - // Only adjust forecast if predicted energy level percentage is outside battery debounce - // interval - if (Math.abs(energyLevelPredictionList.get(i) - energyLevelPercentageDefaultList.get(i)) - <= BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - continue; - } - if (chargeAndDischargeZonesList.get(i) == 1 && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { + // Interval from charge zone index till end of forecast Double energyLevelIntervalMaximum = Collections.max( - energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); - - double chargeSpaceLeft = - energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum; + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + double chargeSpaceOverall = + round( + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum, + batteryPercentageRoundingPrecision); - if (chargeSpaceLeft > 0) { + if (chargeSpaceOverall > 0) { double chargeNeeded = - energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + round( + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); double chargeAvailableLeft = 0; if (dischargePercentageWantBatteryList.get(i) >= 0) { double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); } if (chargeAvailableLeft > 0) { chargeAvailableLeft = - Math.min(Math.min(chargeSpaceLeft, chargeAvailableLeft), chargeNeeded); + Math.min(Math.min(chargeSpaceOverall, chargeAvailableLeft), chargeNeeded); + chargeSpaceOverall = + round( + chargeSpaceOverall - chargeAvailableLeft, + batteryPercentageRoundingPrecision); - for (int j = i; j < energyLevelPredictionList.size(); j++) { + for (int j = i; j < energyLevelPredictionListSize; j++) { energyLevelPredictionList.set( - j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + j, + round( + energyLevelPredictionList.get(j) + chargeAvailableLeft, + batteryPercentageRoundingPrecision)); } } - } else { - double chargeZoneEnergyLevel = energyLevelPredictionList.get(i); - - int energyLevelIntervalMaximumIndex = energyLevelPredictionList.size(); - - if (energyLevelIntervalMaximum >= energyLevelPercentageMaximumBattery) { - energyLevelIntervalMaximumIndex = - energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); - } - - for (int k = i; k <= energyLevelIntervalMaximumIndex; k++) { - double chargeNeeded = energyLevelPredictionList.get(k) - chargeZoneEnergyLevel; - - if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { - double chargeAvailableLeft = 0; - - if (dischargePercentageWantBatteryList.get(i) >= 0) { - double chargeInUse = - energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; - } + } - if (chargeAvailableLeft > 0) { + // Get the index of the first maximum + energyLevelIntervalMaximum = + Collections.max( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + int energyLevelIntervalMaximumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); + + if (chargeSpaceOverall <= 0 && i < energyLevelIntervalMaximumIndex) { + double chargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + double chargeAvailableLeft = + round( + chargePercentageAvailableBatteryList.get(i) - chargeInUse, + batteryPercentageRoundingPrecision); + + int energyLevelIntervalIndex = 0; + dt = 0; + dtStart = services.getTimerService().getCurrentTimeMillis(); + + while (chargeAvailableLeft > 0 + && energyLevelIntervalIndex < energyLevelIntervalMaximumIndex) { + + for (int k = i + 1; k <= energyLevelIntervalMaximumIndex; k++) { + energyLevelIntervalIndex = k; + double chargeNeeded = + round( + energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + + // Only allow moving charging moment into charge zone if no charge is wanted at + // index k + if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { double energyLevelIntervalMaximum2 = - Collections.max(energyLevelPredictionList.subList(i, k - 1)); + Collections.max(energyLevelPredictionList.subList(i, k)); double chargeSpaceInterval = - energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2; + round( + energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2, + batteryPercentageRoundingPrecision); if (chargeSpaceInterval > 0) { - chargeAvailableLeft = + double chargeChange = Math.min( Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); + chargeAvailableLeft = + round( + chargeAvailableLeft - chargeChange, + batteryPercentageRoundingPrecision); for (int j = i; j < k; j++) { energyLevelPredictionList.set( - j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + j, + round( + energyLevelPredictionList.get(j) + chargeChange, + batteryPercentageRoundingPrecision)); } + break; } } + } + if (dt > timeoutMillis) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation timed out during tariff charge optimisation at timestamp = %s", + timestampsMillisList.get(i))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); break; } + + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; } } } else if (chargeAndDischargeZonesList.get(i) == -1 && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { + // Interval from discharge zone index till end of forecast Double energyLevelIntervalMinimum = Collections.min( - energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + double dischargeSpaceOverall = + round( + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum, + batteryPercentageRoundingPrecision); - double dischargeSpaceLeft = - energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum; - - if (dischargeSpaceLeft < 0) { + if (dischargeSpaceOverall < 0) { double dischargeNeeded = - energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + round( + energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); double dischargeAvailableLeft = 0; if (chargePercentageWantBatteryList.get(i) <= 0) { double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); dischargeAvailableLeft = - dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); } if (dischargeAvailableLeft < 0) { dischargeAvailableLeft = - Math.max(Math.max(dischargeSpaceLeft, dischargeAvailableLeft), dischargeNeeded); - - for (int j = i; j < energyLevelPredictionList.size(); j++) { + Math.max( + Math.max(dischargeSpaceOverall, dischargeAvailableLeft), dischargeNeeded); + dischargeSpaceOverall = + round( + dischargeSpaceOverall - dischargeAvailableLeft, + batteryPercentageRoundingPrecision); + + for (int j = i; j < energyLevelPredictionListSize; j++) { energyLevelPredictionList.set( - j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); + j, + round( + energyLevelPredictionList.get(j) + dischargeAvailableLeft, + batteryPercentageRoundingPrecision)); } } - } else { - double dischargeZoneEnergyLevel = energyLevelPredictionList.get(i); - - int energyLevelIntervalMinimumIndex = energyLevelPredictionList.size(); - - if (energyLevelIntervalMinimum <= energyLevelPercentageMinimumBattery) { - energyLevelIntervalMinimumIndex = - energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); - } - - for (int k = i; k <= energyLevelIntervalMinimumIndex; k++) { - double dischargeNeeded = - energyLevelPredictionList.get(k) - dischargeZoneEnergyLevel; - - if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { - double dischargeAvailableLeft = 0; - - if (chargePercentageWantBatteryList.get(i) <= 0) { - double dischargeInUse = - energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); - dischargeAvailableLeft = - dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; - } + } - if (dischargeAvailableLeft < 0) { + // Get the index of the first minimum + energyLevelIntervalMinimum = + Collections.min( + energyLevelPredictionList.subList(i, energyLevelPredictionListSize)); + int energyLevelIntervalMinimumIndex = + energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); + + if (dischargeSpaceOverall >= 0 && i < energyLevelIntervalMinimumIndex) { + double dischargeInUse = + energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + double dischargeAvailableLeft = + round( + dischargePercentageAvailableBatteryList.get(i) - dischargeInUse, + batteryPercentageRoundingPrecision); + + int energyLevelIntervalIndex = 0; + dt = 0; + dtStart = services.getTimerService().getCurrentTimeMillis(); + + while (dischargeAvailableLeft < 0 + && energyLevelIntervalIndex < energyLevelIntervalMinimumIndex) { + + for (int k = i + 1; k <= energyLevelIntervalMinimumIndex; k++) { + energyLevelIntervalIndex = k; + double dischargeNeeded = + round( + energyLevelPredictionList.get(k) - energyLevelPredictionList.get(i), + batteryPercentageRoundingPrecision); + + // Only allow moving discharging moment into discharge zone if no discharge is + // wanted at index k + if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { double energyLevelIntervalMinimum2 = - Collections.min(energyLevelPredictionList.subList(i, k - 1)); + Collections.min(energyLevelPredictionList.subList(i, k)); double dischargeSpaceInterval = - energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2; + round( + energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2, + batteryPercentageRoundingPrecision); if (dischargeSpaceInterval < 0) { - dischargeAvailableLeft = + double dischargeChange = Math.max( Math.max(dischargeAvailableLeft, dischargeNeeded), dischargeSpaceInterval); + dischargeAvailableLeft = + round( + dischargeAvailableLeft - dischargeChange, + batteryPercentageRoundingPrecision); for (int j = i; j < k; j++) { energyLevelPredictionList.set( - j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); + j, + round( + energyLevelPredictionList.get(j) + dischargeChange, + batteryPercentageRoundingPrecision)); } + break; } } + } + if (dt > timeoutMillis) { + infoStr.insert( + 0, + String.format( + "Battery energy level percentage calculation timed out during tariff discharge optimisation at timestamp = %s", + timestampsMillisList.get(i))); + infoStr.append( + String.format( + "energyLevelPredictionListOptimised = %s \n", energyLevelPredictionList)); + LOG.warning(infoStr.toString()); break; } + + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; } } } } } - // System.out.println("After Tariffs: energyLevelPredictionList = " + + // System.out.println("energyLevelPredictionListOptimisedLimitsAndTariffs = " + // energyLevelPredictionList + "\n"); // Get the current energy level percentage target @@ -1148,26 +1367,27 @@ private Map batteryCalculateForecasts( // System.out.println("powerChangeTotalList = " + powerChangeTotalList); + // Update list with power flexible changes for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { - totalPowerConsumptionProductionFlexList.set( - i, totalPowerConsumptionProductionFlexList.get(i) - powerChangeTotalList.get(i)); + totalPowerConsumptionProductionFlexibleList.set( + i, totalPowerConsumptionProductionFlexibleList.get(i) - powerChangeTotalList.get(i)); } - // System.out.println("totalPowerConsumptionProductionFlexList = " + - // totalPowerConsumptionProductionFlexList); + // System.out.println("totalPowerConsumptionProductionFlexibleList = " + + // totalPowerConsumptionProductionFlexibleList); List> energyLevelPercentageForecast = new ArrayList<>(); List> powerSetpointForecast = new ArrayList<>(); // Update energy level percentage forecast starting after current time - for (int i = 1; i < timestampsMillisList.size(); i++) { + for (int i = 1; i < numberOfTimestamps; i++) { energyLevelPercentageForecast.add( new ValueDatapoint<>( timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); } // Update power set-point starting from current power limit - for (int i = 0; i < timestampsMillisList.size(); i++) { + for (int i = 0; i < numberOfTimestamps; i++) { powerSetpointForecast.add( new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); } @@ -1184,16 +1404,14 @@ private Map batteryCalculateForecasts( batteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), powerSetpointForecast); - - // System.out.println("UPDATED forecasts"); } return batteryEnergyLevelPercentageTargets; } - private Map batteryCalculatePowerFlexibleAvailable( + private Map batteryCalculatePowerFlexibleAvailable( List electricityBatteryAssets) { - HashMap powerFlexibleAvailable = new HashMap<>(); + Map powerFlexibleAvailable = new HashMap<>(); for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); @@ -1224,7 +1442,7 @@ private Map batteryCalculatePowerFlexibleAvailable( powerFlexibleAvailable.put( electricityBatteryAsset.getId(), - new double[] {chargePowerMaximum, dischargePowerMaximum}); + new ChargeDischarge(chargePowerMaximum, dischargePowerMaximum)); } return powerFlexibleAvailable; @@ -1233,25 +1451,15 @@ private Map batteryCalculatePowerFlexibleAvailable( private Map batteryCalculatePowerSetpoints( EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, + long powerSetpointTimestampMillisLatest, + Map batteryEnergyLevelPercentageTargets, Services services, - String logPrefix) { - Map powerFlexibleAvailable = + String logPrefixEnergyOptimisation) { + Map powerFlexibleAvailable = batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); Map powerSetpointsNew; - // Get latest power set-point across all batteries - long powerSetpointTimestampMillisLatest = 0L; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - long powerSetpointTimestampMillis = - electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); - - if (powerSetpointTimestampMillis > powerSetpointTimestampMillisLatest) { - powerSetpointTimestampMillisLatest = powerSetpointTimestampMillis; - } - } - - // Check if power net is updated since last battery power set-points update + // Check if power net updated since last batteries power set-point update Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); @@ -1263,170 +1471,276 @@ private Map batteryCalculatePowerSetpoints( return powerSetpointsNew; } - // Check if power limits are present and calculate additional power limits + // Check if power limits are present Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - Double powerLimitMaximumBatteries = null; + // Calculate debounce power limits + Double powerLimitMaximumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); + Double powerLimitMinimumVirtual = + calculatePowerLimitVirtual(energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); - if (powerLimitMaximumProfileTotal != null) { - powerLimitMaximumBatteries = - round( - powerLimitMaximumProfileTotal - * (1 - POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), - 3); - } - - Double powerLimitMinimumBatteries = null; - - if (powerLimitMinimumProfileTotal != null) { - powerLimitMinimumBatteries = - round( - powerLimitMinimumProfileTotal - * (1 - POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), - 3); - } - - // TODO: add battery limits cross-over warning - // TODO: add advanced settings - - int currentMinute = LocalDateTime.now().getMinute(); - Map batteryEnergyLevelPercentageTargets = - batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); + // Send warning LOG message when too large fluctuation margins are set + if (powerLimitMaximumProfileTotal != null + && powerLimitMinimumProfileTotal != null + && powerLimitMaximumVirtual != null + && powerLimitMinimumVirtual != null + && powerLimitMaximumVirtual < powerLimitMinimumVirtual) { + double diffPowerLimit = powerLimitMaximumProfileTotal - powerLimitMinimumProfileTotal; + double fluctuationMarginSum = + Math.abs(powerLimitMaximumProfileTotal - powerLimitMaximumVirtual) + + Math.abs(powerLimitMinimumProfileTotal - powerLimitMinimumVirtual); + + Long powerLimitMaximumProfileTimestampMillis = + energyOptimisationAsset.getPowerLimitMaximumProfileTotalTimestamp().orElse(null); + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + String powerLimitMaximumProfileDateTime = ""; + + if (powerLimitMaximumProfileTimestampMillis != null) { + powerLimitMaximumProfileDateTime = + formatter.format(Instant.ofEpochMilli(powerLimitMaximumProfileTimestampMillis)); + } - // Calculate battery energy level forecast - // TODO: add 15 minute skip protection with forecast update timestamp for robust forecast - // updating - if ((currentMinute % 15) == 0) { - batteryEnergyLevelPercentageTargets = - batteryCalculateForecasts(electricityBatteryAssets, energyOptimisationAsset, services); + LOG.warning( + String.format( + "%s; Failed to perform '%s' energy optimisation method. The difference between '%s' - %s = %s kW is smaller than the fluctuation margin sum = %s kW for timestamp='%s'", + logPrefixEnergyOptimisation, + optimisationMethodName, + EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), + diffPowerLimit, + fluctuationMarginSum, + powerLimitMaximumProfileDateTime)); } // Calculate virtual power consumption Map powerSetpointsCurrent = new HashMap<>(); for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - powerSetpointsCurrent.put(electricityBatteryAssetId, powerSetpoint); + powerSetpointsCurrent.put(electricityBatteryAsset.getId(), powerSetpoint); } double powerSetpointsCurrentSum = powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; - // Calculate new power set-points - if (powerLimitMaximumProfileTotal != null - && powerConsumptionVirtual > powerLimitMaximumProfileTotal) { - Double powerLimitMaximumVirtual = - round( - powerLimitMaximumProfileTotal - * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), - 3); + // Calculate new power set-points without limits + Map powerSetpointsNewWithoutLimits = + batteryCalculatePowerSetpointsWithoutLimits( + electricityBatteryAssets, + batteryEnergyLevelPercentageTargets, + energyOptimisationAsset, + powerFlexibleAvailable, + services); + + // Apply power limits and adjust new power set-points + double powerSetpointsNewWithoutLimitsSum = + powerSetpointsNewWithoutLimits.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerNetVirtualNew = powerConsumptionVirtual + powerSetpointsNewWithoutLimitsSum; + + double powerLimitMaximumFluctuation = + calculatePowerFluctuationMargin( + energyOptimisationAsset, powerLimitMaximumProfileTotal, "max"); + double powerLimitMinimumFluctuation = + calculatePowerFluctuationMargin( + energyOptimisationAsset, powerLimitMinimumProfileTotal, "min"); + + if (powerLimitMaximumVirtual != null + && (powerNetVirtualNew + powerLimitMaximumFluctuation) > powerLimitMaximumVirtual) { powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach( - powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMaximumVirtual, "max"); - } else if (powerLimitMinimumProfileTotal != null - && powerConsumptionVirtual < powerLimitMinimumProfileTotal) { - Double powerLimitMinimumVirtual = - round( - powerLimitMinimumProfileTotal - * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), - 3); + powerNetVirtualNew, + powerLimitMaximumVirtual, + electricityBatteryAssets, + powerFlexibleAvailable, + powerSetpointsNewWithoutLimits); + } else if (powerLimitMinimumVirtual != null + && (powerNetVirtualNew - powerLimitMinimumFluctuation) < powerLimitMinimumVirtual) { powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach( - powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMinimumVirtual, "min"); - } else { - powerSetpointsNew = - batteryRestoreEnergyLevels( - energyOptimisationAsset, + powerNetVirtualNew, + powerLimitMinimumVirtual, electricityBatteryAssets, powerFlexibleAvailable, - powerNet, - powerSetpointsCurrentSum, - powerLimitMinimumBatteries, - powerLimitMaximumBatteries, - batteryEnergyLevelPercentageTargets); + powerSetpointsNewWithoutLimits); + } else { + powerSetpointsNew = powerSetpointsNewWithoutLimits; } - // Create log messages on power limit breach + // Send warning LOG message on power limit breach double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; - if (powerLimitMaximumProfileTotal != null && powerNet > powerLimitMaximumProfileTotal) { - double powerExceededAmount = round((powerNet - powerLimitMaximumProfileTotal), 3); + if (powerLimitMaximumProfileTotal != null + && powerNetNewVirtual > powerLimitMaximumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); LOG.warning( String.format( - "%s; Power net is %s kW above power limit maximum", logPrefix, powerExceededAmount)); - - if (powerNetNewVirtual > powerLimitMaximumProfileTotal) { - double powerReductionShortage = - round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); - LOG.warning( - String.format( - "%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", - logPrefix, powerReductionShortage)); - } - } else if (powerLimitMinimumProfileTotal != null && powerNet < powerLimitMinimumProfileTotal) { - double powerExceededAmount = round((powerNet - powerLimitMinimumProfileTotal), 3); + "%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", + logPrefixEnergyOptimisation, powerReductionShortage)); + } else if (powerLimitMinimumProfileTotal != null + && powerNetNewVirtual < powerLimitMinimumProfileTotal) { + double powerReductionShortage = + round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); LOG.warning( String.format( - "%s; Power net is %s kW below power limit minimum", logPrefix, powerExceededAmount)); - - if (powerNetNewVirtual < powerLimitMinimumProfileTotal) { - double powerReductionShortage = - round((powerNetNewVirtual - powerLimitMinimumProfileTotal), 3); - LOG.warning( - String.format( - "%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", - logPrefix, powerReductionShortage)); - } + "%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", + logPrefixEnergyOptimisation, powerReductionShortage)); } return powerSetpointsNew; } private Map batteryCalculatePowerSetpointsOnLimitBreach( - Map powerFlexibleAvailable, - Double powerConsumptionVirtual, - Double powerLimitVirtual, - String maxOrMin) { + double power, + double powerLimitVirtual, + List electricityBatteryAssets, + Map powerFlexibleAvailable, + Map powerSetpointsNewWithoutLimits) { Map powerSetpointsNew = new HashMap<>(); - boolean isMin = "min".equals(maxOrMin); + double powerChangeNeeded = round(power - powerLimitVirtual, 3); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); - // Calculate the total power adjustment needed - double powerReduction = powerConsumptionVirtual - powerLimitVirtual; + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + double powerSetpointNewWithoutLimits = + powerSetpointsNewWithoutLimits.getOrDefault(electricityBatteryAssetId, 0.0); + double chargePowerAvailableTotal = chargePowerAvailable - powerSetpointNewWithoutLimits; + double dischargePowerAvailableTotal = dischargePowerAvailable - powerSetpointNewWithoutLimits; - if (isMin) { - powerReduction = -powerReduction; + double powerSetpointChange = 0.0; + + if (powerChangeNeeded > 0) { + powerSetpointChange = Math.max(-powerChangeNeeded, dischargePowerAvailableTotal); + } else if (powerChangeNeeded < 0) { + powerSetpointChange = Math.min(-powerChangeNeeded, chargePowerAvailableTotal); + } + + powerChangeNeeded = round(powerChangeNeeded + powerSetpointChange, 3); + + double powerSetpointNew = round(powerSetpointNewWithoutLimits + powerSetpointChange, 3); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); } - // Iterate over each battery asset - for (Map.Entry entry : powerFlexibleAvailable.entrySet()) { - String electricityBatteryAssetId = entry.getKey(); - double[] powerBatteryAvailableChargeDischarge = entry.getValue(); + return powerSetpointsNew; + } - // Choose available power based on breach type - double powerBatteryAvailable = - isMin - ? -powerBatteryAvailableChargeDischarge[0] - : powerBatteryAvailableChargeDischarge[1]; + private Map batteryCalculatePowerSetpointsWithoutLimits( + List electricityBatteryAssets, + Map batteryEnergyLevelPercentageTargets, + EmsEnergyOptimisationAsset energyOptimisationAsset, + Map powerFlexibleAvailable, + Services services) { + // Find battery power set-points for a system without power limits + Map powerSetpointsNew = new HashMap<>(); + + long intervalMillis = 15 * 60000; + long endTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeMillis = endTimeMillis - endTimeMillis % intervalMillis; - if (powerReduction > 0) { - // Limit the power set-point to available battery power or remaining reduction needed - double setpoint = round(Math.max(-powerReduction, powerBatteryAvailable), 3); + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + // Set initial power set-point of 0 + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + + if (energyLevelPercentage == null) { + continue; + } - if (isMin) { - setpoint = -setpoint; + double energyCapacity = electricityBatteryAsset.getEnergyCapacity().orElse(0.0); + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + + // Get battery energy level percentage target + Integer energyLevelPercentageTarget = + batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); + + if (energyLevelPercentageTarget == null) { + Integer energyLevelPercentageMaximum = + electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = + electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + energyLevelPercentageTarget = + batteryCalculateEnergyLevelPercentageDefault( + energyLevelPercentageMaximum, + energyLevelPercentageMinimum, + energyOptimisationAsset); + } + + // Get battery power set-point target + AssetDatapointAllQuery assetDatapointQueryPredicted = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> powerSetpointForecastList = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + electricityBatteryAssetId, + EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), + assetDatapointQueryPredicted); + + // Calculate battery power set-point target + double energyLevelPercentageRounded = round(energyLevelPercentage, 1); + double powerNeeded = + round( + (energyLevelPercentageTarget - energyLevelPercentageRounded) * 0.01 * energyCapacity, + 3); + double powerSetpointNeeded = + round(powerNeeded * 60 * BATTERY_POWER_SETPOINT_RESPONSIVENESS_DEFAULT, 3); + + if (energyLevelPercentageRounded < energyLevelPercentageTarget + || (powerSetpointCurrent > 0.0 + && energyLevelPercentageRounded < energyLevelPercentageTarget)) { + // Start charging or continue charging + int chargeEfficiencyPercentage = electricityBatteryAsset.getChargeEfficiency().orElse(100); + + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double powerSetpointEfficiency = + round(powerSetpointNeeded / (chargeEfficiencyPercentage * 0.01), 3); + double powerSetpointNew = Math.min(powerSetpointEfficiency, chargePowerAvailable); + + if (!powerSetpointForecastList.isEmpty()) { + Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); + + if (powerSetpointForecast != null && powerSetpointForecast > 0) { + powerSetpointNew = Math.min(powerSetpointNew, powerSetpointForecast); + } } - powerSetpointsNew.put(electricityBatteryAssetId, setpoint); - powerReduction += setpoint; - } else { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } else if (energyLevelPercentageRounded > energyLevelPercentageTarget + || (powerSetpointCurrent < 0.0 + && energyLevelPercentageRounded > energyLevelPercentageTarget)) { + // Start discharging or continue discharging + int dischargeEfficiencyPercentage = + electricityBatteryAsset.getDischargeEfficiency().orElse(100); + + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + double powerSetpointEfficiency = + round(powerSetpointNeeded / (dischargeEfficiencyPercentage * 0.01), 3); + double powerSetpointNew = Math.max(powerSetpointEfficiency, dischargePowerAvailable); + + if (!powerSetpointForecastList.isEmpty()) { + Double powerSetpointForecast = (Double) powerSetpointForecastList.getLast().getValue(); + + if (powerSetpointForecast != null && powerSetpointForecast < 0) { + powerSetpointNew = Math.max(powerSetpointNew, powerSetpointForecast); + } + } + + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); } } @@ -1435,8 +1749,8 @@ private Map batteryCalculatePowerSetpointsOnLimitBreach( private void batteryCheckConnection( List electricityBatteryAssets, Services services) { - // This method checks if a battery is connected based on if the power and energyLevelPercentage - // attributes update within the active time interval + // This method checks if a battery is connected based on if the 'power' and + // 'energyLevelPercentage' attributes update within the active time interval String connected = EmsElectricityBatteryAsset.EmsElectricityBatteryConnectionStatusValueType.connected .toString(); @@ -1537,6 +1851,42 @@ private void batteryCheckConnection( } } + private Map batteryCheckPowerSetpointsCurrent( + List electricityBatteryAssets, + Map powerFlexibleAvailable) { + Map powerSetpointsNew = new HashMap<>(); + + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + Double energyLevelPercentage = + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + + if (energyLevelPercentage == null) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + continue; + } + + double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId).charge; + double dischargePowerAvailable = + powerFlexibleAvailable.get(electricityBatteryAssetId).discharge; + + // Set initial power set-point new + double powerSetpointNew = 0.0; + + // Check if power set-point current is within available power limits + if (powerSetpointCurrent > 0.0) { + powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); + } else if (powerSetpointCurrent < 0.0) { + powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); + } + + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); + } + + return powerSetpointsNew; + } + private void batteryCheckSetup(List electricityBatteryAssets) { for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { // Check if the battery is enabled @@ -1555,133 +1905,112 @@ private void batteryCheckSetup(List electricityBatte electricityBatteryAsset.getAssetName()); // Check if the following attributes are connected - Double energyLevelPercentage = - electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Double power = electricityBatteryAsset.getPower().orElse(null); - StringBuilder logMessageBatteryConnect = new StringBuilder(); - - if (energyLevelPercentage == null) { - logMessageBatteryConnect.append( - String.format(" '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName())); - } - - if (power == null) { - logMessageBatteryConnect.append( - String.format(" '%s',", EmsElectricityBatteryAsset.POWER.getName())); - } + Map requiredFieldsConnection = new HashMap<>(); + + requiredFieldsConnection.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), + electricityBatteryAsset.getEnergyLevelPercentage().orElse(null)); + requiredFieldsConnection.put( + EmsElectricityBatteryAsset.POWER.getName(), + electricityBatteryAsset.getPower().orElse(null)); + + List missingFieldsConnection = + requiredFieldsConnection.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .toList(); - if (!logMessageBatteryConnect.isEmpty()) { - logMessageBatteryConnect.setLength(logMessageBatteryConnect.length() - 1); - logMessageBatteryConnect.insert( - 0, + if (!missingFieldsConnection.isEmpty()) { + LOG.warning( String.format( - "%s; Can't use battery for flexible power. The following attributes are not connected:", - logPrefixBattery)); - LOG.warning(logMessageBatteryConnect.toString()); + "%s; Can't use battery for flexible power. The following attributes are not connected: %s", + logPrefixBattery, + String.join( + ", ", + missingFieldsConnection.stream().map(attr -> "'" + attr + "'").toList()))); } // Check if the following attributes are set - Double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(null); - Double dischargePowerMaximum = - electricityBatteryAsset.getDischargePowerMaximum().orElse(null); + Map requiredFieldsSetup = new HashMap<>(); + Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - StringBuilder logMessageBatterySet = new StringBuilder(); - - if (chargePowerMaximum == null) { - logMessageBatterySet.append( - String.format(" '%s',", EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName())); - } - - if (dischargePowerMaximum == null) { - logMessageBatterySet.append( - String.format(" '%s',", EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName())); - } - - if (energyLevelPercentageMaximum == null) { - logMessageBatterySet.append( - String.format( - " '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName())); - } - - if (energyLevelPercentageMinimum == null) { - logMessageBatterySet.append( - String.format( - " '%s',", EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName())); - } + Integer chargeEfficiency = electricityBatteryAsset.getChargeEfficiency().orElse(null); + Integer dischargeEfficiency = electricityBatteryAsset.getDischargeEfficiency().orElse(null); + + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.CHARGE_EFFICIENCY.getName(), chargeEfficiency); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.CHARGE_POWER_MAXIMUM.getName(), + electricityBatteryAsset.getChargePowerMaximum().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.DISCHARGE_EFFICIENCY.getName(), dischargeEfficiency); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.DISCHARGE_POWER_MAXIMUM.getName(), + electricityBatteryAsset.getDischargePowerMaximum().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_CAPACITY.getName(), + electricityBatteryAsset.getEnergyCapacity().orElse(null)); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), + energyLevelPercentageMaximum); + requiredFieldsSetup.put( + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMinimum); + + List missingFieldsSetup = + requiredFieldsSetup.entrySet().stream() + .filter(entry -> entry.getValue() == null) + .map(Map.Entry::getKey) + .toList(); - if (!logMessageBatterySet.isEmpty()) { - logMessageBatterySet.setLength(logMessageBatterySet.length() - 1); - logMessageBatterySet.insert( - 0, + if (!missingFieldsSetup.isEmpty()) { + LOG.warning( String.format( - "%s; Can't use battery for flexible power. The following attributes are not set:", - logPrefixBattery)); - LOG.warning(logMessageBatterySet.toString()); + "%s; Can't use battery for flexible power. The following attributes are not set: %s", + logPrefixBattery, + String.join( + ", ", missingFieldsSetup.stream().map(attr -> "'" + attr + "'").toList()))); } + // Check if the following attributes are set correctly if (energyLevelPercentageMaximum != null && energyLevelPercentageMinimum != null - && (energyLevelPercentageMaximum - energyLevelPercentageMinimum) - < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - int diff = energyLevelPercentageMaximum - energyLevelPercentageMinimum; + && energyLevelPercentageMaximum <= energyLevelPercentageMinimum) { LOG.warning( String.format( - "%s; Can't use battery for flexible power. %s - %s = %s - %s = %s < %s%%)", + "%s; Can't use battery for flexible power. '%s' = %s%% is smaller than or equal to '%s'= %s%%)", logPrefixBattery, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), - EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), energyLevelPercentageMaximum, - energyLevelPercentageMinimum, - diff, - BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)); + EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMinimum)); } - } - } - - private Map batteryCheckPowerSetpointsCurrent( - List electricityBatteryAssets, - Map powerFlexibleAvailable) { - Map powerSetpointsNew = new HashMap<>(); - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = - electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - if (energyLevelPercentage == null) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - continue; + if (chargeEfficiency != null && chargeEfficiency <= 0) { + LOG.warning( + String.format( + "The charge efficiency = %s%% is smaller than or equal to 0", chargeEfficiency)); } - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - double chargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - double dischargePowerAvailable = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; - - // Set initial power set-point new - double powerSetpointNew = 0.0; - - // Check if power set-point current is within available power limits - if (powerSetpointCurrent > 0.0) { - powerSetpointNew = Math.min(powerSetpointCurrent, chargePowerAvailable); - } else if (powerSetpointCurrent < 0.0) { - powerSetpointNew = Math.max(powerSetpointCurrent, dischargePowerAvailable); + if (dischargeEfficiency != null && dischargeEfficiency <= 0) { + LOG.warning( + String.format( + "The discharge efficiency = %s%% is smaller than or equal to 0", + dischargeEfficiency)); } - - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); } - - return powerSetpointsNew; } private Map batteryGetEnergyLevelPercentageTargetsCurrent( List electricityBatteryAssets, Services services) { Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); + long intervalMillis = 15 * 60000; long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - long endTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000) + (15 * 60000); + long endTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis + intervalMillis; for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { String batteryAssetId = electricityBatteryAsset.getId(); @@ -1708,132 +2037,28 @@ private Map batteryGetEnergyLevelPercentageTargetsCurrent( return batteryEnergyLevelPercentageTargetsCurrent; } - private Map batteryRestoreEnergyLevels( - EmsEnergyOptimisationAsset energyOptimisationAsset, - List electricityBatteryAssets, - Map powerFlexibleAvailable, - Double powerNet, - Double powerSetpointsCurrentSum, - Double powerLimitMinimumBatteries, - Double powerLimitMaximumBatteries, - Map batteryEnergyLevelPercentageTargets) { - Map powerSetpointsNew = new HashMap<>(); - - // Find battery power set-points for a system without power limits - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = - electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - - // Set initial power set-point of 0 - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - - if (energyLevelPercentage == null) { - continue; - } - - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - - // Get battery energy level target - Integer energyLevelPercentageTarget = - batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); - - if (energyLevelPercentageTarget == null) { - Integer energyLevelPercentageMaximum = - electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = - electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - energyLevelPercentageTarget = - batteryCalculateEnergyLevelPercentageDefault( - energyLevelPercentageMaximum, - energyLevelPercentageMinimum, - energyOptimisationAsset); - } - - if (energyLevelPercentage - < (energyLevelPercentageTarget - BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { - // Start charging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (energyLevelPercentage - > (energyLevelPercentageTarget + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)) { - // Start discharging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (powerSetpointCurrent > 0.0 - && energyLevelPercentage < energyLevelPercentageTarget) { - // Continue charging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[0]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } else if (powerSetpointCurrent < 0.0 - && energyLevelPercentage > energyLevelPercentageTarget) { - // Continue discharging - double powerSetpointNew = powerFlexibleAvailable.get(electricityBatteryAssetId)[1]; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointNew); - } - } - - // Adjust battery power set-points in a system with power limits - double powerSetpointsNewSum = - powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; - - Double chargingSpace = null; - Double dischargingSpace = null; - - if (powerLimitMaximumBatteries != null) { - chargingSpace = powerLimitMaximumBatteries - powerConsumptionVirtual; - } - - if (powerLimitMinimumBatteries != null) { - dischargingSpace = powerLimitMinimumBatteries - powerConsumptionVirtual; - } - - if (chargingSpace != null && powerSetpointsNewSum > chargingSpace) { - // Adjust battery power set-points in case of charging space shortage - double powerReduction = powerSetpointsNewSum - chargingSpace; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - - // Only adjust battery power set-point for charging batteries - if (powerSetpointNew > 0.0) { - powerReduction = powerReduction - powerSetpointNew; - - if (powerReduction >= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = round(Math.min(-powerReduction, powerSetpointNew), 3); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } - } - } - } else if (dischargingSpace != null && powerSetpointsNewSum < dischargingSpace) { - // Adjust battery power set-points in case of discharging space shortage - double powerReduction = powerSetpointsNewSum - dischargingSpace; - - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - - // Only adjust battery power set-point for discharging batteries - if (powerSetpointNew < 0.0) { - powerReduction = powerReduction - powerSetpointNew; - - if (powerReduction <= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = Math.max(-powerReduction, powerSetpointNew); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } - } - } - } - - return powerSetpointsNew; + private List batteryOrder( + List electricityBatteryAssets) { + List sorted = new ArrayList<>(electricityBatteryAssets); + + sorted.sort( + Comparator.comparing( + (EmsElectricityBatteryAsset b) -> + b.getEnergyCapacity().isPresent() + && b.getDischargePowerMaximum().orElse(0.0) != 0.0) + .reversed() + .thenComparing( + b -> + b.getDischargePowerMaximum().orElse(0.0) == 0 + ? null + : round( + b.getEnergyCapacity().orElse(0.0) + / b.getDischargePowerMaximum().orElse(0.0), + 2), + Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(EmsElectricityBatteryAsset::getId)); + + return sorted; } private void batteryUpdatePowerSetpoints( @@ -1855,6 +2080,59 @@ private void batteryUpdatePowerSetpoints( } } + private double calculatePowerFluctuationMargin( + EmsEnergyOptimisationAsset energyOptimisationAsset, + Double powerLimitProfileTotal, + String maxOrMin) { + if (powerLimitProfileTotal == null) { + return 0.0; + } + + boolean isMin = "min".equals(maxOrMin); + String fluctuationMarginAttributeName = POWER_LIMIT_MAXIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; + + if (isMin) { + fluctuationMarginAttributeName = POWER_LIMIT_MINIMUM_FLUCTUATION_MARGIN_ATTRIBUTE_NAME; + } + + Double powerLimitFluctuationMargin = + (Double) + energyOptimisationAsset + .getAttribute(fluctuationMarginAttributeName) + .flatMap(Attribute::getValue) + .orElse(null); + + if (powerLimitFluctuationMargin == null) { + powerLimitFluctuationMargin = + powerLimitProfileTotal * POWER_LIMIT_FLUCTUATION_MARGIN_PERCENTAGE_DEFAULT * 0.01; + } + + return round(Math.abs(powerLimitFluctuationMargin), 3); + } + + private Double calculatePowerLimitVirtual( + EmsEnergyOptimisationAsset energyOptimisationAsset, + Double powerLimitProfileTotal, + String maxOrMin) { + if (powerLimitProfileTotal == null) { + return null; + } + + double powerLimitFluctuationMargin = + calculatePowerFluctuationMargin(energyOptimisationAsset, powerLimitProfileTotal, maxOrMin); + boolean isMin = "min".equals(maxOrMin); + + double powerLimitVirtual; + + if (isMin) { + powerLimitVirtual = powerLimitProfileTotal + powerLimitFluctuationMargin; + } else { + powerLimitVirtual = powerLimitProfileTotal - powerLimitFluctuationMargin; + } + + return round(powerLimitVirtual, 3); + } + private Map calculateTariffChargeAndDischargeZones( List> tariffImportDatapoints, List> tariffExportDatapoints, @@ -1866,14 +2144,14 @@ private Map calculateTariffChargeAndDischargeZones( return chargeAndDischargeZonesMap; } - // Find the best import price for each day - ZoneId zoneId = ZoneId.systemDefault(); - + // Find the best import/export tariff window for each day List> tariffImportMovingAverage = movingAverage(tariffImportDatapoints, window); List> tariffExportMovingAverage = movingAverage(tariffExportDatapoints, window); + ZoneId zoneId = ZoneId.systemDefault(); + Map tariffImportDailyMinimumMap = IntStream.range(0, tariffImportMovingAverage.size()) .mapToObj( @@ -1976,27 +2254,6 @@ private EmsDayAheadAsset getDayAheadAsset( return asset; } - private List> getTariffDayAheadDatapoints( - Asset asset, String attributeName, Services services) { - // Get the start of the day (00:00) in milliseconds - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); - - // Get tariff data-points of yesterday, today and tomorrow - long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; - long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; - AssetDatapointAllQuery assetDatapointQueryHistoric = - new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - List> tariffHistoric = - services - .getAssetDatapointService() - .queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); - - return tariffHistoric; - } - private List> getTariffDatapoints( EmsEnergyOptimisationAsset energyOptimisationAsset, String attributeName, Services services) { // Get the start of the day (00:00) in milliseconds @@ -2023,13 +2280,35 @@ private List> getTariffDatapoints( .queryDatapoints( energyOptimisationAsset.getId(), attributeName, assetDatapointQueryPredicted); - // Combine historic and predicted data-points into one list - List> tariffCombined = new ArrayList<>(tariffHistoric); - tariffCombined.addAll(tariffPredicted); + // Combine historic and predicted data-points, timestamps are ordered from newest to oldest + // (descending order) + List> tariffCombined = new ArrayList<>(tariffPredicted); + tariffCombined.addAll(tariffHistoric); return tariffCombined; } + private List> getTariffDayAheadDatapoints( + Asset asset, String attributeName, Services services) { + // Get the start of the day (00:00) in milliseconds + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + ZoneId zoneId = ZoneId.systemDefault(); + LocalDate date = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); + long startOfCurrentDayMillis = date.atStartOfDay(zoneId).toInstant().toEpochMilli(); + + // Get tariff data-points of yesterday, today and tomorrow + long startTimeMillis = startOfCurrentDayMillis - 24 * 60 * 60000; + long endTimeMillis = startOfCurrentDayMillis + 2 * 24 * 60 * 60000; + AssetDatapointAllQuery assetDatapointQueryHistoric = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> tariffHistoric = + services + .getAssetDatapointService() + .queryDatapoints(asset.getId(), attributeName, assetDatapointQueryHistoric); + + return tariffHistoric; + } + public static List> intervalAverage( List> dataPoints, long intervalMillis) { // Map @@ -2111,8 +2390,7 @@ private List> intervalInterpolate( return interpolatedList; } - public static List> movingAverage( - List> dataPoints, int window) { + public List> movingAverage(List> dataPoints, int window) { List> result = new ArrayList<>(); if (window <= 0 || dataPoints.size() < window) { return result; @@ -2121,24 +2399,21 @@ public static List> movingAverage( double sum = 0.0; for (int i = 0; i < dataPoints.size(); i++) { - // Extract numeric value - Number value = (Number) dataPoints.get(i).getValue(); - sum += value.doubleValue(); + Double value = (Double) dataPoints.get(i).getValue(); + sum += value; - // Remove value exiting the sliding window if (i >= window) { - Number oldValue = (Number) dataPoints.get(i - window).getValue(); - sum -= oldValue.doubleValue(); + Double valueOld = (Double) dataPoints.get(i - window).getValue(); + sum -= valueOld; } - // When the window is "full", generate output datapoint if (i >= window - 1) { double avg = sum / window; - // Use the timestamp of the window end (index i) ValueDatapoint original = dataPoints.get(i); - ValueDatapoint averaged = new ValueDatapoint<>(original.getTimestamp(), avg); + ValueDatapoint averaged = + new ValueDatapoint<>(original.getTimestamp(), round(avg, 7)); result.add(averaged); } } @@ -2184,102 +2459,105 @@ private void updateDayAheadAsset( } } + Long lastUpdateForecastsTimestamp = + dayAheadAsset.getLastUpdateForecastsTimestamp().orElse(null); + + if (collectTime == null || lastUpdateForecastsTimestamp == null) { + return; + } + + // Calculate the 15-minute interval for collecting the day ahead tariff forecasts + LocalDate currentDate = LocalDate.now(); + LocalDateTime currentCollectDateTime = LocalDateTime.of(currentDate, collectTime); + long collectTimeStartMillis = + currentCollectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long collectTimeEndMillis = collectTimeStartMillis + 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + + String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); + LocalDateTime lastUpdateDateTime = currentCollectDateTime.plusDays(-1); + + if (!lastUpdateForecasts.isEmpty()) { + lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); + } + + LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + // Collect the day ahead tariff forecasts at the desired collect time - if (collectTime != null) { - // Calculate the 15-minute interval for collecting the day ahead tariff forecasts - LocalDate currentDate = LocalDate.now(); - LocalDateTime collectDateTime = LocalDateTime.of(currentDate, collectTime); - long collectTimeMillisStart = - collectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long collectTimeMillisEnd = collectTimeMillisStart + 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - - if (currentTimeMillis >= collectTimeMillisStart && currentTimeMillis < collectTimeMillisEnd) { - // Create asset datapoint query - LocalDate tomorrowDate = currentDate.plusDays(1); - LocalDateTime startOfNextDay = tomorrowDate.atStartOfDay(); - long startTimeMillis = - startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; - AssetDatapointAllQuery assetDatapointQuery = - new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - // Get the tariffs number of data-points from day ahead asset - int tariffExportDayAheadSize = - services - .getAssetDatapointService() - .queryDatapoints( - dayAheadAssetId, - EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), - assetDatapointQuery) - .size(); - int tariffImportDayAheadSize = - services - .getAssetDatapointService() - .queryDatapoints( - dayAheadAssetId, - EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), - assetDatapointQuery) - .size(); - - // Only update the historic datapoint table if there are no day ahead tariffs present - if (tariffExportDayAheadSize == 0) { - List> tariffExport = - services - .getAssetPredictedDatapointService() - .queryDatapoints( - energyOptimisationAsset.getId(), - EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), - assetDatapointQuery); + if (currentTimeMillis >= collectTimeStartMillis + && currentTimeMillis < collectTimeEndMillis + && collectTimeStartMillis > lastUpdateForecastsTimestamp + && currentDate.isEqual(nextUpdateDate)) { + // Create asset datapoint query + LocalDateTime startOfNextDay = currentDate.plusDays(1).atStartOfDay(); + long startTimeMillis = + startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; + AssetDatapointAllQuery assetDatapointQuery = + new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get the tariffs number of data-points from day ahead asset + int tariffExportDayAheadSize = services .getAssetDatapointService() - .upsertValues( + .queryDatapoints( dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), - tariffExport); - } - - if (tariffImportDayAheadSize == 0) { - List> tariffImport = - services - .getAssetPredictedDatapointService() - .queryDatapoints( - energyOptimisationAsset.getId(), - EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), - assetDatapointQuery); + assetDatapointQuery) + .size(); + int tariffImportDayAheadSize = services .getAssetDatapointService() - .upsertValues( + .queryDatapoints( dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), - tariffImport); - } - - // Update the last update forecasts datetime field - String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); - String lastUpdateForecastsNew = collectDateTime.toString(); - - LocalDateTime lastUpdateDateTime = collectDateTime.plusDays(-1); - - if (!lastUpdateForecasts.isEmpty()) { - lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); - } + assetDatapointQuery) + .size(); - LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + // Only update the historic data-point table if there are no day ahead tariffs present in the + // historic data-point table for current interval + if (tariffExportDayAheadSize == 0) { + List> tariffExport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); + } - if (nextUpdateDate.isEqual(currentDate)) { - services - .getAssetProcessingService() - .sendAttributeEvent( - new AttributeEvent( - dayAheadAssetId, - EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), - lastUpdateForecastsNew, - collectTimeMillisStart)); - } + if (tariffImportDayAheadSize == 0) { + List> tariffImport = + services + .getAssetPredictedDatapointService() + .queryDatapoints( + energyOptimisationAsset.getId(), + EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), + assetDatapointQuery); + services + .getAssetDatapointService() + .upsertValues( + dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), tariffImport); } + + // Update the 'last update forecasts' datetime field with current update datetime + String lastUpdateForecastsNew = currentCollectDateTime.toString(); + services + .getAssetProcessingService() + .sendAttributeEvent( + new AttributeEvent( + dayAheadAssetId, + EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), + lastUpdateForecastsNew, + collectTimeStartMillis)); } } - record IndexedDatapoint(long timestamp, double value, int index) {} + private record ChargeDischarge(double charge, double discharge) {} + + private record IndexedDatapoint(long timestamp, double value, int index) {} }