mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-24 02:38:11 +00:00
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
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:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
70
openapi.json
70
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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user