From 8a8ff60d2aa645dca0c50c35967352140ebda621 Mon Sep 17 00:00:00 2001 From: ChristophCaina Date: Fri, 22 May 2026 17:23:51 +0000 Subject: [PATCH] feat: add SunEnergyXT 500 Series battery storage module --- .../modules/devices/sunenergyxt/__init__.py | 0 .../sunenergyxt/sunenergyxt/__init__.py | 0 .../devices/sunenergyxt/sunenergyxt/bat.py | 87 +++++++++++++++++++ .../devices/sunenergyxt/sunenergyxt/config.py | 40 +++++++++ .../devices/sunenergyxt/sunenergyxt/device.py | 31 +++++++ .../sunenergyxt/sunenergyxt/device.vue | 56 ++++++++++++ .../modules/devices/sunenergyxt/vendor.py | 11 +++ 7 files changed, 225 insertions(+) create mode 100644 packages/modules/devices/sunenergyxt/__init__.py create mode 100644 packages/modules/devices/sunenergyxt/sunenergyxt/__init__.py create mode 100644 packages/modules/devices/sunenergyxt/sunenergyxt/bat.py create mode 100644 packages/modules/devices/sunenergyxt/sunenergyxt/config.py create mode 100644 packages/modules/devices/sunenergyxt/sunenergyxt/device.py create mode 100644 packages/modules/devices/sunenergyxt/sunenergyxt/device.vue create mode 100644 packages/modules/devices/sunenergyxt/vendor.py diff --git a/packages/modules/devices/sunenergyxt/__init__.py b/packages/modules/devices/sunenergyxt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/sunenergyxt/sunenergyxt/__init__.py b/packages/modules/devices/sunenergyxt/sunenergyxt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/devices/sunenergyxt/sunenergyxt/bat.py b/packages/modules/devices/sunenergyxt/sunenergyxt/bat.py new file mode 100644 index 0000000000..01945f82a0 --- /dev/null +++ b/packages/modules/devices/sunenergyxt/sunenergyxt/bat.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""SunEnergyXT 500 Series – openWB Batteriespeicher-Modul.""" +import logging +from typing import Any, Optional +import requests +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.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.sunenergyxt.sunenergyxt.config import SunEnergyXT, SunEnergyXTBatSetup + +log = logging.getLogger(__name__) + + +class SunEnergyXTBat(AbstractBat): + def __init__(self, component_config: SunEnergyXTBatSetup, **kwargs: Any) -> None: + self.component_config = component_config + self.kwargs = kwargs + + def initialize(self) -> None: + self.device_config: SunEnergyXT = self.kwargs['device_config'] + 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._base_url = ( + f"http://{self.device_config.configuration.ip_address}" + f":{self.device_config.configuration.port}" + ) + self._timeout = self.device_config.configuration.timeout + + def _read(self) -> dict: + url = f"{self._base_url}/read" + resp = requests.get(url, timeout=self._timeout) + resp.raise_for_status() + return resp.json() + + def _write(self, **kwargs) -> None: + url = f"{self._base_url}/write" + payload = {"state": kwargs} + resp = requests.post(url, json=payload, timeout=self._timeout) + resp.raise_for_status() + log.debug("SunEnergyXT write %s → %s", kwargs, resp.text) + + def update(self) -> None: + data = self._read() + reported = data.get("state", {}).get("reported", data) + + soc = int(float(reported.get("SC", 0))) + power = float(reported.get("PB", 0)) + max_power = float(reported.get("IS", 0)) + + imported, exported = self.sim_counter.sim_count(power) + + bat_state = BatState( + power=power, + soc=soc, + imported=imported, + exported=exported, + ) + bat_state.max_charge_power = max_power + bat_state.max_discharge_power = max_power + self.store.set(bat_state) + log.debug("SunEnergyXT: SoC=%d%%, PB=%.0fW, IS=%.0fW", soc, power, max_power) + + def set_power_limit(self, power_limit: Optional[int]) -> None: + if power_limit is None: + log.debug("SunEnergyXT: Automatik (MM=1, GS=0)") + self._write(MM=1, GS=0) + elif power_limit == 0: + log.debug("SunEnergyXT: Entladung gesperrt (MM=0, GS=0)") + self._write(MM=0, GS=0) + elif power_limit > 0: + p = int(min(power_limit, 9999)) + log.debug("SunEnergyXT: Entladen mit %dW", p) + self._write(MM=0, GS=p) + else: + p = int(min(abs(power_limit), 9999)) + log.debug("SunEnergyXT: Laden mit %dW", p) + self._write(MM=0, GS=-p) + + def power_limit_controllable(self) -> bool: + return True + + +component_descriptor = ComponentDescriptor(configuration_factory=SunEnergyXTBatSetup) diff --git a/packages/modules/devices/sunenergyxt/sunenergyxt/config.py b/packages/modules/devices/sunenergyxt/sunenergyxt/config.py new file mode 100644 index 0000000000..a42ba8aace --- /dev/null +++ b/packages/modules/devices/sunenergyxt/sunenergyxt/config.py @@ -0,0 +1,40 @@ +from typing import Optional +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class SunEnergyXTConfiguration: + def __init__(self, + ip_address: Optional[str] = "192.168.1.100", + port: int = 80, + timeout: int = 5): + self.ip_address = ip_address + self.port = port + self.timeout = timeout + + +class SunEnergyXT: + def __init__(self, + name: str = "SunEnergyXT 500 Series", + type: str = "sunenergyxt", + id: int = 0, + configuration: SunEnergyXTConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or SunEnergyXTConfiguration() + + +class SunEnergyXTBatConfiguration: + def __init__(self): + pass + + +class SunEnergyXTBatSetup(ComponentSetup[SunEnergyXTBatConfiguration]): + def __init__(self, + name: str = "SunEnergyXT Speicher", + type: str = "bat", + id: int = 0, + configuration: SunEnergyXTBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SunEnergyXTBatConfiguration()) diff --git a/packages/modules/devices/sunenergyxt/sunenergyxt/device.py b/packages/modules/devices/sunenergyxt/sunenergyxt/device.py new file mode 100644 index 0000000000..f0a69ddd1d --- /dev/null +++ b/packages/modules/devices/sunenergyxt/sunenergyxt/device.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable +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.devices.sunenergyxt.sunenergyxt.bat import SunEnergyXTBat +from modules.devices.sunenergyxt.sunenergyxt.config import SunEnergyXT, SunEnergyXTBatSetup + +log = logging.getLogger(__name__) + + +def create_device(device_config: SunEnergyXT): + def create_bat_component(component_config: SunEnergyXTBatSetup): + return SunEnergyXTBat(component_config, device_config=device_config) + + def update_components(components: Iterable[SunEnergyXTBat]): + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update() + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=SunEnergyXT) diff --git a/packages/modules/devices/sunenergyxt/sunenergyxt/device.vue b/packages/modules/devices/sunenergyxt/sunenergyxt/device.vue new file mode 100644 index 0000000000..fa2bddeb5c --- /dev/null +++ b/packages/modules/devices/sunenergyxt/sunenergyxt/device.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/modules/devices/sunenergyxt/vendor.py b/packages/modules/devices/sunenergyxt/vendor.py new file mode 100644 index 0000000000..50df2f6d64 --- /dev/null +++ b/packages/modules/devices/sunenergyxt/vendor.py @@ -0,0 +1,11 @@ +from pathlib import Path +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "SunEnergyXT" + self.group = VendorGroup.VENDORS.value + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)