diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2ee5ab..cf359711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). `ac_charge` array kept the optimizer's unused gene values. The simulation ignored them, so they were never costed - but a controller acting on the plan would grid-charge the battery anyway. The disabled AC charge is now cleared in the reported plan as well. +- The energy left in the battery at the end of the horizon is now valued with a concave curve + derived from the trailing horizon window (`optimization.terminal_value_mode = AUTO`, the new + default): the first stored kWh replaces the most expensive hour that PV cannot cover, the next + one the second most expensive, and energy beyond the residual load is credited only when it can + be exported. A single price per kWh could not express this - with the previous default of 0 the + optimizer emptied the battery towards the end of the horizon, with a high value it hoarded it. + The curve is built once per run and reported as `terminal_value` in the solution. + `terminal_value_mode = FIXED` restores the old scalar behaviour. - EV Bug (wrong output in genetic.py / no senseful results) - Direktvermarktung active / Battery discharge into grid (new state / action battery_grid_export_allowed) + (new simulation output Feed_in_tariff) - New PV forecast providers giving operators more cloud forecast sources to choose from in diff --git a/docs/_generated/configexample.md b/docs/_generated/configexample.md index 215fb56b..fb08bb46 100644 --- a/docs/_generated/configexample.md +++ b/docs/_generated/configexample.md @@ -240,7 +240,9 @@ "interval": 3600, "algorithm": "GENETIC", "visualize_pdf": true, + "terminal_value_mode": "AUTO", "terminal_value_euro_per_kwh": 0.0, + "terminal_value_window_hours": 24, "genetic": { "individuals": 400, "generations": 400, diff --git a/docs/_generated/configoptimization.md b/docs/_generated/configoptimization.md index 973cd3e3..f3337425 100644 --- a/docs/_generated/configoptimization.md +++ b/docs/_generated/configoptimization.md @@ -13,7 +13,9 @@ | horizon_hours | `EOS_OPTIMIZATION__HORIZON_HOURS` | `int` | `rw` | `24` | The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours. | | interval | `EOS_OPTIMIZATION__INTERVAL` | `int` | `rw` | `3600` | The optimization interval (slot length) [sec]. The genetic optimizer supports 3600 (1 hour) and 900 (15 min); other values fall back to 3600. Defaults to 3600 seconds (1 hour). | | keys | | `list[str]` | `ro` | `N/A` | The keys of the solution. | -| terminal_value_euro_per_kwh | `EOS_OPTIMIZATION__TERMINAL_VALUE_EURO_PER_KWH` | `float` | `rw` | `0.0` | Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh. | +| terminal_value_euro_per_kwh | `EOS_OPTIMIZATION__TERMINAL_VALUE_EURO_PER_KWH` | `float` | `rw` | `0.0` | Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Only used with terminal_value_mode = FIXED. Defaults to 0 EUR/kWh. | +| terminal_value_mode | `EOS_OPTIMIZATION__TERMINAL_VALUE_MODE` | `` | `rw` | `AUTO` | How to value the energy left in the battery at the end of the optimization horizon. AUTO derives a concave value curve from the trailing horizon window and needs no configuration; FIXED uses 'terminal_value_euro_per_kwh'. Defaults to AUTO. | +| terminal_value_window_hours | `EOS_OPTIMIZATION__TERMINAL_VALUE_WINDOW_HOURS` | `int` | `rw` | `24` | Length of the trailing horizon window the AUTO terminal value curve is derived from [h]. One day covers a full load and PV cycle. Defaults to 24 hours. | | visualize_pdf | `EOS_OPTIMIZATION__VISUALIZE_PDF` | `bool` | `rw` | `True` | Generate the PDF visualization after each optimization run. Disable for headless setups (e.g. Node-RED integration) to save several seconds per run. Defaults to True. | ::: @@ -30,7 +32,9 @@ "interval": 3600, "algorithm": "GENETIC", "visualize_pdf": true, + "terminal_value_mode": "AUTO", "terminal_value_euro_per_kwh": 0.0, + "terminal_value_window_hours": 24, "genetic": { "individuals": 400, "generations": 400, @@ -56,7 +60,9 @@ "interval": 3600, "algorithm": "GENETIC", "visualize_pdf": true, + "terminal_value_mode": "AUTO", "terminal_value_euro_per_kwh": 0.0, + "terminal_value_window_hours": 24, "genetic": { "individuals": 400, "generations": 400, diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index 3b31d605..534d071e 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,6 +1,6 @@ # Akkudoktor-EOS -**Version**: `v0.3.0.dev2609031505836006` +**Version**: `v0.3.0.dev2609040861878062` **Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. diff --git a/docs/akkudoktoreos/optimpost.md b/docs/akkudoktoreos/optimpost.md index 0f2c2379..e1d5dd11 100644 --- a/docs/akkudoktoreos/optimpost.md +++ b/docs/akkudoktoreos/optimpost.md @@ -274,8 +274,42 @@ C_{LCOS} = \frac{E_{bat,out}}{1000}\,c_{LCOS} ``` where `E_bat,out` is in Wh and `c_LCOS` is in EUR/kWh. This cost is included in -`Kosten_Euro_pro_Stunde`, `Gesamtkosten_Euro`, and therefore `Gesamtbilanz_Euro`. The terminal value -`preis_euro_pro_wh_akku`, by contrast, applies only to usable energy remaining after the last slot. +`Kosten_Euro_pro_Stunde`, `Gesamtkosten_Euro`, and therefore `Gesamtbilanz_Euro`. The terminal value, +by contrast, applies only to usable energy remaining after the last slot. + +#### Terminal Value of Stored Energy + +The optimization stops at the horizon, but the energy still in the battery keeps its worth: it +replaces grid imports that would otherwise be paid for afterwards. How that worth is credited is +set by `optimization.terminal_value_mode`. + +`AUTO` (the default) derives a **concave value curve** instead of using a single price. The value of +stored energy is not linear in the amount stored: + +- The first kWh replaces the most expensive hour that PV cannot cover. +- The next one replaces the second most expensive hour, and so on. +- Once every such hour is served, further energy replaces nothing - it is worth an export at best, + and nothing at worst. + +A single price has to pick one slope for all of it: high enough for the first kWh means hoarding a +full battery, low enough for the last kWh means running the battery empty by the end of the horizon. +The latter is what `terminal_value_euro_per_kwh = 0` does, and it is why `AUTO` is the default. + +There is no forecast beyond the horizon, so the trailing window of the horizon itself +(`optimization.terminal_value_window_hours`, 24 h by default) stands in for the day that follows: +same season, same household rhythm, same tariff structure. Within that window the residual load +`max(load - PV, 0)` of every slot is priced at its import price, sorted by price and accumulated - +that is the curve. The battery LCOS is subtracted from every marginal value so stored energy is not +credited twice, and energy beyond the residual load is only credited when direct marketing allows +the battery to export. + +`FIXED` restores the previous behaviour: every stored kWh is credited with +`optimization.terminal_value_euro_per_kwh`, or with `preis_euro_pro_wh_akku` of the request. In +`AUTO` mode that request field is ignored. + +The curve is built once per optimization run and only interpolated during the search, so it costs +nothing per candidate solution. It is a planning aid derived from a proxy day, not a forecast - see +`terminal_value` in the response to check what a run actually used. #### State of Charge (SoC) @@ -469,6 +503,14 @@ be unreachable. `deadline_policy` decides what happens then: - `battery_grid_export_factor`: Export level per slot as factor of the rated discharge power (`0.0` where no export is planned). Empty when direct marketing is disabled. A solution without this array exports at full power wherever `battery_grid_export_allowed` is 1. +- `terminal_value`: What the run credited for the energy left in the battery, and the curve it was + read from: + - `mode`: `AUTO` or `FIXED` + - `battery_energy_wh`: usable AC energy left at the end of the horizon + - `credited_euro`: the credit applied to the total balance + - `curve.energy_wh` / `curve.value_euro`: breakpoints of the value curve + - `curve.marginal_euro_per_kwh`: slope of each segment, monotonically decreasing + - `curve.window_slots`: how many trailing horizon slots the curve was derived from With direct marketing enabled, `dc_charge = 1` and `discharge_allowed = 1` may occur together. This is the normal self-consumption mode: within a coarse optimization slot, the battery may cover diff --git a/openapi.json b/openapi.json index 3093a87c..0312a781 100644 --- a/openapi.json +++ b/openapi.json @@ -8,7 +8,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "v0.3.0.dev2609031505836006" + "version": "v0.3.0.dev2609040861878062" }, "paths": { "/v1/admin/cache/clear": { @@ -5570,6 +5570,17 @@ "title": "Battery Grid Export Allowed", "description": "Array with battery-to-grid export values (1 for export discharge, 0 otherwise)." }, + "terminal_value": { + "anyOf": [ + { + "$ref": "#/components/schemas/TerminalValueResult" + }, + { + "type": "null" + } + ], + "description": "The terminal value applied to the energy left in the battery at the end of the horizon, including the curve it was read from. None when no battery is part of the optimization." + }, "battery_grid_export_factor": { "items": { "type": "number" @@ -7533,16 +7544,35 @@ false ] }, + "terminal_value_mode": { + "$ref": "#/components/schemas/TerminalValueMode", + "description": "How to value the energy left in the battery at the end of the optimization horizon. AUTO derives a concave value curve from the trailing horizon window and needs no configuration; FIXED uses 'terminal_value_euro_per_kwh'. Defaults to AUTO.", + "default": "AUTO", + "examples": [ + "AUTO", + "FIXED" + ] + }, "terminal_value_euro_per_kwh": { "type": "number", "title": "Terminal Value Euro Per Kwh", - "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh.", + "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Only used with terminal_value_mode = FIXED. Defaults to 0 EUR/kWh.", "default": 0.0, "examples": [ 0.0, 0.2 ] }, + "terminal_value_window_hours": { + "type": "integer", + "minimum": 1.0, + "title": "Terminal Value Window Hours", + "description": "Length of the trailing horizon window the AUTO terminal value curve is derived from [h]. One day covers a full load and PV cycle. Defaults to 24 hours.", + "default": 24, + "examples": [ + 24 + ] + }, "genetic": { "$ref": "#/components/schemas/GeneticCommonSettings", "description": "Genetic optimization algorithm configuration.", @@ -7603,16 +7633,35 @@ false ] }, + "terminal_value_mode": { + "$ref": "#/components/schemas/TerminalValueMode", + "description": "How to value the energy left in the battery at the end of the optimization horizon. AUTO derives a concave value curve from the trailing horizon window and needs no configuration; FIXED uses 'terminal_value_euro_per_kwh'. Defaults to AUTO.", + "default": "AUTO", + "examples": [ + "AUTO", + "FIXED" + ] + }, "terminal_value_euro_per_kwh": { "type": "number", "title": "Terminal Value Euro Per Kwh", - "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Defaults to 0 EUR/kWh.", + "description": "Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Only used with terminal_value_mode = FIXED. Defaults to 0 EUR/kWh.", "default": 0.0, "examples": [ 0.0, 0.2 ] }, + "terminal_value_window_hours": { + "type": "integer", + "minimum": 1.0, + "title": "Terminal Value Window Hours", + "description": "Length of the trailing horizon window the AUTO terminal value curve is derived from [h]. One day covers a full load and PV cycle. Defaults to 24 hours.", + "default": 24, + "examples": [ + 24 + ] + }, "genetic": { "$ref": "#/components/schemas/GeneticCommonSettings", "description": "Genetic optimization algorithm configuration.", @@ -9612,6 +9661,94 @@ "title": "SolarPanelBatteryParameters", "description": "PV battery device simulation configuration." }, + "TerminalValueCurve": { + "properties": { + "energy_wh": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Energy Wh", + "description": "Breakpoints of usable AC energy left in the battery [Wh]." + }, + "value_euro": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Value Euro", + "description": "Cumulative credit at each breakpoint [EUR]." + }, + "marginal_euro_per_kwh": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Marginal Euro Per Kwh", + "description": "Marginal value of the segment that starts at each breakpoint [EUR/kWh]. Monotonically decreasing." + }, + "window_slots": { + "type": "integer", + "title": "Window Slots", + "description": "Number of trailing horizon slots the curve was derived from. Fewer slots than a full day mean a shorter proxy period.", + "default": 0 + } + }, + "type": "object", + "title": "TerminalValueCurve", + "description": "Piecewise linear, concave value of battery energy left at the horizon.\n\n``energy_wh`` and ``value_euro`` are the breakpoints of the cumulative\nvalue, ``marginal_euro_per_kwh`` the slope of each segment. Both arrays\nstart at the origin; the curve is flat beyond its last breakpoint." + }, + "TerminalValueMode": { + "type": "string", + "enum": [ + "AUTO", + "FIXED" + ], + "title": "TerminalValueMode", + "description": "How the energy left in the battery at the end of the horizon is valued.\n\nModes\n-----\n- AUTO:\n Derive a concave value curve from the trailing horizon window: the\n first stored kWh replaces the most expensive hour that PV cannot\n cover, the next one the second most expensive, and so on. Needs no\n configuration and adapts to prices, load and PV of the day.\n\n- FIXED:\n Credit every stored kWh with the configured\n ``terminal_value_euro_per_kwh`` (or ``preis_euro_pro_wh_akku`` of the\n request). The historical behaviour; a value of 0 makes the optimizer\n empty the battery towards the end of the horizon." + }, + "TerminalValueResult": { + "properties": { + "mode": { + "type": "string", + "title": "Mode", + "description": "Terminal value mode the run used: AUTO or FIXED.", + "examples": [ + "AUTO", + "FIXED" + ] + }, + "battery_energy_wh": { + "type": "number", + "title": "Battery Energy Wh", + "description": "Usable AC energy left in the battery at the end of the horizon [Wh].", + "default": 0.0 + }, + "credited_euro": { + "type": "number", + "title": "Credited Euro", + "description": "Credit applied to the total balance [EUR].", + "default": 0.0 + }, + "curve": { + "anyOf": [ + { + "$ref": "#/components/schemas/TerminalValueCurve" + }, + { + "type": "null" + } + ], + "description": "The value curve the credit was read from; None in FIXED mode." + } + }, + "type": "object", + "required": [ + "mode" + ], + "title": "TerminalValueResult", + "description": "What the optimizer credited for the energy left in the battery." + }, "TimeWindow-Input": { "properties": { "start_time": { diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index c40e8fe7..14657d3d 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -22,6 +22,12 @@ from akkudoktoreos.optimization.genetic.geneticparams import ( GeneticEnergyManagementParameters, GeneticOptimizationParameters, ) +from akkudoktoreos.optimization.genetic.terminalvalue import ( + TerminalValueCurve, + TerminalValueResult, + build_terminal_value_curve, + trailing_window, +) from akkudoktoreos.optimization.genetic.geneticsolution import ( GeneticSimulationResult, GeneticSolution, @@ -673,6 +679,9 @@ class GeneticOptimization(OptimizationBase): # Slot by which the EV has to reach its target SoC. None means the SoC is # only required at the end of the horizon (the behaviour without a deadline). self._ev_soc_deadline_slot: Optional[int] = None + # Concave value of the energy left in the battery at the end of the + # horizon. None means the fixed scalar terminal value is used instead. + self._terminal_value_curve: Optional[TerminalValueCurve] = None self.verbose = verbose self.fix_seed = fixed_seed self.optimize_ev = True @@ -811,6 +820,109 @@ class GeneticOptimization(OptimizationBase): # A deadline in the past means the target is due right now. return max(deadline_slot, start_slot) + def _build_terminal_value_curve( + self, + battery: Optional[Battery], + inverter: Optional[Inverter], + ) -> Optional[TerminalValueCurve]: + """Derive the terminal value curve from the trailing horizon window. + + Only built in AUTO mode and only with a battery: the curve describes + what the energy left in that battery is worth once the horizon ends. + + Args: + battery: The house battery of this run, if any. + inverter: The inverter, needed for the DC/AC conversion. + + Returns: + The curve, or None when the fixed scalar terminal value applies. + """ + if battery is None: + return None + try: + mode = self.config.optimization.terminal_value_mode + window_hours = self.config.optimization.terminal_value_window_hours + except Exception: + return None + if str(mode) != "AUTO": + return None + + dc_to_ac = inverter.dc_to_ac_efficiency if inverter else 1.0 + # A full battery, expressed in the same unit as the curve: AC energy + # that can actually leave the house. + max_energy_wh = ( + max(battery.max_soc_wh - battery.min_soc_wh, 0.0) + * battery.discharging_efficiency + * dc_to_ac + ) + window_slots = max(int(window_hours) * self.slots_per_hour, 1) + end_slot = min( + self.total_slots, + self._start_day_slot() + self.config.optimization.horizon_hours * self.slots_per_hour, + ) + + curve = build_terminal_value_curve( + prices_euro_per_wh=trailing_window( + self.simulation.elect_price_hourly, end_slot, window_slots + ), + load_wh=trailing_window(self.simulation.load_energy_array, end_slot, window_slots), + pv_wh=trailing_window(self.simulation.pv_prediction_wh, end_slot, window_slots), + feed_in_euro_per_wh=trailing_window( + self.simulation.elect_revenue_per_hour_arr, end_slot, window_slots + ), + max_energy_wh=max_energy_wh, + lcos_euro_per_kwh=getattr(battery, "levelized_cost_of_storage_kwh", 0.0), + dc_to_ac_efficiency=dc_to_ac, + grid_export_allowed=self.optimize_battery_grid_export, + ) + if curve.energy_wh: + logger.debug( + "Terminal value curve: {} segments, first {:.3f} EUR/kWh, last {:.3f} EUR/kWh, " + "knee at {:.0f} Wh.", + len(curve.marginal_euro_per_kwh), + curve.marginal_euro_per_kwh[0], + curve.marginal_euro_per_kwh[-1], + curve.energy_wh[-1], + ) + return curve + + def _terminal_value( + self, parameters: GeneticOptimizationParameters + ) -> tuple[float, TerminalValueResult]: + """Credit for the energy left in the battery, plus its report. + + Args: + parameters: Optimization parameters, holding the fixed scalar value. + + Returns: + The credit in EUR and the result object for the solution. + """ + battery = self.simulation.battery + if battery is None: + return 0.0, TerminalValueResult(mode="FIXED") + + # Usable DC energy, converted to the AC energy that can serve a load. + energy_wh = battery.current_energy_content() + if self.simulation.inverter: + energy_wh *= self.simulation.inverter.dc_to_ac_efficiency + + curve = getattr(self, "_terminal_value_curve", None) + if curve is not None and curve.energy_wh: + credit = curve.value(energy_wh) + return credit, TerminalValueResult( + mode="AUTO", + battery_energy_wh=energy_wh, + credited_euro=credit, + curve=curve, + ) + + credit = energy_wh * parameters.ems.preis_euro_pro_wh_akku + return credit, TerminalValueResult( + mode="FIXED", + battery_energy_wh=energy_wh, + credited_euro=credit, + ) + def _build_appliance_layout( self, appliances: list[HomeAppliance], slot0_datetime: Any ) -> ApplianceGeneLayout: @@ -2433,14 +2545,12 @@ class GeneticOptimization(OptimizationBase): else 0, ) - # Adjust total balance with battery value and penalties for unmet SOC + # Adjust total balance with battery value and penalties for unmet SOC. + # The terminal value is concave in AUTO mode: the first stored kWh + # replaces the most expensive hour after the horizon, the last one + # replaces nothing. A scalar cannot express that (see terminalvalue.py). if self.simulation.battery: - battery_energy_content = self.simulation.battery.current_energy_content() - # Apply DC→AC inverter efficiency to residual battery value - # (stored DC energy must pass through inverter to be usable as AC) - if self.simulation.inverter: - battery_energy_content *= self.simulation.inverter.dc_to_ac_efficiency - restwert_akku = battery_energy_content * parameters.ems.preis_euro_pro_wh_akku + restwert_akku, _ = self._terminal_value(parameters) gesamtbilanz += -restwert_akku # --- AC charging break-even penalty --- @@ -2958,6 +3068,11 @@ class GeneticOptimization(OptimizationBase): direct_marketing_enabled=direct_marketing_enabled, ) + # Terminal value of the energy left in the battery. Built once per run - + # it needs the prepared price/load/PV series - so every fitness + # evaluation only interpolates on it. + self._terminal_value_curve = self._build_terminal_value_curve(akku, inverter) + # Setup the DEAP environment and optimization process. The appliance # genome layout (built above) drives the appliance gene block; evaluate # gets the slot index (its break-even loop walks the slot arrays from "now"). @@ -2978,6 +3093,8 @@ class GeneticOptimization(OptimizationBase): # Perform final evaluation on the best solution simulation_result = self.evaluate_inner(start_solution) + # Read the terminal value off the final battery state, for the solution. + _, terminal_value_result = self._terminal_value(parameters) # Prepare results discharge_hours_bin, eautocharge_hours_index, appliance_gene_values = self.split_individual( @@ -3086,6 +3203,7 @@ class GeneticOptimization(OptimizationBase): "discharge_allowed": discharge, "battery_grid_export_allowed": battery_grid_export, "battery_grid_export_factor": battery_grid_export_factor, + "terminal_value": terminal_value_result, "eautocharge_hours_float": eautocharge_hours_float, "result": GeneticSimulationResult(**simulation_result), "eauto_obj": self.simulation.ev, diff --git a/src/akkudoktoreos/optimization/genetic/geneticsolution.py b/src/akkudoktoreos/optimization/genetic/geneticsolution.py index 1a6d5e81..64e74e9a 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticsolution.py +++ b/src/akkudoktoreos/optimization/genetic/geneticsolution.py @@ -24,6 +24,7 @@ from akkudoktoreos.devices.devicesabc import ( ) from akkudoktoreos.devices.genetic.battery import Battery from akkudoktoreos.optimization.genetic.geneticdevices import GeneticParametersBaseModel +from akkudoktoreos.optimization.genetic.terminalvalue import TerminalValueResult from akkudoktoreos.optimization.optimization import OptimizationSolution from akkudoktoreos.utils.datetimeutil import DateTime, to_datetime, to_duration from akkudoktoreos.utils.utils import NumpyEncoder @@ -201,6 +202,16 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): "description": "Array with battery-to-grid export values (1 for export discharge, 0 otherwise)." }, ) + terminal_value: Optional[TerminalValueResult] = Field( + default=None, + json_schema_extra={ + "description": ( + "The terminal value applied to the energy left in the battery at " + "the end of the horizon, including the curve it was read from. " + "None when no battery is part of the optimization." + ) + }, + ) battery_grid_export_factor: list[float] = Field( default_factory=list, json_schema_extra={ @@ -236,8 +247,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): default_factory=dict, json_schema_extra={ "description": ( - "Scheduled run start times per appliance device_id as absolute " - "local datetimes." + "Scheduled run start times per appliance device_id as absolute " "local datetimes." ) }, ) diff --git a/src/akkudoktoreos/optimization/genetic/terminalvalue.py b/src/akkudoktoreos/optimization/genetic/terminalvalue.py new file mode 100644 index 00000000..1ee3d605 --- /dev/null +++ b/src/akkudoktoreos/optimization/genetic/terminalvalue.py @@ -0,0 +1,239 @@ +"""Terminal value of the energy left in the battery at the end of the horizon. + +The optimizer stops at the horizon, but the energy still stored in the battery +keeps its worth: it replaces grid imports that would otherwise be paid for +afterwards. Crediting that worth with a single price per kWh - the historical +``preis_euro_pro_wh_akku`` - cannot describe it, because the value of stored +energy is **not linear in the amount stored**: + +- The first kWh replaces the most expensive hour after the horizon. +- The next one replaces the second most expensive hour, and so on. +- Once every hour that PV cannot cover is served, further energy replaces + nothing; it is worth an export at best, and nothing at worst. + +The resulting value function is monotone and concave. A scalar has to pick one +slope: high enough for the first kWh means hoarding a full battery, low enough +for the last kWh means running it empty by midnight. This module builds the +curve instead. + +There is no forecast beyond the horizon, so the trailing window of the horizon +itself stands in for the day that follows: same season, same household rhythm, +same tariff structure. That approximation is the reason the curve is a planning +aid, not a prediction - which is also why the marginal values are deliberately +conservative wherever a choice exists. +""" + +from typing import Optional + +import numpy as np +from loguru import logger + +from akkudoktoreos.core.pydantic import PydanticBaseModel +from pydantic import Field + + +class TerminalValueCurve(PydanticBaseModel): + """Piecewise linear, concave value of battery energy left at the horizon. + + ``energy_wh`` and ``value_euro`` are the breakpoints of the cumulative + value, ``marginal_euro_per_kwh`` the slope of each segment. Both arrays + start at the origin; the curve is flat beyond its last breakpoint. + """ + + energy_wh: list[float] = Field( + default_factory=list, + json_schema_extra={ + "description": "Breakpoints of usable AC energy left in the battery [Wh]." + }, + ) + value_euro: list[float] = Field( + default_factory=list, + json_schema_extra={"description": "Cumulative credit at each breakpoint [EUR]."}, + ) + marginal_euro_per_kwh: list[float] = Field( + default_factory=list, + json_schema_extra={ + "description": ( + "Marginal value of the segment that starts at each breakpoint " + "[EUR/kWh]. Monotonically decreasing." + ) + }, + ) + window_slots: int = Field( + default=0, + json_schema_extra={ + "description": ( + "Number of trailing horizon slots the curve was derived from. " + "Fewer slots than a full day mean a shorter proxy period." + ) + }, + ) + + def value(self, energy_wh: float) -> float: + """Return the credit for ``energy_wh`` of usable AC energy [EUR]. + + Args: + energy_wh: Usable AC energy left in the battery. + + Returns: + Interpolated value of the curve; 0.0 for an empty curve. + """ + if not self.energy_wh or energy_wh <= 0.0: + return 0.0 + return float(np.interp(energy_wh, self.energy_wh, self.value_euro)) + + +class TerminalValueResult(PydanticBaseModel): + """What the optimizer credited for the energy left in the battery.""" + + mode: str = Field( + json_schema_extra={ + "description": "Terminal value mode the run used: AUTO or FIXED.", + "examples": ["AUTO", "FIXED"], + } + ) + battery_energy_wh: float = Field( + default=0.0, + json_schema_extra={ + "description": "Usable AC energy left in the battery at the end of the horizon [Wh]." + }, + ) + credited_euro: float = Field( + default=0.0, + json_schema_extra={"description": "Credit applied to the total balance [EUR]."}, + ) + curve: Optional[TerminalValueCurve] = Field( + default=None, + json_schema_extra={ + "description": "The value curve the credit was read from; None in FIXED mode." + }, + ) + + +def build_terminal_value_curve( + *, + prices_euro_per_wh: np.ndarray, + load_wh: np.ndarray, + pv_wh: np.ndarray, + feed_in_euro_per_wh: np.ndarray, + max_energy_wh: float, + lcos_euro_per_kwh: float = 0.0, + dc_to_ac_efficiency: float = 1.0, + grid_export_allowed: bool = False, +) -> TerminalValueCurve: + """Build the terminal value curve from the trailing horizon window. + + Every slot of the window contributes its residual load - the part of the + load that PV does not cover - at its import price. Sorting those slots by + price and accumulating them yields the marginal value of the first, second, + ... kWh in the battery. Energy beyond the residual load can only be + exported, and only when direct marketing allows it. + + Args: + prices_euro_per_wh: Import prices of the window [EUR/Wh]. + load_wh: Load per slot of the window [Wh]. + pv_wh: PV generation per slot of the window [Wh]. + feed_in_euro_per_wh: Feed-in tariff of the window [EUR/Wh]. + max_energy_wh: Usable AC energy of a full battery [Wh]; the curve ends here. + lcos_euro_per_kwh: Levelized cost of storage, already charged per + delivered DC energy in the simulation and therefore subtracted here + so stored energy is not credited twice. + dc_to_ac_efficiency: Inverter efficiency, used to convert the LCOS from + delivered DC energy to the AC energy of the curve. + grid_export_allowed: Whether the battery may feed the grid (direct + marketing). Without it, energy beyond the residual load gets no + credit: it can neither be exported nor is its use covered by the + proxy window. + + Returns: + The curve; empty when the window carries no usable information. + """ + window = min(len(prices_euro_per_wh), len(load_wh), len(pv_wh)) + if window <= 0 or max_energy_wh <= 0.0: + return TerminalValueCurve() + + residual = np.maximum(load_wh[:window] - pv_wh[:window], 0.0) + prices = np.asarray(prices_euro_per_wh[:window], dtype=float) + + # LCOS is charged on delivered DC energy; the curve is in AC energy. + lcos_per_wh_ac = (lcos_euro_per_kwh / 1000.0) / max(dc_to_ac_efficiency, 1e-9) + + order = np.argsort(-prices) + energy_points: list[float] = [0.0] + value_points: list[float] = [0.0] + marginals: list[float] = [] + + cumulative_energy = 0.0 + cumulative_value = 0.0 + for index in order: + slot_energy = float(residual[index]) + if slot_energy <= 0.0: + continue + # Negative or very cheap hours are not worth storing energy for. + marginal = max(float(prices[index]) - lcos_per_wh_ac, 0.0) + if marginal <= 0.0: + continue + slot_energy = min(slot_energy, max_energy_wh - cumulative_energy) + if slot_energy <= 0.0: + break + cumulative_energy += slot_energy + cumulative_value += slot_energy * marginal + energy_points.append(cumulative_energy) + value_points.append(cumulative_value) + marginals.append(marginal * 1000.0) + + # Everything beyond the residual load can only be sold. A median feed-in + # tariff rather than the best one: exporting all of it in the single best + # slot is not something the horizon can promise. + if grid_export_allowed and cumulative_energy < max_energy_wh: + positive_feed_in = [ + float(value) for value in feed_in_euro_per_wh[:window] if float(value) > 0.0 + ] + export_marginal = max( + (float(np.median(positive_feed_in)) if positive_feed_in else 0.0) - lcos_per_wh_ac, + 0.0, + ) + if export_marginal > 0.0: + remaining = max_energy_wh - cumulative_energy + cumulative_energy += remaining + cumulative_value += remaining * export_marginal + energy_points.append(cumulative_energy) + value_points.append(cumulative_value) + marginals.append(export_marginal * 1000.0) + + if len(energy_points) <= 1: + logger.debug("Terminal value curve is empty - no priced residual load in the window.") + return TerminalValueCurve(window_slots=window) + + # The segment slopes are decreasing by construction (prices were sorted), + # so the curve is concave; the export tail is the flattest segment. + return TerminalValueCurve( + energy_wh=energy_points, + value_euro=value_points, + marginal_euro_per_kwh=marginals, + window_slots=window, + ) + + +def trailing_window( + values: Optional[np.ndarray], + end_slot: int, + window_slots: int, +) -> np.ndarray: + """Return the ``window_slots`` values in front of ``end_slot``. + + Args: + values: Full slot array, or None. + end_slot: Exclusive end of the window (end of the optimization horizon). + window_slots: Desired window length; a shorter horizon yields less. + + Returns: + The window as a float array, empty when no data is available. + """ + if values is None: + return np.zeros(0, dtype=float) + end = min(int(end_slot), len(values)) + start = max(end - int(window_slots), 0) + if end <= start: + return np.zeros(0, dtype=float) + return np.asarray(values[start:end], dtype=float) diff --git a/src/akkudoktoreos/optimization/optimization.py b/src/akkudoktoreos/optimization/optimization.py index f1d0c0c6..04f34928 100644 --- a/src/akkudoktoreos/optimization/optimization.py +++ b/src/akkudoktoreos/optimization/optimization.py @@ -1,3 +1,4 @@ +from enum import StrEnum from typing import Optional, Union from pydantic import Field, computed_field @@ -11,6 +12,28 @@ from akkudoktoreos.core.pydantic import ( from akkudoktoreos.utils.datetimeutil import DateTime +class TerminalValueMode(StrEnum): + """How the energy left in the battery at the end of the horizon is valued. + + Modes + ----- + - AUTO: + Derive a concave value curve from the trailing horizon window: the + first stored kWh replaces the most expensive hour that PV cannot + cover, the next one the second most expensive, and so on. Needs no + configuration and adapts to prices, load and PV of the day. + + - FIXED: + Credit every stored kWh with the configured + ``terminal_value_euro_per_kwh`` (or ``preis_euro_pro_wh_akku`` of the + request). The historical behaviour; a value of 0 makes the optimizer + empty the battery towards the end of the horizon. + """ + + AUTO = "AUTO" + FIXED = "FIXED" + + class GeneticCommonSettings(SettingsBaseModel): """General Genetic Optimization Algorithm Configuration.""" @@ -101,18 +124,45 @@ class OptimizationCommonSettings(SettingsBaseModel): }, ) + terminal_value_mode: TerminalValueMode = Field( + default=TerminalValueMode.AUTO, + json_schema_extra={ + "description": ( + "How to value the energy left in the battery at the end of the " + "optimization horizon. AUTO derives a concave value curve from " + "the trailing horizon window and needs no configuration; FIXED " + "uses 'terminal_value_euro_per_kwh'. Defaults to AUTO." + ), + "examples": ["AUTO", "FIXED"], + }, + ) + terminal_value_euro_per_kwh: float = Field( default=0.0, json_schema_extra={ "description": ( "Value assigned to usable battery energy remaining at the end of the " "optimization horizon [EUR/kWh]. This terminal value is independent " - "of the battery LCOS. Defaults to 0 EUR/kWh." + "of the battery LCOS. Only used with terminal_value_mode = FIXED. " + "Defaults to 0 EUR/kWh." ), "examples": [0.0, 0.20], }, ) + terminal_value_window_hours: int = Field( + default=24, + ge=1, + json_schema_extra={ + "description": ( + "Length of the trailing horizon window the AUTO terminal value " + "curve is derived from [h]. One day covers a full load and PV " + "cycle. Defaults to 24 hours." + ), + "examples": [24], + }, + ) + genetic: GeneticCommonSettings = Field( default_factory=GeneticCommonSettings, json_schema_extra={ diff --git a/tests/test_geneticoptimize.py b/tests/test_geneticoptimize.py index 60285b8e..9e16b38e 100644 --- a/tests/test_geneticoptimize.py +++ b/tests/test_geneticoptimize.py @@ -379,3 +379,93 @@ def test_ev_deadline_charges_before_departure(config_eos: ConfigEOS): # Slot 6 is the first slot at or after the deadline, so its start-of-slot SoC # is what the target is checked against. assert soc_per_hour[6] >= 60.0 + + +def _terminal_value_run(config_eos: ConfigEOS, mode: str) -> GeneticSolution: + """48 h with expensive energy and two dirt-cheap slots at the very end. + + Charging in those last slots only pays off when the stored energy keeps a + value beyond the horizon. + """ + hours = 48 + config_eos.merge_settings_from_dict( + { + "prediction": {"hours": hours}, + "optimization": { + "horizon_hours": hours, + "interval": 3600, + "terminal_value_mode": mode, + "terminal_value_euro_per_kwh": 0.0, + "genetic": {"individuals": 80, "generations": 20}, + }, + } + ) + ems_eos.set_start_datetime(to_datetime().set(hour=0, minute=0)) + CacheEnergyManagementStore().clear() + + prices = [0.0004] * (hours - 2) + [0.00002] * 2 + parameters = GeneticOptimizationParameters( + ems={ + "pv_prognose_wh": [0.0] * hours, + "strompreis_euro_pro_wh": prices, + "einspeiseverguetung_euro_pro_wh": [0.00007] * hours, + "preis_euro_pro_wh_akku": 0.0, + "gesamtlast": [200.0] * hours, + }, + pv_akku={ + "device_id": "battery1", + "capacity_wh": 10000, + "initial_soc_percentage": 20, + "min_soc_percentage": 0, + "max_soc_percentage": 100, + "charging_efficiency": 1.0, + "discharging_efficiency": 1.0, + "max_charge_power_w": 5000, + }, + inverter={ + "device_id": "inverter1", + "max_power_wh": 10000, + "battery_id": "battery1", + "ac_to_dc_efficiency": 1.0, + "dc_to_ac_efficiency": 1.0, + "max_ac_charge_power_w": 5000, + }, + eauto=None, + ) + return GeneticOptimization(fixed_seed=7).optimierung_ems( + parameters=parameters, start_hour=0, ngen=20 + ) + + +def test_terminal_value_auto_keeps_energy_that_fixed_zero_throws_away(config_eos: ConfigEOS): + """AUTO values the energy left in the battery, a fixed zero does not.""" + auto = _terminal_value_run(config_eos, "AUTO") + fixed = _terminal_value_run(config_eos, "FIXED") + + assert auto.terminal_value is not None + assert auto.terminal_value.mode == "AUTO" + assert auto.terminal_value.curve is not None + assert auto.terminal_value.credited_euro > 0.0 + + assert fixed.terminal_value is not None + assert fixed.terminal_value.mode == "FIXED" + assert fixed.terminal_value.credited_euro == 0.0 + + # The cheap slots at the end are only worth using with a terminal value. + assert auto.result.akku_soc_pro_stunde[-1] > fixed.result.akku_soc_pro_stunde[-1] + + +def test_terminal_value_curve_is_concave_and_reported(config_eos: ConfigEOS): + """The reported curve is what the credit was read from.""" + solution = _terminal_value_run(config_eos, "AUTO") + curve = solution.terminal_value.curve + + assert curve.window_slots == 24 + assert len(curve.energy_wh) == len(curve.value_euro) + assert len(curve.marginal_euro_per_kwh) == len(curve.energy_wh) - 1 + marginals = curve.marginal_euro_per_kwh + assert all(a >= b for a, b in zip(marginals, marginals[1:])) + + # The credit is the curve evaluated at the energy left in the battery. + expected = curve.value(solution.terminal_value.battery_energy_wh) + assert solution.terminal_value.credited_euro == pytest.approx(expected) diff --git a/tests/test_terminalvalue.py b/tests/test_terminalvalue.py new file mode 100644 index 00000000..a82b2157 --- /dev/null +++ b/tests/test_terminalvalue.py @@ -0,0 +1,114 @@ +"""Tests for the concave terminal value of the energy left in the battery.""" + +import numpy as np +import pytest + +from akkudoktoreos.optimization.genetic.terminalvalue import ( + build_terminal_value_curve, + trailing_window, +) + + +def _curve(**overrides): + """Two expensive slots, one cheap one, no PV, 10 kWh of usable battery.""" + params = dict( + prices_euro_per_wh=np.array([0.0004, 0.0003, 0.0001]), + load_wh=np.array([1000.0, 1000.0, 1000.0]), + pv_wh=np.array([0.0, 0.0, 0.0]), + feed_in_euro_per_wh=np.array([0.00008, 0.00008, 0.00008]), + max_energy_wh=10000.0, + lcos_euro_per_kwh=0.0, + dc_to_ac_efficiency=1.0, + grid_export_allowed=False, + ) + params.update(overrides) + return build_terminal_value_curve(**params) + + +def test_marginal_value_follows_the_most_expensive_hours_first(): + """The first stored kWh replaces the most expensive slot, then the next.""" + curve = _curve() + + # 0.40, 0.30 and 0.10 EUR/kWh, in that order. + assert curve.marginal_euro_per_kwh == pytest.approx([0.4, 0.3, 0.1]) + assert curve.energy_wh == pytest.approx([0.0, 1000.0, 2000.0, 3000.0]) + assert curve.value_euro == pytest.approx([0.0, 0.4, 0.7, 0.8]) + + +def test_curve_is_concave_and_saturates(): + """Marginal values only decrease, and beyond the last breakpoint nothing is added.""" + curve = _curve() + marginals = curve.marginal_euro_per_kwh + + assert all(a >= b for a, b in zip(marginals, marginals[1:])) + # The residual load of the window is 3 kWh - more energy replaces nothing. + assert curve.value(3000.0) == pytest.approx(0.8) + assert curve.value(9000.0) == pytest.approx(0.8) + + +def test_value_interpolates_within_a_segment(): + """Half of the first slot is worth half of the first segment.""" + curve = _curve() + assert curve.value(500.0) == pytest.approx(0.2) + + +def test_pv_reduces_the_residual_load(): + """Only load that PV cannot cover can be replaced by stored energy.""" + curve = _curve(pv_wh=np.array([600.0, 1000.0, 0.0])) + + # Slot 0 keeps 400 Wh, slot 1 is fully covered by PV, slot 2 keeps 1000 Wh. + assert curve.energy_wh == pytest.approx([0.0, 400.0, 1400.0]) + assert curve.marginal_euro_per_kwh == pytest.approx([0.4, 0.1]) + + +def test_lcos_is_subtracted_from_the_marginal_value(): + """Storage cost is already charged on discharge and must not be credited twice.""" + curve = _curve(lcos_euro_per_kwh=0.05, dc_to_ac_efficiency=1.0) + assert curve.marginal_euro_per_kwh == pytest.approx([0.35, 0.25, 0.05]) + + +def test_negative_prices_do_not_create_value(): + """Storing energy for an hour that pays nothing is not worth anything.""" + curve = _curve(prices_euro_per_wh=np.array([0.0004, -0.0001, 0.0])) + assert curve.marginal_euro_per_kwh == pytest.approx([0.4]) + assert curve.value(5000.0) == pytest.approx(0.4) + + +def test_export_tail_only_with_direct_marketing(): + """Surplus beyond the residual load is worth an export - if export is allowed.""" + without = _curve(grid_export_allowed=False) + with_export = _curve(grid_export_allowed=True) + + assert without.value(10000.0) == pytest.approx(0.8) + # 7 kWh beyond the residual load at the median feed-in tariff of 0.08 EUR/kWh. + assert with_export.value(10000.0) == pytest.approx(0.8 + 7.0 * 0.08) + assert with_export.marginal_euro_per_kwh[-1] == pytest.approx(0.08) + + +def test_curve_is_capped_by_the_usable_battery_energy(): + """A battery smaller than the residual load ends the curve early.""" + curve = _curve(max_energy_wh=1500.0) + assert curve.energy_wh[-1] == pytest.approx(1500.0) + assert curve.value(5000.0) == pytest.approx(0.4 + 0.5 * 0.3) + + +def test_empty_window_yields_an_empty_curve(): + """Without data there is no curve, and no credit.""" + curve = build_terminal_value_curve( + prices_euro_per_wh=np.zeros(0), + load_wh=np.zeros(0), + pv_wh=np.zeros(0), + feed_in_euro_per_wh=np.zeros(0), + max_energy_wh=10000.0, + ) + assert curve.energy_wh == [] + assert curve.value(5000.0) == 0.0 + + +def test_trailing_window_takes_the_end_of_the_horizon(): + values = np.arange(10, dtype=float) + + assert list(trailing_window(values, end_slot=8, window_slots=3)) == [5.0, 6.0, 7.0] + # A window longer than the horizon yields what there is. + assert list(trailing_window(values, end_slot=2, window_slots=5)) == [0.0, 1.0] + assert list(trailing_window(None, end_slot=8, window_slots=3)) == [] diff --git a/tests/testdata/optimize_result_1.json b/tests/testdata/optimize_result_1.json index 2333b345..fce12fa8 100644 --- a/tests/testdata/optimize_result_1.json +++ b/tests/testdata/optimize_result_1.json @@ -116,13 +116,14 @@ 0, 0, 0, + 0, + 0, + 0, 1, 1, 1, - 1, - 1, - 1, - 1, + 0, + 0, 1, 1, 1, @@ -133,9 +134,7 @@ 1, 1, 0, - 0, - 0, - 0, + 1, 0, 0, 0, @@ -144,12 +143,75 @@ 0, 0, 1, + 1, + 1, + 1, 0, - 1, - 1, - 1 + 0 ], "battery_grid_export_allowed": [], + "terminal_value": { + "mode": "AUTO", + "battery_energy_wh": 17857.05093527103, + "credited_euro": 2.9274511210000003, + "curve": { + "energy_wh": [ + 0.0, + 516.37, + 1005.26, + 1873.31, + 2673.16, + 3180.0699999999997, + 3736.3799999999997, + 4430.719999999999, + 5039.509999999999, + 5297.15, + 5863.84, + 6670.2300000000005, + 7657.240000000001, + 8391.230000000001, + 8984.2, + 9335.85 + ], + "value_euro": [ + 0.0, + 0.174739608, + 0.337735534, + 0.625754524, + 0.889864994, + 1.056638384, + 1.239608743, + 1.4676299990000001, + 1.667495756, + 1.749811736, + 1.924408925, + 2.169712763, + 2.462756032, + 2.6771545110000003, + 2.8420001710000005, + 2.9274511210000003 + ], + "marginal_euro_per_kwh": [ + 0.3384, + 0.33340000000000003, + 0.3318, + 0.3302, + 0.32899999999999996, + 0.3289, + 0.3284, + 0.32830000000000004, + 0.3195, + 0.3081, + 0.3042, + 0.2969, + 0.2921, + 0.27799999999999997, + 0.243 + ], + "window_slots": 24 + } + }, + "battery_grid_export_factor": [], "eautocharge_hours_float": null, "result": { "Last_Wh_pro_Stunde": [ @@ -239,9 +301,9 @@ 0.0, 0.022582049506752234, 0.3039575, - 0.1926250109597104, - 0.13346423958241627, - 0.053398267180554584, + 0.19320652266312205, + 0.1358062627100041, + 0.0692592282596561, 0.0, 0.0, 0.0, @@ -262,20 +324,20 @@ 0.0, 0.0, 0.0, - 0.024351216461056053, + 0.065955694066579, 0.15236390731688437, 0.10316291465819699, - 0.05435777788576338, + 0.029150936607659956, 0.0, 0.0, 0.0, 0.0, 0.0 ], - "Gesamt_Verluste": 3807.1176630027076, - "Gesamtbilanz_Euro": 0.22702041221600888, - "Gesamteinnahmen_Euro": 1.0402628835513343, - "Gesamtkosten_Euro": 1.2672832957673432, + "Gesamt_Verluste": 3596.923518513394, + "Gesamtbilanz_Euro": 0.4923980569215065, + "Gesamteinnahmen_Euro": 1.0754450157888547, + "Gesamtkosten_Euro": 1.5678430727103612, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -324,6 +386,14 @@ 0.07557452231671152, 0.0, 4.55656845588237e-17, + 0.001414013162203277, + 0.005881449073870462, + 0.05258762370598476, + 0.0, + 0.0, + 0.0, + 0.26650619799999997, + 0.19588158, 0.0, 0.0, 0.0, @@ -333,16 +403,8 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.25364873699864443, 0.1306329312971816, - 0.07362195915902499, + 0.0, 0.060401289430882174, 0.009619897970888898, 0.07029121023060134, @@ -350,12 +412,12 @@ 2.3325608707865465e-05, 0.0021886029750169123, 0.013012984677295973, - 0.08357424731947552, - 0.0, - 0.19011028252189552, 0.0, 0.0, - 0.0 + 0.0, + 0.0, + 0.214398479, + 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ 439.1848126015972, @@ -364,6 +426,14 @@ 402.20607938643707, 0.0, 2.2737367544323206e-13, + 6.433180901743754, + 25.909467285772962, + 175.4675465665157, + 0.0, + 0.0, + 0.0, + 912.38, + 704.61, 0.0, 0.0, 0.0, @@ -373,16 +443,8 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 833.8222781020527, 537.58407941227, - 322.9033296448464, + 0.0, 273.0618871197205, 45.96224544141853, 374.088399311343, @@ -390,12 +452,12 @@ 0.11639525303326081, 9.957247384062384, 57.32592368852852, - 278.85968408233407, - 0.0, - 617.0408390843736, 0.0, 0.0, - 0.0 + 0.0, + 0.0, + 733.99, + 592.97 ], "Netzeinspeisung_Wh_pro_Stunde": [ 0.0, @@ -404,9 +466,9 @@ 0.0, 322.60070723931767, 4342.25, - 2751.785870853006, - 1906.6319940345184, - 762.832388293637, + 2760.093180901744, + 1940.089467285773, + 989.4175465665157, 0.0, 0.0, 0.0, @@ -427,10 +489,10 @@ 0.0, 0.0, 0.0, - 347.87452087222937, + 942.2242009511288, 2176.6272473840627, 1473.7559236885286, - 776.5396840823341, + 416.4419515379994, 0.0, 0.0, 0.0, @@ -444,14 +506,14 @@ 51.82392952637247, 543.9059151312817, 0.0, - 1.8741291469953731, - 7.548005965483245, - 51.11761170636123, + 0.0, + 0.0, + 0.0, 108.98319763338179, 106.80230977350088, 133.7321802766326, - 124.41545454545451, - 96.08318181818186, + 0.0, + 0.0, 70.41409090909087, 118.37045454545455, 94.68272727272722, @@ -460,22 +522,22 @@ 66.66681818181814, 69.12409090909085, 109.07302361034766, - 3.2918733722463145, + 116.99491129525349, 22.312089529472388, - 54.18759955738153, + 98.21987178167876, 86.6234264543665, 165.15986945297027, 65.92300791736113, 538.2984000000001, - 400.1930249256966, - 0.0, + 328.87106331622863, 0.0, 0.0, + 81.2380484620021, 111.78035844493081, - 6.04210069012484, + 90.18403329253945, 134.59227272727276, - 100.08954545454549, - 80.85954545454547 + 0.0, + 0.0 ], "akku_soc_pro_stunde": [ 80.0, @@ -491,31 +553,31 @@ 98.93741213017658, 95.76274429012544, 91.54148603150185, - 87.61423052185997, - 84.5813018028517, - 82.35863479183242, - 78.62219367612994, - 75.6334712243393, - 73.01299085243846, - 70.61840579734205, - 68.5140239102897, - 66.33207659624011, - 62.88918635529183, - 62.98062728229866, - 63.600407547006235, - 65.10561864582239, - 67.51182493622146, - 72.09959908769285, - 73.93079375206398, - 88.88352708539732, + 91.54148603150185, + 91.54148603150185, + 89.31881902048255, + 85.58237790478007, + 82.59365545298944, + 79.97317508108861, + 77.57859002599218, + 75.47420813893983, + 73.29226082489025, + 69.84937058394196, + 66.35170046539932, + 66.97148073010689, + 67.08678424608539, + 69.49299053648446, + 74.08076468795585, + 75.91195935232699, + 90.86469268566032, 100.0, 100.0, 100.0, 100.0, 98.60068046043587, - 98.76851659071711, - 94.52002313341684, - 91.360630915786 + 96.11252124341868, + 91.86402778611841, + 91.86402778611841 ], "Electricity_price": [ 0.000228, @@ -709,12 +771,15 @@ "initial_soc_percentage": 54 }, "start_solution": [ - 2.0, - 2.0, - 1.0, - 1.0, 0.0, - 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -728,10 +793,8 @@ 1.0, 1.0, 1.0, - 1.0, - 1.0, - 1.0, - 1.0, + 0.0, + 0.0, 1.0, 1.0, 1.0, @@ -742,9 +805,7 @@ 1.0, 1.0, 0.0, - 0.0, - 0.0, - 0.0, + 1.0, 0.0, 0.0, 0.0, @@ -753,11 +814,13 @@ 0.0, 0.0, 1.0, + 1.0, + 1.0, + 1.0, 0.0, - 1.0, - 1.0, - 1.0 + 0.0 ], "washingstart": null, - "appliance_starts": {} + "appliance_starts": {}, + "appliance_deadline_missed": {} } \ No newline at end of file diff --git a/tests/testdata/optimize_result_1_be.json b/tests/testdata/optimize_result_1_be.json index c0384ba8..a1537eca 100644 --- a/tests/testdata/optimize_result_1_be.json +++ b/tests/testdata/optimize_result_1_be.json @@ -150,6 +150,68 @@ 0 ], "battery_grid_export_allowed": [], + "terminal_value": { + "mode": "AUTO", + "battery_energy_wh": 17857.05093527103, + "credited_euro": 2.9274511210000003, + "curve": { + "energy_wh": [ + 0.0, + 516.37, + 1005.26, + 1873.31, + 2673.16, + 3180.0699999999997, + 3736.3799999999997, + 4430.719999999999, + 5039.509999999999, + 5297.15, + 5863.84, + 6670.2300000000005, + 7657.240000000001, + 8391.230000000001, + 8984.2, + 9335.85 + ], + "value_euro": [ + 0.0, + 0.174739608, + 0.337735534, + 0.625754524, + 0.889864994, + 1.056638384, + 1.239608743, + 1.4676299990000001, + 1.667495756, + 1.749811736, + 1.924408925, + 2.169712763, + 2.462756032, + 2.6771545110000003, + 2.8420001710000005, + 2.9274511210000003 + ], + "marginal_euro_per_kwh": [ + 0.3384, + 0.33340000000000003, + 0.3318, + 0.3302, + 0.32899999999999996, + 0.3289, + 0.3284, + 0.32830000000000004, + 0.3195, + 0.3081, + 0.3042, + 0.2969, + 0.2921, + 0.27799999999999997, + 0.243 + ], + "window_slots": 24 + } + }, + "battery_grid_export_factor": [], "eautocharge_hours_float": null, "result": { "Last_Wh_pro_Stunde": [ @@ -759,5 +821,6 @@ 0.0 ], "washingstart": null, - "appliance_starts": {} + "appliance_starts": {}, + "appliance_deadline_missed": {} } \ No newline at end of file diff --git a/tests/testdata/optimize_result_2.json b/tests/testdata/optimize_result_2.json index 392db83d..463b90a1 100644 --- a/tests/testdata/optimize_result_2.json +++ b/tests/testdata/optimize_result_2.json @@ -11,8 +11,6 @@ 0.0, 0.0, 0.0, - 1.0, - 1.0, 0.0, 0.0, 0.0, @@ -23,7 +21,9 @@ 0.0, 0.0, 0.0, - 1.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -113,15 +113,14 @@ 0, 0, 0, - 1, 0, - 1, + 0, + 0, 0, 0, 0, 1, - 0, - 0, + 1, 1, 0, 0, @@ -133,16 +132,17 @@ 1, 1, 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, 1, 0, - 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, 1, 1, 1, @@ -150,6 +150,68 @@ 0 ], "battery_grid_export_allowed": [], + "terminal_value": { + "mode": "AUTO", + "battery_energy_wh": 10043.348787942483, + "credited_euro": 2.9274511210000003, + "curve": { + "energy_wh": [ + 0.0, + 516.37, + 1005.26, + 1873.31, + 2673.16, + 3180.0699999999997, + 3736.3799999999997, + 4430.719999999999, + 5039.509999999999, + 5297.15, + 5863.84, + 6670.2300000000005, + 7657.240000000001, + 8391.230000000001, + 8984.2, + 9335.85 + ], + "value_euro": [ + 0.0, + 0.174739608, + 0.337735534, + 0.625754524, + 0.889864994, + 1.056638384, + 1.239608743, + 1.4676299990000001, + 1.667495756, + 1.749811736, + 1.924408925, + 2.169712763, + 2.462756032, + 2.6771545110000003, + 2.8420001710000005, + 2.9274511210000003 + ], + "marginal_euro_per_kwh": [ + 0.3384, + 0.33340000000000003, + 0.3318, + 0.3302, + 0.32899999999999996, + 0.3289, + 0.3284, + 0.32830000000000004, + 0.3195, + 0.3081, + 0.3042, + 0.2969, + 0.2921, + 0.27799999999999997, + 0.243 + ], + "window_slots": 24 + } + }, + "battery_grid_export_factor": [], "eautocharge_hours_float": [ 0.0, 0.0, @@ -162,123 +224,123 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.625, - 0.0, - 0.75, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.75, - 0.0, - 0.0, - 0.625, - 0.0, - 0.0, - 0.0, - 0.5, 0.875, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.75, + 0.0, + 0.0, 0.375, + 0.875, + 0.625, + 0.5, + 0.375, + 0.875, 0.1, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0 ], "result": { "Last_Wh_pro_Stunde": [ 1053.07, - 5677.336751616955, + 10723.91, 1320.56, - 8032.03, + 1132.03, 1163.67, - 9456.82, + 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, - 3550.98, - 3488.56, + 1050.98, + 988.56, 912.38, - 2704.61, + 704.61, 516.37, 868.05, 694.34, 608.79, 556.31, - 8768.89, + 488.89, 506.91, 804.89, - 8041.98, + 1141.98, 1056.97, 992.46, 1155.99, - 6347.01, - 10917.98, - 9512.67, + 11607.01, + 3757.98, + 1232.67, 5011.26, - 1964.88, - 1158.03, - 1222.72, - 1221.04, - 949.99, - 987.01, + 10520.88, + 8058.03, + 6742.72, + 5361.04, + 10609.99, + 2091.01, 733.99, 592.97 ], "EAuto_SoC_pro_Stunde": [ 5.0, 5.0, - 5.0, - 5.0, - 15.925, - 15.925, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 29.035, - 42.144999999999996, - 42.144999999999996, - 42.144999999999996, - 53.06999999999999, - 53.06999999999999, - 53.06999999999999, - 53.06999999999999, - 61.809999999999995, - 77.105, - 90.215, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 20.294999999999998, + 33.405, + 33.405, + 33.405, + 39.96, + 55.254999999999995, + 66.18, + 74.92, + 81.475, 96.77, 98.518, - 98.518, - 98.518, - 98.518, - 98.518, - 98.518, 98.518 ], "Einnahmen_Euro_pro_Stunde": [ @@ -287,10 +349,10 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 0.29947942211993916, + 0.19320652266312205, + 0.1358062627100041, + 0.0692592282596561, 0.0, 0.0, 0.0, @@ -321,11 +383,27 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 10997.311193085054, - "Gesamtbilanz_Euro": 9.400002543796743, - "Gesamteinnahmen_Euro": 0.0, - "Gesamtkosten_Euro": 9.400002543796743, + "Gesamt_Verluste": 7016.419901387913, + "Gesamtbilanz_Euro": 11.897504510077836, + "Gesamteinnahmen_Euro": 0.6977514357527215, + "Gesamtkosten_Euro": 12.595255945830557, "Home_appliance_wh_per_hour": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -347,26 +425,26 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0 ], "home_appliance_energy_wh": { "dishwasher1": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -388,101 +466,85 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0 ] }, "Kosten_Euro_pro_Stunde": [ 0.10013413727316416, - 1.110569992, + 2.141361992, 0.11436917344552285, - 0.4642292905233547, - 0.0, + 0.07557452231671152, 0.0, + 4.55656845588237e-17, 0.001414013162203277, 0.005881449073870462, 0.05258762370598476, 0.0, - 0.995566611, - 1.033413892, 0.0, - 0.7518815799999999, + 0.0, + 0.26650619799999997, + 0.19588158, 0.174739608, - 0.28801899, 0.0, 0.0, 0.0, - 1.4565879259999999, 0.0, 0.0, - 1.0058038379999998, + 0.0, + 0.0, + 0.0, 0.1306329312971816, 0.07362195915902499, + 0.060401289430882174, + 1.9781208038728355, + 0.4410873661898387, + 4.179128154646605e-17, + 0.19837609024735353, + 1.6519184206056259, + 1.2504841688205104, + 1.5077152912298486, 0.0, - 0.0, - 0.9554559982283223, - 0.1443477211581475, - 0.14848868609267263, - 0.0, - 0.013012984677295973, - 0.0, - 0.0, - 0.0, + 1.7952031890000002, 0.0, 0.214398479, 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ 439.1848126015972, - 5020.66, + 9680.66, 546.436566868241, - 2470.61889581349, - 0.0, + 402.20607938643707, 0.0, + 2.2737367544323206e-13, 6.433180901743754, 25.909467285772962, 175.4675465665157, 0.0, - 3231.31, - 3480.68, 0.0, - 2704.61, + 0.0, + 912.38, + 704.61, 516.37, - 868.05, 0.0, 0.0, 0.0, - 4368.889999999999, 0.0, 0.0, - 3306.3899999999994, + 0.0, + 0.0, + 0.0, 537.58407941227, 322.9033296448464, + 273.0618871197205, + 9451.12663102167, + 2347.45804252176, + 2.2737367544323206e-13, + 989.9006499368938, + 7515.552414038334, + 5508.740831808416, + 5030.748385818647, 0.0, - 0.0, - 5084.917499884632, - 785.3521281727285, - 740.9615074484663, - 0.0, - 57.32592368852852, - 0.0, - 0.0, - 0.0, + 5826.6900000000005, 0.0, 733.99, 592.97 @@ -493,10 +555,10 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 4278.277458856274, + 2760.093180901744, + 1940.089467285773, + 989.4175465665157, 0.0, 0.0, 0.0, @@ -529,83 +591,83 @@ ], "Verluste_Pro_Stunde": [ 37.96737751219166, - 599.9999999999998, + 483.0, 39.91398802418894, - 945.0334674976189, + 51.82392952637247, 582.6179999999995, - 1026.1905162926748, - 331.2111817082091, - 232.81073607429266, - 118.73010558798194, + 7.676704937247038, + 0.0, + 0.0, + 0.0, 108.98319763338179, + 106.80230977350088, + 133.7321802766326, 0.0, 0.0, - 124.41545454545451, - 240.0, - 0.0, 0.0, + 118.37045454545455, 94.68272727272722, 83.01681818181817, 75.86045454545456, - 1014.0, + 66.66681818181814, 69.12409090909085, 109.07302361034766, - 945.0, + 116.99491129525349, 22.312089529472388, 54.18759955738153, - 123.8591383343284, - 853.5941151729826, - 1083.022499986156, - 906.7055359846831, - 304.8583246953946, - 225.52229247529465, - 176.85071084262336, - 131.21108264656203, - 111.78035844493081, - 90.18403329253945, - 134.59227272727276, + 86.6234264543665, + 414.1795957226005, + 2.727365102611202, + 538.2984000000001, + 270.9120779924272, + 485.6666896846002, + 348.0204998170099, + 277.01140629823766, + 806.8360377309995, + 1083.0, + 340.3377272727273, 0.0, 0.0 ], "akku_soc_pro_stunde": [ 80.0, 81.05464937533866, - 97.72131604200533, - 98.83003793156611, - 79.89157364488383, - 96.07540697821715, - 78.84078381044188, - 88.04109441344768, - 94.50805930440028, - 97.80611779295532, - 96.7435299231319, - 96.7435299231319, - 96.7435299231319, - 92.81627441349004, - 99.48294108015669, - 99.48294108015669, - 99.48294108015669, - 96.49421862836606, - 93.87373825646522, - 91.4791532013688, - 72.53975926197485, - 70.35781194792527, - 66.91492170697698, - 47.975527767583046, - 48.59530803229061, - 50.10051913110676, - 51.331355728325214, - 33.27368862539725, - 14.334919685618749, - 1.0715355651189202, - 1.7753354997896456, - 5.155440452534742, - 10.06796019816317, - 11.456100036623162, - 10.05678049705903, - 7.568621280041836, - 3.32012782274156, - 3.32012782274156 + 81.05464937533866, + 82.16337126489945, + 83.60292486285425, + 99.78675819618759, + 100.0, + 100.0, + 100.0, + 100.0, + 98.93741213017658, + 95.76274429012544, + 91.54148603150185, + 91.54148603150185, + 91.54148603150185, + 91.54148603150185, + 87.80504491579936, + 84.81632246400872, + 82.1958420921079, + 79.80125703701147, + 77.69687514995913, + 75.51492783590953, + 72.07203759496124, + 68.5743674764186, + 69.19414774112617, + 70.69935883994233, + 73.1055651303414, + 73.11055390041365, + 73.18631404215284, + 88.13904737548619, + 89.91438287527582, + 89.98845758873692, + 90.07236036143165, + 90.10045498082714, + 71.17062150457336, + 52.23122756517942, + 43.23066799217666, + 43.23066799217666 ], "Electricity_price": [ 0.000228, @@ -703,42 +765,42 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.625, - 0.0, - 0.75, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.75, - 0.0, - 0.0, - 0.625, - 0.0, - 0.0, - 0.0, - 0.5, 0.875, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.75, + 0.0, + 0.0, 0.375, + 0.875, + 0.625, + 0.5, + 0.375, + 0.875, 0.1, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0 ], "discharge_array": [ @@ -799,51 +861,6 @@ "initial_soc_percentage": 5 }, "start_solution": [ - 2.0, - 2.0, - 1.0, - 1.0, - 1.0, - 0.0, - 1.0, - 2.0, - 2.0, - 0.0, - 0.0, - 2.0, - 2.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 2.0, - 0.0, - 0.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 0.0, - 0.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0, - 1.0, 1.0, 0.0, 0.0, @@ -860,9 +877,36 @@ 0.0, 0.0, 0.0, - 3.0, 0.0, - 4.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, 0.0, 0.0, 0.0, @@ -876,31 +920,50 @@ 0.0, 0.0, 0.0, - 4.0, - 0.0, - 0.0, - 3.0, - 0.0, - 0.0, - 0.0, - 2.0, 5.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 4.0, + 0.0, + 0.0, 1.0, - 2.0, - 6.0, - 5.0, - 6.0, - 4.0, 5.0, 3.0, + 2.0, 1.0, - 10.0 + 5.0, + 2.0, + 3.0, + 4.0, + 26.0 ], - "washingstart": 20, + "washingstart": 36, "appliance_starts": { "dishwasher1": [ - "2025-01-15 20:00:00+01:00" + "2025-01-16 12:00:00+01:00" ] - } + }, + "appliance_deadline_missed": {} } \ No newline at end of file diff --git a/tests/testdata/optimize_result_2_be.json b/tests/testdata/optimize_result_2_be.json index 91076309..c386f09b 100644 --- a/tests/testdata/optimize_result_2_be.json +++ b/tests/testdata/optimize_result_2_be.json @@ -113,17 +113,13 @@ 0, 0, 0, - 0, - 0, - 0, - 0, - 1, - 0, 1, 1, - 0, - 0, - 0, + 1, + 1, + 1, + 1, + 1, 1, 1, 1, @@ -134,7 +130,8 @@ 1, 1, 0, - 0, + 1, + 1, 0, 0, 0, @@ -147,29 +144,75 @@ 0, 0, 1, - 0 + 0, + 0, + 0, + 1 ], "battery_grid_export_allowed": [], + "terminal_value": { + "mode": "AUTO", + "battery_energy_wh": 11418.875532127204, + "credited_euro": 2.9274511210000003, + "curve": { + "energy_wh": [ + 0.0, + 516.37, + 1005.26, + 1873.31, + 2673.16, + 3180.0699999999997, + 3736.3799999999997, + 4430.719999999999, + 5039.509999999999, + 5297.15, + 5863.84, + 6670.2300000000005, + 7657.240000000001, + 8391.230000000001, + 8984.2, + 9335.85 + ], + "value_euro": [ + 0.0, + 0.174739608, + 0.337735534, + 0.625754524, + 0.889864994, + 1.056638384, + 1.239608743, + 1.4676299990000001, + 1.667495756, + 1.749811736, + 1.924408925, + 2.169712763, + 2.462756032, + 2.6771545110000003, + 2.8420001710000005, + 2.9274511210000003 + ], + "marginal_euro_per_kwh": [ + 0.3384, + 0.33340000000000003, + 0.3318, + 0.3302, + 0.32899999999999996, + 0.3289, + 0.3284, + 0.32830000000000004, + 0.3195, + 0.3081, + 0.3042, + 0.2969, + 0.2921, + 0.27799999999999997, + 0.243 + ], + "window_slots": 24 + } + }, + "battery_grid_export_factor": [], "eautocharge_hours_float": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, 0.0, 0.0, @@ -181,17 +224,36 @@ 0.0, 0.0, 0.625, - 0.5, - 0.75, 0.375, + 1.0, + 0.75, + 1.0, + 0.875, 0.0, 0.75, - 0.5, - 0.5, 0.0, - 0.5, - 0.5, - 0.4, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -202,14 +264,14 @@ ], "result": { "Last_Wh_pro_Stunde": [ - 1053.07, - 1063.91, - 1320.56, - 1132.03, - 1163.67, - 1176.82, + 7953.07, + 5203.91, + 12360.56, + 9412.03, + 12203.67, + 10836.82, 1216.22, - 1103.78, + 9383.78, 1129.12, 1178.71, 1050.98, @@ -221,19 +283,19 @@ 694.34, 608.79, 556.31, - 7388.89, - 6026.91, - 9084.89, - 5281.98, + 488.89, + 506.91, + 3304.89, + 3641.98, 1056.97, - 9272.46, - 6675.99, - 6347.01, + 992.46, + 1155.99, + 827.01, 1257.98, - 6752.67, - 6391.26, - 7776.88, - 3658.0299999999997, + 1232.67, + 871.26, + 860.88, + 1158.03, 1222.72, 1221.04, 949.99, @@ -242,55 +304,55 @@ 592.97 ], "EAuto_SoC_pro_Stunde": [ - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, - 5.0, 5.0, 15.925, - 24.665, - 37.775, - 44.330000000000005, - 44.330000000000005, - 57.440000000000005, - 66.18, - 74.92, - 74.92, - 83.66, - 92.4, - 99.392, - 99.392, - 99.392, - 99.392, - 99.392, - 99.392, - 99.392 + 22.48, + 39.96, + 53.06999999999999, + 70.55, + 85.845, + 85.845, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955, + 98.955 ], "Einnahmen_Euro_pro_Stunde": [ 0.0, 0.0, 0.0, 0.0, - 0.022582049506752234, - 0.3039575, - 0.19320652266312205, - 0.13346423958241627, - 0.0692592282596561, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -321,10 +383,10 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 7236.761593008068, - "Gesamtbilanz_Euro": 10.765839318494601, - "Gesamteinnahmen_Euro": 0.7224695400119466, - "Gesamtkosten_Euro": 11.488308858506548, + "Gesamt_Verluste": 8640.018140681446, + "Gesamtbilanz_Euro": 11.094627860497381, + "Gesamteinnahmen_Euro": 0.0, + "Gesamtkosten_Euro": 11.094627860497381, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -347,15 +409,6 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 2500.0, 2500.0, 0.0, @@ -363,6 +416,15 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0 ], "home_appliance_energy_wh": { @@ -388,15 +450,6 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 2500.0, 2500.0, 0.0, @@ -404,99 +457,108 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0 ] }, "Kosten_Euro_pro_Stunde": [ - 0.10013413727316416, - 0.09007999454232955, - 0.11436917344552285, - 0.07557452231671152, + 1.6012075076051746, + 0.9205989357975131, + 2.3554245259999997, + 0.7234986866944951, + 0.405034546, + 0.22805572907568722, 0.0, - 4.55656845588237e-17, - 0.001414013162203277, - 0.0, - 0.05258762370598476, - 0.0, - 0.0, - 0.29116746986009023, - 0.26650619799999997, - 0.19588158, + 0.453046634437127, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.9964959260000001, - 0.5352533899999999, - 1.5452864699999995, - 0.16621183799999983, + 0.0, + 0.0, + 0.23080181969641217, + 0.22802125600000003, + 0.199865757, + 0.182970359, + 0.162995926, + 0.16677339, + 1.08961047, + 1.005803838, 0.1306329312971816, - 1.8585251365618425, - 1.1230913228365589, - 0.8820174288094884, + 0.07362195915902499, + 0.060401289430882174, + 0.009619897970888898, 0.07029121023060134, - 0.3006368911258292, - 0.437965956045862, - 1.0551552313933648, - 0.2896168262092554, + 4.179128154646605e-17, + 2.3325608707865465e-05, + 0.0021886029750169123, + 0.013012984677295973, 0.08357424731947552, - 0.17784012884918773, + 0.0, 0.19011028252189552, 0.293043269, - 0.0, - 0.16484566 + 0.214398479, + 0.0 ], "Netzbezug_Wh_pro_Stunde": [ - 439.1848126015972, - 407.23324838304495, - 546.436566868241, - 402.20607938643707, + 7022.839945636731, + 4161.83967358731, + 11253.82, + 3850.4453788956635, + 2203.67, + 1138.0026400982397, 0.0, - 2.2737367544323206e-13, - 6.433180901743754, - 0.0, - 175.4675465665157, - 0.0, - 0.0, - 980.6920507244535, - 912.38, - 704.61, + 1995.8001517054054, 0.0, 0.0, 0.0, 0.0, 0.0, - 2988.8900000000003, - 1626.9099999999999, - 4679.8499999999985, - 546.3899999999994, + 0.0, + 0.0, + 695.6052432079933, + 694.34, + 608.79, + 556.31, + 488.89, + 506.91, + 3299.85, + 3306.3900000000003, 537.58407941227, - 8151.426037551941, - 5077.266378103792, - 4214.130094646385, + 322.9033296448464, + 273.0618871197205, + 45.96224544141853, 374.088399311343, - 1635.6740540034234, - 2185.4588625043016, - 4800.5242556568, - 1275.8450493799796, + 2.2737367544323206e-13, + 0.11639525303326081, + 9.957247384062384, + 57.32592368852852, 278.85968408233407, - 556.6201215937018, + 0.0, 617.0408390843736, 987.01, - 0.0, - 592.97 + 733.99, + 0.0 ], "Netzeinspeisung_Wh_pro_Stunde": [ 0.0, 0.0, 0.0, 0.0, - 322.60070723931767, - 4342.25, - 2760.093180901744, - 1906.6319940345184, - 989.4175465665157, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -528,84 +590,84 @@ 0.0 ], "Verluste_Pro_Stunde": [ - 37.96737751219166, - 46.38878980596536, - 39.91398802418894, - 51.82392952637247, - 543.9059151312817, - 0.0, - 0.0, - 7.548005965483245, - 0.0, + 345.00599347640775, + 207.1415608304772, + 552.0, + 1014.0126454674794, + 1151.7833573253401, + 1109.4303168117888, + 332.088433649356, + 1017.5976182046486, + 142.6574983015977, 108.98319763338179, 106.80230977350088, - 0.0014460869344160802, - 0.0, - 0.0, + 133.7321802766326, + 124.41545454545451, + 96.08318181818186, 70.41409090909087, - 118.37045454545455, - 94.68272727272722, - 83.01681818181817, - 75.86045454545456, - 945.0, - 876.0, - 1014.0, - 807.0, + 23.515194108000884, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0658141036401502e-16, + 6.8212102632969615e-15, 22.312089529472388, - 414.01032450623296, - 276.72796537245506, - 278.9400113575662, + 54.18759955738153, + 86.6234264543665, + 165.15986945297027, 65.92300791736113, - 348.1792864804107, - 317.77906350051614, - 226.9433106788162, - 23.073005925597585, + 538.2984000000001, + 441.9379674303641, + 261.1952696860876, + 176.85071084262336, 93.18476208988011, - 35.877614591244196, + 111.78035844493081, 6.04210069012484, 0.0, - 100.08954545454549, - 0.0 + 0.0, + 80.85954545454547 ], "akku_soc_pro_stunde": [ 80.0, - 81.05464937533866, - 82.3432268699488, - 83.45194875950959, - 84.8915023574644, - 100.0, - 100.0, - 100.0, - 100.0, - 100.0, - 98.93741213017658, - 95.76274429012544, - 95.76278445920696, - 95.76278445920696, - 95.76278445920696, - 93.54011744818767, - 89.80367633248518, - 86.81495388069455, - 84.19447350879372, - 81.79988845369729, - 62.860494514303355, - 43.92110057490942, - 24.981706635515476, - 6.042312696121536, - 6.662092960829104, - 6.662379752668908, - 6.682601013014882, - 6.764267995169498, - 8.595462659540642, - 10.600442839552054, - 11.760972381233058, - 11.931619900089059, - 12.57253673135566, - 15.161002344963439, - 16.157602750275778, - 16.325438880557027, - 16.325438880557027, - 13.16604666292617 + 80.00016648545576, + 80.0040987307468, + 80.0040987307468, + 61.0650560543384, + 43.71292212507574, + 25.507703652675932, + 34.68032322218595, + 15.840863121810026, + 18.38363774945549, + 17.321049879632074, + 14.146382039580937, + 9.925123780957328, + 5.997868271315456, + 2.964939552307191, + 0.7422725412879071, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.960594732333751e-18, + 1.9243865760169378e-16, + 0.6197802647075668, + 2.124991363523721, + 4.5311976539227885, + 9.118971805394184, + 10.950166469765326, + 25.902899803098663, + 38.1789544539421, + 45.434378611888974, + 50.34689835751741, + 52.93536397112519, + 51.53604443156106, + 51.7038805618423, + 51.7038805618423, + 51.7038805618423 ], "Electricity_price": [ 0.000228, @@ -692,25 +754,6 @@ "device_id": "ev1", "hours": 48, "charge_array": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, 0.0, 0.0, @@ -722,17 +765,36 @@ 0.0, 0.0, 0.625, - 0.5, - 0.75, 0.375, + 1.0, + 0.75, + 1.0, + 0.875, 0.0, 0.75, - 0.5, - 0.5, 0.0, - 0.5, - 0.5, - 0.4, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -795,7 +857,7 @@ "capacity_wh": 60000, "charging_efficiency": 0.95, "max_charge_power_w": 11040, - "soc_wh": 59635.2, + "soc_wh": 59373.0, "initial_soc_percentage": 5 }, "start_solution": [ @@ -803,11 +865,6 @@ 0.0, 0.0, 0.0, - 2.0, - 0.0, - 0.0, - 0.0, - 1.0, 0.0, 0.0, 0.0, @@ -816,12 +873,6 @@ 0.0, 0.0, 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 0.0, 0.0, 1.0, 1.0, @@ -832,8 +883,16 @@ 1.0, 1.0, 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, 0.0, - 0.0, + 1.0, + 1.0, 0.0, 0.0, 0.0, @@ -849,58 +908,62 @@ 0.0, 0.0, 0.0, + 1.0, + 1.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 3.0, 0.0, 0.0, 3.0, - 2.0, + 3.0, + 3.0, + 6.0, 4.0, + 3.0, 1.0, + 6.0, + 4.0, + 6.0, + 5.0, 0.0, 4.0, - 2.0, - 2.0, - 0.0, - 2.0, - 2.0, - 4.0, - 0.0, + 3.0, 3.0, 2.0, - 0.0, - 5.0, + 1.0, 2.0, 5.0, - 30.0 + 4.0, + 2.0, + 4.0, + 5.0, + 5.0, + 0.0, + 3.0, + 3.0, + 5.0, + 6.0, + 6.0, + 4.0, + 1.0, + 1.0, + 1.0, + 4.0, + 1.0, + 6.0, + 1.0, + 2.0, + 5.0, + 0.0, + 0.0, + 0.0, + 21.0 ], - "washingstart": 40, + "washingstart": 31, "appliance_starts": { "dishwasher1": [ - "2025-01-16 16:00:00+01:00" + "2025-01-16 07:00:00+01:00" ] - } + }, + "appliance_deadline_missed": {} } \ No newline at end of file