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 FroniusProductionCountConfiguration:
def __init__(self, meter_id: int = 0, variant: int = 0):
self.meter_id = meter_id
self.variant = variant


class FroniusProductionCountSetup(ComponentSetup[FroniusProductionCountConfiguration]):
def __init__(self,
name: str = "Fronius Erzeugerzähler",
type: str = "inverter_production_count",
id: int = 0,
configuration: FroniusProductionCountConfiguration = None) -> None:
super().__init__(name, type, id, configuration or FroniusProductionCountConfiguration())
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)
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_count import FroniusProductionCount

log = logging.getLogger(__name__)

fronius_component_classes = Union[FroniusBat, FroniusSmCounter,
FroniusS0Counter, FroniusInverter, FroniusSecondaryInverter]
fronius_component_classes = Union[FroniusBat, FroniusSmCounter, FroniusS0Counter,
FroniusInverter, FroniusSecondaryInverter, FroniusProductionCount]


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_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
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_counter_production=create_inverter_counter_production_component,
),
component_updater=MultiComponentUpdater(update_components)
)
Expand Down
113 changes: 113 additions & 0 deletions packages/modules/devices/fronius/fronius/inverter_production_count.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 FroniusProductionCountSetup

log = logging.getLogger(__name__)


class KwargsDict(TypedDict):
device_id: int
device_config: FroniusConfiguration


class FroniusProductionCount(AbstractInverter):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warum bist Du von der üblichen Benennung mit Counter abgewichen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die farbliche Darstellung in der Nutzeroberfläche unter "Geräte und Komponenten" hängt von der Benennung ab. Die Funktion getComponentTypeIcon(type) in der HardwareInstallation.vue führt eine regex Filterung durch.
In der if-else Verzweigung wird zurerst nach counter gefiltert. Dadurch wird die Komponente als Zähler (rot) statt als WR (grün) dargestellt, was hier nicht korrekt ist.
Will man die Logik nicht aufbohren bleibt nur die Umbenennung. Ich habe alles angepasst damit es in UI und Backend konsistent ist.
Besser nur den type anpassen?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wie wäre es mit "Meter" anstatt "Counter"? Wäre sogar vom Begriff her richtig. Counter ist eher "Denglisch".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es gibt auch noch diese Funktion

def type_to_topic_mapping(component_type: str) -> str:

Damit wird aus dem Komponententyp, der weitestgehend indiviudell benannt werden kann, zugeordnet, ob es ein Zähler, WR, Speicher, ... ist. Die Regelung muss ja auch wissen, ist es als Zähler oder WR zu behandeln. Deshalb hatten wir damals festgelegt, dass immer counter, bat oder inverter im Komponententyp vorkommen muss.

def __init__(self, component_config: FroniusProductionCountSetup, **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=FroniusProductionCountSetup)
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_count
from modules.devices.fronius.fronius.config import FroniusConfiguration, FroniusProductionCountSetup
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_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_count, "get_inverter_value_store",
Mock(return_value=mock_inverter_value_store))

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_count.FroniusProductionCount(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"
}
}