diff --git a/packages/control/bat.py b/packages/control/bat.py index eedb3c63b2..51e604cb25 100644 --- a/packages/control/bat.py +++ b/packages/control/bat.py @@ -21,6 +21,8 @@ class Get: fault_str: str = field(default=NO_ERROR, metadata={"topic": "get/fault_str"}) power: float = field(default=0, metadata={"topic": "get/power"}) power_limit_controllable: bool = field(default=False, metadata={"topic": "get/power_limit_controllable"}) + max_charge_power: float = field(default=0, metadata={"topic": "get/max_charge_power"}) + max_discharge_power: float = field(default=0, metadata={"topic": "get/max_discharge_power"}) def get_factory() -> Get: diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 676ca7fd03..56bea8608d 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -38,18 +38,50 @@ class BatConsiderationMode(Enum): MIN_SOC_BAT = "min_soc_bat_mode" +class BatPowerLimitCondition(Enum): + MANUAL = "manual" + VEHICLE_CHARGING = "vehicle_charging" + PRICE_LIMIT = "price_limit" + SCHEDULED = "scheduled" + + class BatPowerLimitMode(Enum): - NO_LIMIT = "no_limit" - LIMIT_STOP = "limit_stop" - LIMIT_TO_HOME_CONSUMPTION = "limit_to_home_consumption" + MODE_NO_DISCHARGE = "mode_no_discharge" + MODE_DISCHARGE_HOME_CONSUMPTION = "mode_discharge_home_consumption" + MODE_CHARGE_PV_PRODUCTION = "mode_charge_pv_production" + + +class BatChargeMode(Enum): + BAT_SELF_REGULATION = "bat_self_regulation" + BAT_USE_LIMIT = "bat_use_limit" + BAT_FORCE_CHARGE = "bat_force_charge" + BAT_FORCE_DISCHARGE = "bat_force_discharge" # in DE nicht erlaubt + + +class ManualMode(Enum): + MANUAL_DISABLE = "manual_disable" + MANUAL_LIMIT = "manual_limit" + MANUAL_CHARGE = "manual_charge" + MANUAL_DISCHARGE = "manual_discharge" # in DE nicht erlaubt @dataclass class Config: configured: bool = field(default=False, metadata={"topic": "config/configured"}) - power_limit_mode: str = field(default=BatPowerLimitMode.NO_LIMIT.value, - metadata={"topic": "config/power_limit_mode"}) 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"}) + bat_control_condition: str = field(default=BatPowerLimitCondition.VEHICLE_CHARGING.value, + metadata={"topic": "config/bat_control_condition"}) + manual_mode: str = field(default=ManualMode.MANUAL_DISABLE.value, + metadata={"topic": "config/manual_mode"}) + bat_control_min_soc: str = field(default=10, metadata={"topic": "config/bat_control_min_soc"}) + bat_control_max_soc: str = field(default=90, metadata={"topic": "config/bat_control_max_soc"}) + price_limit_activated: bool = field(default=False, metadata={"topic": "config/price_limit_activated"}) + price_charge_activated: bool = field(default=False, metadata={"topic": "config/price_charge_activated"}) + price_limit: float = field(default=0.30, metadata={"topic": "config/price_limit"}) + charge_limit: float = field(default=0.30, metadata={"topic": "config/charge_limit"}) def config_factory() -> Config: @@ -175,6 +207,81 @@ def _limit_bat_power_discharge(self, required_power): " begrenzt.") return required_power + def _set_bat_power_active_control(self, power): + controllable_bat_components = get_controllable_bat_components() + # maximal mögliche Lade- und Entaldeleistung des Systems unter Einbeziehung + # der erlaubten Lade-/ Entladeleistung und SoC der regelbaren Speicher ermitteln + max_charge_power_total = 0 + bat_ready_to_charge = 0 + max_discharge_power_total = 0 + bat_ready_to_discharge = 0 + for bat_component in controllable_bat_components: + bat_component_data = data.data.bat_data[f"bat{bat_component.component_config.id}"].data + if bat_component_data.get.soc < self.data.config.bat_control_max_soc: + max_charge_power_total += bat_component_data.get.max_charge_power + bat_ready_to_charge += 1 + if bat_component_data.get.soc > self.data.config.bat_control_min_soc: + max_discharge_power_total += bat_component_data.get.max_discharge_power + bat_ready_to_discharge += 1 + log.debug((f"Aktive Speichersteuerung: {power}W auf " + f"{len(controllable_bat_components)} regelbare Speicher verteilen.")) + log.debug((f"Ladung: {bat_ready_to_charge} Speicher unterhalb des maximalen SoC mit " + f"{max_charge_power_total}W regelbarer Lade-Leistung")) + log.debug((f"Entladung: {bat_ready_to_discharge} Speicher oberhalb des minimalen SoC mit " + f"{max_discharge_power_total}W regelbarer Entlade-Leistung")) + + # Leistung an einzelne Speicher übergeben + for bat_component in controllable_bat_components: + bat_component_data = data.data.bat_data[f"bat{bat_component.component_config.id}"].data + # Falls keine leistung übergeben wird greift die Eigenregelung der Speicher + if power is None: + power_limit = None + log.debug(("Aktive Speichersteuerung: Eigenregelung - Speicher " + f"(ID: {bat_component.component_config.id}) auf Eigenregelung gesetzt.")) + elif power == 0: + power_limit = 0 + log.debug((f"Aktive Speichersteuerung: Kein Laden/Entladen - " + f"Speicher (ID: {bat_component.component_config.id}) auf 0W gesetzt.")) + elif power < 0: + # Eigenregelung aller Speicher, da Entladung nicht möglich + if max_discharge_power_total == 0: + power_limit = None + log.debug(("Aktive Speichersteuerung: Entladung - alle Speicher befinden sich unterhalb minimal " + f"SoC. Speicher (ID: {bat_component.component_config.id}) auf Eigenregelung gesetzt.")) + else: + # unterhalb des minimal SoC greift die Eigenregelung + # das verhindert Tiefenentladung + if bat_component_data.get.soc <= self.data.config.bat_control_min_soc: + power_limit = None + log.debug(("Aktive Speichersteuerung: Entladung - " + f"Speicher (ID: {bat_component.component_config.id}) " + "befindet sich unterhalb minimal SoC - auf Eigenregelung gesetzt.")) + # setze Entladeleistung als Bruchteil der möglichen Entladeleistung + else: + factor = max(power / max_discharge_power_total, -1) + power_limit = bat_component_data.get.max_discharge_power * factor + log.debug(("Aktive Speichersteuerung: Entladung - " + f"Speicher (ID: {bat_component.component_config.id}) " + f"entlädt mit {power_limit} ({factor} x " + f"{bat_component_data.get.max_discharge_power})")) + else: + # oberhalb des max_soc soll Speicher nicht entladen wenn andere Speicher laden + if bat_component_data.get.soc >= self.data.config.bat_control_max_soc: + power_limit = 0 + log.debug(("Aktive Speichersteuerung: Ladung - " + f"Speicher (ID: {bat_component.component_config.id}) " + "befindet sich oberhalb maximal SoC - Speicher sperren.")) + else: + factor = min(power / max_charge_power_total, 1) + power_limit = bat_component_data.get.max_charge_power * factor + log.debug(("Aktive Speichersteuerung: Ladung - " + f"Speicher (ID: {bat_component.component_config.id}) " + f"lädt mit {power_limit} ({factor} x {bat_component_data.get.max_charge_power})")) + + # power_limit = self._limit_bat_power_discharge(power_limit) + data.data.bat_data[f"bat{bat_component.component_config.id}"].data.set.power_limit = power_limit + log.debug(f"Power Limit {power_limit}W an Speicher übergeben!") + def setup_bat(self): """ prüft, ob mind ein Speicher vorhanden ist und berechnet die Summen-Topics. """ @@ -215,9 +322,7 @@ def _get_charging_power_left(self): # Speicher sollte weder ge- noch entladen werden. charging_power_left = self.data.get.power else: - # Speicher soll geladen werden um min SoC zu erreichen if self.data.get.soc < config.min_bat_soc: - self.data.set.hysteresis_discharge = False 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 @@ -238,30 +343,10 @@ def _get_charging_power_left(self): # Speicher wird geladen charging_power_left = 0 self.data.set.regulate_up = True - # Speicher zwischen min und max SoC - elif int(self.data.get.soc) >= config.min_bat_soc and int(self.data.get.soc) < config.max_bat_soc: - # Speicher soll aktiv weder ge- noch entladen werden. - # Mindest-SoC wird gehalten oder der Speicher mit weiterem vorhanden Überschuss geladen. - if self.data.set.hysteresis_discharge is False: - charging_power_left = self.data.get.power - # Speicher darf wegen Hysterese bis min_bat_soc entladen werden. - else: - if self.data.set.power_limit is None: - if config.bat_power_discharge_active: - # Wenn der Speicher mit mehr als der erlaubten Entladeleistung entladen wird, muss das - # vom Überschuss subtrahiert werden. - charging_power_left = config.bat_power_discharge + self.data.get.power - log.debug(f"Erlaubte Entlade-Leistung nutzen {charging_power_left}W") - else: - # Speicher sollte weder ge- noch entladen werden. - charging_power_left = self.data.get.power - else: - log.debug("Keine erlaubte Entladeleistung freigeben, da der Speicher mit einer vorgegeben " - "Leistung entladen wird.") - charging_power_left = 0 - # Speicher oberhalb max SoC. Darf bis min SoC entladen werden. + elif int(self.data.get.soc) == config.min_bat_soc: + # Speicher sollte weder ge- noch entladen werden, um den Mindest-SoC zu halten. + charging_power_left = self.data.get.power else: - self.data.set.hysteresis_discharge = True if self.data.set.power_limit is None: if config.bat_power_discharge_active: # Wenn der Speicher mit mehr als der erlaubten Entladeleistung entladen wird, muss das @@ -312,52 +397,153 @@ def set_power_limit_controllable(self): else: self.data.get.power_limit_controllable = False - def get_power_limit(self): - if self.data.config.bat_control_permitted is False: - self.data.set.power_limit = None + def get_charge_mode_vehicle_charge(self): + chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) + # Fahrzeuge laden + vehicle_charging = (len(chargepoint_by_chargemodes) > 0 and + data.data.cp_all_data.data.get.power > 100) + # 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)) + # 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) + + if ( + vehicle_charging and + evu_power_valid and + bat_power_valid + ): + charge_mode = BatChargeMode.BAT_USE_LIMIT + log.debug("Speicher-Entladung beschränken da Fahrzeuge laden.") else: - chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) - # Falls aktive Steuerung an und Fahrzeuge laden und kein Überschuss im System ist, - # dann Speicherleistung begrenzen. - if (self.data.config.power_limit_mode != BatPowerLimitMode.NO_LIMIT.value and - len(chargepoint_by_chargemodes) > 0 and - data.data.cp_all_data.data.get.power > 100 and - self.data.get.power_limit_controllable and - self.data.get.power <= 0 and - data.data.counter_all_data.get_evu_counter().data.get.power >= -100): - if self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_STOP.value: - self.data.set.power_limit = 0 - elif self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value: - 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") + charge_mode = BatChargeMode.BAT_SELF_REGULATION + + # Debug Informationen + control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] + control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] + control_range_center = control_range_high - (control_range_high - control_range_low) / 2 + if len(chargepoint_by_chargemodes) == 0: + log.debug("Speicher-Leistung nicht begrenzen, da keine Ladepunkte in einem aktiven Lademodus sind.") + elif data.data.cp_all_data.data.get.power <= 100: + log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt lädt.") + elif self.data.get.power > 0: + log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") + elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: + # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen + # werden. + log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") else: - self.data.set.power_limit = None - control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] - control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] - control_range_center = control_range_high - (control_range_high - control_range_low) / 2 - if len(chargepoint_by_chargemodes) == 0: - log.debug("Speicher-Leistung nicht begrenzen, " - "da keine Ladepunkte in einem Lademodus mit Netzbezug sind.") - elif data.data.cp_all_data.data.get.power <= 100: - log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt mit Netzbezug lädt.") - elif self.data.get.power_limit_controllable is False: - log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") - elif self.data.get.power > 0: - log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") - elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: - # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen - # werden. - log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") - else: - log.debug("Speicher-Leistung nicht begrenzen.") - remaining_power_limit = self.data.set.power_limit - for bat_component in get_controllable_bat_components(): - if self.data.set.power_limit is None: - power_limit = None + log.debug("Speicher-Leistung nicht begrenzen.") + return charge_mode + + 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 or + self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value) + 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)) + if (evu_power_valid and bat_power_valid): + if self.data.config.manual_mode == ManualMode.MANUAL_LIMIT.value: + log.debug("Aktive Speichersteuerung: Manueller Modus - Regellimit anwenden.") + return BatChargeMode.BAT_USE_LIMIT + elif self.data.config.manual_mode == ManualMode.MANUAL_CHARGE.value: + log.debug("Aktive Speichersteuerung: Manueller Modus - Speicher laden.") + return BatChargeMode.BAT_FORCE_CHARGE + elif self.data.config.manual_mode == ManualMode.MANUAL_DISCHARGE.value: + log.debug("Aktive Speichersteuerung: Manueller Modus - Speicher entladen.") + return BatChargeMode.BAT_FORCE_DISCHARGE else: - power_limit = self._limit_bat_power_discharge(remaining_power_limit) + log.debug("Aktive Speichersteuerung: Manueller Modus - Steuerung Aus.") + return BatChargeMode.BAT_SELF_REGULATION + # manual_disable and fallback + else: + if evu_power_valid: + log.debug("Aktive Speichersteuerung: Manueller Modus - EVU-Einspeisung vorhanden oder " + "Ladung in Höhe des PV-Ertrags konfiguriert.") + if bat_power_valid: + log.debug("Aktive Speichersteuerung: Manueller Modus - Speicher entlädt oder " + "Ladung in Höhe des PV-Ertrags konfiguriert.") + return BatChargeMode.BAT_SELF_REGULATION + + def get_charge_mode_electricity_tariff(self): + if data.data.optional_data.data.electricity_pricing.configured: + if (self.data.config.price_charge_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.charge_limit)): + log.debug((f"Aktive Speichersteuerung: Ladung erzwingen. " + f"Preislimit: {self.data.config.charge_limit}")) + return BatChargeMode.BAT_FORCE_CHARGE + elif (self.data.config.price_limit_activated and + data.data.optional_data.ep_is_charging_allowed_price_threshold( + self.data.config.price_limit)): + log.debug((f"Aktive Speichersteuerung: Ladelimit anwenden. " + f"Preislimit: {self.data.config.price_limit}")) + return BatChargeMode.BAT_USE_LIMIT + else: + return BatChargeMode.BAT_SELF_REGULATION + else: + return BatChargeMode.BAT_SELF_REGULATION - data.data.bat_data[f"bat{bat_component.component_config.id}"].data.set.power_limit = power_limit + def get_charge_mode_scheduled(self): + pass + + def get_power_limit(self): + # Falls kein steuerbarer Speicher installiert ist, der Disclaimer nicht akzeptiert wurde + # oder die aktive Speichersteuerung deaktiviert wurde + if (self.data.get.power_limit_controllable is False or + self.data.config.bat_control_permitted is False or + self.data.config.bat_control_activated is False): + charge_mode = BatChargeMode.BAT_SELF_REGULATION + if self.data.get.power_limit_controllable is False: + log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") + elif self.data.config.bat_control_permitted is False: + log.debug("Speicher-Leistung nicht begrenzen, da der aktiven Speichersteuerung nicht zugestimmt wurde.") + elif self.data.get.power_limit_controllable is False: + 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: + 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: + 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: + log.debug("Aktive Speichersteuerung: Strompreisbasiert.") + charge_mode = self.get_charge_mode_electricity_tariff() + elif self.data.config.power_limit_condition == BatPowerLimitCondition.SCHEDULED.value: + log.debug("Aktive Speichersteuerung: Vorhersagebasiertes Zielladen.") + pass + + # calculate power_limit + controllable_bat_components = get_controllable_bat_components() + if charge_mode == BatChargeMode.BAT_SELF_REGULATION: + 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: + 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: + 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: + 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: + # maximal konfigurierte Ladeleistung des Speichers setzen + max_charge_power_total = 0 + for bat_component in controllable_bat_components: + bat_component_data = data.data.bat_data[f"bat{bat_component.component_config.id}"].data + max_charge_power_total += bat_component_data.get.max_charge_power + self.data.set.power_limit = max_charge_power_total + elif charge_mode == BatChargeMode.BAT_FORCE_DISCHARGE: + # das ist in Deutschland (noch) nicht erlaubt + pass + self._set_bat_power_active_control(self.data.set.power_limit) def get_controllable_bat_components() -> List: diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 1429cdd1ad..61d1f080cf 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -6,7 +6,7 @@ from control import bat_all from control.bat import Bat -from control.bat_all import BatAll, BatPowerLimitMode +from control.bat_all import BatAll, BatPowerLimitMode, BatPowerLimitCondition, ManualMode from control import data from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_all import AllChargepointData, AllChargepoints, AllGet @@ -170,41 +170,171 @@ def default_chargepoint_factory() -> List[Chargepoint]: @dataclass -class PowerLimitParams: +class BatControlParams: name: str expected_power_limit_bat: Optional[float] - power_limit_mode: str = BatPowerLimitMode.NO_LIMIT.value + 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 cps: List[Chargepoint] = field(default_factory=default_chargepoint_factory) power_limit_controllable: bool = True bat_power: float = -10 + bat_soc: float = 50.0 evu_power: float = 200 + bat_control_permitted: bool = True + bat_control_activated: bool = True + max_charge_power: float = 5000 + max_discharge_power: float = 5000 + bat_control_min_soc: float = 10.0 + bat_control_max_soc: float = 90.0 + price_limit_activated: bool = False + price_charge_activated: bool = False + price_limit: float = 0.30 + charge_limit: float = 0.30 cases = [ - PowerLimitParams("keine Begrenzung", None), - PowerLimitParams("Begrenzung immer, keine LP im Sofortladen", None, cps=[], - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung immer, Speicher nicht regelbar", None, power_limit_controllable=False, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung immer, Speicher lädt", None, bat_power=100, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung immer,Einspeisung", None, evu_power=-110, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung immer", 0, power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung Hausverbrauch", -456, - power_limit_mode=BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value), + BatControlParams("Speicher nicht regelbar", None, power_limit_controllable=False, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Disclaimer nicht akzeptiert", None, bat_control_permitted=False, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Speichersteuerung deaktiviert", None, bat_control_activated=False, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + # Manuelle Steuerung + BatControlParams("Manuelle Steuerung, Speichersteuerung deaktiviert", None, + power_limit_condition=BatPowerLimitCondition.MANUAL.value, + bat_manual_mode=ManualMode.MANUAL_DISABLE.value), + BatControlParams("Manuelle Steuerung, Entladung sperren", 0, + power_limit_condition=BatPowerLimitCondition.MANUAL.value, + bat_manual_mode=ManualMode.MANUAL_LIMIT.value), + 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), + 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), + BatControlParams("Manuelle Steuerung, Aktive Ladung", 5000, + power_limit_condition=BatPowerLimitCondition.MANUAL.value, + bat_manual_mode=ManualMode.MANUAL_CHARGE.value), + # Wenn Fahrzeuge Laden + BatControlParams("Fahrzeuge laden, Begrenzung immer, keine LP im Sofortladen", None, cps=[], + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Fahrzeuge laden, Begrenzung immer, Speicher lädt", None, bat_power=100, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Fahrzeuge laden, Begrenzung immer,Einspeisung", None, evu_power=-110, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Fahrzeuge laden, Begrenzung immer", 0, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Fahrzeuge laden, Begrenzung Hausverbrauch", -456, + power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value), + BatControlParams("Fahrzeuge laden, Ladung PV Überschuss", 654, + power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value), ] @pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): +def test_active_bat_control(params: BatControlParams, data_, monkeypatch): b_all = BatAll() - b_all.data.config.bat_control_permitted = True + b_all.data.config.bat_control_permitted = params.bat_control_permitted + b_all.data.config.bat_control_activated = params.bat_control_activated b_all.data.config.power_limit_mode = params.power_limit_mode + b_all.data.config.power_limit_condition = params.power_limit_condition + b_all.data.config.manual_mode = params.bat_manual_mode b_all.data.get.power_limit_controllable = params.power_limit_controllable + b_all.data.config.bat_control_min_soc = params.bat_control_min_soc + b_all.data.config.bat_control_max_soc = params.bat_control_max_soc + b_all.data.config.price_limit_activated = params.price_limit_activated + b_all.data.config.price_charge_activated = params.price_charge_activated + b_all.data.config.price_limit = params.price_limit + b_all.data.config.charge_limit = params.charge_limit + + b_all.data.get.power = params.bat_power + # b_all.data.get.soc = 50.0 + data.data.counter_all_data = hierarchy_standard() + data.data.counter_all_data.data.set.home_consumption = 456 + data.data.pv_all_data.data.get.power = -654 + data.data.cp_all_data.data.get.power = 1400 + data.data.counter_data["counter0"].data.get.power = params.evu_power + data.data.bat_all_data = b_all + + get_chargepoints_by_chargemodes_mock = Mock(return_value=params.cps) + monkeypatch.setattr(bat_all, "get_chargepoints_by_chargemodes", get_chargepoints_by_chargemodes_mock) + get_evu_counter_mock = Mock(return_value=data.data.counter_data["counter0"]) + monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock) + get_controllable_bat_components_mock = Mock(return_value=[MqttBat(MqttBatSetup(id=2), device_id=0)]) + data.data.bat_data["bat2"].data.get.soc = params.bat_soc + data.data.bat_data["bat2"].data.get.max_charge_power = params.max_charge_power + data.data.bat_data["bat2"].data.get.max_discharge_power = params.max_discharge_power + monkeypatch.setattr(bat_all, "get_controllable_bat_components", get_controllable_bat_components_mock) + + data.data.bat_all_data.get_power_limit() + + assert data.data.bat_data["bat2"].data.set.power_limit == params.expected_power_limit_bat + + +cases = [ + # Nach Preisgrenze + BatControlParams("Preisgrenze, Grenze deaktiviert, Eigenregelung", None, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_limit_activated=False, + price_limit=0.40, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Preisgrenze, Entladung sperren, Grenze unterschritten", 0, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_limit_activated=True, + price_limit=0.30, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + BatControlParams("Preisgrenze, Überschuss Laden, Grenze unterschritten", 654, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_limit_activated=True, + price_limit=0.30, + power_limit_mode=BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value), + BatControlParams("Preisgrenze, Entladung sperren, Grenze greift nicht", None, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_limit_activated=True, + price_limit=0.10, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + # Aktive Ladung + BatControlParams("Preisgrenze, Grenze deaktiviert, Eigenregelung", None, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_charge_activated=False, + charge_limit=0.40), + BatControlParams("Preisgrenze, Grenze unterschritten, Ladung", 5000, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_charge_activated=True, + charge_limit=0.30), + BatControlParams("Preisgrenze, Grenze greift nicht, Eigenregelung", None, + power_limit_condition=BatPowerLimitCondition.PRICE_LIMIT.value, + price_charge_activated=True, + charge_limit=0.10), +] + + +@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) +def test_control_price_limit(params: BatControlParams, data_, monkeypatch): + monkeypatch.setattr(data.data.optional_data, "ep_get_current_price", Mock(return_value=0.2)) + b_all = BatAll() + b_all.data.config.bat_control_permitted = params.bat_control_permitted + b_all.data.config.bat_control_activated = params.bat_control_activated + b_all.data.config.power_limit_mode = params.power_limit_mode + b_all.data.config.power_limit_condition = params.power_limit_condition + b_all.data.config.manual_mode = params.bat_manual_mode + b_all.data.get.power_limit_controllable = params.power_limit_controllable + b_all.data.config.bat_control_min_soc = params.bat_control_min_soc + b_all.data.config.bat_control_max_soc = params.bat_control_max_soc + b_all.data.config.price_limit_activated = params.price_limit_activated + b_all.data.config.price_charge_activated = params.price_charge_activated + b_all.data.config.price_limit = params.price_limit + b_all.data.config.charge_limit = params.charge_limit + b_all.data.get.power = params.bat_power + # b_all.data.get.soc = 50.0 + data.data.optional_data.data.electricity_pricing.configured = True data.data.counter_all_data = hierarchy_standard() data.data.counter_all_data.data.set.home_consumption = 456 + data.data.pv_all_data.data.get.power = -654 data.data.cp_all_data.data.get.power = 1400 data.data.counter_data["counter0"].data.get.power = params.evu_power data.data.bat_all_data = b_all @@ -214,6 +344,9 @@ def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): get_evu_counter_mock = Mock(return_value=data.data.counter_data["counter0"]) monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock) get_controllable_bat_components_mock = Mock(return_value=[MqttBat(MqttBatSetup(id=2), device_id=0)]) + data.data.bat_data["bat2"].data.get.soc = params.bat_soc + data.data.bat_data["bat2"].data.get.max_charge_power = params.max_charge_power + data.data.bat_data["bat2"].data.get.max_discharge_power = params.max_discharge_power monkeypatch.setattr(bat_all, "get_controllable_bat_components", get_controllable_bat_components_mock) data.data.bat_all_data.get_power_limit() diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 067ede9af8..3c0f1bf582 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -659,10 +659,22 @@ def process_bat_topic(self, msg: mqtt.MQTTMessage): """ try: if ("openWB/set/bat/config/bat_control_permitted" in msg.topic or + "openWB/set/bat/config/bat_control_activated" in msg.topic or + "openWB/set/bat/config/price_limit_actived" in msg.topic or + "openWB/set/bat/config/price_charge_activated" in msg.topic or "openWB/set/bat/config/configured" in msg.topic or "openWB/set/bat/get/power_limit_controllable" in msg.topic or "openWB/set/bat/set/regulate_up" in msg.topic): self._validate_value(msg, bool) + elif (re.search("openWB/set/bat/[0-9]+/get/max_charge_power$", msg.topic) is not None or + re.search("openWB/set/bat/[0-9]+/get/max_discharge_power$", msg.topic) is not None): + self._validate_value(msg, float, [(0, float("inf"))]) + elif ("openWB/set/bat/config/price_limit" in msg.topic or + "openWB/set/bat/config/charge_limit" in msg.topic): + self._validate_value(msg, float, [(0, 99.99)]) + elif ("openWB/set/bat/config/bat_control_min_soc" in msg.topic or + "openWB/set/bat/config/bat_control_max_soc" in msg.topic): + self._validate_value(msg, int, [(0, 100)]) elif "openWB/set/bat/set/charging_power_left" in msg.topic: self._validate_value(msg, float) elif "openWB/set/bat/get/soc" in msg.topic: @@ -678,7 +690,9 @@ def process_bat_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/bat/get/fault_state" in msg.topic: self._validate_value(msg, int, [(0, 2)]) elif ("openWB/set/bat/get/fault_str" in msg.topic or - "openWB/set/bat/config/power_limit_mode" in msg.topic): + "openWB/set/bat/config/power_limit_mode" in msg.topic or + "openWB/set/bat/config/power_limit_condition" in msg.topic or + "openWB/set/bat/config/manual_mode" in msg.topic): self._validate_value(msg, str) elif "/config" in msg.topic: self._validate_value(msg, "json") diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index a4791b4962..a9a479bc98 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -61,8 +61,20 @@ class UpdateConfig: valid_topic = [ "^openWB/bat/config/bat_control_permitted$", - "^openWB/bat/config/configured$", + "^openWB/bat/config/bat_control_activated$", "^openWB/bat/config/power_limit_mode$", + "^openWB/bat/config/power_limit_condition$", + "^openWB/bat/config/bat_control_min_soc$", + "^openWB/bat/config/bat_control_max_soc$", + "^openWB/bat/config/manual_mode$", + "^openWB/bat/config/price_limit_activated$", + "^openWB/bat/config/price_limit$", + "^openWB/bat/config/price_charge_activated$", + "^openWB/bat/config/charge_limit$", + "^openWB/bat/[0-9]+/get/max_charge_power$", + "^openWB/bat/[0-9]+/get/max_discharge_power$", + + "^openWB/bat/config/configured$", "^openWB/bat/set/charging_power_left$", "^openWB/bat/set/regulate_up$", "^openWB/bat/get/fault_state$", @@ -511,8 +523,19 @@ class UpdateConfig: ] default_topic = ( ("openWB/bat/config/bat_control_permitted", False), + ("openWB/bat/config/bat_control_activated", False), + ("openWB/bat/config/power_limit_mode", "mode_no_discharge"), + ("openWB/bat/config/power_limit_condition", "vehicle_charging"), + ("openWB/bat/config/bat_control_min_soc", 10), + ("openWB/bat/config/bat_control_max_soc", 90), + ("openWB/bat/config/manual_mode", "manual_disable"), + ("openWB/bat/config/price_limit_activated", False), + # "^openWB/bat/config/price_limit$", + ("openWB/bat/config/price_charge_activated", False), + # "^openWB/bat/config/charge_limit$", + # ("openWB/bat/[0-9]+/get/max_charge_power", ), + # ("openWB/bat/[0-9]+/get/max_discharge_power",), ("openWB/bat/config/configured", False), - ("openWB/bat/config/power_limit_mode", "no_limit"), ("openWB/bat/get/fault_state", 0), ("openWB/bat/get/fault_str", NO_ERROR), ("openWB/bat/get/power_limit_controllable", False),