Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 20 additions & 4 deletions packages/modules/devices/sma/sma_shm/counter_test.py
Original file line number Diff line number Diff line change
@@ -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 = """
Expand All @@ -19,15 +19,15 @@
"""


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)
sma_counter = counter.create_component(counter.component_descriptor.configuration_factory())
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],
Expand All @@ -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,
))
13 changes: 12 additions & 1 deletion packages/modules/devices/sma/sma_shm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Comment on lines +49 to +50
elif isinstance(state, InverterState):
_, state.exported = self.peak_filter.check_values(state.power, None, state.exported)
self.store.set(state)
return True
return False
5 changes: 3 additions & 2 deletions packages/modules/devices/solaredge/solaredge/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/modules/devices/solaredge/solaredge/inverter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -31,3 +32,4 @@ def test_read_state():
imported=100,
)
)
inverter.peak_filter.check_values.assert_called_once_with(-1415.2, None, 8980404)
13 changes: 9 additions & 4 deletions packages/modules/devices/tesla/tesla/bat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"])
Comment on lines +24 to +25
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"]
))

Expand Down
22 changes: 13 additions & 9 deletions packages/modules/devices/tesla/tesla/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -142,15 +146,15 @@ 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

powerwall_state = CounterState(
imported=imported,
exported=exported,
power=aggregate["site"]["instant_power"],
power=power,
voltages=voltages,
currents=currents,
powers=p_list,
Expand All @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions packages/modules/devices/tesla/tesla/inverter.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
))

Expand Down
9 changes: 8 additions & 1 deletion packages/modules/devices/tesla/tesla/tesla_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down
Loading