From 535593845ef829723bb4fc18a15382fcd0b7b9df Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 11 Nov 2025 09:54:28 +0100 Subject: [PATCH 1/4] adjust registers/ add bat control --- .../modules/devices/solarmax/solarmax/bat.py | 38 +++++++++++++-- .../devices/solarmax/solarmax/config.py | 28 +++++++++++ .../solarmax/solarmax/counter_maxstorage.py | 47 +++++++++++++++++++ .../devices/solarmax/solarmax/device.py | 21 +++++++-- .../solarmax/solarmax/inverter_maxstorage.py | 46 ++++++++++++++++++ python_test.sh | 1 + 6 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 packages/modules/devices/solarmax/solarmax/counter_maxstorage.py create mode 100644 packages/modules/devices/solarmax/solarmax/inverter_maxstorage.py create mode 100755 python_test.sh diff --git a/packages/modules/devices/solarmax/solarmax/bat.py b/packages/modules/devices/solarmax/solarmax/bat.py index c15135050e..f8dda4f93d 100644 --- a/packages/modules/devices/solarmax/solarmax/bat.py +++ b/packages/modules/devices/solarmax/solarmax/bat.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -from typing import TypedDict, Any +import logging +from typing import TypedDict, Any, Optional +from pymodbus.constants import Endian from modules.common.abstract_device import AbstractBat from modules.common.component_state import BatState from modules.common.component_type import ComponentDescriptor @@ -10,6 +12,8 @@ from modules.common.store import get_bat_value_store from modules.devices.solarmax.solarmax.config import SolarmaxBatSetup +log = logging.getLogger(__name__) + class KwargsDict(TypedDict): device_id: int @@ -30,8 +34,8 @@ def initialize(self) -> None: def update(self) -> None: unit = self.component_config.configuration.modbus_id - power = self.client.read_holding_registers(114, ModbusDataType.INT_32, unit=unit) - soc = self.client.read_holding_registers(122, ModbusDataType.INT_16, unit=unit) + power = self.client.read_input_registers(114, ModbusDataType.INT_32, unit=unit, wordorder=Endian.Little) + soc = self.client.read_input_registers(122, ModbusDataType.INT_16, unit=unit) imported, exported = self.sim_counter.sim_count(power) bat_state = BatState( @@ -42,5 +46,33 @@ def update(self) -> None: ) self.store.set(bat_state) + def set_power_limit(self, power_limit: Optional[int]) -> None: + unit = self.component_config.configuration.modbus_id + log.debug(f'last_mode: {self.last_mode}') + # reg 142 is automatically reset every 60s so needs to be written continuously + if power_limit is None: + log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter") + if self.last_mode is not None: + self.__tcp_client.write_registers(142, [0], data_type=ModbusDataType.INT_16, unit=unit) + self.last_mode = None + elif power_limit == 0: + log.debug("Aktive Batteriesteuerung. Batterie wird auf Stop gesetzt und nicht entladen") + self.__tcp_client.write_registers(140, [0], data_type=ModbusDataType.INT_16, unit=unit) + self.__tcp_client.write_registers(141, [0], data_type=ModbusDataType.INT_16, unit=unit) + self.__tcp_client.write_registers(142, [1], data_type=ModbusDataType.INT_16, unit=unit) + self.last_mode = 'stop' + elif power_limit < 0: + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_limit} W entladen für den Hausverbrauch") + self.__tcp_client.write_registers(142, [1], data_type=ModbusDataType.INT_16, unit=unit) + self.last_mode = 'discharge' + # Die maximale Entladeleistung begrenzen auf 5000W, maximaler Wertebereich Modbusregister. + power_value = int(min(abs(power_limit), 7000)) + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_value} W entladen für den Hausverbrauch") + self.__tcp_client.write_registers(140, [power_value], data_type=ModbusDataType.INT_16, unit=unit) + self.__tcp_client.write_registers(141, [power_value], data_type=ModbusDataType.INT_16, unit=unit) + + def power_limit_controllable(self) -> bool: + return True + component_descriptor = ComponentDescriptor(configuration_factory=SolarmaxBatSetup) diff --git a/packages/modules/devices/solarmax/solarmax/config.py b/packages/modules/devices/solarmax/solarmax/config.py index 32596ef821..6f8bd082d7 100644 --- a/packages/modules/devices/solarmax/solarmax/config.py +++ b/packages/modules/devices/solarmax/solarmax/config.py @@ -23,6 +23,20 @@ def __init__(self, self.configuration = configuration or SolarmaxConfiguration() +class SolarmaxMsCounterConfiguration: + def __init__(self): + pass + + +class SolarmaxMsCounterSetup(ComponentSetup[SolarmaxMsCounterConfiguration]): + def __init__(self, + name: str = "Solarmax MAX.STORAGE / MAX.STORAGE Ultimate Zähler", + type: str = "counter_maxstorage", + id: Optional[int] = 0, + configuration: SolarmaxMsCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SolarmaxMsCounterConfiguration()) + + class SolarmaxBatConfiguration: def __init__(self, modbus_id: int = 1): self.modbus_id = modbus_id @@ -37,6 +51,20 @@ def __init__(self, super().__init__(name, type, id, configuration or SolarmaxBatConfiguration()) +class SolarmaxMsInverterConfiguration: + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id + + +class SolarmaxMsInverterSetup(ComponentSetup[SolarmaxMsInverterConfiguration]): + def __init__(self, + name: str = "Solarmax MAX.STORAGE / MAX.STORAGE UltimateWechselrichter", + type: str = "inverter_maxstorage", + id: int = 0, + configuration: SolarmaxMsInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SolarmaxMsInverterConfiguration()) + + class SolarmaxInverterConfiguration: def __init__(self, modbus_id: int = 1): self.modbus_id = modbus_id diff --git a/packages/modules/devices/solarmax/solarmax/counter_maxstorage.py b/packages/modules/devices/solarmax/solarmax/counter_maxstorage.py new file mode 100644 index 0000000000..8d524f4856 --- /dev/null +++ b/packages/modules/devices/solarmax/solarmax/counter_maxstorage.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from pymodbus.constants import Endian +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, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.solarmax.solarmax.config import SolarmaxMsCounterSetup + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class SolarmaxMsCounter(AbstractCounter): + def __init__(self, + component_config: SolarmaxMsCounterSetup, + **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)) + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + power = self.client.read_input_registers(118, ModbusDataType.INT_32, unit=unit, wordorder=Endian.Little) * -1 + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + power=power, + imported=imported, + exported=exported + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=SolarmaxMsCounterSetup) diff --git a/packages/modules/devices/solarmax/solarmax/device.py b/packages/modules/devices/solarmax/solarmax/device.py index 3fb514d09d..069daa0320 100644 --- a/packages/modules/devices/solarmax/solarmax/device.py +++ b/packages/modules/devices/solarmax/solarmax/device.py @@ -8,9 +8,13 @@ from modules.common.component_context import SingleComponentUpdateContext from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.devices.solarmax.solarmax import inverter +from modules.devices.solarmax.solarmax.inverter import SolarmaxInverter from modules.devices.solarmax.solarmax.bat import SolarmaxBat -from modules.devices.solarmax.solarmax.config import ( - Solarmax, SolarmaxBatSetup, SolarmaxConfiguration, SolarmaxInverterSetup) +from modules.devices.solarmax.solarmax.counter_maxstorage import SolarmaxMsCounter +from modules.devices.solarmax.solarmax.inverter_maxstorage import SolarmaxMsInverter +from modules.devices.solarmax.solarmax.config import (Solarmax, SolarmaxConfiguration, + SolarmaxBatSetup, SolarmaxMsCounterSetup, + SolarmaxInverterSetup, SolarmaxMsInverterSetup) log = logging.getLogger(__name__) @@ -24,9 +28,18 @@ def create_bat_component(component_config: SolarmaxBatSetup): def create_inverter_component(component_config: SolarmaxInverterSetup): nonlocal client - return inverter.SolarmaxInverter(component_config, device_id=device_config.id, client=client) + return SolarmaxInverter(component_config, device_id=device_config.id, client=client) - def update_components(components: Iterable[Union[SolarmaxBat, inverter.SolarmaxInverter]]): + def create_inverter_ms_component(component_config: SolarmaxMsInverterSetup): + nonlocal client + return SolarmaxMsInverter(component_config, device_id=device_config.id, client=client) + + def create_counter_ms_component(component_config: SolarmaxMsCounterSetup): + nonlocal client + return SolarmaxMsCounter(component_config, device_id=device_config.id, client=client) + + def update_components(components: Iterable[Union[SolarmaxBat, SolarmaxInverter, + SolarmaxMsCounter, SolarmaxMsInverter]]): nonlocal client with client: for component in components: diff --git a/packages/modules/devices/solarmax/solarmax/inverter_maxstorage.py b/packages/modules/devices/solarmax/solarmax/inverter_maxstorage.py new file mode 100644 index 0000000000..d8d9722380 --- /dev/null +++ b/packages/modules/devices/solarmax/solarmax/inverter_maxstorage.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +from pymodbus.constants import Endian +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, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.solarmax.solarmax.config import SolarmaxMsInverterSetup + + +class KwargsDict(TypedDict): + device_id: int + client: ModbusTcpClient_ + + +class SolarmaxMsInverter(AbstractInverter): + def __init__(self, + component_config: SolarmaxMsInverterSetup, + **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)) + + def update(self) -> None: + unit = self.component_config.configuration.modbus_id + power = self.client.read_input_registers(120, ModbusDataType.INT_32, unit=unit, wordorder=Endian.Little) * -1 + _, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + exported=exported + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=SolarmaxMsInverterSetup) diff --git a/python_test.sh b/python_test.sh new file mode 100755 index 0000000000..0a3fd62db5 --- /dev/null +++ b/python_test.sh @@ -0,0 +1 @@ +PYTHONPATH="/var/www/html/openWB/packages" python3.9 -m pytest -vv -o log_cli=true --log-cli-level=DEBUG; python3.9 -m flake8 --max-line-length 120 /var/www/html/openWB/packages --exclude='/var/www/html/openWB/packages/modules/display_themes','/var/www/html/openWB/packages/modules/web_themes' From b64153fd599020620f4ad15b0e552574c2e31fa7 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 11 Nov 2025 09:56:27 +0100 Subject: [PATCH 2/4] remove test --- python_test.sh | 1 - 1 file changed, 1 deletion(-) delete mode 100755 python_test.sh diff --git a/python_test.sh b/python_test.sh deleted file mode 100755 index 0a3fd62db5..0000000000 --- a/python_test.sh +++ /dev/null @@ -1 +0,0 @@ -PYTHONPATH="/var/www/html/openWB/packages" python3.9 -m pytest -vv -o log_cli=true --log-cli-level=DEBUG; python3.9 -m flake8 --max-line-length 120 /var/www/html/openWB/packages --exclude='/var/www/html/openWB/packages/modules/display_themes','/var/www/html/openWB/packages/modules/web_themes' From 0fd7faf7bc416053303e4dfefcbceb605ecca82c Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 11 Nov 2025 11:18:31 +0100 Subject: [PATCH 3/4] add variables --- packages/modules/devices/solarmax/solarmax/bat.py | 2 +- packages/modules/devices/solarmax/solarmax/config.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/modules/devices/solarmax/solarmax/bat.py b/packages/modules/devices/solarmax/solarmax/bat.py index f8dda4f93d..97e91d9f79 100644 --- a/packages/modules/devices/solarmax/solarmax/bat.py +++ b/packages/modules/devices/solarmax/solarmax/bat.py @@ -72,7 +72,7 @@ def set_power_limit(self, power_limit: Optional[int]) -> None: self.__tcp_client.write_registers(141, [power_value], data_type=ModbusDataType.INT_16, unit=unit) def power_limit_controllable(self) -> bool: - return True + return self.component_config.configuration.power_limit_controllable component_descriptor = ComponentDescriptor(configuration_factory=SolarmaxBatSetup) diff --git a/packages/modules/devices/solarmax/solarmax/config.py b/packages/modules/devices/solarmax/solarmax/config.py index 6f8bd082d7..b0efd51e34 100644 --- a/packages/modules/devices/solarmax/solarmax/config.py +++ b/packages/modules/devices/solarmax/solarmax/config.py @@ -24,8 +24,8 @@ def __init__(self, class SolarmaxMsCounterConfiguration: - def __init__(self): - pass + def __init__(self, modbus_id: int = 1): + self.modbus_id = modbus_id class SolarmaxMsCounterSetup(ComponentSetup[SolarmaxMsCounterConfiguration]): @@ -38,8 +38,9 @@ def __init__(self, class SolarmaxBatConfiguration: - def __init__(self, modbus_id: int = 1): + def __init__(self, modbus_id: int = 1, power_limit_controllable: bool = False): self.modbus_id = modbus_id + self.power_limit_controllable = power_limit_controllable class SolarmaxBatSetup(ComponentSetup[SolarmaxBatConfiguration]): @@ -58,7 +59,7 @@ def __init__(self, modbus_id: int = 1): class SolarmaxMsInverterSetup(ComponentSetup[SolarmaxMsInverterConfiguration]): def __init__(self, - name: str = "Solarmax MAX.STORAGE / MAX.STORAGE UltimateWechselrichter", + name: str = "Solarmax MAX.STORAGE / MAX.STORAGE Ultimate Wechselrichter", type: str = "inverter_maxstorage", id: int = 0, configuration: SolarmaxMsInverterConfiguration = None) -> None: From 07cf0b3ef4d12ac67d0453234baf6f82362db9fa Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 11 Nov 2025 13:17:16 +0100 Subject: [PATCH 4/4] create components --- packages/modules/devices/solarmax/solarmax/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/modules/devices/solarmax/solarmax/device.py b/packages/modules/devices/solarmax/solarmax/device.py index 069daa0320..75181d9f23 100644 --- a/packages/modules/devices/solarmax/solarmax/device.py +++ b/packages/modules/devices/solarmax/solarmax/device.py @@ -56,6 +56,9 @@ def initializer(): component_factory=ComponentFactoryByType( bat=create_bat_component, inverter=create_inverter_component, + counter_maxstorage=create_counter_ms_component, + inverter_maxstorage=create_inverter_ms_component, + ), component_updater=MultiComponentUpdater(update_components) )