From a2250da2c55eab7202e7999c4d16699ca576c604 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 2 May 2025 15:21:49 +0200 Subject: [PATCH 1/3] avm, nibe, we5114, mystrom --- packages/modules/devices/avm/__init__ .py | 0 packages/modules/devices/avm/avm/__init__.py | 0 packages/modules/devices/avm/avm/config.py | 47 +++++++++++ packages/modules/devices/avm/avm/counter.py | 48 +++++++++++ packages/modules/devices/avm/avm/device.py | 84 +++++++++++++++++++ packages/modules/devices/avm/vendor.py | 14 ++++ packages/modules/devices/mystrom/__init__.py | 0 .../devices/mystrom/mystrom/__init__.py | 0 .../modules/devices/mystrom/mystrom/config.py | 38 +++++++++ .../devices/mystrom/mystrom/counter.py | 43 ++++++++++ .../modules/devices/mystrom/mystrom/device.py | 35 ++++++++ packages/modules/devices/mystrom/vendor.py | 14 ++++ packages/modules/devices/nibe/__init__ .py | 0 .../modules/devices/nibe/nibe/__init__.py | 0 packages/modules/devices/nibe/nibe/config.py | 39 +++++++++ packages/modules/devices/nibe/nibe/counter.py | 43 ++++++++++ packages/modules/devices/nibe/nibe/device.py | 40 +++++++++ packages/modules/devices/nibe/vendor.py | 14 ++++ packages/modules/devices/orno/__init__ .py | 0 .../modules/devices/orno/orno/__init__.py | 0 packages/modules/devices/orno/orno/config.py | 39 +++++++++ packages/modules/devices/orno/orno/counter.py | 41 +++++++++ packages/modules/devices/orno/orno/device.py | 42 ++++++++++ packages/modules/devices/orno/vendor.py | 14 ++++ 24 files changed, 595 insertions(+) create mode 100644 packages/modules/devices/avm/__init__ .py create mode 100644 packages/modules/devices/avm/avm/__init__.py create mode 100644 packages/modules/devices/avm/avm/config.py create mode 100644 packages/modules/devices/avm/avm/counter.py create mode 100644 packages/modules/devices/avm/avm/device.py create mode 100644 packages/modules/devices/avm/vendor.py create mode 100644 packages/modules/devices/mystrom/__init__.py create mode 100644 packages/modules/devices/mystrom/mystrom/__init__.py create mode 100644 packages/modules/devices/mystrom/mystrom/config.py create mode 100644 packages/modules/devices/mystrom/mystrom/counter.py create mode 100644 packages/modules/devices/mystrom/mystrom/device.py create mode 100644 packages/modules/devices/mystrom/vendor.py create mode 100644 packages/modules/devices/nibe/__init__ .py create mode 100644 packages/modules/devices/nibe/nibe/__init__.py create mode 100644 packages/modules/devices/nibe/nibe/config.py create mode 100644 packages/modules/devices/nibe/nibe/counter.py create mode 100644 packages/modules/devices/nibe/nibe/device.py create mode 100644 packages/modules/devices/nibe/vendor.py create mode 100644 packages/modules/devices/orno/__init__ .py create mode 100644 packages/modules/devices/orno/orno/__init__.py create mode 100644 packages/modules/devices/orno/orno/config.py create mode 100644 packages/modules/devices/orno/orno/counter.py create mode 100644 packages/modules/devices/orno/orno/device.py create mode 100644 packages/modules/devices/orno/vendor.py diff --git a/packages/modules/devices/avm/__init__ .py b/packages/modules/devices/avm/__init__ .py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/avm/avm/__init__.py b/packages/modules/devices/avm/avm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/avm/avm/config.py b/packages/modules/devices/avm/avm/config.py new file mode 100644 index 0000000000..44a9cc1913 --- /dev/null +++ b/packages/modules/devices/avm/avm/config.py @@ -0,0 +1,47 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + + +@auto_str +class AvmConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + session_id: Optional[str] = None, + session_mtime: Optional[str] = None) -> None: + self.ip_address = ip_address + self.username = username + self.password = password + self.session_id = session_id # don't show in UI + self.session_mtime = session_mtime # don't show in UI + + +@auto_str +class Avm: + def __init__(self, + name: str = "AVM Fritz!Box", + type: str = "avm", + id: int = 0, + configuration: AvmConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or AvmConfiguration() + + +@auto_str +class AvmCounterConfiguration: + def __init__(self, name: Optional[str] = None): + self.name = name + + +@auto_str +class AvmCounterSetup(ComponentSetup[AvmCounterConfiguration]): + def __init__(self, + name: str = "Avm Zähler", + type: str = "counter", + id: int = 0, + configuration: AvmCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or AvmCounterConfiguration()) diff --git a/packages/modules/devices/avm/avm/counter.py b/packages/modules/devices/avm/avm/counter.py new file mode 100644 index 0000000000..fb73a393c6 --- /dev/null +++ b/packages/modules/devices/avm/avm/counter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +from xml.etree.ElementTree import Element + +from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store import get_counter_value_store +from modules.devices.avm.avm.config import AvmCounterSetup + + +class AvmCounter(AbstractCounter): + def __init__(self, component_config: AvmCounterSetup) -> None: + self.component_config = component_config + + def initialize(self) -> None: + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self, deviceListElementTree: Element): + for device in deviceListElementTree: + name = device.find("name").text + if name == self.component_config.configuration.name: + presentText = device.find("present").text + if presentText != '1': + continue + + powermeterBlock = device.find("powermeter") + if powermeterBlock is not None: + # AVM returns mW, convert to W here + power = float(powermeterBlock.find("power").text)/1000 + # AVM returns mV, convert to V here + voltageInfo = powermeterBlock.find("voltage") + if voltageInfo is not None: + voltages = [float(voltageInfo.text)/1000, 0, 0] + # AVM returns Wh + imported = powermeterBlock.find("energy").text + + counter_state = CounterState( + imported=imported, + exported=0, + power=power, + voltages=voltages + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=AvmCounterSetup) diff --git a/packages/modules/devices/avm/avm/device.py b/packages/modules/devices/avm/avm/device.py new file mode 100644 index 0000000000..4db3eca9d2 --- /dev/null +++ b/packages/modules/devices/avm/avm/device.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import hashlib +import logging +import time +from typing import Iterable +import xml.etree.ElementTree as ET + +from dataclass_utils._dataclass_asdict import asdict +from helpermodules.pub import Pub +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.devices.avm.avm.config import Avm, AvmCounterSetup +from modules.devices.avm.avm.counter import AvmCounter + +log = logging.getLogger(__name__) + +INVALID_SESSIONID = "0000000000000000" + + +def create_device(device_config: Avm): + def create_counter_component(component_config: AvmCounterSetup): + return AvmCounter(component_config) + + def update_components(components: Iterable[AvmCounter]): + if (device_config.configuration.session_id is None or + device_config.configuration.session_mtime is None or + time.time() - device_config.configuration.session_mtime > 300): + device_config.configuration.session_mtime = time.time() + device_config.configuration.session_id = get_session_id() + Pub().pub(f"openWB/set/system/device/{device_config.id}/config", asdict(device_config)) + + response = req.get_http_session().get( + f"http://{device_config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" + f"{device_config.configuration.session_id}&switchcmd=getdevicelistinfos") + deviceListElementTree = ET.fromstring(response.text.strip()) + + for component in components: + component.update(deviceListElementTree) + + def get_session_id(): + # checking existing sessionID + response = req.get_http_session().post(f"http://{device_config.configuration.ip_address}/login_sid.lua") + challengeResponse = ET.fromstring(response.content) + session_id = challengeResponse.find('SID').text + if session_id != INVALID_SESSIONID: + return + blockTimeXML = challengeResponse.find('BlockTime') + if blockTimeXML is not None and int(blockTimeXML.text) > 0: + raise Exception("Durch Anmeldefehler in der Vergangenheit ist der Zugang zur FRITZ!Box " + f"noch für {blockTimeXML.text} Sekunden gesperrt.") + + # last sessionID was invalid, performing new challenge-response authentication + challenge = challengeResponse.find('Challenge').text + m = hashlib.md5() + m.update((f"{challenge}-{device_config.configuration.password}").encode('utf-16le')) + hashedPassword = m.hexdigest() + + data = { + 'username': device_config.configuration.username, + 'response': challenge + "-" + hashedPassword + } + try: + response = req.get_http_session().post( + f"http://{device_config.configuration.ip_address}/login_sid.lua", data=data, timeout=5) + session_info = ET.fromstring(response.content) + session_id = session_info.find('SID').text + except Exception: + session_id = None + raise Exception("Anmeldung fehlgeschlagen, bitte Benutzername und Passwort überprüfen. Anmeldung für " + f"die nächsten {session_info.find('BlockTime').text} Sekunden durch FRITZ!Box-Webinterface " + "gesperrt.") + return session_id + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Avm) diff --git a/packages/modules/devices/avm/vendor.py b/packages/modules/devices/avm/vendor.py new file mode 100644 index 0000000000..de4025faae --- /dev/null +++ b/packages/modules/devices/avm/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "AVM Fritz!Box" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/devices/mystrom/__init__.py b/packages/modules/devices/mystrom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/mystrom/mystrom/__init__.py b/packages/modules/devices/mystrom/mystrom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/mystrom/mystrom/config.py b/packages/modules/devices/mystrom/mystrom/config.py new file mode 100644 index 0000000000..b85cfb4642 --- /dev/null +++ b/packages/modules/devices/mystrom/mystrom/config.py @@ -0,0 +1,38 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + + +@auto_str +class MystromConfiguration: + def __init__(self, ip_address: Optional[str] = None): + self.ip_address = ip_address + + +@auto_str +class Mystrom: + def __init__(self, + name: str = "mystrom", + type: str = "mystrom", + id: int = 0, + configuration: MystromConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or MystromConfiguration() + + +@auto_str +class MystromCounterConfiguration: + def __init__(self): + pass + + +@auto_str +class MystromCounterSetup(ComponentSetup[MystromCounterConfiguration]): + def __init__(self, + name: str = "mystrom Zähler", + type: str = "counter", + id: int = 0, + configuration: MystromCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or MystromCounterConfiguration()) diff --git a/packages/modules/devices/mystrom/mystrom/counter.py b/packages/modules/devices/mystrom/mystrom/counter.py new file mode 100644 index 0000000000..17b82538e4 --- /dev/null +++ b/packages/modules/devices/mystrom/mystrom/counter.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +from requests import Session +from typing import TypedDict, Any +from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.mystrom.mystrom.config import MystromCounterSetup + + +class KwargsDict(TypedDict): + device_id: int + ip_address: str + + +class MystromCounter(AbstractCounter): + def __init__(self, component_config: MystromCounterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.__device_id: int = self.kwargs['device_id'] + self.ip_address: str = self.kwargs['ip_address'] + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self, session: Session): + resp = session.get(f"http://{self.ip_address}/report").json() + power = resp["power"] + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=MystromCounterSetup) diff --git a/packages/modules/devices/mystrom/mystrom/device.py b/packages/modules/devices/mystrom/mystrom/device.py new file mode 100644 index 0000000000..51c3a0a019 --- /dev/null +++ b/packages/modules/devices/mystrom/mystrom/device.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import logging + +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, IndependentComponentUpdater +from modules.devices.mystrom.mystrom.config import Mystrom, MystromCounterSetup +from modules.devices.mystrom.mystrom.counter import MystromCounter + +log = logging.getLogger(__name__) + + +def create_device(device_config: Mystrom): + session = None + + def create_counter_component(component_config: MystromCounterSetup): + return MystromCounter(component_config, + device_id=device_config.id, + ip_address=device_config.configuration.ip_address) + + def initializer(): + nonlocal session + session = req.get_http_session() + + return ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + ), + component_updater=IndependentComponentUpdater(lambda component: component.update(session)) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Mystrom) diff --git a/packages/modules/devices/mystrom/vendor.py b/packages/modules/devices/mystrom/vendor.py new file mode 100644 index 0000000000..bea7943107 --- /dev/null +++ b/packages/modules/devices/mystrom/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "mystrom" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/devices/nibe/__init__ .py b/packages/modules/devices/nibe/__init__ .py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/nibe/nibe/__init__.py b/packages/modules/devices/nibe/nibe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/nibe/nibe/config.py b/packages/modules/devices/nibe/nibe/config.py new file mode 100644 index 0000000000..6c9a8cf440 --- /dev/null +++ b/packages/modules/devices/nibe/nibe/config.py @@ -0,0 +1,39 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + + +@auto_str +class NibeConfiguration: + def __init__(self, ip_address: Optional[str] = None, port: int = 502): + self.ip_address = ip_address + self.port = port + + +@auto_str +class Nibe: + def __init__(self, + name: str = "Nibe S-Series", + type: str = "nibe", + id: int = 0, + configuration: NibeConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or NibeConfiguration() + + +@auto_str +class NibeCounterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class NibeCounterSetup(ComponentSetup[NibeCounterConfiguration]): + def __init__(self, + name: str = "Nibe Zähler", + type: str = "counter", + id: int = 0, + configuration: NibeCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or NibeCounterConfiguration()) diff --git a/packages/modules/devices/nibe/nibe/counter.py b/packages/modules/devices/nibe/nibe/counter.py new file mode 100644 index 0000000000..1c16e00b51 --- /dev/null +++ b/packages/modules/devices/nibe/nibe/counter.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any +from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.nibe.nibe.config import NibeCounterSetup + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class NibeCounter(AbstractCounter): + def __init__(self, component_config: NibeCounterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.__device_id: int = self.kwargs['device_id'] + self.client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self): + unit = self.component_config.configuration.modbus_id + power = self.client.read_input_registers(2166, ModbusDataType.UINT_32, unit=unit) / 10 + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=NibeCounterSetup) diff --git a/packages/modules/devices/nibe/nibe/device.py b/packages/modules/devices/nibe/nibe/device.py new file mode 100644 index 0000000000..67d955a882 --- /dev/null +++ b/packages/modules/devices/nibe/nibe/device.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.modbus import ModbusTcpClient_ +from modules.devices.nibe.nibe.config import Nibe, NibeCounterSetup +from modules.devices.nibe.nibe.counter import NibeCounter + +log = logging.getLogger(__name__) + + +def create_device(device_config: Nibe): + client = None + + def create_counter_component(component_config: NibeCounterSetup): + nonlocal client + return NibeCounter(component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[NibeCounter]): + with client: + for component in components: + component.update() + + def initializer(): + nonlocal client + client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + + return ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Nibe) diff --git a/packages/modules/devices/nibe/vendor.py b/packages/modules/devices/nibe/vendor.py new file mode 100644 index 0000000000..48ccad8553 --- /dev/null +++ b/packages/modules/devices/nibe/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "Nibe" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/devices/orno/__init__ .py b/packages/modules/devices/orno/__init__ .py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/orno/orno/__init__.py b/packages/modules/devices/orno/orno/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/orno/orno/config.py b/packages/modules/devices/orno/orno/config.py new file mode 100644 index 0000000000..3687b620b0 --- /dev/null +++ b/packages/modules/devices/orno/orno/config.py @@ -0,0 +1,39 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + + +@auto_str +class OrnoConfiguration: + def __init__(self, ip_address: Optional[str] = None, port: int = 502) -> None: + self.ip_address = ip_address + self.port = port + + +@auto_str +class Orno: + def __init__(self, + name: str = "Orno WE-514", + type: str = "orno", + id: int = 0, + configuration: OrnoConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or OrnoConfiguration() + + +@auto_str +class OrnoCounterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class OrnoCounterSetup(ComponentSetup[OrnoCounterConfiguration]): + def __init__(self, + name: str = "Orno Zähler", + type: str = "counter", + id: int = 0, + configuration: OrnoCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or OrnoCounterConfiguration()) diff --git a/packages/modules/devices/orno/orno/counter.py b/packages/modules/devices/orno/orno/counter.py new file mode 100644 index 0000000000..6f34f1ede7 --- /dev/null +++ b/packages/modules/devices/orno/orno/counter.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.store import get_counter_value_store +from modules.devices.orno.orno.config import OrnoCounterSetup + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + + +class OrnoCounter(AbstractCounter): + def __init__(self, component_config: OrnoCounterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.client: ModbusTcpClient_ = self.kwargs['client'] + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self): + power = self.client.read_holding_registers( + 0x141, ModbusDataType.INT_32, unit=self.component_config.configuration.modbus_id) + imported = self.client.read_holding_registers( + 0xA001, ModbusDataType.INT_32, unit=self.component_config.configuration.modbus_id) * 10 + + counter_state = CounterState( + imported=imported, + exported=0, + power=power, + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=OrnoCounterSetup) diff --git a/packages/modules/devices/orno/orno/device.py b/packages/modules/devices/orno/orno/device.py new file mode 100644 index 0000000000..4884bd0e43 --- /dev/null +++ b/packages/modules/devices/orno/orno/device.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import logging +from pymodbus.transaction import ModbusRtuFramer +from typing import Iterable + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.modbus import ModbusTcpClient_ +from modules.devices.orno.orno.config import Orno, OrnoCounterSetup +from modules.devices.orno.orno.counter import OrnoCounter + +log = logging.getLogger(__name__) + + +def create_device(device_config: Orno): + client = None + + def create_counter_component(component_config: OrnoCounterSetup): + nonlocal client + return OrnoCounter(component_config, client=client) + + def update_components(components: Iterable[OrnoCounter]): + with client: + for component in components: + component.update() + + def initializer(): + nonlocal client + client = ModbusTcpClient_(device_config.configuration.ip_address, + device_config.configuration.port, framer=ModbusRtuFramer) + + return ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Orno) diff --git a/packages/modules/devices/orno/vendor.py b/packages/modules/devices/orno/vendor.py new file mode 100644 index 0000000000..ba5dcd3586 --- /dev/null +++ b/packages/modules/devices/orno/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "Orno" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) From ee73a9bdc0fbad7b7203501f1b6b18eb68abc1bf Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 2 May 2025 15:22:31 +0200 Subject: [PATCH 2/3] ammend --- packages/modules/common/modbus.py | 4 +++- packages/modules/conftest.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/modules/common/modbus.py b/packages/modules/common/modbus.py index c492a5356e..49fbaa4ac9 100644 --- a/packages/modules/common/modbus.py +++ b/packages/modules/common/modbus.py @@ -14,6 +14,7 @@ from pymodbus.client.sync import ModbusTcpClient, ModbusSerialClient from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadDecoder +from pymodbus.transaction import ModbusSocketFramer from urllib3.util import parse_url log = logging.getLogger(__name__) @@ -197,12 +198,13 @@ def __init__(self, address: str, port: int = 502, sleep_after_connect: Optional[int] = 0, + framer: type[ModbusSocketFramer] = ModbusSocketFramer, **kwargs): parsed_url = parse_url(address) host = parsed_url.host if parsed_url.port is not None: port = parsed_url.port - super().__init__(ModbusTcpClient(host, port, **kwargs), address, port, sleep_after_connect) + super().__init__(ModbusTcpClient(host, port, framer, **kwargs), address, port, sleep_after_connect) class ModbusSerialClient_(ModbusClient): diff --git a/packages/modules/conftest.py b/packages/modules/conftest.py index 70bb2a186b..7d233ffba4 100644 --- a/packages/modules/conftest.py +++ b/packages/modules/conftest.py @@ -33,6 +33,10 @@ module.BinaryPayloadDecoder = Mock() sys.modules['pymodbus.payload'] = module +module = type(sys)('pymodbus.transaction') +module.ModbusSocketFramer = Mock() +sys.modules['pymodbus.transaction'] = module + module = type(sys)('socketserver') module.TCPServer = Mock() sys.modules['socketserver'] = module From f7a81e270cf3ebed87328d29b0c051292c47edee Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 2 May 2025 15:50:14 +0200 Subject: [PATCH 3/3] fix pytest --- packages/modules/conftest.py | 1 + packages/modules/devices/avm/avm/config.py | 3 +++ packages/modules/devices/mystrom/mystrom/config.py | 3 +++ packages/modules/devices/nibe/nibe/config.py | 3 +++ packages/modules/devices/orno/orno/config.py | 3 +++ 5 files changed, 13 insertions(+) diff --git a/packages/modules/conftest.py b/packages/modules/conftest.py index 7d233ffba4..c5e6f1f617 100644 --- a/packages/modules/conftest.py +++ b/packages/modules/conftest.py @@ -35,6 +35,7 @@ module = type(sys)('pymodbus.transaction') module.ModbusSocketFramer = Mock() +module.ModbusRtuFramer = Mock() sys.modules['pymodbus.transaction'] = module module = type(sys)('socketserver') diff --git a/packages/modules/devices/avm/avm/config.py b/packages/modules/devices/avm/avm/config.py index 44a9cc1913..72cbda823e 100644 --- a/packages/modules/devices/avm/avm/config.py +++ b/packages/modules/devices/avm/avm/config.py @@ -2,6 +2,8 @@ from helpermodules.auto_str import auto_str from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + @auto_str class AvmConfiguration: @@ -27,6 +29,7 @@ def __init__(self, configuration: AvmConfiguration = None) -> None: self.name = name self.type = type + self.vendor = vendor_descriptor.configuration_factory().type self.id = id self.configuration = configuration or AvmConfiguration() diff --git a/packages/modules/devices/mystrom/mystrom/config.py b/packages/modules/devices/mystrom/mystrom/config.py index b85cfb4642..7a92d551c6 100644 --- a/packages/modules/devices/mystrom/mystrom/config.py +++ b/packages/modules/devices/mystrom/mystrom/config.py @@ -2,6 +2,8 @@ from helpermodules.auto_str import auto_str from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + @auto_str class MystromConfiguration: @@ -18,6 +20,7 @@ def __init__(self, configuration: MystromConfiguration = None) -> None: self.name = name self.type = type + self.vendor = vendor_descriptor.configuration_factory().type self.id = id self.configuration = configuration or MystromConfiguration() diff --git a/packages/modules/devices/nibe/nibe/config.py b/packages/modules/devices/nibe/nibe/config.py index 6c9a8cf440..713da6d9a2 100644 --- a/packages/modules/devices/nibe/nibe/config.py +++ b/packages/modules/devices/nibe/nibe/config.py @@ -2,6 +2,8 @@ from helpermodules.auto_str import auto_str from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + @auto_str class NibeConfiguration: @@ -19,6 +21,7 @@ def __init__(self, configuration: NibeConfiguration = None) -> None: self.name = name self.type = type + self.vendor = vendor_descriptor.configuration_factory().type self.id = id self.configuration = configuration or NibeConfiguration() diff --git a/packages/modules/devices/orno/orno/config.py b/packages/modules/devices/orno/orno/config.py index 3687b620b0..e767d3a373 100644 --- a/packages/modules/devices/orno/orno/config.py +++ b/packages/modules/devices/orno/orno/config.py @@ -2,6 +2,8 @@ from helpermodules.auto_str import auto_str from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + @auto_str class OrnoConfiguration: @@ -19,6 +21,7 @@ def __init__(self, configuration: OrnoConfiguration = None) -> None: self.name = name self.type = type + self.vendor = vendor_descriptor.configuration_factory().type self.id = id self.configuration = configuration or OrnoConfiguration()