feat: adapt configuration for multi optimization algorithms

Decouple configuration from optimization algorithm parameters. Add to_[algorithm]_param() methods
to the configuration that derive optimization algorithm specific parameters from the configuration.
Add x-scope tags to the configuration options that describe for which specific algorithms the
configuration option is for.

The whole device settings are restructured. There are now general settings for the device classes
with the afore mentioned to_[algorithm]_param() methods. The general device settings got their own
directory `devices/settings`. By this the parameter class also does not have to be a pydantic model
which can be used for future optimization/ simulations speed up.

Also the parameter class for a device is now part of the device module. This better decouples and
also is the natural place for parameters of a device.

Besides this feature there are also fixes and improvements:

* feat: extend home appliance time window settings and simulation

  Home appliance can now be configured for multiple runs with per-cycle allowed time windows. The
  number of remaining cycles to plan is determined at runtime by reading the
  ``cycles_completed_measurement_key`` from the measurement store.

* feat: specialiced CycleTimeWindowSequence for time window sequences

  Sequence of time windows associated to cycles.

  This model specializes ``ValueTimeWindowSequence`` so that the ``value``
  field of each ``ValueTimeWindow`` encodes the **cycle index** (0-based
  integer) the window belongs to.

  Typical use: an appliance that must run ``n`` times per day, each run
  constrained to a distinct time window.  Assign ``value=0`` to windows
  for the first cycle, ``value=1`` for the second, and so on.  Multiple
  windows may share the same cycle index (their allowed regions are unioned).
  Windows with ``value=None`` are silently ignored by all cycle-aware methods.

* fix: Make test_configmigrate also regard the _ANY_SENTENIEL in key values

* chore: Make devices configurations a map instead of a list

  This makes config paths stable regardless of declaration order and lets each device settings
  class build its own config path from ``self.device_id`` without needing an external index.
  Tests are adapted likewise.

  Devices configurations are automatically migrated from lists to maps.

* chore: rename levelized_cost_of_storage_kwh to levelized_cost_of_storage_amt kwh

  This better fits in the naming scheme and also makes clear the costs are money.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-08-25 15:18:00 +02:00
