diff --git a/packages/modules/devices/kaco/kaco_tx/config.py b/packages/modules/devices/kaco/kaco_tx/config.py new file mode 100644 index 0000000000..0e343f1211 --- /dev/null +++ b/packages/modules/devices/kaco/kaco_tx/config.py @@ -0,0 +1,37 @@ +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class KacoConfiguration: + def __init__(self, + port: int = 502, + ip_address=None): + self.port = port + self.ip_address = ip_address + + +class Kaco: + def __init__(self, + name: str = "Kaco Tx1 & Tx3 Serie", + type: str = "kaco", + id: int = 0, + configuration: KacoConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or KacoConfiguration() + + +class KacoInverterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +class KacoInverterSetup(ComponentSetup[KacoInverterConfiguration]): + def __init__(self, + name: str = "Kaco Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: KacoInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or KacoInverterConfiguration()) diff --git a/packages/modules/devices/kaco/kaco_tx/device.py b/packages/modules/devices/kaco/kaco_tx/device.py new file mode 100644 index 0000000000..6a5b2f3f01 --- /dev/null +++ b/packages/modules/devices/kaco/kaco_tx/device.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable + +from modules.common import modbus +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.devices.kaco.kaco_tx.inverter import KacoInverter +from modules.devices.kaco.kaco_tx.config import (Kaco, KacoInverterSetup) + +log = logging.getLogger(__name__) + +reconnect_delay = 1.2 + + +def create_device(device_config: Kaco): + client = None + + def create_inverter_component(component_config: KacoInverterSetup): + nonlocal client + return KacoInverter(component_config, client=client) + + def update_components(components: Iterable[KacoInverter]): + nonlocal client + with client: + for component in components: + component.update() + + def initializer(): + nonlocal client + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, + device_config.configuration.port, + reconnect_delay=reconnect_delay) + + device = ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + inverter=create_inverter_component + ), + component_updater=MultiComponentUpdater(update_components) + ) + return device + + +device_descriptor = DeviceDescriptor(configuration_factory=Kaco) diff --git a/packages/modules/devices/kaco/kaco_tx/inverter.py b/packages/modules/devices/kaco/kaco_tx/inverter.py new file mode 100644 index 0000000000..d089b3d17f --- /dev/null +++ b/packages/modules/devices/kaco/kaco_tx/inverter.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from modules.common import modbus +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.modbus import ModbusDataType +from modules.common.store import get_inverter_value_store +from modules.devices.kaco.kaco_tx.config import KacoInverterSetup +from modules.devices.kaco.kaco_tx.scale import create_scaled_reader + + +class KwargsDict(TypedDict): + client: modbus.ModbusTcpClient_ + + +class KacoInverter(AbstractInverter): + def __init__(self, + component_config: KacoInverterSetup, + **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.__tcp_client = self.kwargs['client'] + self.store = get_inverter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self._read_scaled_int16 = create_scaled_reader( + self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.INT_16 + ) + self._read_scaled_int32 = create_scaled_reader( + self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.INT_32 + ) + + def update(self) -> None: + self.store.set(self.read_state()) + + def read_state(self): + # 40084 | Total AC Power | int16 + # 40085 | AC Power scale factor | sunssf + power = self._read_scaled_int16(40084, 1)[0] * -1 + + # 40094 | AC Energy | acc32 + # 40096 | AC Energy scale factor | sunssf + exported = self._read_scaled_int32(40094, 1)[0] + + return InverterState( + power=power, + exported=exported + ) + + +component_descriptor = ComponentDescriptor(configuration_factory=KacoInverterSetup) diff --git a/packages/modules/devices/kaco/kaco_tx/scale.py b/packages/modules/devices/kaco/kaco_tx/scale.py new file mode 100644 index 0000000000..bfe78d2ddd --- /dev/null +++ b/packages/modules/devices/kaco/kaco_tx/scale.py @@ -0,0 +1,27 @@ +import logging +import math +from typing import List + +from modules.common.modbus import ModbusDataType, ModbusTcpClient_, Number + +log = logging.getLogger(__name__) + +# Registers that are not applicable to a meter class return the unsupported value. (e.g. Single Phase +# meters will support only summary and phase A values): + +UINT16_UNSUPPORTED = 0xFFFF + + +def scale_registers(registers: List[Number]) -> List[float]: + log.debug("Registers %s, Scale %s", registers[:-1], registers[-1]) + scale = math.pow(10, registers[-1]) + return [register * scale if register != UINT16_UNSUPPORTED else 0 for register in registers[:-1]] + + +def create_scaled_reader(client: ModbusTcpClient_, modbus_id: int, type: ModbusDataType): + def scaled_reader(address: int, count: int): + return scale_registers( + client.read_holding_registers(address, [type] * count + [ModbusDataType.INT_16], unit=modbus_id) + ) + + return scaled_reader diff --git a/packages/modules/devices/kaco/vendor.py b/packages/modules/devices/kaco/vendor.py new file mode 100644 index 0000000000..f68b3c044e --- /dev/null +++ b/packages/modules/devices/kaco/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 = "Kaco" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)