Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions packages/control/bat_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ class Config:
configured: bool = field(default=False, metadata={"topic": "config/configured"})
bat_control_permitted: bool = field(default=False, metadata={"topic": "config/bat_control_permitted"})
bat_control_activated: bool = field(default=False, metadata={"topic": "config/bat_control_activated"})
power_limit_mode: str = field(default=BatPowerLimitMode.MODE_NO_DISCHARGE.value,
metadata={"topic": "config/power_limit_mode"})
power_limit_condition: str = field(default=BatPowerLimitCondition.VEHICLE_CHARGING.value,
metadata={"topic": "config/power_limit_condition"})
manual_mode: str = field(default=ManualMode.MANUAL_DISABLE.value,
metadata={"topic": "config/manual_mode"})
power_limit_mode: BatPowerLimitMode = field(default=BatPowerLimitMode.MODE_NO_DISCHARGE,
metadata={"topic": "config/power_limit_mode"})
power_limit_condition: BatPowerLimitCondition = field(default=BatPowerLimitCondition.VEHICLE_CHARGING,
metadata={"topic": "config/power_limit_condition"})
manual_mode: ManualMode = field(default=ManualMode.MANUAL_DISABLE,
metadata={"topic": "config/manual_mode"})
Comment on lines +79 to +84
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config fields are now typed/stored as Enum instances, but MQTT subscription writes per-field config values via SubData.set_json_payload_class without Enum coercion (it assigns decoded payloads directly). That means these attributes will typically become plain strings at runtime (e.g., "mode_no_discharge"), and later Enum comparisons will fail. Either keep these config fields as str (and compare to .value), or update the subscription/deserialization path to convert strings to the declared Enum type for dataclass fields.

Copilot uses AI. Check for mistakes.
bat_control_min_soc: int = field(default=10, metadata={"topic": "config/bat_control_min_soc"})
bat_control_max_soc: int = field(default=90, metadata={"topic": "config/bat_control_max_soc"})
price_limit_activated: bool = field(default=False, metadata={"topic": "config/price_limit_activated"})
Expand Down Expand Up @@ -117,7 +117,7 @@ class Set:
power_limit: Optional[float] = field(default=None, metadata={"topic": "set/power_limit"})
regulate_up: bool = field(default=False, metadata={"topic": "set/regulate_up"})
hysteresis_discharge: bool = field(default=False, metadata={"topic": "set/hysteresis_discharge"})
current_state: str = field(default=CurrentState.STARTUP.value, metadata={"topic": "set/current_state"})
current_state: CurrentState = field(default=CurrentState.STARTUP, metadata={"topic": "set/current_state"})
set_limit: bool = False


Expand Down Expand Up @@ -340,7 +340,7 @@ def _get_charging_power_left(self):
config = data.data.general_data.data.chargemode_config.pv_charging

self.data.set.regulate_up = False
if config.bat_mode == BatConsiderationMode.BAT_MODE.value:
if config.bat_mode == BatConsiderationMode.BAT_MODE:
if self.data.get.power < 0:
# Wenn der Speicher entladen wird, darf diese Leistung nicht zum Laden der Fahrzeuge genutzt werden.
# Wenn der Speicher schneller regelt als die LP, würde sonst der Speicher reduziert werden.
Expand All @@ -349,7 +349,7 @@ def _get_charging_power_left(self):
charging_power_left = 0
self.data.set.regulate_up = True if self.data.get.soc < 100 else False
# ev wird nach Speicher geladen
elif config.bat_mode == BatConsiderationMode.EV_MODE.value:
elif config.bat_mode == BatConsiderationMode.EV_MODE:
Comment on lines 343 to +352
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pv_charging.bat_mode is populated as a string in practice (see PvCharging.bat_mode default in packages/control/general.py and SubData.set_json_payload_class assignment), but this code now compares it against BatConsiderationMode Enum members. This will make both branches unreachable unless the value is explicitly converted to the Enum elsewhere. Compare against .value or ensure bat_mode is stored as BatConsiderationMode (with conversion on MQTT receive).

