diff --git a/packages/modules/devices/fronius/fronius/config.py b/packages/modules/devices/fronius/fronius/config.py index 4f6953ccb3..c8e4f1a500 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 FroniusProductionMeterConfiguration: + def __init__(self, meter_id: int = 0, variant: int = 0): + self.meter_id = meter_id + self.variant = variant + + +class FroniusProductionMeterSetup(ComponentSetup[FroniusProductionMeterConfiguration]): + def __init__(self, + name: str = "Fronius Erzeugerzähler", + type: str = "inverter_production_meter", + id: int = 0, + 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 a8eb412a7b..842c213cfb 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) + 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_meter import FroniusProductionMeter log = logging.getLogger(__name__) -fronius_component_classes = Union[FroniusBat, FroniusSmCounter, - FroniusS0Counter, FroniusInverter, FroniusSecondaryInverter] +fronius_component_classes = Union[FroniusBat, FroniusSmCounter, FroniusS0Counter, + FroniusInverter, FroniusSecondaryInverter, FroniusProductionMeter] 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_production_meter_component(component_config: FroniusProductionMeterSetup): + return FroniusProductionMeter(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_production_meter=create_inverter_production_meter_component, ), component_updater=MultiComponentUpdater(update_components) ) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_meter.py b/packages/modules/devices/fronius/fronius/inverter_production_meter.py new file mode 100644 index 0000000000..a806eb1efe --- /dev/null +++ b/packages/modules/devices/fronius/fronius/inverter_production_meter.py @@ -0,0 +1,113 @@ +#!/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 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 FroniusProductionMeterSetup + +log = logging.getLogger(__name__) + + +class KwargsDict(TypedDict): + device_id: int + device_config: FroniusConfiguration + + +class FroniusProductionMeter(AbstractInverter): + def __init__(self, component_config: FroniusProductionMeterSetup, **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: + inverter_state = self.__update_variant_0_1(session) + elif variant == 2: + inverter_state = self.__update_variant_2(session) + else: + raise ValueError("Unbekannte Variante: "+str(variant)) + self.store.set(inverter_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.grid: + 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)] + _, exported = self.sim_counter.sim_count(power) + return InverterState( + currents=currents, + power=power, + exported=exported + ) + + 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.grid: + 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)] + _, exported = self.sim_counter.sim_count(power) + return InverterState( + currents=currents, + power=power, + exported=exported + ) + + +component_descriptor = ComponentDescriptor(configuration_factory=FroniusProductionMeterSetup) diff --git a/packages/modules/devices/fronius/fronius/inverter_production_meter_test.py b/packages/modules/devices/fronius/fronius/inverter_production_meter_test.py new file mode 100644 index 0000000000..8fddaafb65 --- /dev/null +++ b/packages/modules/devices/fronius/fronius/inverter_production_meter_test.py @@ -0,0 +1,121 @@ +from unittest.mock import Mock + +import pytest +import requests_mock + +from dataclass_utils import dataclass_from_dict +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_meter +from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionMeterSetup +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_production_count(monkeypatch, requests_mock: requests_mock.mock): + mock_inverter_value_store = Mock() + 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_meter, "get_inverter_value_store", + Mock(return_value=mock_inverter_value_store)) + + 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_meter.FroniusProductionMeter(component_config, device_config=dataclass_from_dict( + FroniusConfiguration, device_config), device_id=0) + i.initialize() + + # execution + i.update() + + # evaluation + assert vars(mock_inverter_value_store.set.call_args[0][0]) == vars(SAMPLE_INVERTER_STATE) + + +SAMPLE_INVERTER_STATE = InverterState(power=3809.4, + currents=[-5.373121093182142, -5.664436188811191, -5.585225225225224], + exported=200) + + +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" + } +}