diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index b086943d42..33a2faed6b 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -24,10 +24,20 @@ 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-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 + SMA_UINT64_NAN = 0xFFFFFFFFFFFFFFFF def __init__(self, component_config: SmaSunnyBoyInverterSetup, @@ -35,6 +45,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,45 +69,85 @@ 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 + + # 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) + + 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) + 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, 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, 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)) - if power_total == self.SMA_INT32_NAN or power_total == self.SMA_NAN: - power_total = 0 - # WR geht nachts in Standby und gibt einen NaN-Wert für die Leistung aus. + 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. + # 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 == self.SMA_UINT32_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)