From 847ca7614c1d0d07364b9400d8aeac587a6655cd Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Mon, 23 Feb 2026 11:55:57 +0100 Subject: [PATCH 1/3] Update 'EMS optimisation' with 'EMS optimisation beta' changes --- .../optimisationMethods/EmsOptimisation.java | 1636 +++++++++++------ 1 file changed, 1107 insertions(+), 529 deletions(-) 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 4bdacc4..ce98b6d 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 @@ -38,6 +38,7 @@ import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.openremote.model.syslog.SyslogCategory.DATA; @@ -48,15 +49,16 @@ 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 + // 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 battery energy level percentage settings - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_TARGET_DEFAULT = 50; - private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL = 5; + // Battery settings + private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT = 50; + private final int BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL = 3; + private final int BATTERY_EFFICIENCY_BUFFER_PERCENTAGE = 3; @Override @@ -81,6 +83,8 @@ 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() @@ -120,7 +124,16 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal != null && powerLimitMaximumProfileTotal <= powerLimitMinimumProfileTotal) { LOG.warning(String.format("%s; Failed to perform '%s' energy optimisation method. The '%s'= %s kW is lower than or equal to '%s'= %s kW for timestamp='%s'", - logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal, powerLimitMaximumProfileDateTime)); + logPrefixEnergyOptimisation, optimisationMethodName, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), powerLimitMaximumProfileTotal, + EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), powerLimitMinimumProfileTotal, powerLimitMaximumProfileDateTime)); + } + + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + + // Update day ahead asset at specific time of day + if (dayAheadAsset != null) { + updateDayAheadAsset(energyOptimisationAsset, dayAheadAsset, services); } // Check for battery assets @@ -141,573 +154,869 @@ private void runOptimisationMethodEms(String energyOptimisationAssetId, Services batteryUpdatePowerSetpoints(electricityBatteryAssets, powerSetpointsNew, services); } - private Map batteryCalculatePowerSetpoints(EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, Services services, String logPrefix) { - Map powerFlexibleAvailable = batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); - Map powerSetpointsNew; + private int batteryCalculateEnergyLevelPercentageDefault(Integer energyLevelPercentageMaximumBattery, Integer energyLevelPercentageMinimumBattery, EmsEnergyOptimisationAsset energyOptimisationAsset) { + if (energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { + return BATTERY_ENERGY_LEVEL_PERCENTAGE_DEFAULT; + } - // Get latest power set-point across all batteries - long powerSetpointTimestampMillisLatest = 0L; + Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumInput().orElse(null); + Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumInput().orElse(null); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - long powerSetpointTimestampMillis = electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); + int energyLevelPercentageDefault; - if (powerSetpointTimestampMillis > powerSetpointTimestampMillisLatest) { - powerSetpointTimestampMillisLatest = powerSetpointTimestampMillis; - } + if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { + energyLevelPercentageDefault = energyLevelPercentageMinimumBattery; + } else if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { + energyLevelPercentageDefault = energyLevelPercentageMaximumBattery; + } else { + energyLevelPercentageDefault = (int) Math.round((double) (energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) / 2 + energyLevelPercentageMinimumBattery); } - // Check if power net is updated since last battery power set-points update - Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); - long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); + return energyLevelPercentageDefault; + } - // Turn off batteries that do not have flexible power available during main power meter disconnect - if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { - powerSetpointsNew = batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); - return powerSetpointsNew; + private Map batteryCalculateForecasts(List electricityBatteryAssets, EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { + Map batteryEnergyLevelPercentageTargets = new HashMap<>(); + + // Get power consumption forecast for 1 week + long intervalMillis = 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long startTimeMillis = currentTimeMillis - currentTimeMillis % intervalMillis; + long endTimeMillis = startTimeMillis + 7 * 24 * 60 * 60000; + + String energyOptimisationAssetId = energyOptimisationAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryConsumptionPredicted = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + List> energyOptimisationPowerConsumptionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_CONSUMPTION.getName(), assetDatapointQueryConsumptionPredicted); + +// System.out.println("energyOptimisationPowerConsumptionPredicted = " + energyOptimisationPowerConsumptionPredicted); + + if (energyOptimisationPowerConsumptionPredicted.isEmpty()) { + return batteryEnergyLevelPercentageTargets; } - // Check if power limits are present and calculate additional power limits - Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); - Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); + // Calculate power average for each 15-minute interval + List> totalPowerConsumptionAveraged = intervalAverage(energyOptimisationPowerConsumptionPredicted, intervalMillis); + totalPowerConsumptionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - Double powerLimitMaximumBatteries = null; +// System.out.println("totalPowerConsumptionAveraged = " + totalPowerConsumptionAveraged); - if (powerLimitMaximumProfileTotal != null) { - powerLimitMaximumBatteries = Math.round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01) * 1000.0) / 1000.0; + long startTimestampMillis = totalPowerConsumptionAveraged.getFirst().getTimestamp(); + long endTimestampMillis = totalPowerConsumptionAveraged.getLast().getTimestamp(); + + // Interpolate power average values for each 15-minute interval + List> totalPowerConsumptionInterpolated = intervalInterpolate(totalPowerConsumptionAveraged, startTimestampMillis, endTimestampMillis, intervalMillis); + + List timestampsMillisList = new ArrayList<>(); + List totalPowerConsumptionList = new ArrayList<>(); + + for (ValueDatapoint datapoint : totalPowerConsumptionInterpolated) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); + + timestampsMillisList.add(timestampMillis); + totalPowerConsumptionList.add(value); } - Double powerLimitMinimumBatteries = null; +// System.out.println("timestampsMillisList = " + timestampsMillisList); +// System.out.println("totalPowerConsumptionList = " + totalPowerConsumptionList); - if (powerLimitMinimumProfileTotal != null) { - powerLimitMinimumBatteries = Math.round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01) * 1000.0) / 1000.0; + if (totalPowerConsumptionList.isEmpty()) { + return batteryEnergyLevelPercentageTargets; } - // TODO: add battery limits cross-over warning + AssetDatapointAllQuery assetDatapointQueryPeriodPredicted = new AssetDatapointAllQuery(startTimestampMillis, endTimestampMillis); + List> energyOptimisationPowerProductionPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_PRODUCTION.getName(), assetDatapointQueryPeriodPredicted); - // Calculate battery energy level target - int batteryEnergyLevelPercentageTarget = BATTERY_ENERGY_LEVEL_PERCENTAGE_TARGET_DEFAULT; +// System.out.println("energyOptimisationPowerProductionPredicted = " + energyOptimisationPowerProductionPredicted); - if (powerLimitMaximumProfileTotal != null && powerLimitMinimumProfileTotal == null) { - batteryEnergyLevelPercentageTarget = 100; - } else if (powerLimitMaximumProfileTotal == null && powerLimitMinimumProfileTotal != null) { - batteryEnergyLevelPercentageTarget = 0; - } + Map totalPowerProductionMap = new HashMap<>(); - // Calculate battery energy level forecast - Map batteryEnergyLevelPercentageTargets = batteryCalculateEnergyLevelPercentageTargetsCurrent(batteryEnergyLevelPercentageTarget, electricityBatteryAssets, energyOptimisationAsset, services); + if (!energyOptimisationPowerProductionPredicted.isEmpty()) { + // Calculate power average for each 15-minute interval + List> totalPowerProductionAveraged = intervalAverage(energyOptimisationPowerProductionPredicted, intervalMillis); + totalPowerProductionAveraged.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - // Calculate virtual power consumption - Map powerSetpointsCurrent = new HashMap<>(); + // Interpolate power average values for each 15-minute interval + List> totalPowerProductionInterpolated = intervalInterpolate(totalPowerProductionAveraged, totalPowerProductionAveraged.getFirst().getTimestamp(), totalPowerProductionAveraged.getLast().getTimestamp(), intervalMillis); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); - powerSetpointsCurrent.put(electricityBatteryAssetId, powerSetpoint); + totalPowerProductionMap = totalPowerProductionInterpolated.stream().collect(Collectors.toMap(ValueDatapoint::getTimestamp, dp -> ((Double) dp.getValue()))); } - double powerSetpointsCurrentSum = powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + // List to store the sum of total power consumption, production and flexible power + List totalPowerConsumptionProductionFlexList = new ArrayList<>(); - // TODO: change fixed percentage based power limit to a dynamic volatility limit + // Sum total power consumption and production + for (int i = 0; i < timestampsMillisList.size(); i++) { + Double powerConsumption = totalPowerConsumptionList.get(i); + Double powerProduction = totalPowerProductionMap.get(timestampsMillisList.get(i)); - // Calculate new power set-points - if (powerLimitMaximumProfileTotal != null && powerConsumptionVirtual > powerLimitMaximumProfileTotal) { - Double powerLimitMaximumVirtual = Math.round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01) * 1000.0) / 1000.0; - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMaximumVirtual, "max"); - } else if (powerLimitMinimumProfileTotal != null && powerConsumptionVirtual < powerLimitMinimumProfileTotal) { - Double powerLimitMinimumVirtual = Math.round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01) * 1000.0) / 1000.0; - powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMinimumVirtual, "min"); - } else { - powerSetpointsNew = batteryRestoreEnergyLevels(electricityBatteryAssets, powerFlexibleAvailable, powerNet, powerSetpointsCurrentSum, powerLimitMinimumBatteries, powerLimitMaximumBatteries, batteryEnergyLevelPercentageTargets); + if (powerProduction != null) { + double sum = powerConsumption + powerProduction; + totalPowerConsumptionProductionFlexList.add(sum); + } else { + totalPowerConsumptionProductionFlexList.add(powerConsumption); + } } - // Create log messages on power limit breach - double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; +// System.out.println("totalPowerConsumptionProductionFlexList = " + totalPowerConsumptionProductionFlexList); +// System.out.println(); - if (powerLimitMaximumProfileTotal != null && powerNet > powerLimitMaximumProfileTotal) { - double powerExceededAmount = Math.round((powerNet - powerLimitMaximumProfileTotal) * 1000.0) / 1000.0; - LOG.warning(String.format("%s; Power net is %s kW above power limit maximum", logPrefix, powerExceededAmount)); + // Get power limits forecasts + List> energyOptimisationPowerLimitMaximumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MAXIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); + List> energyOptimisationPowerLimitMinimumPredicted = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAssetId, EmsEnergyOptimisationAsset.POWER_LIMIT_MINIMUM_PROFILE_TOTAL.getName(), assetDatapointQueryPeriodPredicted); - if (powerNetNewVirtual > powerLimitMaximumProfileTotal) { - double powerReductionShortage = Math.round((powerNetNewVirtual - powerLimitMaximumProfileTotal) * 1000.0) / 1000.0; - 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 = Math.round((powerNet - powerLimitMinimumProfileTotal) * 1000.0) / 1000.0; - LOG.warning(String.format("%s; Power net is %s kW below power limit minimum", logPrefix, powerExceededAmount)); + Map powerLimitMaximumMap = new HashMap<>(); + Map powerLimitMinimumMap = new HashMap<>(); - if (powerNetNewVirtual < powerLimitMinimumProfileTotal) { - double powerReductionShortage = Math.round((powerNetNewVirtual - powerLimitMinimumProfileTotal) * 1000.0) / 1000.0; - LOG.warning(String.format("%s; Not enough flexible power to get above power limit minimum; Shortage of %s kW", logPrefix, powerReductionShortage)); - } + for (ValueDatapoint dp : energyOptimisationPowerLimitMaximumPredicted) { + powerLimitMaximumMap.put(dp.getTimestamp(), (Double) dp.getValue()); } - return powerSetpointsNew; - } + for (ValueDatapoint dp : energyOptimisationPowerLimitMinimumPredicted) { + powerLimitMinimumMap.put(dp.getTimestamp(), (Double) dp.getValue()); + } - private Map batteryRestoreEnergyLevels(List electricityBatteryAssets, Map powerFlexibleAvailable, Double powerNet, Double powerSetpointsCurrentSum, Double powerLimitMinimumBatteries, Double powerLimitMaximumBatteries, Map batteryEnergyLevelPercentageTargets) { - Map powerSetpointsNew = new HashMap<>(); + List chargePowerAvailableTotalList = new ArrayList<>(); + List dischargePowerAvailableTotalList = new ArrayList<>(); - // Find battery power set-points for a system without power limits + // Add current available charge and discharge power + chargePowerAvailableTotalList.add(0.0); + dischargePowerAvailableTotalList.add(0.0); + + double intervalHour = (double) intervalMillis / (60 * 60000); + + // Calculate forecast for each battery for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + String batteryAssetId = electricityBatteryAsset.getId(); + Integer chargeEfficiencyBattery = electricityBatteryAsset.getChargeEfficiency().orElse(null); + Double chargePowerMaximumBattery = electricityBatteryAsset.getChargePowerMaximum().orElse(null); + Integer dischargeEfficiencyBattery = electricityBatteryAsset.getDischargeEfficiency().orElse(null); + Double dischargePowerMaximumBattery = electricityBatteryAsset.getDischargePowerMaximum().orElse(null); + Double energyCapacityBattery = electricityBatteryAsset.getEnergyCapacity().orElse(null); + Double energyLevelPercentageBattery = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximumBattery = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimumBattery = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); - // Set initial power set-point of 0 - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + if (chargeEfficiencyBattery == null || chargePowerMaximumBattery == null || dischargeEfficiencyBattery == null || dischargePowerMaximumBattery == null || energyCapacityBattery == null || + energyLevelPercentageBattery == null || energyLevelPercentageMaximumBattery == null || energyLevelPercentageMinimumBattery == null) { + continue; + } - if (energyLevelPercentage == null) { + // Check if the energy level percentage maximum and minimum are valid + if ((energyLevelPercentageMaximumBattery - energyLevelPercentageMinimumBattery) < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { continue; } - double powerSetpointCurrent = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + // Calculate total available charge and discharge power + for (int i = 0; i < timestampsMillisList.size(); i++) { + long timestampMillis = timestampsMillisList.get(i); + double totalPower = totalPowerConsumptionProductionFlexList.get(i); - // Get battery energy level target - Integer energyLevelPercentageTarget = batteryEnergyLevelPercentageTargets.get(electricityBatteryAssetId); + Double powerLimitMaximum = powerLimitMaximumMap.getOrDefault(timestampMillis, null); + Double powerLimitMinimum = powerLimitMinimumMap.getOrDefault(timestampMillis, null); - if (energyLevelPercentageTarget == null) { - energyLevelPercentageTarget = BATTERY_ENERGY_LEVEL_PERCENTAGE_TARGET_DEFAULT; - } + if (powerLimitMaximum != null) { + double powerLimitMaximumVirtual = powerLimitMaximum * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + double chargePowerTotalAvailable = powerLimitMaximumVirtual - totalPower; + chargePowerAvailableTotalList.add(chargePowerTotalAvailable); + } else { + chargePowerAvailableTotalList.add(Double.POSITIVE_INFINITY); + } - if (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); + if (powerLimitMinimum != null) { + double powerLimitMinimumVirtual = powerLimitMinimum * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01); + double dischargePowerTotalAvailable = powerLimitMinimumVirtual - totalPower; + dischargePowerAvailableTotalList.add(dischargePowerTotalAvailable); + } else { + dischargePowerAvailableTotalList.add(Double.NEGATIVE_INFINITY); + } } - } - // Adjust battery power set-points in a system with power limits - double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); - double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; + int numberOfDataPoints = chargePowerAvailableTotalList.size(); - Double chargingSpace = null; - Double dischargingSpace = null; +// System.out.println("numberOfDataPoints = " + numberOfDataPoints); +// System.out.println("chargePowerAvailableTotalList = " + chargePowerAvailableTotalList); +// System.out.println("dischargePowerAvailableTotalList = " + dischargePowerAvailableTotalList); - if (powerLimitMaximumBatteries != null) { - chargingSpace = powerLimitMaximumBatteries - powerConsumptionVirtual; - } + List chargeNeededTotalList = dischargePowerAvailableTotalList.stream().map(x -> x > 0 ? x : 0).toList(); + List dischargeNeededTotalList = chargePowerAvailableTotalList.stream().map(x -> x < 0 ? x : 0).toList(); - if (powerLimitMinimumBatteries != null) { - dischargingSpace = powerLimitMinimumBatteries - powerConsumptionVirtual; - } +// System.out.println("chargeNeededTotalList = " + chargeNeededTotalList); +// System.out.println("dischargeNeededTotalList = " + dischargeNeededTotalList); -// System.out.println("RESTORE"); -// System.out.println("chargingSpace: " + chargingSpace); -// System.out.println("dischargingSpace: " + dischargingSpace); + 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(); - if (chargingSpace != null && powerSetpointsNewSum > chargingSpace) { - // Adjust battery power set-points in case of charging space shortage - double powerReduction = powerSetpointsNewSum - chargingSpace; +// System.out.println("chargeAvailableBatteryList = " + chargePowerAvailableBatteryList); +// System.out.println("dischargeAvailableBatteryList = " + dischargePowerAvailableBatteryList); +// System.out.println(); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); + // Convert power to energy level percentage + List chargePercentageNeededTotalList = new ArrayList<>(); + List dischargePercentageNeededTotalList = new ArrayList<>(); - // Only adjust battery power set-point for charging batteries - if (powerSetpointNew > 0.0) { - powerReduction = powerReduction - powerSetpointNew; + chargeEfficiencyBattery = chargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; + dischargeEfficiencyBattery = dischargeEfficiencyBattery - BATTERY_EFFICIENCY_BUFFER_PERCENTAGE; - if (powerReduction >= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = Math.round(Math.min(-powerReduction, powerSetpointNew) * 1000.0) / 1000.0; - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } - } + for (int i = 0; i < numberOfDataPoints; i++) { + double c = intervalHour * chargeNeededTotalList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; + double d = 10000 * intervalHour * dischargeNeededTotalList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageNeededTotalList.add(c); + dischargePercentageNeededTotalList.add(d); } - } 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); +// System.out.println("chargePercentageNeededTotalList = " + chargePercentageNeededTotalList); +// System.out.println("dischargePercentageNeededTotalList = " + dischargePercentageNeededTotalList); - // Only adjust battery power set-point for discharging batteries - if (powerSetpointNew < 0.0) { - powerReduction = powerReduction - powerSetpointNew; + List chargePercentageAvailableBatteryList = new ArrayList<>(); + List dischargePercentageAvailableBatteryList = new ArrayList<>(); - if (powerReduction <= 0.0) { - powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - } else { - double powerSetpointAdjusted = Math.max(-powerReduction, powerSetpointNew); - powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); - break; - } + for (int i = 0; i < numberOfDataPoints; i++) { + double c = intervalHour * chargePowerAvailableBatteryList.get(i) * chargeEfficiencyBattery / energyCapacityBattery; + double d = 10000 * intervalHour * dischargePowerAvailableBatteryList.get(i) / (energyCapacityBattery * dischargeEfficiencyBattery); + chargePercentageAvailableBatteryList.add(c); + dischargePercentageAvailableBatteryList.add(d); + } + +// System.out.println("chargePercentageAvailableBatteryList = " + chargePercentageAvailableBatteryList); +// System.out.println("dischargePercentageAvailableBatteryList = " + dischargePercentageAvailableBatteryList); +// System.out.println(); + + // Calculate forecast when current energy level percentage is outside limits + int getToLimitIndex = 0; + double getToLimitPercentage = energyLevelPercentageBattery; + List getToLimitPercentageList = new ArrayList<>(); + + for (int i = 0; i < numberOfDataPoints; i++) { + getToLimitIndex = i; + + if (getToLimitPercentage > energyLevelPercentageMaximumBattery) { + double dischargePercentageNeeded = energyLevelPercentageMaximumBattery - getToLimitPercentage; + double dischargePercentageAvailable = Math.max(dischargePercentageAvailableBatteryList.get(i), dischargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + dischargePercentageAvailable; + dischargePercentageAvailableBatteryList.set(i, dischargePercentageAvailableBatteryList.get(i) - dischargePercentageAvailable); + getToLimitPercentageList.add(getToLimitPercentage); + } else if (getToLimitPercentage < energyLevelPercentageMinimumBattery) { + double chargePercentageNeeded = energyLevelPercentageMinimumBattery - getToLimitPercentage; + double chargePercentageAvailable = Math.min(chargePercentageAvailableBatteryList.get(i), chargePercentageNeeded); + getToLimitPercentage = getToLimitPercentage + chargePercentageAvailable; + chargePercentageAvailableBatteryList.set(i, chargePercentageAvailableBatteryList.get(i) - chargePercentageAvailable); + getToLimitPercentageList.add(getToLimitPercentage); + } else { + break; } } - } - return powerSetpointsNew; - } +// System.out.println("getToLimitIndex = " + getToLimitIndex); +// System.out.println("getToLimitPercentage = " + getToLimitPercentage); +// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); +// System.out.println("chargePercentageAvailableBatteryList = " + chargePercentageAvailableBatteryList); +// System.out.println("dischargePercentageAvailableBatteryList = " + dischargePercentageAvailableBatteryList); - private Map batteryCalculateEnergyLevelPercentageTargetsCurrent(int batteryEnergyLevelPercentageTargetDefault, List electricityBatteryAssets, EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - // Calculate the general energy level percentage target forecast - Map energyLevelPercentageTargetMap = calculateGeneralEnergyLevelPercentageTargetForecast(batteryEnergyLevelPercentageTargetDefault, energyOptimisationAsset, services); + List energyLevelPredictionList = new ArrayList<>(); - // Set default energy level percentage targets - Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String assetId = electricityBatteryAsset.getId(); - batteryEnergyLevelPercentageTargetsCurrent.put(assetId, BATTERY_ENERGY_LEVEL_PERCENTAGE_TARGET_DEFAULT); - } + if (getToLimitPercentage > energyLevelPercentageMaximumBattery || getToLimitPercentage < energyLevelPercentageMinimumBattery) { + // Energy level percentage forecast when whole forecast is outside of battery percentage limits + energyLevelPredictionList = getToLimitPercentageList; + } else { + // Calculate energy level percentage forecast starting from value within battery percentage limits + List chargePercentageWantBatteryList = new ArrayList<>(); + List dischargePercentageWantBatteryList = new ArrayList<>(); - if (energyLevelPercentageTargetMap.isEmpty()) { - return batteryEnergyLevelPercentageTargetsCurrent; - } + for (int i = 0; i < numberOfDataPoints; i++) { + chargePercentageWantBatteryList.add(Math.min(chargePercentageNeededTotalList.get(i), chargePercentageAvailableBatteryList.get(i))); + dischargePercentageWantBatteryList.add(Math.max(dischargePercentageNeededTotalList.get(i), dischargePercentageAvailableBatteryList.get(i))); + } - // Calculate energy level percentage targets - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); +// System.out.println("chargePercentageWantBatteryList = " + chargePercentageWantBatteryList); +// System.out.println("dischargePercentageWantBatteryList = " + dischargePercentageWantBatteryList); - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - String electricityBatteryAssetId = electricityBatteryAsset.getId(); + List chargeAndDischargePercentageWantBatteryList = new ArrayList<>(); - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + for (int i = 0; i < numberOfDataPoints; i++) { + chargeAndDischargePercentageWantBatteryList.add(chargePercentageWantBatteryList.get(i) + dischargePercentageWantBatteryList.get(i)); + } - if (energyLevelPercentageMaximum == null || energyLevelPercentageMinimum == null) { - continue; - } +// System.out.println("chargeAndDischargePercentageWantBatteryList = " + chargeAndDischargePercentageWantBatteryList); +// System.out.println(); - // Check if the energy level percentage maximum and minimum are valid - if ((energyLevelPercentageMaximum - energyLevelPercentageMinimum) < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - continue; - } + // Calculate forecast without battery percentage limits + int startRunningSumIndex = getToLimitIndex > 0 ? getToLimitIndex - 1 : 0; + List runningSumList = new ArrayList<>(); + double sum = getToLimitPercentage; - // Calculate the energy level percentage target forecast - Map batteryEnergyLevelPercentageTargetMap = new HashMap<>(); - - for (Map.Entry entry : energyLevelPercentageTargetMap.entrySet()) { - long timestampMillis = entry.getKey(); - int value = entry.getValue(); - - Integer batteryEnergyLevelPercentageTarget = null; - - if (value == BATTERY_ENERGY_LEVEL_PERCENTAGE_TARGET_DEFAULT) { - batteryEnergyLevelPercentageTarget = (energyLevelPercentageMaximum + energyLevelPercentageMinimum) / 2; - } else if (value == 100 && energyLevelPercentageMaximum >= 100) { - batteryEnergyLevelPercentageTarget = 100; - } else if (value == 100) { - batteryEnergyLevelPercentageTarget = energyLevelPercentageMaximum - BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL; - } else if (value == 0) { - // TODO: Remove when power net forecast is included in the power set-point prediction - batteryEnergyLevelPercentageTarget = energyLevelPercentageMinimum + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL + 25; - if (batteryEnergyLevelPercentageTarget > 100) { - batteryEnergyLevelPercentageTarget = 100; - } -// batteryEnergyLevelPercentageTarget = energyLevelPercentageMinimum + BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL; + for (int i = startRunningSumIndex; i < numberOfDataPoints; i++) { + sum = sum + chargeAndDischargePercentageWantBatteryList.get(i); + runningSumList.add(sum); } - batteryEnergyLevelPercentageTargetMap.put(timestampMillis, batteryEnergyLevelPercentageTarget); - } +// System.out.println("getToLimitPercentageList = " + getToLimitPercentageList); +// System.out.println("runningSumList = " + runningSumList); +// System.out.println(); -// System.out.println("batteryEnergyLevelPercentageTargetMap"); -// batteryEnergyLevelPercentageTargetMap.entrySet().stream() -// .sorted(Map.Entry.comparingByKey().reversed()) -// .forEach(entry -> System.out.println(entry.getKey() + "," + entry.getValue())); + // Combine outside limits and running sum energy level percentages + if (getToLimitPercentageList.size() > 1) { + energyLevelPredictionList.addAll(getToLimitPercentageList.subList(0, getToLimitPercentageList.size() - 1)); + } + energyLevelPredictionList.addAll(runningSumList); - // Get the current energy level percentage target - long energyLevelPercentageTargetTimestampCurrent = currentTimeMillis - currentTimeMillis % 900000; - Integer energyLevelPercentageTargetCurrent = batteryEnergyLevelPercentageTargetMap.get(energyLevelPercentageTargetTimestampCurrent); - batteryEnergyLevelPercentageTargetsCurrent.put(electricityBatteryAssetId, energyLevelPercentageTargetCurrent); +// System.out.println("energyLevelPredictionList = " + energyLevelPredictionList); +// System.out.println(); - // Update forecasts every 15 minutes - long endTimeMillis = energyLevelPercentageTargetTimestampCurrent + 900000; - AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); - List> energyLevelPercentagePredictedCurrent = services.getAssetPredictedDatapointService().queryDatapoints(electricityBatteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), assetDatapointQueryPredicted); + // Calculate forecast starting within battery percentage limits + double energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); + double energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); -// System.out.println("energyLevelPercentagePredictedCurrent: " + energyLevelPercentagePredictedCurrent); + long dtStart = services.getTimerService().getCurrentTimeMillis(); + long dt = 0; + long timeoutMillis = 10000; - int currentMinute = LocalDateTime.now().getMinute(); + int intervalEndIndex = startRunningSumIndex; + int predictionListIndex = startRunningSumIndex; - if ((currentMinute % 15) == 0) { - batteryUpdateForecasts(electricityBatteryAsset, currentTimeMillis, batteryEnergyLevelPercentageTargetMap, services); - } else if (energyLevelPercentagePredictedCurrent.isEmpty()) { - batteryUpdateForecasts(electricityBatteryAsset, currentTimeMillis, batteryEnergyLevelPercentageTargetMap, services); - } - } + while (energyLevelPredictionMaximum > energyLevelPercentageMaximumBattery || energyLevelPredictionMinimum < energyLevelPercentageMinimumBattery) { + String chargeOrDischarge = ""; + int intervalStartIndex = startRunningSumIndex; -// System.out.println("batteryEnergyLevelPercentageTargets: " + batteryEnergyLevelPercentageTargetsCurrent); + for (int i = intervalEndIndex; i < energyLevelPredictionList.size(); i++) { + predictionListIndex = i; - return batteryEnergyLevelPercentageTargetsCurrent; - } + if (energyLevelPredictionList.get(i) < energyLevelPercentageMinimumBattery) { + intervalEndIndex = i; - private Map calculateGeneralEnergyLevelPercentageTargetForecast(int batteryEnergyLevelPercentageTargetDefault, EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { - Map energyLevelPercentageTargetMap = new HashMap<>(); + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) >= energyLevelPercentageMaximumBattery) { + intervalStartIndex = j; + break; + } + } - // Get day ahead asset - EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); - boolean useDayAheadTariffs = false; + chargeOrDischarge = "charge"; - if (dayAheadAsset != null) { - String dayAheadAssetId = dayAheadAsset.getId(); - String logPrefixDayAhead = String.format("assetType='%s', assetId='%s', assetName='%s'", dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); +// System.out.println("CHARGE"); +// System.out.println("intervalStartIndex = " + intervalStartIndex); +// System.out.println("intervalEndIndex = " + intervalEndIndex); - useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); - String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); + break; + } else if (energyLevelPredictionList.get(i) > energyLevelPercentageMaximumBattery) { + intervalEndIndex = i; - if (useDayAheadTariffs && collectTimeForecasts.isBlank()) { - LOG.warning(String.format("%s, attributeName='%s'; Set time to collect day ahead forecasts", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); - } + for (int j = intervalEndIndex; j >= startRunningSumIndex; j--) { + if (energyLevelPredictionList.get(j) <= energyLevelPercentageMinimumBattery) { + intervalStartIndex = j; + break; + } + } - // Parse the time string - LocalTime collectTime = null; + chargeOrDischarge = "discharge"; - if (!collectTimeForecasts.isBlank()) { - try { - collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); - } catch (Exception e) { - LOG.warning(String.format("%s, attributeName='%s'; Error while parsing collect time; Exception: %s", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); - } - } +// System.out.println("DISCHARGE"); +// System.out.println("intervalStartIndex = " + intervalStartIndex); +// System.out.println("intervalEndIndex = " + intervalEndIndex); + + break; + } + } - // Collect the day ahead tariff forecasts at the desired collect time - if (collectTime != null) { - // Calculate the 15-minute interval for collecting the day ahead tariff forecasts - LocalDate currentDate = LocalDate.now(); - LocalDateTime collectDateTime = LocalDateTime.of(currentDate, collectTime); - long collectTimeMillisStart = collectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long collectTimeMillisEnd = collectTimeMillisStart + 15 * 60000; - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - - if (currentTimeMillis >= collectTimeMillisStart && currentTimeMillis < collectTimeMillisEnd) { - // Create asset datapoint query - LocalDate tomorrowDate = currentDate.plusDays(1); - LocalDateTime startOfNextDay = tomorrowDate.atStartOfDay(); - long startTimeMillis = startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; - AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); - - // Get the tariffs number of data-points from day ahead asset - int tariffExportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); - int tariffImportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); - - // Only update the historic datapoint table if there are no day ahead tariffs present - if (tariffExportDayAheadSize == 0) { - List> tariffExport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), assetDatapointQuery); - services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); + int changeIndex = -1; + double energyLevelPredictionValueBefore = energyLevelPredictionList.get(intervalEndIndex); + + if (chargeOrDischarge.equals("charge")) { + double chargeAvailable = 0; + double chargeAvailableInterval = energyLevelPercentageMaximumBattery - Collections.max(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + + if (chargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; + } + + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + } + + if (chargeAvailableLeft > 0) { + changeIndex = i; + chargeAvailable = Math.min(chargeAvailableInterval, chargeAvailableLeft); + break; + } + } + } + + double chargeNeeded = energyLevelPercentageMinimumBattery - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.min(chargeAvailable, chargeNeeded); + + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = chargeNeeded; + } + + for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { + energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); + } + } else if (chargeOrDischarge.equals("discharge")) { + double dischargeAvailable = 0; + double dischargeAvailableInterval = energyLevelPercentageMinimumBattery - Collections.min(energyLevelPredictionList.subList(intervalStartIndex, intervalEndIndex)); + + if (dischargeAvailableInterval > 0) { + for (int i = intervalEndIndex - 1; i >= intervalStartIndex; i--) { + if (i - 1 < 0) { + break; + } + + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + if (dischargeAvailableLeft < 0) { + changeIndex = i; + dischargeAvailable = Math.max(dischargeAvailableInterval, dischargeAvailableLeft); + break; + } + } + } + + double dischargeNeeded = energyLevelPercentageMaximumBattery - energyLevelPredictionList.get(intervalEndIndex); + double changeValue = Math.max(dischargeAvailable, dischargeNeeded); + + if (changeIndex == -1) { + changeIndex = intervalEndIndex; + changeValue = dischargeNeeded; + } + + for (int i = changeIndex; i < energyLevelPredictionList.size(); i++) { + energyLevelPredictionList.set(i, energyLevelPredictionList.get(i) + changeValue); + } } - if (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); + if (energyLevelPredictionValueBefore == energyLevelPredictionList.get(intervalEndIndex)) { + LOG.warning(String.format("Battery energy level percentage calculation error at index = %s", predictionListIndex)); +// System.out.println("ERROR: calculation error at index = " + predictionListIndex); + break; + } else if (dt > timeoutMillis) { + LOG.warning(String.format("Battery energy level percentage calculation timed out at index = %s", predictionListIndex)); +// System.out.println("ERROR: calculation timeout at index = " + predictionListIndex); + break; } - // Update the last update forecasts datetime field - String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); - String lastUpdateForecastsNew = collectDateTime.toString(); + energyLevelPredictionMaximum = Collections.max(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); + energyLevelPredictionMinimum = Collections.min(energyLevelPredictionList.subList(startRunningSumIndex, energyLevelPredictionList.size())); + dt = services.getTimerService().getCurrentTimeMillis() - dtStart; + } - LocalDateTime lastUpdateDateTime = collectDateTime.plusDays(-1); +// System.out.println("After limits: energyLevelPredictionList = " + energyLevelPredictionList + "\n"); - if (!lastUpdateForecasts.isEmpty()) { - lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); + // Get day ahead asset + EmsDayAheadAsset dayAheadAsset = getDayAheadAsset(energyOptimisationAsset, services); + boolean useDayAheadTariffs = false; + + if (dayAheadAsset != null) { + useDayAheadTariffs = dayAheadAsset.getUseTariffDayAheadForecasts().orElse(false); + } + + // Get the tariff forecasts from energy optimisation asset for 1 week + List> tariffExportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), services); + List> tariffImportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), services); + + // Overwrite the current tariff forecasts with the day ahead tariff forecasts + if (useDayAheadTariffs) { + List> tariffExportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); + List> tariffImportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); + + // Combine tariff export forecasts + if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); + + for (ValueDatapoint dp : tariffExportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } + + for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } + + tariffExportDatapoints = new ArrayList<>(mergedMap.values()); + tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); } - LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + // Combine tariff import forecasts + if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { + Map> mergedMap = new HashMap<>(); + + for (ValueDatapoint dp : tariffImportDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } + + for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { + mergedMap.put(dp.getTimestamp(), dp); + } - if (nextUpdateDate.isEqual(currentDate)) { - services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(dayAheadAssetId, EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), lastUpdateForecastsNew, collectTimeMillisStart)); + tariffImportDatapoints = new ArrayList<>(mergedMap.values()); + tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); } } - } - } - // Get the tariff forecasts from energy optimisation asset for 1 week - List> tariffExportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), services); - List> tariffImportDatapoints = getTariffDatapoints(energyOptimisationAsset, EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), services); + // Calculate battery energy level percentage default + double energyLevelPercentageDefault = batteryCalculateEnergyLevelPercentageDefault(energyLevelPercentageMaximumBattery, energyLevelPercentageMinimumBattery, energyOptimisationAsset); - // Overwrite the current tariff forecasts with the day ahead tariff forecasts - if (useDayAheadTariffs) { - List> tariffExportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), services); - List> tariffImportDayAheadAssetDatapoints = getTariffDayAheadDatapoints(dayAheadAsset, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), services); + // 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); - // Combine tariff export forecasts - if (!tariffExportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); + // Charge zone = 1, discharge zone = -1 + List chargeAndDischargeZonesList = new ArrayList<>(Collections.nCopies(energyLevelPredictionList.size(), 0)); - for (ValueDatapoint dp : tariffExportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); + for (Map.Entry entry : chargeAndDischargeZonesMap.entrySet()) { + long timestampMillis = entry.getKey(); + int value = entry.getValue(); + + int timestampIndex = timestampsMillisList.indexOf(timestampMillis); + + if (timestampIndex != -1) { + // Add 1 to align list indices + chargeAndDischargeZonesList.set(timestampIndex + 1, value); + } } - for (ValueDatapoint dp : tariffExportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); +// System.out.println("chargeAndDischargeZonesList = " + chargeAndDischargeZonesList); + + // Default energy level percentage for each interval + List energyLevelPercentageDefaultList = new ArrayList<>(Collections.nCopies(chargeAndDischargeZonesList.size(), energyLevelPercentageDefault)); + + for (int i = 0; i < chargeAndDischargeZonesList.size(); i++) { + if (chargeAndDischargeZonesList.get(i) == 1) { + energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMaximumBattery)); + } else if (chargeAndDischargeZonesList.get(i) == -1) { + energyLevelPercentageDefaultList.set(i, Double.valueOf(energyLevelPercentageMinimumBattery)); + } } - tariffExportDatapoints = new ArrayList<>(mergedMap.values()); - tariffExportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); - } +// System.out.println("energyLevelPercentageDefaultList = " + energyLevelPercentageDefaultList); + + // TODO: handle debounce interval in forecast + // Optimise forecast based on optimal tariffs + for (int i = 1; i < chargeAndDischargeZonesList.size() && i < energyLevelPredictionList.size(); i++) { + // Only adjust forecast if predicted energy level percentage is outside battery debounce interval + if (Math.abs(energyLevelPredictionList.get(i) - energyLevelPercentageDefaultList.get(i)) <= BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { + continue; + } + + if (chargeAndDischargeZonesList.get(i) == 1 && energyLevelPredictionList.get(i) < energyLevelPercentageDefaultList.get(i)) { + Double energyLevelIntervalMaximum = Collections.max(energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); + + double chargeSpaceLeft = energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum; + + if (chargeSpaceLeft > 0) { + double chargeNeeded = energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + } + + if (chargeAvailableLeft > 0) { + chargeAvailableLeft = Math.min(Math.min(chargeSpaceLeft, chargeAvailableLeft), chargeNeeded); + + for (int j = i; j < energyLevelPredictionList.size(); j++) { + energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + } + } + } else { + double chargeZoneEnergyLevel = energyLevelPredictionList.get(i); + + int energyLevelIntervalMaximumIndex = energyLevelPredictionList.size(); + + if (energyLevelIntervalMaximum >= energyLevelPercentageMaximumBattery) { + energyLevelIntervalMaximumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMaximum); + } + + for (int k = i; k <= energyLevelIntervalMaximumIndex; k++) { + double chargeNeeded = energyLevelPredictionList.get(k) - chargeZoneEnergyLevel; + + if (chargeNeeded > 0 && chargePercentageWantBatteryList.get(k) == 0) { + double chargeAvailableLeft = 0; + + if (dischargePercentageWantBatteryList.get(i) >= 0) { + double chargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + chargeAvailableLeft = chargePercentageAvailableBatteryList.get(i) - chargeInUse; + } + + if (chargeAvailableLeft > 0) { + double energyLevelIntervalMaximum2 = Collections.max(energyLevelPredictionList.subList(i, k - 1)); + double chargeSpaceInterval = energyLevelPercentageMaximumBattery - energyLevelIntervalMaximum2; + + if (chargeSpaceInterval > 0) { + chargeAvailableLeft = Math.min(Math.min(chargeAvailableLeft, chargeNeeded), chargeSpaceInterval); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + chargeAvailableLeft); + } + } + } + + break; + } + } + } + } else if (chargeAndDischargeZonesList.get(i) == -1 && energyLevelPredictionList.get(i) > energyLevelPercentageDefaultList.get(i)) { + Double energyLevelIntervalMinimum = Collections.min(energyLevelPredictionList.subList(i, energyLevelPredictionList.size())); + + double dischargeSpaceLeft = energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum; + + if (dischargeSpaceLeft < 0) { + double dischargeNeeded = energyLevelPercentageDefaultList.get(i) - energyLevelPredictionList.get(i); + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + + if (dischargeAvailableLeft < 0) { + dischargeAvailableLeft = Math.max(Math.max(dischargeSpaceLeft, dischargeAvailableLeft), dischargeNeeded); + + for (int j = i; j < energyLevelPredictionList.size(); j++) { + energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); + } + } + } else { + double dischargeZoneEnergyLevel = energyLevelPredictionList.get(i); + + int energyLevelIntervalMinimumIndex = energyLevelPredictionList.size(); - // Combine tariff import forecasts - if (!tariffImportDayAheadAssetDatapoints.isEmpty()) { - Map> mergedMap = new HashMap<>(); + if (energyLevelIntervalMinimum <= energyLevelPercentageMinimumBattery) { + energyLevelIntervalMinimumIndex = energyLevelPredictionList.indexOf(energyLevelIntervalMinimum); + } - for (ValueDatapoint dp : tariffImportDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); + for (int k = i; k <= energyLevelIntervalMinimumIndex; k++) { + double dischargeNeeded = energyLevelPredictionList.get(k) - dischargeZoneEnergyLevel; + + if (dischargeNeeded < 0 && dischargePercentageWantBatteryList.get(k) == 0) { + double dischargeAvailableLeft = 0; + + if (chargePercentageWantBatteryList.get(i) <= 0) { + double dischargeInUse = energyLevelPredictionList.get(i) - energyLevelPredictionList.get(i - 1); + dischargeAvailableLeft = dischargePercentageAvailableBatteryList.get(i) - dischargeInUse; + } + + if (dischargeAvailableLeft < 0) { + double energyLevelIntervalMinimum2 = Collections.min(energyLevelPredictionList.subList(i, k - 1)); + double dischargeSpaceInterval = energyLevelPercentageMinimumBattery - energyLevelIntervalMinimum2; + + if (dischargeSpaceInterval < 0) { + dischargeAvailableLeft = Math.max(Math.max(dischargeAvailableLeft, dischargeNeeded), dischargeSpaceInterval); + + for (int j = i; j < k; j++) { + energyLevelPredictionList.set(j, energyLevelPredictionList.get(j) + dischargeAvailableLeft); + } + } + } + + break; + } + } + } + } } + } + +// System.out.println("After Tariffs: energyLevelPredictionList = " + energyLevelPredictionList + "\n"); + + // Get the current energy level percentage target + int batteryEnergyLevelPercentageTarget = (int) Math.round(energyLevelPredictionList.get(1)); + batteryEnergyLevelPercentageTargets.put(batteryAssetId, batteryEnergyLevelPercentageTarget); + + // Calculate energy level percentage change per interval + List percentageChangeBatteryList = new ArrayList<>(); - for (ValueDatapoint dp : tariffImportDayAheadAssetDatapoints) { - mergedMap.put(dp.getTimestamp(), dp); + for (int i = 0; i < energyLevelPredictionList.size() - 1; i++) { + double percentageChange = energyLevelPredictionList.get(i + 1) - energyLevelPredictionList.get(i); + percentageChangeBatteryList.add(percentageChange); + } + +// System.out.println("percentageChangeBatteryList = " + percentageChangeBatteryList); + + // Calculate total power change for the EMS + List powerChangeTotalList = new ArrayList<>(); + + for (double percentageChange : percentageChangeBatteryList) { + double power = 0; + + if (percentageChange > 0) { + power = percentageChange * energyCapacityBattery / (intervalHour * chargeEfficiencyBattery); + } else if (percentageChange < 0) { + power = percentageChange * energyCapacityBattery * dischargeEfficiencyBattery / (10000 * intervalHour); } - tariffImportDatapoints = new ArrayList<>(mergedMap.values()); - tariffImportDatapoints.sort(Comparator.comparingLong(ValueDatapoint::getTimestamp)); + powerChangeTotalList.add(round(power, 3)); } - } - // Return empty map when there is no tariff import forecast present - if (tariffImportDatapoints.isEmpty()) { - return energyLevelPercentageTargetMap; - } +// System.out.println("powerChangeTotalList = " + powerChangeTotalList); - // Create a map with 15-minute intervals and an initial default target energy level percentage - long newestTimestampMillis = tariffImportDatapoints.stream() - .mapToLong(ValueDatapoint::getTimestamp) - .max().orElse(0L); + for (int i = 0; i < powerChangeTotalList.size() - 1; i++) { + totalPowerConsumptionProductionFlexList.set(i, totalPowerConsumptionProductionFlexList.get(i) - powerChangeTotalList.get(i)); + } - long oldestTimestampMillis = tariffImportDatapoints.stream() - .mapToLong(ValueDatapoint::getTimestamp) - .min().orElse(0L); +// System.out.println("totalPowerConsumptionProductionFlexList = " + totalPowerConsumptionProductionFlexList); - long startTimestampMillis = oldestTimestampMillis - oldestTimestampMillis % 900000; - long endTimestampMillis = newestTimestampMillis - newestTimestampMillis % 900000; + List> energyLevelPercentageForecast = new ArrayList<>(); + List> powerSetpointForecast = new ArrayList<>(); - if ((endTimestampMillis - startTimestampMillis) <= 0) { - return energyLevelPercentageTargetMap; - } + // Update energy level percentage forecast starting after current time + for (int i = 1; i < timestampsMillisList.size(); i++) { + energyLevelPercentageForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), (int) Math.round(energyLevelPredictionList.get(i)))); + } - for (long timestampMillis = startTimestampMillis; timestampMillis <= endTimestampMillis; timestampMillis += 900000) { - energyLevelPercentageTargetMap.put(timestampMillis, batteryEnergyLevelPercentageTargetDefault); + // Update power set-point starting from current power limit + for (int i = 0; i < timestampsMillisList.size(); i++) { + powerSetpointForecast.add(new ValueDatapoint<>(timestampsMillisList.get(i), powerChangeTotalList.get(i))); + } + + services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), energyLevelPercentageForecast); + services.getAssetPredictedDatapointService().updateValues(batteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), powerSetpointForecast); + +// System.out.println("UPDATED forecasts"); } - // TODO: with the change to 15-minute pricing, change from general to per battery to get the average best price for the battery charging duration - // Find the best import price for each day - ZoneId zoneId = ZoneId.systemDefault(); + return batteryEnergyLevelPercentageTargets; + } - Map> tariffImportDailyMinimumMap = tariffImportDatapoints.stream() - // Convert to concrete type - .map(dp -> new ValueDatapoint<>(dp.getTimestamp(), (Double) dp.getValue())) - // Group by LocalDate - .collect(Collectors.toMap(dp -> Instant.ofEpochMilli(dp.getTimestamp()) - .atZone(zoneId) - .toLocalDate(), Function.identity(), BinaryOperator.minBy(Comparator.comparing(ValueDatapoint::getValue)))); + private Map batteryCalculatePowerFlexibleAvailable(List electricityBatteryAssets) { + HashMap powerFlexibleAvailable = new HashMap<>(); - long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); - LocalDate dateCurrent = Instant.ofEpochMilli(currentTimeMillis).atZone(zoneId).toLocalDate(); - ValueDatapoint tariffImportMinimumToday = tariffImportDailyMinimumMap.get(dateCurrent); + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); + boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); + Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); + Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + + double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); + double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + + if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { + chargePowerMaximum = 0.0; + } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { + chargePowerMaximum = 0.0; + } + + if (!allowDischarging || energyLevelPercentageMinimum == null || energyLevelPercentage == null) { + dischargePowerMaximum = 0.0; + } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { + dischargePowerMaximum = 0.0; + } - if (tariffImportMinimumToday == null) { - return energyLevelPercentageTargetMap; + powerFlexibleAvailable.put(electricityBatteryAsset.getId(), new double[]{chargePowerMaximum, dischargePowerMaximum}); } - for (int i = 0; i < tariffImportDailyMinimumMap.size(); i++) { - ValueDatapoint tariffImportMinimumStart = tariffImportDailyMinimumMap.get(dateCurrent.plusDays(i - 1)); - ValueDatapoint tariffImportMinimumEnd = tariffImportDailyMinimumMap.get(dateCurrent.plusDays(i)); + return powerFlexibleAvailable; + } - // Set the values that cannot yet be predicted at the end of the forecast period to the last predicted value - if (i == (tariffImportDailyMinimumMap.size() - 1) && tariffImportMinimumStart != null && tariffImportMinimumEnd == null) { - long lastPredictedTimeMillis = tariffImportMinimumStart.getTimestamp() - 15 * 60000; - Integer lastPredictedValue = energyLevelPercentageTargetMap.get(lastPredictedTimeMillis); + private Map batteryCalculatePowerSetpoints(EmsEnergyOptimisationAsset energyOptimisationAsset, List electricityBatteryAssets, Services services, String logPrefix) { + Map powerFlexibleAvailable = batteryCalculatePowerFlexibleAvailable(electricityBatteryAssets); + Map powerSetpointsNew; - for (Map.Entry entry : energyLevelPercentageTargetMap.entrySet()) { - long timeMillis = entry.getKey(); + // Get latest power set-point across all batteries + long powerSetpointTimestampMillisLatest = 0L; - if (timeMillis > lastPredictedTimeMillis) { - entry.setValue(lastPredictedValue); - } - } - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + long powerSetpointTimestampMillis = electricityBatteryAsset.getPowerSetpointTimestamp().orElse(0L); - if (tariffImportMinimumStart == null || tariffImportMinimumEnd == null) { - continue; + if (powerSetpointTimestampMillis > powerSetpointTimestampMillisLatest) { + powerSetpointTimestampMillisLatest = powerSetpointTimestampMillis; } + } + + // Check if power net is updated since last battery power set-points update + Double powerNet = energyOptimisationAsset.getPowerNet().orElse(null); + long powerNetTimestampMillis = energyOptimisationAsset.getPowerNetTimestamp().orElse(0L); + + // Turn off batteries that do not have flexible power available during main power meter disconnect + if (powerSetpointTimestampMillisLatest > powerNetTimestampMillis || powerNet == null) { + powerSetpointsNew = batteryCheckPowerSetpointsCurrent(electricityBatteryAssets, powerFlexibleAvailable); + return powerSetpointsNew; + } + + // Check if power limits are present and calculate additional power limits + Double powerLimitMaximumProfileTotal = energyOptimisationAsset.getPowerLimitMaximumProfileTotal().orElse(null); + Double powerLimitMinimumProfileTotal = energyOptimisationAsset.getPowerLimitMinimumProfileTotal().orElse(null); - long startTimeMillis = tariffImportMinimumStart.getTimestamp(); - long endTimeMillis = tariffImportMinimumEnd.getTimestamp(); + Double powerLimitMaximumBatteries = null; - // Find the best export price for each day - Optional> tariffExportMinimum = tariffExportDatapoints.stream() - .filter(dp -> dp.getTimestamp() > startTimeMillis && dp.getTimestamp() < endTimeMillis) - .map(dp -> new ValueDatapoint<>(dp.getTimestamp(), (Double) dp.getValue())) - .min(Comparator.comparing(ValueDatapoint::getValue)); + if (powerLimitMaximumProfileTotal != null) { + powerLimitMaximumBatteries = round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), 3); + } - // Check if there is a tariff export value-datapoint - if (tariffExportMinimum.isEmpty()) { - continue; - } + Double powerLimitMinimumBatteries = null; - // Check if there is an import and export price - if (tariffImportMinimumStart.getValue() == null || tariffExportMinimum.get().getValue() == null) { - continue; - } + if (powerLimitMinimumProfileTotal != null) { + powerLimitMinimumBatteries = round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_BATTERIES_MARGIN_PERCENTAGE * 0.01), 3); + } - double tariffExportMinimumPrice = tariffExportMinimum.get().getValue(); - double tariffImportMinimumPrice = tariffImportMinimumStart.getValue(); + // TODO: add battery limits cross-over warning + // TODO: add advanced settings - // Calculate if profit can be made by discharging - if ((tariffImportMinimumPrice + tariffExportMinimumPrice) > 0) { - continue; - } + int currentMinute = LocalDateTime.now().getMinute(); + Map batteryEnergyLevelPercentageTargets = batteryGetEnergyLevelPercentageTargetsCurrent(electricityBatteryAssets, services); - long dischargeTimeMillis = tariffExportMinimum.get().getTimestamp(); + // 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); + } - // Set the general energy level percentage targets - for (Map.Entry entry : energyLevelPercentageTargetMap.entrySet()) { - long timeMillis = entry.getKey(); + // Calculate virtual power consumption + Map powerSetpointsCurrent = new HashMap<>(); - if (timeMillis >= startTimeMillis && timeMillis < dischargeTimeMillis) { - entry.setValue(100); - } else if (timeMillis >= dischargeTimeMillis && timeMillis < endTimeMillis) { - entry.setValue(0); - } - } + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + double powerSetpoint = electricityBatteryAsset.getPowerSetpoint().orElse(0.0); + powerSetpointsCurrent.put(electricityBatteryAssetId, powerSetpoint); } - return energyLevelPercentageTargetMap; - } + double powerSetpointsCurrentSum = powerSetpointsCurrent.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerConsumptionVirtual = powerNet - powerSetpointsCurrentSum; - private Map batteryCalculatePowerFlexibleAvailable(List electricityBatteryAssets) { - HashMap powerFlexibleAvailable = new HashMap<>(); + // Calculate new power set-points + if (powerLimitMaximumProfileTotal != null && powerConsumptionVirtual > powerLimitMaximumProfileTotal) { + Double powerLimitMaximumVirtual = round(powerLimitMaximumProfileTotal * (1 - POWER_LIMIT_MAXIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), 3); + powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMaximumVirtual, "max"); + } else if (powerLimitMinimumProfileTotal != null && powerConsumptionVirtual < powerLimitMinimumProfileTotal) { + Double powerLimitMinimumVirtual = round(powerLimitMinimumProfileTotal * (1 - POWER_LIMIT_MINIMUM_SAFETY_MARGIN_PERCENTAGE * 0.01), 3); + powerSetpointsNew = batteryCalculatePowerSetpointsOnLimitBreach(powerFlexibleAvailable, powerConsumptionVirtual, powerLimitMinimumVirtual, "min"); + } else { + powerSetpointsNew = batteryRestoreEnergyLevels(energyOptimisationAsset, electricityBatteryAssets, powerFlexibleAvailable, powerNet, powerSetpointsCurrentSum, powerLimitMinimumBatteries, powerLimitMaximumBatteries, batteryEnergyLevelPercentageTargets); + } - for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { - boolean allowCharging = electricityBatteryAsset.getAllowCharging().orElse(false); - boolean allowDischarging = electricityBatteryAsset.getAllowDischarging().orElse(false); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); - Integer energyLevelPercentageMaximum = electricityBatteryAsset.getEnergyLevelPercentageMaximum().orElse(null); - Integer energyLevelPercentageMinimum = electricityBatteryAsset.getEnergyLevelPercentageMinimum().orElse(null); + // Create log messages on power limit breach + double powerSetpointsNewSum = powerSetpointsNew.values().stream().mapToDouble(Double::doubleValue).sum(); + double powerNetNewVirtual = powerConsumptionVirtual + powerSetpointsNewSum; - double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(0.0); - double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(0.0); + if (powerLimitMaximumProfileTotal != null && powerNet > powerLimitMaximumProfileTotal) { + double powerExceededAmount = round((powerNet - powerLimitMaximumProfileTotal), 3); + LOG.warning(String.format("%s; Power net is %s kW above power limit maximum", logPrefix, powerExceededAmount)); - if (!allowCharging || energyLevelPercentageMaximum == null || energyLevelPercentage == null) { - chargePowerMaximum = 0.0; - } else if (energyLevelPercentage >= energyLevelPercentageMaximum) { - chargePowerMaximum = 0.0; + if (powerNetNewVirtual > powerLimitMaximumProfileTotal) { + double powerReductionShortage = round((powerNetNewVirtual - powerLimitMaximumProfileTotal), 3); + LOG.warning(String.format("%s; Not enough flexible power to get below power limit maximum; Shortage of %s kW", logPrefix, powerReductionShortage)); } + } else if (powerLimitMinimumProfileTotal != null && powerNet < powerLimitMinimumProfileTotal) { + double powerExceededAmount = round((powerNet - powerLimitMinimumProfileTotal), 3); + LOG.warning(String.format("%s; Power net is %s kW below power limit minimum", logPrefix, powerExceededAmount)); - if (!allowDischarging || energyLevelPercentageMinimum == null || energyLevelPercentage == null) { - dischargePowerMaximum = 0.0; - } else if (energyLevelPercentage <= energyLevelPercentageMinimum) { - dischargePowerMaximum = 0.0; + 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)); } - - powerFlexibleAvailable.put(electricityBatteryAsset.getId(), new double[]{chargePowerMaximum, dischargePowerMaximum}); } - return powerFlexibleAvailable; + return powerSetpointsNew; } private Map batteryCalculatePowerSetpointsOnLimitBreach(Map powerFlexibleAvailable, Double powerConsumptionVirtual, Double powerLimitVirtual, String maxOrMin) { @@ -731,7 +1040,7 @@ private Map batteryCalculatePowerSetpointsOnLimitBreach(Map 0) { // Limit the power set-point to available battery power or remaining reduction needed - double setpoint = Math.round(Math.max(-powerReduction, powerBatteryAvailable) * 1000.0) / 1000.0; + double setpoint = round(Math.max(-powerReduction, powerBatteryAvailable), 3); if (isMin) { setpoint = -setpoint; @@ -852,8 +1161,9 @@ private void batteryCheckSetup(List electricityBatte } if (energyLevelPercentageMaximum != null && energyLevelPercentageMinimum != null && (energyLevelPercentageMaximum - energyLevelPercentageMinimum) < BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL) { - LOG.warning(String.format("%s; Can't use battery for flexible power. The (%s=%s - %s=%s < %s%%)", logPrefixBattery, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), energyLevelPercentageMaximum, - EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), energyLevelPercentageMinimum, BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)); + int diff = energyLevelPercentageMaximum - energyLevelPercentageMinimum; + LOG.warning(String.format("%s; Can't use battery for flexible power. %s - %s = %s - %s = %s < %s%%)", logPrefixBattery, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MAXIMUM.getName(), EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE_MINIMUM.getName(), + energyLevelPercentageMaximum, energyLevelPercentageMinimum, diff, BATTERY_ENERGY_LEVEL_PERCENTAGE_DEBOUNCE_INTERVAL)); } } } @@ -890,110 +1200,136 @@ private Map batteryCheckPowerSetpointsCurrent(List batteryEnergyLevelPercentageTargetMap, Services services) { - Double chargePowerMaximum = electricityBatteryAsset.getChargePowerMaximum().orElse(null); - Double dischargePowerMaximum = electricityBatteryAsset.getDischargePowerMaximum().orElse(null); - Double energyCapacity = electricityBatteryAsset.getEnergyCapacity().orElse(null); - Double energyLevelPercentage = electricityBatteryAsset.getEnergyLevelPercentage().orElse(null); + private Map batteryGetEnergyLevelPercentageTargetsCurrent(List electricityBatteryAssets, Services services) { + Map batteryEnergyLevelPercentageTargetsCurrent = new HashMap<>(); + + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + long endTimeMillis = currentTimeMillis - currentTimeMillis % (15 * 60000) + (15 * 60000); - if (chargePowerMaximum == null || dischargePowerMaximum == null || energyCapacity == null || energyLevelPercentage == null) { - return; + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String batteryAssetId = electricityBatteryAsset.getId(); + AssetDatapointAllQuery assetDatapointQueryPredicted = new AssetDatapointAllQuery(currentTimeMillis, endTimeMillis); + List> energyLevelPercentagePredictedCurrent = services.getAssetPredictedDatapointService().queryDatapoints(batteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), assetDatapointQueryPredicted); + + Integer energyLevelPercentageTarget = null; + + if (!energyLevelPercentagePredictedCurrent.isEmpty()) { + Double value = ((Double) energyLevelPercentagePredictedCurrent.getLast().getValue()); + energyLevelPercentageTarget = (value != null) ? value.intValue() : null; + } + + batteryEnergyLevelPercentageTargetsCurrent.put(batteryAssetId, energyLevelPercentageTarget); } - // Calculate the battery efficiency factors - Integer chargeEfficiency = electricityBatteryAsset.getChargeEfficiency().orElse(100); - Integer dischargeEfficiency = electricityBatteryAsset.getDischargeEfficiency().orElse(100); + 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<>(); - Double chargeEfficiencyFactor = chargeEfficiency / 100.0; - Double dischargeEfficiencyFactor = 100.0 / dischargeEfficiency; + // 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); - // Add current energy level percentage as first entry - int energyLevelPercentageRounded = (int) Math.round(energyLevelPercentage); - long firstEntryTimestampMillis = currentTimeMillis - 900000; - batteryEnergyLevelPercentageTargetMap.put(firstEntryTimestampMillis, energyLevelPercentageRounded); + // Set initial power set-point of 0 + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); - // Order data-points by timestamp - List> batteryEnergyLevelPercentageTargetList = batteryEnergyLevelPercentageTargetMap.entrySet() - .stream() - .filter(entry -> entry.getKey() >= firstEntryTimestampMillis) - .sorted(Map.Entry.comparingByKey()) - .toList(); + if (energyLevelPercentage == null) { + continue; + } - // Calculate energy level percentage forecast - List> energyLevelPercentageForecastList = new ArrayList<>(); - energyLevelPercentageForecastList.add((new ValueDatapoint<>(firstEntryTimestampMillis, Math.round(energyLevelPercentage)))); - - for (int i = 0; i < (batteryEnergyLevelPercentageTargetList.size() - 1); i++) { - // Calculate time interval - long targetTimestampMillis = batteryEnergyLevelPercentageTargetList.get(i).getKey(); - long targetTimestampMillisNext = batteryEnergyLevelPercentageTargetList.get(i + 1).getKey(); - long intervalMillis = targetTimestampMillisNext - targetTimestampMillis; - - // Calculate predicted energy level percentage - long energyLevelPercentagePredicted = (long) energyLevelPercentageForecastList.get(i).getValue(); - long energyLevelPercentagePredictedNext; - int energyLevelPercentageTargetNext = batteryEnergyLevelPercentageTargetList.get(i + 1).getValue(); - - if (energyLevelPercentagePredicted > energyLevelPercentageTargetNext) { - // Calculate discharge amount for interval - double dischargePercentage = dischargeEfficiencyFactor * dischargePowerMaximum * intervalMillis / (energyCapacity * 36000); - energyLevelPercentagePredictedNext = Math.round(Math.max(energyLevelPercentagePredicted + dischargePercentage, energyLevelPercentageTargetNext)); - } else { - // Calculate charge amount for interval - double chargePercentage = chargeEfficiencyFactor * chargePowerMaximum * intervalMillis / (energyCapacity * 36000); - energyLevelPercentagePredictedNext = Math.round(Math.min(energyLevelPercentagePredicted + chargePercentage, energyLevelPercentageTargetNext)); + 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; - long predictedTimestampMillisNext = targetTimestampMillisNext + 900000; + if (powerLimitMaximumBatteries != null) { + chargingSpace = powerLimitMaximumBatteries - powerConsumptionVirtual; + } - energyLevelPercentageForecastList.add((new ValueDatapoint<>(predictedTimestampMillisNext, energyLevelPercentagePredictedNext))); + if (powerLimitMinimumBatteries != null) { + dischargingSpace = powerLimitMinimumBatteries - powerConsumptionVirtual; } - // Calculate power set-point forecast - List> powerSetpointForecastList = new ArrayList<>(); + if (chargingSpace != null && powerSetpointsNewSum > chargingSpace) { + // Adjust battery power set-points in case of charging space shortage + double powerReduction = powerSetpointsNewSum - chargingSpace; - for (int i = 1; i < energyLevelPercentageForecastList.size(); i++) { - long energyLevelPercentagePredicted = (long) energyLevelPercentageForecastList.get(i).getValue(); - long energyLevelPercentagePredictedPrevious = (long) energyLevelPercentageForecastList.get(i - 1).getValue(); - long timestampMillisPrevious = energyLevelPercentageForecastList.get(i - 1).getTimestamp(); + for (EmsElectricityBatteryAsset electricityBatteryAsset : electricityBatteryAssets) { + String electricityBatteryAssetId = electricityBatteryAsset.getId(); + double powerSetpointNew = powerSetpointsNew.getOrDefault(electricityBatteryAssetId, 0.0); - double powerSetpointPredicted = 0.0; + // Only adjust battery power set-point for charging batteries + if (powerSetpointNew > 0.0) { + powerReduction = powerReduction - powerSetpointNew; - if (energyLevelPercentagePredicted > energyLevelPercentagePredictedPrevious) { - powerSetpointPredicted = chargePowerMaximum; - } else if (energyLevelPercentagePredicted < energyLevelPercentagePredictedPrevious) { - powerSetpointPredicted = dischargePowerMaximum; + 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; - powerSetpointForecastList.add((new ValueDatapoint<>(timestampMillisPrevious, powerSetpointPredicted))); + if (powerReduction <= 0.0) { + powerSetpointsNew.put(electricityBatteryAssetId, 0.0); + } else { + double powerSetpointAdjusted = Math.max(-powerReduction, powerSetpointNew); + powerSetpointsNew.put(electricityBatteryAssetId, powerSetpointAdjusted); + break; + } + } + } } - // Remove the current timestamp data-point from forecast - energyLevelPercentageForecastList.removeFirst(); - powerSetpointForecastList.removeFirst(); - - String electricityBatteryAssetId = electricityBatteryAsset.getId(); - - services.getAssetPredictedDatapointService().updateValues(electricityBatteryAssetId, EmsElectricityBatteryAsset.ENERGY_LEVEL_PERCENTAGE.getName(), energyLevelPercentageForecastList); - services.getAssetPredictedDatapointService().updateValues(electricityBatteryAssetId, EmsElectricityBatteryAsset.POWER_SETPOINT.getName(), powerSetpointForecastList); - -// System.out.print("Target energy levels: ["); -// for (Map.Entry entry : batteryEnergyLevelPercentageTargetList) { -// System.out.print(entry.getValue() + ", "); -// } -// System.out.println("]"); -// -// System.out.print("Actual energy levels: ["); -// for (ValueDatapoint datapoint : energyLevelPercentageForecastList) { -// System.out.print(datapoint.getTimestamp() + ": " + datapoint.getValue() + ", "); -// } -// System.out.println("]"); -// -// System.out.print("Setpoint levels: ["); -// for (ValueDatapoint datapoint : powerSetpointForecastList) { -// System.out.print(datapoint.getTimestamp() + ": " + datapoint.getValue() + ", "); -// } -// System.out.println("]"); + return powerSetpointsNew; } private void batteryUpdatePowerSetpoints(List electricityBatteryAssets, Map powerSetpointsNew, Services services) { @@ -1005,6 +1341,82 @@ private void batteryUpdatePowerSetpoints(List electr } } + private Map calculateTariffChargeAndDischargeZones(List> tariffImportDatapoints, List> tariffExportDatapoints, int window) { + Map chargeAndDischargeZonesMap = new HashMap<>(); + + // Return empty map when there is no tariff forecast present + if (tariffImportDatapoints.size() < window || tariffExportDatapoints.size() < window) { + return chargeAndDischargeZonesMap; + } + + // Find the best import price for each day + ZoneId zoneId = ZoneId.systemDefault(); + + List> tariffImportMovingAverage = movingAverage(tariffImportDatapoints, window); + List> tariffExportMovingAverage = movingAverage(tariffExportDatapoints, window); + + Map tariffImportDailyMinimumMap = + IntStream.range(0, tariffImportMovingAverage.size()) + .mapToObj(i -> { + ValueDatapoint dp = tariffImportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect(Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) + )); + + Map tariffExportDailyMinimumMap = + IntStream.range(0, tariffExportMovingAverage.size()) + .mapToObj(i -> { + ValueDatapoint dp = tariffExportMovingAverage.get(i); + return new IndexedDatapoint(dp.getTimestamp(), (Double) dp.getValue(), i); + }) + .collect(Collectors.toMap( + dp -> Instant.ofEpochMilli(dp.timestamp()).atZone(zoneId).toLocalDate(), + Function.identity(), + BinaryOperator.minBy(Comparator.comparing(IndexedDatapoint::value)) + )); + + Map chargeZonesMap = new HashMap<>(); + Map dischargeZonesMap = new HashMap<>(); + + for (Map.Entry entry : tariffImportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; + + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffImportDatapoints.get(i).getTimestamp(); + chargeZonesMap.put(timeMillis, 1); + } + } + + for (Map.Entry entry : tariffExportDailyMinimumMap.entrySet()) { + int startIndex = entry.getValue().index; + int endIndex = startIndex + window; + + for (int i = startIndex; i < endIndex; i++) { + long timeMillis = tariffExportDatapoints.get(i).getTimestamp(); + dischargeZonesMap.put(timeMillis, -1); + } + } + + for (Map.Entry entry : chargeZonesMap.entrySet()) { + if (!dischargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } + } + + for (Map.Entry entry : dischargeZonesMap.entrySet()) { + if (!chargeZonesMap.containsKey(entry.getKey())) { + chargeAndDischargeZonesMap.put(entry.getKey(), entry.getValue()); + } + } + + return chargeAndDischargeZonesMap; + } + private EmsDayAheadAsset getDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, Services services) { // Find assets in database List assets = services.getAssetStorageService() @@ -1062,23 +1474,189 @@ private List> getTariffDatapoints(EmsEnergyOptimisationAsset e return tariffCombined; } -} -// TODO: Checked -// batteryCalculatePowerFlexibleAvailable -// batteryCalculatePowerSetpoints -// batteryCalculatePowerSetpointsOnLimitBreach -// batteryCheckConnection -// batteryCheckPowerSetpointsCurrent -// batteryCheckSetup -// batteryRestoreEnergyLevels -// batteryUpdateForecasts -// batteryUpdatePowerSetpoints -// calculateGeneralEnergyLevelPercentageTargetForecast -// getDayAheadAsset -// getTariffDayAheadDatapoints -// getTariffDatapoints - - -// TODO: add order to battery assets from longest to smallest discharge duration -// TODO: predict charging/discharging space + predict battery power needed per 15-minute interval for 48 hours -> Adjust energy level percentage and power set-point forecasts + public static List> intervalAverage(List> dataPoints, long intervalMillis) { + // Map + Map> valuesPerIntervalMap = new HashMap<>(); + + for (ValueDatapoint datapoint : dataPoints) { + long timestampMillis = datapoint.getTimestamp(); + Double value = (Double) datapoint.getValue(); + + if (value != null) { + // Calculate start of 15-minute interval + long intervalStartMillis = timestampMillis - timestampMillis % intervalMillis; + + // Add value to corresponding interval + valuesPerIntervalMap.computeIfAbsent(intervalStartMillis, key -> new ArrayList<>()).add(value); + } + } + + // List with average values per interval + List> averageList = new ArrayList<>(); + + for (Map.Entry> entry : valuesPerIntervalMap.entrySet()) { + Long intervalStartMillis = entry.getKey(); + List values = entry.getValue(); + + // Compute the average + double sum = 0; + for (double value : values) { + sum += value; + } + double average = sum / values.size(); + + averageList.add(new ValueDatapoint<>(intervalStartMillis, average)); + } + + return averageList; + } + + private List> intervalInterpolate(List> dataPoints, long startTimeMillis, long endTimeMillis, long intervalMillis) { + List> interpolatedList = new ArrayList<>(); + + if (dataPoints == null || dataPoints.size() < 2) { + return interpolatedList; + } + + int idx1 = 0; + + for (long intervalTimeMillis = startTimeMillis; intervalTimeMillis <= endTimeMillis; intervalTimeMillis += intervalMillis) { + // Find data-point before and after interval + while (idx1 < (dataPoints.size() - 1) && dataPoints.get(idx1 + 1).getTimestamp() < intervalTimeMillis) { + idx1++; + } + + long timeBeforeMillis = dataPoints.get(idx1).getTimestamp(); + long timeAfterMillis = dataPoints.get(idx1 + 1).getTimestamp(); + + // Interpolate value + if (intervalTimeMillis >= timeBeforeMillis && intervalTimeMillis <= timeAfterMillis) { + double valueBefore = (double) dataPoints.get(idx1).getValue(); + double valueAfter = (double) dataPoints.get(idx1 + 1).getValue(); + + double factor = (double) (intervalTimeMillis - timeBeforeMillis) / (timeAfterMillis - timeBeforeMillis); + double interpolatedValue = valueBefore + factor * (valueAfter - valueBefore); + + interpolatedList.add(new ValueDatapoint<>(intervalTimeMillis, interpolatedValue)); + } + } + + return interpolatedList; + } + + public static List> movingAverage(List> dataPoints, int window) { + List> result = new ArrayList<>(); + if (window <= 0 || dataPoints.size() < window) { + return result; + } + + 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(); + + // Remove value exiting the sliding window + if (i >= window) { + Number oldValue = (Number) dataPoints.get(i - window).getValue(); + sum -= oldValue.doubleValue(); + } + + // 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); + + result.add(averaged); + } + } + + return result; + } + + private double round(double value, int precision) { + double scale = Math.pow(10, precision); + return Math.round(value * scale) / scale; + } + + private void updateDayAheadAsset(EmsEnergyOptimisationAsset energyOptimisationAsset, EmsDayAheadAsset dayAheadAsset, Services services) { + String dayAheadAssetId = dayAheadAsset.getId(); + String logPrefixDayAhead = String.format("assetType='%s', assetId='%s', assetName='%s'", dayAheadAsset.getAssetType(), dayAheadAssetId, dayAheadAsset.getAssetName()); + + String collectTimeForecasts = dayAheadAsset.getCollectTimeForecasts().orElse(""); + + if (collectTimeForecasts.isBlank()) { + LOG.warning(String.format("%s, attributeName='%s'; Set time to collect day ahead forecasts", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName())); + } + + // Parse the time string + LocalTime collectTime = null; + + if (!collectTimeForecasts.isBlank()) { + try { + collectTime = LocalTime.parse(collectTimeForecasts, DateTimeFormatter.ofPattern("HH:mm")); + } catch (Exception e) { + LOG.warning(String.format("%s, attributeName='%s'; Error while parsing collect time; Exception: %s", logPrefixDayAhead, EmsDayAheadAsset.COLLECT_TIME_FORECASTS.getName(), e)); + } + } + + // Collect the day ahead tariff forecasts at the desired collect time + if (collectTime != null) { + // Calculate the 15-minute interval for collecting the day ahead tariff forecasts + LocalDate currentDate = LocalDate.now(); + LocalDateTime collectDateTime = LocalDateTime.of(currentDate, collectTime); + long collectTimeMillisStart = collectDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long collectTimeMillisEnd = collectTimeMillisStart + 15 * 60000; + long currentTimeMillis = services.getTimerService().getCurrentTimeMillis(); + + if (currentTimeMillis >= collectTimeMillisStart && currentTimeMillis < collectTimeMillisEnd) { + // Create asset datapoint query + LocalDate tomorrowDate = currentDate.plusDays(1); + LocalDateTime startOfNextDay = tomorrowDate.atStartOfDay(); + long startTimeMillis = startOfNextDay.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + long endTimeMillis = startTimeMillis + 24 * 60 * 60000 - 60000; + AssetDatapointAllQuery assetDatapointQuery = new AssetDatapointAllQuery(startTimeMillis, endTimeMillis); + + // Get the tariffs number of data-points from day ahead asset + int tariffExportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); + int tariffImportDayAheadSize = services.getAssetDatapointService().queryDatapoints(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), assetDatapointQuery).size(); + + // Only update the historic datapoint table if there are no day ahead tariffs present + if (tariffExportDayAheadSize == 0) { + List> tariffExport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_EXPORT.getName(), assetDatapointQuery); + services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_EXPORT_DAY_AHEAD.getName(), tariffExport); + } + + if (tariffImportDayAheadSize == 0) { + List> tariffImport = services.getAssetPredictedDatapointService().queryDatapoints(energyOptimisationAsset.getId(), EmsEnergyOptimisationAsset.TARIFF_IMPORT.getName(), assetDatapointQuery); + services.getAssetDatapointService().upsertValues(dayAheadAssetId, EmsDayAheadAsset.TARIFF_IMPORT_DAY_AHEAD.getName(), tariffImport); + } + + // Update the last update forecasts datetime field + String lastUpdateForecasts = dayAheadAsset.getLastUpdateForecasts().orElse(""); + String lastUpdateForecastsNew = collectDateTime.toString(); + + LocalDateTime lastUpdateDateTime = collectDateTime.plusDays(-1); + + if (!lastUpdateForecasts.isEmpty()) { + lastUpdateDateTime = LocalDateTime.parse(lastUpdateForecasts); + } + + LocalDate nextUpdateDate = lastUpdateDateTime.plusDays(1).toLocalDate(); + + if (nextUpdateDate.isEqual(currentDate)) { + services.getAssetProcessingService().sendAttributeEvent(new AttributeEvent(dayAheadAssetId, EmsDayAheadAsset.LAST_UPDATE_FORECASTS.getName(), lastUpdateForecastsNew, collectTimeMillisStart)); + } + } + } + } + + record IndexedDatapoint(long timestamp, double value, int index) { + } +} From a2e1b6e4686e6e14848b3b8308e126721d390d53 Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Tue, 24 Feb 2026 10:26:58 +0100 Subject: [PATCH 2/3] Added 'advancedSettingsAttributes' attribute to EmsEnergyOptimisationAsset --- .../ems/agent/EmsEnergyOptimisationAsset.java | 18 +++++++++++++----- ...60223_01__AddAdvancedSettingsAttributes.sql | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql diff --git a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java index 08af8a1..52311e3 100644 --- a/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java +++ b/ems/src/main/java/org/openremote/extension/ems/agent/EmsEnergyOptimisationAsset.java @@ -29,12 +29,16 @@ import java.util.Optional; import static org.openremote.model.Constants.*; -import static org.openremote.model.value.ValueType.BOOLEAN; @Entity public class EmsEnergyOptimisationAsset extends Asset { - public static final AttributeDescriptor ENABLE_DETAILED_LOGGING = new AttributeDescriptor<>("enableDetailedLogging", BOOLEAN); + public static final AttributeDescriptor ADVANCED_SETTINGS_ATTRIBUTES = new AttributeDescriptor<>("advancedSettingsAttributes", ValueType.TEXT, + new MetaItem<>(MetaItemType.MULTILINE), + new MetaItem<>(MetaItemType.READ_ONLY) + ); + + public static final AttributeDescriptor ENABLE_DETAILED_LOGGING = new AttributeDescriptor<>("enableDetailedLogging", ValueType.BOOLEAN); public static final AttributeDescriptor ENERGY_EXPORT_TOTAL = new AttributeDescriptor<>("energyExportTotal", ValueType.NUMBER, new MetaItem<>(MetaItemType.DATA_POINTS_MAX_AGE_DAYS, 7), @@ -48,11 +52,11 @@ public class EmsEnergyOptimisationAsset extends Asset(MetaItemType.STORE_DATA_POINTS) ).withUnits(UNITS_KILO, UNITS_WATT, UNITS_HOUR); - public static final AttributeDescriptor GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMaximumProfileManualInput", BOOLEAN); + public static final AttributeDescriptor GENERATE_POWER_LIMIT_MAXIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMaximumProfileManualInput", ValueType.BOOLEAN); - public static final AttributeDescriptor GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMinimumProfileManualInput", BOOLEAN); + public static final AttributeDescriptor GENERATE_POWER_LIMIT_MINIMUM_PROFILE_MANUAL_INPUT = new AttributeDescriptor<>("generatePowerLimitMinimumProfileManualInput", ValueType.BOOLEAN); - public static final AttributeDescriptor OPTIMISATION_DISABLED = new AttributeDescriptor<>("optimisationDisabled", BOOLEAN); + public static final AttributeDescriptor OPTIMISATION_DISABLED = new AttributeDescriptor<>("optimisationDisabled", ValueType.BOOLEAN); public enum OptimisationMethodValueType { None, @@ -165,6 +169,10 @@ public EmsEnergyOptimisationAsset(String name) { super(name); } + public Optional getAdvancedSettingsAttributes() { + return getAttribute(ADVANCED_SETTINGS_ATTRIBUTES).flatMap(AbstractNameValueHolder::getValue); + } + public Optional getEnableDetailedLogging() { return getAttribute(ENABLE_DETAILED_LOGGING).flatMap(AbstractNameValueHolder::getValue); } diff --git a/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql b/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql new file mode 100644 index 0000000..b17cc72 --- /dev/null +++ b/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql @@ -0,0 +1,3 @@ +-- Add attribute estimatedSolarCapacity to Ems energy optimisation Asset +SELECT a.id, ADD_ATTRIBUTE(a, 'advancedSettingsAttributes', 'text', null, now(), jsonb_build_object('multiline', true, 'readOnly', true)) +FROM asset a WHERE a.type = 'EmsEnergyOptimisationAsset'; \ No newline at end of file From be644973b17889e607a59edbef635618f69bd9bc Mon Sep 17 00:00:00 2001 From: Igor Rutka Date: Wed, 25 Feb 2026 09:58:26 +0100 Subject: [PATCH 3/3] Changed attibute name in comment to 'advancedSettingsAttributes' --- .../database/V20260223_01__AddAdvancedSettingsAttributes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql b/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql index b17cc72..20b2942 100644 --- a/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql +++ b/ems/src/main/resources/org/openremote/manager/setup/database/V20260223_01__AddAdvancedSettingsAttributes.sql @@ -1,3 +1,3 @@ --- Add attribute estimatedSolarCapacity to Ems energy optimisation Asset +-- Add attribute advancedSettingsAttributes to Ems energy optimisation Asset SELECT a.id, ADD_ATTRIBUTE(a, 'advancedSettingsAttributes', 'text', null, now(), jsonb_build_object('multiline', true, 'readOnly', true)) FROM asset a WHERE a.type = 'EmsEnergyOptimisationAsset'; \ No newline at end of file