From bdbab90c169b031d897cc952e80daa106e8e1326 Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Wed, 15 Jul 2026 23:20:28 +0200 Subject: [PATCH 1/4] Refactor NaN constants and update checks --- .../devices/sma/sma_sunny_boy/inverter.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index b086943d42..358a4218a3 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -24,10 +24,10 @@ class KwargsDict(TypedDict): class SmaSunnyBoyInverter(AbstractInverter): - - SMA_INT32_NAN = -0x80000000 # SMA uses this value to represent NaN - SMA_UINT32_NAN = 0xFFFFFFFF # SMA uses this value to represent NaN - SMA_NAN = -0xC000 + SMA_INT16_NAN = -0x8000 + SMA_INT32_NAN = -0x80000000 + SMA_UINT32_NAN = 0xFFFFFFFF + SMA_UINT64_NAN = 0xFFFFFFFFFFFFFFFF def __init__(self, component_config: SmaSunnyBoyInverterSetup, @@ -75,11 +75,14 @@ def update(self) -> None: currents = [(power_total / 3 / 230)] * 3 else: raise ValueError("Unbekannte Version "+str(self.component_config.configuration.version)) - if power_total == self.SMA_INT32_NAN or power_total == self.SMA_NAN: + + # WR geht nachts in Standby und gibt einen NaN-Wert für die Leistung aus. + if power_total in (self.SMA_INT16_NAN, self.SMA_INT32_NAN, + -self.SMA_INT16_NAN * 10, -self.SMA_INT32_NAN): power_total = 0 - # WR geht nachts in Standby und gibt einen NaN-Wert für die Leistung aus. + dc_power = 0 currents = [0, 0, 0] - if energy == self.SMA_UINT32_NAN: + if energy in (self.SMA_UINT32_NAN, self.SMA_UINT32_NAN * 100, self.SMA_UINT64_NAN): raise ValueError( f'Wechselrichter lieferte nicht plausiblen Zählerstand: {energy}. ' 'Sobald PV Ertrag vorhanden ist sollte sich dieser Wert ändern, ' From e1c87309d04d66a5fbdb52a4675d7639d307f3b9 Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Wed, 12 Aug 2026 13:41:37 +0200 Subject: [PATCH 2/4] Add check_nan function --- .../devices/sma/sma_sunny_boy/inverter.py | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index 358a4218a3..2cbe44fba9 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -24,6 +24,7 @@ class KwargsDict(TypedDict): class SmaSunnyBoyInverter(AbstractInverter): + # SMA-Sentinel-Werte für "kein gültiger Wert" -- abhängig vom Modbus-Registertyp. SMA_INT16_NAN = -0x8000 SMA_INT32_NAN = -0x80000000 SMA_UINT32_NAN = 0xFFFFFFFF @@ -35,6 +36,19 @@ def __init__(self, self.component_config = component_config self.kwargs: KwargsDict = kwargs + @staticmethod + def check_nan(value: int, nan_value: int, default_value: int = 0) -> "tuple[int, bool]": + """Prüft einen Rohwert direkt beim Auslesen gegen den zum jeweiligen + Registertyp passenden NaN-Sentinel. Multiplikator/Vorzeichen werden + erst NACH diesem Check auf den Rückgabewert angewendet -- so bleibt + der Vergleich immer exakt beim Rohwert und es kann kein Vorzeichen- + Fehler beim getrennten Nachbilden des Sentinels mehr passieren. + Gibt (Wert bzw. default_value, ob NaN erkannt wurde) zurück. + """ + if value == nan_value: + return default_value, True + return value, False + def initialize(self) -> None: self.tcp_client = self.kwargs['client'] self.store = get_component_value_store(self.component_config.type, self.component_config.id) @@ -46,48 +60,83 @@ def update(self) -> None: unit = self.component_config.configuration.modbus_id if self.component_config.configuration.version == SmaInverterVersion.default: - power_total = self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) * -1 - energy = self.tcp_client.read_holding_registers(30529, ModbusDataType.UINT_32, unit=unit) - dc_power = (self.tcp_client.read_holding_registers(30773, ModbusDataType.INT_32, unit=unit) + - self.tcp_client.read_holding_registers(30961, ModbusDataType.INT_32, unit=unit)) * -1 + power_total, power_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit), + self.SMA_INT32_NAN) + power_total *= -1 + + energy, energy_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(30529, ModbusDataType.UINT_32, unit=unit), + self.SMA_UINT32_NAN) - currents = self.tcp_client.read_holding_registers(30977, [ModbusDataType.INT_32]*3, unit=unit) + dc1, _ = self.check_nan( + self.tcp_client.read_holding_registers(30773, ModbusDataType.INT_32, unit=unit), self.SMA_INT32_NAN) + dc2, _ = self.check_nan( + self.tcp_client.read_holding_registers(30961, ModbusDataType.INT_32, unit=unit), self.SMA_INT32_NAN) + dc_power = (dc1 + dc2) * -1 + + currents = self.tcp_client.read_holding_registers(30977, [ModbusDataType.INT_32] * 3, unit=unit) if all(c == self.SMA_INT32_NAN for c in currents): currents = None else: - currents = [current / -1000 if current != self.SMA_INT32_NAN else 0 for current in currents] + currents = [self.check_nan(c, self.SMA_INT32_NAN)[0] / -1000 for c in currents] + elif self.component_config.configuration.version == SmaInverterVersion.core2: - power_total = self.tcp_client.read_holding_registers(40084, ModbusDataType.INT_16, unit=unit) * -10 - energy = self.tcp_client.read_holding_registers(40094, ModbusDataType.UINT_32, unit=unit) * 100 - dc_power = self.tcp_client.read_holding_registers(40101, ModbusDataType.UINT_32, unit=unit) * -100 - currents = self.tcp_client.read_holding_registers(30977, [ModbusDataType.INT_32]*3, unit=unit) + power_total, power_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(40084, ModbusDataType.INT_16, unit=unit), + self.SMA_INT16_NAN) + power_total *= -10 + + energy, energy_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(40094, ModbusDataType.UINT_32, unit=unit), + self.SMA_UINT32_NAN) + energy *= 100 + + dc_power, _ = self.check_nan( + self.tcp_client.read_holding_registers(40101, ModbusDataType.UINT_32, unit=unit), + self.SMA_UINT32_NAN) + dc_power *= -100 + + currents = self.tcp_client.read_holding_registers(30977, [ModbusDataType.INT_32] * 3, unit=unit) if all(c == self.SMA_INT32_NAN for c in currents): currents = None else: - currents = [current / -1000 if current != self.SMA_INT32_NAN else 0 for current in currents] + currents = [self.check_nan(c, self.SMA_INT32_NAN)[0] / -1000 for c in currents] + elif self.component_config.configuration.version == SmaInverterVersion.datamanager: - power_total = self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) * -1 - energy = self.tcp_client.read_holding_registers(30513, ModbusDataType.UINT_64, unit=unit) - # Aus kompatibilitätsgründen wird dc_power auf den Wert der AC-Wirkleistung gesetzt. + power_total, power_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit), + self.SMA_INT32_NAN) + power_total *= -1 + + energy, energy_is_nan = self.check_nan( + self.tcp_client.read_holding_registers(30513, ModbusDataType.UINT_64, unit=unit), + self.SMA_UINT64_NAN) + + # Aus Kompatibilitätsgründen wird dc_power auf den Wert der AC-Wirkleistung gesetzt. dc_power = power_total + # Der Data-Manager/Cluster-Controller bietet keine Modbus-Register mit Phasenströmen an. - # Daher die Phasenströme berechnen (es wird davon ausgegangen, dass eine symmetrische Erzeugung erfolgt) + # Daher die Phasenströme berechnen (es wird davon ausgegangen, dass eine symmetrische + # Erzeugung erfolgt). currents = [(power_total / 3 / 230)] * 3 + else: - raise ValueError("Unbekannte Version "+str(self.component_config.configuration.version)) + raise ValueError("Unbekannte Version " + str(self.component_config.configuration.version)) # WR geht nachts in Standby und gibt einen NaN-Wert für die Leistung aus. - if power_total in (self.SMA_INT16_NAN, self.SMA_INT32_NAN, - -self.SMA_INT16_NAN * 10, -self.SMA_INT32_NAN): - power_total = 0 + # power_total ist durch check_nan() bereits 0 -- dc_power/currents ziehen hier nach. + if power_is_nan: dc_power = 0 currents = [0, 0, 0] - if energy in (self.SMA_UINT32_NAN, self.SMA_UINT32_NAN * 100, self.SMA_UINT64_NAN): + + if energy_is_nan: raise ValueError( f'Wechselrichter lieferte nicht plausiblen Zählerstand: {energy}. ' 'Sobald PV Ertrag vorhanden ist sollte sich dieser Wert ändern, ' 'andernfalls kann ein Defekt vorliegen.' ) + _, exported = self.peak_filter.check_values(power_total, None, energy) imported, _ = self.sim_counter.sim_count(power_total) From 4ab599951fe84599a8b60d37a2841ba6625924a7 Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Wed, 12 Aug 2026 13:56:46 +0200 Subject: [PATCH 3/4] Fix NaN handling for energy calculations --- packages/modules/devices/sma/sma_sunny_boy/inverter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index 2cbe44fba9..4d4ad10217 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -65,9 +65,11 @@ def update(self) -> None: self.SMA_INT32_NAN) power_total *= -1 + # default_value=nan_value: bei NaN bleibt der reale Sentinel-Wert erhalten, + # damit er in der ValueError-Meldung unten sichtbar ist (statt ihn mit 0 zu überschreiben). energy, energy_is_nan = self.check_nan( self.tcp_client.read_holding_registers(30529, ModbusDataType.UINT_32, unit=unit), - self.SMA_UINT32_NAN) + self.SMA_UINT32_NAN, self.SMA_UINT32_NAN) dc1, _ = self.check_nan( self.tcp_client.read_holding_registers(30773, ModbusDataType.INT_32, unit=unit), self.SMA_INT32_NAN) @@ -89,7 +91,7 @@ def update(self) -> None: energy, energy_is_nan = self.check_nan( self.tcp_client.read_holding_registers(40094, ModbusDataType.UINT_32, unit=unit), - self.SMA_UINT32_NAN) + self.SMA_UINT32_NAN, self.SMA_UINT32_NAN) energy *= 100 dc_power, _ = self.check_nan( @@ -111,7 +113,7 @@ def update(self) -> None: energy, energy_is_nan = self.check_nan( self.tcp_client.read_holding_registers(30513, ModbusDataType.UINT_64, unit=unit), - self.SMA_UINT64_NAN) + self.SMA_UINT64_NAN, self.SMA_UINT64_NAN) # Aus Kompatibilitätsgründen wird dc_power auf den Wert der AC-Wirkleistung gesetzt. dc_power = power_total From 5982b05b44039e3680f9fb557b038d68dfa1883c Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Thu, 13 Aug 2026 13:19:59 +0200 Subject: [PATCH 4/4] Enhance comments for SMA sentinel values in inverter.py Added comments explaining the SMA sentinel values and their handling in Modbus. --- packages/modules/devices/sma/sma_sunny_boy/inverter.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index 4d4ad10217..33a2faed6b 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -25,6 +25,15 @@ class KwargsDict(TypedDict): class SmaSunnyBoyInverter(AbstractInverter): # SMA-Sentinel-Werte für "kein gültiger Wert" -- abhängig vom Modbus-Registertyp. + # SMA_INT16_NAN und SMA_INT32_NAN sind bewusst NEGATIV definiert: Der Modbus-Client + # (modules.common.modbus) dekodiert Register vom Typ INT_16/INT_32 direkt beim Lesen + # anhand ihres Datentyps als signed Integer (pymodbus decode_16bit_int/decode_32bit_int). + # Der SMA-Sentinel 0x8000 bzw. 0x8000 0000 (per SMA-Doku für vorzeichenbehaftete + # Formate definiert) hat das höchstwertige Bit gesetzt und wird deshalb schon beim + # Auslesen automatisch zu -32768 bzw. -2147483648 -- nicht zum positiven Bitmuster-Wert. + # SMA_UINT32_NAN/SMA_UINT64_NAN bleiben dagegen positiv, weil UINT_32/UINT_64 unsigned + # dekodiert werden und dort kein Vorzeichenbit existiert. + # Ausführliche Herleitung/Diskussion siehe PR #3675. SMA_INT16_NAN = -0x8000 SMA_INT32_NAN = -0x80000000 SMA_UINT32_NAN = 0xFFFFFFFF