From 8e383d820e43f684e4ede19263017e5f307e9e9a Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 26 Aug 2026 15:11:36 +0200 Subject: [PATCH 1/3] add ecoflow stream --- .../modules/devices/ecoflow/stream/bat.py | 47 +++++++++++ .../modules/devices/ecoflow/stream/config.py | 68 ++++++++++++++++ .../modules/devices/ecoflow/stream/counter.py | 44 +++++++++++ .../modules/devices/ecoflow/stream/device.py | 79 +++++++++++++++++++ .../devices/ecoflow/stream/inverter.py | 44 +++++++++++ packages/modules/devices/ecoflow/vendor.py | 14 ++++ 6 files changed, 296 insertions(+) create mode 100644 packages/modules/devices/ecoflow/stream/bat.py create mode 100644 packages/modules/devices/ecoflow/stream/config.py create mode 100644 packages/modules/devices/ecoflow/stream/counter.py create mode 100644 packages/modules/devices/ecoflow/stream/device.py create mode 100644 packages/modules/devices/ecoflow/stream/inverter.py create mode 100644 packages/modules/devices/ecoflow/vendor.py diff --git a/packages/modules/devices/ecoflow/stream/bat.py b/packages/modules/devices/ecoflow/stream/bat.py new file mode 100644 index 0000000000..4c204defc7 --- /dev/null +++ b/packages/modules/devices/ecoflow/stream/bat.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +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.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.devices.ecoflow.stream.config import EcoflowStreamBatSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + + +class KwargsDict(TypedDict): + device_id: int + + +class EcoflowStreamBat(AbstractBat): + def __init__(self, component_config: EcoflowStreamBatSetup, **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.sim_counter = SimCounter(self.__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) + + def update(self, response) -> None: + power = float(response['data']['powGetBpCms']) + soc = float(response['data']['cmsBattSoc']) + + 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 + ) + self.store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=EcoflowStreamBatSetup) diff --git a/packages/modules/devices/ecoflow/stream/config.py b/packages/modules/devices/ecoflow/stream/config.py new file mode 100644 index 0000000000..44d2f57b7b --- /dev/null +++ b/packages/modules/devices/ecoflow/stream/config.py @@ -0,0 +1,68 @@ +from modules.common.component_setup import ComponentSetup +from ..vendor import vendor_descriptor + + +class EcoflowStreamConfiguration: + def __init__(self, + serial=None, + access_key=None, + secret_key=None): + self.serial = serial + self.access_key = access_key + self.secret_key = secret_key + + +class EcoflowStream: + + def __init__(self, + name: str = "Ecoflow Stream", + type: str = "stream", + id: int = 0, + configuration: EcoflowStreamConfiguration = None) -> None: + self.name = name + self.type = type + self.vendor = vendor_descriptor.configuration_factory().type + self.id = id + self.configuration = configuration or EcoflowStreamConfiguration() + + +class EcoflowStreamBatConfiguration: + def __init__(self): + pass + + +class EcoflowStreamBatSetup(ComponentSetup[EcoflowStreamBatConfiguration]): + def __init__(self, + name: str = "Ecoflow Stream Speicher", + type: str = "bat", + id: int = 0, + configuration: EcoflowStreamBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or EcoflowStreamBatConfiguration()) + + +class EcoflowStreamCounterConfiguration: + def __init__(self): + pass + + +class EcoflowStreamCounterSetup(ComponentSetup[EcoflowStreamCounterConfiguration]): + def __init__(self, + name: str = "Ecoflow Stream Zähler", + type: str = "counter", + id: int = 0, + configuration: EcoflowStreamCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or EcoflowStreamCounterConfiguration()) + + +class EcoflowStreamInverterConfiguration: + def __init__(self): + pass + + +class EcoflowStreamInverterSetup(ComponentSetup[EcoflowStreamInverterConfiguration]): + def __init__(self, + name: str = "Ecoflow Stream Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: EcoflowStreamInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or EcoflowStreamInverterConfiguration()) diff --git a/packages/modules/devices/ecoflow/stream/counter.py b/packages/modules/devices/ecoflow/stream/counter.py new file mode 100644 index 0000000000..968d19f843 --- /dev/null +++ b/packages/modules/devices/ecoflow/stream/counter.py @@ -0,0 +1,44 @@ +#!/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.simcount._simcounter import SimCounter +from modules.common.store import get_counter_value_store +from modules.devices.ecoflow.stream.config import EcoflowStreamCounterSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + + +class KwargsDict(TypedDict): + device_id: int + + +class EcoflowStreamCounter(AbstractCounter): + def __init__(self, component_config: EcoflowStreamCounterSetup, **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.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state) + + def update(self, response) -> None: + power = float(response['data']['powGetSysGrid']) + self.peak_filter.check_values(power) + imported, exported = self.sim_counter.sim_count(power) + + counter_state = CounterState( + imported=imported, + exported=exported, + power=power + ) + self.store.set(counter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=EcoflowStreamCounterSetup) diff --git a/packages/modules/devices/ecoflow/stream/device.py b/packages/modules/devices/ecoflow/stream/device.py new file mode 100644 index 0000000000..0029912b92 --- /dev/null +++ b/packages/modules/devices/ecoflow/stream/device.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import logging +from typing import Union, Iterable +import hmac +import hashlib +import time +import uuid +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.configurable_device import (ConfigurableDevice, ComponentFactoryByType, + MultiComponentUpdater) +from modules.devices.ecoflow.stream.bat import EcoflowStreamBat +from modules.devices.ecoflow.stream.config import (EcoflowStream, + EcoflowStreamBatSetup, + EcoflowStreamCounterSetup, + EcoflowStreamInverterSetup) +from modules.devices.ecoflow.stream.counter import EcoflowStreamCounter +from modules.devices.ecoflow.stream.inverter import EcoflowStreamInverter + +log = logging.getLogger(__name__) +EcoflowStreamComponent = Union[EcoflowStreamBat, EcoflowStreamCounter, EcoflowStreamInverter] + + +def create_device(device_config: EcoflowStream): + def create_bat(component_config: EcoflowStreamBatSetup) -> EcoflowStreamBat: + return EcoflowStreamBat(component_config=component_config, device_id=device_config.id) + + def create_counter(component_config: EcoflowStreamCounterSetup) -> EcoflowStreamCounter: + return EcoflowStreamCounter(component_config=component_config, device_id=device_config.id) + + def create_inverter(component_config: EcoflowStreamInverterSetup) -> EcoflowStreamInverter: + return EcoflowStreamInverter(component_config=component_config, device_id=device_config.id) + + def generate_sign(access_key, secret_key, timestamp, nonce): + message = f"accessKey={access_key}&nonce={nonce}×tamp={timestamp}" + sign = hmac.new( + secret_key.encode("utf-8"), + message.encode("utf-8"), + hashlib.sha256 + ).hexdigest() + return sign + + def update_components(components: Iterable[EcoflowStreamComponent]): + access_key = device_config.configuration.access_key + secret_key = device_config.configuration.secret_key + serial = device_config.configuration.serial + base_url = "https://api-e.ecoflow.com" + + timestamp = str(int(time.time() * 1000)) + nonce = str(uuid.uuid4()) + sign = generate_sign(access_key, secret_key, timestamp, nonce) + + headers = { + "accessKey": access_key, + "timestamp": timestamp, + "nonce": nonce, + "sign": sign, + "Content-Type": "application/json" + } + url = f"{base_url}/iot-open/sign/device/quota/all?sn={serial}" + + response = req.get_http_session().get( + url, + headers=headers, + timeout=10 + ).json() + for component in components: + with SingleComponentUpdateContext(component.fault_state): + component.update(response) + + return ConfigurableDevice( + device_config, + component_factory=ComponentFactoryByType(bat=create_bat, counter=create_counter, inverter=create_inverter), + component_updater=MultiComponentUpdater(update_components) + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=EcoflowStream) diff --git a/packages/modules/devices/ecoflow/stream/inverter.py b/packages/modules/devices/ecoflow/stream/inverter.py new file mode 100644 index 0000000000..db42bf57ea --- /dev/null +++ b/packages/modules/devices/ecoflow/stream/inverter.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +from typing import TypedDict, Any + +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.simcount import SimCounter +from modules.common.store import get_inverter_value_store +from modules.devices.ecoflow.stream.config import EcoflowStreamInverterSetup +from modules.common.utils.peak_filter import PeakFilter +from modules.common.component_type import ComponentType + + +class KwargsDict(TypedDict): + device_id: int + + +class EcoflowStreamInverter(AbstractInverter): + def __init__(self, component_config: EcoflowStreamInverterSetup, **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.sim_counter = SimCounter(self.__device_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)) + self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state) + + def update(self, response) -> None: + power = float(response['data']['powGetPvSum']) * -1 + + self.peak_filter.check_values(power) + _, exported = self.sim_counter.sim_count(power) + + inverter_state = InverterState( + power=power, + exported=exported + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=EcoflowStreamInverterSetup) diff --git a/packages/modules/devices/ecoflow/vendor.py b/packages/modules/devices/ecoflow/vendor.py new file mode 100644 index 0000000000..de5cb80bc9 --- /dev/null +++ b/packages/modules/devices/ecoflow/vendor.py @@ -0,0 +1,14 @@ +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 = "Ecoflow" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) From 2a923a5d0a060ba04b472b5968e6890c6a6ae758 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 26 Aug 2026 15:18:51 +0200 Subject: [PATCH 2/3] rename type to ecoflow_stream --- packages/modules/devices/ecoflow/stream/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/devices/ecoflow/stream/config.py b/packages/modules/devices/ecoflow/stream/config.py index 44d2f57b7b..2453b0b7ad 100644 --- a/packages/modules/devices/ecoflow/stream/config.py +++ b/packages/modules/devices/ecoflow/stream/config.py @@ -16,7 +16,7 @@ class EcoflowStream: def __init__(self, name: str = "Ecoflow Stream", - type: str = "stream", + type: str = "ecoflow_stream", id: int = 0, configuration: EcoflowStreamConfiguration = None) -> None: self.name = name From d8f1e38eb2dc8bfea3ba8fece85c8036a1fdf1ab Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 26 Aug 2026 15:49:56 +0200 Subject: [PATCH 3/3] use get_component_value_store --- packages/modules/devices/ecoflow/stream/bat.py | 4 ++-- packages/modules/devices/ecoflow/stream/counter.py | 4 ++-- packages/modules/devices/ecoflow/stream/inverter.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/modules/devices/ecoflow/stream/bat.py b/packages/modules/devices/ecoflow/stream/bat.py index 4c204defc7..8ab86bc6cd 100644 --- a/packages/modules/devices/ecoflow/stream/bat.py +++ b/packages/modules/devices/ecoflow/stream/bat.py @@ -6,7 +6,7 @@ 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.common.store import get_component_value_store from modules.devices.ecoflow.stream.config import EcoflowStreamBatSetup from modules.common.utils.peak_filter import PeakFilter from modules.common.component_type import ComponentType @@ -24,7 +24,7 @@ def __init__(self, component_config: EcoflowStreamBatSetup, **kwargs: Any) -> No def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") - self.store = get_bat_value_store(self.component_config.id) + self.store = get_component_value_store(self.component_config.type, 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) diff --git a/packages/modules/devices/ecoflow/stream/counter.py b/packages/modules/devices/ecoflow/stream/counter.py index 968d19f843..65398c8d50 100644 --- a/packages/modules/devices/ecoflow/stream/counter.py +++ b/packages/modules/devices/ecoflow/stream/counter.py @@ -6,7 +6,7 @@ from modules.common.component_type import ComponentDescriptor from modules.common.fault_state import ComponentInfo, FaultState from modules.common.simcount._simcounter import SimCounter -from modules.common.store import get_counter_value_store +from modules.common.store import get_component_value_store from modules.devices.ecoflow.stream.config import EcoflowStreamCounterSetup from modules.common.utils.peak_filter import PeakFilter from modules.common.component_type import ComponentType @@ -24,7 +24,7 @@ def __init__(self, component_config: EcoflowStreamCounterSetup, **kwargs: Any) - def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") - self.store = get_counter_value_store(self.component_config.id) + self.store = get_component_value_store(self.component_config.type, self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state) diff --git a/packages/modules/devices/ecoflow/stream/inverter.py b/packages/modules/devices/ecoflow/stream/inverter.py index db42bf57ea..5f8948f4fb 100644 --- a/packages/modules/devices/ecoflow/stream/inverter.py +++ b/packages/modules/devices/ecoflow/stream/inverter.py @@ -6,7 +6,7 @@ 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_inverter_value_store +from modules.common.store import get_component_value_store from modules.devices.ecoflow.stream.config import EcoflowStreamInverterSetup from modules.common.utils.peak_filter import PeakFilter from modules.common.component_type import ComponentType @@ -24,7 +24,7 @@ def __init__(self, component_config: EcoflowStreamInverterSetup, **kwargs: Any) def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") - self.store = get_inverter_value_store(self.component_config.id) + self.store = get_component_value_store(self.component_config.type, self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state)