feat: rename remaining German API fields and deprecate legacy endpoints (#1164)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled

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 <tobias.wizneteu@gmail.com>
This commit is contained in:
7tobias
2026-07-21 04:22:50 -04:00
committed by GitHub
parent 641873d867
commit 8d72177671
10 changed files with 215 additions and 101 deletions

View File

@@ -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:

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS
**Version**: `v0.3.0.dev2607201457168588`
**Version**: `v0.3.0.dev2607210205856856`
<!-- pyml disable line-length -->
**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.

View File

@@ -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",

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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.

View File

@@ -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,

View File

@@ -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"]