From c6672018db99d9b175653315304c1f9bb0f9eb54 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 16 Apr 2026 14:04:39 +0200 Subject: [PATCH 1/3] add support for Deye LSW Dongle --- .../modules/devices/deye/deye_solarman/bat.py | 69 ++++++++++++++++ .../devices/deye/deye_solarman/config.py | 78 +++++++++++++++++++ .../devices/deye/deye_solarman/counter.py | 74 ++++++++++++++++++ .../devices/deye/deye_solarman/device.py | 60 ++++++++++++++ .../devices/deye/deye_solarman/device_type.py | 9 +++ .../devices/deye/deye_solarman/inverter.py | 56 +++++++++++++ requirements.txt | 1 + 7 files changed, 347 insertions(+) create mode 100644 packages/modules/devices/deye/deye_solarman/bat.py create mode 100644 packages/modules/devices/deye/deye_solarman/config.py create mode 100644 packages/modules/devices/deye/deye_solarman/counter.py create mode 100644 packages/modules/devices/deye/deye_solarman/device.py create mode 100644 packages/modules/devices/deye/deye_solarman/device_type.py create mode 100644 packages/modules/devices/deye/deye_solarman/inverter.py diff --git a/packages/modules/devices/deye/deye_solarman/bat.py b/packages/modules/devices/deye/deye_solarman/bat.py new file mode 100644 index 0000000000..dde1183f5b --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/bat.py @@ -0,0 +1,69 @@ +import logging +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.simcount import SimCounter +from modules.common.utils.peak_filter import PeakFilter +from modules.common.store import get_bat_value_store +from modules.devices.deye.deye_solarman.config import DeyeSolarmanBatSetup +from modules.devices.deye.deye_solarman.device_type import DeviceType +from modules.common.component_type import ComponentType +from pysolarmanv5 import PySolarmanV5 as ModbusSolarmanClient_ + +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusSolarmanClient_ + + +class DeyeSolarmanBat(AbstractBat): + def __init__(self, component_config: DeyeSolarmanBatSetup, **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: ModbusSolarmanClient_ = self.kwargs['client'] + 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.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") + self.device_type = DeviceType(self.client.read_holding_registers(0, 1)[0]) + + def update(self) -> None: + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + power = self.client.read_holding_registers(190, 1)[0] * -1 + soc = self.client.read_holding_registers(184, 1)[0] + + if self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + imported = self.client.read_holding_registers(72, 1)[0] * 100 + exported = self.client.read_holding_registers(74, 1)[0] * 100 + imported, exported = self.peak_filter.check_values(power, imported, exported) + + elif self.device_type == DeviceType.SINGLE_PHASE_STRING: + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) + power = self.client.read_holding_registers(590, 1)[0] * -1 + + if self.device_type == DeviceType.THREE_PHASE_HV: + power = power * 10 + soc = self.client.read_holding_registers(588, 1)[0] + 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) + + +component_descriptor = ComponentDescriptor(configuration_factory=DeyeSolarmanBatSetup) diff --git a/packages/modules/devices/deye/deye_solarman/config.py b/packages/modules/devices/deye/deye_solarman/config.py new file mode 100644 index 0000000000..6950e6657e --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/config.py @@ -0,0 +1,78 @@ +from typing import Optional +from helpermodules.auto_str import auto_str + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class DeyeSolarmanConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + serial: int = None, + port: int = 502, + modbus_id: int = 1): + self.ip_address = ip_address + self.serial = serial + self.port = port + self.modbus_id = modbus_id + + +class DeyeSolarman: + def __init__(self, + name: str = "Deye/Jinko (Anbindung per LSW Dongle)", + type: str = "deye_solarman", + id: int = 0, + configuration: DeyeSolarmanConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or DeyeSolarmanConfiguration() + + +@auto_str +class DeyeSolarmanBatConfiguration: + def __init__(self): + pass + + +@auto_str +class DeyeSolarmanBatSetup(ComponentSetup[DeyeSolarmanBatConfiguration]): + def __init__(self, + name: str = "Deye/Jinko Speicher", + type: str = "bat", + id: int = 0, + configuration: DeyeSolarmanBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or DeyeSolarmanBatConfiguration()) + + +@auto_str +class DeyeSolarmanCounterConfiguration: + def __init__(self): + pass + + +@auto_str +class DeyeSolarmanCounterSetup(ComponentSetup[DeyeSolarmanCounterConfiguration]): + def __init__(self, + name: str = "Deye/Jinko Zähler", + type: str = "counter", + id: int = 0, + configuration: DeyeSolarmanCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or DeyeSolarmanCounterConfiguration()) + + +@auto_str +class DeyeSolarmanInverterConfiguration: + def __init__(self): + pass + + +@auto_str +class DeyeSolarmanInverterSetup(ComponentSetup[DeyeSolarmanInverterConfiguration]): + def __init__(self, + name: str = "Deye/Jinko Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: DeyeSolarmanInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or DeyeSolarmanInverterConfiguration()) diff --git a/packages/modules/devices/deye/deye_solarman/counter.py b/packages/modules/devices/deye/deye_solarman/counter.py new file mode 100644 index 0000000000..1604e58459 --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/counter.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +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.utils.peak_filter import PeakFilter +from modules.common.store import get_counter_value_store +from modules.devices.deye.deye_solarman.config import DeyeSolarmanCounterSetup +from modules.devices.deye.deye_solarman.device_type import DeviceType +from modules.common.component_type import ComponentType +from pysolarmanv5 import PySolarmanV5 as ModbusSolarmanClient_ + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusSolarmanClient_ + + +class DeyeSolarmanCounter(AbstractCounter): + def __init__(self, component_config: DeyeSolarmanCounterSetup, **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: ModbusSolarmanClient_ = self.kwargs['client'] + 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) + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.device_type = DeviceType(self.client.read_holding_registers(0, 1)[0]) + + def update(self): + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + frequency = self.client.read_holding_registers(79, 1)[0] / 100 + + if self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + powers = [0]*3 + currents = [0]*3 + voltages = [0]*3 + power = [0] + + elif self.device_type == DeviceType.SINGLE_PHASE_STRING: + currents = [c / 100 for c in self.client.read_holding_registers(76, 3)] + voltages = [v / 10 for v in self.client.read_holding_registers(70, 3)] + powers = [currents[i] * voltages[i] for i in range(0, 3)] + power = sum(powers) + + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) + currents = [c / 100 for c in self.client.read_holding_registers(613, 3)] + voltages = [v / 10 for v in self.client.read_holding_registers(644, 3)] + powers = self.client.read_holding_registers(616, 3) + power = sum(powers) + frequency = self.client.read_holding_registers(609, 1)[0] / 100 + + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + counter_state = CounterState( + currents=currents, + voltages=voltages, + powers=powers, + power=power, + imported=imported, + exported=exported, + frequency=frequency + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=DeyeSolarmanCounterSetup) diff --git a/packages/modules/devices/deye/deye_solarman/device.py b/packages/modules/devices/deye/deye_solarman/device.py new file mode 100644 index 0000000000..9f9ef0d2b5 --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/device.py @@ -0,0 +1,60 @@ +#!/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 ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.devices.deye.deye_solarman.counter import DeyeSolarmanCounter +from modules.devices.deye.deye_solarman.inverter import DeyeSolarmanInverter +from modules.devices.deye.deye_solarman.bat import DeyeSolarmanBat +from modules.devices.deye.deye_solarman.config import (DeyeSolarman, DeyeSolarmanCounterSetup, + DeyeSolarmanInverterSetup, DeyeSolarmanBatSetup) +from pysolarmanv5 import PySolarmanV5 as ModbusSolarmanClient_ + +log = logging.getLogger(__name__) + + +def create_device(device_config: DeyeSolarman): + client = None + + def create_bat_component(component_config: DeyeSolarmanBatSetup): + nonlocal client + return DeyeSolarmanBat(component_config=component_config, device_id=device_config.id, client=client) + + def create_counter_component(component_config: DeyeSolarmanCounterSetup): + nonlocal client + return DeyeSolarmanCounter(component_config=component_config, device_id=device_config.id, client=client) + + def create_inverter_component(component_config: DeyeSolarmanInverterSetup): + nonlocal client + return DeyeSolarmanInverter(component_config=component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[Union[DeyeSolarmanBat, DeyeSolarmanCounter, DeyeSolarmanInverter]]): + nonlocal client + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = ModbusSolarmanClient_(device_config.configuration.ip_address, + device_config.configuration.serial, + port=device_config.configuration.port, + mb_slave_id=device_config.configuration.modbus_id, + auto_reconnect=True) + + 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=DeyeSolarman) diff --git a/packages/modules/devices/deye/deye_solarman/device_type.py b/packages/modules/devices/deye/deye_solarman/device_type.py new file mode 100644 index 0000000000..c5241c1a63 --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/device_type.py @@ -0,0 +1,9 @@ +from enum import IntEnum + + +class DeviceType(IntEnum): + SINGLE_PHASE_STRING = 0x0200 + SINGLE_PHASE_HYBRID = 0x0300 + THREE_PHASE_LV_0 = 0x0500 + THREE_PHASE_LV_1 = 0x0005 + THREE_PHASE_HV = 0x0006 diff --git a/packages/modules/devices/deye/deye_solarman/inverter.py b/packages/modules/devices/deye/deye_solarman/inverter.py new file mode 100644 index 0000000000..4089779538 --- /dev/null +++ b/packages/modules/devices/deye/deye_solarman/inverter.py @@ -0,0 +1,56 @@ +#!/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.simcount import SimCounter +from modules.common.utils.peak_filter import PeakFilter +from modules.common.store import get_inverter_value_store +from modules.devices.deye.deye_solarman.config import DeyeSolarmanInverterSetup +from modules.devices.deye.deye_solarman.device_type import DeviceType +from modules.common.component_type import ComponentType +from pysolarmanv5 import PySolarmanV5 as ModbusSolarmanClient_ + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusSolarmanClient_ + + +class DeyeSolarmanInverter(AbstractInverter): + def __init__(self, component_config: DeyeSolarmanInverterSetup, **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: ModbusSolarmanClient_ = 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.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state) + self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") + self.device_type = DeviceType(self.client.read_holding_registers(0, 1)[0]) + + def update(self) -> None: + if self.device_type == DeviceType.SINGLE_PHASE_STRING or self.device_type == DeviceType.SINGLE_PHASE_HYBRID: + power = sum(self.client.read_holding_registers(186, 4)) * -1 + + else: # THREE_PHASE_LV (0x0500, 0x0005), THREE_PHASE_HV (0x0006) + power = sum(self.client.read_holding_registers(672, 2)) * -1 + + if self.device_type == DeviceType.THREE_PHASE_HV: + power = power * 10 + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + imported=imported, + exported=exported, + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=DeyeSolarmanInverterSetup) diff --git a/requirements.txt b/requirements.txt index d3f1163050..fa46e2c5ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ websockets==12.0 pycarwings3==0.7.14 asyncio==3.4.3 passlib==1.7.4 +pysolarmanv5==3.0.6 From c8338a60bfc3b5ac4eb9003dc9e9f0f976c38f5f Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Mon, 20 Apr 2026 10:59:06 +0200 Subject: [PATCH 2/3] change default port --- packages/modules/devices/deye/deye_solarman/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/devices/deye/deye_solarman/config.py b/packages/modules/devices/deye/deye_solarman/config.py index 6950e6657e..0e4bbf4554 100644 --- a/packages/modules/devices/deye/deye_solarman/config.py +++ b/packages/modules/devices/deye/deye_solarman/config.py @@ -9,7 +9,7 @@ class DeyeSolarmanConfiguration: def __init__(self, ip_address: Optional[str] = None, serial: int = None, - port: int = 502, + port: int = 8899, modbus_id: int = 1): self.ip_address = ip_address self.serial = serial From 040d781e988288a5c815c8f9fe0f264a86a3305a Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Mon, 20 Apr 2026 11:30:37 +0200 Subject: [PATCH 3/3] fix update function --- packages/modules/devices/deye/deye_solarman/device.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/modules/devices/deye/deye_solarman/device.py b/packages/modules/devices/deye/deye_solarman/device.py index 9f9ef0d2b5..b5b9c533ad 100644 --- a/packages/modules/devices/deye/deye_solarman/device.py +++ b/packages/modules/devices/deye/deye_solarman/device.py @@ -32,10 +32,9 @@ def create_inverter_component(component_config: DeyeSolarmanInverterSetup): def update_components(components: Iterable[Union[DeyeSolarmanBat, DeyeSolarmanCounter, DeyeSolarmanInverter]]): nonlocal client - with client: - for component in components: - with SingleComponentUpdateContext(component.fault_state): - component.update() + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() def initializer(): nonlocal client