From 7d07f70c1dd5390e276b8882dd3ea52b074bbec8 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 14 Oct 2025 12:56:17 +0200 Subject: [PATCH 1/9] add production counter --- .../modules/devices/fronius/fronius/config.py | 15 +++ .../modules/devices/fronius/fronius/device.py | 13 +- .../fronius/inverter_production_counter.py | 112 +++++++++++++++++ .../inverter_production_counter_test.py | 116 ++++++++++++++++++ 4 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 packages/modules/devices/fronius/fronius/inverter_production_counter.py create mode 100644 packages/modules/devices/fronius/fronius/inverter_production_counter_test.py diff --git a/packages/modules/devices/fronius/fronius/config.py b/packages/modules/devices/fronius/fronius/config.py index 4f6953ccb3..e11f327eb6 100644 --- a/packages/modules/devices/fronius/fronius/config.py +++ b/packages/modules/devices/fronius/fronius/config.py @@ -112,3 +112,18 @@ def __init__(self, id: int = 0, configuration: FroniusSecondaryInverterConfiguration = None) -> None: super().__init__(name, type, id, configuration or FroniusSecondaryInverterConfiguration()) + + +class FroniusProductionCounterConfiguration: + def __init__(self, meter_id: int = 0, variant: int = 0): + self.meter_id = meter_id + self.variant = variant + + +class FroniusProductionCounterSetup(ComponentSetup[FroniusProductionCounterConfiguration]): + def __init__(self, + name: str = "Fronius Erzeugerzähler", + type: str = "inverter_counter_production", + id: int = 0, + configuration: FroniusProductionCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FroniusProductionCounterConfiguration()) diff --git a/packages/modules/devices/fronius/fronius/device.py b/packages/modules/devices/fronius/fronius/device.py index a8eb412a7b..a17d79057b 100644 --- a/packages/modules/devices/fronius/fronius/device.py +++ b/packages/modules/devices/fronius/fronius/device.py @@ -10,16 +10,17 @@ from modules.devices.fronius.fronius.bat import FroniusBat from modules.devices.fronius.fronius.config import (Fronius, FroniusBatSetup, FroniusSecondaryInverterSetup, FroniusSmCounterSetup, FroniusS0CounterSetup, - FroniusInverterSetup) + FroniusProductionCounterSetup, FroniusInverterSetup) from modules.devices.fronius.fronius.counter_s0 import FroniusS0Counter from modules.devices.fronius.fronius.counter_sm import FroniusSmCounter from modules.devices.fronius.fronius.inverter import FroniusInverter from modules.devices.fronius.fronius.inverter_secondary import FroniusSecondaryInverter +from modules.devices.fronius.fronius.inverter_production_counter import FroniusProductionCounter log = logging.getLogger(__name__) -fronius_component_classes = Union[FroniusBat, FroniusSmCounter, - FroniusS0Counter, FroniusInverter, FroniusSecondaryInverter] +fronius_component_classes = Union[FroniusBat, FroniusSmCounter, FroniusS0Counter, + FroniusInverter, FroniusSecondaryInverter, FroniusProductionCounter] def create_device(device_config: Fronius): @@ -46,6 +47,11 @@ def create_inverter_secondary_component(component_config: FroniusSecondaryInvert return FroniusSecondaryInverter(component_config=component_config, device_id=device_config.id) + def create_inverter_counter_production_component(component_config: FroniusProductionCounterSetup): + return FroniusProductionCounter(component_config=component_config, + device_id=device_config.id, + device_config=device_config.configuration) + def update_components(components: Iterable[fronius_component_classes]): inverter_response = None for component in components: @@ -80,6 +86,7 @@ def update_components(components: Iterable[fronius_component_classes]): counter_s0=create_counter_s0_component, inverter=create_inverter_component, inverter_secondary=create_inverter_secondary_component, + inverter_counter_production=create_inverter_counter_production_component, ), component_updater=MultiComponentUpdater(update_components) ) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter.py b/packages/modules/devices/fronius/fronius/inverter_production_counter.py new file mode 100644 index 0000000000..a0eb42ee4c --- /dev/null +++ b/packages/modules/devices/fronius/fronius/inverter_production_counter.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import logging +from typing import TypedDict, Any + +from requests import Session + +from modules.common import req +from modules.common.abstract_device import AbstractCounter +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.fronius.fronius.config import FroniusConfiguration, MeterLocation +from modules.devices.fronius.fronius.config import FroniusProductionCounterSetup + +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + device_id: int + device_config: FroniusConfiguration + + +class FroniusProductionCounter(AbstractCounter): + def __init__(self, component_config: FroniusProductionCounterSetup, **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.device_config: FroniusConfiguration = self.kwargs['device_config'] + 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)) + + def update(self) -> None: + session = req.get_http_session() + variant = self.component_config.configuration.variant + if variant == 0 or variant == 1: + counter_state = self.__update_variant_0_1(session) + elif variant == 2: + counter_state = self.__update_variant_2(session) + else: + raise ValueError("Unbekannte Variante: "+str(variant)) + counter_state.imported, counter_state.exported = self.sim_counter.sim_count(counter_state.power) + self.store.set(counter_state) + + def __update_variant_0_1(self, session: Session) -> InverterState: + variant = self.component_config.configuration.variant + meter_id = self.component_config.configuration.meter_id + if variant == 0: + params = ( + ('Scope', 'Device'), + ('DeviceId', meter_id), + ) + elif variant == 1: + params = ( + ('Scope', 'Device'), + ('DeviceId', meter_id), + ('DataCollection', 'MeterRealtimeData'), + ) + else: + raise ValueError("Unbekannte Generation: "+str(variant)) + response = session.get( + 'http://' + self.device_config.ip_address + '/solar_api/v1/GetMeterRealtimeData.cgi', + params=params, + timeout=5) + response_json_id = response.json()["Body"]["Data"] + + meter_location = MeterLocation.get(response_json_id["Meter_Location_Current"]) + log.debug("Einbauort: "+str(meter_location)) + + powers = [response_json_id["PowerReal_P_Phase_"+str(num)] for num in range(1, 4)] + if meter_location != MeterLocation.external: + raise ValueError("Fehler: Dieser Zähler ist kein Erzeugerzähler.") + else: + power = response_json_id["PowerReal_P_Sum"] * -1 + voltages = [response_json_id["Voltage_AC_Phase_"+str(num)] for num in range(1, 4)] + currents = [powers[i] / voltages[i] for i in range(0, 3)] + + return InverterState( + currents=currents, + power=power + ) + + def __update_variant_2(self, session: Session) -> InverterState: + meter_id = str(self.component_config.configuration.meter_id) + response = session.get( + 'http://' + self.device_config.ip_address + '/solar_api/v1/GetMeterRealtimeData.cgi', + params=(('Scope', 'System'),), + timeout=5) + response_json_id = dict(response.json()["Body"]["Data"]).get(meter_id) + + meter_location = MeterLocation.get(response_json_id["SMARTMETER_VALUE_LOCATION_U16"]) + log.debug("Einbauort: "+str(meter_location)) + + powers = [response_json_id["SMARTMETER_POWERACTIVE_MEAN_0"+str(num)+"_F64"] for num in range(1, 4)] + if meter_location != MeterLocation.external: + raise ValueError("Fehler: Dieser Zähler ist kein Erzeugerzähler.") + else: + power = response_json_id["SMARTMETER_POWERACTIVE_MEAN_SUM_F64"] + voltages = [response_json_id["SMARTMETER_VOLTAGE_0"+str(num)+"_F64"] for num in range(1, 4)] + currents = [powers[i] / voltages[i] for i in range(0, 3)] + + return InverterState( + currents=currents, + power=power + ) + + +component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCounter) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py b/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py new file mode 100644 index 0000000000..7b53383bcb --- /dev/null +++ b/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py @@ -0,0 +1,116 @@ +from unittest.mock import Mock + +import pytest +import requests_mock + +from dataclass_utils import dataclass_from_dict +from helpermodules import compatibility +from modules.common.store._api import LoggingValueStore +from modules.conftest import SAMPLE_IP +from modules.devices.fronius.fronius import inverter_production_counter +from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCounterSetup +from test_utils.mock_ramdisk import MockRamdisk + + +@pytest.fixture +def mock_ramdisk(monkeypatch): + monkeypatch.setattr(compatibility, "is_ramdisk_in_use", lambda: True) + return MockRamdisk(monkeypatch) + + +def test_update_external_var2(monkeypatch, requests_mock: requests_mock.Mocker, mock_ramdisk, mock_simcount): + component_config = FroniusProductionCounterSetup() + component_config.configuration.variant = 2 + device_config = FroniusConfiguration() + device_config.ip_address = SAMPLE_IP + component_config.configuration.meter_id = 1 + inverter = inverter_production_counter.FroniusProductionCounter(component_config, device_config=dataclass_from_dict( + FroniusConfiguration, device_config), device_id=0) + inverter.initialize() + + mock = Mock(return_value=None) + monkeypatch.setattr(LoggingValueStore, "set", mock) + mock_simcount.return_value = 0, 0 + requests_mock.get( + "http://" + SAMPLE_IP + "/solar_api/v1/GetMeterRealtimeData.cgi", + json=json_ext_var2) + + inverter.update() + + # mock.assert_called_once() + inverter_state = mock.call_args[0][0] + assert inverter_state.currents == [-5.373121093182142, -5.664436188811191, -5.585225225225224] + assert inverter_state.power == 3809.4 + + +json_ext_var2 = { + "Body": { + "Data": { + "1": { + "ACBRIDGE_CURRENT_ACTIVE_MEAN_01_F32": -8.4849999999999994, + "ACBRIDGE_CURRENT_ACTIVE_MEAN_02_F32": -8.5009999999999994, + "ACBRIDGE_CURRENT_ACTIVE_MEAN_03_F32": -8.5350000000000001, + "ACBRIDGE_CURRENT_AC_SUM_NOW_F64": -25.520999999999997, + "ACBRIDGE_VOLTAGE_MEAN_12_F32": 396.69999999999999, + "ACBRIDGE_VOLTAGE_MEAN_23_F32": 396.80000000000001, + "ACBRIDGE_VOLTAGE_MEAN_31_F32": 397.19999999999999, + "COMPONENTS_MODE_ENABLE_U16": 1.0, + "COMPONENTS_MODE_VISIBLE_U16": 1.0, + "COMPONENTS_TIME_STAMP_U64": 1611650230.0, + "Details": { + "Manufacturer": "Fronius", + "Model": "Smart Meter TS 65A-3", + "Serial": "1234567890" + }, + "GRID_FREQUENCY_MEAN_F32": 49.899999999999999, + "SMARTMETER_ENERGYACTIVE_ABSOLUT_MINUS_F64": 28233.0, + "SMARTMETER_ENERGYACTIVE_ABSOLUT_PLUS_F64": 5094426.0, + "SMARTMETER_ENERGYACTIVE_CONSUMED_SUM_F64": 28233.0, + "SMARTMETER_ENERGYACTIVE_PRODUCED_SUM_F64": 5094426.0, + "SMARTMETER_ENERGYREACTIVE_CONSUMED_SUM_F64": 5905771.0, + "SMARTMETER_ENERGYREACTIVE_PRODUCED_SUM_F64": 31815.0, + "SMARTMETER_FACTOR_POWER_01_F64": 0.64300000000000002, + "SMARTMETER_FACTOR_POWER_02_F64": 0.68000000000000005, + "SMARTMETER_FACTOR_POWER_03_F64": 0.66700000000000004, + "SMARTMETER_FACTOR_POWER_SUM_F64": 0.66300000000000003, + "SMARTMETER_POWERACTIVE_01_F64": 1229.7, + "SMARTMETER_POWERACTIVE_02_F64": 1298.0999999999999, + "SMARTMETER_POWERACTIVE_03_F64": 1281.5, + "SMARTMETER_POWERACTIVE_MEAN_01_F64": -1232.0566666666653, + "SMARTMETER_POWERACTIVE_MEAN_02_F64": -1296.0230000000006, + "SMARTMETER_POWERACTIVE_MEAN_03_F64": -1281.2506666666663, + "SMARTMETER_POWERACTIVE_MEAN_SUM_F64": 3809.4000000000001, + "SMARTMETER_POWERAPPARENT_01_F64": 1911.8, + "SMARTMETER_POWERAPPARENT_02_F64": 1910.0999999999999, + "SMARTMETER_POWERAPPARENT_03_F64": 1922.3, + "SMARTMETER_POWERAPPARENT_MEAN_01_F64": 1910.7656666666664, + "SMARTMETER_POWERAPPARENT_MEAN_02_F64": 1904.090666666666, + "SMARTMETER_POWERAPPARENT_MEAN_03_F64": 1923.9343333333331, + "SMARTMETER_POWERAPPARENT_MEAN_SUM_F64": 5744.3000000000002, + "SMARTMETER_POWERREACTIVE_01_F64": 1463.8, + "SMARTMETER_POWERREACTIVE_02_F64": 1401.0999999999999, + "SMARTMETER_POWERREACTIVE_03_F64": 1432.8, + "SMARTMETER_POWERREACTIVE_MEAN_SUM_F64": 4297.8999999999996, + "SMARTMETER_VALUE_LOCATION_U16": 3.0, + "SMARTMETER_VOLTAGE_01_F64": 229.30000000000001, + "SMARTMETER_VOLTAGE_02_F64": 228.80000000000001, + "SMARTMETER_VOLTAGE_03_F64": 229.40000000000001, + "SMARTMETER_VOLTAGE_MEAN_01_F64": 228.8716666666669, + "SMARTMETER_VOLTAGE_MEAN_02_F64": 228.90133333333321, + "SMARTMETER_VOLTAGE_MEAN_03_F64": 229.3593333333333 + } + } + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-01-26T08:37:11+00:00" + } +} From e640ff200623c968e11bb66b187f7dce4675e742 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 14 Oct 2025 15:08:00 +0200 Subject: [PATCH 2/9] add production counter and test --- .../fronius/inverter_production_counter.py | 17 ++++----- .../inverter_production_counter_test.py | 35 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter.py b/packages/modules/devices/fronius/fronius/inverter_production_counter.py index a0eb42ee4c..9f67f68324 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_counter.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_counter.py @@ -38,13 +38,12 @@ def update(self) -> None: session = req.get_http_session() variant = self.component_config.configuration.variant if variant == 0 or variant == 1: - counter_state = self.__update_variant_0_1(session) + inverter_state = self.__update_variant_0_1(session) elif variant == 2: - counter_state = self.__update_variant_2(session) + inverter_state = self.__update_variant_2(session) else: raise ValueError("Unbekannte Variante: "+str(variant)) - counter_state.imported, counter_state.exported = self.sim_counter.sim_count(counter_state.power) - self.store.set(counter_state) + self.store.set(inverter_state) def __update_variant_0_1(self, session: Session) -> InverterState: variant = self.component_config.configuration.variant @@ -78,10 +77,11 @@ def __update_variant_0_1(self, session: Session) -> InverterState: power = response_json_id["PowerReal_P_Sum"] * -1 voltages = [response_json_id["Voltage_AC_Phase_"+str(num)] for num in range(1, 4)] currents = [powers[i] / voltages[i] for i in range(0, 3)] - + _, exported = self.sim_counter.sim_count(power) return InverterState( currents=currents, - power=power + power=power, + exported=exported ) def __update_variant_2(self, session: Session) -> InverterState: @@ -102,10 +102,11 @@ def __update_variant_2(self, session: Session) -> InverterState: power = response_json_id["SMARTMETER_POWERACTIVE_MEAN_SUM_F64"] voltages = [response_json_id["SMARTMETER_VOLTAGE_0"+str(num)+"_F64"] for num in range(1, 4)] currents = [powers[i] / voltages[i] for i in range(0, 3)] - + _, exported = self.sim_counter.sim_count(power) return InverterState( currents=currents, - power=power + power=power, + exported=exported ) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py b/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py index 7b53383bcb..dabecdf50a 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py @@ -5,8 +5,8 @@ from dataclass_utils import dataclass_from_dict from helpermodules import compatibility -from modules.common.store._api import LoggingValueStore from modules.conftest import SAMPLE_IP +from modules.common.component_state import InverterState from modules.devices.fronius.fronius import inverter_production_counter from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCounterSetup from test_utils.mock_ramdisk import MockRamdisk @@ -18,29 +18,34 @@ def mock_ramdisk(monkeypatch): return MockRamdisk(monkeypatch) -def test_update_external_var2(monkeypatch, requests_mock: requests_mock.Mocker, mock_ramdisk, mock_simcount): +def test_production_counter(monkeypatch, requests_mock: requests_mock.mock): + mock_inverter_value_store = Mock() + monkeypatch.setattr(inverter_production_counter, "get_inverter_value_store", + Mock(return_value=mock_inverter_value_store)) + requests_mock.get(f"http://{SAMPLE_IP}/solar_api/v1/GetMeterRealtimeData.cgi", json=json_ext_var2) + mock_inverter_value_store = Mock() + monkeypatch.setattr(inverter_production_counter, "get_inverter_value_store", + Mock(return_value=mock_inverter_value_store)) + component_config = FroniusProductionCounterSetup() component_config.configuration.variant = 2 device_config = FroniusConfiguration() device_config.ip_address = SAMPLE_IP component_config.configuration.meter_id = 1 - inverter = inverter_production_counter.FroniusProductionCounter(component_config, device_config=dataclass_from_dict( + i = inverter_production_counter.FroniusProductionCounter(component_config, device_config=dataclass_from_dict( FroniusConfiguration, device_config), device_id=0) - inverter.initialize() + i.initialize() + + # execution + i.update() - mock = Mock(return_value=None) - monkeypatch.setattr(LoggingValueStore, "set", mock) - mock_simcount.return_value = 0, 0 - requests_mock.get( - "http://" + SAMPLE_IP + "/solar_api/v1/GetMeterRealtimeData.cgi", - json=json_ext_var2) + # evaluation + assert vars(mock_inverter_value_store.set.call_args[0][0]) == vars(SAMPLE_INVERTER_STATE) - inverter.update() - # mock.assert_called_once() - inverter_state = mock.call_args[0][0] - assert inverter_state.currents == [-5.373121093182142, -5.664436188811191, -5.585225225225224] - assert inverter_state.power == 3809.4 +SAMPLE_INVERTER_STATE = InverterState(power=3809.4, + currents=[-5.373121093182142, -5.664436188811191, -5.585225225225224], + exported=200) json_ext_var2 = { From cfd66715dc02654ba85b1c972ecc574eb61b72f6 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 15 Oct 2025 10:43:54 +0200 Subject: [PATCH 3/9] fix typo --- .../devices/fronius/fronius/inverter_production_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter.py b/packages/modules/devices/fronius/fronius/inverter_production_counter.py index 9f67f68324..c016557fbe 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_counter.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_counter.py @@ -110,4 +110,4 @@ def __update_variant_2(self, session: Session) -> InverterState: ) -component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCounter) +component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCounterSetup) From 8dd41774be0038a1874056068e6bc2f3ce05d495 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Wed, 15 Oct 2025 12:05:04 +0200 Subject: [PATCH 4/9] change component name --- packages/modules/devices/fronius/fronius/config.py | 10 +++++----- packages/modules/devices/fronius/fronius/device.py | 14 +++++++------- ...ion_counter.py => inverter_production_count.py} | 14 +++++++------- ...r_test.py => inverter_production_count_test.py} | 14 +++++++------- 4 files changed, 26 insertions(+), 26 deletions(-) rename packages/modules/devices/fronius/fronius/{inverter_production_counter.py => inverter_production_count.py} (92%) rename packages/modules/devices/fronius/fronius/{inverter_production_counter_test.py => inverter_production_count_test.py} (92%) diff --git a/packages/modules/devices/fronius/fronius/config.py b/packages/modules/devices/fronius/fronius/config.py index e11f327eb6..1bede58932 100644 --- a/packages/modules/devices/fronius/fronius/config.py +++ b/packages/modules/devices/fronius/fronius/config.py @@ -114,16 +114,16 @@ def __init__(self, super().__init__(name, type, id, configuration or FroniusSecondaryInverterConfiguration()) -class FroniusProductionCounterConfiguration: +class FroniusProductionCountConfiguration: def __init__(self, meter_id: int = 0, variant: int = 0): self.meter_id = meter_id self.variant = variant -class FroniusProductionCounterSetup(ComponentSetup[FroniusProductionCounterConfiguration]): +class FroniusProductionCountSetup(ComponentSetup[FroniusProductionCountConfiguration]): def __init__(self, name: str = "Fronius Erzeugerzähler", - type: str = "inverter_counter_production", + type: str = "inverter_production_count", id: int = 0, - configuration: FroniusProductionCounterConfiguration = None) -> None: - super().__init__(name, type, id, configuration or FroniusProductionCounterConfiguration()) + configuration: FroniusProductionCountConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FroniusProductionCountConfiguration()) diff --git a/packages/modules/devices/fronius/fronius/device.py b/packages/modules/devices/fronius/fronius/device.py index a17d79057b..3774876cf4 100644 --- a/packages/modules/devices/fronius/fronius/device.py +++ b/packages/modules/devices/fronius/fronius/device.py @@ -10,17 +10,17 @@ from modules.devices.fronius.fronius.bat import FroniusBat from modules.devices.fronius.fronius.config import (Fronius, FroniusBatSetup, FroniusSecondaryInverterSetup, FroniusSmCounterSetup, FroniusS0CounterSetup, - FroniusProductionCounterSetup, FroniusInverterSetup) + FroniusProductionCountSetup, FroniusInverterSetup) from modules.devices.fronius.fronius.counter_s0 import FroniusS0Counter from modules.devices.fronius.fronius.counter_sm import FroniusSmCounter from modules.devices.fronius.fronius.inverter import FroniusInverter from modules.devices.fronius.fronius.inverter_secondary import FroniusSecondaryInverter -from modules.devices.fronius.fronius.inverter_production_counter import FroniusProductionCounter +from modules.devices.fronius.fronius.inverter_production_count import FroniusProductionCount log = logging.getLogger(__name__) fronius_component_classes = Union[FroniusBat, FroniusSmCounter, FroniusS0Counter, - FroniusInverter, FroniusSecondaryInverter, FroniusProductionCounter] + FroniusInverter, FroniusSecondaryInverter, FroniusProductionCount] def create_device(device_config: Fronius): @@ -47,10 +47,10 @@ def create_inverter_secondary_component(component_config: FroniusSecondaryInvert return FroniusSecondaryInverter(component_config=component_config, device_id=device_config.id) - def create_inverter_counter_production_component(component_config: FroniusProductionCounterSetup): - return FroniusProductionCounter(component_config=component_config, - device_id=device_config.id, - device_config=device_config.configuration) + def create_inverter_counter_production_component(component_config: FroniusProductionCountSetup): + return FroniusProductionCount(component_config=component_config, + device_id=device_config.id, + device_config=device_config.configuration) def update_components(components: Iterable[fronius_component_classes]): inverter_response = None diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter.py b/packages/modules/devices/fronius/fronius/inverter_production_count.py similarity index 92% rename from packages/modules/devices/fronius/fronius/inverter_production_counter.py rename to packages/modules/devices/fronius/fronius/inverter_production_count.py index c016557fbe..a35d157b3d 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_counter.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_count.py @@ -5,14 +5,14 @@ from requests import Session from modules.common import req -from modules.common.abstract_device import AbstractCounter +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.fronius.fronius.config import FroniusConfiguration, MeterLocation -from modules.devices.fronius.fronius.config import FroniusProductionCounterSetup +from modules.devices.fronius.fronius.config import FroniusProductionCountSetup log = logging.getLogger(__name__) @@ -22,8 +22,8 @@ class KwargsDict(TypedDict): device_config: FroniusConfiguration -class FroniusProductionCounter(AbstractCounter): - def __init__(self, component_config: FroniusProductionCounterSetup, **kwargs: Any) -> None: +class FroniusProductionCount(AbstractInverter): + def __init__(self, component_config: FroniusProductionCountSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs @@ -71,7 +71,7 @@ def __update_variant_0_1(self, session: Session) -> InverterState: log.debug("Einbauort: "+str(meter_location)) powers = [response_json_id["PowerReal_P_Phase_"+str(num)] for num in range(1, 4)] - if meter_location != MeterLocation.external: + if meter_location == MeterLocation.grid: raise ValueError("Fehler: Dieser Zähler ist kein Erzeugerzähler.") else: power = response_json_id["PowerReal_P_Sum"] * -1 @@ -96,7 +96,7 @@ def __update_variant_2(self, session: Session) -> InverterState: log.debug("Einbauort: "+str(meter_location)) powers = [response_json_id["SMARTMETER_POWERACTIVE_MEAN_0"+str(num)+"_F64"] for num in range(1, 4)] - if meter_location != MeterLocation.external: + if meter_location == MeterLocation.grid: raise ValueError("Fehler: Dieser Zähler ist kein Erzeugerzähler.") else: power = response_json_id["SMARTMETER_POWERACTIVE_MEAN_SUM_F64"] @@ -110,4 +110,4 @@ def __update_variant_2(self, session: Session) -> InverterState: ) -component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCounterSetup) +component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCountSetup) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py b/packages/modules/devices/fronius/fronius/inverter_production_count_test.py similarity index 92% rename from packages/modules/devices/fronius/fronius/inverter_production_counter_test.py rename to packages/modules/devices/fronius/fronius/inverter_production_count_test.py index dabecdf50a..2ecc36ca09 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_counter_test.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_count_test.py @@ -7,8 +7,8 @@ from helpermodules import compatibility from modules.conftest import SAMPLE_IP from modules.common.component_state import InverterState -from modules.devices.fronius.fronius import inverter_production_counter -from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCounterSetup +from modules.devices.fronius.fronius import inverter_production_count +from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCountSetup from test_utils.mock_ramdisk import MockRamdisk @@ -18,21 +18,21 @@ def mock_ramdisk(monkeypatch): return MockRamdisk(monkeypatch) -def test_production_counter(monkeypatch, requests_mock: requests_mock.mock): +def test_production_count(monkeypatch, requests_mock: requests_mock.mock): mock_inverter_value_store = Mock() - monkeypatch.setattr(inverter_production_counter, "get_inverter_value_store", + monkeypatch.setattr(inverter_production_count, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) requests_mock.get(f"http://{SAMPLE_IP}/solar_api/v1/GetMeterRealtimeData.cgi", json=json_ext_var2) mock_inverter_value_store = Mock() - monkeypatch.setattr(inverter_production_counter, "get_inverter_value_store", + monkeypatch.setattr(inverter_production_count, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) - component_config = FroniusProductionCounterSetup() + component_config = FroniusProductionCountSetup() component_config.configuration.variant = 2 device_config = FroniusConfiguration() device_config.ip_address = SAMPLE_IP component_config.configuration.meter_id = 1 - i = inverter_production_counter.FroniusProductionCounter(component_config, device_config=dataclass_from_dict( + i = inverter_production_count.FroniusProductionCount(component_config, device_config=dataclass_from_dict( FroniusConfiguration, device_config), device_id=0) i.initialize() From 104ad3d80a908217efe5fd5b6692a8a107d0f943 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Fri, 17 Oct 2025 09:41:49 +0200 Subject: [PATCH 5/9] change structure of bat control --- packages/control/bat_all.py | 141 ++++++++++++++++++++++--------- packages/control/bat_all_test.py | 14 +-- 2 files changed, 108 insertions(+), 47 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index d1085b0d2b..af6c5db0f3 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -38,18 +38,38 @@ class BatConsiderationMode(Enum): MIN_SOC_BAT = "min_soc_bat_mode" +class BatPowerLimitCondition(Enum): + MANUAL = "manual" + VEHICLE_CHARGING = "vehicle_charging" + PRICE_LIMIT = "price_limit" + SCHEDULED = "scheduled" + + class BatPowerLimitMode(Enum): - NO_LIMIT = "no_limit" - LIMIT_STOP = "limit_stop" - LIMIT_TO_HOME_CONSUMPTION = "limit_to_home_consumption" + MODE_NO_DISCHARGE = "mode_no_discharge" + MODE_DISCHARGE_HOME_CONSUMPTION = "mode_discharge_home_consumption" + MODE_CHARGE_PV_PRODUCTION = "mode_charge_pv_production" + + +class BatChargeMode(Enum): + BAT_SELF_REGULATION = "bat_self_regulation" + BAT_USE_LIMIT = "bat_use_limit" + BAT_FORCE_CHARGE = "bat_force_charge" + BAT_FORCE_DISCHARGE = "bat_force_discharge" # in DE nicht erlaubt @dataclass class Config: configured: bool = field(default=False, metadata={"topic": "config/configured"}) - power_limit_mode: str = field(default=BatPowerLimitMode.NO_LIMIT.value, - metadata={"topic": "config/power_limit_mode"}) bat_control_permitted: bool = field(default=False, metadata={"topic": "config/bat_control_permitted"}) + bat_control_activated: bool = field(default=False, metadata={"topic": "config/bat_control_activated"}) + power_limit_mode: str = field(default=BatPowerLimitMode.MODE_NO_DISCHARGE.value, + metadata={"topic": "config/power_limit_mode"}) + bat_control_condition: str = field(default=BatPowerLimitCondition.VEHICLE_CHARGING.value, + metadata={"topic": "config/bat_control_condition"}) + manual_mode: str = field(default=BatChargeMode.BAT_SELF_REGULATION.value, + metadata={"topic": "config/manual_mode"}) + vehicle_mode_force_pv: bool = field(default=False, metadata={"topic": "config/vehicle_mode_force_pv"}) def config_factory() -> Config: @@ -289,44 +309,85 @@ def set_power_limit_controllable(self): else: self.data.get.power_limit_controllable = False + def get_charge_mode_vehicle_charge(self): + chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) + # Falls Fahrzeuge in aktivem Lademodus sind und Laden + # und Speicher entladen wird und kein EVU-Überschuss vorhanden ist + if (len(chargepoint_by_chargemodes) > 0 and + data.data.cp_all_data.data.get.power > 100 and + self.data.get.power <= 0 and + data.data.counter_all_data.get_evu_counter().data.get.power >= -100): + charge_mode = BatChargeMode.BAT_BLOCK_DISCHARGE + log.debug("Speicher-Entladung beschränken da Fahrzeuge laden.") + else: + charge_mode = BatChargeMode.BAT_SELF_REGULATION + control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] + control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] + control_range_center = control_range_high - (control_range_high - control_range_low) / 2 + if len(chargepoint_by_chargemodes) == 0: + log.debug("Speicher-Leistung nicht begrenzen, da keine Ladepunkte in einem aktiven Lademodus sind.") + elif data.data.cp_all_data.data.get.power <= 100: + log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt lädt.") + elif self.data.get.power > 0: + log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") + elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: + # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen + # werden. + log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") + else: + log.debug("Speicher-Leistung nicht begrenzen.") + # Wird nötig für PV Ladung in Speicher - Fahrzeugladung aus Netz + # charge_mode = BatChargeMode.BAT_FORCE_CHARGE + return charge_mode + def get_power_limit(self): - if self.data.config.bat_control_permitted is False: - self.data.set.power_limit = None + # Falls kein steuerbarer Speicher installiert ist, der Disclaimer nicht akzeptiert wurde + # oder die aktive Speichersteuerung deaktiviert wurde + if (self.data.get.power_limit_controllable is False or + self.data.config.bat_control_permitted is False or + self.data.config.bat_control_activated is False): + charge_mode = BatChargeMode.BAT_SELF_REGULATION + if self.data.get.power_limit_controllable is False: + log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") + elif self.data.config.bat_control_permitted is False: + log.debug("Speicher-Leistung nicht begrenzen, da der aktiven Speichersteuerung nicht zugestimmt wurde.") + elif self.data.get.power_limit_controllable is False: + log.debug("Speicher-Leistung nicht begrenzen, da aktive Speichersteuerung deaktiviert wurde.") else: - chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) - # Falls aktive Steuerung an und Fahrzeuge laden und kein Überschuss im System ist, - # dann Speicherleistung begrenzen. - if (self.data.config.power_limit_mode != BatPowerLimitMode.NO_LIMIT.value and - len(chargepoint_by_chargemodes) > 0 and - data.data.cp_all_data.data.get.power > 100 and - self.data.get.power_limit_controllable and - self.data.get.power <= 0 and - data.data.counter_all_data.get_evu_counter().data.get.power >= -100): - if self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_STOP.value: - self.data.set.power_limit = 0 - elif self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value: - self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 + charge_mode = BatChargeMode.BAT_SELF_REGULATION + if self.data.config.bat_control_condition == BatPowerLimitCondition.MANUAL: + log.debug("Aktive Speichersteuerung: Manueller Modus.") + charge_mode = BatChargeMode(self.data.config.manual_mode) + elif self.data.config.bat_control_condition == BatPowerLimitCondition.VEHICLE_CHARGING: + log.debug("Aktive Speichersteuerung: Wenn Fahrzeuge laden.") + charge_mode = self.get_charge_mode_vehicle_charge() + elif self.data.config.bat_control_condition == BatPowerLimitCondition.PRICE_LIMIT: + log.debug("Aktive Speichersteuerung: Strompreisbasiert.") + pass + elif self.data.config.bat_control_condition == BatPowerLimitCondition.SCHEDULED: + log.debug("Aktive Speichersteuerung: Vorhersagebasiertes Zielladen.") + pass + + # calculate power_limit + if charge_mode == BatChargeMode.BAT_SELF_REGULATION: + self.data.set.power_limit = None + elif charge_mode == BatChargeMode.BAT_USE_LIMIT: + if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_NO_DISCHARGE.value: + self.data.set.power_limit = 0 + log.debug("Speicher-Leistung begrenzen auf 0kW") + elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value: + self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 log.debug(f"Speicher-Leistung begrenzen auf {self.data.set.power_limit/1000}kW") - else: - self.data.set.power_limit = None - control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] - control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] - control_range_center = control_range_high - (control_range_high - control_range_low) / 2 - if len(chargepoint_by_chargemodes) == 0: - log.debug("Speicher-Leistung nicht begrenzen, " - "da keine Ladepunkte in einem Lademodus mit Netzbezug sind.") - elif data.data.cp_all_data.data.get.power <= 100: - log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt mit Netzbezug lädt.") - elif self.data.get.power_limit_controllable is False: - log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") - elif self.data.get.power > 0: - log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") - elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: - # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen - # werden. - log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") - else: - log.debug("Speicher-Leistung nicht begrenzen.") + elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value: + # PV-Ertrag und maximale Ladeleistung Speicher berücksichtigen + self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 + log.debug(f"Speicher in Höhe des PV-Ertrags laden: {self.data.set.power_limit/1000}kW") + elif charge_mode == BatChargeMode.BAT_FORCE_CHARGE: + # maximal konfigurierte Ladeleistung des Speichers setzen + pass + elif charge_mode == BatChargeMode.BAT_FORCE_DISCHARGE: + # das ist in Deutschland (noch) nicht erlaubt + pass remaining_power_limit = self.data.set.power_limit for bat_component in get_controllable_bat_components(): if self.data.set.power_limit is None: diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 2256629529..e62562b050 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -163,7 +163,7 @@ def default_chargepoint_factory() -> List[Chargepoint]: class PowerLimitParams: name: str expected_power_limit_bat: Optional[float] - power_limit_mode: str = BatPowerLimitMode.NO_LIMIT.value + power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE.value cps: List[Chargepoint] = field(default_factory=default_chargepoint_factory) power_limit_controllable: bool = True bat_power: float = -10 @@ -173,16 +173,16 @@ class PowerLimitParams: cases = [ PowerLimitParams("keine Begrenzung", None), PowerLimitParams("Begrenzung immer, keine LP im Sofortladen", None, cps=[], - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer, Speicher nicht regelbar", None, power_limit_controllable=False, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer, Speicher lädt", None, bat_power=100, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer,Einspeisung", None, evu_power=-110, - power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), - PowerLimitParams("Begrenzung immer", 0, power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + PowerLimitParams("Begrenzung immer", 0, power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung Hausverbrauch", -456, - power_limit_mode=BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value), + power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value), ] From d532aeba46814bf33d5bf9f957afd5e739e918d8 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Fri, 17 Oct 2025 11:02:19 +0200 Subject: [PATCH 6/9] adjust test --- packages/control/bat_all.py | 10 +++++----- packages/control/bat_all_test.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index af6c5db0f3..dcf3482fc9 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -317,7 +317,7 @@ def get_charge_mode_vehicle_charge(self): data.data.cp_all_data.data.get.power > 100 and self.data.get.power <= 0 and data.data.counter_all_data.get_evu_counter().data.get.power >= -100): - charge_mode = BatChargeMode.BAT_BLOCK_DISCHARGE + charge_mode = BatChargeMode.BAT_USE_LIMIT log.debug("Speicher-Entladung beschränken da Fahrzeuge laden.") else: charge_mode = BatChargeMode.BAT_SELF_REGULATION @@ -355,16 +355,16 @@ def get_power_limit(self): log.debug("Speicher-Leistung nicht begrenzen, da aktive Speichersteuerung deaktiviert wurde.") else: charge_mode = BatChargeMode.BAT_SELF_REGULATION - if self.data.config.bat_control_condition == BatPowerLimitCondition.MANUAL: + if self.data.config.bat_control_condition == BatPowerLimitCondition.MANUAL.value: log.debug("Aktive Speichersteuerung: Manueller Modus.") charge_mode = BatChargeMode(self.data.config.manual_mode) - elif self.data.config.bat_control_condition == BatPowerLimitCondition.VEHICLE_CHARGING: + elif self.data.config.bat_control_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value: log.debug("Aktive Speichersteuerung: Wenn Fahrzeuge laden.") charge_mode = self.get_charge_mode_vehicle_charge() - elif self.data.config.bat_control_condition == BatPowerLimitCondition.PRICE_LIMIT: + elif self.data.config.bat_control_condition == BatPowerLimitCondition.PRICE_LIMIT.value: log.debug("Aktive Speichersteuerung: Strompreisbasiert.") pass - elif self.data.config.bat_control_condition == BatPowerLimitCondition.SCHEDULED: + elif self.data.config.bat_control_condition == BatPowerLimitCondition.SCHEDULED.value: log.debug("Aktive Speichersteuerung: Vorhersagebasiertes Zielladen.") pass diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index e62562b050..c0a73d63e9 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -6,7 +6,7 @@ from control import bat_all from control.bat import Bat -from control.bat_all import BatAll, BatPowerLimitMode +from control.bat_all import BatAll, BatPowerLimitMode, BatPowerLimitCondition from control import data from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_all import AllChargepointData, AllChargepoints, AllGet @@ -164,18 +164,21 @@ class PowerLimitParams: name: str expected_power_limit_bat: Optional[float] power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE.value + power_limit_condition: str = BatPowerLimitCondition.VEHICLE_CHARGING.value cps: List[Chargepoint] = field(default_factory=default_chargepoint_factory) power_limit_controllable: bool = True bat_power: float = -10 evu_power: float = 200 + bat_control_activated: bool = True cases = [ - PowerLimitParams("keine Begrenzung", None), - PowerLimitParams("Begrenzung immer, keine LP im Sofortladen", None, cps=[], - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer, Speicher nicht regelbar", None, power_limit_controllable=False, power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + PowerLimitParams("Begrenzung immer, Speichersteuerung deaktiviert", None, bat_control_activated=False, + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + PowerLimitParams("Begrenzung immer, keine LP im Sofortladen", None, cps=[], + power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer, Speicher lädt", None, bat_power=100, power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), PowerLimitParams("Begrenzung immer,Einspeisung", None, evu_power=-110, @@ -190,7 +193,9 @@ class PowerLimitParams: def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): b_all = BatAll() b_all.data.config.bat_control_permitted = True + b_all.data.config.bat_control_activated = params.bat_control_activated b_all.data.config.power_limit_mode = params.power_limit_mode + b_all.data.config.power_limit_condition = params.power_limit_condition b_all.data.get.power_limit_controllable = params.power_limit_controllable b_all.data.get.power = params.bat_power data.data.counter_all_data = hierarchy_standard() From 7198a836fa727c3f975b7c40553cafda50d361a2 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Fri, 24 Oct 2025 07:57:45 +0200 Subject: [PATCH 7/9] rename component/ remove control --- packages/control/bat_all.py | 141 +++++------------- packages/control/bat_all_test.py | 25 ++-- .../modules/devices/fronius/fronius/config.py | 10 +- .../modules/devices/fronius/fronius/device.py | 12 +- ..._count.py => inverter_production_meter.py} | 8 +- ...t.py => inverter_production_meter_test.py} | 12 +- python_test.sh | 1 + 7 files changed, 72 insertions(+), 137 deletions(-) rename packages/modules/devices/fronius/fronius/{inverter_production_count.py => inverter_production_meter.py} (96%) rename packages/modules/devices/fronius/fronius/{inverter_production_count_test.py => inverter_production_meter_test.py} (94%) create mode 100755 python_test.sh diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index dcf3482fc9..d1085b0d2b 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -38,38 +38,18 @@ class BatConsiderationMode(Enum): MIN_SOC_BAT = "min_soc_bat_mode" -class BatPowerLimitCondition(Enum): - MANUAL = "manual" - VEHICLE_CHARGING = "vehicle_charging" - PRICE_LIMIT = "price_limit" - SCHEDULED = "scheduled" - - class BatPowerLimitMode(Enum): - MODE_NO_DISCHARGE = "mode_no_discharge" - MODE_DISCHARGE_HOME_CONSUMPTION = "mode_discharge_home_consumption" - MODE_CHARGE_PV_PRODUCTION = "mode_charge_pv_production" - - -class BatChargeMode(Enum): - BAT_SELF_REGULATION = "bat_self_regulation" - BAT_USE_LIMIT = "bat_use_limit" - BAT_FORCE_CHARGE = "bat_force_charge" - BAT_FORCE_DISCHARGE = "bat_force_discharge" # in DE nicht erlaubt + NO_LIMIT = "no_limit" + LIMIT_STOP = "limit_stop" + LIMIT_TO_HOME_CONSUMPTION = "limit_to_home_consumption" @dataclass class Config: configured: bool = field(default=False, metadata={"topic": "config/configured"}) - bat_control_permitted: bool = field(default=False, metadata={"topic": "config/bat_control_permitted"}) - bat_control_activated: bool = field(default=False, metadata={"topic": "config/bat_control_activated"}) - power_limit_mode: str = field(default=BatPowerLimitMode.MODE_NO_DISCHARGE.value, + power_limit_mode: str = field(default=BatPowerLimitMode.NO_LIMIT.value, metadata={"topic": "config/power_limit_mode"}) - bat_control_condition: str = field(default=BatPowerLimitCondition.VEHICLE_CHARGING.value, - metadata={"topic": "config/bat_control_condition"}) - manual_mode: str = field(default=BatChargeMode.BAT_SELF_REGULATION.value, - metadata={"topic": "config/manual_mode"}) - vehicle_mode_force_pv: bool = field(default=False, metadata={"topic": "config/vehicle_mode_force_pv"}) + bat_control_permitted: bool = field(default=False, metadata={"topic": "config/bat_control_permitted"}) def config_factory() -> Config: @@ -309,85 +289,44 @@ def set_power_limit_controllable(self): else: self.data.get.power_limit_controllable = False - def get_charge_mode_vehicle_charge(self): - chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) - # Falls Fahrzeuge in aktivem Lademodus sind und Laden - # und Speicher entladen wird und kein EVU-Überschuss vorhanden ist - if (len(chargepoint_by_chargemodes) > 0 and - data.data.cp_all_data.data.get.power > 100 and - self.data.get.power <= 0 and - data.data.counter_all_data.get_evu_counter().data.get.power >= -100): - charge_mode = BatChargeMode.BAT_USE_LIMIT - log.debug("Speicher-Entladung beschränken da Fahrzeuge laden.") - else: - charge_mode = BatChargeMode.BAT_SELF_REGULATION - control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] - control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] - control_range_center = control_range_high - (control_range_high - control_range_low) / 2 - if len(chargepoint_by_chargemodes) == 0: - log.debug("Speicher-Leistung nicht begrenzen, da keine Ladepunkte in einem aktiven Lademodus sind.") - elif data.data.cp_all_data.data.get.power <= 100: - log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt lädt.") - elif self.data.get.power > 0: - log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") - elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: - # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen - # werden. - log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") - else: - log.debug("Speicher-Leistung nicht begrenzen.") - # Wird nötig für PV Ladung in Speicher - Fahrzeugladung aus Netz - # charge_mode = BatChargeMode.BAT_FORCE_CHARGE - return charge_mode - def get_power_limit(self): - # Falls kein steuerbarer Speicher installiert ist, der Disclaimer nicht akzeptiert wurde - # oder die aktive Speichersteuerung deaktiviert wurde - if (self.data.get.power_limit_controllable is False or - self.data.config.bat_control_permitted is False or - self.data.config.bat_control_activated is False): - charge_mode = BatChargeMode.BAT_SELF_REGULATION - if self.data.get.power_limit_controllable is False: - log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") - elif self.data.config.bat_control_permitted is False: - log.debug("Speicher-Leistung nicht begrenzen, da der aktiven Speichersteuerung nicht zugestimmt wurde.") - elif self.data.get.power_limit_controllable is False: - log.debug("Speicher-Leistung nicht begrenzen, da aktive Speichersteuerung deaktiviert wurde.") - else: - charge_mode = BatChargeMode.BAT_SELF_REGULATION - if self.data.config.bat_control_condition == BatPowerLimitCondition.MANUAL.value: - log.debug("Aktive Speichersteuerung: Manueller Modus.") - charge_mode = BatChargeMode(self.data.config.manual_mode) - elif self.data.config.bat_control_condition == BatPowerLimitCondition.VEHICLE_CHARGING.value: - log.debug("Aktive Speichersteuerung: Wenn Fahrzeuge laden.") - charge_mode = self.get_charge_mode_vehicle_charge() - elif self.data.config.bat_control_condition == BatPowerLimitCondition.PRICE_LIMIT.value: - log.debug("Aktive Speichersteuerung: Strompreisbasiert.") - pass - elif self.data.config.bat_control_condition == BatPowerLimitCondition.SCHEDULED.value: - log.debug("Aktive Speichersteuerung: Vorhersagebasiertes Zielladen.") - pass - - # calculate power_limit - if charge_mode == BatChargeMode.BAT_SELF_REGULATION: + if self.data.config.bat_control_permitted is False: self.data.set.power_limit = None - elif charge_mode == BatChargeMode.BAT_USE_LIMIT: - if self.data.config.power_limit_mode == BatPowerLimitMode.MODE_NO_DISCHARGE.value: - self.data.set.power_limit = 0 - log.debug("Speicher-Leistung begrenzen auf 0kW") - elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value: - self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 + else: + chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) + # Falls aktive Steuerung an und Fahrzeuge laden und kein Überschuss im System ist, + # dann Speicherleistung begrenzen. + if (self.data.config.power_limit_mode != BatPowerLimitMode.NO_LIMIT.value and + len(chargepoint_by_chargemodes) > 0 and + data.data.cp_all_data.data.get.power > 100 and + self.data.get.power_limit_controllable and + self.data.get.power <= 0 and + data.data.counter_all_data.get_evu_counter().data.get.power >= -100): + if self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_STOP.value: + self.data.set.power_limit = 0 + elif self.data.config.power_limit_mode == BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value: + self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 log.debug(f"Speicher-Leistung begrenzen auf {self.data.set.power_limit/1000}kW") - elif self.data.config.power_limit_mode == BatPowerLimitMode.MODE_CHARGE_PV_PRODUCTION.value: - # PV-Ertrag und maximale Ladeleistung Speicher berücksichtigen - self.data.set.power_limit = data.data.counter_all_data.data.set.home_consumption * -1 - log.debug(f"Speicher in Höhe des PV-Ertrags laden: {self.data.set.power_limit/1000}kW") - elif charge_mode == BatChargeMode.BAT_FORCE_CHARGE: - # maximal konfigurierte Ladeleistung des Speichers setzen - pass - elif charge_mode == BatChargeMode.BAT_FORCE_DISCHARGE: - # das ist in Deutschland (noch) nicht erlaubt - pass + else: + self.data.set.power_limit = None + control_range_low = data.data.general_data.data.chargemode_config.pv_charging.control_range[0] + control_range_high = data.data.general_data.data.chargemode_config.pv_charging.control_range[1] + control_range_center = control_range_high - (control_range_high - control_range_low) / 2 + if len(chargepoint_by_chargemodes) == 0: + log.debug("Speicher-Leistung nicht begrenzen, " + "da keine Ladepunkte in einem Lademodus mit Netzbezug sind.") + elif data.data.cp_all_data.data.get.power <= 100: + log.debug("Speicher-Leistung nicht begrenzen, da kein Ladepunkt mit Netzbezug lädt.") + elif self.data.get.power_limit_controllable is False: + log.debug("Speicher-Leistung nicht begrenzen, da keine regelbaren Speicher vorhanden sind.") + elif self.data.get.power > 0: + log.debug("Speicher-Leistung nicht begrenzen, da kein Speicher entladen wird.") + elif data.data.counter_all_data.get_evu_counter().data.get.power < control_range_center + 80: + # Wenn der Regelbereich zB auf Bezug steht, darf auch die Leistung des Regelbereichs entladen + # werden. + log.debug("Speicher-Leistung nicht begrenzen, da EVU-Überschuss vorhanden ist.") + else: + log.debug("Speicher-Leistung nicht begrenzen.") remaining_power_limit = self.data.set.power_limit for bat_component in get_controllable_bat_components(): if self.data.set.power_limit is None: diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index c0a73d63e9..2256629529 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -6,7 +6,7 @@ from control import bat_all from control.bat import Bat -from control.bat_all import BatAll, BatPowerLimitMode, BatPowerLimitCondition +from control.bat_all import BatAll, BatPowerLimitMode from control import data from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_all import AllChargepointData, AllChargepoints, AllGet @@ -163,29 +163,26 @@ def default_chargepoint_factory() -> List[Chargepoint]: class PowerLimitParams: name: str expected_power_limit_bat: Optional[float] - power_limit_mode: str = BatPowerLimitMode.MODE_NO_DISCHARGE.value - power_limit_condition: str = BatPowerLimitCondition.VEHICLE_CHARGING.value + power_limit_mode: str = BatPowerLimitMode.NO_LIMIT.value cps: List[Chargepoint] = field(default_factory=default_chargepoint_factory) power_limit_controllable: bool = True bat_power: float = -10 evu_power: float = 200 - bat_control_activated: bool = True cases = [ - PowerLimitParams("Begrenzung immer, Speicher nicht regelbar", None, power_limit_controllable=False, - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), - PowerLimitParams("Begrenzung immer, Speichersteuerung deaktiviert", None, bat_control_activated=False, - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + PowerLimitParams("keine Begrenzung", None), PowerLimitParams("Begrenzung immer, keine LP im Sofortladen", None, cps=[], - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + PowerLimitParams("Begrenzung immer, Speicher nicht regelbar", None, power_limit_controllable=False, + power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), PowerLimitParams("Begrenzung immer, Speicher lädt", None, bat_power=100, - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), PowerLimitParams("Begrenzung immer,Einspeisung", None, evu_power=-110, - power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), - PowerLimitParams("Begrenzung immer", 0, power_limit_mode=BatPowerLimitMode.MODE_NO_DISCHARGE.value), + power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), + PowerLimitParams("Begrenzung immer", 0, power_limit_mode=BatPowerLimitMode.LIMIT_STOP.value), PowerLimitParams("Begrenzung Hausverbrauch", -456, - power_limit_mode=BatPowerLimitMode.MODE_DISCHARGE_HOME_CONSUMPTION.value), + power_limit_mode=BatPowerLimitMode.LIMIT_TO_HOME_CONSUMPTION.value), ] @@ -193,9 +190,7 @@ class PowerLimitParams: def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): b_all = BatAll() b_all.data.config.bat_control_permitted = True - b_all.data.config.bat_control_activated = params.bat_control_activated b_all.data.config.power_limit_mode = params.power_limit_mode - b_all.data.config.power_limit_condition = params.power_limit_condition b_all.data.get.power_limit_controllable = params.power_limit_controllable b_all.data.get.power = params.bat_power data.data.counter_all_data = hierarchy_standard() diff --git a/packages/modules/devices/fronius/fronius/config.py b/packages/modules/devices/fronius/fronius/config.py index 1bede58932..c8e4f1a500 100644 --- a/packages/modules/devices/fronius/fronius/config.py +++ b/packages/modules/devices/fronius/fronius/config.py @@ -114,16 +114,16 @@ def __init__(self, super().__init__(name, type, id, configuration or FroniusSecondaryInverterConfiguration()) -class FroniusProductionCountConfiguration: +class FroniusProductionMeterConfiguration: def __init__(self, meter_id: int = 0, variant: int = 0): self.meter_id = meter_id self.variant = variant -class FroniusProductionCountSetup(ComponentSetup[FroniusProductionCountConfiguration]): +class FroniusProductionMeterSetup(ComponentSetup[FroniusProductionMeterConfiguration]): def __init__(self, name: str = "Fronius Erzeugerzähler", - type: str = "inverter_production_count", + type: str = "inverter_production_meter", id: int = 0, - configuration: FroniusProductionCountConfiguration = None) -> None: - super().__init__(name, type, id, configuration or FroniusProductionCountConfiguration()) + configuration: FroniusProductionMeterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or FroniusProductionMeterConfiguration()) diff --git a/packages/modules/devices/fronius/fronius/device.py b/packages/modules/devices/fronius/fronius/device.py index 3774876cf4..302ee9dc16 100644 --- a/packages/modules/devices/fronius/fronius/device.py +++ b/packages/modules/devices/fronius/fronius/device.py @@ -10,17 +10,17 @@ from modules.devices.fronius.fronius.bat import FroniusBat from modules.devices.fronius.fronius.config import (Fronius, FroniusBatSetup, FroniusSecondaryInverterSetup, FroniusSmCounterSetup, FroniusS0CounterSetup, - FroniusProductionCountSetup, FroniusInverterSetup) + FroniusProductionMeterSetup, FroniusInverterSetup) from modules.devices.fronius.fronius.counter_s0 import FroniusS0Counter from modules.devices.fronius.fronius.counter_sm import FroniusSmCounter from modules.devices.fronius.fronius.inverter import FroniusInverter from modules.devices.fronius.fronius.inverter_secondary import FroniusSecondaryInverter -from modules.devices.fronius.fronius.inverter_production_count import FroniusProductionCount +from modules.devices.fronius.fronius.inverter_production_meter import FroniusProductionMeter log = logging.getLogger(__name__) fronius_component_classes = Union[FroniusBat, FroniusSmCounter, FroniusS0Counter, - FroniusInverter, FroniusSecondaryInverter, FroniusProductionCount] + FroniusInverter, FroniusSecondaryInverter, FroniusProductionMeter] def create_device(device_config: Fronius): @@ -47,8 +47,8 @@ def create_inverter_secondary_component(component_config: FroniusSecondaryInvert return FroniusSecondaryInverter(component_config=component_config, device_id=device_config.id) - def create_inverter_counter_production_component(component_config: FroniusProductionCountSetup): - return FroniusProductionCount(component_config=component_config, + def create_inverter_production_meter_component(component_config: FroniusProductionMeterSetup): + return FroniusProductionMeter(component_config=component_config, device_id=device_config.id, device_config=device_config.configuration) @@ -86,7 +86,7 @@ def update_components(components: Iterable[fronius_component_classes]): counter_s0=create_counter_s0_component, inverter=create_inverter_component, inverter_secondary=create_inverter_secondary_component, - inverter_counter_production=create_inverter_counter_production_component, + inverter_counter_production=create_inverter_production_meter_component, ), component_updater=MultiComponentUpdater(update_components) ) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_count.py b/packages/modules/devices/fronius/fronius/inverter_production_meter.py similarity index 96% rename from packages/modules/devices/fronius/fronius/inverter_production_count.py rename to packages/modules/devices/fronius/fronius/inverter_production_meter.py index a35d157b3d..a806eb1efe 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_count.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_meter.py @@ -12,7 +12,7 @@ from modules.common.simcount import SimCounter from modules.common.store import get_inverter_value_store from modules.devices.fronius.fronius.config import FroniusConfiguration, MeterLocation -from modules.devices.fronius.fronius.config import FroniusProductionCountSetup +from modules.devices.fronius.fronius.config import FroniusProductionMeterSetup log = logging.getLogger(__name__) @@ -22,8 +22,8 @@ class KwargsDict(TypedDict): device_config: FroniusConfiguration -class FroniusProductionCount(AbstractInverter): - def __init__(self, component_config: FroniusProductionCountSetup, **kwargs: Any) -> None: +class FroniusProductionMeter(AbstractInverter): + def __init__(self, component_config: FroniusProductionMeterSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs @@ -110,4 +110,4 @@ def __update_variant_2(self, session: Session) -> InverterState: ) -component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionCountSetup) +component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionMeterSetup) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_count_test.py b/packages/modules/devices/fronius/fronius/inverter_production_meter_test.py similarity index 94% rename from packages/modules/devices/fronius/fronius/inverter_production_count_test.py rename to packages/modules/devices/fronius/fronius/inverter_production_meter_test.py index 2ecc36ca09..8fddaafb65 100644 --- a/packages/modules/devices/fronius/fronius/inverter_production_count_test.py +++ b/packages/modules/devices/fronius/fronius/inverter_production_meter_test.py @@ -7,8 +7,8 @@ from helpermodules import compatibility from modules.conftest import SAMPLE_IP from modules.common.component_state import InverterState -from modules.devices.fronius.fronius import inverter_production_count -from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCountSetup +from modules.devices.fronius.fronius import inverter_production_meter +from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionMeterSetup from test_utils.mock_ramdisk import MockRamdisk @@ -20,19 +20,19 @@ def mock_ramdisk(monkeypatch): def test_production_count(monkeypatch, requests_mock: requests_mock.mock): mock_inverter_value_store = Mock() - monkeypatch.setattr(inverter_production_count, "get_inverter_value_store", + monkeypatch.setattr(inverter_production_meter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) requests_mock.get(f"http://{SAMPLE_IP}/solar_api/v1/GetMeterRealtimeData.cgi", json=json_ext_var2) mock_inverter_value_store = Mock() - monkeypatch.setattr(inverter_production_count, "get_inverter_value_store", + monkeypatch.setattr(inverter_production_meter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) - component_config = FroniusProductionCountSetup() + component_config = FroniusProductionMeterSetup() component_config.configuration.variant = 2 device_config = FroniusConfiguration() device_config.ip_address = SAMPLE_IP component_config.configuration.meter_id = 1 - i = inverter_production_count.FroniusProductionCount(component_config, device_config=dataclass_from_dict( + i = inverter_production_meter.FroniusProductionMeter(component_config, device_config=dataclass_from_dict( FroniusConfiguration, device_config), device_id=0) i.initialize() diff --git a/python_test.sh b/python_test.sh new file mode 100755 index 0000000000..0a3fd62db5 --- /dev/null +++ b/python_test.sh @@ -0,0 +1 @@ +PYTHONPATH="/var/www/html/openWB/packages" python3.9 -m pytest -vv -o log_cli=true --log-cli-level=DEBUG; python3.9 -m flake8 --max-line-length 120 /var/www/html/openWB/packages --exclude='/var/www/html/openWB/packages/modules/display_themes','/var/www/html/openWB/packages/modules/web_themes' From 1b1379032a57fc5f8fbb35db3abc881e513d51ad Mon Sep 17 00:00:00 2001 From: ndrsnhs <156670705+ndrsnhs@users.noreply.github.com> Date: Fri, 24 Oct 2025 07:59:10 +0200 Subject: [PATCH 8/9] delete python_test.sh --- python_test.sh | 1 - 1 file changed, 1 deletion(-) delete mode 100755 python_test.sh diff --git a/python_test.sh b/python_test.sh deleted file mode 100755 index 0a3fd62db5..0000000000 --- a/python_test.sh +++ /dev/null @@ -1 +0,0 @@ -PYTHONPATH="/var/www/html/openWB/packages" python3.9 -m pytest -vv -o log_cli=true --log-cli-level=DEBUG; python3.9 -m flake8 --max-line-length 120 /var/www/html/openWB/packages --exclude='/var/www/html/openWB/packages/modules/display_themes','/var/www/html/openWB/packages/modules/web_themes' From 832ad5c5293080f2c4bf009e096d164565da729e Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Fri, 24 Oct 2025 13:03:03 +0200 Subject: [PATCH 9/9] fix typo --- packages/modules/devices/fronius/fronius/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/devices/fronius/fronius/device.py b/packages/modules/devices/fronius/fronius/device.py index 302ee9dc16..842c213cfb 100644 --- a/packages/modules/devices/fronius/fronius/device.py +++ b/packages/modules/devices/fronius/fronius/device.py @@ -86,7 +86,7 @@ def update_components(components: Iterable[fronius_component_classes]): counter_s0=create_counter_s0_component, inverter=create_inverter_component, inverter_secondary=create_inverter_secondary_component, - inverter_counter_production=create_inverter_production_meter_component, + inverter_production_meter=create_inverter_production_meter_component, ), component_updater=MultiComponentUpdater(update_components) )