Copilot uses AI. Check for mistakes.
# Speicher sollte weder ge- noch entladen werden.
charging_power_left = self.data.get.power
else:
Expand Down Expand Up @@ -459,10 +459,10 @@ def get_charge_mode_vehicle_charge(self):
# Speicher entlädt oder Speicher lädt bei gewollter PV-Ladung
bat_power_valid = (self.data.get.power <= 0 or
(self.data.get.power > 0 and
self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value))
self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION))
# EVU Bezug vorhanden oder gewollte PV-Ladung aktiv
evu_power_valid = (data.data.counter_all_data.get_evu_counter().data.get.power >= -100 or
self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value)
self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION)
Comment on lines 459 to +465
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comparisons assume power_limit_mode is a BatPowerLimitMode Enum, but when the value comes from MQTT it will be a string unless it’s converted on receipt. With the current subscription logic this will cause bat_power_valid / evu_power_valid to evaluate incorrectly. Consider comparing to BatPowerLimitMode.*.value or adding Enum coercion when setting config values from topics.

Copilot uses AI. Check for mistakes.

if (
vehicle_charging and
Expand Down Expand Up @@ -496,19 +496,19 @@ def get_charge_mode_manual_charge(self):
# EVU Bezug vorhanden oder gewollte PV-Ladung aktiv
evu_power_valid = data.data.counter_all_data.get_evu_counter().data.get.power >= -100
bat_power_valid = self.data.get.power <= 0
if self.data.config.manual_mode == ManualMode.MANUAL_CHARGE.value:
if self.data.config.manual_mode == ManualMode.MANUAL_CHARGE:
log.debug("Aktive Speichersteuerung: Manueller Modus - Speicher laden.")
return BatChargeMode.BAT_FORCE_CHARGE
elif self.data.config.manual_mode == ManualMode.MANUAL_LIMIT.value:
if (self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value or
elif self.data.config.manual_mode == ManualMode.MANUAL_LIMIT:
if (self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION or
(evu_power_valid and bat_power_valid)):
# Limit anwenden wenn kein Überschuss vorhanden oder der Speicher nicht lädt
log.debug("Aktive Speichersteuerung: Manueller Modus - Regellimit anwenden.")
return BatChargeMode.BAT_USE_LIMIT
else:
log.debug("Aktive Speichersteuerung: Manueller Modus - Kein Limit da Speicher lädt")
return BatChargeMode.BAT_SELF_REGULATION
elif self.data.config.manual_mode == ManualMode.MANUAL_DISCHARGE.value:
elif self.data.config.manual_mode == ManualMode.MANUAL_DISCHARGE:
log.debug("Aktive Speichersteuerung: Manueller Modus - "
"Eigenregelung da aktive Speicherentladung nicht erlaubt ist.")
return BatChargeMode.BAT_SELF_REGULATION
Expand Down Expand Up @@ -554,16 +554,16 @@ def get_power_limit(self):
log.debug("Speicher-Leistung nicht begrenzen, da aktive Speichersteuerung deaktiviert wurde.")
else:
charge_mode = BatChargeMode.BAT_SELF_REGULATION
if self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL.value:
if self.data.config.power_limit_condition == BatPowerLimitCondition.MANUAL:
log.debug("Aktive Speichersteuerung: Manueller Modus.")
charge_mode = self.get_charge_mode_manual_charge()
elif self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value:
elif self.data.config.power_limit_condition == BatPowerLimitCondition.VEHICLE_CHARGING:
log.debug("Aktive Speichersteuerung: Wenn Fahrzeuge laden.")
charge_mode = self.get_charge_mode_vehicle_charge()
elif self.data.config.power_limit_condition == BatPowerLimitCondition.PRICE_LIMIT.value:
elif self.data.config.power_limit_condition == BatPowerLimitCondition.PRICE_LIMIT:
log.debug("Aktive Speichersteuerung: Strompreisbasiert.")
charge_mode = self.get_charge_mode_electricity_tariff()
elif self.data.config.power_limit_condition == BatPowerLimitCondition.SCHEDULED.value:
elif self.data.config.power_limit_condition == BatPowerLimitCondition.SCHEDULED:
log.debug("Aktive Speichersteuerung: Vorhersagebasiertes Zielladen.")
charge_mode = self.get_charge_mode_scheduled()

Expand All @@ -573,13 +573,13 @@ def get_power_limit(self):
self.data.set.power_limit = None
log.debug("Speicher-Leistung nicht begrenzen")
elif charge_mode == BatChargeMode.BAT_USE_LIMIT:
if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_NO_DISCHARGE.value:
if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_NO_DISCHARGE:
self.data.set.power_limit = 0
log.debug("Speicher-Leistung begrenzen auf 0kW")
elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value:
elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION:
self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1
log.debug(f"Speicher-Leistung begrenzen auf {self.data.set.power_limit/1000}kW")
elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value:
elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION:
self.data.set.power_limit = data.data.pv_all_data.data.get.power * -1
log.debug(f"Speicher in Höhe des PV-Ertrags laden: {self.data.set.power_limit/1000}kW")
elif charge_mode == BatChargeMode.BAT_FORCE_CHARGE:
Expand All @@ -595,18 +595,18 @@ def get_power_limit(self):

if ((self.data.config.bat_control_permitted is False or
self.data.config.bat_control_activated is False)
and self.data.set.current_state == CurrentState.STARTUP.value):
and self.data.set.current_state == CurrentState.STARTUP):
self.data.set.set_limit = False
elif (self.data.set.current_state == CurrentState.IDLE.value and
elif (self.data.set.current_state == CurrentState.IDLE and
charge_mode == BatChargeMode.BAT_SELF_REGULATION):
self.data.set.set_limit = False
else:
self.data.set.set_limit = True

if charge_mode == BatChargeMode.BAT_SELF_REGULATION:
self.data.set.current_state = CurrentState.IDLE.value
self.data.set.current_state = CurrentState.IDLE
else:
self.data.set.current_state = CurrentState.ACTIVE.value
self.data.set.current_state = CurrentState.ACTIVE
Comment on lines 596 to +609
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current_state is now modeled as CurrentState, but the MQTT subscription path sets set/current_state directly from the decoded payload (string). This can lead to mixed types (str vs Enum) and make these state checks/assignments behave incorrectly after retained topics are received. Either keep current_state as str and use .value, or ensure Enum conversion when consuming the topic payload.

Copilot uses AI. Check for mistakes.


def get_controllable_bat_components() -> List:
Expand Down
70 changes: 35 additions & 35 deletions packages/control/bat_all_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ def default_chargepoint_factory() -> List[Chargepoint]:
class BatControlParams:
name: str
expected_power_limit_bat: Optional[float]
power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE.value
power_limit_condition: str = BatPowerLimitCondition.VEHICLE_CHARGING.value
bat_manual_mode: str = ManualMode.MANUAL_DISABLE.value
power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE
power_limit_condition: str = BatPowerLimitCondition.VEHICLE_CHARGING
bat_manual_mode: str = ManualMode.MANUAL_DISABLE
Comment on lines +187 to +189
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields are still annotated as str but now default to Enum members. Update the type hints to the corresponding Enum types (BatPowerLimitMode, BatPowerLimitCondition, ManualMode) to match how the params are used and avoid misleading typing in tests.

Suggested change
power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE
power_limit_condition: str = BatPowerLimitCondition.VEHICLE_CHARGING
bat_manual_mode: str = ManualMode.MANUAL_DISABLE
power_limit_mode: BatPowerLimitMode = BatPowerLimitMode.MODE_NO_DISCHARGE
power_limit_condition: BatPowerLimitCondition = BatPowerLimitCondition.VEHICLE_CHARGING
bat_manual_mode: ManualMode = ManualMode.MANUAL_DISABLE

Copilot uses AI. Check for mistakes.
cps: List[Chargepoint] = field(default_factory=default_chargepoint_factory)
power_limit_controllable: bool = True
bat_power: float = -10
Expand All @@ -206,42 +206,42 @@ class BatControlParams:

cases = [
BatControlParams("Speicher nicht regelbar", None, power_limit_controllable=False,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Disclaimer nicht akzeptiert", None, bat_control_permitted=False,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Speichersteuerung deaktiviert", None, bat_control_activated=False,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
# Manuelle Steuerung
BatControlParams("Manuelle Steuerung, Speichersteuerung deaktiviert", None,
power_limit_condition=BatPowerLimitCondition.MANUAL.value,
bat_manual_mode=ManualMode.MANUAL_DISABLE.value),
power_limit_condition=BatPowerLimitCondition.MANUAL,
bat_manual_mode=ManualMode.MANUAL_DISABLE),
BatControlParams("Manuelle Steuerung, Entladung sperren", 0,
power_limit_condition=BatPowerLimitCondition.MANUAL.value,
bat_manual_mode=ManualMode.MANUAL_LIMIT.value),
power_limit_condition=BatPowerLimitCondition.MANUAL,
bat_manual_mode=ManualMode.MANUAL_LIMIT),
BatControlParams("Manuelle Steuerung, Begrenzung Hausverbrauch", -456,
power_limit_condition=BatPowerLimitCondition.MANUAL.value,
bat_manual_mode=ManualMode.MANUAL_LIMIT.value,
power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value),
power_limit_condition=BatPowerLimitCondition.MANUAL,
bat_manual_mode=ManualMode.MANUAL_LIMIT,
power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION),
BatControlParams("Manuelle Steuerung, Ladung PV Überschuss", 654,
power_limit_condition=BatPowerLimitCondition.MANUAL.value,
bat_manual_mode=ManualMode.MANUAL_LIMIT.value,
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value),
power_limit_condition=BatPowerLimitCondition.MANUAL,
bat_manual_mode=ManualMode.MANUAL_LIMIT,
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION),
BatControlParams("Manuelle Steuerung, Aktive Ladung", 5000,
power_limit_condition=BatPowerLimitCondition.MANUAL.value,
bat_manual_mode=ManualMode.MANUAL_CHARGE.value),
power_limit_condition=BatPowerLimitCondition.MANUAL,
bat_manual_mode=ManualMode.MANUAL_CHARGE),
# Wenn Fahrzeuge Laden
BatControlParams("Fahrzeuge laden, Begrenzung immer, keine LP im Sofortladen", None, cps=[],
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Fahrzeuge laden, Begrenzung immer, Speicher lädt", None, bat_power=100,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Fahrzeuge laden, Begrenzung immer,Einspeisung", None, evu_power=-110,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Fahrzeuge laden, Begrenzung immer", 0,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Fahrzeuge laden, Begrenzung Hausverbrauch", -456,
power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value),
power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION),
BatControlParams("Fahrzeuge laden, Ladung PV Überschuss", 654,
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value),
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION),
]


