From 8d72177671689a59452aae89d7cdf9507e904e48 Mon Sep 17 00:00:00 2001 From: 7tobias Date: Tue, 21 Jul 2026 04:22:50 -0400 Subject: [PATCH] feat: rename remaining German API fields and deprecate legacy endpoints (#1164) Complete the English API translation started in #675: - rename input fields pv_akku to pv_battery and eauto to ev, and the solution fields eautocharge_hours_float to ev_charge_hours_float and eauto_obj to ev_obj; the German names are still accepted on input via validation aliases and re-emitted in responses as deprecated computed fields, same pattern as #675 - mark the legacy endpoints /strompreis, /gesamtlast and /gesamtlast_simple as deprecated in the OpenAPI schema - use amount instead of a euro reference in the battery LCOS log message Co-authored-by: Tobias Welz --- CONTRIBUTING.md | 2 + docs/_generated/openapi.md | 2 +- openapi.json | 70 +++++++++--- src/akkudoktoreos/devices/genetic/inverter.py | 4 +- .../optimization/genetic/genetic.py | 106 +++++++++--------- .../optimization/genetic/geneticparams.py | 29 ++++- .../optimization/genetic/geneticsolution.py | 47 +++++--- src/akkudoktoreos/server/eos.py | 6 +- tests/single_test_optimization.py | 8 +- tests/test_translations.py | 42 +++++++ 10 files changed, 215 insertions(+), 101 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb96f998..28135cf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,8 @@ The server can be started with `make run`. A full overview of the main shortcuts ### Code Style +All code, comments, docstrings, identifiers and API field names are written in English. + Our code style checks use [`pre-commit`](https://pre-commit.com). To run formatting automatically before every commit: diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index affca63c..1c3e8855 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,6 +1,6 @@ # Akkudoktor-EOS -**Version**: `v0.3.0.dev2607201457168588` +**Version**: `v0.3.0.dev2607210205856856` **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/openapi.json b/openapi.json index 5bc83eb9..b6965e65 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.dev2607201457168588" + "version": "v0.3.0.dev2607210205856856" }, "paths": { "/v1/admin/cache/clear": { @@ -1958,7 +1958,8 @@ } } } - } + }, + "deprecated": true } }, "/gesamtlast": { @@ -2004,7 +2005,8 @@ } } } - } + }, + "deprecated": true } }, "/gesamtlast_simple": { @@ -2015,6 +2017,7 @@ "summary": "Fastapi Gesamtlast Simple", "description": "Deprecated: Total Load Prediction.\n\nEndpoint to handle total load prediction.\n\nTotal load prediction starts at 00.00.00 today and is provided for 48 hours.\nIf no prediction values are available the missing ones at the start of the series are\nfilled with the first available prediction value.\n\nArgs:\n year_energy (float): Yearly energy consumption in Wh.\n\nNote:\n Set LoadAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=loadforecast_power_w' instead.", "operationId": "fastapi_gesamtlast_simple_gesamtlast_simple_get", + "deprecated": true, "parameters": [ { "name": "year_energy", @@ -4934,7 +4937,7 @@ "ems": { "$ref": "#/components/schemas/GeneticEnergyManagementParameters" }, - "pv_akku": { + "pv_battery": { "anyOf": [ { "$ref": "#/components/schemas/SolarPanelBatteryParameters" @@ -4942,7 +4945,8 @@ { "type": "null" } - ] + ], + "description": "PV battery parameters." }, "inverter": { "anyOf": [ @@ -4954,7 +4958,7 @@ } ] }, - "eauto": { + "ev": { "anyOf": [ { "$ref": "#/components/schemas/ElectricVehicleParameters" @@ -4962,7 +4966,8 @@ { "type": "null" } - ] + ], + "description": "Electric vehicle parameters." }, "dishwasher": { "anyOf": [ @@ -5012,13 +5017,12 @@ "description": "Can be `null` or contain a previous solution (if available)." } }, - "additionalProperties": false, "type": "object", "required": [ "ems", - "pv_akku", + "pv_battery", "inverter", - "eauto" + "ev" ], "title": "GeneticOptimizationParameters", "description": "Main parameter class for running the genetic energy optimization.\n\nCollects all model and configuration parameters necessary to run the\noptimization process, such as forecasts, pricing, battery and appliance models." @@ -5328,7 +5332,7 @@ "title": "Discharge Allowed", "description": "Array with discharge values (1 for discharge, 0 otherwise)." }, - "eautocharge_hours_float": { + "ev_charge_hours_float": { "anyOf": [ { "items": { @@ -5340,13 +5344,13 @@ "type": "null" } ], - "title": "Eautocharge Hours Float", - "description": "TBD" + "title": "Ev Charge Hours Float", + "description": "Array with EV charging values as relative power (0.0-1.0), or `null` if no EV is optimized." }, "result": { "$ref": "#/components/schemas/GeneticSimulationResult" }, - "eauto_obj": { + "ev_obj": { "anyOf": [ { "$ref": "#/components/schemas/ElectricVehicleResult" @@ -5354,7 +5358,8 @@ { "type": "null" } - ] + ], + "description": "Electric vehicle state after optimization." }, "start_solution": { "anyOf": [ @@ -5382,16 +5387,47 @@ ], "title": "Washingstart", "description": "Can be `null` or contain an object representing the start of washing (if applicable)." + }, + "eautocharge_hours_float": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Eautocharge Hours Float", + "description": "Deprecated: Use ev_charge_hours_float instead.", + "deprecated": true, + "readOnly": true + }, + "eauto_obj": { + "anyOf": [ + { + "$ref": "#/components/schemas/ElectricVehicleResult" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use ev_obj instead.", + "deprecated": true, + "readOnly": true } }, - "additionalProperties": false, "type": "object", "required": [ "ac_charge", "dc_charge", "discharge_allowed", - "eautocharge_hours_float", + "ev_charge_hours_float", "result", + "ev_obj", + "eautocharge_hours_float", "eauto_obj" ], "title": "GeneticSolution", diff --git a/src/akkudoktoreos/devices/genetic/inverter.py b/src/akkudoktoreos/devices/genetic/inverter.py index e6898963..9781d4c4 100644 --- a/src/akkudoktoreos/devices/genetic/inverter.py +++ b/src/akkudoktoreos/devices/genetic/inverter.py @@ -60,7 +60,7 @@ class Inverter: remaining_load_evq = (generation - consumption) * (1.0 - scr) if remaining_load_evq > 0: - # Akku muss den Restverbrauch decken + # The battery must cover the remaining consumption if self.battery: # Request more DC from battery to account for DC→AC conversion loss dc_request = remaining_load_evq / dc_to_ac_eff @@ -75,7 +75,7 @@ class Inverter: else: from_battery_ac = 0.0 - # Wenn der Akku den Restverbrauch nicht vollständig decken kann, wird der Rest ins Netz gezogen + # If the battery cannot fully cover the remaining consumption, the rest is drawn from the grid if remaining_load_evq > 0: grid_import += remaining_load_evq remaining_load_evq = 0 diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index 4c43f93d..3dd3f540 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -435,12 +435,10 @@ class GeneticOptimization(OptimizationBase): ): """Initialize the optimization problem with the required parameters.""" self.opti_param: dict[str, Any] = {} - self.fixed_eauto_hours = ( - self.config.prediction.hours - self.config.optimization.horizon_hours - ) + self.fixed_ev_hours = self.config.prediction.hours - self.config.optimization.horizon_hours self.ev_possible_charge_values: list[float] = [1.0] # Separate charge-level list for battery AC charging (independent of EV rates). - # Populated from parameters.pv_akku.charge_rates in optimize_ems. + # Populated from parameters.pv_battery.charge_rates in optimize_ems. self.bat_possible_charge_values: list[float] = [1.0] self.verbose = verbose self.fix_seed = fixed_seed @@ -526,9 +524,9 @@ class GeneticOptimization(OptimizationBase): self.config.prediction.hours : self.config.prediction.hours * 2 ] (ev_charge_part_mutated,) = self.toolbox.mutate_ev_charge_index(ev_charge_part) - ev_charge_part_mutated[self.config.prediction.hours - self.fixed_eauto_hours :] = [ + ev_charge_part_mutated[self.config.prediction.hours - self.fixed_ev_hours :] = [ 0 - ] * self.fixed_eauto_hours + ] * self.fixed_ev_hours individual[self.config.prediction.hours : self.config.prediction.hours * 2] = ( ev_charge_part_mutated ) @@ -563,14 +561,14 @@ class GeneticOptimization(OptimizationBase): def merge_individual( self, discharge_hours_bin: np.ndarray, - eautocharge_hours_index: Optional[np.ndarray], + ev_charge_hours_index: Optional[np.ndarray], washingstart_int: Optional[int], ) -> list[int]: """Merge the individual components back into a single solution list. Parameters: discharge_hours_bin (np.ndarray): Binary discharge hours. - eautocharge_hours_index (Optional[np.ndarray]): EV charge hours as integers, or None. + ev_charge_hours_index (Optional[np.ndarray]): EV charge hours as integers, or None. washingstart_int (Optional[int]): Dishwasher start time as integer, or None. Returns: @@ -580,8 +578,8 @@ class GeneticOptimization(OptimizationBase): individual = discharge_hours_bin.tolist() # Add EV charge hours if applicable - if self.optimize_ev and eautocharge_hours_index is not None: - individual.extend(eautocharge_hours_index.tolist()) + if self.optimize_ev and ev_charge_hours_index is not None: + individual.extend(ev_charge_hours_index.tolist()) elif self.optimize_ev: # If optimize_ev is active but no EV data is available, append zeros individual.extend([0] * self.config.prediction.hours) @@ -609,7 +607,7 @@ class GeneticOptimization(OptimizationBase): discharge_hours_bin = np.array(individual[: self.config.prediction.hours], dtype=int) # EV charge hours as a NumPy array of ints (if optimize_ev is True) - eautocharge_hours_index = ( + ev_charge_hours_index = ( # append ev charging states to individual np.array( individual[self.config.prediction.hours : self.config.prediction.hours * 2], @@ -626,7 +624,7 @@ class GeneticOptimization(OptimizationBase): else None ) - return discharge_hours_bin, eautocharge_hours_index, washingstart_int + return discharge_hours_bin, ev_charge_hours_index, washingstart_int def setup_deap_environment(self, opti_param: dict[str, Any], start_hour: int) -> None: """Set up the DEAP environment with fitness and individual creation rules.""" @@ -701,7 +699,7 @@ class GeneticOptimization(OptimizationBase): This is an internal function. """ self.simulation.reset() - discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual( + discharge_hours_bin, ev_charge_hours_index, washingstart_int = self.split_individual( individual ) @@ -721,13 +719,13 @@ class GeneticOptimization(OptimizationBase): self.simulation.dc_charge_hours = np.full(self.config.prediction.hours, 1) self.simulation.ac_charge_hours = ac_charge_hours - if eautocharge_hours_index is not None: - eautocharge_hours_float = np.array( - [self.ev_possible_charge_values[i] for i in eautocharge_hours_index], + if ev_charge_hours_index is not None: + ev_charge_hours_float = np.array( + [self.ev_possible_charge_values[i] for i in ev_charge_hours_index], float, ) # discharge is set to 0 by default - self.simulation.ev_charge_hours = eautocharge_hours_float + self.simulation.ev_charge_hours = ev_charge_hours_float else: # discharge is set to 0 by default self.simulation.ev_charge_hours = np.full(self.config.prediction.hours, 0) @@ -786,34 +784,32 @@ class GeneticOptimization(OptimizationBase): # EV 100% & charge not allowed if self.optimize_ev: - discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual( + discharge_hours_bin, ev_charge_hours_index, washingstart_int = self.split_individual( individual ) - eauto_soc_per_hour = np.array( + ev_soc_per_hour = np.array( simulation_result.get("EAuto_SoC_pro_Stunde", []) ) # Beispielkey - if eauto_soc_per_hour is None or eautocharge_hours_index is None: - raise ValueError("eauto_soc_per_hour or eautocharge_hours_index is None") - min_length = min(eauto_soc_per_hour.size, eautocharge_hours_index.size) - eauto_soc_per_hour_tail = eauto_soc_per_hour[-min_length:] - eautocharge_hours_index_tail = eautocharge_hours_index[-min_length:] + if ev_soc_per_hour is None or ev_charge_hours_index is None: + raise ValueError("ev_soc_per_hour or ev_charge_hours_index is None") + min_length = min(ev_soc_per_hour.size, ev_charge_hours_index.size) + ev_soc_per_hour_tail = ev_soc_per_hour[-min_length:] + ev_charge_hours_index_tail = ev_charge_hours_index[-min_length:] # Mask - invalid_charge_mask = (eauto_soc_per_hour_tail == 100) & ( - eautocharge_hours_index_tail > 0 - ) + invalid_charge_mask = (ev_soc_per_hour_tail == 100) & (ev_charge_hours_index_tail > 0) if np.any(invalid_charge_mask): invalid_indices = np.where(invalid_charge_mask)[0] if len(invalid_indices) > 1: - eautocharge_hours_index_tail[invalid_indices] = 0 + ev_charge_hours_index_tail[invalid_indices] = 0 - eautocharge_hours_index[-min_length:] = eautocharge_hours_index_tail.tolist() + ev_charge_hours_index[-min_length:] = ev_charge_hours_index_tail.tolist() adjusted_individual = self.merge_individual( - discharge_hours_bin, eautocharge_hours_index, washingstart_int + discharge_hours_bin, ev_charge_hours_index, washingstart_int ) individual[:] = adjusted_individual @@ -852,7 +848,7 @@ class GeneticOptimization(OptimizationBase): # # Merge the updated discharge_hours_bin back into the individual # adjusted_individual = self.merge_individual( - # discharge_hours_bin, eautocharge_hours_index, washingstart_int + # discharge_hours_bin, ev_charge_hours_index, washingstart_int # ) # individual[:] = adjusted_individual @@ -860,8 +856,8 @@ class GeneticOptimization(OptimizationBase): individual.extra_data = ( # type: ignore[attr-defined] simulation_result["Gesamtbilanz_Euro"], simulation_result["Gesamt_Verluste"], - parameters.eauto.min_soc_percentage - self.simulation.ev.current_soc_percentage() - if parameters.eauto and self.simulation.ev + parameters.ev.min_soc_percentage - self.simulation.ev.current_soc_percentage() + if parameters.ev and self.simulation.ev else 0, ) @@ -971,7 +967,7 @@ class GeneticOptimization(OptimizationBase): excess_cost_per_wh = break_even_price - best_uncovered_price total_balance += ac_wh * excess_cost_per_wh * ac_penalty_factor - if self.optimize_ev and parameters.eauto and self.simulation.ev: + if self.optimize_ev and parameters.ev and self.simulation.ev: try: penalty = self.config.optimization.genetic.penalties["ev_soc_miss"] except Exception: @@ -982,12 +978,10 @@ class GeneticOptimization(OptimizationBase): ) ev_soc_percentage = self.simulation.ev.current_soc_percentage() if ( - ev_soc_percentage < parameters.eauto.min_soc_percentage - or ev_soc_percentage > parameters.eauto.max_soc_percentage + ev_soc_percentage < parameters.ev.min_soc_percentage + or ev_soc_percentage > parameters.ev.max_soc_percentage ): - total_balance += ( - abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty - ) + total_balance += abs(parameters.ev.min_soc_percentage - ev_soc_percentage) * penalty return (total_balance,) @@ -1084,26 +1078,26 @@ class GeneticOptimization(OptimizationBase): # Initialize PV and EV batteries battery: Optional[Battery] = None - if parameters.pv_akku: + if parameters.pv_battery: battery = Battery( - parameters.pv_akku, + parameters.pv_battery, prediction_hours=self.config.prediction.hours, ) battery.set_charge_per_hour(np.full(self.config.prediction.hours, 0)) ev: Optional[Battery] = None - if parameters.eauto: + if parameters.ev: ev = Battery( - parameters.eauto, + parameters.ev, prediction_hours=self.config.prediction.hours, ) ev.set_charge_per_hour(np.full(self.config.prediction.hours, 1)) self.optimize_ev = ( - parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0 + parameters.ev.min_soc_percentage - parameters.ev.initial_soc_percentage >= 0 ) # electrical vehicle charge rates - if parameters.eauto.charge_rates is not None: - self.ev_possible_charge_values = parameters.eauto.charge_rates + if parameters.ev.charge_rates is not None: + self.ev_possible_charge_values = parameters.ev.charge_rates elif ( self.config.devices.electric_vehicles and self.config.devices.electric_vehicles[0] @@ -1134,9 +1128,9 @@ class GeneticOptimization(OptimizationBase): # Battery AC charge rates — use the battery's configured charge_rates so the # optimizer can select partial AC charge power (e.g. 10 %, 50 %, 100 %) instead # of always forcing full power. Falls back to [1.0] when not configured. - if parameters.pv_akku and parameters.pv_akku.charge_rates: + if parameters.pv_battery and parameters.pv_battery.charge_rates: self.bat_possible_charge_values = [ - r for r in parameters.pv_akku.charge_rates if r > 0.0 + r for r in parameters.pv_battery.charge_rates if r > 0.0 ] or [1.0] elif ( self.config.devices.batteries @@ -1195,16 +1189,16 @@ class GeneticOptimization(OptimizationBase): simulation_result = self.evaluate_inner(start_solution) # Prepare results - discharge_hours_bin, eautocharge_hours_index, washingstart_int = self.split_individual( + discharge_hours_bin, ev_charge_hours_index, washingstart_int = self.split_individual( start_solution ) # home appliance may have choosen a different appliance start hour if self.simulation.home_appliance: washingstart_int = self.simulation.home_appliance_start_hour - eautocharge_hours_float = ( - [self.ev_possible_charge_values[i] for i in eautocharge_hours_index] - if eautocharge_hours_index is not None + ev_charge_hours_float = ( + [self.ev_possible_charge_values[i] for i in ev_charge_hours_index] + if ev_charge_hours_index is not None else None ) @@ -1233,9 +1227,9 @@ class GeneticOptimization(OptimizationBase): "ac_charge": ac_charge_hours, "dc_charge": dc_charge_hours, "discharge_allowed": discharge, - "eautocharge_hours_float": eautocharge_hours_float, + "ev_charge_hours_float": ev_charge_hours_float, "result": GeneticSimulationResult(**simulation_result).model_dump(), - "eauto_obj": self.simulation.ev.to_dict() if self.simulation.ev else None, + "ev_obj": self.simulation.ev.to_dict() if self.simulation.ev else None, "start_solution": start_solution, "washingstart": washingstart_int, "extra_data": extra_data, @@ -1254,9 +1248,9 @@ class GeneticOptimization(OptimizationBase): "ac_charge": ac_charge_hours, "dc_charge": dc_charge_hours, "discharge_allowed": discharge, - "eautocharge_hours_float": eautocharge_hours_float, + "ev_charge_hours_float": ev_charge_hours_float, "result": GeneticSimulationResult(**simulation_result), - "eauto_obj": self.simulation.ev, + "ev_obj": self.simulation.ev, "start_solution": start_solution, "washingstart": washingstart_int, } diff --git a/src/akkudoktoreos/optimization/genetic/geneticparams.py b/src/akkudoktoreos/optimization/genetic/geneticparams.py index 5802b294..5bac89ae 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticparams.py +++ b/src/akkudoktoreos/optimization/genetic/geneticparams.py @@ -137,10 +137,18 @@ class GeneticOptimizationParameters( optimization process, such as forecasts, pricing, battery and appliance models. """ + model_config = ConfigDict(populate_by_name=True, extra="ignore") + ems: GeneticEnergyManagementParameters - pv_akku: Optional[SolarPanelBatteryParameters] + pv_battery: Optional[SolarPanelBatteryParameters] = Field( + validation_alias=AliasChoices("pv_battery", "pv_akku"), + json_schema_extra={"description": "PV battery parameters."}, + ) inverter: Optional[InverterParameters] - eauto: Optional[ElectricVehicleParameters] + ev: Optional[ElectricVehicleParameters] = Field( + validation_alias=AliasChoices("ev", "eauto"), + json_schema_extra={"description": "Electric vehicle parameters."}, + ) dishwasher: Optional[HomeApplianceParameters] = None temperature_forecast: Optional[list[Optional[float]]] = Field( default=None, @@ -155,6 +163,17 @@ class GeneticOptimizationParameters( }, ) + # Computed fields for backward compatibility (deprecated German names) + @computed_field(json_schema_extra={"deprecated": True}) + def pv_akku(self) -> Optional[SolarPanelBatteryParameters]: + """Deprecated: Use pv_battery instead.""" + return self.pv_battery + + @computed_field(json_schema_extra={"deprecated": True}) + def eauto(self) -> Optional[ElectricVehicleParameters]: + """Deprecated: Use ev instead.""" + return self.ev + @model_validator(mode="after") def validate_list_length(self) -> Self: """Ensure that temperature forecast list matches the PV forecast length. @@ -457,7 +476,7 @@ class GeneticOptimizationParameters( # Levelized cost of ownership if battery_config.levelized_cost_of_storage_kwh is None: logger.info( - "No battery device LCOS data available - defaulting to 0 €/kWh. Parameter preparation attempt {}.", + "No battery device LCOS data available - defaulting to 0 [amount/kWh]. Parameter preparation attempt {}.", attempt, ) battery_config.levelized_cost_of_storage_kwh = 0 @@ -669,8 +688,8 @@ class GeneticOptimizationParameters( price_per_wh_battery=battery_lcos_kwh / 1000, ), temperature_forecast=weather_temp_air, - pv_akku=battery_params, - eauto=electric_vehicle_params, + pv_battery=battery_params, + ev=electric_vehicle_params, inverter=inverter_params, dishwasher=home_appliance_params, start_solution=start_solution, diff --git a/src/akkudoktoreos/optimization/genetic/geneticsolution.py b/src/akkudoktoreos/optimization/genetic/geneticsolution.py index a30ad53f..a3a7d85b 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticsolution.py +++ b/src/akkudoktoreos/optimization/genetic/geneticsolution.py @@ -242,6 +242,8 @@ class GeneticSimulationResult(GeneticParametersBaseModel): class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): """**Note**: The first value of "load_wh_per_hour", "grid_feed_in_wh_per_hour", and "grid_consumption_wh_per_hour", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged.""" + model_config = ConfigDict(populate_by_name=True, extra="ignore") + ac_charge: list[float] = Field( json_schema_extra={ "description": "Array with AC charging values as relative power (0.0-1.0), other values set to 0." @@ -257,9 +259,17 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): "description": "Array with discharge values (1 for discharge, 0 otherwise)." } ) - eautocharge_hours_float: Optional[list[float]] = Field(json_schema_extra={"description": "TBD"}) + ev_charge_hours_float: Optional[list[float]] = Field( + validation_alias=AliasChoices("ev_charge_hours_float", "eautocharge_hours_float"), + json_schema_extra={ + "description": "Array with EV charging values as relative power (0.0-1.0), or `null` if no EV is optimized." + }, + ) result: GeneticSimulationResult - eauto_obj: Optional[ElectricVehicleResult] + ev_obj: Optional[ElectricVehicleResult] = Field( + validation_alias=AliasChoices("ev_obj", "eauto_obj"), + json_schema_extra={"description": "Electric vehicle state after optimization."}, + ) start_solution: Optional[list[float]] = Field( default=None, json_schema_extra={ @@ -273,6 +283,17 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): }, ) + # Computed fields for backward compatibility (deprecated German names) + @computed_field(json_schema_extra={"deprecated": True}) + def eautocharge_hours_float(self) -> Optional[list[float]]: + """Deprecated: Use ev_charge_hours_float instead.""" + return self.ev_charge_hours_float + + @computed_field(json_schema_extra={"deprecated": True}) + def eauto_obj(self) -> Optional[ElectricVehicleResult]: + """Deprecated: Use ev_obj instead.""" + return self.ev_obj + @field_validator( "ac_charge", "dc_charge", @@ -283,7 +304,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): return NumpyEncoder.convert_numpy(field)[0] @field_validator( - "eauto_obj", + "ev_obj", mode="before", ) def convert_eauto(cls, field: Any) -> Any: @@ -548,14 +569,14 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): solution[key] = operation[key] # Add EV battery solution - # eautocharge_hours_float start at hour 0 of start day + # ev_charge_hours_float start at hour 0 of start day # result.ev_soc_per_hour start at start_datetime.hour - if self.eauto_obj: + if self.ev_obj: ev_device_id = self._ev_device_id() - if self.eautocharge_hours_float is None: + if self.ev_charge_hours_float is None: # Electric vehicle is full enough. No load times. solution[f"{ev_device_id}_soc_factor"] = [ - self.eauto_obj.initial_soc_percentage / 100.0 + self.ev_obj.initial_soc_percentage / 100.0 ] * n_points solution["genetic_ev_charge_factor"] = [0.0] * n_points # operation modes @@ -576,7 +597,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): operation = { "genetic_ev_charge_factor": [], } - for hour_idx, rate in enumerate(self.eautocharge_hours_float): + for hour_idx, rate in enumerate(self.ev_charge_hours_float): if hour_idx < start_day_hour: continue if hour_idx >= start_day_hour + n_points: @@ -791,10 +812,10 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): ) # Add EV battery instructions (fill rate based control) - # eautocharge_hours_float start at hour 0 of start day - if self.eauto_obj: + # ev_charge_hours_float start at hour 0 of start day + if self.ev_obj: resource_id = self._ev_device_id() - if self.eautocharge_hours_float is None: + if self.ev_charge_hours_float is None: # Electric vehicle is full enough. No load times. logger.debug("EV: {} - SoC >= min, no optimization", resource_id) plan.add_instruction( @@ -810,9 +831,9 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): last_operation_mode = None last_operation_mode_factor = None logger.debug( - "EV: {} - {}", resource_id, self.eautocharge_hours_float[start_day_hour:] + "EV: {} - {}", resource_id, self.ev_charge_hours_float[start_day_hour:] ) - for hour_idx, rate in enumerate(self.eautocharge_hours_float): + for hour_idx, rate in enumerate(self.ev_charge_hours_float): if hour_idx < start_day_hour: continue operation_mode, operation_mode_factor = self._battery_operation_from_solution( diff --git a/src/akkudoktoreos/server/eos.py b/src/akkudoktoreos/server/eos.py index c51d96d2..98e3111e 100755 --- a/src/akkudoktoreos/server/eos.py +++ b/src/akkudoktoreos/server/eos.py @@ -1173,7 +1173,7 @@ def fastapi_energy_management_plan_get() -> EnergyManagementPlan: return plan -@app.get("/strompreis", tags=["prediction"]) +@app.get("/strompreis", tags=["prediction"], deprecated=True) async def fastapi_strompreis() -> list[float]: """Deprecated: Electricity Market Price Prediction per Wh [amount/Wh]. @@ -1236,7 +1236,7 @@ class GesamtlastRequest(PydanticBaseModel): hours: int -@app.post("/gesamtlast", tags=["prediction"]) +@app.post("/gesamtlast", tags=["prediction"], deprecated=True) async def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]: """Deprecated: Total Load Prediction with adjustment. @@ -1333,7 +1333,7 @@ async def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]: return prediction_list -@app.get("/gesamtlast_simple", tags=["prediction"]) +@app.get("/gesamtlast_simple", tags=["prediction"], deprecated=True) async def fastapi_gesamtlast_simple(year_energy: float) -> list[float]: """Deprecated: Total Load Prediction. diff --git a/tests/single_test_optimization.py b/tests/single_test_optimization.py index 007daf1a..1cec22b2 100755 --- a/tests/single_test_optimization.py +++ b/tests/single_test_optimization.py @@ -179,7 +179,7 @@ async def prepare_optimization_real_parameters() -> GeneticOptimizationParameter "pv_forecast_wh": pv_forecast, "electricity_price_per_wh": electricity_price_per_wh, }, - "pv_akku": { + "pv_battery": { "device_id": "battery 1", "capacity_wh": 26400, "initial_soc_percentage": 15, @@ -190,7 +190,7 @@ async def prepare_optimization_real_parameters() -> GeneticOptimizationParameter "max_power_wh": 10000, "battery_id": "battery 1", }, - "eauto": { + "ev": { "device_id": "electric vehicle 1", "min_soc_percentage": 50, "capacity_wh": 60000, @@ -375,7 +375,7 @@ def prepare_optimization_parameters() -> GeneticOptimizationParameters: "pv_forecast_wh": pv_forecast, "electricity_price_per_wh": electricity_price_per_wh, }, - "pv_akku": { + "pv_battery": { "device_id": "battery 1", "capacity_wh": 26400, "initial_soc_percentage": 15, @@ -386,7 +386,7 @@ def prepare_optimization_parameters() -> GeneticOptimizationParameters: "max_power_wh": 10000, "battery_id": "battery 1", }, - "eauto": { + "ev": { "device_id": "electric vehicle 1", "min_soc_percentage": 50, "capacity_wh": 60000, diff --git a/tests/test_translations.py b/tests/test_translations.py index f80c8c9c..e24d9ce2 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -126,3 +126,45 @@ if __name__ == "__main__": test_genetic_params_english_output() test_simulation_result_translations() print("\n✅✅✅ All translation tests passed! ✅✅✅") + +def test_optimization_parameters_device_translations(): + """Test that German device field names are accepted and re-emitted.""" + from akkudoktoreos.optimization.genetic.geneticparams import ( + GeneticOptimizationParameters, + ) + + ems_de = { + "pv_prognose_wh": [100.0, 200.0], + "strompreis_euro_pro_wh": [0.0003, 0.0003], + "einspeiseverguetung_euro_pro_wh": 0.00007, + "preis_euro_pro_wh_akku": 0.0001, + "gesamtlast": [500.0, 600.0], + } + params = GeneticOptimizationParameters( + ems=ems_de, + pv_akku={"device_id": "battery1", "capacity_wh": 8000}, + inverter=None, + eauto={"device_id": "ev1", "capacity_wh": 60000}, + ) + # English attributes are populated from the German input names + assert params.pv_battery is not None + assert params.pv_battery.capacity_wh == 8000 + assert params.ev is not None + assert params.ev.capacity_wh == 60000 + + # English input names work as well + params_en = GeneticOptimizationParameters( + ems=ems_de, + pv_battery={"device_id": "battery1", "capacity_wh": 8000}, + inverter=None, + ev={"device_id": "ev1", "capacity_wh": 60000}, + ) + assert params_en.pv_battery is not None + assert params_en.ev is not None + + # Both names are present in the output (backward compatibility) + dumped = params.model_dump() + for name in ("pv_battery", "pv_akku", "ev", "eauto"): + assert name in dumped, f"{name} missing in output" + assert dumped["pv_akku"] == dumped["pv_battery"] + assert dumped["eauto"] == dumped["ev"]