diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 15530c627d..23e0484feb 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -44,7 +44,7 @@ from control.optional_data import Ocpp from modules.common.abstract_vehicle import GeneralVehicleConfig from modules.common.component_type import ComponentType -from modules.devices.sungrow.sungrow.version import Version +from modules.devices.sungrow.sungrow_sh.version import Version from modules.display_themes.cards.config import CardsDisplayTheme from modules.io_actions.controllable_consumers.ripple_control_receiver.config import RippleControlReceiverSetup from modules.web_themes.koala.config import KoalaWebTheme @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 107 + DATASTORE_VERSION = 108 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2701,3 +2701,80 @@ def upgrade(topic: str, payload) -> None: return {topic: provider} self._loop_all_received_topics(upgrade) self._append_datastore_version(107) + + def upgrade_datastore_108(self) -> None: + """ + Migrate old single 'sungrow' devices into new modules: + - 'sungrow_sg' for SG family (no version field) + - 'sungrow_sh' for SH family (keeps numeric version 0 or 1) + + Old version mapping: + * 0 -> sungrow_sh, version 0 + * 1 -> sungrow_sg (remove version) + * 2 -> sungrow_sg (remove version) + * 3 -> sungrow_sh, version 1 + Default for missing/unknown old version: sungrow_sg (no version). + """ + def upgrade(topic: str, payload) -> None: + if re.search(r"^openWB/system/device/[0-9]+$", topic) is not None: + device = decode_payload(payload) + if device.get("type") == "sungrow": + old_version = device.get("configuration", {}).get("version") + new_type = "sungrow_sg" + new_version = None + if old_version == 0: + new_type = "sungrow_sh" + new_version = 0 + elif old_version in (1, 2): + new_type = "sungrow_sg" + new_version = None + elif old_version == 3: + new_type = "sungrow_sh" + new_version = 1 + else: + new_type = "sungrow_sg" + new_version = None + changed = False + if device.get("type") != new_type: + device["type"] = new_type + changed = True + if "configuration" not in device or device["configuration"] is None: + device["configuration"] = {} + if new_version is None: + if "version" in device["configuration"]: + device["configuration"].pop("version", None) + changed = True + else: + if device["configuration"].get("version") != new_version: + device["configuration"]["version"] = new_version + changed = True + if changed: + device_name = device.get("name") + device_id = device.get("id") + log.info( + f"Upgrading sungrow device {device_name!r} (id={device_id}) -> " + f"type='{new_type}'" + + (f", version={new_version}" if new_version is not None else ", no version") + ) + Pub().pub(topic, device) + if new_version is not None: + try: + version_name = Version(new_version).name + except Exception: + version_name = str(new_version) + pub_system_message( + device, + (f"Die Konfiguration von '{device_name}' wurde aktualisiert. " + f"Bitte in den Geräteeinstellungen sicherstellen, dass Version " + f"'{version_name}' korrekt ist"), + MessageType.INFO, + ) + else: + pub_system_message( + device, + (f"Die Sungrow-Geräte-Konfiguration wurde aktualisiert: Gerät " + f"'{device_name}' auf Typ '{new_type}'."), + MessageType.INFO, + ) + self._loop_all_received_topics(upgrade) + self._append_datastore_version(108) diff --git a/packages/modules/devices/sungrow/sungrow/config.py b/packages/modules/devices/sungrow/sungrow/config.py deleted file mode 100644 index bc112dd867..0000000000 --- a/packages/modules/devices/sungrow/sungrow/config.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Optional - -from modules.common.component_setup import ComponentSetup -from modules.devices.sungrow.sungrow.version import Version -from ..vendor import vendor_descriptor - - -class SungrowConfiguration: - def __init__(self, - ip_address: Optional[str] = None, - port: int = 502, - modbus_id: int = 1, - version: Version = Version.SG): - self.ip_address = ip_address - self.port = port - self.modbus_id = modbus_id - self.version = version - - -class Sungrow: - def __init__(self, - name: str = "Sungrow", - type: str = "sungrow", - id: int = 0, - configuration: SungrowConfiguration = None) -> None: - self.name = name - self.type = type - self.vendor = vendor_descriptor.configuration_factory().type - self.id = id - self.configuration = configuration or SungrowConfiguration() - - -class SungrowBatConfiguration: - def __init__(self): - pass - - -class SungrowBatSetup(ComponentSetup[SungrowBatConfiguration]): - def __init__(self, - name: str = "Sungrow Speicher", - type: str = "bat", - id: int = 0, - configuration: SungrowBatConfiguration = None) -> None: - super().__init__(name, type, id, configuration or SungrowBatConfiguration()) - - -class SungrowCounterConfiguration: - def __init__(self): - pass - - -class SungrowCounterSetup(ComponentSetup[SungrowCounterConfiguration]): - def __init__(self, - name: str = "Sungrow Zähler", - type: str = "counter", - id: int = 0, - configuration: SungrowCounterConfiguration = None) -> None: - super().__init__(name, type, id, configuration or SungrowCounterConfiguration()) - - -class SungrowInverterConfiguration: - def __init__(self): - pass - - -class SungrowInverterSetup(ComponentSetup[SungrowInverterConfiguration]): - def __init__(self, - name: str = "Sungrow Wechselrichter", - type: str = "inverter", - id: int = 0, - configuration: SungrowInverterConfiguration = None) -> None: - super().__init__(name, type, id, configuration or SungrowInverterConfiguration()) diff --git a/packages/modules/devices/sungrow/sungrow/device.py b/packages/modules/devices/sungrow/sungrow/device.py deleted file mode 100644 index 69e5ed1e3a..0000000000 --- a/packages/modules/devices/sungrow/sungrow/device.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -import logging -from typing import Iterable, Union - -from modules.common import modbus -from modules.common.abstract_device import DeviceDescriptor -from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater -from modules.devices.sungrow.sungrow.bat import SungrowBat -from modules.devices.sungrow.sungrow.config import Sungrow, SungrowBatSetup, SungrowCounterSetup, SungrowInverterSetup -from modules.devices.sungrow.sungrow.counter import SungrowCounter -from modules.devices.sungrow.sungrow.inverter import SungrowInverter - -log = logging.getLogger(__name__) - - -def create_device(device_config: Sungrow): - client = None - - def create_bat_component(component_config: SungrowBatSetup): - nonlocal client - return SungrowBat(component_config, device_config=device_config, client=client) - - def create_counter_component(component_config: SungrowCounterSetup): - nonlocal client - return SungrowCounter(component_config, device_config=device_config, client=client) - - def create_inverter_component(component_config: SungrowInverterSetup): - nonlocal client - return SungrowInverter(component_config, device_config=device_config, client=client) - - def update_components(components: Iterable[Union[SungrowBat, SungrowCounter, SungrowInverter]]): - pv_power = 0 - nonlocal client - with client: - for component in components: - if isinstance(component, SungrowInverter): - pv_power = component.update() - for component in components: - if isinstance(component, SungrowCounter): - component.update(pv_power) - for component in components: - if isinstance(component, SungrowBat): - component.update() - - def initializer(): - nonlocal client - client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) - - 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=Sungrow) diff --git a/packages/modules/devices/sungrow/sungrow/inverter.py b/packages/modules/devices/sungrow/sungrow/inverter.py deleted file mode 100644 index db2d5a3807..0000000000 --- a/packages/modules/devices/sungrow/sungrow/inverter.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -from typing import Any, TypedDict - -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, Endian, ModbusTcpClient_ -from modules.common.simcount import SimCounter -from modules.common.store import get_inverter_value_store -from modules.devices.sungrow.sungrow.config import SungrowInverterSetup, Sungrow -from modules.devices.sungrow.sungrow.version import Version - - -class KwargsDict(TypedDict): - client: ModbusTcpClient_ - device_config: Sungrow - - -class SungrowInverter(AbstractInverter): - def __init__(self, component_config: SungrowInverterSetup, **kwargs: Any) -> None: - self.component_config = component_config - self.kwargs: KwargsDict = kwargs - - def initialize(self) -> None: - self.device_config: Sungrow = self.kwargs['device_config'] - self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] - self.sim_counter = SimCounter(self.device_config.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) -> float: - unit = self.device_config.configuration.modbus_id - - if self.device_config.configuration.version in (Version.SH, Version.SH_winet_dongle): - power = self.__tcp_client.read_input_registers(13033, ModbusDataType.INT_32, - wordorder=Endian.Little, unit=unit) * -1 - dc_power = self.__tcp_client.read_input_registers(5016, ModbusDataType.UINT_32, - wordorder=Endian.Little, unit=unit) * -1 - - currents = self.__tcp_client.read_input_registers(13030, [ModbusDataType.INT_16]*3, unit=unit) - currents = [value * -0.1 for value in currents] - - elif self.device_config.configuration.version in (Version.SG, Version.SG_winet_dongle): - power = self.__tcp_client.read_input_registers(5030, ModbusDataType.INT_32, - wordorder=Endian.Little, unit=unit) * -1 - dc_power = self.__tcp_client.read_input_registers(5016, ModbusDataType.UINT_32, - wordorder=Endian.Little, unit=unit) * -1 - - currents = self.__tcp_client.read_input_registers(5021, [ModbusDataType.INT_16]*3, unit=unit) - currents = [value * -0.1 for value in currents] - - imported, exported = self.sim_counter.sim_count(power, dc_power) - - inverter_state = InverterState( - power=power, - dc_power=dc_power, - currents=currents, - imported=imported, - exported=exported - ) - self.store.set(inverter_state) - return power - - -component_descriptor = ComponentDescriptor(configuration_factory=SungrowInverterSetup) diff --git a/packages/modules/devices/sungrow/sungrow/modbus.md b/packages/modules/devices/sungrow/sungrow/modbus.md deleted file mode 100644 index 04a50389d2..0000000000 --- a/packages/modules/devices/sungrow/sungrow/modbus.md +++ /dev/null @@ -1,44 +0,0 @@ -# Modbus Adressen für Sungrow SH\* und SG\* Wechselrichter - -## Datengrundlage - -* TI_20230918_Communication Protocol of Residential and Commercial PV Grid-connected Inverter_V1.1.58_EN.pdf -* TI_20231019_Communication Protocol of Residential Hybrid Inverter_V1.1.2_EN.pdf -* modbus_finder.py an SH10RT-V112 (LAN) Firmware SAPPHIRE-H_B001.V000.P005-20231027 -* modbus_finder.py an SH10RT-V112 (WiNet-S) Firmware WINET-SV200.001.00.P023 -* modbus_finder.py an SG10RT (WiNet-S) Firmware BERYL-S_B000.V000.P039-20230626 / WINET-SV200.001.00.P023 - -## Werte - -| Wert | SH_LAN | SH_WiNet | SG_WiNet | Einheit | Typ | Bemerkung | -|-------------------------------------|--------|----------|---------------|---------|----------------|------------------------------------------------------------------| -| WR: Zähler inkl. Batterieentladung | 5003 | 5003 | -- | 0.1 kWh | UINT_32 mixed | Delta zu 'WR: Zähler Gesamtertrag' ist entladene **Netz**energie | -| WR: Zähler Gesamtertrag | 5660 | -- | 5143 | 0.1 kWh | UINT_32 mixed | Wirkleistungszähler, abweichend zu Gesamt-PV-Stromerzeugung | -| WR: AC Ausgangsspannung Phase A | 5018 | 5018 | 5018 | 0.1 V | UINT_16 little | Unterscheidet sich pro WR (nicht vom Meter gemessen) | -| WR: AC Ausgangsspannung Phase B | 5019 | 5019 | 5019 | 0.1 V | UINT_16 little | Unterscheidet sich pro WR (nicht vom Meter gemessen) | -| WR: AC Ausgangsspannung Phase C | 5020 | 5020 | 5020 | 0.1 V | UINT_16 little | Unterscheidet sich pro WR (nicht vom Meter gemessen) | -| WR: Akt. DC Bruttoleistung | 5016 | 5016 | 5016 | 1 W | INT_32 mixed | | -| WR: Akt. AC Wirkleistung | 13033 | 13033 | -- | 1 W | INT_32 mixed | ggf. Speicherladung addieren für effektive PV-Leistung | -| WR: Akt. AC Wirkleistung | 5030 | -- | 5030 | 1 W | INT_32 mixed | 5030 "altes" Register, 13033 bevorzugt für SH Versionen | -| WR: Akt. Leistungsfluss | 13000 | 13000 | -- | ja/nein | 8-bit bitmask | (v.r.) Bit0: PV-Erzeugung, Bit1: Batt. lädt, Bit2: Batt. entlädt | -| BAT: Akt. Leistung (ein-/ausgehend) | 13021 | 13021 | -- | 1 W | UINT16 little | Immer positiv, bei Be- und Entladung. WR Leistungsfluss beachten | -| BAT: SoC | 13022 | 13022 | -- | 0.1 % | UINT16 little | | -| BAT: Zähler Ladung von PV | 13012 | 13012 | -- | 0.1 kWh | UINT32 mixed | | -| BAT: Zähler Gesamtladung | 13026 | 13026 | -- | 0.1 kWh | UINT32 mixed | | -| BAT: Zähler Gesamtentladung | 13040 | 13040 | -- | 0.1 kWh | UINT32 mixed | | -| Netz: Akt. Wirkleistung | 13009 | 13009 | -- | -1 W | INT_32 mixed | | -| Netz: Akt. Wirkleistung | -- | -- | ? 5090 ? 5082 | 1 W | INT_32 mixed | | -| Netz: Akt. Frequenz | 5035 | -- | 5035 | 0.1 Hz | UINT_16 little | | -| Netz: Akt. Frequenz | -- | 5035 | -- | 0.01 Hz | UINT_16 little | | -| Netz: Akt. Leistungsfaktor | 5034 | 5034 | 5034 | 0.001 | INT_16 little | Nur über alle Phasen vorhanden | -| Netz: Zähler Netzentnahme | 13036 | 13036 | -- | 0.1 kWh | UINT_32 mixed | | -| Netz: Zähler Einspeisung | 13045 | 13045 | -- | 0.1 kWh | UINT_32 mixed | | -| Meter: AC Wirkleistung Phase A | 5602 | 5602 | 5084 | 1 W | INT_32 mixed | Im Unterschied zu 13009 Vorzeichen korrekt | -| Meter: AC Wirkleistung Phase B | 5604 | 5604 | 5086 | 1 W | INT_32 mixed | Im Unterschied zu 13009 Vorzeichen korrekt | -| Meter: AC Wirkleistung Phase C | 5606 | 5606 | 5088 | 1 W | INT_32 mixed | Im Unterschied zu 13009 Vorzeichen korrekt | -| Meter: AC Spannung Phase A | 5740 | -- | -- | 0.1 V | UINT_16 little | | -| Meter: AC Spannung Phase B | 5741 | -- | -- | 0.1 V | UINT_16 little | | -| Meter: AC Spannung Phase C | 5742 | -- | -- | 0.1 V | UINT_16 little | | -| Meter: AC Strom Phase A | 5743 | -- | -- | 0.01 A | UINT_16 little | Immer positiv, auch bei Einspeisung | -| Meter: AC Strom Phase B | 5744 | -- | -- | 0.01 A | UINT_16 little | Immer positiv, auch bei Einspeisung | -| Meter: AC Strom Phase C | 5745 | -- | -- | 0.01 A | UINT_16 little | Immer positiv, auch bei Einspeisung | diff --git a/packages/modules/devices/sungrow/sungrow/__init__.py b/packages/modules/devices/sungrow/sungrow_ihm/__init__.py similarity index 100% rename from packages/modules/devices/sungrow/sungrow/__init__.py rename to packages/modules/devices/sungrow/sungrow_ihm/__init__.py diff --git a/packages/modules/devices/sungrow/sungrow_ihm/bat.py b/packages/modules/devices/sungrow/sungrow_ihm/bat.py new file mode 100644 index 0000000000..302ea96058 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_ihm/bat.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +import logging +from typing import Any, Optional, TypedDict + +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 ModbusDataType, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.sungrow.sungrow_ihm.config import SungrowIHMBatSetup, SungrowIHM + +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowIHM + + +class SungrowIHMBat(AbstractBat): + def __init__(self, component_config: SungrowIHMBatSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowIHM = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.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.last_mode = 'Undefined' + + def update(self) -> None: + unit = self.device_config.configuration.modbus_id + soc = int(self.__tcp_client.read_input_registers(8162, ModbusDataType.UINT_16, unit=unit) / 10) + + bat_power = self.__tcp_client.read_input_registers(8160, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -10 + + imported, exported = self.sim_counter.sim_count(bat_power) + + bat_state = BatState( + power=bat_power, + soc=soc, + imported=imported, + exported=exported + ) + self.store.set(bat_state) + + def set_power_limit(self, power_limit: Optional[int]) -> None: + unit = self.device_config.configuration.modbus_id + log.debug(f'last_mode: {self.last_mode}') + + if power_limit is None: + log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter") + if self.last_mode is not None: + self.__tcp_client.write_register(8023, 1, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(8024, 0xCC, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = None + elif power_limit == 0: + log.debug("Aktive Batteriesteuerung. Batterie wird auf Stop gesetzt und nicht entladen") + if self.last_mode != 'stop': + self.__tcp_client.write_register(8023, 5, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(8024, 0xCC, data_type=ModbusDataType.UINT_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") + if self.last_mode != 'discharge': + self.__tcp_client.write_register(8023, 5, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(8024, 0xBB, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = 'discharge' + power_value = int(power_limit / 100) + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_limit} W entladen für den Hausverbrauch") + self.__tcp_client.write_register(8025, power_value, data_type=ModbusDataType.UINT_32, + wordorder=Endian.Little, unit=unit) + elif power_limit > 0: + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_limit} W geladen") + if self.last_mode != 'charge': + self.__tcp_client.write_register(8023, 5, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(8025, 0xAA, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = 'charge' + power_value = int(power_limit / 100) + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_limit} W geladen") + self.__tcp_client.write_register(8025, power_value, data_type=ModbusDataType.UINT_32, + wordorder=Endian.Little, unit=unit) + + def power_limit_controllable(self) -> bool: + return True + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowIHMBatSetup) diff --git a/packages/modules/devices/sungrow/sungrow_ihm/config.py b/packages/modules/devices/sungrow/sungrow_ihm/config.py new file mode 100644 index 0000000000..c66e690824 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_ihm/config.py @@ -0,0 +1,69 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class SungrowIHMConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 503, + modbus_id: int = 247): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +class SungrowIHM: + def __init__(self, + name: str = "Sungrow iHomeManager", + type: str = "sungrow_ihm", + id: int = 0, + configuration: SungrowIHMConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or SungrowIHMConfiguration() + + +class SungrowIHMBatConfiguration: + def __init__(self): + pass + + +class SungrowIHMBatSetup(ComponentSetup[SungrowIHMBatConfiguration]): + def __init__(self, + name: str = " iHM Speicher", + type: str = "bat", + id: int = 0, + configuration: SungrowIHMBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowIHMBatConfiguration()) + + +class SungrowIHMCounterConfiguration: + def __init__(self): + pass + + +class SungrowIHMCounterSetup(ComponentSetup[SungrowIHMCounterConfiguration]): + def __init__(self, + name: str = "Sungrow iHM Zähler", + type: str = "counter", + id: int = 0, + configuration: SungrowIHMCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowIHMCounterConfiguration()) + + +class SungrowIHMInverterConfiguration: + def __init__(self): + pass + + +class SungrowIHMInverterSetup(ComponentSetup[SungrowIHMInverterConfiguration]): + def __init__(self, + name: str = "Sungrow iHM Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: SungrowIHMInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowIHMInverterConfiguration()) diff --git a/packages/modules/devices/sungrow/sungrow_ihm/counter.py b/packages/modules/devices/sungrow/sungrow_ihm/counter.py new file mode 100644 index 0000000000..17dbaf150b --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_ihm/counter.py @@ -0,0 +1,59 @@ +#!/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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.sungrow.sungrow_ihm.config import SungrowIHM, SungrowIHMCounterSetup + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowIHM + + +class SungrowIHMCounter(AbstractCounter): + def __init__(self, component_config: SungrowIHMCounterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowIHM = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.id, self.component_config.id, prefix="evu") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def update(self): + unit = self.device_config.configuration.modbus_id + power = self.__tcp_client.read_input_registers(8156, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -10 + + powers = self.__tcp_client.read_input_registers(8558, [ModbusDataType.UINT_32] * 3, + wordorder=Endian.Little, unit=unit) + + frequency = self.__tcp_client.read_input_registers(8557, ModbusDataType.UINT_16, unit=unit) / 10 + + voltages = self.__tcp_client.read_input_registers(8554, [ModbusDataType.UINT_16] * 3, + wordorder=Endian.Little, unit=unit) + + voltages = [value / 10 for value in voltages] + + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + powers=powers, + voltages=voltages, + frequency=frequency, + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowIHMCounterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_ihm/device.py b/packages/modules/devices/sungrow/sungrow_ihm/device.py new file mode 100644 index 0000000000..4619549b00 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_ihm/device.py @@ -0,0 +1,55 @@ +#!/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 ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.common import modbus +from modules.devices.sungrow.sungrow_ihm.bat import SungrowIHMBat +from modules.devices.sungrow.sungrow_ihm.config import SungrowIHM, SungrowIHMBatSetup +from modules.devices.sungrow.sungrow_ihm.config import SungrowIHMCounterSetup, SungrowIHMInverterSetup +from modules.devices.sungrow.sungrow_ihm.counter import SungrowIHMCounter +from modules.devices.sungrow.sungrow_ihm.inverter import SungrowIHMInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: SungrowIHM): + client = None + + def create_bat_component(component_config: SungrowIHMBatSetup): + nonlocal client + return SungrowIHMBat(component_config, device_config=device_config, client=client) + + def create_counter_component(component_config: SungrowIHMCounterSetup): + nonlocal client + return SungrowIHMCounter(component_config, device_config=device_config, client=client) + + def create_inverter_component(component_config: SungrowIHMInverterSetup): + nonlocal client + return SungrowIHMInverter(component_config, device_config=device_config, client=client) + + def update_components(components: Iterable[Union[SungrowIHMBat, SungrowIHMCounter, SungrowIHMInverter]]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + + 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=SungrowIHM) diff --git a/packages/modules/devices/sungrow/sungrow_ihm/inverter.py b/packages/modules/devices/sungrow/sungrow_ihm/inverter.py new file mode 100644 index 0000000000..eaa705b9fa --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_ihm/inverter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict + +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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.sungrow.sungrow_ihm.config import SungrowIHMInverterSetup, SungrowIHM + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowIHM + + +class SungrowIHMInverter(AbstractInverter): + def __init__(self, component_config: SungrowIHMInverterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowIHM = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.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) -> float: + unit = self.device_config.configuration.modbus_id + + power = self.__tcp_client.read_input_registers(8154, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -10 + + imported, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + imported=imported, + exported=exported + ) + self.store.set(inverter_state) + return power + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowIHMInverterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_micro/__init__.py b/packages/modules/devices/sungrow/sungrow_micro/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/sungrow/sungrow_micro/config.py b/packages/modules/devices/sungrow/sungrow_micro/config.py new file mode 100644 index 0000000000..b2bf9fcc99 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_micro/config.py @@ -0,0 +1,41 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class SungrowMicroConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 1): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +class SungrowMicro: + def __init__(self, + name: str = "Sungrow Microwechselrichter", + type: str = "sungrow_micro", + id: int = 0, + configuration: SungrowMicroConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or SungrowMicroConfiguration() + + +class SungrowMicroInverterConfiguration: + def __init__(self): + pass + + +class SungrowMicroInverterSetup(ComponentSetup[SungrowMicroInverterConfiguration]): + def __init__(self, + name: str = "Sungrow Microwechselrichter SxxxxS", + type: str = "inverter", + id: int = 0, + configuration: SungrowMicroInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowMicroInverterConfiguration()) diff --git a/packages/modules/devices/sungrow/sungrow_micro/device.py b/packages/modules/devices/sungrow/sungrow_micro/device.py new file mode 100644 index 0000000000..27593e5f35 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_micro/device.py @@ -0,0 +1,42 @@ +#!/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 ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.common import modbus +from modules.devices.sungrow.sungrow_micro.config import SungrowMicro, SungrowMicroInverterSetup +from modules.devices.sungrow.sungrow_micro.inverter import SungrowMicroInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: SungrowMicro): + client = None + + def create_inverter_component(component_config: SungrowMicroInverterSetup): + nonlocal client + return SungrowMicroInverter(component_config, device_config=device_config, client=client) + + def update_components(components: Iterable[Union[SungrowMicroInverter]]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + + return ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=SungrowMicro) diff --git a/packages/modules/devices/sungrow/sungrow_micro/inverter.py b/packages/modules/devices/sungrow/sungrow_micro/inverter.py new file mode 100644 index 0000000000..9eb479e4b7 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_micro/inverter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict + +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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.sungrow.sungrow_micro.config import SungrowMicroInverterSetup, SungrowMicro + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowMicro + + +class SungrowMicroInverter(AbstractInverter): + def __init__(self, component_config: SungrowMicroInverterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowMicro = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.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) -> float: + unit = self.device_config.configuration.modbus_id + + power = self.__tcp_client.read_input_registers(32213, ModbusDataType.UINT_32, + wordorder=Endian.Little, unit=unit) * -1 + + imported, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + imported=imported, + exported=exported + ) + self.store.set(inverter_state) + return power + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowMicroInverterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_sg/__init__.py b/packages/modules/devices/sungrow/sungrow_sg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/sungrow/sungrow_sg/config.py b/packages/modules/devices/sungrow/sungrow_sg/config.py new file mode 100644 index 0000000000..a6bf4a3b76 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sg/config.py @@ -0,0 +1,55 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class SungrowSGConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 1): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +class SungrowSG: + def __init__(self, + name: str = "Sungrow SG", + type: str = "sungrow_sg", + id: int = 0, + configuration: SungrowSGConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or SungrowSGConfiguration() + + +class SungrowSGCounterConfiguration: + def __init__(self): + pass + + +class SungrowSGCounterSetup(ComponentSetup[SungrowSGCounterConfiguration]): + def __init__(self, + name: str = "Sungrow SG Zähler", + type: str = "counter", + id: int = 0, + configuration: SungrowSGCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowSGCounterConfiguration()) + + +class SungrowSGInverterConfiguration: + def __init__(self): + pass + + +class SungrowSGInverterSetup(ComponentSetup[SungrowSGInverterConfiguration]): + def __init__(self, + name: str = "Sungrow SG Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: SungrowSGInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowSGInverterConfiguration()) diff --git a/packages/modules/devices/sungrow/sungrow_sg/counter.py b/packages/modules/devices/sungrow/sungrow_sg/counter.py new file mode 100644 index 0000000000..593ef19c10 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sg/counter.py @@ -0,0 +1,69 @@ +#!/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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.sungrow.sungrow_sg.config import SungrowSG, SungrowSGCounterSetup + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowSG + + +class SungrowSGCounter(AbstractCounter): + def __init__(self, component_config: SungrowSGCounterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowSG = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.id, self.component_config.id, prefix="evu") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.fault_text = "Dieser Sungrow Zähler liefert von Werk aus (entgegen der Dokumentation) "\ + "keine Leistung der einzelnen Phasen. "\ + "Das Lastmanagement ist daher nur anhand der Gesamtleistung (nicht phasenbasiert) möglich." + + def update(self): + unit = self.device_config.configuration.modbus_id + power = self.__tcp_client.read_input_registers(5082, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) + + try: + powers = self.__tcp_client.read_input_registers(5084, [ModbusDataType.INT_32] * 3, + wordorder=Endian.Little, unit=unit) + except Exception: + powers = None + self.fault_state.no_error(self.fault_text) + + frequency = self.__tcp_client.read_input_registers(5035, ModbusDataType.UINT_16, unit=unit) / 10 + + power_factor = self.__tcp_client.read_input_registers(5034, ModbusDataType.INT_16, unit=unit) / 1000 + + voltages = self.__tcp_client.read_input_registers(5018, [ModbusDataType.UINT_16] * 3, + wordorder=Endian.Little, unit=unit) + + voltages = [value / 10 for value in voltages] + + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power, + powers=powers, + voltages=voltages, + frequency=frequency, + power_factors=[power_factor] * 3 + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowSGCounterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_sg/device.py b/packages/modules/devices/sungrow/sungrow_sg/device.py new file mode 100644 index 0000000000..7bdf4c44ad --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sg/device.py @@ -0,0 +1,48 @@ +#!/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 ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.common import modbus +from modules.devices.sungrow.sungrow_sg.config import SungrowSG, SungrowSGCounterSetup, SungrowSGInverterSetup +from modules.devices.sungrow.sungrow_sg.counter import SungrowSGCounter +from modules.devices.sungrow.sungrow_sg.inverter import SungrowSGInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: SungrowSG): + client = None + + def create_counter_component(component_config: SungrowSGCounterSetup): + nonlocal client + return SungrowSGCounter(component_config, device_config=device_config, client=client) + + def create_inverter_component(component_config: SungrowSGInverterSetup): + nonlocal client + return SungrowSGInverter(component_config, device_config=device_config, client=client) + + def update_components(components: Iterable[Union[SungrowSGCounter, SungrowSGInverter]]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + + return ConfigurableDevice( + device_config=device_config, + initializer=initializer, + component_factory=ComponentFactoryByType( + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=SungrowSG) diff --git a/packages/modules/devices/sungrow/sungrow_sg/inverter.py b/packages/modules/devices/sungrow/sungrow_sg/inverter.py new file mode 100644 index 0000000000..344c17a3f9 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sg/inverter.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict + +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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.sungrow.sungrow_sg.config import SungrowSGInverterSetup, SungrowSG + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowSG + + +class SungrowSGInverter(AbstractInverter): + def __init__(self, component_config: SungrowSGInverterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowSG = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.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) -> float: + unit = self.device_config.configuration.modbus_id + + power = self.__tcp_client.read_input_registers(5030, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + dc_power = self.__tcp_client.read_input_registers(5016, ModbusDataType.UINT_32, + wordorder=Endian.Little, unit=unit) * -1 + + currents = self.__tcp_client.read_input_registers(5021, [ModbusDataType.INT_16]*3, unit=unit) + + currents = [value * -0.1 for value in currents] + + imported, exported = self.sim_counter.sim_count(power, dc_power) + + inverter_state = InverterState( + power=power, + dc_power=dc_power, + currents=currents, + imported=imported, + exported=exported + ) + self.store.set(inverter_state) + return power + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowSGInverterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_sh/__init__.py b/packages/modules/devices/sungrow/sungrow_sh/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/sungrow/sungrow/bat.py b/packages/modules/devices/sungrow/sungrow_sh/bat.py similarity index 79% rename from packages/modules/devices/sungrow/sungrow/bat.py rename to packages/modules/devices/sungrow/sungrow_sh/bat.py index ef99e62237..fc5a6a767d 100644 --- a/packages/modules/devices/sungrow/sungrow/bat.py +++ b/packages/modules/devices/sungrow/sungrow_sh/bat.py @@ -9,24 +9,24 @@ from modules.common.modbus import ModbusDataType, Endian, ModbusTcpClient_ from modules.common.simcount import SimCounter from modules.common.store import get_bat_value_store -from modules.devices.sungrow.sungrow.config import SungrowBatSetup, Sungrow -from modules.devices.sungrow.sungrow.registers import RegMode +from modules.devices.sungrow.sungrow_sh.config import SungrowSHBatSetup, SungrowSH +from modules.devices.sungrow.sungrow_sh.registers import RegMode log = logging.getLogger(__name__) class KwargsDict(TypedDict): client: ModbusTcpClient_ - device_config: Sungrow + device_config: SungrowSH -class SungrowBat(AbstractBat): - def __init__(self, component_config: SungrowBatSetup, **kwargs: Any) -> None: +class SungrowSHBat(AbstractBat): + def __init__(self, component_config: SungrowSHBatSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs def initialize(self) -> None: - self.device_config: Sungrow = self.kwargs['device_config'] + self.device_config: SungrowSH = self.kwargs['device_config'] self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] self.sim_counter = SimCounter(self.device_config.id, self.component_config.id, prefix="speicher") self.store = get_bat_value_store(self.component_config.id) @@ -121,28 +121,38 @@ def set_power_limit(self, power_limit: Optional[int]) -> None: if power_limit is None: log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter") if self.last_mode is not None: - self.__tcp_client.write_registers(13049, [0], data_type=ModbusDataType.UINT_16, unit=unit) - self.__tcp_client.write_registers(13050, [0xCC], data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13049, 0, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13050, 0xCC, data_type=ModbusDataType.UINT_16, unit=unit) self.last_mode = None elif power_limit == 0: log.debug("Aktive Batteriesteuerung. Batterie wird auf Stop gesetzt und nicht entladen") if self.last_mode != 'stop': - self.__tcp_client.write_registers(13049, [2], data_type=ModbusDataType.UINT_16, unit=unit) - self.__tcp_client.write_registers(13050, [0xCC], data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13049, 2, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13050, 0xCC, data_type=ModbusDataType.UINT_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") if self.last_mode != 'discharge': - self.__tcp_client.write_registers(13049, [2], data_type=ModbusDataType.UINT_16, unit=unit) - self.__tcp_client.write_registers(13050, [0xBB], data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13049, 2, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13050, 0xBB, data_type=ModbusDataType.UINT_16, unit=unit) self.last_mode = 'discharge' # Die maximale Entladeleistung begrenzen auf 5000W, maximaler Wertebereich Modbusregister. power_value = int(min(abs(power_limit), 5000)) log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_value} W entladen für den Hausverbrauch") - self.__tcp_client.write_registers(13051, [power_value], data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13051, power_value, data_type=ModbusDataType.UINT_16, unit=unit) + elif power_limit > 0: + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_limit} W geladen") + if self.last_mode != 'charge': + self.__tcp_client.write_register(13049, 2, data_type=ModbusDataType.UINT_16, unit=unit) + self.__tcp_client.write_register(13050, 0xAA, data_type=ModbusDataType.UINT_16, unit=unit) + self.last_mode = 'charge' + # Die maximale Entladeleistung begrenzen auf 5000W, maximaler Wertebereich Modbusregister. + power_value = int(min(power_limit, 5000)) + log.debug(f"Aktive Batteriesteuerung. Batterie wird mit {power_value} W geladen") + self.__tcp_client.write_register(13051, power_value, data_type=ModbusDataType.UINT_16, unit=unit) def power_limit_controllable(self) -> bool: return True -component_descriptor = ComponentDescriptor(configuration_factory=SungrowBatSetup) +component_descriptor = ComponentDescriptor(configuration_factory=SungrowSHBatSetup) diff --git a/packages/modules/devices/sungrow/sungrow_sh/config.py b/packages/modules/devices/sungrow/sungrow_sh/config.py new file mode 100644 index 0000000000..6a1e8b1def --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sh/config.py @@ -0,0 +1,72 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup +from modules.devices.sungrow.sungrow_sh.version import Version +from ..vendor import vendor_descriptor + + +class SungrowSHConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 1, + version: Version = Version.SH): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + self.version = version + + +class SungrowSH: + def __init__(self, + name: str = "Sungrow SH", + type: str = "sungrow_sh", + id: int = 0, + configuration: SungrowSHConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or SungrowSHConfiguration() + + +class SungrowSHBatConfiguration: + def __init__(self): + pass + + +class SungrowSHBatSetup(ComponentSetup[SungrowSHBatConfiguration]): + def __init__(self, + name: str = "Sungrow SH Speicher", + type: str = "bat", + id: int = 0, + configuration: SungrowSHBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowSHBatConfiguration()) + + +class SungrowSHCounterConfiguration: + def __init__(self): + pass + + +class SungrowSHCounterSetup(ComponentSetup[SungrowSHCounterConfiguration]): + def __init__(self, + name: str = "Sungrow SH Zähler", + type: str = "counter", + id: int = 0, + configuration: SungrowSHCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowSHCounterConfiguration()) + + +class SungrowSHInverterConfiguration: + def __init__(self): + pass + + +class SungrowSHInverterSetup(ComponentSetup[SungrowSHInverterConfiguration]): + def __init__(self, + name: str = "Sungrow SH Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: SungrowSHInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SungrowSHInverterConfiguration()) diff --git a/packages/modules/devices/sungrow/sungrow/counter.py b/packages/modules/devices/sungrow/sungrow_sh/counter.py similarity index 58% rename from packages/modules/devices/sungrow/sungrow/counter.py rename to packages/modules/devices/sungrow/sungrow_sh/counter.py index 67e6cf849d..d1ad8e4d9e 100644 --- a/packages/modules/devices/sungrow/sungrow/counter.py +++ b/packages/modules/devices/sungrow/sungrow_sh/counter.py @@ -5,25 +5,25 @@ 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 Endian, ModbusDataType, ModbusTcpClient_ -from modules.common.simcount._simcounter import SimCounter +from modules.common.modbus import ModbusDataType, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter from modules.common.store import get_counter_value_store -from modules.devices.sungrow.sungrow.config import Sungrow, SungrowCounterSetup -from modules.devices.sungrow.sungrow.version import Version +from modules.devices.sungrow.sungrow_sh.config import SungrowSH, SungrowSHCounterSetup +from modules.devices.sungrow.sungrow_sh.version import Version class KwargsDict(TypedDict): client: ModbusTcpClient_ - device_config: Sungrow + device_config: SungrowSH -class SungrowCounter(AbstractCounter): - def __init__(self, component_config: SungrowCounterSetup, **kwargs: Any) -> None: +class SungrowSHCounter(AbstractCounter): + def __init__(self, component_config: SungrowSHCounterSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs def initialize(self) -> None: - self.device_config: Sungrow = self.kwargs['device_config'] + self.device_config: SungrowSH = self.kwargs['device_config'] self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] self.sim_counter = SimCounter(self.device_config.id, self.component_config.id, prefix="evu") self.store = get_counter_value_store(self.component_config.id) @@ -32,30 +32,16 @@ def initialize(self) -> None: "keine Leistung der einzelnen Phasen. "\ "Das Lastmanagement ist daher nur anhand der Gesamtleistung (nicht phasenbasiert) möglich." - def update(self, pv_power: float): + def update(self): unit = self.device_config.configuration.modbus_id - if self.device_config.configuration.version in (Version.SH, Version.SH_winet_dongle): - power = self.__tcp_client.read_input_registers(13009, ModbusDataType.INT_32, - wordorder=Endian.Little, unit=unit) * -1 - try: - powers = self.__tcp_client.read_input_registers(5602, [ModbusDataType.INT_32] * 3, - wordorder=Endian.Little, unit=unit) - except Exception: - powers = None - self.fault_state.no_error(self.fault_text) - else: - if pv_power != 0: - power = self.__tcp_client.read_input_registers(5082, ModbusDataType.INT_32, - wordorder=Endian.Little, unit=unit) - else: - power = self.__tcp_client.read_input_registers(5090, ModbusDataType.INT_32, - wordorder=Endian.Little, unit=unit) - try: - powers = self.__tcp_client.read_input_registers(5084, [ModbusDataType.INT_32] * 3, - wordorder=Endian.Little, unit=unit) - except Exception: - powers = None - self.fault_state.no_error(self.fault_text) + power = self.__tcp_client.read_input_registers(13009, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + try: + powers = self.__tcp_client.read_input_registers(5602, [ModbusDataType.INT_32] * 3, + wordorder=Endian.Little, unit=unit) + except Exception: + powers = None + self.fault_state.no_error(self.fault_text) frequency = self.__tcp_client.read_input_registers(5035, ModbusDataType.UINT_16, unit=unit) / 10 if self.device_config.configuration.version == Version.SH_winet_dongle: @@ -89,4 +75,4 @@ def update(self, pv_power: float): self.store.set(counter_state) -component_descriptor = ComponentDescriptor(configuration_factory=SungrowCounterSetup) +component_descriptor = ComponentDescriptor(configuration_factory=SungrowSHCounterSetup) diff --git a/packages/modules/devices/sungrow/sungrow_sh/device.py b/packages/modules/devices/sungrow/sungrow_sh/device.py new file mode 100644 index 0000000000..e5d6c47a78 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sh/device.py @@ -0,0 +1,55 @@ +#!/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 ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater +from modules.common import modbus +from modules.devices.sungrow.sungrow_sh.bat import SungrowSHBat +from modules.devices.sungrow.sungrow_sh.config import SungrowSH, SungrowSHBatSetup +from modules.devices.sungrow.sungrow_sh.config import SungrowSHCounterSetup, SungrowSHInverterSetup +from modules.devices.sungrow.sungrow_sh.counter import SungrowSHCounter +from modules.devices.sungrow.sungrow_sh.inverter import SungrowSHInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: SungrowSH): + client = None + + def create_bat_component(component_config: SungrowSHBatSetup): + nonlocal client + return SungrowSHBat(component_config, device_config=device_config, client=client) + + def create_counter_component(component_config: SungrowSHCounterSetup): + nonlocal client + return SungrowSHCounter(component_config, device_config=device_config, client=client) + + def create_inverter_component(component_config: SungrowSHInverterSetup): + nonlocal client + return SungrowSHInverter(component_config, device_config=device_config, client=client) + + def update_components(components: Iterable[Union[SungrowSHBat, SungrowSHCounter, SungrowSHInverter]]): + with client: + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + def initializer(): + nonlocal client + client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port) + + 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=SungrowSH) diff --git a/packages/modules/devices/sungrow/sungrow_sh/inverter.py b/packages/modules/devices/sungrow/sungrow_sh/inverter.py new file mode 100644 index 0000000000..acaf859403 --- /dev/null +++ b/packages/modules/devices/sungrow/sungrow_sh/inverter.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +from typing import Any, TypedDict + +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, Endian, ModbusTcpClient_ +from modules.common.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.sungrow.sungrow_sh.config import SungrowSHInverterSetup, SungrowSH + + +class KwargsDict(TypedDict): + client: ModbusTcpClient_ + device_config: SungrowSH + + +class SungrowSHInverter(AbstractInverter): + def __init__(self, component_config: SungrowSHInverterSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs: KwargsDict = kwargs + + def initialize(self) -> None: + self.device_config: SungrowSH = self.kwargs['device_config'] + self.__tcp_client: ModbusTcpClient_ = self.kwargs['client'] + self.sim_counter = SimCounter(self.device_config.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) -> float: + unit = self.device_config.configuration.modbus_id + + power = self.__tcp_client.read_input_registers(13033, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=unit) * -1 + dc_power = self.__tcp_client.read_input_registers(5016, ModbusDataType.UINT_32, + wordorder=Endian.Little, unit=unit) * -1 + + currents = self.__tcp_client.read_input_registers(13030, [ModbusDataType.INT_16]*3, unit=unit) + + currents = [value * -0.1 for value in currents] + + imported, exported = self.sim_counter.sim_count(power, dc_power) + + inverter_state = InverterState( + power=power, + dc_power=dc_power, + currents=currents, + imported=imported, + exported=exported + ) + self.store.set(inverter_state) + return power + + +component_descriptor = ComponentDescriptor(configuration_factory=SungrowSHInverterSetup) diff --git a/packages/modules/devices/sungrow/sungrow/registers.py b/packages/modules/devices/sungrow/sungrow_sh/registers.py similarity index 100% rename from packages/modules/devices/sungrow/sungrow/registers.py rename to packages/modules/devices/sungrow/sungrow_sh/registers.py diff --git a/packages/modules/devices/sungrow/sungrow/version.py b/packages/modules/devices/sungrow/sungrow_sh/version.py similarity index 51% rename from packages/modules/devices/sungrow/sungrow/version.py rename to packages/modules/devices/sungrow/sungrow_sh/version.py index 539705095e..57a66e8f47 100644 --- a/packages/modules/devices/sungrow/sungrow/version.py +++ b/packages/modules/devices/sungrow/sungrow_sh/version.py @@ -3,6 +3,4 @@ class Version(IntEnum): SH = 0 - SG = 1 - SG_winet_dongle = 2 - SH_winet_dongle = 3 + SH_winet_dongle = 1