From 6933b33542f7d323367e09171bca2b6b272d6d37 Mon Sep 17 00:00:00 2001 From: 7tobias Date: Sat, 18 Jul 2026 02:08:25 -0400 Subject: [PATCH] feat: rename genetic optimization API fields to English (#675) Rename the German field names of the genetic optimization API to English with full backward compatibility: - English names are canonical and documented in the OpenAPI schema; the German names are still accepted on input via validation aliases and re-emitted in responses as deprecated computed fields - fix visualization receiving the raw German-keyed simulation dict (KeyError: load_wh_per_hour) - rename internal German identifiers (optimize_ems, total_balance, battery_residual_value, self_consumption, extra_data keys) - use amount instead of currency/euro in chart labels and schema descriptions; document currency handling (whole currency units, never cents) in the API description - regenerate openapi.json and generated docs Co-authored-by: Tobias Welz --- docs/_generated/configdevices.md | 2 +- docs/_generated/configelecprice.md | 2 +- docs/_generated/configfeedintariff.md | 2 +- docs/_generated/openapi.md | 17 +- openapi.json | 255 ++++++++++++++---- src/akkudoktoreos/core/ems.py | 4 +- src/akkudoktoreos/devices/devices.py | 2 +- .../optimization/genetic/genetic.py | 92 +++---- .../optimization/genetic/geneticparams.py | 88 ++++-- .../optimization/genetic/geneticsolution.py | 211 ++++++++++----- src/akkudoktoreos/prediction/elecprice.py | 2 +- src/akkudoktoreos/prediction/elecpriceabc.py | 4 +- .../prediction/feedintariffabc.py | 4 +- .../prediction/feedintarifffixed.py | 2 +- src/akkudoktoreos/server/dash/plan.py | 4 +- src/akkudoktoreos/server/dash/prediction.py | 2 +- src/akkudoktoreos/server/eos.py | 17 +- src/akkudoktoreos/utils/visualize.py | 62 ++--- tests/single_test_optimization.py | 42 +-- tests/test_geneticoptimize.py | 4 +- tests/test_inverter_efficiency.py | 2 +- tests/test_translations.py | 128 +++++++++ .../testdata/docs/_generated/configdevices.md | 2 +- .../docs/_generated/configelecprice.md | 2 +- .../docs/_generated/configfeedintariff.md | 2 +- 25 files changed, 690 insertions(+), 264 deletions(-) create mode 100644 tests/test_translations.py diff --git a/docs/_generated/configdevices.md b/docs/_generated/configdevices.md index 38e3d691..5891a0c1 100644 --- a/docs/_generated/configdevices.md +++ b/docs/_generated/configdevices.md @@ -456,7 +456,7 @@ as a cohesive unit for scheduling and availability checking. | charging_efficiency | `float` | `rw` | `0.88` | Charging efficiency [0.01 ... 1.00]. | | device_id | `str` | `rw` | `` | ID of device | | discharging_efficiency | `float` | `rw` | `0.88` | Discharge efficiency [0.01 ... 1.00]. | -| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh]. | +| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh]. | | max_charge_power_w | `Optional[float]` | `rw` | `5000` | Maximum charging power [W]. | | max_soc_percentage | `int` | `rw` | `100` | Maximum state of charge (SOC) as percentage of capacity [%]. | | measurement_key_power_3_phase_sym_w | `str` | `ro` | `N/A` | Measurement key for the symmetric 3 phase power the battery is charged or discharged with [W]. | diff --git a/docs/_generated/configelecprice.md b/docs/_generated/configelecprice.md index 4a3e0ef4..f2c3f544 100644 --- a/docs/_generated/configelecprice.md +++ b/docs/_generated/configelecprice.md @@ -7,7 +7,7 @@ | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [€/kWh]. Will be added to variable market price. | +| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [amount/kWh]. Will be added to variable market price. | | elecpricefixed | `EOS_ELECPRICE__ELECPRICEFIXED` | `ElecPriceFixedCommonSettings` | `rw` | `required` | Fixed electricity price provider settings. | | elecpriceimport | `EOS_ELECPRICE__ELECPRICEIMPORT` | `ElecPriceImportCommonSettings` | `rw` | `required` | Import provider settings. | | energycharts | `EOS_ELECPRICE__ENERGYCHARTS` | `ElecPriceEnergyChartsCommonSettings` | `rw` | `required` | Energy Charts provider settings. | diff --git a/docs/_generated/configfeedintariff.md b/docs/_generated/configfeedintariff.md index 8e882a1e..dd855ef1 100644 --- a/docs/_generated/configfeedintariff.md +++ b/docs/_generated/configfeedintariff.md @@ -95,7 +95,7 @@ | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | Electricity price feed in tariff [€/kWH]. | +| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | Electricity price feed in tariff [amount/kWh]. | ::: diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index e2bd0271..b7130adf 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,9 +1,22 @@ # Akkudoktor-EOS -**Version**: `v0.3.0.dev2607171544999797` +**Version**: `v0.3.0.dev2607171715033828` **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. + +## Currency Information + +All monetary values in this API are expressed in the locally configured currency of the EOS installation. The system is designed to work with any currency (e.g., EUR, SEK, CHF, USD, GBP, etc.). Each installation uses a single, consistent currency throughout all endpoints and responses. + +Values are given in whole currency units, not in hundredth subunits: a value of `0.0003` in an installation configured for Euro means 0.0003 EUR/Wh (i.e. 0.30 EUR/kWh), never cents. The same applies to all totals (e.g. `total_costs` of `1.5` = 1.50 EUR). + +Field names containing cost, price, revenue, tariff, or similar monetary terms (e.g., `total_costs`, `electricity_price_per_wh`, `revenue_per_hour`) represent amounts in the configured currency, without explicit currency designation in the field name to maintain currency-neutrality. + +## Deprecated Field Names + +The genetic optimization API fields were renamed from German to English. For backward compatibility the old German field names (e.g. `gesamtlast`, `pv_prognose_wh`, `Gesamtbilanz_Euro`) are still accepted on input and are emitted in responses alongside the English names, marked as deprecated in the schema. They will be removed in a future release — new clients should use the English field names only. + **Base URL**: `No base URL provided.` @@ -184,7 +197,7 @@ Fastapi Strompreis ```python """ -Deprecated: Electricity Market Price Prediction per Wh (€/Wh). +Deprecated: Electricity Market Price Prediction per Wh [amount/Wh]. Electricity prices start at 00.00.00 today and are provided for 48 hours. If no prices are available the missing ones at the start of the series are diff --git a/openapi.json b/openapi.json index dc35f7dc..c2ca35f4 100644 --- a/openapi.json +++ b/openapi.json @@ -3,12 +3,12 @@ "info": { "title": "Akkudoktor-EOS", "summary": "Comprehensive solution for simulating and optimizing an energy system based on renewable energy sources", - "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.", + "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.\n\n## Currency Information\n\nAll monetary values in this API are expressed in the locally configured currency of the EOS installation. The system is designed to work with any currency (e.g., EUR, SEK, CHF, USD, GBP, etc.). Each installation uses a single, consistent currency throughout all endpoints and responses.\n\nValues are given in whole currency units, not in hundredth subunits: a value of `0.0003` in an installation configured for Euro means 0.0003 EUR/Wh (i.e. 0.30 EUR/kWh), never cents. The same applies to all totals (e.g. `total_costs` of `1.5` = 1.50 EUR).\n\nField names containing cost, price, revenue, tariff, or similar monetary terms (e.g., `total_costs`, `electricity_price_per_wh`, `revenue_per_hour`) represent amounts in the configured currency, without explicit currency designation in the field name to maintain currency-neutrality.\n\n## Deprecated Field Names\n\nThe genetic optimization API fields were renamed from German to English. For backward compatibility the old German field names (e.g. `gesamtlast`, `pv_prognose_wh`, `Gesamtbilanz_Euro`) are still accepted on input and are emitted in responses alongside the English names, marked as deprecated in the schema. They will be removed in a future release \u2014 new clients should use the English field names only.\n", "license": { "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "v0.3.0.dev2607171544999797" + "version": "v0.3.0.dev2607171715033828" }, "paths": { "/v1/admin/cache/clear": { @@ -1941,7 +1941,7 @@ "prediction" ], "summary": "Fastapi Strompreis", - "description": "Deprecated: Electricity Market Price Prediction per Wh (\u20ac/Wh).\n\nElectricity prices start at 00.00.00 today and are provided for 48 hours.\nIf no prices are available the missing ones at the start of the series are\nfilled with the first available price.\n\nNote:\n Electricity price charges are added.\n\nNote:\n Set ElecPriceAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=elecprice_marketprice_wh' or\n '/v1/prediction/list?key=elecprice_marketprice_kwh' instead.", + "description": "Deprecated: Electricity Market Price Prediction per Wh [amount/Wh].\n\nElectricity prices start at 00.00.00 today and are provided for 48 hours.\nIf no prices are available the missing ones at the start of the series are\nfilled with the first available price.\n\nNote:\n Electricity price charges are added.\n\nNote:\n Set ElecPriceAkkudoktor as provider, then update data with\n '/v1/prediction/update'\n and then request data with\n '/v1/prediction/list?key=elecprice_marketprice_wh' or\n '/v1/prediction/list?key=elecprice_marketprice_kwh' instead.", "operationId": "fastapi_strompreis_strompreis_get", "responses": { "200": { @@ -2319,7 +2319,7 @@ "levelized_cost_of_storage_kwh": { "type": "number", "title": "Levelized Cost Of Storage Kwh", - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [\u20ac/kWh].", + "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh].", "default": 0.0, "examples": [ 0.12 @@ -2473,7 +2473,7 @@ "levelized_cost_of_storage_kwh": { "type": "number", "title": "Levelized Cost Of Storage Kwh", - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [\u20ac/kWh].", + "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh].", "default": 0.0, "examples": [ 0.12 @@ -3462,7 +3462,7 @@ } ], "title": "Charges Kwh", - "description": "Electricity price charges [\u20ac/kWh]. Will be added to variable market price.", + "description": "Electricity price charges [amount/kWh]. Will be added to variable market price.", "examples": [ 0.21 ] @@ -3529,7 +3529,7 @@ } ], "title": "Charges Kwh", - "description": "Electricity price charges [\u20ac/kWh]. Will be added to variable market price.", + "description": "Electricity price charges [amount/kWh]. Will be added to variable market price.", "examples": [ 0.21 ] @@ -4447,7 +4447,7 @@ } ], "title": "Feed In Tariff Kwh", - "description": "Electricity price feed in tariff [\u20ac/kWH].", + "description": "Electricity price feed in tariff [amount/kWh].", "examples": [ 0.078 ] @@ -4861,23 +4861,23 @@ }, "GeneticEnergyManagementParameters": { "properties": { - "pv_prognose_wh": { + "pv_forecast_wh": { "items": { "type": "number" }, "type": "array", - "title": "Pv Prognose Wh", + "title": "Pv Forecast Wh", "description": "An array of floats representing the forecasted photovoltaic output in watts for different time intervals." }, - "strompreis_euro_pro_wh": { + "electricity_price_per_wh": { "items": { "type": "number" }, "type": "array", - "title": "Strompreis Euro Pro Wh", - "description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals." + "title": "Electricity Price Per Wh", + "description": "An array of floats representing the electricity price per watt-hour for different time intervals." }, - "einspeiseverguetung_euro_pro_wh": { + "feed_in_tariff_per_wh": { "anyOf": [ { "items": { @@ -4889,31 +4889,30 @@ "type": "number" } ], - "title": "Einspeiseverguetung Euro Pro Wh", - "description": "A float or array of floats representing the feed-in compensation in euros per watt-hour." + "title": "Feed In Tariff Per Wh", + "description": "A float or array of floats representing the feed-in compensation per watt-hour." }, - "preis_euro_pro_wh_akku": { + "price_per_wh_battery": { "type": "number", - "title": "Preis Euro Pro Wh Akku", + "title": "Price Per Wh Battery", "description": "A float representing the cost of battery energy per watt-hour." }, - "gesamtlast": { + "total_load": { "items": { "type": "number" }, "type": "array", - "title": "Gesamtlast", + "title": "Total Load", "description": "An array of floats representing the total load (consumption) in watts for different time intervals." } }, - "additionalProperties": false, "type": "object", "required": [ - "pv_prognose_wh", - "strompreis_euro_pro_wh", - "einspeiseverguetung_euro_pro_wh", - "preis_euro_pro_wh_akku", - "gesamtlast" + "pv_forecast_wh", + "electricity_price_per_wh", + "feed_in_tariff_per_wh", + "price_per_wh_battery", + "total_load" ], "title": "GeneticEnergyManagementParameters", "description": "Encapsulates energy-related forecasts and costs used in GENETIC optimization." @@ -5014,51 +5013,51 @@ }, "GeneticSimulationResult": { "properties": { - "Last_Wh_pro_Stunde": { + "load_wh_per_hour": { "items": { "type": "number" }, "type": "array", - "title": "Last Wh Pro Stunde", - "description": "TBD" + "title": "Load Wh Per Hour", + "description": "The load in watt-hours per hour." }, - "EAuto_SoC_pro_Stunde": { + "ev_soc_per_hour": { "items": { "type": "number" }, "type": "array", - "title": "Eauto Soc Pro Stunde", + "title": "Ev Soc Per Hour", "description": "The state of charge of the EV for each hour." }, - "Einnahmen_Euro_pro_Stunde": { + "revenue_per_hour": { "items": { "type": "number" }, "type": "array", - "title": "Einnahmen Euro Pro Stunde", - "description": "The revenue from grid feed-in or other sources in euros per hour." + "title": "Revenue Per Hour", + "description": "The revenue from grid feed-in or other sources per hour." }, - "Gesamt_Verluste": { + "total_losses": { "type": "number", - "title": "Gesamt Verluste", + "title": "Total Losses", "description": "The total losses in watt-hours over the entire period." }, - "Gesamtbilanz_Euro": { + "total_balance": { "type": "number", - "title": "Gesamtbilanz Euro", - "description": "The total balance of revenues minus costs in euros." + "title": "Total Balance", + "description": "The total balance of revenues minus costs." }, - "Gesamteinnahmen_Euro": { + "total_revenue": { "type": "number", - "title": "Gesamteinnahmen Euro", - "description": "The total revenues in euros." + "title": "Total Revenue", + "description": "The total revenues." }, - "Gesamtkosten_Euro": { + "total_costs": { "type": "number", - "title": "Gesamtkosten Euro", - "description": "The total costs in euros." + "title": "Total Costs", + "description": "The total costs." }, - "Home_appliance_wh_per_hour": { + "home_appliance_wh_per_hour": { "items": { "anyOf": [ { @@ -5073,13 +5072,138 @@ "title": "Home Appliance Wh Per Hour", "description": "The energy consumption of a household appliance in watt-hours per hour." }, + "costs_per_hour": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Costs Per Hour", + "description": "The costs per hour." + }, + "grid_consumption_wh_per_hour": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Grid Consumption Wh Per Hour", + "description": "The grid energy drawn in watt-hours per hour." + }, + "grid_feed_in_wh_per_hour": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Grid Feed In Wh Per Hour", + "description": "The energy fed into the grid in watt-hours per hour." + }, + "losses_per_hour": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Losses Per Hour", + "description": "The losses in watt-hours per hour." + }, + "battery_soc_per_hour": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Battery Soc Per Hour", + "description": "The state of charge of the battery (not the EV) in percentage per hour." + }, + "electricity_price": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Electricity Price", + "description": "Used Electricity Price, including predictions" + }, + "Last_Wh_pro_Stunde": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Last Wh Pro Stunde", + "description": "Deprecated: Use load_wh_per_hour instead.", + "deprecated": true, + "readOnly": true + }, + "EAuto_SoC_pro_Stunde": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Eauto Soc Pro Stunde", + "description": "Deprecated: Use ev_soc_per_hour instead.", + "deprecated": true, + "readOnly": true + }, + "Einnahmen_Euro_pro_Stunde": { + "items": { + "type": "number" + }, + "type": "array", + "title": "Einnahmen Euro Pro Stunde", + "description": "Deprecated: Use revenue_per_hour instead.", + "deprecated": true, + "readOnly": true + }, + "Gesamt_Verluste": { + "type": "number", + "title": "Gesamt Verluste", + "description": "Deprecated: Use total_losses instead.", + "deprecated": true, + "readOnly": true + }, + "Gesamtbilanz_Euro": { + "type": "number", + "title": "Gesamtbilanz Euro", + "description": "Deprecated: Use total_balance instead.", + "deprecated": true, + "readOnly": true + }, + "Gesamteinnahmen_Euro": { + "type": "number", + "title": "Gesamteinnahmen Euro", + "description": "Deprecated: Use total_revenue instead.", + "deprecated": true, + "readOnly": true + }, + "Gesamtkosten_Euro": { + "type": "number", + "title": "Gesamtkosten Euro", + "description": "Deprecated: Use total_costs instead.", + "deprecated": true, + "readOnly": true + }, + "Home_appliance_wh_per_hour": { + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "type": "array", + "title": "Home Appliance Wh Per Hour", + "description": "Deprecated: Use home_appliance_wh_per_hour instead.", + "deprecated": true, + "readOnly": true + }, "Kosten_Euro_pro_Stunde": { "items": { "type": "number" }, "type": "array", "title": "Kosten Euro Pro Stunde", - "description": "The costs in euros per hour." + "description": "Deprecated: Use costs_per_hour instead.", + "deprecated": true, + "readOnly": true }, "Netzbezug_Wh_pro_Stunde": { "items": { @@ -5087,7 +5211,9 @@ }, "type": "array", "title": "Netzbezug Wh Pro Stunde", - "description": "The grid energy drawn in watt-hours per hour." + "description": "Deprecated: Use grid_consumption_wh_per_hour instead.", + "deprecated": true, + "readOnly": true }, "Netzeinspeisung_Wh_pro_Stunde": { "items": { @@ -5095,7 +5221,9 @@ }, "type": "array", "title": "Netzeinspeisung Wh Pro Stunde", - "description": "The energy fed into the grid in watt-hours per hour." + "description": "Deprecated: Use grid_feed_in_wh_per_hour instead.", + "deprecated": true, + "readOnly": true }, "Verluste_Pro_Stunde": { "items": { @@ -5103,7 +5231,9 @@ }, "type": "array", "title": "Verluste Pro Stunde", - "description": "The losses in watt-hours per hour." + "description": "Deprecated: Use losses_per_hour instead.", + "deprecated": true, + "readOnly": true }, "akku_soc_pro_stunde": { "items": { @@ -5111,7 +5241,9 @@ }, "type": "array", "title": "Akku Soc Pro Stunde", - "description": "The state of charge of the battery (not the EV) in percentage per hour." + "description": "Deprecated: Use battery_soc_per_hour instead.", + "deprecated": true, + "readOnly": true }, "Electricity_price": { "items": { @@ -5119,12 +5251,27 @@ }, "type": "array", "title": "Electricity Price", - "description": "Used Electricity Price, including predictions" + "description": "Deprecated: Use electricity_price instead.", + "deprecated": true, + "readOnly": true } }, - "additionalProperties": false, "type": "object", "required": [ + "load_wh_per_hour", + "ev_soc_per_hour", + "revenue_per_hour", + "total_losses", + "total_balance", + "total_revenue", + "total_costs", + "home_appliance_wh_per_hour", + "costs_per_hour", + "grid_consumption_wh_per_hour", + "grid_feed_in_wh_per_hour", + "losses_per_hour", + "battery_soc_per_hour", + "electricity_price", "Last_Wh_pro_Stunde", "EAuto_SoC_pro_Stunde", "Einnahmen_Euro_pro_Stunde", @@ -5236,7 +5383,7 @@ "eauto_obj" ], "title": "GeneticSolution", - "description": "**Note**: The first value of \"Last_Wh_per_hour\", \"Netzeinspeisung_Wh_per_hour\", and \"Netzbezug_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." + "description": "**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." }, "GesamtlastRequest": { "properties": { diff --git a/src/akkudoktoreos/core/ems.py b/src/akkudoktoreos/core/ems.py index 1b34a46e..d5b7f422 100644 --- a/src/akkudoktoreos/core/ems.py +++ b/src/akkudoktoreos/core/ems.py @@ -278,7 +278,7 @@ class EnergyManagement( start_hour = EnergyManagement._start_datetime.hour solution = await loop.run_in_executor( None, - lambda: optimization.optimierung_ems( + lambda: optimization.optimize_ems( start_hour=start_hour, parameters=cast( GeneticOptimizationParameters, genetic_parameters @@ -355,7 +355,7 @@ class EnergyManagement( start_hour = EnergyManagement._start_datetime.hour solution = await loop.run_in_executor( None, - lambda: optimization.optimierung_ems( + lambda: optimization.optimize_ems( start_hour=start_hour, parameters=genetic_parameters, ngen=genetic_individuals, diff --git a/src/akkudoktoreos/devices/devices.py b/src/akkudoktoreos/devices/devices.py index 98b155a7..a0d1f743 100644 --- a/src/akkudoktoreos/devices/devices.py +++ b/src/akkudoktoreos/devices/devices.py @@ -51,7 +51,7 @@ class BatteriesCommonSettings(DevicesBaseSettings): levelized_cost_of_storage_kwh: float = Field( default=0.0, json_schema_extra={ - "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh].", + "description": "Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh].", "examples": [0.12], }, ) diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index 212b77e6..a420df2f 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -67,13 +67,13 @@ class GeneticSimulation(PydanticBaseModel): elect_price_hourly: Optional[NDArray[Shape["*"], float]] = Field( default=None, json_schema_extra={ - "description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals." + "description": "An array of floats representing the electricity price per watt-hour for different time intervals." }, ) elect_revenue_per_hour_arr: Optional[NDArray[Shape["*"], float]] = Field( default=None, json_schema_extra={ - "description": "An array of floats representing the feed-in compensation in euros per watt-hour." + "description": "An array of floats representing the feed-in compensation per watt-hour." }, ) @@ -121,15 +121,13 @@ class GeneticSimulation(PydanticBaseModel): self.prediction_hours = prediction_hours # Load arrays from provided EMS parameters - self.load_energy_array = np.array(parameters.gesamtlast, float) - self.pv_prediction_wh = np.array(parameters.pv_prognose_wh, float) - self.elect_price_hourly = np.array(parameters.strompreis_euro_pro_wh, float) + self.load_energy_array = np.array(parameters.total_load, float) + self.pv_prediction_wh = np.array(parameters.pv_forecast_wh, float) + self.elect_price_hourly = np.array(parameters.electricity_price_per_wh, float) self.elect_revenue_per_hour_arr = ( - parameters.einspeiseverguetung_euro_pro_wh - if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list) - else np.full( - len(self.load_energy_array), parameters.einspeiseverguetung_euro_pro_wh, float - ) + parameters.feed_in_tariff_per_wh + if isinstance(parameters.feed_in_tariff_per_wh, list) + else np.full(len(self.load_energy_array), parameters.feed_in_tariff_per_wh, float) ) # Associate devices @@ -159,8 +157,8 @@ class GeneticSimulation(PydanticBaseModel): def simulate(self, start_hour: int) -> dict[str, Any]: """Simulate energy usage and costs for the given start hour. - akku_soc_pro_stunde begin of the hour, initial hour state! - last_wh_pro_stunde integral of last hour (end state) + battery_soc_per_hour begin of the hour, initial hour state! + load_wh_per_hour integral of last hour (end state) """ # Remember start hour self.start_hour = start_hour @@ -328,11 +326,11 @@ class GeneticSimulation(PydanticBaseModel): if ev_fast: soc_ev_per_hour[hour_idx] = ev_fast.current_soc_percentage() # save begin state if ev_charge_hours_fast[hour] > 0: - loaded_energy_ev, verluste_eauto = ev_fast.charge_energy( + loaded_energy_ev, ev_charge_losses = ev_fast.charge_energy( wh=None, hour=hour, charge_factor=ev_charge_hours_fast[hour] ) consumption += loaded_energy_ev - losses_wh_per_hour[hour_idx] += verluste_eauto + losses_wh_per_hour[hour_idx] += ev_charge_losses # Save battery SOC before inverter processing = true begin-of-interval state. # Must be recorded here (before DC charge/discharge) so the displayed SOC at @@ -342,9 +340,9 @@ class GeneticSimulation(PydanticBaseModel): soc_per_hour[hour_idx] = battery_fast.current_soc_percentage() # Process inverter logic - energy_feedin_grid_actual = energy_consumption_grid_actual = losses = eigenverbrauch = ( - 0.0 - ) + energy_feedin_grid_actual = energy_consumption_grid_actual = losses = ( + self_consumption + ) = 0.0 if inverter_fast: energy_produced = pv_prediction_wh_fast[hour] @@ -352,7 +350,7 @@ class GeneticSimulation(PydanticBaseModel): energy_feedin_grid_actual, energy_consumption_grid_actual, losses, - eigenverbrauch, + self_consumption, ) = inverter_fast.process_energy(energy_produced, consumption, hour) # AC PV Battery Charge @@ -442,7 +440,7 @@ class GeneticOptimization(OptimizationBase): ) 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 optimierung_ems. + # Populated from parameters.pv_akku.charge_rates in optimize_ems. self.bat_possible_charge_values: list[float] = [1.0] self.verbose = verbose self.fix_seed = fixed_seed @@ -585,14 +583,14 @@ class GeneticOptimization(OptimizationBase): if self.optimize_ev and eautocharge_hours_index is not None: individual.extend(eautocharge_hours_index.tolist()) elif self.optimize_ev: - # Falls optimize_ev aktiv ist, aber keine EV-Daten vorhanden sind, fügen wir Nullen hinzu + # If optimize_ev is active but no EV data is available, append zeros individual.extend([0] * self.config.prediction.hours) # Add dishwasher start time if applicable if self.opti_param.get("home_appliance", 0) > 0 and washingstart_int is not None: individual.append(washingstart_int) elif self.opti_param.get("home_appliance", 0) > 0: - # Falls ein Haushaltsgerät optimiert wird, aber kein Startzeitpunkt vorhanden ist + # If a home appliance is optimized but no start time is available individual.append(0) return individual @@ -784,7 +782,7 @@ class GeneticOptimization(OptimizationBase): # Return bad fitness score ("FitnessMin") in case of an exception return (100000.0,) - gesamtbilanz = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0) + total_balance = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0) # EV 100% & charge not allowed if self.optimize_ev: @@ -846,7 +844,7 @@ class GeneticOptimization(OptimizationBase): # # discharge_hours_bin_tail[zero_soc_mask] = ( # # len_ac + 2 # # ) # Activate discharge for these hours - # set_to_len_ac_plus_2 = np.random.rand() < 0.5 # True mit 50% Wahrscheinlichkeit + # set_to_len_ac_plus_2 = np.random.rand() < 0.5 # True with 50% probability # # Werte setzen basierend auf der zufälligen Entscheidung # value_to_set = len_ac + 2 if set_to_len_ac_plus_2 else 0 @@ -874,8 +872,8 @@ class GeneticOptimization(OptimizationBase): # (stored DC energy must pass through inverter to be usable as AC) if self.simulation.inverter: battery_energy_content *= self.simulation.inverter.dc_to_ac_efficiency - restwert_akku = battery_energy_content * parameters.ems.preis_euro_pro_wh_akku - gesamtbilanz += -restwert_akku + battery_residual_value = battery_energy_content * parameters.ems.price_per_wh_battery + total_balance += -battery_residual_value # --- AC charging break-even penalty --- # Penalise AC charging decisions that cannot be economically justified given the @@ -924,7 +922,7 @@ class GeneticOptimization(OptimizationBase): * inv.dc_to_ac_efficiency ) - # Configurable penalty multiplier (default 1 = economic loss in €) + # Configurable penalty multiplier (default 1 = economic loss in currency units) try: ac_penalty_factor = float( self.config.optimization.genetic.penalties["ac_charge_break_even"] @@ -971,7 +969,7 @@ class GeneticOptimization(OptimizationBase): dc_wh = bat.max_charge_power_w * ac_factor ac_wh = dc_wh / max(inv.ac_to_dc_efficiency, 1e-9) excess_cost_per_wh = break_even_price - best_uncovered_price - gesamtbilanz += ac_wh * excess_cost_per_wh * ac_penalty_factor + total_balance += ac_wh * excess_cost_per_wh * ac_penalty_factor if self.optimize_ev and parameters.eauto and self.simulation.ev: try: @@ -987,11 +985,11 @@ class GeneticOptimization(OptimizationBase): ev_soc_percentage < parameters.eauto.min_soc_percentage or ev_soc_percentage > parameters.eauto.max_soc_percentage ): - gesamtbilanz += ( + total_balance += ( abs(parameters.eauto.min_soc_percentage - ev_soc_percentage) * penalty ) - return (gesamtbilanz,) + return (total_balance,) def optimize( self, @@ -1000,7 +998,7 @@ class GeneticOptimization(OptimizationBase): ) -> tuple[Any, dict[str, list[Any]]]: """Run the optimization process using a genetic algorithm. - @TODO: optimize() ngen default (200) is different from optimierung_ems() ngen default (400). + @TODO: optimize() ngen default (200) is different from optimize_ems() ngen default (400). """ # Set the number of inviduals in a generation try: @@ -1047,17 +1045,17 @@ class GeneticOptimization(OptimizationBase): "min": log.select("min"), # Minimum fitness for each generation (Y-axis) } - member: dict[str, list[float]] = {"bilanz": [], "verluste": [], "nebenbedingung": []} + member: dict[str, list[float]] = {"balance": [], "losses": [], "constraints": []} for ind in population: if hasattr(ind, "extra_data"): extra_value1, extra_value2, extra_value3 = ind.extra_data - member["bilanz"].append(extra_value1) - member["verluste"].append(extra_value2) - member["nebenbedingung"].append(extra_value3) + member["balance"].append(extra_value1) + member["losses"].append(extra_value2) + member["constraints"].append(extra_value3) return hof[0], member - def optimierung_ems( + def optimize_ems( self, parameters: GeneticOptimizationParameters, start_hour: Optional[int] = None, @@ -1082,28 +1080,24 @@ class GeneticOptimization(OptimizationBase): generations = 400 logger.error("Generations not configured. Using {}.", generations) - einspeiseverguetung_euro_pro_wh = np.full( - self.config.prediction.hours, parameters.ems.einspeiseverguetung_euro_pro_wh - ) - self.simulation.reset() # Initialize PV and EV batteries - akku: Optional[Battery] = None + battery: Optional[Battery] = None if parameters.pv_akku: - akku = Battery( + battery = Battery( parameters.pv_akku, prediction_hours=self.config.prediction.hours, ) - akku.set_charge_per_hour(np.full(self.config.prediction.hours, 0)) + battery.set_charge_per_hour(np.full(self.config.prediction.hours, 0)) - eauto: Optional[Battery] = None + ev: Optional[Battery] = None if parameters.eauto: - eauto = Battery( + ev = Battery( parameters.eauto, prediction_hours=self.config.prediction.hours, ) - eauto.set_charge_per_hour(np.full(self.config.prediction.hours, 1)) + 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 ) @@ -1172,7 +1166,7 @@ class GeneticOptimization(OptimizationBase): if parameters.inverter: inverter = Inverter( parameters.inverter, - battery=akku, + battery=battery, ) # Prepare device simulation @@ -1181,7 +1175,7 @@ class GeneticOptimization(OptimizationBase): optimization_hours=self.config.optimization.horizon_hours, prediction_hours=self.config.prediction.hours, inverter=inverter, # battery is part of inverter - ev=eauto, + ev=ev, home_appliance=dishwasher, ) @@ -1240,10 +1234,10 @@ class GeneticOptimization(OptimizationBase): "dc_charge": dc_charge_hours, "discharge_allowed": discharge, "eautocharge_hours_float": eautocharge_hours_float, - "result": simulation_result, + "result": GeneticSimulationResult(**simulation_result).model_dump(), "eauto_obj": self.simulation.ev.to_dict() if self.simulation.ev else None, "start_solution": start_solution, - "spuelstart": washingstart_int, + "washingstart": washingstart_int, "extra_data": extra_data, "fitness_history": self.fitness_history, "fixed_seed": self.fix_seed, diff --git a/src/akkudoktoreos/optimization/genetic/geneticparams.py b/src/akkudoktoreos/optimization/genetic/geneticparams.py index 262746c3..4469576f 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticparams.py +++ b/src/akkudoktoreos/optimization/genetic/geneticparams.py @@ -11,7 +11,14 @@ forecasts, and fallback defaults, preparing them for optimization runs. from typing import Optional, Union from loguru import logger -from pydantic import Field, field_validator, model_validator +from pydantic import ( + AliasChoices, + ConfigDict, + Field, + computed_field, + field_validator, + model_validator, +) from typing_extensions import Self from akkudoktoreos.core.coreabc import ( @@ -37,32 +44,65 @@ from akkudoktoreos.utils.datetimeutil import to_duration class GeneticEnergyManagementParameters(GeneticParametersBaseModel): """Encapsulates energy-related forecasts and costs used in GENETIC optimization.""" - pv_prognose_wh: list[float] = Field( + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + pv_forecast_wh: list[float] = Field( + validation_alias=AliasChoices("pv_forecast_wh", "pv_prognose_wh"), json_schema_extra={ "description": "An array of floats representing the forecasted photovoltaic output in watts for different time intervals." - } + }, ) - strompreis_euro_pro_wh: list[float] = Field( + electricity_price_per_wh: list[float] = Field( + validation_alias=AliasChoices("electricity_price_per_wh", "strompreis_euro_pro_wh"), json_schema_extra={ - "description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals." - } + "description": "An array of floats representing the electricity price per watt-hour for different time intervals." + }, ) - einspeiseverguetung_euro_pro_wh: Union[list[float], float] = Field( + feed_in_tariff_per_wh: Union[list[float], float] = Field( + validation_alias=AliasChoices("feed_in_tariff_per_wh", "einspeiseverguetung_euro_pro_wh"), json_schema_extra={ - "description": "A float or array of floats representing the feed-in compensation in euros per watt-hour." - } + "description": "A float or array of floats representing the feed-in compensation per watt-hour." + }, ) - preis_euro_pro_wh_akku: float = Field( + price_per_wh_battery: float = Field( + validation_alias=AliasChoices("price_per_wh_battery", "preis_euro_pro_wh_akku"), json_schema_extra={ "description": "A float representing the cost of battery energy per watt-hour." - } + }, ) - gesamtlast: list[float] = Field( + total_load: list[float] = Field( + validation_alias=AliasChoices("total_load", "gesamtlast"), json_schema_extra={ "description": "An array of floats representing the total load (consumption) in watts for different time intervals." - } + }, ) + # Computed fields for backward compatibility (deprecated German names) + @computed_field(json_schema_extra={"deprecated": True}) + def pv_prognose_wh(self) -> list[float]: + """Deprecated: Use pv_forecast_wh instead.""" + return self.pv_forecast_wh + + @computed_field(json_schema_extra={"deprecated": True}) + def strompreis_euro_pro_wh(self) -> list[float]: + """Deprecated: Use electricity_price_per_wh instead.""" + return self.electricity_price_per_wh + + @computed_field(json_schema_extra={"deprecated": True}) + def einspeiseverguetung_euro_pro_wh(self) -> Union[list[float], float]: + """Deprecated: Use feed_in_tariff_per_wh instead.""" + return self.feed_in_tariff_per_wh + + @computed_field(json_schema_extra={"deprecated": True}) + def preis_euro_pro_wh_akku(self) -> float: + """Deprecated: Use price_per_wh_battery instead.""" + return self.price_per_wh_battery + + @computed_field(json_schema_extra={"deprecated": True}) + def gesamtlast(self) -> list[float]: + """Deprecated: Use total_load instead.""" + return self.total_load + @model_validator(mode="after") def validate_list_length(self) -> Self: """Validate that all input lists are of the same length. @@ -70,13 +110,13 @@ class GeneticEnergyManagementParameters(GeneticParametersBaseModel): Raises: ValueError: If input list lengths differ. """ - pv_prognose_length = len(self.pv_prognose_wh) + pv_forecast_length = len(self.pv_forecast_wh) if ( - pv_prognose_length != len(self.strompreis_euro_pro_wh) - or pv_prognose_length != len(self.gesamtlast) + pv_forecast_length != len(self.electricity_price_per_wh) + or pv_forecast_length != len(self.total_load) or ( - isinstance(self.einspeiseverguetung_euro_pro_wh, list) - and pv_prognose_length != len(self.einspeiseverguetung_euro_pro_wh) + isinstance(self.feed_in_tariff_per_wh, list) + and pv_forecast_length != len(self.feed_in_tariff_per_wh) ) ): raise ValueError("Input lists have different lengths") @@ -122,7 +162,7 @@ class GeneticOptimizationParameters( Raises: ValueError: If list lengths mismatch. """ - arr_length = len(self.ems.pv_prognose_wh) + arr_length = len(self.ems.pv_forecast_wh) if self.temperature_forecast is not None and arr_length != len(self.temperature_forecast): raise ValueError("Input lists have different lengths") return self @@ -615,11 +655,11 @@ class GeneticOptimizationParameters( try: oparams = GeneticOptimizationParameters( ems=GeneticEnergyManagementParameters( - pv_prognose_wh=pvforecast_ac_power, - strompreis_euro_pro_wh=elecprice_marketprice_wh, - einspeiseverguetung_euro_pro_wh=feed_in_tariff_wh, - gesamtlast=loadforecast_power_w, - preis_euro_pro_wh_akku=battery_lcos_kwh / 1000, + pv_forecast_wh=pvforecast_ac_power, + electricity_price_per_wh=elecprice_marketprice_wh, + feed_in_tariff_per_wh=feed_in_tariff_wh, + total_load=loadforecast_power_w, + price_per_wh_battery=battery_lcos_kwh / 1000, ), temperature_forecast=weather_temp_air, pv_akku=battery_params, diff --git a/src/akkudoktoreos/optimization/genetic/geneticsolution.py b/src/akkudoktoreos/optimization/genetic/geneticsolution.py index 50a03ddb..a30ad53f 100644 --- a/src/akkudoktoreos/optimization/genetic/geneticsolution.py +++ b/src/akkudoktoreos/optimization/genetic/geneticsolution.py @@ -5,7 +5,7 @@ from typing import Any, Optional import numpy as np import pandas as pd from loguru import logger -from pydantic import Field, field_validator +from pydantic import AliasChoices, ConfigDict, Field, computed_field, field_validator from akkudoktoreos.core.coreabc import ( ConfigMixin, @@ -86,62 +86,153 @@ class ElectricVehicleResult(DeviceOptimizeResult): class GeneticSimulationResult(GeneticParametersBaseModel): """This object contains the results of the simulation and provides insights into various parameters over the entire forecast period.""" - Last_Wh_pro_Stunde: list[float] = Field(json_schema_extra={"description": "TBD"}) - EAuto_SoC_pro_Stunde: list[float] = Field( - json_schema_extra={"description": "The state of charge of the EV for each hour."} + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + load_wh_per_hour: list[float] = Field( + validation_alias=AliasChoices("load_wh_per_hour", "Last_Wh_pro_Stunde"), + json_schema_extra={"description": "The load in watt-hours per hour."}, ) - Einnahmen_Euro_pro_Stunde: list[float] = Field( + ev_soc_per_hour: list[float] = Field( + validation_alias=AliasChoices("ev_soc_per_hour", "EAuto_SoC_pro_Stunde"), + json_schema_extra={"description": "The state of charge of the EV for each hour."}, + ) + revenue_per_hour: list[float] = Field( + validation_alias=AliasChoices("revenue_per_hour", "Einnahmen_Euro_pro_Stunde"), json_schema_extra={ - "description": "The revenue from grid feed-in or other sources in euros per hour." - } + "description": "The revenue from grid feed-in or other sources per hour." + }, ) - Gesamt_Verluste: float = Field( - json_schema_extra={"description": "The total losses in watt-hours over the entire period."} + total_losses: float = Field( + validation_alias=AliasChoices("total_losses", "Gesamt_Verluste"), + json_schema_extra={"description": "The total losses in watt-hours over the entire period."}, ) - Gesamtbilanz_Euro: float = Field( - json_schema_extra={"description": "The total balance of revenues minus costs in euros."} + total_balance: float = Field( + validation_alias=AliasChoices("total_balance", "Gesamtbilanz_Euro"), + json_schema_extra={"description": "The total balance of revenues minus costs."}, ) - Gesamteinnahmen_Euro: float = Field( - json_schema_extra={"description": "The total revenues in euros."} + total_revenue: float = Field( + validation_alias=AliasChoices("total_revenue", "Gesamteinnahmen_Euro"), + json_schema_extra={"description": "The total revenues."}, ) - Gesamtkosten_Euro: float = Field(json_schema_extra={"description": "The total costs in euros."}) - Home_appliance_wh_per_hour: list[Optional[float]] = Field( + total_costs: float = Field( + validation_alias=AliasChoices("total_costs", "Gesamtkosten_Euro"), + json_schema_extra={"description": "The total costs."}, + ) + home_appliance_wh_per_hour: list[Optional[float]] = Field( + validation_alias=AliasChoices("home_appliance_wh_per_hour", "Home_appliance_wh_per_hour"), json_schema_extra={ "description": "The energy consumption of a household appliance in watt-hours per hour." - } + }, ) - Kosten_Euro_pro_Stunde: list[float] = Field( - json_schema_extra={"description": "The costs in euros per hour."} + costs_per_hour: list[float] = Field( + validation_alias=AliasChoices("costs_per_hour", "Kosten_Euro_pro_Stunde"), + json_schema_extra={"description": "The costs per hour."}, ) - Netzbezug_Wh_pro_Stunde: list[float] = Field( - json_schema_extra={"description": "The grid energy drawn in watt-hours per hour."} + grid_consumption_wh_per_hour: list[float] = Field( + validation_alias=AliasChoices("grid_consumption_wh_per_hour", "Netzbezug_Wh_pro_Stunde"), + json_schema_extra={"description": "The grid energy drawn in watt-hours per hour."}, ) - Netzeinspeisung_Wh_pro_Stunde: list[float] = Field( - json_schema_extra={"description": "The energy fed into the grid in watt-hours per hour."} + grid_feed_in_wh_per_hour: list[float] = Field( + validation_alias=AliasChoices("grid_feed_in_wh_per_hour", "Netzeinspeisung_Wh_pro_Stunde"), + json_schema_extra={"description": "The energy fed into the grid in watt-hours per hour."}, ) - Verluste_Pro_Stunde: list[float] = Field( - json_schema_extra={"description": "The losses in watt-hours per hour."} + losses_per_hour: list[float] = Field( + validation_alias=AliasChoices("losses_per_hour", "Verluste_Pro_Stunde"), + json_schema_extra={"description": "The losses in watt-hours per hour."}, ) - akku_soc_pro_stunde: list[float] = Field( + battery_soc_per_hour: list[float] = Field( + validation_alias=AliasChoices("battery_soc_per_hour", "akku_soc_pro_stunde"), json_schema_extra={ "description": "The state of charge of the battery (not the EV) in percentage per hour." - } + }, ) - Electricity_price: list[float] = Field( - json_schema_extra={"description": "Used Electricity Price, including predictions"} + electricity_price: list[float] = Field( + validation_alias=AliasChoices("electricity_price", "Electricity_price"), + json_schema_extra={"description": "Used Electricity Price, including predictions"}, ) + # Computed fields for backward compatibility (deprecated German names) + @computed_field(json_schema_extra={"deprecated": True}) + def Last_Wh_pro_Stunde(self) -> list[float]: + """Deprecated: Use load_wh_per_hour instead.""" + return self.load_wh_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def EAuto_SoC_pro_Stunde(self) -> list[float]: + """Deprecated: Use ev_soc_per_hour instead.""" + return self.ev_soc_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Einnahmen_Euro_pro_Stunde(self) -> list[float]: + """Deprecated: Use revenue_per_hour instead.""" + return self.revenue_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Gesamt_Verluste(self) -> float: + """Deprecated: Use total_losses instead.""" + return self.total_losses + + @computed_field(json_schema_extra={"deprecated": True}) + def Gesamtbilanz_Euro(self) -> float: + """Deprecated: Use total_balance instead.""" + return self.total_balance + + @computed_field(json_schema_extra={"deprecated": True}) + def Gesamteinnahmen_Euro(self) -> float: + """Deprecated: Use total_revenue instead.""" + return self.total_revenue + + @computed_field(json_schema_extra={"deprecated": True}) + def Gesamtkosten_Euro(self) -> float: + """Deprecated: Use total_costs instead.""" + return self.total_costs + + @computed_field(json_schema_extra={"deprecated": True}) + def Home_appliance_wh_per_hour(self) -> list[Optional[float]]: + """Deprecated: Use home_appliance_wh_per_hour instead.""" + return self.home_appliance_wh_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Kosten_Euro_pro_Stunde(self) -> list[float]: + """Deprecated: Use costs_per_hour instead.""" + return self.costs_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Netzbezug_Wh_pro_Stunde(self) -> list[float]: + """Deprecated: Use grid_consumption_wh_per_hour instead.""" + return self.grid_consumption_wh_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Netzeinspeisung_Wh_pro_Stunde(self) -> list[float]: + """Deprecated: Use grid_feed_in_wh_per_hour instead.""" + return self.grid_feed_in_wh_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Verluste_Pro_Stunde(self) -> list[float]: + """Deprecated: Use losses_per_hour instead.""" + return self.losses_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def akku_soc_pro_stunde(self) -> list[float]: + """Deprecated: Use battery_soc_per_hour instead.""" + return self.battery_soc_per_hour + + @computed_field(json_schema_extra={"deprecated": True}) + def Electricity_price(self) -> list[float]: + """Deprecated: Use electricity_price instead.""" + return self.electricity_price + @field_validator( - "Last_Wh_pro_Stunde", - "Netzeinspeisung_Wh_pro_Stunde", - "akku_soc_pro_stunde", - "Netzbezug_Wh_pro_Stunde", - "Kosten_Euro_pro_Stunde", - "Einnahmen_Euro_pro_Stunde", - "EAuto_SoC_pro_Stunde", - "Verluste_Pro_Stunde", - "Home_appliance_wh_per_hour", - "Electricity_price", + "load_wh_per_hour", + "grid_feed_in_wh_per_hour", + "battery_soc_per_hour", + "grid_consumption_wh_per_hour", + "costs_per_hour", + "revenue_per_hour", + "ev_soc_per_hour", + "losses_per_hour", + "home_appliance_wh_per_hour", + "electricity_price", mode="before", ) def convert_numpy(cls, field: Any) -> Any: @@ -149,7 +240,7 @@ class GeneticSimulationResult(GeneticParametersBaseModel): class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): - """**Note**: The first value of "Last_Wh_per_hour", "Netzeinspeisung_Wh_per_hour", and "Netzbezug_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.""" + """**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.""" ac_charge: list[float] = Field( json_schema_extra={ @@ -363,7 +454,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): # --- Create index based on list length and interval --- # Ensure we only use the minimum of results and commands if differing - periods = min(len(self.result.Kosten_Euro_pro_Stunde), len(self.ac_charge) - start_day_hour) + periods = min(len(self.result.costs_per_hour), len(self.ac_charge) - start_day_hour) time_index = pd.date_range( start=start_datetime, periods=periods, @@ -387,12 +478,12 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): { "date_time": time_index, # result starts at start_day_hour - "load_energy_wh": self.result.Last_Wh_pro_Stunde[:n_points], - "grid_feedin_energy_wh": self.result.Netzeinspeisung_Wh_pro_Stunde[:n_points], - "grid_consumption_energy_wh": self.result.Netzbezug_Wh_pro_Stunde[:n_points], - "costs_amt": self.result.Kosten_Euro_pro_Stunde[:n_points], - "revenue_amt": self.result.Einnahmen_Euro_pro_Stunde[:n_points], - "losses_energy_wh": self.result.Verluste_Pro_Stunde[:n_points], + "load_energy_wh": self.result.load_wh_per_hour[:n_points], + "grid_feedin_energy_wh": self.result.grid_feed_in_wh_per_hour[:n_points], + "grid_consumption_energy_wh": self.result.grid_consumption_wh_per_hour[:n_points], + "costs_amt": self.result.costs_per_hour[:n_points], + "revenue_amt": self.result.revenue_per_hour[:n_points], + "losses_energy_wh": self.result.losses_per_hour[:n_points], }, index=time_index, ) @@ -401,7 +492,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): battery_device_id = self._battery_device_id() solution[f"{battery_device_id}_soc_factor"] = [ v / 100 - for v in self.result.akku_soc_pro_stunde[:n_points] # result starts at start_day_hour + for v in self.result.battery_soc_per_hour[:n_points] # result starts at start_day_hour ] operation: dict[str, list[float]] = { "genetic_ac_charge_factor": [], @@ -427,8 +518,8 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): # this hour given the expected battery state of charge. result_idx = hour_idx - start_day_hour soc_h_pct = ( - self.result.akku_soc_pro_stunde[result_idx] - if result_idx < len(self.result.akku_soc_pro_stunde) + self.result.battery_soc_per_hour[result_idx] + if result_idx < len(self.result.battery_soc_per_hour) else 0.0 ) eff_ac, eff_dc, eff_dis = self._soc_clamped_operation_factors( @@ -458,7 +549,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): # Add EV battery solution # eautocharge_hours_float start at hour 0 of start day - # result.EAuto_SoC_pro_Stunde start at start_datetime.hour + # result.ev_soc_per_hour start at start_datetime.hour if self.eauto_obj: ev_device_id = self._ev_device_id() if self.eautocharge_hours_float is None: @@ -480,7 +571,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): solution[factor_key] = [0.0] * n_points else: solution[f"{ev_device_id}_soc_factor"] = [ - v / 100 for v in self.result.EAuto_SoC_pro_Stunde[:n_points] + v / 100 for v in self.result.ev_soc_per_hour[:n_points] ] operation = { "genetic_ev_charge_factor": [], @@ -520,7 +611,7 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): homeappliance_device_id = self._homeappliance_device_id() # result starts at start_day_hour solution[f"{homeappliance_device_id}_energy_wh"] = ( - self.result.Home_appliance_wh_per_hour[:n_points] + self.result.home_appliance_wh_per_hour[:n_points] ) operation = { f"{homeappliance_device_id}_run_op_mode": [], @@ -629,11 +720,11 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): comment="Optimization solution derived from GeneticSolution.", valid_from=start_datetime, valid_until=start_datetime.add(hours=self.config.optimization.horizon_hours), - total_losses_energy_wh=self.result.Gesamt_Verluste, - total_revenues_amt=self.result.Gesamteinnahmen_Euro, - total_costs_amt=self.result.Gesamtkosten_Euro, + total_losses_energy_wh=self.result.total_losses, + total_revenues_amt=self.result.total_revenue, + total_costs_amt=self.result.total_costs, fitness_score={ - self.result.Gesamtkosten_Euro, + self.result.total_costs, }, prediction=PydanticDateTimeDataFrame.from_dataframe(prediction), solution=PydanticDateTimeDataFrame.from_dataframe(solution), @@ -667,8 +758,8 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): # dataframe (genetic_*_factor columns). result_idx = hour_idx - start_day_hour soc_h_pct = ( - self.result.akku_soc_pro_stunde[result_idx] - if result_idx < len(self.result.akku_soc_pro_stunde) + self.result.battery_soc_per_hour[result_idx] + if result_idx < len(self.result.battery_soc_per_hour) else 0.0 ) eff_ac, eff_dc, eff_dis = self._soc_clamped_operation_factors( @@ -752,11 +843,11 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel): # even if configured to be started. resource_id = self._homeappliance_device_id() last_energy: Optional[float] = None - for hours, energy in enumerate(self.result.Home_appliance_wh_per_hour): + for hours, energy in enumerate(self.result.home_appliance_wh_per_hour): # hours starts at start_datetime with 0 if energy is None: raise ValueError( - f"Unexpected value {energy} in {self.result.Home_appliance_wh_per_hour}" + f"Unexpected value {energy} in {self.result.home_appliance_wh_per_hour}" ) if last_energy is None or energy != last_energy: if energy > 0.0: diff --git a/src/akkudoktoreos/prediction/elecprice.py b/src/akkudoktoreos/prediction/elecprice.py index cd6f7eb0..e67bdb91 100644 --- a/src/akkudoktoreos/prediction/elecprice.py +++ b/src/akkudoktoreos/prediction/elecprice.py @@ -43,7 +43,7 @@ class ElecPriceCommonSettings(SettingsBaseModel): default=None, ge=0, json_schema_extra={ - "description": "Electricity price charges [€/kWh]. Will be added to variable market price.", + "description": "Electricity price charges [amount/kWh]. Will be added to variable market price.", "examples": [0.21], }, ) diff --git a/src/akkudoktoreos/prediction/elecpriceabc.py b/src/akkudoktoreos/prediction/elecpriceabc.py index 5a723136..b4698291 100644 --- a/src/akkudoktoreos/prediction/elecpriceabc.py +++ b/src/akkudoktoreos/prediction/elecpriceabc.py @@ -21,14 +21,14 @@ class ElecPriceDataRecord(PredictionRecord): """ elecprice_marketprice_wh: Optional[float] = Field( - None, json_schema_extra={"description": "Electricity market price per Wh (€/Wh)"} + None, json_schema_extra={"description": "Electricity market price per Wh [amount/Wh]"} ) # Computed fields @computed_field # type: ignore[prop-decorator] @property def elecprice_marketprice_kwh(self) -> Optional[float]: - """Electricity market price per kWh (€/kWh). + """Electricity market price per kWh [amount/kWh]. Convenience attribute calculated from `elecprice_marketprice_wh`. """ diff --git a/src/akkudoktoreos/prediction/feedintariffabc.py b/src/akkudoktoreos/prediction/feedintariffabc.py index 85ef487b..22cfd2ce 100644 --- a/src/akkudoktoreos/prediction/feedintariffabc.py +++ b/src/akkudoktoreos/prediction/feedintariffabc.py @@ -21,14 +21,14 @@ class FeedInTariffDataRecord(PredictionRecord): """ feed_in_tariff_wh: Optional[float] = Field( - None, json_schema_extra={"description": "Feed in tariff per Wh (€/Wh)"} + None, json_schema_extra={"description": "Feed in tariff per Wh [amount/Wh]"} ) # Computed fields @computed_field # type: ignore[prop-decorator] @property def feed_in_tariff_kwh(self) -> Optional[float]: - """Feed in tariff per kWh (€/kWh). + """Feed in tariff per kWh [amount/kWh]. Convenience attribute calculated from `feed_in_tariff_wh`. """ diff --git a/src/akkudoktoreos/prediction/feedintarifffixed.py b/src/akkudoktoreos/prediction/feedintarifffixed.py index 2d7bd88a..eab525b4 100644 --- a/src/akkudoktoreos/prediction/feedintarifffixed.py +++ b/src/akkudoktoreos/prediction/feedintarifffixed.py @@ -17,7 +17,7 @@ class FeedInTariffFixedCommonSettings(SettingsBaseModel): default=None, ge=0, json_schema_extra={ - "description": "Electricity price feed in tariff [€/kWH].", + "description": "Electricity price feed in tariff [amount/kWh].", "examples": [0.078], }, ) diff --git a/src/akkudoktoreos/server/dash/plan.py b/src/akkudoktoreos/server/dash/plan.py index 50efad58..bd11b11b 100644 --- a/src/akkudoktoreos/server/dash/plan.py +++ b/src/akkudoktoreos/server/dash/plan.py @@ -265,11 +265,11 @@ def SolutionCard(solution: OptimizationSolution, config: SettingsEOS, data: Opti y2_axis = LinearAxis(y_range_name="factor", axis_label="Factor [0.0..1.0]") plot.add_layout(y2_axis, "left") # y3 axis - y3_axis = LinearAxis(y_range_name="amt_kwh", axis_label="Electricty Price [currency/kWh]") + y3_axis = LinearAxis(y_range_name="amt_kwh", axis_label="Electricity Price [amount/kWh]") y3_axis.axis_label_text_color = "red" plot.add_layout(y3_axis, "right") # y4 axis - y4_axis = LinearAxis(y_range_name="amt", axis_label="Amount [currency]") + y4_axis = LinearAxis(y_range_name="amt", axis_label="Amount") plot.add_layout(y4_axis, "right") plot.toolbar.autohide = True diff --git a/src/akkudoktoreos/server/dash/prediction.py b/src/akkudoktoreos/server/dash/prediction.py index efb86273..cfc5dcc4 100644 --- a/src/akkudoktoreos/server/dash/prediction.py +++ b/src/akkudoktoreos/server/dash/prediction.py @@ -54,7 +54,7 @@ def ElectricityPriceForecast( ), title=f"Electricity Price Prediction ({provider})", x_axis_label=f"Datetime [localtime {date_time_tz}]", - y_axis_label="Price [€/kWh]", + y_axis_label="Price [amount/kWh]", sizing_mode="stretch_width", height=400, ) diff --git a/src/akkudoktoreos/server/eos.py b/src/akkudoktoreos/server/eos.py index aefdbc55..6a1b416c 100755 --- a/src/akkudoktoreos/server/eos.py +++ b/src/akkudoktoreos/server/eos.py @@ -204,7 +204,20 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app = FastAPI( title="Akkudoktor-EOS", - 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.", + 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. + +## Currency Information + +All monetary values in this API are expressed in the locally configured currency of the EOS installation. The system is designed to work with any currency (e.g., EUR, SEK, CHF, USD, GBP, etc.). Each installation uses a single, consistent currency throughout all endpoints and responses. + +Values are given in whole currency units, not in hundredth subunits: a value of `0.0003` in an installation configured for Euro means 0.0003 EUR/Wh (i.e. 0.30 EUR/kWh), never cents. The same applies to all totals (e.g. `total_costs` of `1.5` = 1.50 EUR). + +Field names containing cost, price, revenue, tariff, or similar monetary terms (e.g., `total_costs`, `electricity_price_per_wh`, `revenue_per_hour`) represent amounts in the configured currency, without explicit currency designation in the field name to maintain currency-neutrality. + +## Deprecated Field Names + +The genetic optimization API fields were renamed from German to English. For backward compatibility the old German field names (e.g. `gesamtlast`, `pv_prognose_wh`, `Gesamtbilanz_Euro`) are still accepted on input and are emitted in responses alongside the English names, marked as deprecated in the schema. They will be removed in a future release — new clients should use the English field names only. +""", summary="Comprehensive solution for simulating and optimizing an energy system based on renewable energy sources", version=f"v{__version__}", license_info={ @@ -1160,7 +1173,7 @@ def fastapi_energy_management_plan_get() -> EnergyManagementPlan: @app.get("/strompreis", tags=["prediction"]) async def fastapi_strompreis() -> list[float]: - """Deprecated: Electricity Market Price Prediction per Wh (€/Wh). + """Deprecated: Electricity Market Price Prediction per Wh [amount/Wh]. Electricity prices start at 00.00.00 today and are provided for 48 hours. If no prices are available the missing ones at the start of the series are diff --git a/src/akkudoktoreos/utils/visualize.py b/src/akkudoktoreos/utils/visualize.py index b2b481f8..61cecb71 100644 --- a/src/akkudoktoreos/utils/visualize.py +++ b/src/akkudoktoreos/utils/visualize.py @@ -448,7 +448,7 @@ def prepare_visualize( report.create_line_chart_date( next_full_hour_date, [ - parameters.ems.gesamtlast[start_hour:], + parameters.ems.total_load[start_hour:], ], title="Load Profile", # xlabel="Hours", # not enough space @@ -458,7 +458,7 @@ def prepare_visualize( report.create_line_chart_date( next_full_hour_date, [ - parameters.ems.pv_prognose_wh[start_hour:], + parameters.ems.pv_forecast_wh[start_hour:], ], title="PV Forecast", # xlabel="Hours", # not enough space @@ -469,15 +469,15 @@ def prepare_visualize( next_full_hour_date, [ np.full( - len(parameters.ems.gesamtlast) - start_hour, - parameters.ems.einspeiseverguetung_euro_pro_wh[start_hour:] - if isinstance(parameters.ems.einspeiseverguetung_euro_pro_wh, list) - else parameters.ems.einspeiseverguetung_euro_pro_wh, + len(parameters.ems.total_load) - start_hour, + parameters.ems.feed_in_tariff_per_wh[start_hour:] + if isinstance(parameters.ems.feed_in_tariff_per_wh, list) + else parameters.ems.feed_in_tariff_per_wh, ) ], title="Remuneration", # xlabel="Hours", # not enough space - ylabel="€/Wh", + ylabel="amount/Wh", x2label=None, # not enough space ) if parameters.temperature_forecast: @@ -497,11 +497,11 @@ def prepare_visualize( report.create_line_chart_date( next_full_hour_date, # start_date [ - results["result"]["Last_Wh_pro_Stunde"], - results["result"]["Home_appliance_wh_per_hour"], - results["result"]["Netzeinspeisung_Wh_pro_Stunde"], - results["result"]["Netzbezug_Wh_pro_Stunde"], - results["result"]["Verluste_Pro_Stunde"], + results["result"]["load_wh_per_hour"], + results["result"]["home_appliance_wh_per_hour"], + results["result"]["grid_feed_in_wh_per_hour"], + results["result"]["grid_consumption_wh_per_hour"], + results["result"]["losses_per_hour"], ], title="Energy Flow per Hour", # xlabel="Date", # not enough space @@ -521,7 +521,7 @@ def prepare_visualize( # Group 3: report.create_line_chart_date( next_full_hour_date, # start_date - [results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]], + [results["result"]["battery_soc_per_hour"], results["result"]["ev_soc_per_hour"]], title="Battery SOC", # xlabel="Date", # not enough space ylabel="%", @@ -533,10 +533,10 @@ def prepare_visualize( ) report.create_line_chart_date( next_full_hour_date, # start_date - [parameters.ems.strompreis_euro_pro_wh[start_hour:]], + [parameters.ems.electricity_price_per_wh[start_hour:]], # title="Electricity Price", # not enough space # xlabel="Date", # not enough space - ylabel="Electricity Price (€/Wh)", + ylabel="Electricity Price (amount/Wh)", x2label=None, # not enough space ) @@ -570,50 +570,50 @@ def prepare_visualize( report.create_line_chart_date( next_full_hour_date, # start_date [ - results["result"]["Kosten_Euro_pro_Stunde"], - results["result"]["Einnahmen_Euro_pro_Stunde"], + results["result"]["costs_per_hour"], + results["result"]["revenue_per_hour"], ], title="Financial Balance per Hour", # xlabel="Date", # not enough space - ylabel="Euro", + ylabel="Amount", labels=["Costs", "Revenue"], ) extra_data = results["extra_data"] report.create_scatter_plot( - extra_data["verluste"], - extra_data["bilanz"], + extra_data["losses"], + extra_data["balance"], title="Scatter Plot", xlabel="losses", ylabel="balance", - c=extra_data["nebenbedingung"], + c=extra_data["constraints"], ) values_list = [ [ - results["result"]["Gesamtkosten_Euro"], - results["result"]["Gesamteinnahmen_Euro"], - results["result"]["Gesamtbilanz_Euro"], + results["result"]["total_costs"], + results["result"]["total_revenue"], + results["result"]["total_balance"], ] ] - labels = ["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"] + labels = ["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"] report.create_bar_chart( labels=labels, values_list=values_list, title="Financial Overview", - ylabel="Euro", - xlabels=["Total Costs [€]", "Total Revenue [€]", "Total Balance [€]"], + ylabel="Amount", + xlabels=["Total Costs [amount]", "Total Revenue [amount]", "Total Balance [amount]"], ) report.finalize_group() # Group 1: Scatter plot of losses vs balance with color-coded constraints - f1 = np.array(extra_data["verluste"]) # Losses - f2 = np.array(extra_data["bilanz"]) # Balance - n1 = np.array(extra_data["nebenbedingung"]) # Constraints + f1 = np.array(extra_data["losses"]) # Losses + f2 = np.array(extra_data["balance"]) # Balance + n1 = np.array(extra_data["constraints"]) # Constraints - # Filter data where 'nebenbedingung' < 0.01 + # Filter data where 'constraints' < 0.01 filtered_indices = n1 < 0.01 filtered_losses = f1[filtered_indices] filtered_balance = f2[filtered_indices] diff --git a/tests/single_test_optimization.py b/tests/single_test_optimization.py index b384002c..007daf1a 100755 --- a/tests/single_test_optimization.py +++ b/tests/single_test_optimization.py @@ -148,22 +148,22 @@ async def prepare_optimization_real_parameters() -> GeneticOptimizationParameter ) print(f"temperature_forecast: {temperature_forecast}") - # Electricity Price (in Euro per Wh) - strompreis_euro_pro_wh = await prediction_eos.key_to_array( + # Electricity price (per Wh) + electricity_price_per_wh = await prediction_eos.key_to_array( key="elecprice_marketprice_wh", start_datetime=prediction_eos.ems_start_datetime, end_datetime=prediction_eos.end_datetime, fill_method="ffill", ) - print(f"strompreis_euro_pro_wh: {strompreis_euro_pro_wh}") + print(f"electricity_price_per_wh: {electricity_price_per_wh}") # Overall System Load (in W) - gesamtlast = await prediction_eos.key_to_array( + total_load = await prediction_eos.key_to_array( key="load_mean", start_datetime=prediction_eos.ems_start_datetime, end_datetime=prediction_eos.end_datetime, ) - print(f"gesamtlast: {gesamtlast}") + print(f"total_load: {total_load}") # Start Solution (binary) start_solution = None @@ -173,11 +173,11 @@ async def prepare_optimization_real_parameters() -> GeneticOptimizationParameter return GeneticOptimizationParameters( **{ "ems": { - "preis_euro_pro_wh_akku": 0e-05, - "einspeiseverguetung_euro_pro_wh": 7e-05, - "gesamtlast": gesamtlast, - "pv_prognose_wh": pv_forecast, - "strompreis_euro_pro_wh": strompreis_euro_pro_wh, + "price_per_wh_battery": 0e-05, + "feed_in_tariff_per_wh": 7e-05, + "total_load": total_load, + "pv_forecast_wh": pv_forecast, + "electricity_price_per_wh": electricity_price_per_wh, }, "pv_akku": { "device_id": "battery 1", @@ -304,14 +304,14 @@ def prepare_optimization_parameters() -> GeneticOptimizationParameters: 17.4, ] - # Electricity Price (in Euro per Wh) - strompreis_euro_pro_wh = np.full(48, 0.001) - strompreis_euro_pro_wh[0:10] = 0.00001 - strompreis_euro_pro_wh[11:15] = 0.00005 - strompreis_euro_pro_wh[20] = 0.00001 + # Electricity price (per Wh) + electricity_price_per_wh = np.full(48, 0.001) + electricity_price_per_wh[0:10] = 0.00001 + electricity_price_per_wh[11:15] = 0.00005 + electricity_price_per_wh[20] = 0.00001 # Overall System Load (in W) - gesamtlast = [ + total_load = [ 676.71, 876.19, 527.13, @@ -369,11 +369,11 @@ def prepare_optimization_parameters() -> GeneticOptimizationParameters: return GeneticOptimizationParameters( **{ "ems": { - "preis_euro_pro_wh_akku": 0e-05, - "einspeiseverguetung_euro_pro_wh": 7e-05, - "gesamtlast": gesamtlast, - "pv_prognose_wh": pv_forecast, - "strompreis_euro_pro_wh": strompreis_euro_pro_wh, + "price_per_wh_battery": 0e-05, + "feed_in_tariff_per_wh": 7e-05, + "total_load": total_load, + "pv_forecast_wh": pv_forecast, + "electricity_price_per_wh": electricity_price_per_wh, }, "pv_akku": { "device_id": "battery 1", diff --git a/tests/test_geneticoptimize.py b/tests/test_geneticoptimize.py index 8797f6ba..3a140fae 100644 --- a/tests/test_geneticoptimize.py +++ b/tests/test_geneticoptimize.py @@ -56,7 +56,7 @@ def test_optimize( config_eos: ConfigEOS, is_finalize: bool, ): - """Test optimierung_ems.""" + """Test optimize_ems.""" # Test parameters fixed_start_hour = 10 fixed_seed = 42 @@ -125,7 +125,7 @@ def test_optimize( ), ) as prepare_visualize_patch: # Call the optimization function - genetic_solution = genetic_optimization.optimierung_ems( + genetic_solution = genetic_optimization.optimize_ems( parameters=input_data, start_hour=fixed_start_hour, ngen=ngen ) # The function creates a visualization result PDF as a side-effect. diff --git a/tests/test_inverter_efficiency.py b/tests/test_inverter_efficiency.py index c13eaccc..582130b0 100644 --- a/tests/test_inverter_efficiency.py +++ b/tests/test_inverter_efficiency.py @@ -582,7 +582,7 @@ def _run_evaluate_with_mocked_sim( fitness = optim.evaluate( fake_individual, parameters=Mock( - ems=Mock(preis_euro_pro_wh_akku=0.0), + ems=Mock(price_per_wh_battery=0.0), eauto=None, ), start_hour=start_hour, diff --git a/tests/test_translations.py b/tests/test_translations.py new file mode 100644 index 00000000..f80c8c9c --- /dev/null +++ b/tests/test_translations.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Quick test to verify API field name translations work correctly.""" + +import json + +from akkudoktoreos.optimization.genetic.geneticparams import ( + GeneticEnergyManagementParameters, +) +from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSimulationResult + + +def test_genetic_params_german_input(): + """Test that German field names are accepted as input.""" + data_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 = GeneticEnergyManagementParameters(**data_de) + assert params.pv_forecast_wh == [100.0, 200.0] + print("✅ German input accepted") + +def test_genetic_params_english_input(): + """Test that English field names are accepted as input.""" + data_en = { + "pv_forecast_wh": [100.0, 200.0], + "electricity_price_per_wh": [0.0003, 0.0003], + "feed_in_tariff_per_wh": 0.00007, + "price_per_wh_battery": 0.0001, + "total_load": [500.0, 600.0], + } + params = GeneticEnergyManagementParameters(**data_en) + assert params.pv_forecast_wh == [100.0, 200.0] + print("✅ English input accepted") + +def test_genetic_params_english_output(): + """Test that both English and German field names are in JSON output (backward compatibility).""" + data_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 = GeneticEnergyManagementParameters(**data_de) + json_output = json.loads(params.model_dump_json(by_alias=True)) + + # English names should be in output + assert "pv_forecast_wh" in json_output + assert "electricity_price_per_wh" in json_output + assert "feed_in_tariff_per_wh" in json_output + assert "price_per_wh_battery" in json_output + assert "total_load" in json_output + + # German names should ALSO be in output (backward compatibility) + assert "pv_prognose_wh" in json_output + assert "strompreis_euro_pro_wh" in json_output + assert "einspeiseverguetung_euro_pro_wh" in json_output + assert "preis_euro_pro_wh_akku" in json_output + assert "gesamtlast" in json_output + + # Both should have same values + assert json_output["pv_forecast_wh"] == json_output["pv_prognose_wh"] + assert json_output["electricity_price_per_wh"] == json_output["strompreis_euro_pro_wh"] + print("✅ Both English and German output generated") + +def test_simulation_result_translations(): + """Test simulation result field translations.""" + data_de = { + "Last_Wh_pro_Stunde": [100.0, 200.0], + "EAuto_SoC_pro_Stunde": [50.0, 60.0], + "Einnahmen_Euro_pro_Stunde": [0.1, 0.2], + "Gesamt_Verluste": 50.0, + "Gesamtbilanz_Euro": -10.0, + "Gesamteinnahmen_Euro": 5.0, + "Gesamtkosten_Euro": 15.0, + "Home_appliance_wh_per_hour": [0.0, 100.0], + "Kosten_Euro_pro_Stunde": [0.2, 0.3], + "Netzbezug_Wh_pro_Stunde": [50.0, 60.0], + "Netzeinspeisung_Wh_pro_Stunde": [10.0, 20.0], + "Verluste_Pro_Stunde": [5.0, 10.0], + "akku_soc_pro_stunde": [80.0, 90.0], + "Electricity_price": [0.0003, 0.0003], + } + result = GeneticSimulationResult(**data_de) + json_output = json.loads(result.model_dump_json(by_alias=True)) + + # Check English field names in output + english_names = [ + "load_wh_per_hour", + "ev_soc_per_hour", + "revenue_per_hour", + "total_losses", + "total_balance", + "total_revenue", + "total_costs", + "home_appliance_wh_per_hour", + "costs_per_hour", + "grid_consumption_wh_per_hour", + "grid_feed_in_wh_per_hour", + "losses_per_hour", + "battery_soc_per_hour", + "electricity_price", + ] + for name in english_names: + assert name in json_output, f"English field {name} missing in output" + + # Check ALL deprecated German field names are still in output (backward compatibility) + for name in data_de: + assert name in json_output, f"Deprecated field {name} missing in output" + + # Deprecated fields must mirror the values of the renamed fields + assert json_output["Last_Wh_pro_Stunde"] == json_output["load_wh_per_hour"] + assert json_output["Home_appliance_wh_per_hour"] == json_output["home_appliance_wh_per_hour"] + assert json_output["Gesamtbilanz_Euro"] == json_output["total_balance"] + assert json_output["akku_soc_pro_stunde"] == json_output["battery_soc_per_hour"] + assert json_output["Electricity_price"] == json_output["electricity_price"] + + print("✅ Simulation result translations work") + +if __name__ == "__main__": + test_genetic_params_german_input() + test_genetic_params_english_input() + test_genetic_params_english_output() + test_simulation_result_translations() + print("\n✅✅✅ All translation tests passed! ✅✅✅") diff --git a/tests/testdata/docs/_generated/configdevices.md b/tests/testdata/docs/_generated/configdevices.md index 38e3d691..5891a0c1 100644 --- a/tests/testdata/docs/_generated/configdevices.md +++ b/tests/testdata/docs/_generated/configdevices.md @@ -456,7 +456,7 @@ as a cohesive unit for scheduling and availability checking. | charging_efficiency | `float` | `rw` | `0.88` | Charging efficiency [0.01 ... 1.00]. | | device_id | `str` | `rw` | `` | ID of device | | discharging_efficiency | `float` | `rw` | `0.88` | Discharge efficiency [0.01 ... 1.00]. | -| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [€/kWh]. | +| levelized_cost_of_storage_kwh | `float` | `rw` | `0.0` | Levelized cost of storage (LCOS), the average lifetime cost of delivering one kWh [amount/kWh]. | | max_charge_power_w | `Optional[float]` | `rw` | `5000` | Maximum charging power [W]. | | max_soc_percentage | `int` | `rw` | `100` | Maximum state of charge (SOC) as percentage of capacity [%]. | | measurement_key_power_3_phase_sym_w | `str` | `ro` | `N/A` | Measurement key for the symmetric 3 phase power the battery is charged or discharged with [W]. | diff --git a/tests/testdata/docs/_generated/configelecprice.md b/tests/testdata/docs/_generated/configelecprice.md index 4a3e0ef4..f2c3f544 100644 --- a/tests/testdata/docs/_generated/configelecprice.md +++ b/tests/testdata/docs/_generated/configelecprice.md @@ -7,7 +7,7 @@ | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [€/kWh]. Will be added to variable market price. | +| charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [amount/kWh]. Will be added to variable market price. | | elecpricefixed | `EOS_ELECPRICE__ELECPRICEFIXED` | `ElecPriceFixedCommonSettings` | `rw` | `required` | Fixed electricity price provider settings. | | elecpriceimport | `EOS_ELECPRICE__ELECPRICEIMPORT` | `ElecPriceImportCommonSettings` | `rw` | `required` | Import provider settings. | | energycharts | `EOS_ELECPRICE__ENERGYCHARTS` | `ElecPriceEnergyChartsCommonSettings` | `rw` | `required` | Energy Charts provider settings. | diff --git a/tests/testdata/docs/_generated/configfeedintariff.md b/tests/testdata/docs/_generated/configfeedintariff.md index 8e882a1e..dd855ef1 100644 --- a/tests/testdata/docs/_generated/configfeedintariff.md +++ b/tests/testdata/docs/_generated/configfeedintariff.md @@ -95,7 +95,7 @@ | Name | Type | Read-Only | Default | Description | | ---- | ---- | --------- | ------- | ----------- | -| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | Electricity price feed in tariff [€/kWH]. | +| feed_in_tariff_kwh | `Optional[float]` | `rw` | `None` | Electricity price feed in tariff [amount/kWh]. | :::