Expand Down Expand Up @@ -290,36 +290,36 @@ def test_active_bat_control(params: BatControlParams, data_, monkeypatch):
cases = [
# Nach Preisgrenze
BatControlParams("Preisgrenze, Grenze deaktiviert, Eigenregelung", None,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_limit_activated=False,
price_limit=0.40,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Preisgrenze, Entladung sperren, Grenze unterschritten", 0,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_limit_activated=True,
price_limit=0.30,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
BatControlParams("Preisgrenze, Überschuss Laden, Grenze unterschritten", 654,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_limit_activated=True,
price_limit=0.30,
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value),
power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION),
BatControlParams("Preisgrenze, Entladung sperren, Grenze greift nicht", None,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_limit_activated=True,
price_limit=0.10,
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value),
power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE),
# Aktive Ladung
BatControlParams("Preisgrenze, Grenze deaktiviert, Eigenregelung", None,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_charge_activated=False,
charge_limit=0.40),
BatControlParams("Preisgrenze, Grenze unterschritten, Ladung", 5000,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_charge_activated=True,
charge_limit=0.30),
BatControlParams("Preisgrenze, Grenze greift nicht, Eigenregelung", None,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value,
power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT,
price_charge_activated=True,
charge_limit=0.10),
]
Expand Down
Loading