diff --git a/packages/modules/common/algodue.py b/packages/modules/common/algodue.py new file mode 100644 index 0000000000..3370a78944 --- /dev/null +++ b/packages/modules/common/algodue.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +import logging +from typing import List, Tuple, Optional + +from helpermodules.logger import ModifyLoglevelContext + +from modules.common import modbus +from modules.common.abstract_counter import AbstractCounter +from modules.common.modbus import ModbusDataType + +log = logging.getLogger(__name__) + + +class Algodue(AbstractCounter): + serial_cached: Optional[str] = None + model_cached: Optional[str] = None + + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + self.client = client + self.id = modbus_id + + def get_imported(self) -> float: + return self.client.read_input_registers(0x1106, ModbusDataType.FLOAT_32, unit=self.id) + + def get_exported(self) -> float: + return self.client.read_input_registers(0x110e, ModbusDataType.FLOAT_32, unit=self.id) + + def get_frequency(self) -> float: + return self.client.read_input_registers(0x1038, ModbusDataType.FLOAT_32, unit=self.id) + + def get_serial_number(self) -> Optional[str]: + # serial will never change - at least until power cycle + if self.serial_cached is None: + serial_chars = self.client.read_holding_registers(0x500, [ModbusDataType.UINT_8]*10, unit=self.id) + serial_string = "" + for x in serial_chars: + serial_string += chr(x) + # due to caching this appears rarely - but it's nice to have always have it in main log + with ModifyLoglevelContext(log, logging.DEBUG): + log.debug("Algodue meter serial " + serial_string) + self.serial_cached = serial_string + return self.serial_cached + + def get_currents(self) -> List[float]: + return self.client.read_input_registers(0x100E, [ModbusDataType.FLOAT_32]*3, unit=self.id) + + def get_power_factors(self) -> List[float]: + return self.client.read_input_registers(0x1018, [ModbusDataType.FLOAT_32]*3, unit=self.id) + + def get_power(self) -> Tuple[List[float], float]: + powers = self.client.read_input_registers(0x1020, [ModbusDataType.FLOAT_32]*3, unit=self.id) + power = sum(powers) + return powers, power + + def get_voltages(self) -> List[float]: + return self.client.read_input_registers(0x1000, [ModbusDataType.FLOAT_32]*3, unit=self.id) + + def get_model(self) -> Optional[str]: + # model will never change - at least until power cycle + if self.model_cached is None: + model_id = self.client.read_holding_registers(0x505, ModbusDataType.UINT_16, unit=self.id) + model_string = "unknown" + if model_id == 0x03: + model_string = "6 A, 3 phases, 4 wires" + elif model_id == 0x08: + model_string = "80 A, 3 phases, 4 wires" + elif model_id == 0x0c: + model_string = "80 A, 1 phase, 2 wires" + elif model_id == 0x10: + model_string = "40 A, 1 phase, 2 wires" + elif model_id == 0x12: + model_string = "63 A, 3 phases, 4 wires" + + type_id = self.client.read_holding_registers(0x506, ModbusDataType.UINT_16, unit=self.id) + type_string = "unknown" + if type_id == 0x00: + type_string = "NO MID, RESET" + elif type_id == 0x01: + type_string = "MID" + elif type_id == 0x02: + type_string = "NO MID" + elif type_id == 0x03: + type_string = "NO MID, Wiring selection" + elif type_id == 0x05: + type_string = "MID no varh" + elif type_id == 0x09: + type_string = "MID Wiring selection" + elif type_id == 0x0a: + type_string = "MID no varh, Wiring selection" + elif type_id == 0x0b: + type_string = "NO MID, RESET, Wiring selection" + meterinfo = "Algodue UEM " + model_string + ", " + type_string + + # due to caching this appears rarely - but it's nice to have always have it in main log + with ModifyLoglevelContext(log, logging.DEBUG): + log.debug("Algodue model: " + meterinfo) + self.model_cached = meterinfo + return self.model_cached diff --git a/packages/modules/devices/algodue/__init__.py b/packages/modules/devices/algodue/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/algodue/algodue/__init__.py b/packages/modules/devices/algodue/algodue/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/algodue/algodue/config.py b/packages/modules/devices/algodue/algodue/config.py new file mode 100644 index 0000000000..130719974f --- /dev/null +++ b/packages/modules/devices/algodue/algodue/config.py @@ -0,0 +1,38 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class AlgodueConfiguration: + def __init__(self, modbus_id: int = 1, ip_address: Optional[str] = None, port: int = 502): + self.modbus_id = modbus_id + self.ip_address = ip_address + self.port = port + + +class Algodue: + def __init__(self, + name: str = "Algodue", + type: str = "Algodue", + id: int = 0, + configuration: AlgodueConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or AlgodueConfiguration() + + +class AlgodueCounterConfiguration: + def __init__(self): + pass + + +class AlgodueCounterSetup(ComponentSetup[AlgodueCounterConfiguration]): + def __init__(self, + name: str = "Algodue Zähler", + type: str = "counter", + id: int = 0, + configuration: AlgodueCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or AlgodueCounterConfiguration()) diff --git a/packages/modules/devices/algodue/algodue/counter.py b/packages/modules/devices/algodue/algodue/counter.py new file mode 100644 index 0000000000..ca13f036f1 --- /dev/null +++ b/packages/modules/devices/algodue/algodue/counter.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +from typing import Dict, Union + +from dataclass_utils import dataclass_from_dict +from modules.devices.algodue.algodue.config import AlgodueCounterSetup +from modules.common import modbus +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 +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store + + +class AlgodueCounter(AbstractCounter): + def __init__(self, + device_id: int, + component_config: Union[Dict, AlgodueCounterSetup], + tcp_client: modbus.ModbusTcpClient_, + modbus_id: int) -> None: + self.__device_id = device_id + self.component_config = dataclass_from_dict(AlgodueCounterSetup, component_config) + self.__tcp_client = tcp_client + self.__modbus_id = modbus_id + 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): + with self.__tcp_client: + + frequency = self.__tcp_client.read_input_registers(0x1038, ModbusDataType.FLOAT_32, unit=self.id) + currents = self.__tcp_client.read_input_registers(0x100E, [ModbusDataType.FLOAT_32]*3, unit=self.id) + powers = self.__tcp_client.read_input_registers(0x1020, [ModbusDataType.FLOAT_32]*3, unit=self.id) + power = sum(powers) + voltages = self.__tcp_client.read_input_registers(0x1000, [ModbusDataType.FLOAT_32]*3, unit=self.id) + + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + voltages=voltages, + currents=currents, + powers=powers, + imported=imported, + exported=exported, + power=power, + frequency=frequency + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=AlgodueCounterSetup) diff --git a/packages/modules/devices/algodue/algodue/device.py b/packages/modules/devices/algodue/algodue/device.py new file mode 100644 index 0000000000..900f4bfde8 --- /dev/null +++ b/packages/modules/devices/algodue/algodue/device.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable + +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.algodue.algodue import counter +from modules.devices.algodue.algodue.config import Algodue, AlgodueCounterSetup +from modules.common import modbus +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext + +log = logging.getLogger(__name__) + + +def create_device(device_config: Algodue): + def create_counter_component(component_config: AlgodueCounterSetup): + return counter.AlgodueCounter(device_config.id, component_config, client, + device_config.configuration.modbus_id) + + def update_components(components: Iterable[counter.AlgodueCounter]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + try: + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + except Exception: + log.exception("Fehler in create_device") + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + counter=create_counter_component + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Algodue) diff --git a/packages/modules/devices/algodue/vendor.py b/packages/modules/devices/algodue/vendor.py new file mode 100644 index 0000000000..d4970b9763 --- /dev/null +++ b/packages/modules/devices/algodue/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 = "Algodue" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)