Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/modules/devices/deye/deye_solarman/bat.py
Original file line number Diff line number Diff line change
@@ -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)
78 changes: 78 additions & 0 deletions packages/modules/devices/deye/deye_solarman/config.py
Original file line number Diff line number Diff line change
@@ -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,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serial is annotated as int but defaults to None. This is inconsistent with the type hint and makes static analysis / IDE assistance inaccurate. Consider changing it to Optional[int] (and/or validating that a serial is provided before creating the client).

Suggested change
serial: int = None,
serial: Optional[int] = None,

Copilot uses AI. Check for mistakes.
port: int = 8899,
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())
74 changes: 74 additions & 0 deletions packages/modules/devices/deye/deye_solarman/counter.py
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a commented-out import (# from modules.common.modbus import ModbusDataType) left in the file. Please remove it to avoid dead code and keep the module clean.

Suggested change
# from modules.common.modbus import ModbusDataType

Copilot uses AI. Check for mistakes.
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]
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the SINGLE_PHASE_HYBRID branch, power is set to a list ([0]). PeakFilter.check_values(power) and SimCounter.sim_count(power) expect a numeric (float/int) and will raise a TypeError when this device type is detected. Use a numeric 0 (or compute a scalar power) instead of a list.

Suggested change
power = [0]
power = 0

Copilot uses AI. Check for mistakes.

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)
59 changes: 59 additions & 0 deletions packages/modules/devices/deye/deye_solarman/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/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
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)
9 changes: 9 additions & 0 deletions packages/modules/devices/deye/deye_solarman/device_type.py
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions packages/modules/devices/deye/deye_solarman/inverter.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ websockets==12.0
pycarwings3==0.7.14
asyncio==3.4.3
passlib==1.7.4
pysolarmanv5==3.0.6
Loading