Skip to content
15 changes: 15 additions & 0 deletions packages/modules/devices/fronius/fronius/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
13 changes: 10 additions & 3 deletions packages/modules/devices/fronius/fronius/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)
)
Expand Down
113 changes: 113 additions & 0 deletions packages/modules/devices/fronius/fronius/inverter_production_meter.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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"
}
}