diff --git a/packages/modules/devices/fronius/fronius_sunspec/inverter.py b/packages/modules/devices/fronius/fronius_sunspec/inverter.py index a7b37d1dcb..6856ce470f 100644 --- a/packages/modules/devices/fronius/fronius_sunspec/inverter.py +++ b/packages/modules/devices/fronius/fronius_sunspec/inverter.py @@ -62,7 +62,7 @@ def update(self) -> None: exported = self.__tcp_client.read_holding_registers(40102, ModbusDataType.FLOAT_32, unit=unit) # WH - self.peak_filter.check_values(power) + _, exported = self.peak_filter.check_values(power, None, exported) imported, _ = self.sim_counter.sim_count(power) self.store.set(InverterState(power=power, exported=exported, imported=imported)) diff --git a/packages/modules/devices/sma/sma_shm/counter_test.py b/packages/modules/devices/sma/sma_shm/counter_test.py index 2d9c31700b..434a2a4cd9 100644 --- a/packages/modules/devices/sma/sma_shm/counter_test.py +++ b/packages/modules/devices/sma/sma_shm/counter_test.py @@ -1,7 +1,7 @@ import base64 -from modules.common.component_state import CounterState -from modules.devices.sma.sma_shm import counter, speedwiredecoder +from modules.common.component_state import CounterState, InverterState +from modules.devices.sma.sma_shm import counter, inverter, speedwiredecoder # This sample was collected from an SMA Energy Meter with Firmware 2.0.18.R on 2021-12-22: SAMPLE_SMA_ENERGY_EM = """ @@ -19,7 +19,7 @@ """ -def test_process_datagram_energy_meter(): +def test_process_datagram_energy_meter(data_): # setup data = base64.b64decode(SAMPLE_SMA_ENERGY_EM) sma_data = speedwiredecoder.decode_speedwire(data) @@ -27,7 +27,7 @@ def test_process_datagram_energy_meter(): sma_counter.initialize() # execution - sma_counter.read_datagram(sma_data) + assert sma_counter.read_datagram(sma_data) # evaluation assert vars(sma_counter.store.delegate.delegate.state) == vars(CounterState(currents=[-17.16, -17.1, -16.02], @@ -39,3 +39,19 @@ def test_process_datagram_energy_meter(): power=-11967.0, frequency=50, serial_number=None)) + + +def test_process_datagram_inverter(data_): + # setup + decoded_data = speedwiredecoder.decode_speedwire(base64.b64decode(SAMPLE_SMA_ENERGY_EM)) + sma_inverter = inverter.create_component(inverter.component_descriptor.configuration_factory(id=1)) + sma_inverter.initialize() + + # execution + assert sma_inverter.read_datagram(decoded_data) + + # evaluation + assert vars(sma_inverter.store.delegate.delegate.state) == vars(InverterState( + power=-11967, + exported=86688627.0, + )) diff --git a/packages/modules/devices/sma/sma_shm/utils.py b/packages/modules/devices/sma/sma_shm/utils.py index 4d7a208e84..509d6eb6a0 100644 --- a/packages/modules/devices/sma/sma_shm/utils.py +++ b/packages/modules/devices/sma/sma_shm/utils.py @@ -2,8 +2,11 @@ from typing import Any, TypeVar, Generic, Callable, Optional, Union, TypedDict from modules.common.component_context import SingleComponentUpdateContext +from modules.common.component_state import CounterState, InverterState +from modules.common.component_type import special_to_general_type_mapping from modules.common.fault_state import ComponentInfo, FaultState from modules.common.store import ValueStore +from modules.common.utils.peak_filter import PeakFilter from modules.devices.sma.sma_shm.config import SmaHomeManagerCounterSetup, SmaHomeManagerInverterSetup T = TypeVar("T") @@ -35,10 +38,18 @@ def initialize(self) -> None: self.__parser = self.kwargs['parser'] self.__serial_matcher = _create_serial_matcher(self.component_config.configuration.serials) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.component_type = special_to_general_type_mapping(self.component_config.type) + self.peak_filter = PeakFilter(self.component_type, self.component_config.id, self.fault_state) def read_datagram(self, datagram: dict) -> bool: if self.__serial_matcher(datagram): with SingleComponentUpdateContext(self.fault_state): - self.store.set(self.__parser(datagram)) + state = self.__parser(datagram) + if isinstance(state, CounterState): + state.imported, state.exported = self.peak_filter.check_values( + state.power, state.imported, state.exported) + elif isinstance(state, InverterState): + _, state.exported = self.peak_filter.check_values(state.power, None, state.exported) + self.store.set(state) return True return False diff --git a/packages/modules/devices/solaredge/solaredge/inverter.py b/packages/modules/devices/solaredge/solaredge/inverter.py index f22e4071ec..baf3751d1f 100644 --- a/packages/modules/devices/solaredge/solaredge/inverter.py +++ b/packages/modules/devices/solaredge/solaredge/inverter.py @@ -65,12 +65,13 @@ def read_state(self): Register.CURRENTS, 30, mapping=self.REG_MAPPING, unit=self.component_config.configuration.modbus_id) power = scale_registers(resp[Register.POWER], resp[Register.POWER_SCALE]) * -1 - self.peak_filter.check_values(power) + exported = scale_registers(resp[Register.EXPORTED], resp[Register.EXPORTED_SCALE]) + _, exported = self.peak_filter.check_values(power, None, exported) imported, _ = self.sim_counter.sim_count(power) return InverterState( power=power, - exported=scale_registers(resp[Register.EXPORTED], resp[Register.EXPORTED_SCALE]), + exported=exported, currents=scale_registers(resp[Register.CURRENTS], resp[Register.CURRENTS_SCALE]), dc_power=scale_registers(resp[Register.DC_POWER], resp[Register.DC_POWER_SCALE]) * -1, imported=imported, diff --git a/packages/modules/devices/solaredge/solaredge/inverter_test.py b/packages/modules/devices/solaredge/solaredge/inverter_test.py index b1f4f83cf7..ade67b64e1 100644 --- a/packages/modules/devices/solaredge/solaredge/inverter_test.py +++ b/packages/modules/devices/solaredge/solaredge/inverter_test.py @@ -18,6 +18,7 @@ def test_read_state(): inverter = SolaredgeInverter(SolaredgeInverterSetup(), client=Mock( spec=ModbusTcpClient_, read_holding_registers_bulk=mock_read_holding_registers_bulk), device_id=1) inverter.initialize() + inverter.peak_filter.check_values = Mock(return_value=(None, 8980404)) # execution inverter_state = inverter.read_state() @@ -31,3 +32,4 @@ def test_read_state(): imported=100, ) ) + inverter.peak_filter.check_values.assert_called_once_with(-1415.2, None, 8980404) diff --git a/packages/modules/devices/tesla/tesla/bat.py b/packages/modules/devices/tesla/tesla/bat.py index f08e6540f6..5bda112fb0 100644 --- a/packages/modules/devices/tesla/tesla/bat.py +++ b/packages/modules/devices/tesla/tesla/bat.py @@ -2,9 +2,10 @@ from modules.common.abstract_device import AbstractBat from modules.common.component_state import BatState -from modules.common.component_type import ComponentDescriptor +from modules.common.component_type import ComponentDescriptor, ComponentType from modules.common.fault_state import ComponentInfo, FaultState from modules.common.store import get_component_value_store +from modules.common.utils.peak_filter import PeakFilter from modules.devices.tesla.tesla.http_client import PowerwallHttpClient from modules.devices.tesla.tesla.config import TeslaBatSetup @@ -16,12 +17,16 @@ def __init__(self, component_config: TeslaBatSetup) -> None: def initialize(self) -> None: self.store = get_component_value_store(self.component_config.type, self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state) def update(self, client: PowerwallHttpClient, aggregate) -> None: + power = -aggregate["battery"]["instant_power"] + imported, exported = self.peak_filter.check_values( + power, aggregate["battery"]["energy_imported"], aggregate["battery"]["energy_exported"]) self.store.set(BatState( - imported=aggregate["battery"]["energy_imported"], - exported=aggregate["battery"]["energy_exported"], - power=-aggregate["battery"]["instant_power"], + imported=imported, + exported=exported, + power=power, soc=client.get_json("/api/system_status/soe")["percentage"] )) diff --git a/packages/modules/devices/tesla/tesla/counter.py b/packages/modules/devices/tesla/tesla/counter.py index 91606eecd9..9c2705d98f 100644 --- a/packages/modules/devices/tesla/tesla/counter.py +++ b/packages/modules/devices/tesla/tesla/counter.py @@ -6,9 +6,10 @@ from modules.common.abstract_device import AbstractCounter from modules.common.component_state import CounterState -from modules.common.component_type import ComponentDescriptor +from modules.common.component_type import ComponentDescriptor, ComponentType from modules.common.fault_state import ComponentInfo, FaultState from modules.common.store import get_component_value_store +from modules.common.utils.peak_filter import PeakFilter from modules.devices.tesla.tesla.config import TeslaCounterSetup from modules.devices.tesla.tesla.http_client import PowerwallHttpClient @@ -30,6 +31,7 @@ def initialize(self) -> None: self.fault_state = FaultState( ComponentInfo.from_component_config(self.component_config) ) + self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state) @staticmethod def _nearly_zero(x: float, eps: float = 1e-9) -> bool: @@ -93,6 +95,11 @@ def _get_meters_site(self, client: PowerwallHttpClient): return None def update(self, client: PowerwallHttpClient, aggregate): + site = aggregate["site"] + power = site["instant_power"] + imported, exported = self.peak_filter.check_values( + power, site["energy_imported"], site["energy_exported"]) + try: meters_site = self._get_meters_site(client) if meters_site is None: @@ -119,9 +126,6 @@ def update(self, client: PowerwallHttpClient, aggregate): for phase in ["a", "b", "c"] ] - imported = aggregate["site"]["energy_imported"] - exported = aggregate["site"]["energy_exported"] - calculated_currents, power_factors = ( self._calc_currents_and_pf_from_pqu( voltages=voltages, @@ -142,7 +146,7 @@ def update(self, client: PowerwallHttpClient, aggregate): "Using phase currents from Tesla/Neurio API." ) - frequency = float(aggregate["site"]["frequency"]) + frequency = float(site["frequency"]) serial = cached.get("serial_number") serial_number = str(serial) if serial else None @@ -150,7 +154,7 @@ def update(self, client: PowerwallHttpClient, aggregate): powerwall_state = CounterState( imported=imported, exported=exported, - power=aggregate["site"]["instant_power"], + power=power, voltages=voltages, currents=currents, powers=p_list, @@ -167,9 +171,9 @@ def update(self, client: PowerwallHttpClient, aggregate): ) powerwall_state = CounterState( - imported=aggregate["site"]["energy_imported"], - exported=aggregate["site"]["energy_exported"], - power=aggregate["site"]["instant_power"], + imported=imported, + exported=exported, + power=power, ) self.store.set(powerwall_state) diff --git a/packages/modules/devices/tesla/tesla/inverter.py b/packages/modules/devices/tesla/tesla/inverter.py index 26763342fd..9d705669bf 100644 --- a/packages/modules/devices/tesla/tesla/inverter.py +++ b/packages/modules/devices/tesla/tesla/inverter.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 from modules.common.abstract_device import AbstractInverter from modules.common.component_state import InverterState -from modules.common.component_type import ComponentDescriptor +from modules.common.component_type import ComponentDescriptor, ComponentType from modules.common.fault_state import ComponentInfo, FaultState from modules.common.store import get_component_value_store +from modules.common.utils.peak_filter import PeakFilter from modules.devices.tesla.tesla.http_client import PowerwallHttpClient from modules.devices.tesla.tesla.config import TeslaInverterSetup @@ -15,13 +16,16 @@ def __init__(self, component_config: TeslaInverterSetup) -> None: def initialize(self) -> None: self.store = get_component_value_store(self.component_config.type, self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state) def update(self, client: PowerwallHttpClient, aggregate) -> None: pv_watt = aggregate["solar"]["instant_power"] if pv_watt > 5: pv_watt = pv_watt*-1 + _, exported = self.peak_filter.check_values( + pv_watt, None, aggregate["solar"]["energy_exported"]) self.store.set(InverterState( - exported=aggregate["solar"]["energy_exported"], + exported=exported, power=pv_watt )) diff --git a/packages/modules/devices/tesla/tesla/tesla_test.py b/packages/modules/devices/tesla/tesla/tesla_test.py index 947d59e505..66f3c93ebf 100644 --- a/packages/modules/devices/tesla/tesla/tesla_test.py +++ b/packages/modules/devices/tesla/tesla/tesla_test.py @@ -2,6 +2,7 @@ import requests import requests_mock +from control import data from modules.common.configurable_device import ConfigurableDevice from modules.devices.tesla.tesla import bat @@ -145,8 +146,9 @@ def assert_battery_state_correct(state: BatState): assert state.exported == 1169030 -def test_powerwall_update_if_cookie_cached(monkeypatch, requests_mock: requests_mock.Mocker): +def test_powerwall_update_if_cookie_cached(monkeypatch, requests_mock: requests_mock.Mocker, data_): # setup + data.data.bat_data["bat0"] = Mock(data=Mock(config=Mock(max_power=10000))) mock_bat_value_store = Mock() monkeypatch.setattr(bat, "get_component_value_store", Mock(return_value=mock_bat_value_store)) requests_mock.get("https://sample-address/api/meters/aggregates", text=sample_aggregates_json, @@ -179,6 +181,9 @@ def test_powerwall_counter_falls_back_to_aggregates_if_meters_site_404(monkeypat # setup mock_counter_value_store = Mock() monkeypatch.setattr(counter, "get_component_value_store", Mock(return_value=mock_counter_value_store)) + peak_filter = Mock() + peak_filter.check_values.side_effect = lambda power, imported, exported: (imported, exported) + monkeypatch.setattr(counter, "PeakFilter", Mock(return_value=peak_filter)) requests_mock.get("https://sample-address/api/meters/aggregates", text=sample_aggregates_json) meters_site_mock = requests_mock.get("https://sample-address/api/meters/site", status_code=404) monkeypatch.setattr(device, "_authenticate", @@ -194,6 +199,8 @@ def test_powerwall_counter_falls_back_to_aggregates_if_meters_site_404(monkeypat assert state.power == -21.449996948242188 assert state.imported == 3276432.6625 assert state.exported == 1136916.6875 + assert peak_filter.check_values.call_count == 2 + peak_filter.check_values.assert_called_with(-21.449996948242188, 3276432.6625, 1136916.6875) assert mock_counter_value_store.set.call_count == 2 # endpoint is not requested again once it is known to be missing assert meters_site_mock.call_count == 1