diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index c5a6c1bbaa..32ccd1b00f 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 122 + DATASTORE_VERSION = 123 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -3107,3 +3107,73 @@ def upgrade_datastore_122(self) -> None: except Exception: log.exception(f"Logdatei '{path}' konnte nicht konvertiert werden.") self._append_datastore_version(122) + + def upgrade_datastore_123(self) -> None: + """ + Consolidate 3 separate SMA battery modules (bat, bat_tesvolt, bat_smart_energy) + into single unified bat module with version field. + Maps old component types to new versioned structure: + - bat (with hybrid=True in inverter) -> bat with version:0 (hybrid) + - bat (with hybrid=False in inverter) -> bat with version:1 (sbs - standalone battery) + - bat_smart_energy -> bat with version:0 (hybrid) + - bat_tesvolt -> bat with version:2 (tesvolt) + """ + def upgrade_component(topic: str, payload) -> Optional[dict]: + if re.search(r"^openWB/system/device/[0-9]+/component/[0-9]+/config$", topic) is not None: + component = decode_payload(payload) + device_id = get_index(topic) + device_topic = f"openWB/system/device/{device_id}/config" + if device_topic not in self.all_received_topics: + return None + + device_config = decode_payload(self.all_received_topics[device_topic]) + if not str(device_config.get("type", "")).startswith("sma_sunny_boy"): + return None + + if component.get("type") == "bat_tesvolt": + component["type"] = "bat" + component["configuration"] = { + "version": 2, + "modbus_id": 25 + } + return {topic: component} + + elif component.get("type") == "bat_smart_energy": + component["type"] = "bat" + old_config = component.get("configuration", {}) + component["configuration"] = { + "version": 0, + "modbus_id": old_config.get("modbus_id", 3) + } + return {topic: component} + + elif component.get("type") == "bat": + is_hybrid = False + + for t, p in self.all_received_topics.items(): + if re.search(f"^openWB/system/device/{device_id}/component/[0-9]+/config$", t): + comp_check = decode_payload(p) + if comp_check.get("type") == "inverter": + is_hybrid = comp_check.get("configuration", {}).get("hybrid", False) + break + + old_config = component.get("configuration", {}) + + component["configuration"] = { + "version": 0 if is_hybrid else 1, + "modbus_id": old_config.get("modbus_id", 3) + } + return {topic: component} + + return None + + self._loop_all_received_topics(upgrade_component) + + pub_system_message( + {}, + "Die SMA Speicher-Module wurden erfolgreich zusammengeführt. " + "Deine bestehenden Einstellungen wurden automatisch übernommen.", + MessageType.INFO + ) + + self._append_datastore_version(123) diff --git a/packages/modules/devices/sma/sma_sunny_boy/bat.py b/packages/modules/devices/sma/sma_sunny_boy/bat.py index b405760df1..e6ea740598 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/bat.py +++ b/packages/modules/devices/sma/sma_sunny_boy/bat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import logging -from typing import TypedDict, Any +from typing import Any, TypedDict, Optional from modules.common.abstract_device import AbstractBat from modules.common.component_state import BatState @@ -9,8 +9,10 @@ from modules.common.modbus import ModbusTcpClient_, ModbusDataType from modules.common.store import get_bat_value_store from modules.devices.sma.sma_sunny_boy.config import SmaSunnyBoyBatSetup +from modules.common.simcount import SimCounter from modules.common.utils.peak_filter import PeakFilter from modules.common.component_type import ComponentType +from modules.devices.sma.sma_sunny_boy.bat_version import SmaBatVersion log = logging.getLogger(__name__) @@ -21,6 +23,7 @@ class KwargsDict(TypedDict): class SunnyBoyBat(AbstractBat): SMA_UINT_64_NAN = 0xFFFFFFFFFFFFFFFF # SMA uses this value to represent NaN + SMA_UINT32_NAN = 0xFFFFFFFF # SMA uses this value to represent NaN def __init__(self, component_config: SmaSunnyBoyBatSetup, **kwargs: Any) -> None: self.component_config = component_config @@ -28,40 +31,79 @@ def __init__(self, component_config: SmaSunnyBoyBatSetup, **kwargs: Any) -> None def initialize(self) -> None: self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="speicher") 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.last_mode = 'Undefined' - def read(self) -> BatState: + def update(self) -> None: unit = self.component_config.configuration.modbus_id - soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.UINT_32, unit=unit) - imp = self.__tcp_client.read_holding_registers(31393, ModbusDataType.INT_32, unit=unit) - exp = self.__tcp_client.read_holding_registers(31395, ModbusDataType.INT_32, unit=unit) - if imp > 5: - power = imp - else: - power = exp * -1 + if self.component_config.configuration.version in (SmaBatVersion.hybrid, SmaBatVersion.sbs): + soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.UINT_32, unit=unit) + charge_power = self.__tcp_client.read_holding_registers(31393, ModbusDataType.INT_32, unit=unit) + discharge_power = self.__tcp_client.read_holding_registers(31395, ModbusDataType.INT_32, unit=unit) + + if soc == self.SMA_UINT32_NAN: + # Es werden keine Werte geliefert, wenn die Battery leer ist oder nichts auf der DC Seite erzeugt wird. + soc = 0 + power = 0 + else: + if charge_power > 5: + power = charge_power + else: + power = discharge_power * -1 + + exported = self.__tcp_client.read_holding_registers(31401, ModbusDataType.UINT_64, unit=unit) + imported = self.__tcp_client.read_holding_registers(31397, ModbusDataType.UINT_64, unit=unit) - exported = self.__tcp_client.read_holding_registers(31401, ModbusDataType.UINT_64, unit=unit) - imported = self.__tcp_client.read_holding_registers(31397, ModbusDataType.UINT_64, unit=unit) + if exported == self.SMA_UINT_64_NAN or imported == self.SMA_UINT_64_NAN: + raise ValueError(f'Batterie lieferte nicht plausible Werte. Export: {exported}, Import: {imported}. ', + 'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ', + 'andernfalls kann ein Defekt vorliegen.') + imported, exported = self.peak_filter.check_values(power, imported, exported) + + elif self.component_config.configuration.version == SmaBatVersion.tesvolt: + + soc = self.__tcp_client.read_input_registers(1056, ModbusDataType.INT_32, unit=25) / 10 + power = self.__tcp_client.read_input_registers(1012, ModbusDataType.INT_32, unit=25) * -1 + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + else: + raise ValueError('Unbekannte Batterie Version') - if exported == self.SMA_UINT_64_NAN or imported == self.SMA_UINT_64_NAN: - raise ValueError(f'Batterie lieferte nicht plausible Werte. Export: {exported}, Import: {imported}. ', - 'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ', - 'andernfalls kann ein Defekt vorliegen.') - imported, exported = self.peak_filter.check_values(power, imported, exported) bat_state = BatState( power=power, soc=soc, imported=imported, exported=exported ) - log.debug("Bat {}: {}".format(self.__tcp_client.address, bat_state)) - return bat_state + self.store.set(bat_state) - def update(self) -> None: - self.store.set(self.read()) + def set_power_limit(self, power_limit: Optional[int]) -> None: + unit = self.component_config.configuration.modbus_id + + if power_limit is None: + if self.last_mode is not None: + # Kein Powerlimit gefordert, externe Steuerung war aktiv, externe Steuerung deaktivieren + self.__tcp_client.write_register(40151, 803, data_type=ModbusDataType.UINT_32, unit=unit) + self.__tcp_client.write_register(40149, 0, data_type=ModbusDataType.INT_32, unit=unit) + log.debug("Keine Batteriesteuerung gefordert, deaktiviere externe Steuerung.") + self.last_mode = None + else: + # Powerlimit gefordert, externe Steuerung aktivieren, Limit setzen + self.__tcp_client.write_register(40151, 802, data_type=ModbusDataType.UINT_32, unit=unit) + power_value = int(power_limit) * -1 + self.__tcp_client.write_register(40149, power_value, data_type=ModbusDataType.INT_32, unit=unit) + log.debug(f"Aktive Batteriesteuerung vorhanden. Setze externe Steuerung. Leistung: {power_value}") + self.last_mode = 'limited' + + def power_limit_controllable(self) -> bool: + return self.component_config.configuration.version in ( + SmaBatVersion.hybrid, + SmaBatVersion.sbs + ) component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyBoyBatSetup) diff --git a/packages/modules/devices/sma/sma_sunny_boy/bat_smart_energy.py b/packages/modules/devices/sma/sma_sunny_boy/bat_smart_energy.py deleted file mode 100644 index 8e320ea5be..0000000000 --- a/packages/modules/devices/sma/sma_sunny_boy/bat_smart_energy.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -from typing import TypedDict, Any, Dict, Union, Optional -import logging - -from modules.devices.sma.sma_sunny_boy.config import SmaSunnyBoySmartEnergyBatSetup -from modules.common.store import get_bat_value_store -from modules.common.modbus import ModbusTcpClient_, ModbusDataType -from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.component_type import ComponentDescriptor -from modules.common.component_state import BatState -from modules.common.abstract_device import AbstractBat -from modules.common.utils.peak_filter import PeakFilter -from modules.common.component_type import ComponentType - - -log = logging.getLogger(__name__) - - -class KwargsDict(TypedDict): - client: ModbusTcpClient_ - - -class SunnyBoySmartEnergyBat(AbstractBat): - SMA_UINT32_NAN = 0xFFFFFFFF # SMA uses this value to represent NaN - SMA_UINT_64_NAN = 0xFFFFFFFFFFFFFFFF # SMA uses this value to represent NaN - - # Define all possible registers with their data types - REGISTERS = { - "Battery_SoC": (30845, ModbusDataType.UINT_32), - "Battery_ChargePower": (31393, ModbusDataType.INT_32), - "Battery_DischargePower": (31395, ModbusDataType.INT_32), - "Battery_ChargedEnergy": (31397, ModbusDataType.UINT_64), - "Battery_DischargedEnergy": (31401, ModbusDataType.UINT_64), - "Inverter_Type": (30053, ModbusDataType.UINT_32), - "Externe_Steuerung": (40151, ModbusDataType.UINT_32), - "Wirkleistungsvorgabe": (40149, ModbusDataType.INT_32), - } - - def __init__(self, component_config: SmaSunnyBoySmartEnergyBatSetup, **kwargs: Any) -> None: - self.component_config = component_config - self.kwargs: KwargsDict = kwargs - - def initialize(self) -> None: - self.__tcp_client: ModbusTcpClient_ = 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.last_mode = 'Undefined' - self.inverter_type = None - self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state) - - def update(self) -> None: - self.store.set(self.read()) - - def read(self) -> BatState: - unit = self.component_config.configuration.modbus_id - - registers_to_read = [ - "Battery_SoC", - "Battery_ChargePower", - "Battery_DischargePower", - "Battery_ChargedEnergy", - "Battery_DischargedEnergy" - ] - - if self.inverter_type is None: # Only read Inverter_Type if not already set - registers_to_read.append("Inverter_Type") - - values = self._read_registers(registers_to_read, unit) - - if values["Battery_SoC"] == self.SMA_UINT32_NAN: - # If the storage is empty and nothing is produced on the DC side, the inverter does not supply any values. - values["Battery_SoC"] = 0 - power = 0 - else: - if values["Battery_ChargePower"] > 5: - power = values["Battery_ChargePower"] - else: - power = values["Battery_DischargePower"] * -1 - - if (values["Battery_ChargedEnergy"] == self.SMA_UINT_64_NAN or - values["Battery_DischargedEnergy"] == self.SMA_UINT_64_NAN): - raise ValueError( - f'Batterie lieferte nicht plausible Werte. Geladene Energie: {values["Battery_ChargedEnergy"]}, ' - f'Entladene Energie: {values["Battery_DischargedEnergy"]}. ', - 'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ', - 'andernfalls kann ein Defekt vorliegen.' - ) - - imported, exported = self.peak_filter.check_values(power, - values["Battery_ChargedEnergy"], - values["Battery_DischargedEnergy"]) - bat_state = BatState( - power=power, - soc=values["Battery_SoC"], - exported=exported, - imported=imported - ) - if self.inverter_type is None: - self.inverter_type = values["Inverter_Type"] - log.debug(f"Inverter Type: {self.inverter_type}") - log.debug(f"Bat {self.__tcp_client.address}: {bat_state}") - return bat_state - - def set_power_limit(self, power_limit: Optional[int]) -> None: - unit = self.component_config.configuration.modbus_id - - if power_limit is None: - if self.last_mode is not None: - # Kein Powerlimit gefordert, externe Steuerung war aktiv, externe Steuerung deaktivieren - log.debug("Keine Batteriesteuerung gefordert, deaktiviere externe Steuerung.") - values_to_write = { - "Externe_Steuerung": 803, - "Wirkleistungsvorgabe": 0, - } - self._write_registers(values_to_write, unit) - self.last_mode = None - else: - # Powerlimit gefordert, externe Steuerung aktivieren, Limit setzen - log.debug("Aktive Batteriesteuerung vorhanden. Setze externe Steuerung.") - values_to_write = { - "Externe_Steuerung": 802, - "Wirkleistungsvorgabe": int(power_limit) * -1 - } - self._write_registers(values_to_write, unit) - self.last_mode = 'limited' - - def _read_registers(self, register_names: list, unit: int) -> Dict[str, Union[int, float]]: - values = {} - for key in register_names: - address, data_type = self.REGISTERS[key] - values[key] = self.__tcp_client.read_holding_registers(address, data_type, unit=unit) - log.debug(f"Bat raw values {self.__tcp_client.address}: {values}") - return values - - def _write_registers(self, values_to_write: Dict[str, Union[int, float]], unit: int) -> None: - for key, value in values_to_write.items(): - address, data_type = self.REGISTERS[key] - self.__tcp_client.write_register(address, value, data_type, unit=unit) - log.debug(f"Neuer Wert {value} in Register {address} geschrieben.") - - def power_limit_controllable(self) -> bool: - return True - - -component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyBoySmartEnergyBatSetup) diff --git a/packages/modules/devices/sma/sma_sunny_boy/bat_tesvolt.py b/packages/modules/devices/sma/sma_sunny_boy/bat_tesvolt.py deleted file mode 100644 index 166898839e..0000000000 --- a/packages/modules/devices/sma/sma_sunny_boy/bat_tesvolt.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -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.modbus import ModbusTcpClient_, ModbusDataType -from modules.common.simcount._simcounter import SimCounter -from modules.common.store import get_bat_value_store -from modules.devices.sma.sma_sunny_boy.config import SmaTesvoltBatSetup -from modules.common.utils.peak_filter import PeakFilter -from modules.common.component_type import ComponentType - -log = logging.getLogger(__name__) - - -class KwargsDict(TypedDict): - device_id: int - client: ModbusTcpClient_ - - -class TesvoltBat(AbstractBat): - def __init__(self, component_config: SmaTesvoltBatSetup, **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.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] - self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") - 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) - - def update(self) -> None: - soc = self.__tcp_client.read_input_registers(1056, ModbusDataType.INT_32, unit=25) / 10 - power = self.__tcp_client.read_input_registers(1012, ModbusDataType.INT_32, unit=25) * -1 - 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 - ) - log.debug("Bat {}: {}".format(self.__tcp_client.address, bat_state)) - self.store.set(bat_state) - - -component_descriptor = ComponentDescriptor(configuration_factory=SmaTesvoltBatSetup) diff --git a/packages/modules/devices/sma/sma_sunny_boy/bat_version.py b/packages/modules/devices/sma/sma_sunny_boy/bat_version.py new file mode 100644 index 0000000000..8b3be70627 --- /dev/null +++ b/packages/modules/devices/sma/sma_sunny_boy/bat_version.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +from enum import IntEnum + + +class SmaBatVersion(IntEnum): + hybrid = 0 + sbs = 1 + tesvolt = 2 diff --git a/packages/modules/devices/sma/sma_sunny_boy/config.py b/packages/modules/devices/sma/sma_sunny_boy/config.py index 8889b643a7..61efa13217 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/config.py +++ b/packages/modules/devices/sma/sma_sunny_boy/config.py @@ -2,6 +2,7 @@ from modules.common.component_setup import ComponentSetup from modules.devices.sma.sma_sunny_boy.inv_version import SmaInverterVersion +from modules.devices.sma.sma_sunny_boy.bat_version import SmaBatVersion from ..vendor import vendor_descriptor @@ -13,7 +14,7 @@ def __init__(self, ip_address: Optional[str] = None, port: int = 502): class SmaSunnyBoy: def __init__(self, - name: str = "SMA Sunny Boy/Tripower, Tesvolt", + name: str = "SMA Sunny Boy / Tripower / Tesvolt", type: str = "sma_sunny_boy", id: int = 0, configuration: SmaSunnyBoyConfiguration = None) -> None: @@ -25,47 +26,22 @@ def __init__(self, class SmaSunnyBoyBatConfiguration: - def __init__(self, modbus_id: int = 3): + def __init__(self, + version: SmaBatVersion = SmaBatVersion.hybrid, + modbus_id: int = 3): + self.version = version self.modbus_id = modbus_id class SmaSunnyBoyBatSetup(ComponentSetup[SmaSunnyBoyBatConfiguration]): def __init__(self, - name: str = "Sma Sunny Boy/Tripower Speicher", + name: str = "SMA Sunny Boy / Tripower Hybrid / Tesvolt Speicher", type: str = "bat", id: int = 0, configuration: SmaSunnyBoyBatConfiguration = None) -> None: super().__init__(name, type, id, configuration or SmaSunnyBoyBatConfiguration()) -class SmaSunnyBoySmartEnergyBatConfiguration: - def __init__(self, modbus_id: int = 3): - self.modbus_id = modbus_id - - -class SmaSunnyBoySmartEnergyBatSetup(ComponentSetup[SmaSunnyBoySmartEnergyBatConfiguration]): - def __init__(self, - name: str = "Sma Sunny Boy Smart Energy Speicher", - type: str = "bat_smart_energy", - id: int = 0, - configuration: SmaSunnyBoySmartEnergyBatConfiguration = None) -> None: - super().__init__(name, type, id, configuration or SmaSunnyBoySmartEnergyBatConfiguration()) - - -class SmaTesvoltBatConfiguration: - def __init__(self): - pass - - -class SmaTesvoltBatSetup(ComponentSetup[SmaTesvoltBatConfiguration]): - def __init__(self, - name: str = "Sma Tripower/Tesvolt Speicher", - type: str = "bat_tesvolt", - id: int = 0, - configuration: SmaTesvoltBatConfiguration = None) -> None: - super().__init__(name, type, id, configuration or SmaTesvoltBatConfiguration()) - - class SmaSunnyBoyCounterConfiguration: def __init__(self, modbus_id: int = 3): self.modbus_id = modbus_id @@ -73,7 +49,7 @@ def __init__(self, modbus_id: int = 3): class SmaSunnyBoyCounterSetup(ComponentSetup[SmaSunnyBoyCounterConfiguration]): def __init__(self, - name: str = "Sma Sunny Boy/Tripower Zähler", + name: str = "SMA Sunny Boy / Tripower Zähler", type: str = "counter", id: int = 0, configuration: SmaSunnyBoyCounterConfiguration = None) -> None: @@ -81,17 +57,16 @@ def __init__(self, class SmaSunnyBoyInverterConfiguration: - def __init__(self, hybrid: bool = False, + def __init__(self, version: SmaInverterVersion = SmaInverterVersion.default, modbus_id: int = 3): - self.hybrid = hybrid self.version = version self.modbus_id = modbus_id class SmaSunnyBoyInverterSetup(ComponentSetup[SmaSunnyBoyInverterConfiguration]): def __init__(self, - name: str = "Sma Sunny Boy/Tripower Wechselrichter", + name: str = "SMA Sunny Boy / Tripower Wechselrichter", type: str = "inverter", id: int = 0, configuration: SmaSunnyBoyInverterConfiguration = None) -> None: diff --git a/packages/modules/devices/sma/sma_sunny_boy/device.py b/packages/modules/devices/sma/sma_sunny_boy/device.py index f793b3f229..0e47eef457 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/device.py +++ b/packages/modules/devices/sma/sma_sunny_boy/device.py @@ -7,11 +7,8 @@ from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.common.modbus import ModbusTcpClient_ from modules.devices.sma.sma_sunny_boy.bat import SunnyBoyBat -from modules.devices.sma.sma_sunny_boy.bat_smart_energy import SunnyBoySmartEnergyBat -from modules.devices.sma.sma_sunny_boy.bat_tesvolt import TesvoltBat from modules.devices.sma.sma_sunny_boy.config import (SmaSunnyBoy, SmaSunnyBoyBatSetup, SmaSunnyBoyCounterSetup, - SmaSunnyBoyInverterSetup, SmaSunnyBoySmartEnergyBatSetup, - SmaTesvoltBatSetup) + SmaSunnyBoyInverterSetup) from modules.devices.sma.sma_sunny_boy.counter import SmaSunnyBoyCounter from modules.devices.sma.sma_sunny_boy.inverter import SmaSunnyBoyInverter @@ -20,7 +17,6 @@ sma_modbus_tcp_component_classes = Union[ SunnyBoyBat, - SunnyBoySmartEnergyBat, SmaSunnyBoyCounter, SmaSunnyBoyInverter ] @@ -33,14 +29,6 @@ def create_bat_component(component_config: SmaSunnyBoyBatSetup): nonlocal client return SunnyBoyBat(component_config, device_id=device_config.id, client=client) - def create_bat_smart_energy_component(component_config: SmaSunnyBoySmartEnergyBatSetup): - nonlocal client - return SunnyBoySmartEnergyBat(component_config, client=client) - - def create_bat_tesvolt_component(component_config: SmaTesvoltBatSetup): - nonlocal client - return TesvoltBat(component_config, device_id=device_config.id, client=client) - def create_counter_component(component_config: SmaSunnyBoyCounterSetup): nonlocal client return SmaSunnyBoyCounter(component_config, device_id=device_config.id, client=client) @@ -65,8 +53,6 @@ def initializer(): initializer=initializer, component_factory=ComponentFactoryByType( bat=create_bat_component, - bat_smart_energy=create_bat_smart_energy_component, - bat_tesvolt=create_bat_tesvolt_component, counter=create_counter_component, inverter=create_inverter_component, ), diff --git a/packages/modules/devices/sma/sma_sunny_boy/inverter.py b/packages/modules/devices/sma/sma_sunny_boy/inverter.py index 39db80d84a..d87e5569d4 100644 --- a/packages/modules/devices/sma/sma_sunny_boy/inverter.py +++ b/packages/modules/devices/sma/sma_sunny_boy/inverter.py @@ -43,23 +43,13 @@ def initialize(self) -> None: self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state) def update(self) -> None: - self.store.set(self.read()) - - def read(self) -> InverterState: unit = self.component_config.configuration.modbus_id if self.component_config.configuration.version == SmaInverterVersion.default: - # AC Wirkleistung über alle Phasen (W) [Pac] - power_total = self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) - # Gesamtertrag (Wh) [E-Total] + 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) - # Bei Hybrid Wechselrichtern treten Abweichungen auf, die in der Nacht - # immer wieder Generatorleistung anzeigen (0-50 Watt). Um dies zu verhindern, schauen wir uns - # zunächst an, ob vom DC Teil überhaupt Leistung kommt. Ist dies nicht der Fall, können wir power - # gleich auf 0 setzen. - # Leistung DC an Eingang 1 und 2 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)) + self.tcp_client.read_holding_registers(30961, ModbusDataType.INT_32, unit=unit)) * -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): @@ -67,29 +57,22 @@ def read(self) -> InverterState: else: currents = [current / -1000 if current != self.SMA_INT32_NAN else 0 for current in currents] elif self.component_config.configuration.version == SmaInverterVersion.core2: - # AC Wirkleistung über alle Phasen (W) [Pac] - power_total = self.tcp_client.read_holding_registers(40084, ModbusDataType.INT_16, unit=unit) * 10 - # Gesamtertrag (Wh) [E-Total] SF=2! + 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 - # Power - dc_power = self.tcp_client.read_holding_registers(40101, ModbusDataType.UINT_32, unit=unit) * 100 - # Phasenstöme - current_L1 = self.tcp_client.read_holding_registers(30977, ModbusDataType.INT_32, unit=unit) * -1 - current_L2 = self.tcp_client.read_holding_registers(30979, ModbusDataType.INT_32, unit=unit) * -1 - current_L3 = self.tcp_client.read_holding_registers(30981, ModbusDataType.INT_32, unit=unit) * -1 - currents = [current_L1 / 1000, current_L2 / 1000, current_L3 / 1000] + 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) + 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] elif self.component_config.configuration.version == SmaInverterVersion.datamanager: - # AC Wirkleistung über alle Phasen (W) [Pac] - power_total = self.tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) - # Total eingespeiste Energie auf allen Außenleitern (Wh) [E-Total] + 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) - # DC-Power = power_total - Cluster-Controller gibt in Register 30775 immer korrekte Werte aus, - # daher ist wie bei SmaInverterVersion.default keine Prüfung auf DC-Leistung notwendig. # 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) - currents = [(power_total / 3 / 230) * -1] * 3 + 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: @@ -102,19 +85,17 @@ def read(self) -> InverterState: 'Sobald PV Ertrag vorhanden ist sollte sich dieser Wert ändern, ' 'andernfalls kann ein Defekt vorliegen.' ) - _, exported = self.peak_filter.check_values(power_total * -1, None, energy) - imported, _ = self.sim_counter.sim_count(power_total * -1) + _, exported = self.peak_filter.check_values(power_total, None, energy) + imported, _ = self.sim_counter.sim_count(power_total) inverter_state = InverterState( - power=power_total * -1, - dc_power=dc_power * -1, + power=power_total, + dc_power=dc_power, + currents=currents, exported=exported, imported=imported ) - if 'currents' in locals(): - inverter_state.currents = currents - log.debug("WR {}: {}".format(self.tcp_client.address, inverter_state)) - return inverter_state + self.store.set(inverter_state) component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyBoyInverterSetup) diff --git a/packages/modules/devices/sma/sma_sunny_island/bat.py b/packages/modules/devices/sma/sma_sunny_island/bat.py index 7d866b303d..8e3121a7e8 100644 --- a/packages/modules/devices/sma/sma_sunny_island/bat.py +++ b/packages/modules/devices/sma/sma_sunny_island/bat.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -from typing import TypedDict, Any +from typing import TypedDict, Any, Optional +import logging from modules.common import modbus from modules.common.abstract_device import AbstractBat @@ -12,6 +13,8 @@ from modules.common.utils.peak_filter import PeakFilter from modules.common.component_type import ComponentType +log = logging.getLogger(__name__) + class KwargsDict(TypedDict): client: modbus.ModbusTcpClient_ @@ -27,26 +30,44 @@ def initialize(self) -> None: 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.last_mode = 'Undefined' - def read(self) -> BatState: + def update(self) -> None: unit = self.component_config.configuration.modbus_id - with self.__tcp_client: - soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.INT_32, unit=unit) - - power = self.__tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) * -1 - imported, exported = self.__tcp_client.read_holding_registers(30595, [ModbusDataType.INT_32]*2, unit=unit) + soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.INT_32, unit=unit) + power = self.__tcp_client.read_holding_registers(30775, ModbusDataType.INT_32, unit=unit) * -1 + imported, exported = self.__tcp_client.read_holding_registers(30595, [ModbusDataType.INT_32]*2, unit=unit) imported, exported = self.peak_filter.check_values(power, imported, exported) - return BatState( + bat_state = BatState( power=power, soc=soc, imported=imported, exported=exported ) + self.store.set(bat_state) - def update(self) -> None: - self.store.set(self.read()) + def set_power_limit(self, power_limit: Optional[int]) -> None: + unit = self.component_config.configuration.modbus_id + + if power_limit is None: + if self.last_mode is not None: + # Kein Powerlimit gefordert, externe Steuerung war aktiv, externe Steuerung deaktivieren + self.__tcp_client.write_register(40151, 803, data_type=ModbusDataType.UINT_32, unit=unit) + self.__tcp_client.write_register(40149, 0, data_type=ModbusDataType.INT_32, unit=unit) + log.debug("Keine Batteriesteuerung gefordert, deaktiviere externe Steuerung.") + self.last_mode = None + else: + # Powerlimit gefordert, externe Steuerung aktivieren, Limit setzen + self.__tcp_client.write_register(40151, 802, data_type=ModbusDataType.UINT_32, unit=unit) + power_value = int(power_limit) * -1 + self.__tcp_client.write_register(40149, power_value, data_type=ModbusDataType.INT_32, unit=unit) + log.debug("Aktive Batteriesteuerung vorhanden. Setze externe Steuerung. Leistung: {power_value}") + self.last_mode = 'limited' + + def power_limit_controllable(self) -> bool: + return True component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyIslandBatSetup) diff --git a/packages/modules/devices/sma/sma_sunny_island/config.py b/packages/modules/devices/sma/sma_sunny_island/config.py index 811fa51a04..db9ad5bbc0 100644 --- a/packages/modules/devices/sma/sma_sunny_island/config.py +++ b/packages/modules/devices/sma/sma_sunny_island/config.py @@ -12,7 +12,7 @@ def __init__(self, ip_address: Optional[str] = None, port: int = 502): class SmaSunnyIsland: def __init__(self, - name: str = "SMA Sunny Island, Sunny Tripower X", + name: str = "SMA Sunny Island / Tripower X", type: str = "sma_sunny_island", id: int = 0, configuration: SmaSunnyIslandConfiguration = None) -> None: @@ -30,7 +30,7 @@ def __init__(self, modbus_id: int = 3): class SmaSunnyIslandBatSetup(ComponentSetup[SmaSunnyIslandBatConfiguration]): def __init__(self, - name: str = "SMA Sunny Island Speicher, Sunny Tripower X", + name: str = "SMA Sunny Island / Tripower X Speicher", type: str = "bat", id: int = 0, configuration: SmaSunnyIslandBatConfiguration = None) -> None: