From b3ff9951319798d265e402b4addf0b36d45bb121 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 19 Feb 2025 11:26:38 +0100 Subject: [PATCH 1/3] fix virtual counter --- data/config/mosquitto/openwb_local.conf | 2 + packages/conftest.py | 24 +++++- packages/control/bat_all_test.py | 2 +- packages/helpermodules/update_config.py | 38 +++++++++- .../chargepoints/mqtt/chargepoint_module.py | 56 +++++++++++--- packages/modules/common/store/_counter.py | 66 +++++++++-------- .../modules/common/store/_counter_test.py | 74 +++++++++++++++---- .../modules/common/utils/component_parser.py | 13 +++- packages/modules/devices/generic/mqtt/bat.py | 38 ++++++++-- .../modules/devices/generic/mqtt/counter.py | 40 +++++++++- .../modules/devices/generic/mqtt/device.py | 27 ++++++- .../modules/devices/generic/mqtt/inverter.py | 29 +++++++- .../devices/generic/virtual/counter_test.py | 59 +++++++++++++-- packages/modules/loadvars.py | 4 +- 14 files changed, 390 insertions(+), 82 deletions(-) diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index 8f03b56f6c..0eaec08093 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -31,6 +31,8 @@ topic openWB/internal_chargepoint/# out 2 topic openWB/io/# out 2 topic openWB/internal_io/# out 2 +topic openWB/mqtt/# both 2 + topic openWB/pv/config/configured out 2 topic openWB/pv/get/# out 2 topic openWB/pv/+/config/# out 2 diff --git a/packages/conftest.py b/packages/conftest.py index a66e5bbc15..a185b7cfc0 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -16,6 +16,9 @@ from control.pv import Pv, PvData from control.pv import Get as PvGet from helpermodules import hardware_configuration, pub, timecheck +from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule +from modules.common.component_state import ChargepointState +from modules.common.store._api import LoggingValueStore @pytest.fixture(autouse=True) @@ -118,19 +121,34 @@ def data_() -> None: get=Mock(spec=Get, currents=[30, 0, 0], power=6900, daily_imported=10000, daily_exported=0, imported=56000, fault_state=0), - set=Mock(spec=Set, loadmanagement_available=True))), + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[30, 0, 0], + power=6900))))), "cp4": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, config=Mock(spec=Config, phase_1=2), get=Mock(spec=Get, currents=[0, 15, 15], power=6900, daily_imported=10000, daily_exported=0, imported=60000, fault_state=0), - set=Mock(spec=Set, loadmanagement_available=True))), + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[0, 15, 15], + power=6900))))), "cp5": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, config=Mock(spec=Config, phase_1=3), get=Mock(spec=Get, currents=[10]*3, power=6900, daily_imported=10000, daily_exported=0, imported=62000, fault_state=0), - set=Mock(spec=Set, loadmanagement_available=True)))} + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[10]*3, + power=6900)))))} data.data.bat_data.update({"bat2": Mock(spec=Bat, num=2, data=Mock(spec=BatData, get=Mock( spec=BatGet, power=-5000, daily_imported=7000, daily_exported=3000, imported=12000, exported=10000, currents=None, fault_state=0), diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 25122d5db5..ddca630a69 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -225,7 +225,7 @@ def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): 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))]) + get_controllable_bat_components_mock = Mock(return_value=[MqttBat(MqttBatSetup(id=2), device_id=0)]) 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/update_config.py b/packages/helpermodules/update_config.py index 3936268af8..162ab72765 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -53,7 +53,9 @@ class UpdateConfig: - DATASTORE_VERSION = 77 + + DATASTORE_VERSION = 79 + valid_topic = [ "^openWB/bat/config/configured$", "^openWB/bat/config/power_limit_mode$", @@ -254,6 +256,23 @@ class UpdateConfig: "^openWB/io/action/[0-9]+/config$", "^openWB/io/action/[0-9]+/timestamp$", + "^openWB/mqtt/bat/[0-9]+/get/power$", + "^openWB/mqtt/bat/[0-9]+/get/soc$", + "^openWB/mqtt/bat/[0-9]+/get/imported$", + "^openWB/mqtt/bat/[0-9]+/get/exported$", + "^openWB/mqtt/counter/[0-9]+/get/currents$", + "^openWB/mqtt/counter/[0-9]+/get/imported$", + "^openWB/mqtt/counter/[0-9]+/get/exported$", + "^openWB/mqtt/counter/[0-9]+/get/power$", + "^openWB/mqtt/counter/[0-9]+/get/frequency$", + "^openWB/mqtt/counter/[0-9]+/get/power_factors$", + "^openWB/mqtt/counter/[0-9]+/get/powers$", + "^openWB/mqtt/counter/[0-9]+/get/voltages$", + "^openWB/mqtt/inverter/[0-9]+/get/currents$", + "^openWB/mqtt/inverter/[0-9]+/get/power$", + "^openWB/mqtt/inverter/[0-9]+/get/exported$", + "^openWB/mqtt/inverter/[0-9]+/get/dc_power$", + "^openWB/set/log/request", "^openWB/set/log/data", @@ -489,7 +508,7 @@ class UpdateConfig: ("openWB/general/chargemode_config/phase_switch_delay", 7), ("openWB/general/chargemode_config/pv_charging/phases_to_use", 0), ("openWB/general/chargemode_config/retry_failed_phase_switches", - ChargemodeConfig().retry_failed_phase_switches), + ChargemodeConfig().retry_failed_phase_switches), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use", 0), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv", 0), ("openWB/general/chargemode_config/time_charging/phases_to_use", 1), @@ -2048,3 +2067,18 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: configuration_payload} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 78) + + def upgrade_datastore_78(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + for topic, payload in self.all_received_topics.items(): + if (re.search("openWB/system/device/[0-9]+", topic) is not None or + re.search("openWB/chargepoint/[0-9]+/config", topic) is not None): + payload = decode_payload(payload) + if payload.get("type") == "mqtt": + pub_system_message( + {}, "Die Topics für MQTT-Komponenten und MQTT-Ladepunkte wurden angepasst. Bitte aktualsiere " + "die Topics in Deinen angebundenen Systemen.", MessageType.WARNING) + # Nachricht nur einmal senden + break + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 79) diff --git a/packages/modules/chargepoints/mqtt/chargepoint_module.py b/packages/modules/chargepoints/mqtt/chargepoint_module.py index e340ff7b40..0ab5e561e5 100644 --- a/packages/modules/chargepoints/mqtt/chargepoint_module.py +++ b/packages/modules/chargepoints/mqtt/chargepoint_module.py @@ -1,11 +1,16 @@ import logging -from helpermodules.utils.error_handling import CP_ERROR, ErrorTimerContext +from helpermodules.broker import InternalBrokerClient +from helpermodules.pub import Pub +from helpermodules.utils.topic_parser import decode_payload from modules.chargepoints.mqtt.config import Mqtt from modules.common.abstract_chargepoint import AbstractChargepoint from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import SingleComponentUpdateContext +from modules.common.component_state import ChargepointState from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store._chargepoint import get_chargepoint_value_store + log = logging.getLogger(__name__) @@ -13,21 +18,54 @@ class ChargepointModule(AbstractChargepoint): def __init__(self, config: Mqtt) -> None: self.config = config - self.fault_state = FaultState(ComponentInfo( - self.config.id, - "Ladepunkt", "chargepoint")) - self.client_error_context = ErrorTimerContext( - f"openWB/set/chargepoint/{self.config.id}/get/error_timestamp", CP_ERROR, hide_exception=True) + self.store = get_chargepoint_value_store(self.config.id) + self.fault_state = FaultState(ComponentInfo(self.config.id, "Ladepunkt", "chargepoint")) def set_current(self, current: float) -> None: - log.debug("MQTT-Ladepunkte abonnieren die Soll-Stromstärke direkt vom Broker.") + Pub().pub(f"openWB/mqtt/chargepoint/{self.config.id}/set/current", current) def get_values(self) -> None: with SingleComponentUpdateContext(self.fault_state): - log.debug("MQTT-Ladepunkte müssen nicht ausgelesen werden.") + def on_connect(client, userdata, flags, rc): + client.subscribe(f"openWB/mqtt/chargepoint/{self.config.id}/#") + + def on_message(client, userdata, message): + received_topics.update({message.topic: decode_payload(message.payload)}) + + received_topics = {} + InternalBrokerClient(f"subscribeMqttChargepoint{self.config.id}", + on_connect, on_message).start_finite_loop() + + if received_topics: + log.debug(f"Empfange MQTT Daten für Ladepunkt {self.config.id}: {received_topics}") + topic_prefix = f"openWB/mqtt/chargepoint/{self.config.id}/get/" + chargepoint_state = ChargepointState( + power=received_topics.get(f"{topic_prefix}power"), + phases_in_use=received_topics.get(f"{topic_prefix}phases_in_use"), + imported=received_topics.get(f"{topic_prefix}imported"), + exported=received_topics.get(f"{topic_prefix}exported"), + serial_number=received_topics.get(f"{topic_prefix}serial_number"), + powers=received_topics.get(f"{topic_prefix}powers"), + voltages=received_topics.get(f"{topic_prefix}voltages"), + currents=received_topics.get(f"{topic_prefix}currents"), + power_factors=received_topics.get(f"{topic_prefix}power_factors"), + plug_state=received_topics.get(f"{topic_prefix}plug_state"), + charge_state=received_topics.get(f"{topic_prefix}charge_state"), + rfid=received_topics.get(f"{topic_prefix}rfid"), + rfid_timestamp=received_topics.get(f"{topic_prefix}rfid_timestamp"), + frequency=received_topics.get(f"{topic_prefix}frequency"), + soc=received_topics.get(f"{topic_prefix}soc"), + soc_timestamp=received_topics.get(f"{topic_prefix}soc_timestamp"), + vehicle_id=received_topics.get(f"{topic_prefix}vehicle_id"), + evse_current=received_topics.get(f"{topic_prefix}evse_current"), + max_evse_current=received_topics.get(f"{topic_prefix}max_evse_current") + ) + self.store.set(chargepoint_state) + else: + raise Exception(f"Keine MQTT Daten für Gerät {self.config.id} empfangen") def switch_phases(self, phases_to_use: int, duration: int) -> None: - log.warning("Phasenumschaltung für MQTT-Ladepunkte nicht unterstützt.") + Pub().pub(f"openWB/mqtt/chargepoint/{self.config.id}/set/phases_to_use", phases_to_use) chargepoint_descriptor = DeviceDescriptor(configuration_factory=Mqtt) diff --git a/packages/modules/common/store/_counter.py b/packages/modules/common/store/_counter.py index 06254e4dac..d1f9dcf052 100644 --- a/packages/modules/common/store/_counter.py +++ b/packages/modules/common/store/_counter.py @@ -1,3 +1,4 @@ +import logging from operator import add from control import data @@ -10,6 +11,9 @@ from modules.common.store._api import LoggingValueStore from modules.common.store._broker import pub_to_broker from modules.common.store.ramdisk import files +from modules.common.utils.component_parser import get_component_obj_by_id + +log = logging.getLogger(__name__) class CounterValueStoreRamdisk(ValueStore[CounterState]): @@ -70,50 +74,52 @@ def calc_virtual(self, state: CounterState) -> CounterState: self.incomplete_currents = False def add_current_power(element): - if element.data.get.currents is not None: - if sum(element.data.get.currents) == 0 and element.data.get.power != 0: + if hasattr(element, "currents") and element.currents is not None: + if sum(element.currents) == 0 and element.power != 0: self.currents = [0, 0, 0] self.incomplete_currents = True else: - self.currents = list(map(add, self.currents, element.data.get.currents)) + self.currents = list(map(add, self.currents, element.currents)) else: self.currents = [0, 0, 0] self.incomplete_currents = True - self.power += element.data.get.power + self.power += element.power def add_imported_exported(element): - self.imported += element.data.get.imported - self.exported += element.data.get.exported + self.imported += element.imported + self.exported += element.exported def add_exported(element): - self.exported += element.data.get.exported + self.exported += element.exported counter_all = data.data.counter_all_data elements = counter_all.get_elements_for_downstream_calculation(self.delegate.delegate.num) for element in elements: - if element["type"] == ComponentType.CHARGEPOINT.value: - chargepoint = data.data.cp_data[f"cp{element['id']}"] - try: - self.currents = list(map(add, - self.currents, - convert_cp_currents_to_evu_currents( - chargepoint.data.config.phase_1, - chargepoint.data.get.currents))) - except KeyError: - raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt" - f" {chargepoint.data.config.name} an die Phasen des EVU Zählers " - "angegeben werden.") - self.power += chargepoint.data.get.power - self.imported += chargepoint.data.get.imported - elif element["type"] == ComponentType.BAT.value: - add_current_power(data.data.bat_data[f"bat{element['id']}"]) - add_imported_exported(data.data.bat_data[f"bat{element['id']}"]) - elif element["type"] == ComponentType.COUNTER.value: - add_current_power(data.data.counter_data[f"counter{element['id']}"]) - add_imported_exported(data.data.counter_data[f"counter{element['id']}"]) - elif element["type"] == ComponentType.INVERTER.value: - add_current_power(data.data.pv_data[f"pv{element['id']}"]) - add_exported(data.data.pv_data[f"pv{element['id']}"]) + try: + if element["type"] == ComponentType.CHARGEPOINT.value: + chargepoint = data.data.cp_data[f"cp{element['id']}"] + chargepoint_state = chargepoint.chargepoint_module.store.delegate.state + try: + self.currents = list(map(add, + self.currents, + convert_cp_currents_to_evu_currents( + chargepoint.data.config.phase_1, + chargepoint_state.currents))) + except KeyError: + raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt" + f" {chargepoint.data.config.name} an die Phasen des EVU Zählers " + "angegeben werden.") + self.power += chargepoint_state.power + self.imported += chargepoint_state.imported + else: + component = get_component_obj_by_id(element['id']) + add_current_power(component.store.delegate.state) + if element["type"] == ComponentType.INVERTER.value: + add_exported(component.store.delegate.state) + else: + add_imported_exported(component.store.delegate.state) + except Exception: + log.exception(f"Fehler beim Hinzufügen der Werte für Element {element}") if self.incomplete_currents: self.currents = None diff --git a/packages/modules/common/store/_counter_test.py b/packages/modules/common/store/_counter_test.py index 13772d020e..61fb785c42 100644 --- a/packages/modules/common/store/_counter_test.py +++ b/packages/modules/common/store/_counter_test.py @@ -7,15 +7,17 @@ from control import data -from control.bat import Bat, BatData -from control.bat import Get as BatGet from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter, CounterData, Get from control.counter_all import CounterAll -from control.pv import Pv, PvData -from control.pv import Get as PvGet -from modules.common.component_state import CounterState +from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule +from modules.common.component_state import BatState, ChargepointState, CounterState, InverterState +from modules.common.store import _counter +from modules.common.store._api import LoggingValueStore from modules.common.store._counter import PurgeCounterState +from modules.devices.generic.mqtt.bat import MqttBat +from modules.devices.generic.mqtt.counter import MqttCounter +from modules.devices.generic.mqtt.inverter import MqttInverter @pytest.fixture(autouse=True) @@ -28,21 +30,25 @@ def mock_data() -> None: def add_chargepoint(id: int): data.data.cp_data[f"cp{id}"] = Mock(spec=Chargepoint, id=id, - chargepoint_module=Mock(), data=Mock( config=Mock(phase_1=1), get=Mock(power=13359, currents=[19.36, 19.36, 19.36], imported=0, - exported=0))) + exported=0)), + chargepoint_module=Mock( + spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(power=13359, + currents=[ + 19.36, 19.36, 19.36], + imported=0, + exported=0))))) def mock_data_standard(): add_chargepoint(3) - data.data.bat_data["inverter1"] = Mock(spec=Pv, data=Mock( - spec=PvData, get=Mock(spec=PvGet, power=5786, exported=200))) - data.data.bat_data["bat2"] = Mock(spec=Bat, data=Mock( - spec=BatData, get=Mock(spec=BatGet, power=223, exported=200, imported=100))) data.data.counter_all_data.data.get.hierarchy = [{"id": 0, "type": "counter", "children": [{"id": 3, "type": "cp", "children": []}]}, {"id": 1, "type": "inverter", "children": []}, @@ -63,19 +69,57 @@ def mock_data_nested(): {"id": 3, "type": "cp", "children": []}]}]}] -Params = NamedTuple("Params", [("name", str), ("mock_data", Callable), ("expected_state", CounterState)]) +mock_comp_obj_inv_bat = [ + Mock(spec=MqttBat, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=BatState(power=223, + exported=200, + imported=100)))), + Mock(spec=MqttInverter, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=InverterState(power=5786, + exported=2000))))] + +mock_comp_obj_counter_inv_bat = [Mock(spec=MqttCounter, + store=Mock(spec=LoggingValueStore, + delegate=Mock( + spec=LoggingValueStore, + state=CounterState(power=13359, + exported=0, + imported=0, + currents=[19.36]*3)))), + Mock(spec=MqttBat, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=BatState(power=223, + exported=200, + imported=100)))), + Mock(spec=MqttInverter, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=InverterState(power=5786, + exported=2000)))) + ] + +Params = NamedTuple("Params", [("name", str), ("mock_comp", Mock), + ("mock_data", Callable), ("expected_state", CounterState)]) cases = [ - Params("standard", mock_data_standard, CounterState(power=8358, currents=[26.61]*3, exported=200, imported=100)), - Params("nested virtual", mock_data_nested, CounterState( + Params("standard", mock_comp_obj_inv_bat, mock_data_standard, CounterState( + power=8358, currents=[26.61]*3, exported=200, imported=100)), + Params("nested virtual", mock_comp_obj_counter_inv_bat, mock_data_nested, CounterState( power=21717, currents=[45.97]*3, exported=200, imported=100)) ] @pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_calc_virtual(params): +def test_calc_virtual(params: Params, monkeypatch): # setup params.mock_data() purge = PurgeCounterState(delegate=Mock(delegate=Mock(num=0)), add_child_values=True) + mock_comp_obj = Mock(side_effect=params.mock_comp) + monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_comp_obj) # execution state = purge.calc_virtual(CounterState(power=-5001, currents=[7.25]*3, exported=200, imported=100)) diff --git a/packages/modules/common/utils/component_parser.py b/packages/modules/common/utils/component_parser.py index 408eeda3a0..5dcbb226ac 100644 --- a/packages/modules/common/utils/component_parser.py +++ b/packages/modules/common/utils/component_parser.py @@ -27,7 +27,7 @@ def get_io_name_by_id(id: int): raise ValueError(f"Element {id} konnte keinem Gerät zugeordnet werden.") -def get_component_obj_by_id(id: int, not_finished_threads: List[str]) -> Optional[Any]: +def get_finished_component_obj_by_id(id: int, not_finished_threads: List[str]) -> Optional[Any]: for item in data.data.system_data.values(): if isinstance(item, AbstractDevice): for t in not_finished_threads: @@ -48,3 +48,14 @@ def get_component_obj_by_id(id: int, not_finished_threads: List[str]) -> Optiona else: log.error(f"Element {id} konnte keinem Gerät zugeordnet werden.") return None + + +def get_component_obj_by_id(id: int) -> Optional[Any]: + for item in data.data.system_data.values(): + if isinstance(item, AbstractDevice): + for comp in item.components.values(): + if comp.component_config.id == id: + return comp + else: + log.error(f"Element {id} konnte keinem Gerät zugeordnet werden.") + return None diff --git a/packages/modules/devices/generic/mqtt/bat.py b/packages/modules/devices/generic/mqtt/bat.py index 5e2d541f49..762a70720c 100644 --- a/packages/modules/devices/generic/mqtt/bat.py +++ b/packages/modules/devices/generic/mqtt/bat.py @@ -1,22 +1,50 @@ #!/usr/bin/env python3 -from typing import Optional +from typing import Any, Dict, Optional, TypedDict + +from helpermodules.pub import Pub from modules.common.abstract_device import AbstractBat +from modules.common.component_state import BatState from modules.common.fault_state import ComponentInfo, FaultState from modules.common.component_type import ComponentDescriptor +from modules.common.simcount._simcounter import SimCounter +from modules.common.store._battery import get_bat_value_store from modules.devices.generic.mqtt.config import MqttBatSetup +class KwargsDict(TypedDict): + device_id: int + + class MqttBat(AbstractBat): - def __init__(self, component_config: MqttBatSetup) -> None: + def __init__(self, component_config: MqttBatSetup, **kwargs: Any) -> None: self.component_config = component_config + self.kwargs: KwargsDict = kwargs def initialize(self) -> None: self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="bat") + self.store = get_bat_value_store(self.component_config.id) + + def update(self, received_topics: Dict) -> None: + power = received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/power") + soc = received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/soc") + if (received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/imported") and + received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/exported")): + imported = received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/imported") + exported = received_topics.get(f"openWB/mqtt/bat/{self.component_config.id}/get/exported") + else: + imported, exported = self.sim_counter.sim_count(power) + + bat_state = BatState( + power=power, + soc=soc, + imported=imported, + exported=exported + ) + self.store.set(bat_state) def set_power_limit(self, power_limit: Optional[int]) -> None: - # wird bereits in Regelung gesetzt, eigene Implementierung notwendig, um zu erkennen, - # ob der Speicher die Funktion bietet - pass + Pub().pub(f"openWB/mqtt/bat/{self.component_config.id}/set/powerLimit", power_limit) def power_limit_controllable(self) -> bool: return self.component_config.configuration.power_limit_controllable diff --git a/packages/modules/devices/generic/mqtt/counter.py b/packages/modules/devices/generic/mqtt/counter.py index 80285cf20e..abde9bf9a5 100644 --- a/packages/modules/devices/generic/mqtt/counter.py +++ b/packages/modules/devices/generic/mqtt/counter.py @@ -1,16 +1,54 @@ #!/usr/bin/env python3 +from typing import Any, Dict, TypedDict + from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState from modules.common.fault_state import ComponentInfo, FaultState from modules.common.component_type import ComponentDescriptor +from modules.common.simcount._simcounter import SimCounter +from modules.common.store._counter import get_counter_value_store from modules.devices.generic.mqtt.config import MqttCounterSetup +class KwargsDict(TypedDict): + device_id: int + + class MqttCounter(AbstractCounter): - def __init__(self, component_config: MqttCounterSetup) -> None: + def __init__(self, component_config: MqttCounterSetup, **kwargs: Any) -> None: self.component_config = component_config + self.kwargs: KwargsDict = kwargs def initialize(self) -> None: self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="bezug") + self.store = get_counter_value_store(self.component_config.id) + + def update(self, received_topics: Dict) -> None: + currents = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/currents") + power = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/power") + frequency = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/frequency") + power_factors = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/power_factors") + powers = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/powers") + voltages = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/voltages") + if (received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/imported") and + received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/exported")): + imported = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/imported") + exported = received_topics.get(f"openWB/mqtt/counter/{self.component_config.id}/get/exported") + else: + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + currents=currents, + imported=imported, + exported=exported, + power=power, + frequency=frequency, + power_factors=power_factors, + powers=powers, + voltages=voltages + ) + self.store.set(counter_state) component_descriptor = ComponentDescriptor(configuration_factory=MqttCounterSetup) diff --git a/packages/modules/devices/generic/mqtt/device.py b/packages/modules/devices/generic/mqtt/device.py index f3a70ddf38..81feb04ef8 100644 --- a/packages/modules/devices/generic/mqtt/device.py +++ b/packages/modules/devices/generic/mqtt/device.py @@ -2,7 +2,10 @@ from typing import Iterable, Union import logging +from helpermodules.broker import InternalBrokerClient +from helpermodules.utils.topic_parser import decode_payload from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_type import type_to_topic_mapping from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.generic.mqtt import bat, counter, inverter from modules.devices.generic.mqtt.config import Mqtt, MqttBatSetup, MqttCounterSetup, MqttInverterSetup @@ -12,16 +15,32 @@ def create_device(device_config: Mqtt): def create_bat_component(component_config: MqttBatSetup): - return bat.MqttBat(component_config) + return bat.MqttBat(component_config, device_id=device_config.id) def create_counter_component(component_config: MqttCounterSetup): - return counter.MqttCounter(component_config) + return counter.MqttCounter(component_config, device_id=device_config.id) def create_inverter_component(component_config: MqttInverterSetup): - return inverter.MqttInverter(component_config) + return inverter.MqttInverter(component_config, device_id=device_config.id) def update_components(components: Iterable[Union[bat.MqttBat, counter.MqttCounter, inverter.MqttInverter]]): - log.debug("MQTT-Module müssen nicht ausgelesen werden.") + def on_connect(client, userdata, flags, rc): + for component in components: + client.subscribe(f"openWB/mqtt/{type_to_topic_mapping(component.component_config.type)}/" + f"{component.component_config.id}/#") + + def on_message(client, userdata, message): + received_topics.update({message.topic: decode_payload(message.payload)}) + + received_topics = {} + InternalBrokerClient(f"subscribeMqttDevice{device_config.id}", on_connect, on_message).start_finite_loop() + + if received_topics: + log.debug(f"Empfange MQTT Daten für Gerät {device_config.id}: {received_topics}") + for component in components: + component.update(received_topics) + else: + raise Exception(f"Keine MQTT Daten für Gerät {device_config.id} empfangen") return ConfigurableDevice( device_config=device_config, diff --git a/packages/modules/devices/generic/mqtt/inverter.py b/packages/modules/devices/generic/mqtt/inverter.py index 5e6d1596f5..0eb1f7088b 100644 --- a/packages/modules/devices/generic/mqtt/inverter.py +++ b/packages/modules/devices/generic/mqtt/inverter.py @@ -1,16 +1,43 @@ #!/usr/bin/env python3 +from typing import Any, Dict, TypedDict + from modules.common.abstract_device import AbstractInverter +from modules.common.component_state import InverterState from modules.common.fault_state import ComponentInfo, FaultState from modules.common.component_type import ComponentDescriptor +from modules.common.simcount._simcounter import SimCounter +from modules.common.store._inverter import get_inverter_value_store from modules.devices.generic.mqtt.config import MqttInverterSetup +class KwargsDict(TypedDict): + device_id: int + + class MqttInverter(AbstractInverter): - def __init__(self, component_config: MqttInverterSetup) -> None: + def __init__(self, component_config: MqttInverterSetup, **kwargs: Any) -> None: self.component_config = component_config + self.kwargs: KwargsDict = kwargs def initialize(self) -> None: self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="pv") + self.store = get_inverter_value_store(self.component_config.id) + + def update(self, received_topics: Dict) -> None: + power = received_topics.get(f"openWB/mqtt/pv/{self.component_config.id}/get/power") + if received_topics.get(f"openWB/mqtt/pv/{self.component_config.id}/get/exported"): + exported = received_topics.get(f"openWB/mqtt/pv/{self.component_config.id}/get/exported") + else: + exported = self.sim_counter.sim_count(power)[1] + + inverter_state = InverterState( + currents=received_topics.get(f"openWB/mqtt/pv/{self.component_config.id}/get/currents"), + power=power, + exported=exported, + dc_power=received_topics.get(f"openWB/mqtt/pv/{self.component_config.id}/get/dc_power") + ) + self.store.set(inverter_state) component_descriptor = ComponentDescriptor(configuration_factory=MqttInverterSetup) diff --git a/packages/modules/devices/generic/virtual/counter_test.py b/packages/modules/devices/generic/virtual/counter_test.py index 6aea8b8c90..850d2e42b5 100644 --- a/packages/modules/devices/generic/virtual/counter_test.py +++ b/packages/modules/devices/generic/virtual/counter_test.py @@ -7,7 +7,14 @@ from control import data from control.chargepoint.chargepoint import Chargepoint from control.counter_all import CounterAll -from modules.common.component_state import CounterState +from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule +from modules.chargepoints.mqtt.config import Mqtt +from modules.common.component_state import BatState, ChargepointState, CounterState, InverterState +from modules.common.store import _counter +from modules.common.store._api import LoggingValueStore +from modules.devices.generic.mqtt.bat import MqttBat +from modules.devices.generic.mqtt.counter import MqttCounter +from modules.devices.generic.mqtt.inverter import MqttInverter from modules.devices.generic.virtual import counter from modules.devices.generic.virtual.config import VirtualCounterConfiguration, VirtualCounterSetup from packages.conftest import hierarchy_standard, hierarchy_hybrid, hierarchy_nested @@ -21,9 +28,11 @@ def init_data() -> None: {"id": 6, "type": "counter", "children": [ {"id": 3, "type": "cp", "children": []}, {"id": 4, "type": "cp", "children": []}]}]}] data.data.cp_data["cp3"] = Chargepoint(3, None) - data.data.cp_data["cp3"].data.get.currents = [16, 16, 0] + data.data.cp_data["cp3"].chargepoint_module = ChargepointModule(Mqtt()) + data.data.cp_data["cp3"].chargepoint_module.store.delegate.state = ChargepointState(currents=[16, 16, 0]) data.data.cp_data["cp4"] = Chargepoint(4, None) - data.data.cp_data["cp4"].data.get.currents = [16, 16, 16] + data.data.cp_data["cp4"].chargepoint_module = ChargepointModule(Mqtt()) + data.data.cp_data["cp4"].chargepoint_module.store.delegate.state = ChargepointState(currents=[16, 16, 16]) def init_twisted_cp() -> None: @@ -71,16 +80,50 @@ def test_virtual_counter(mock_pub: Mock, params): pytest.fail("Topic openWB/set/counter/6/get/currents is missing") -@pytest.mark.parametrize("counter_all", - [pytest.param(hierarchy_standard, id="standard"), - pytest.param(hierarchy_hybrid, id="hybrid"), - pytest.param(hierarchy_nested, id="nested")]) -def test_virtual_counter_hierarchies(counter_all: Callable[[], CounterAll], data_, mock_pub: Mock): +mock_comp_obj_inv_bat = [Mock(spec=MqttInverter, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=InverterState(power=-10000, + exported=27000)))), + Mock(spec=MqttBat, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=BatState(power=-5000, + imported=12000, + exported=10000)))) + ] +mock_comp_obj_counter_inv_bat = [Mock(spec=MqttCounter, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=CounterState(currents=[25, 10, 25], + power=13800, + imported=14000, + exported=18000)))), + Mock(spec=MqttInverter, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=InverterState(power=-10000, + exported=27000)))), + Mock(spec=MqttBat, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=BatState(power=-5000, + imported=12000, + exported=10000))))] + + +@pytest.mark.parametrize("mock, counter_all", + [pytest.param(mock_comp_obj_inv_bat, hierarchy_standard, id="standard"), + pytest.param(mock_comp_obj_inv_bat, hierarchy_hybrid, id="hybrid"), + pytest.param(mock_comp_obj_counter_inv_bat, hierarchy_nested, id="nested")]) +def test_virtual_counter_hierarchies(mock, counter_all: Callable[[], CounterAll], data_, mock_pub: Mock, monkeypatch): # setup virtual_counter = counter.VirtualCounter(VirtualCounterSetup( id=0, configuration=VirtualCounterConfiguration(external_consumption=0)), device_id=0) virtual_counter.initialize() data.data.counter_all_data = counter_all() + mock_comp_obj = Mock(side_effect=mock) + monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_comp_obj) # execution virtual_counter.update() diff --git a/packages/modules/loadvars.py b/packages/modules/loadvars.py index 507f6a28a5..f0b057dd49 100644 --- a/packages/modules/loadvars.py +++ b/packages/modules/loadvars.py @@ -8,7 +8,7 @@ from modules.common.abstract_device import AbstractDevice from modules.common.component_type import ComponentType, type_to_topic_mapping from modules.common.store import update_values -from modules.common.utils.component_parser import get_component_obj_by_id +from modules.common.utils.component_parser import get_finished_component_obj_by_id from helpermodules.utils import joined_thread_handler log = logging.getLogger(__name__) @@ -67,7 +67,7 @@ def _update_values_of_level(self, elements, not_finished_threads: List[str]) -> args=(chargepoint.chargepoint_module,), name=f"update values cp{chargepoint.chargepoint_module.config.id}")) else: - component = get_component_obj_by_id(element["id"], not_finished_threads) + component = get_finished_component_obj_by_id(element["id"], not_finished_threads) if component is None: continue modules_threads.append(threading.Thread(target=update_values, args=( From 3a3eb374d7b1fab76f70ece2f6db07c975d91267 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 10 Apr 2025 15:35:21 +0200 Subject: [PATCH 2/3] fixes --- packages/helpermodules/update_config.py | 4 ++-- packages/modules/chargepoints/mqtt/chargepoint_module.py | 6 +++--- packages/modules/devices/generic/mqtt/device.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 162ab72765..829c4e5f4e 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -2076,8 +2076,8 @@ def upgrade(topic: str, payload) -> Optional[dict]: payload = decode_payload(payload) if payload.get("type") == "mqtt": pub_system_message( - {}, "Die Topics für MQTT-Komponenten und MQTT-Ladepunkte wurden angepasst. Bitte aktualsiere " - "die Topics in Deinen angebundenen Systemen.", MessageType.WARNING) + {}, "Die Topics für MQTT-Komponenten und MQTT-Ladepunkte wurden angepasst. Bitte " + "aktualsiere die Topics in Deinen angebundenen Systemen.", MessageType.WARNING) # Nachricht nur einmal senden break self._loop_all_received_topics(upgrade) diff --git a/packages/modules/chargepoints/mqtt/chargepoint_module.py b/packages/modules/chargepoints/mqtt/chargepoint_module.py index 0ab5e561e5..a402a4ba64 100644 --- a/packages/modules/chargepoints/mqtt/chargepoint_module.py +++ b/packages/modules/chargepoints/mqtt/chargepoint_module.py @@ -1,6 +1,6 @@ import logging -from helpermodules.broker import InternalBrokerClient +from helpermodules.broker import BrokerClient from helpermodules.pub import Pub from helpermodules.utils.topic_parser import decode_payload from modules.chargepoints.mqtt.config import Mqtt @@ -33,8 +33,8 @@ def on_message(client, userdata, message): received_topics.update({message.topic: decode_payload(message.payload)}) received_topics = {} - InternalBrokerClient(f"subscribeMqttChargepoint{self.config.id}", - on_connect, on_message).start_finite_loop() + BrokerClient(f"subscribeMqttChargepoint{self.config.id}", + on_connect, on_message).start_finite_loop() if received_topics: log.debug(f"Empfange MQTT Daten für Ladepunkt {self.config.id}: {received_topics}") diff --git a/packages/modules/devices/generic/mqtt/device.py b/packages/modules/devices/generic/mqtt/device.py index 81feb04ef8..a5a476374a 100644 --- a/packages/modules/devices/generic/mqtt/device.py +++ b/packages/modules/devices/generic/mqtt/device.py @@ -2,7 +2,7 @@ from typing import Iterable, Union import logging -from helpermodules.broker import InternalBrokerClient +from helpermodules.broker import BrokerClient from helpermodules.utils.topic_parser import decode_payload from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import type_to_topic_mapping @@ -33,7 +33,7 @@ def on_message(client, userdata, message): received_topics.update({message.topic: decode_payload(message.payload)}) received_topics = {} - InternalBrokerClient(f"subscribeMqttDevice{device_config.id}", on_connect, on_message).start_finite_loop() + BrokerClient(f"subscribeMqttDevice{device_config.id}", on_connect, on_message).start_finite_loop() if received_topics: log.debug(f"Empfange MQTT Daten für Gerät {device_config.id}: {received_topics}") From 588edb731cadc6121d3d3105aa64145e700fa016 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 10 Apr 2025 15:56:45 +0200 Subject: [PATCH 3/3] fixes --- data/config/mosquitto/openwb_local.conf | 2 +- packages/helpermodules/update_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index 0eaec08093..1518d66272 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:17 +# openwb-version:18 listener 1886 localhost allow_anonymous true diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 829c4e5f4e..648125d8e2 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -2077,7 +2077,7 @@ def upgrade(topic: str, payload) -> Optional[dict]: if payload.get("type") == "mqtt": pub_system_message( {}, "Die Topics für MQTT-Komponenten und MQTT-Ladepunkte wurden angepasst. Bitte " - "aktualsiere die Topics in Deinen angebundenen Systemen.", MessageType.WARNING) + "aktualisiere die Topics in Deinen angebundenen (Smarthome-)Systemen.", MessageType.WARNING) # Nachricht nur einmal senden break self._loop_all_received_topics(upgrade)