diff --git a/packages/modules/devices/marstek/__init__.py b/packages/modules/devices/marstek/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/marstek/vendor.py b/packages/modules/devices/marstek/vendor.py new file mode 100644 index 0000000000..20f362f9e9 --- /dev/null +++ b/packages/modules/devices/marstek/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 = "Marstek" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/devices/marstek/venus_c_e/__init__.py b/packages/modules/devices/marstek/venus_c_e/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/marstek/venus_c_e/bat.py b/packages/modules/devices/marstek/venus_c_e/bat.py new file mode 100644 index 0000000000..1f57a668d4 --- /dev/null +++ b/packages/modules/devices/marstek/venus_c_e/bat.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +from typing import Optional, TypedDict, Any, Union +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, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.marstek.venus_c_e.config import VenusCEBatSetup + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class VenusCEBat(AbstractBat): + def __init__(self, component_config: VenusCEBatSetup, **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)) + + def _read_reg(self, addr: int, type_: ModbusDataType) -> Union[int, float]: + return self.client.read_holding_registers(addr, type_, unit=self.component_config.configuration.modbus_id) + + def _write_reg(self, addr: int, val: int) -> None: + # Marstek Venus does not work with write_registers! + self.client._delegate.write_register(addr, val, unit=self.component_config.configuration.modbus_id) + + def update(self) -> None: + power = -self._read_reg(32202, ModbusDataType.INT_32) + soc = self._read_reg(32104, ModbusDataType.UINT_16) + + # Marstek Venus has internal counter but it's buggy, hence we cannot use it + 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: + # Wenn der Speicher die Steuerung der Ladeleistung unterstützt, muss bei Übergabe einer Zahl auf aktive + # Speichersteurung umgeschaltet werden, sodass der Speicher mit der übergebenen Leistung lädt/entlädt. Wird + # None übergeben, muss der Speicher die Null-Punkt-Ausregelung selbst übernehmen. + if (power_limit is None): + self._write_reg(42000, 0x55bb) + else: + self._write_reg(42000, 0x55aa) + if power_limit < 0: + self._write_reg(42010, 2) + self._write_reg(42021, int(min(-power_limit, 2500))) + elif power_limit > 0: + self._write_reg(42010, 1) + self._write_reg(42020, int(min(power_limit, 2500))) + else: + self._write_reg(42010, 0) + + def power_limit_controllable(self) -> bool: + # Wenn der Speicher die Steuerung der Ladeleistung unterstützt, muss True zurückgegeben werden. + return True + + +component_descriptor = ComponentDescriptor(configuration_factory=VenusCEBatSetup) diff --git a/packages/modules/devices/marstek/venus_c_e/config.py b/packages/modules/devices/marstek/venus_c_e/config.py new file mode 100644 index 0000000000..76f1a877ee --- /dev/null +++ b/packages/modules/devices/marstek/venus_c_e/config.py @@ -0,0 +1,42 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + +from ..vendor import vendor_descriptor + + +@auto_str +class VenusCEConfiguration: + def __init__(self, ip_address: Optional[str] = None, port: int = 502): + self.ip_address = ip_address + self.port = port + + +@auto_str +class VenusCE: + def __init__(self, + name: str = "Marstek Venus C, E", + type: str = "venus_c_e", + id: int = 0, + configuration: VenusCEConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or VenusCEConfiguration() + + +@auto_str +class VenusCEBatConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +@auto_str +class VenusCEBatSetup(ComponentSetup[VenusCEBatConfiguration]): + def __init__(self, + name: str = "Marstek Venus C, E Speicher", + type: str = "bat", + id: int = 0, + configuration: VenusCEBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or VenusCEBatConfiguration()) diff --git a/packages/modules/devices/marstek/venus_c_e/device.py b/packages/modules/devices/marstek/venus_c_e/device.py new file mode 100644 index 0000000000..6b402bad0e --- /dev/null +++ b/packages/modules/devices/marstek/venus_c_e/device.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.modbus import ModbusTcpClient_ +from modules.devices.marstek.venus_c_e.bat import VenusCEBat +from modules.devices.marstek.venus_c_e.config import VenusCE, VenusCEBatSetup + +log = logging.getLogger(__name__) + + +def create_device(device_config: VenusCE): + client = None + + def create_bat_component(component_config: VenusCEBatSetup): + nonlocal client + return VenusCEBat(component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[VenusCEBat]): + 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, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=VenusCE)