parent ba76087db9
commit 6ebd343047
49 changed files with 4838 additions and 2130 deletions
@@ -1102,12 +1102,12 @@ class GeneticOptimization(OptimizationBase):
self.ev_possible_charge_values = parameters.ev.charge_rates
elif (
self.config.devices.electric_vehicles
and self.config.devices.electric_vehicles[0]
and self.config.devices.electric_vehicles[0].charge_rates is not None
and len(self.config.devices.electric_vehicles) > 0
and list(self.config.devices.electric_vehicles.values())[0].charge_rates is not None
):
self.ev_possible_charge_values = self.config.devices.electric_vehicles[
0
].charge_rates
self.ev_possible_charge_values = list(
self.config.devices.electric_vehicles.values()
)[0].charge_rates
else:
warning_msg = "No charge rates provided for electric vehicle - using default."
logger.warning(warning_msg)
@@ -1136,11 +1136,11 @@ class GeneticOptimization(OptimizationBase):
] or [1.0]
elif (
self.config.devices.batteries
and self.config.devices.batteries[0]
and self.config.devices.batteries[0].charge_rates
and len(self.config.devices.batteries) > 0
and list(self.config.devices.batteries.values())[0].charge_rates
):
self.bat_possible_charge_values = [
r for r in self.config.devices.batteries[0].charge_rates if r > 0.0
r for r in list(self.config.devices.batteries.values())[0].charge_rates if r > 0.0
] or [1.0]
else:
self.bat_possible_charge_values = [1.0]
@@ -4,7 +4,6 @@ from typing import Optional
from pydantic import Field
from akkudoktoreos.config.configabc import TimeWindowSequence
from akkudoktoreos.optimization.genetic.geneticabc import GeneticParametersBaseModel
@@ -18,179 +17,3 @@ class DeviceParameters(GeneticParametersBaseModel):
"examples": [None],
},
)
def max_charging_power_field(description: Optional[str] = None) -> float:
if description is None:
description = "Maximum charging power in watts."
return Field(default=5000, gt=0, json_schema_extra={"description": description})
def initial_soc_percentage_field(description: str) -> int:
return Field(
default=0, ge=0, le=100, json_schema_extra={"description": description, "examples": [42]}
)
def discharging_efficiency_field(default_value: float) -> float:
return Field(
default=default_value,
gt=0,
le=1,
json_schema_extra={
"description": "A float representing the discharge efficiency of the battery."
},
)
class BaseBatteryParameters(DeviceParameters):
"""Battery Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of battery", "examples": ["battery1"]}
)
capacity_wh: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the capacity of the battery in watt-hours.",
"examples": [8000],
},
)
charging_efficiency: float = Field(
default=0.88,
gt=0,
le=1,
json_schema_extra={
"description": "A float representing the charging efficiency of the battery."
},
)
discharging_efficiency: float = discharging_efficiency_field(0.88)
max_charge_power_w: Optional[float] = max_charging_power_field()
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
)
min_soc_percentage: int = Field(
default=0,
ge=0,
le=100,
json_schema_extra={
"description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.",
"examples": [10],
},
)
max_soc_percentage: int = Field(
default=100,
ge=0,
le=100,
json_schema_extra={
"description": "An integer representing the maximum state of charge (SOC) of the battery in percentage."
},
)
charge_rates: Optional[list[float]] = Field(
default=None,
json_schema_extra={
"description": "Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.",
"examples": [[0.0, 0.25, 0.5, 0.75, 1.0], None],
},
)
class SolarPanelBatteryParameters(BaseBatteryParameters):
"""PV battery device simulation configuration."""
max_charge_power_w: Optional[float] = max_charging_power_field()
class ElectricVehicleParameters(BaseBatteryParameters):
"""Battery Electric Vehicle Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of electric vehicle", "examples": ["ev1"]}
)
discharging_efficiency: float = discharging_efficiency_field(1.0)
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the current state of charge (SOC) of the battery in percentage."
)
class HomeApplianceParameters(DeviceParameters):
"""Home Appliance Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of home appliance", "examples": ["dishwasher"]}
)
consumption_wh: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the energy consumption of a household device in watt-hours.",
"examples": [2000],
},
)
duration_h: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the usage duration of a household device in hours.",
"examples": [3],
},
)
time_windows: Optional[TimeWindowSequence] = Field(
default=None,
json_schema_extra={
"description": "List of allowed time windows. Defaults to optimization general time window.",
"examples": [
[
{"start_time": "10:00", "duration": "3 hours"},
],
],
},
)
class InverterParameters(DeviceParameters):
"""Inverter Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of inverter", "examples": ["inverter1"]}
)
max_power_wh: float = Field(gt=0, json_schema_extra={"examples": [10000]})
battery_id: Optional[str] = Field(
default=None,
json_schema_extra={"description": "ID of battery", "examples": [None, "battery1"]},
)
ac_to_dc_efficiency: float = Field(
default=1.0,
ge=0,
le=1,
json_schema_extra={
"description": (
"Efficiency of AC to DC conversion (for AC/grid charging of battery). "
"Set to 0 to disable AC charging via inverter. "
"Default 1.0 for backward compatibility (no additional inverter loss)."
),
"examples": [0.95, 1.0, 0.0],
},
)
dc_to_ac_efficiency: float = Field(
default=1.0,
gt=0,
le=1,
json_schema_extra={
"description": (
"Efficiency of DC to AC conversion (for battery discharging to AC load/grid). "
"Default 1.0 for backward compatibility (no additional inverter loss)."
),
"examples": [0.95, 1.0],
},
)
max_ac_charge_power_w: Optional[float] = Field(
default=None,
ge=0,
json_schema_extra={
"description": (
"Maximum AC charging power in watts. "
"None means no additional limit (battery's own max_charge_power_w applies). "
"Set to 0 to disable AC charging."
),
"examples": [None, 0, 5000],
},
)
@@ -27,13 +27,13 @@ from akkudoktoreos.core.coreabc import (
PredictionMixin,
get_ems,
)
from akkudoktoreos.optimization.genetic.geneticabc import GeneticParametersBaseModel
from akkudoktoreos.optimization.genetic.geneticdevices import (
from akkudoktoreos.devices.genetic.battery import (
ElectricVehicleParameters,
HomeApplianceParameters,
InverterParameters,
SolarPanelBatteryParameters,
)
from akkudoktoreos.devices.genetic.homeappliance import HomeApplianceParameters
from akkudoktoreos.devices.genetic.inverter import InverterParameters
from akkudoktoreos.optimization.genetic.geneticabc import GeneticParametersBaseModel
from akkudoktoreos.utils.datetimeutil import to_duration
# Do not import directly from akkudoktoreos.core.coreabc
@@ -269,6 +269,11 @@ class GeneticOptimizationParameters(
if "ev_soc_miss" not in cls.config.optimization.genetic.penalties:
logger.info("Genetic penalties unknown - defaulting to ev_soc_miss = 10.")
cls.config.optimization.genetic.penalties["ev_soc_miss"] = 10
# Setup some basic providers if not set
if not cls.config.weather.provider:
cls.config.weather.provider = "OpenMeteo"
if not cls.config.load.provider:
cls.config.load.provider = "LoadAkkudoktor"
# Get start solution from last run
start_solution = None
@@ -557,36 +562,38 @@ class GeneticOptimizationParameters(
else:
if cls.config.devices.batteries is None:
logger.info("No battery device data available - defaulting to demo data.")
cls.config.devices.batteries = [{"device_id": "battery1", "capacity_wh": 8000}]
cls.config.devices.batteries = {
"battery1": {
"device_id": "battery1",
"capacity_wh": 8000,
},
}
try:
battery_config = cls.config.devices.batteries[0]
battery_params = SolarPanelBatteryParameters(
device_id=battery_config.device_id,
capacity_wh=battery_config.capacity_wh,
charging_efficiency=battery_config.charging_efficiency,
discharging_efficiency=battery_config.discharging_efficiency,
max_charge_power_w=battery_config.max_charge_power_w,
min_soc_percentage=battery_config.min_soc_percentage,
max_soc_percentage=battery_config.max_soc_percentage,
charge_rates=battery_config.charge_rates,
)
# Take first battery
battery_config = list(cls.config.devices.batteries.values())[0]
battery_params = battery_config.to_genetic_pv_bat_param()
except Exception as e:
logger.info(
"No battery device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.batteries = [{"device_id": "battery1", "capacity_wh": 8000}]
cls.config.devices.batteries = {
"battery1": {
"device_id": "battery1",
"capacity_wh": 8000,
},
}
# Retry
continue
# Levelized cost of ownership
if battery_config.levelized_cost_of_storage_kwh is None:
if battery_config.levelized_cost_of_storage_amt_kwh is None:
logger.info(
"No battery device LCOS data available - defaulting to 0 [amount/kWh]. Parameter preparation attempt {}.",
attempt,
)
battery_config.levelized_cost_of_storage_kwh = 0
battery_lcos_kwh = battery_config.levelized_cost_of_storage_kwh
battery_config.levelized_cost_of_storage_amt_kwh = 0
battery_lcos_kwh = battery_config.levelized_cost_of_storage_amt_kwh
# Initial SOC
try:
initial_soc_factor = await cls.measurement.key_to_value(
@@ -623,26 +630,18 @@ class GeneticOptimizationParameters(
"No electric vehicle device data available - defaulting to demo data."
)
cls.config.devices.max_electric_vehicles = 1
cls.config.devices.electric_vehicles = [
{
"device_id": "ev11",
cls.config.devices.electric_vehicles = {
"ev1": {
"device_id": "ev1",
"capacity_wh": 50000,
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
"min_soc_percentage": 70,
}
]
},
}
try:
electric_vehicle_config = cls.config.devices.electric_vehicles[0]
electric_vehicle_params = ElectricVehicleParameters(
device_id=electric_vehicle_config.device_id,
capacity_wh=electric_vehicle_config.capacity_wh,
charging_efficiency=electric_vehicle_config.charging_efficiency,
discharging_efficiency=electric_vehicle_config.discharging_efficiency,
charge_rates=electric_vehicle_config.charge_rates,
max_charge_power_w=electric_vehicle_config.max_charge_power_w,
min_soc_percentage=electric_vehicle_config.min_soc_percentage,
max_soc_percentage=electric_vehicle_config.max_soc_percentage,
)
# Take first electric_vehicle
electric_vehicle_config = list(cls.config.devices.electric_vehicles.values())[0]
electric_vehicle_params = electric_vehicle_config.to_genetic_ev_bat_param()
except Exception as e:
logger.info(
"No electric_vehicle device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
@@ -650,14 +649,14 @@ class GeneticOptimizationParameters(
e,
)
cls.config.devices.max_electric_vehicles = 1
cls.config.devices.electric_vehicles = [
{
"device_id": "ev12",
cls.config.devices.electric_vehicles = {
"ev1": {
"device_id": "ev1",
"capacity_wh": 50000,
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
"min_soc_percentage": 70,
}
]
},
}
# Retry
continue
# Initial SOC
@@ -693,36 +692,30 @@ class GeneticOptimizationParameters(
else:
if cls.config.devices.inverters is None:
logger.info("No inverter device data available - defaulting to demo data.")
cls.config.devices.inverters = [
{
cls.config.devices.inverters = {
"inverter1": {
"device_id": "inverter1",
"max_power_w": 10000,
"battery_id": battery_config.device_id,
}
]
},
}
try:
inverter_config = cls.config.devices.inverters[0]
inverter_params = InverterParameters(
device_id=inverter_config.device_id,
max_power_wh=inverter_config.max_power_w,
battery_id=inverter_config.battery_id,
ac_to_dc_efficiency=inverter_config.ac_to_dc_efficiency,
dc_to_ac_efficiency=inverter_config.dc_to_ac_efficiency,
max_ac_charge_power_w=inverter_config.max_ac_charge_power_w,
)
# Take first inverter
inverter_config = list(cls.config.devices.inverters.values())[0]
inverter_params = inverter_config.to_genetic_param()
except Exception as e:
logger.info(
"No inverter device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.inverters = [
{
cls.config.devices.inverters = {
"inverter1": {
"device_id": "inverter1",
"max_power_w": 10000,
"battery_id": battery_config.device_id,
}
]
},
}
# Retry
continue
@@ -739,12 +732,12 @@ class GeneticOptimizationParameters(
logger.info(
"No home appliance device data available - defaulting to demo data."
)
cls.config.devices.home_appliances = [
{
cls.config.devices.home_appliances = {
"dishwasher1": {
"device_id": "dishwasher1",
"consumption_wh": 2000,
"duration_h": 3.0,
"time_windows": {
"cycle_time_windows": {
"windows": [
{
"start_time": "08:00",
@@ -756,30 +749,26 @@ class GeneticOptimizationParameters(
},
],
},
}
]
},
}
try:
home_appliance_config = cls.config.devices.home_appliances[0]
home_appliance_params = HomeApplianceParameters(
device_id=home_appliance_config.device_id,
consumption_wh=home_appliance_config.consumption_wh,
duration_h=home_appliance_config.duration_h,
time_windows=home_appliance_config.time_windows,
)
# Take first appliance
home_appliance_config = list(cls.config.devices.home_appliances.values())[0]
home_appliance_params = home_appliance_config.to_genetic_param()
except Exception as e:
logger.info(
"No home appliance device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.home_appliances = [
{
cls.config.devices.home_appliances = {
"dishwasher1": {
"device_id": "dishwasher1",
"consumption_wh": 2000,
"duration_h": 3.0,
"time_windows": None,
}
]
"cycle_time_windows": None,
},
}
# Retry
continue
@@ -350,21 +350,21 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
def _battery_device_id(self) -> str:
"""Get battery device id."""
try:
return self.config.devices.batteries[0].device_id
return list(self.config.devices.batteries.values())[0].device_id
except Exception:
return "battery1"
def _ev_device_id(self) -> str:
"""Get electric vehicle device id."""
try:
return self.config.devices.electric_vehicles[0].device_id
return self.config.devices.electric_vehicles.values()[0].device_id
except Exception:
return "ev1"
def _homeappliance_device_id(self) -> str:
"""Get home appliance device id."""
try:
return self.config.devices.home_appliances[0].device_id
return self.config.devices.home_appliances.values()[0].device_id
except Exception:
return "homeappliance1"
@@ -453,11 +453,11 @@ class GeneticSolution(ConfigMixin, GeneticParametersBaseModel):
(the inverter curtails automatically, but this makes intent clear).
- Discharge: blocked when SOC is at or below min SOC.
"""
bat_list = self.config.devices.batteries
if not bat_list:
bat_dict = self.config.devices.batteries
if bat_dict is None or len(bat_dict) <= 0:
return ac_charge, dc_charge, discharge_allowed
bat = bat_list[0]
bat = list(bat_dict.values())[0]
min_soc = float(bat.min_soc_percentage)
max_soc = float(bat.max_soc_percentage)
capacity_wh = float(bat.capacity_wh)
@@ -1105,12 +1105,12 @@ class Genetic0Optimization(OptimizationBase):
self.ev_possible_charge_values = parameters.ev.charge_rates
elif (
self.config.devices.electric_vehicles
and self.config.devices.electric_vehicles[0]
and self.config.devices.electric_vehicles[0].charge_rates is not None
and len(self.config.devices.electric_vehicles) > 0
and list(self.config.devices.electric_vehicles.values())[0].charge_rates is not None
):
self.ev_possible_charge_values = self.config.devices.electric_vehicles[
0
].charge_rates
self.ev_possible_charge_values = list(
self.config.devices.electric_vehicles.values()
)[0].charge_rates
else:
warning_msg = "No charge rates provided for electric vehicle - using default."
logger.warning(warning_msg)
@@ -1139,11 +1139,11 @@ class Genetic0Optimization(OptimizationBase):
] or [1.0]
elif (
self.config.devices.batteries
and self.config.devices.batteries[0]
and self.config.devices.batteries[0].charge_rates
and len(self.config.devices.batteries) > 0
and list(self.config.devices.batteries.values())[0].charge_rates
):
self.bat_possible_charge_values = [
r for r in self.config.devices.batteries[0].charge_rates if r > 0.0
r for r in list(self.config.devices.batteries.values())[0].charge_rates if r > 0.0
] or [1.0]
else:
self.bat_possible_charge_values = [1.0]
@@ -4,7 +4,6 @@ from typing import Optional
from pydantic import Field
from akkudoktoreos.config.configabc import TimeWindowSequence
from akkudoktoreos.optimization.genetic0.genetic0abc import Genetic0ParametersBaseModel
@@ -18,179 +17,3 @@ class Genetic0DeviceParameters(Genetic0ParametersBaseModel):
"examples": [None],
},
)
def max_charging_power_field(description: Optional[str] = None) -> float:
if description is None:
description = "Maximum charging power in watts."
return Field(default=5000, gt=0, json_schema_extra={"description": description})
def initial_soc_percentage_field(description: str) -> int:
return Field(
default=0, ge=0, le=100, json_schema_extra={"description": description, "examples": [42]}
)
def discharging_efficiency_field(default_value: float) -> float:
return Field(
default=default_value,
gt=0,
le=1,
json_schema_extra={
"description": "A float representing the discharge efficiency of the battery."
},
)
class Genetic0BaseBatteryParameters(Genetic0DeviceParameters):
"""Battery Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of battery", "examples": ["battery1"]}
)
capacity_wh: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the capacity of the battery in watt-hours.",
"examples": [8000],
},
)
charging_efficiency: float = Field(
default=0.88,
gt=0,
le=1,
json_schema_extra={
"description": "A float representing the charging efficiency of the battery."
},
)
discharging_efficiency: float = discharging_efficiency_field(0.88)
max_charge_power_w: Optional[float] = max_charging_power_field()
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
)
min_soc_percentage: int = Field(
default=0,
ge=0,
le=100,
json_schema_extra={
"description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.",
"examples": [10],
},
)
max_soc_percentage: int = Field(
default=100,
ge=0,
le=100,
json_schema_extra={
"description": "An integer representing the maximum state of charge (SOC) of the battery in percentage."
},
)
charge_rates: Optional[list[float]] = Field(
default=None,
json_schema_extra={
"description": "Charge rates as factor of maximum charging power [0.00 ... 1.00]. None denotes all charge rates are available.",
"examples": [[0.0, 0.25, 0.5, 0.75, 1.0], None],
},
)
class Genetic0SolarPanelBatteryParameters(Genetic0BaseBatteryParameters):
"""PV battery device simulation configuration."""
max_charge_power_w: Optional[float] = max_charging_power_field()
class Genetic0ElectricVehicleParameters(Genetic0BaseBatteryParameters):
"""Battery Electric Vehicle Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of electric vehicle", "examples": ["ev1"]}
)
discharging_efficiency: float = discharging_efficiency_field(1.0)
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the current state of charge (SOC) of the battery in percentage."
)
class Genetic0HomeApplianceParameters(Genetic0DeviceParameters):
"""Home Appliance Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of home appliance", "examples": ["dishwasher"]}
)
consumption_wh: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the energy consumption of a household device in watt-hours.",
"examples": [2000],
},
)
duration_h: int = Field(
gt=0,
json_schema_extra={
"description": "An integer representing the usage duration of a household device in hours.",
"examples": [3],
},
)
time_windows: Optional[TimeWindowSequence] = Field(
default=None,
json_schema_extra={
"description": "List of allowed time windows. Defaults to optimization general time window.",
"examples": [
[
{"start_time": "10:00", "duration": "3 hours"},
],
],
},
)
class Genetic0InverterParameters(Genetic0DeviceParameters):
"""Inverter Device Simulation Configuration."""
device_id: str = Field(
json_schema_extra={"description": "ID of inverter", "examples": ["inverter1"]}
)
max_power_wh: float = Field(gt=0, json_schema_extra={"examples": [10000]})
battery_id: Optional[str] = Field(
default=None,
json_schema_extra={"description": "ID of battery", "examples": [None, "battery1"]},
)
ac_to_dc_efficiency: float = Field(
default=1.0,
ge=0,
le=1,
json_schema_extra={
"description": (
"Efficiency of AC to DC conversion (for AC/grid charging of battery). "
"Set to 0 to disable AC charging via inverter. "
"Default 1.0 for backward compatibility (no additional inverter loss)."
),
"examples": [0.95, 1.0, 0.0],
},
)
dc_to_ac_efficiency: float = Field(
default=1.0,
gt=0,
le=1,
json_schema_extra={
"description": (
"Efficiency of DC to AC conversion (for battery discharging to AC load/grid). "
"Default 1.0 for backward compatibility (no additional inverter loss)."
),
"examples": [0.95, 1.0],
},
)
max_ac_charge_power_w: Optional[float] = Field(
default=None,
ge=0,
json_schema_extra={
"description": (
"Maximum AC charging power in watts. "
"None means no additional limit (battery's own max_charge_power_w applies). "
"Set to 0 to disable AC charging."
),
"examples": [None, 0, 5000],
},
)
@@ -27,13 +27,15 @@ from akkudoktoreos.core.coreabc import (
PredictionMixin,
get_ems,
)
from akkudoktoreos.optimization.genetic0.genetic0abc import Genetic0ParametersBaseModel
from akkudoktoreos.optimization.genetic0.genetic0devices import (
from akkudoktoreos.devices.genetic0.genetic0battery import (
Genetic0ElectricVehicleParameters,
Genetic0HomeApplianceParameters,
Genetic0InverterParameters,
Genetic0SolarPanelBatteryParameters,
)
from akkudoktoreos.devices.genetic0.genetic0homeappliance import (
Genetic0HomeApplianceParameters,
)
from akkudoktoreos.devices.genetic0.genetic0inverter import Genetic0InverterParameters
from akkudoktoreos.optimization.genetic0.genetic0abc import Genetic0ParametersBaseModel
from akkudoktoreos.utils.datetimeutil import to_duration
# Do not import directly from akkudoktoreos.core.coreabc
@@ -260,6 +262,11 @@ class Genetic0OptimizationParameters(
if "ev_soc_miss" not in cls.config.optimization.genetic0.penalties:
logger.info("Genetic penalties unknown - defaulting to ev_soc_miss = 10.")
cls.config.optimization.genetic0.penalties["ev_soc_miss"] = 10
# Setup some basic providers if not set
if not cls.config.weather.provider:
cls.config.weather.provider = "OpenMeteo"
if not cls.config.load.provider:
cls.config.load.provider = "LoadAkkudoktor"
# Get start solution from last run
start_solution = None
@@ -548,36 +555,38 @@ class Genetic0OptimizationParameters(
else:
if cls.config.devices.batteries is None:
logger.info("No battery device data available - defaulting to demo data.")
cls.config.devices.batteries = [{"device_id": "battery1", "capacity_wh": 8000}]
cls.config.devices.batteries = {
"battery1": {
"device_id": "battery1",
"capacity_wh": 8000,
},
}
try:
battery_config = cls.config.devices.batteries[0]
battery_params = Genetic0SolarPanelBatteryParameters(
device_id=battery_config.device_id,
capacity_wh=battery_config.capacity_wh,
charging_efficiency=battery_config.charging_efficiency,
discharging_efficiency=battery_config.discharging_efficiency,
max_charge_power_w=battery_config.max_charge_power_w,
min_soc_percentage=battery_config.min_soc_percentage,
max_soc_percentage=battery_config.max_soc_percentage,
charge_rates=battery_config.charge_rates,
)
# Take first battery
battery_config = list(cls.config.devices.batteries.values())[0]
battery_params = battery_config.to_genetic0_pv_bat_param()
except Exception as e:
logger.info(
"No battery device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.batteries = [{"device_id": "battery1", "capacity_wh": 8000}]
cls.config.devices.batteries = {
"battery1": {
"device_id": "battery1",
"capacity_wh": 8000,
},
}
# Retry
continue
# Levelized cost of ownership
if battery_config.levelized_cost_of_storage_kwh is None:
if battery_config.levelized_cost_of_storage_amt_kwh is None:
logger.info(
"No battery device LCOS data available - defaulting to 0 [amount/kWh]. Parameter preparation attempt {}.",
attempt,
)
battery_config.levelized_cost_of_storage_kwh = 0
battery_lcos_kwh = battery_config.levelized_cost_of_storage_kwh
battery_config.levelized_cost_of_storage_amt_kwh = 0
battery_lcos_kwh = battery_config.levelized_cost_of_storage_amt_kwh
# Initial SOC
try:
initial_soc_factor = await cls.measurement.key_to_value(
@@ -614,26 +623,18 @@ class Genetic0OptimizationParameters(
"No electric vehicle device data available - defaulting to demo data."
)
cls.config.devices.max_electric_vehicles = 1
cls.config.devices.electric_vehicles = [
{
"device_id": "ev11",
cls.config.devices.electric_vehicles = {
"ev1": {
"device_id": "ev1",
"capacity_wh": 50000,
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
"min_soc_percentage": 70,
}
]
},
}
try:
electric_vehicle_config = cls.config.devices.electric_vehicles[0]
electric_vehicle_params = Genetic0ElectricVehicleParameters(
device_id=electric_vehicle_config.device_id,
capacity_wh=electric_vehicle_config.capacity_wh,
charging_efficiency=electric_vehicle_config.charging_efficiency,
discharging_efficiency=electric_vehicle_config.discharging_efficiency,
charge_rates=electric_vehicle_config.charge_rates,
max_charge_power_w=electric_vehicle_config.max_charge_power_w,
min_soc_percentage=electric_vehicle_config.min_soc_percentage,
max_soc_percentage=electric_vehicle_config.max_soc_percentage,
)
# Take first electric_vehicle
electric_vehicle_config = list(cls.config.devices.electric_vehicles.values())[0]
electric_vehicle_params = electric_vehicle_config.to_genetic0_ev_bat_param()
except Exception as e:
logger.info(
"No electric_vehicle device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
@@ -641,14 +642,14 @@ class Genetic0OptimizationParameters(
e,
)
cls.config.devices.max_electric_vehicles = 1
cls.config.devices.electric_vehicles = [
{
"device_id": "ev12",
cls.config.devices.electric_vehicles = {
"ev1": {
"device_id": "ev1",
"capacity_wh": 50000,
"charge_rates": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
"min_soc_percentage": 70,
}
]
},
}
# Retry
continue
# Initial SOC
@@ -684,36 +685,30 @@ class Genetic0OptimizationParameters(
else:
if cls.config.devices.inverters is None:
logger.info("No inverter device data available - defaulting to demo data.")
cls.config.devices.inverters = [
{
cls.config.devices.inverters = {
"inverter1": {
"device_id": "inverter1",
"max_power_w": 10000,
"battery_id": battery_config.device_id,
}
]
},
}
try:
inverter_config = cls.config.devices.inverters[0]
inverter_params = Genetic0InverterParameters(
device_id=inverter_config.device_id,
max_power_wh=inverter_config.max_power_w,
battery_id=inverter_config.battery_id,
ac_to_dc_efficiency=inverter_config.ac_to_dc_efficiency,
dc_to_ac_efficiency=inverter_config.dc_to_ac_efficiency,
max_ac_charge_power_w=inverter_config.max_ac_charge_power_w,
)
# Take first inverter
inverter_config = list(cls.config.devices.inverters.values())[0]
inverter_params = inverter_config.to_genetic0_param()
except Exception as e:
logger.info(
"No inverter device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.inverters = [
{
cls.config.devices.inverters = {
"inverter1": {
"device_id": "inverter1",
"max_power_w": 10000,
"battery_id": battery_config.device_id,
}
]
},
}
# Retry
continue
@@ -730,12 +725,12 @@ class Genetic0OptimizationParameters(
logger.info(
"No home appliance device data available - defaulting to demo data."
)
cls.config.devices.home_appliances = [
{
cls.config.devices.home_appliances = {
"dishwasher1": {
"device_id": "dishwasher1",
"consumption_wh": 2000,
"duration_h": 3.0,
"time_windows": {
"cycle_time_windows": {
"windows": [
{
"start_time": "08:00",
@@ -747,30 +742,26 @@ class Genetic0OptimizationParameters(
},
],
},
}
]
},
}
try:
home_appliance_config = cls.config.devices.home_appliances[0]
home_appliance_params = Genetic0HomeApplianceParameters(
device_id=home_appliance_config.device_id,
consumption_wh=home_appliance_config.consumption_wh,
duration_h=home_appliance_config.duration_h,
time_windows=home_appliance_config.time_windows,
)
# Take first appliance
home_appliance_config = list(cls.config.devices.home_appliances.values())[0]
home_appliance_params = home_appliance_config.to_genetic0_param()
except Exception as e:
logger.info(
"No home appliance device data available - defaulting to demo data. Parameter preparation attempt {}: {}",
attempt,
e,
)
cls.config.devices.home_appliances = [
{
cls.config.devices.home_appliances = {
"dishwasher1": {
"device_id": "dishwasher1",
"consumption_wh": 2000,
"duration_h": 3.0,
"time_windows": None,
}
]
"cycle_time_windows": None,
},
}
# Retry
continue
@@ -427,21 +427,21 @@ class Genetic0Solution(ConfigMixin, Genetic0ParametersBaseModel):
def _battery_device_id(self) -> str:
"""Get battery device id."""
try:
return self.config.devices.batteries[0].device_id
return list(self.config.devices.batteries.values())[0].device_id
except Exception:
return "battery1"
def _ev_device_id(self) -> str:
"""Get electric vehicle device id."""
try:
return self.config.devices.electric_vehicles[0].device_id
return self.config.devices.electric_vehicles.values()[0].device_id
except Exception:
return "ev1"
def _homeappliance_device_id(self) -> str:
"""Get home appliance device id."""
try:
return self.config.devices.home_appliances[0].device_id
return self.config.devices.home_appliances.values()[0].device_id
except Exception:
return "homeappliance1"
@@ -530,11 +530,11 @@ class Genetic0Solution(ConfigMixin, Genetic0ParametersBaseModel):
(the inverter curtails automatically, but this makes intent clear).
- Discharge: blocked when SOC is at or below min SOC.
"""
bat_list = self.config.devices.batteries
if not bat_list:
bat_dict = self.config.devices.batteries
if bat_dict is None or len(bat_dict) <= 0:
return ac_charge, dc_charge, discharge_allowed
bat = bat_list[0]
bat = list(bat_dict.values())[0]
min_soc = float(bat.min_soc_percentage)
max_soc = float(bat.max_soc_percentage)
capacity_wh = float(bat.capacity_wh)