diff --git a/packages/modules/devices/anker/__init__.py b/packages/modules/devices/anker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/anker/anker_solix/__init__.py b/packages/modules/devices/anker/anker_solix/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/anker/anker_solix/bat.py b/packages/modules/devices/anker/anker_solix/bat.py new file mode 100644 index 0000000000..1297333c42 --- /dev/null +++ b/packages/modules/devices/anker/anker_solix/bat.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import logging +from typing import Any, Optional, TypedDict + +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 ModbusDataType, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.anker.anker_solix.config import AnkerBatSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class AnkerBat(AbstractBat): + def __init__(self, component_config: AnkerBatSetup, **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="speicher") + self.store = get_bat_value_store(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.last_mode = 'Undefined' + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + + power = self.client.read_input_registers(10008, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + soc = self.client.read_input_registers(10014, ModbusDataType.UINT_16, unit=unit) + + self.peak_filter.check_values(power) + 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: + unit = self.component_config.configuration.modbus_id + + if power_limit is None: + log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter") + if self.last_mode is not None: + self.client.write_register(10064, 0, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = None + else: + if self.last_mode != 'limited': + self.client.write_register(10064, 3, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = 'limited' + + # Berechne power value: 0 = stop, != 0 = multipliziere mit -1 + # Laut Doku ist der min Wert 100W, ggf. noch Anpassung für power_limit=0 notwendig + + power_value = 0 if power_limit == 0 else int(power_limit) * -1 + self.client.write_register(10071, power_value, data_type=ModbusDataType.INT_32, unit=unit) + log.debug("Aktive Batteriesteuerung angefordert, angeforderte Leistung: {power_value} W") + + def power_limit_controllable(self) -> bool: + return True + + +component_descriptor = ComponentDescriptor(configuration_factory=AnkerBatSetup) diff --git a/packages/modules/devices/anker/anker_solix/config.py b/packages/modules/devices/anker/anker_solix/config.py new file mode 100644 index 0000000000..74fc9ec810 --- /dev/null +++ b/packages/modules/devices/anker/anker_solix/config.py @@ -0,0 +1,70 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class AnkerConfiguration: + def __init__(self, ip_address: Optional[str] = None, port: int = 502): + self.ip_address = ip_address + self.port = port + + +class Anker: + def __init__(self, + name: str = "Anker", + type: str = "anker", + id: int = 0, + configuration: AnkerConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or AnkerConfiguration() + + +class AnkerBatConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +class AnkerBatSetup(ComponentSetup[AnkerBatConfiguration]): + def __init__(self, + name: str = "Anker Speicher", + type: str = "bat", + id: int = 0, + configuration: AnkerBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or AnkerBatConfiguration()) + + +class AnkerCounterConfiguration: + 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 AnkerCounterSetup(ComponentSetup[AnkerCounterConfiguration]): + def __init__(self, + name: str = "Anker Zähler", + type: str = "counter", + id: int = 0, + configuration: AnkerCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or AnkerCounterConfiguration()) + + +class AnkerInverterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +class AnkerInverterSetup(ComponentSetup[AnkerInverterConfiguration]): + def __init__(self, + name: str = "Anker Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: AnkerInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or AnkerInverterConfiguration()) diff --git a/packages/modules/devices/anker/anker_solix/counter.py b/packages/modules/devices/anker/anker_solix/counter.py new file mode 100644 index 0000000000..3671872a65 --- /dev/null +++ b/packages/modules/devices/anker/anker_solix/counter.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict + +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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.anker.anker_solix.config import AnkerCounterSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class AnkerCounter(AbstractCounter): + def __init__(self, component_config: AnkerCounterSetup, **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)) + self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state) + + def update(self): + unit = self.component_config.configuration.modbus_id + + power = self.client.read_input_registers(10644, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + powers = self.client.read_input_registers(10638, [ModbusDataType.INT_32] * 3, + wordorder=Endian.Little, unit=unit) + voltages = self.client.read_input_registers(10632, [ModbusDataType.UINT_16] * 3, + wordorder=Endian.Little, unit=unit) + currents = self.client.read_input_registers(10666, [ModbusDataType.INT_16] * 3, + wordorder=Endian.Little, unit=unit) + + voltages = [value / 10 for value in voltages] + currents = [value / -100 for value in currents] + + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + powers=powers, + voltages=voltages, + currents=currents + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=AnkerCounterSetup) diff --git a/packages/modules/devices/anker/anker_solix/device.py b/packages/modules/devices/anker/anker_solix/device.py new file mode 100644 index 0000000000..5000adee4f --- /dev/null +++ b/packages/modules/devices/anker/anker_solix/device.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable, Union + +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.common.modbus import ModbusTcpClient_ +from modules.devices.anker.anker_solix.bat import AnkerBat +from modules.devices.anker.anker_solix.config import Anker, AnkerBatSetup, AnkerCounterSetup, AnkerInverterSetup +from modules.devices.anker.anker_solix.counter import AnkerCounter +from modules.devices.anker.anker_solix.inverter import AnkerInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: Anker): + client = None + + def create_bat_component(component_config: AnkerBatSetup): + nonlocal client + return AnkerBat(component_config, device_id=device_config.id, client=client) + + def create_counter_component(component_config: AnkerCounterSetup): + nonlocal client + return AnkerCounter(component_config, device_id=device_config.id, client=client) + + def create_inverter_component(component_config: AnkerInverterSetup): + nonlocal client + return AnkerInverter(component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[Union[AnkerBat, AnkerCounter, AnkerInverter]]): + nonlocal client + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + 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( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Anker) diff --git a/packages/modules/devices/anker/anker_solix/inverter.py b/packages/modules/devices/anker/anker_solix/inverter.py new file mode 100644 index 0000000000..4110529842 --- /dev/null +++ b/packages/modules/devices/anker/anker_solix/inverter.py @@ -0,0 +1,55 @@ +#!/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.modbus import ModbusDataType, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.anker.anker_solix.config import AnkerInverterSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class AnkerInverter(AbstractInverter): + def __init__(self, component_config: AnkerInverterSetup, **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="pv") + self.store = get_inverter_value_store(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) + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + + # Register 10002 ist die PV_power also die DC Leistung + # Register 10010 ist "Load_power" unklar ob dies wirklich die AC Leistung des Inverters ist + power = self.client.read_input_registers(10010, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + dc_power = self.client.read_input_registers(10002, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + inverter_state = InverterState( + power=power, + dc_power=dc_power, + imported=imported, + exported=exported + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=AnkerInverterSetup) diff --git a/packages/modules/devices/anker/vendor.py b/packages/modules/devices/anker/vendor.py new file mode 100644 index 0000000000..69365e206f --- /dev/null +++ b/packages/modules/devices/anker/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 = "Anker" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)