diff --git a/packages/modules/devices/generic/modbus/bat.py b/packages/modules/devices/generic/modbus/bat.py new file mode 100644 index 0000000000..bb4691abc9 --- /dev/null +++ b/packages/modules/devices/generic/modbus/bat.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from modules.common.abstract_device import AbstractBat +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusTcpClient_ +from modules.common.store._factory import get_component_value_store +from modules.devices.generic.modbus.config import GenericModbusBatSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType +from modules.common.simcount import SimCounter + +from modules.devices.generic.modbus.helper import read_phase_values, read_value + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class GenericModbusBat(AbstractBat): + def __init__(self, component_config: GenericModbusBatSetup, **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.store = get_component_value_store(self.component_config.type, self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state) + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, self.component_config.type) + + def update(self) -> None: + + unit = self.component_config.configuration.modbus_id + config = self.component_config.configuration + + # power + power = read_value(self.client, unit, config.power) + if power is None: + raise ValueError("Leistungsregister muss angegeben werden.") + + # SOC + soc_value = read_value(self.client, unit, config.soc) + if soc_value is not None: + soc = soc_value + + # currents + currents_value = read_phase_values(self.client, unit, config.current_L1, config.current_L2, config.current_L3) + if currents_value is not None: + currents = currents_value + + # Import + imported = read_value(self.client, unit, config.imported) + + # Export + exported = read_value(self.client, unit, config.exported) + + # Serial Number + serial_number_value = read_value(self.client, unit, config.serial_number) + if serial_number_value is not None: + serial_number = serial_number_value + + if imported is None or exported is None: + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + else: + imported, exported = self.peak_filter.check_values(power, imported, exported) + + bat_state = BatState( + imported=imported, + exported=exported, + power=power, + ) + + if "soc" in locals(): + bat_state.soc = soc + if "currents" in locals(): + bat_state.currents = currents + if "serial_number" in locals(): + bat_state.serial_number = serial_number + + self.store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=GenericModbusBatSetup) diff --git a/packages/modules/devices/generic/modbus/config.py b/packages/modules/devices/generic/modbus/config.py new file mode 100644 index 0000000000..c1237e6e6c --- /dev/null +++ b/packages/modules/devices/generic/modbus/config.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +from modules.common.component_setup import ComponentSetup +from typing import Optional +from ..vendor import vendor_descriptor + +from dataclasses import dataclass, field + + +class GenericModbusConfiguration: + def __init__(self, ip_address: Optional[str] = None, port: int = 502): + self.ip_address = ip_address + self.port = port + + +class GenericModbus: + def __init__(self, + name: str = "Modbus", + type: str = "modbus", + id: int = 0, + configuration: GenericModbusConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or GenericModbusConfiguration() + + +@dataclass +class RegisterConfig: + reg_address: Optional[int] = None + reg_type: Optional[str] = None + byteorder: Optional[str] = None + wordorder: Optional[str] = None + + +@dataclass +class GenericModbusCounterConfiguration: + modbus_id: int = 105 + voltage_L1: RegisterConfig = field(default_factory=RegisterConfig) + voltage_L2: RegisterConfig = field(default_factory=RegisterConfig) + voltage_L3: RegisterConfig = field(default_factory=RegisterConfig) + current_L1: RegisterConfig = field(default_factory=RegisterConfig) + current_L2: RegisterConfig = field(default_factory=RegisterConfig) + current_L3: RegisterConfig = field(default_factory=RegisterConfig) + powers_L1: RegisterConfig = field(default_factory=RegisterConfig) + powers_L2: RegisterConfig = field(default_factory=RegisterConfig) + powers_L3: RegisterConfig = field(default_factory=RegisterConfig) + power_factor_L1: RegisterConfig = field(default_factory=RegisterConfig) + power_factor_L2: RegisterConfig = field(default_factory=RegisterConfig) + power_factor_L3: RegisterConfig = field(default_factory=RegisterConfig) + imported: RegisterConfig = field(default_factory=RegisterConfig) + exported: RegisterConfig = field(default_factory=RegisterConfig) + power: RegisterConfig = field(default_factory=RegisterConfig) + frequency: RegisterConfig = field(default_factory=RegisterConfig) + serial_number: RegisterConfig = field(default_factory=RegisterConfig) + + +class GenericModbusCounterSetup(ComponentSetup[GenericModbusCounterConfiguration]): + def __init__(self, + name: str = "Universeller Modbus Zähler", + type: str = "counter", + id: int = 0, + configuration: GenericModbusCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or GenericModbusCounterConfiguration()) + + +@dataclass +class GenericModbusBatConfiguration: + modbus_id: int = 100 + current_L1: RegisterConfig = field(default_factory=RegisterConfig) + current_L2: RegisterConfig = field(default_factory=RegisterConfig) + current_L3: RegisterConfig = field(default_factory=RegisterConfig) + imported: RegisterConfig = field(default_factory=RegisterConfig) + exported: RegisterConfig = field(default_factory=RegisterConfig) + power: RegisterConfig = field(default_factory=RegisterConfig) + soc: RegisterConfig = field(default_factory=RegisterConfig) + serial_number: RegisterConfig = field(default_factory=RegisterConfig) + + +class GenericModbusBatSetup(ComponentSetup[GenericModbusBatConfiguration]): + def __init__(self, + name: str = "Universeller Modbus Batterie", + type: str = "bat", + id: int = 0, + configuration: GenericModbusBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or GenericModbusBatConfiguration()) + + +@dataclass +class GenericModbusInverterConfiguration: + modbus_id: int = 100 + current_L1: RegisterConfig = field(default_factory=RegisterConfig) + current_L2: RegisterConfig = field(default_factory=RegisterConfig) + current_L3: RegisterConfig = field(default_factory=RegisterConfig) + imported: RegisterConfig = field(default_factory=RegisterConfig) + exported: RegisterConfig = field(default_factory=RegisterConfig) + power: RegisterConfig = field(default_factory=RegisterConfig) + dc_power: RegisterConfig = field(default_factory=RegisterConfig) + serial_number: RegisterConfig = field(default_factory=RegisterConfig) + + +class GenericModbusInverterSetup(ComponentSetup[GenericModbusInverterConfiguration]): + def __init__(self, + name: str = "Universeller Modbus Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: GenericModbusInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or GenericModbusInverterConfiguration()) diff --git a/packages/modules/devices/generic/modbus/counter.py b/packages/modules/devices/generic/modbus/counter.py new file mode 100644 index 0000000000..98fd0234ea --- /dev/null +++ b/packages/modules/devices/generic/modbus/counter.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict +from modules.devices.generic.modbus.helper import read_phase_values, read_value +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.devices.generic.modbus.config import GenericModbusCounterSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + +from modules.common.store._factory import get_component_value_store + +from modules.common.modbus import ModbusTcpClient_ + +import logging +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class GenericModbusCounter(AbstractCounter): + def __init__(self, component_config: GenericModbusCounterSetup, **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, self.component_config.type) + self.store = get_component_value_store(self.component_config.type, self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state) + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + config = self.component_config.configuration + + # Power + power = read_value(self.client, unit, config.power) + if power is None: + raise ValueError("Leistungsregister muss angegeben werden.") + + # Voltages + voltages_value = read_phase_values(self.client, unit, config.voltage_L1, config.voltage_L2, config.voltage_L3) + if voltages_value is not None: + voltages = voltages_value + + # Currents + currents_value = read_phase_values(self.client, unit, config.current_L1, config.current_L2, config.current_L3) + if currents_value is not None: + currents = currents_value + + # Powers + powers_value = read_phase_values(self.client, unit, config.powers_L1, config.powers_L2, config.powers_L3) + if powers_value is not None: + powers = powers_value + + # Power Factors + power_factors_value = read_phase_values( + self.client, + unit, + config.power_factor_L1, + config.power_factor_L2, + config.power_factor_L3, + ) + if power_factors_value is not None: + power_factors = power_factors_value + + # Frequency + frequency_value = read_value(self.client, unit, config.frequency) + if frequency_value is not None: + frequency = frequency_value + + # Imported + imported = read_value(self.client, unit, config.imported) + + # Exported + exported = read_value(self.client, unit, config.exported) + + # Serial Number + serial_number_value = read_value(self.client, unit, config.serial_number) + if serial_number_value is not None: + serial_number = serial_number_value + + if power is not None: + self.peak_filter.check_values(power) + if imported is None or exported is None: + imported, exported = self.sim_counter.sim_count(power) + imported, exported = self.peak_filter.check_values(power, imported, exported) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power + ) + + if "voltages" in locals(): + counter_state.voltages = voltages + if "currents" in locals(): + counter_state.currents = currents + if "powers" in locals(): + counter_state.powers = powers + if "power_factors" in locals(): + counter_state.power_factors = power_factors + if "frequency" in locals(): + counter_state.frequency = frequency + if "serial_number" in locals(): + counter_state.serial_number = serial_number + + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=GenericModbusCounterSetup) diff --git a/packages/modules/devices/generic/modbus/device.py b/packages/modules/devices/generic/modbus/device.py new file mode 100644 index 0000000000..8672a0777e --- /dev/null +++ b/packages/modules/devices/generic/modbus/device.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +from typing import Iterable, Union +import logging + +from modules.devices.generic.modbus.bat import GenericModbusBat +from modules.devices.generic.modbus.inverter import GenericModbusInverter +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.generic.modbus.counter import GenericModbusCounter + +from modules.devices.generic.modbus.config import ( + GenericModbus, + GenericModbusCounterSetup, + GenericModbusBatSetup, + GenericModbusInverterSetup, +) + +from modules.common import modbus + +log = logging.getLogger(__name__) + + +def create_device(device_config: GenericModbus): + client = None + + def create_counter_component(component_config: GenericModbusCounterSetup): + return GenericModbusCounter(component_config, device_id=device_config.id, client=client) + + def create_bat_component(component_config: GenericModbusBatSetup): + return GenericModbusBat(component_config, device_id=device_config.id, client=client) + + def create_inverter_component(component_config: GenericModbusInverterSetup): + return GenericModbusInverter(component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[Union[GenericModbusCounter, GenericModbusBat, GenericModbusInverter]]): + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = modbus.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, + inverter=create_inverter_component, + bat=create_bat_component + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=GenericModbus) diff --git a/packages/modules/devices/generic/modbus/helper.py b/packages/modules/devices/generic/modbus/helper.py new file mode 100644 index 0000000000..ed7c3ae68c --- /dev/null +++ b/packages/modules/devices/generic/modbus/helper.py @@ -0,0 +1,37 @@ +from typing import Any + +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.devices.generic.modbus.config import RegisterConfig + + +def check_data(wert): + if (wert.reg_type is None or + wert.byteorder is None or + wert.wordorder is None): + raise ValueError( + f"Unvollständige Konfiguration für Universeller-Modbus: Register-Adresse {wert.reg_address}") + + +def read_value(client: ModbusTcpClient_, unit: int, register_config: RegisterConfig) -> Any: + if register_config.reg_address is None: + return None + + check_data(register_config) + return client.read_input_registers( + register_config.reg_address, + ModbusDataType[register_config.reg_type], + byteorder=register_config.byteorder, + wordorder=register_config.wordorder, + unit=unit, + ) + + +def read_phase_values(client: ModbusTcpClient_, unit: int, *register_configs: RegisterConfig) -> Any: + values = [0.0] * 3 + has_value = False + for index, register_config in enumerate(register_configs): + value = read_value(client, unit, register_config) + if value is not None: + values[index] = value + has_value = True + return values if has_value else None diff --git a/packages/modules/devices/generic/modbus/inverter.py b/packages/modules/devices/generic/modbus/inverter.py new file mode 100644 index 0000000000..d55cdcb824 --- /dev/null +++ b/packages/modules/devices/generic/modbus/inverter.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from modules.common.abstract_device import AbstractInverter +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.utils.peak_filter import PeakFilter +from modules.common.modbus import ModbusTcpClient_ +from modules.common.store._factory import get_component_value_store +from modules.devices.generic.modbus.config import GenericModbusInverterSetup +from modules.common.component_type import ComponentType +from modules.common.simcount import SimCounter + +from modules.devices.generic.modbus.helper import read_phase_values, read_value + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class GenericModbusInverter(AbstractInverter): + def __init__(self, component_config: GenericModbusInverterSetup, **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_component_value_store(self.component_config.type, self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state) + self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, self.component_config.type) + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + config = self.component_config.configuration + + # Power + power = read_value(self.client, unit, config.power) + if power is None: + raise ValueError("Leistungsregister muss angegeben werden.") + + # Exported + exported = read_value(self.client, unit, config.exported) + + # Imported + imported = read_value(self.client, unit, config.imported) + + # Currents + currents_value = read_phase_values(self.client, unit, config.current_L1, config.current_L2, config.current_L3) + if currents_value is not None: + currents = currents_value + + # DC Power + dc_power_value = read_value(self.client, unit, config.dc_power) + if dc_power_value is not None: + dc_power = dc_power_value + + # Serial Number + serial_number_value = read_value(self.client, unit, config.serial_number) + if serial_number_value is not None: + serial_number = serial_number_value + + if imported is None or exported is None: + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + else: + imported, exported = self.peak_filter.check_values(power, imported, exported) + + inverter_state = InverterState( + power=power, + exported=exported, + imported=imported, + ) + + if "dc_power" in locals(): + inverter_state.dc_power = dc_power + if "currents" in locals(): + inverter_state.currents = currents + if "serial_number" in locals(): + inverter_state.serial_number = serial_number + + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=GenericModbusInverterSetup)