diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index d0e70655b1..017af2dca8 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 97 + DATASTORE_VERSION = 98 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2572,3 +2572,14 @@ def upgrade_datastore_96(self) -> None: MessageType.INFO, ) self.__update_topic("openWB/system/datastore_version", 97) + + def upgrade_datastore_97(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/system/device/[0-9]+/config$", topic) is not None: + payload = decode_payload(payload) + # add phase + if payload.get("type") == "shelly" and "phase" not in payload["configuration"]: + payload["configuration"].update({"phase": 1}) + Pub().pub(topic, payload) + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 98) diff --git a/packages/modules/devices/shelly/shelly/bat.py b/packages/modules/devices/shelly/shelly/bat.py index c5e9f8c337..caf4a9760c 100644 --- a/packages/modules/devices/shelly/shelly/bat.py +++ b/packages/modules/devices/shelly/shelly/bat.py @@ -17,6 +17,7 @@ class KwargsDict(TypedDict): device_id: int ip_address: str factor: int + phase: int generation: Optional[int] @@ -29,6 +30,7 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.address: str = self.kwargs['ip_address'] self.factor: int = self.kwargs['factor'] + self.phase: int = self.kwargs['phase'] self.generation: Optional[int] = self.kwargs['generation'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") self.store = get_bat_value_store(self.component_config.id) @@ -43,38 +45,54 @@ def update(self) -> None: status = req.get_http_session().get(status_url, timeout=3).json() try: - if self.generation == 1: - if 'meters' in status: - meters = status['meters'] # shelly - else: - meters = status['emeters'] # shellyEM & shelly3EM - # shellyEM has one meter, shelly3EM has three meters: - for meter in meters: - power = power + meter['power'] - currents = [0, 0, 0] + alphabetical_index = ['a', 'b', 'c'] + currents = [0.0, 0.0, 0.0] + # GEN 1 + if "meters" in status: + meters = status['meters'] # einphasiger shelly? + for i in range(len(meters)): + currents[(i+self.phase-1) % 3] = ((float(meters[i]['power']) * self.factor) / 230 + if meters[i].get('power') else 0) + power = power + (float(meters[i]['power'] * self.factor)) + elif "emeters" in status: + meters = status['emeters'] # shellyEM & shelly3EM + # shellyEM has one meter, shelly3EM has three meters + for i in range(len(meters)): + currents[(i+self.phase-1) % 3] = (float(meters[i]['current']) * self.factor + if meters[i].get('current') else 0) + power = power + (float(meters[i]['power'] * self.factor)) + # GEN 2+ + # shelly Pro3EM + elif "em:0" in status: + meters = status['em:0'] + for i in range(0, 3): + currents[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_current']) * self.factor + if meters.get(f'{alphabetical_index[i]}_current') else 0) + power = float(meters['total_act_power']) * self.factor + # Shelly MiniPM G3 + elif "pm1:0" in status: + log.debug("single phase shelly") + meters = status['pm1:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['apower'] * self.factor + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + meters = status['switch:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['apower'] * self.factor else: - if 'switch:0' in status and 'apower' in status['switch:0']: - power = status['switch:0']['apower'] - currents = [status['switch:0']['current'], 0, 0] - elif 'em1:0' in status: - power = status['em1:0']['act_power'] # shelly Pro EM Gen 2 - currents = [status['em1:0']['current'], 0, 0] - elif 'pm1:0' in status: - power = status['pm1:0']['apower'] # shelly PM Mini Gen 3 - currents = [status['pm1:0']['current'], 0, 0] - else: - power = status['em:0']['total_act_power'] # shelly Pro3EM - currents = [status['em:0'][f'{i}_current'] for i in 'abc'] - - power = power * self.factor + log.debug("single phase shelly") + meters = status['em1:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['act_power'] * self.factor # shelly Pro EM Gen 2 imported, exported = self.sim_counter.sim_count(power) + bat_state = BatState( power=power, + currents=currents, imported=imported, exported=exported ) - if 'currents' in locals(): - bat_state.currents = currents self.store.set(bat_state) except KeyError: log.exception("unsupported shelly device.") diff --git a/packages/modules/devices/shelly/shelly/config.py b/packages/modules/devices/shelly/shelly/config.py index 1e5570f33a..b4c87adcfc 100644 --- a/packages/modules/devices/shelly/shelly/config.py +++ b/packages/modules/devices/shelly/shelly/config.py @@ -7,9 +7,10 @@ @auto_str class ShellyConfiguration: - def __init__(self, ip_address: Optional[str] = None, factor: Optional[int] = -1): + def __init__(self, ip_address: Optional[str] = None, factor: Optional[int] = -1, phase: Optional[int] = 1): self.ip_address = ip_address self.factor = factor + self.phase = phase @auto_str diff --git a/packages/modules/devices/shelly/shelly/counter.py b/packages/modules/devices/shelly/shelly/counter.py index 3c4ebadf41..558d2249ec 100644 --- a/packages/modules/devices/shelly/shelly/counter.py +++ b/packages/modules/devices/shelly/shelly/counter.py @@ -17,6 +17,7 @@ class KwargsDict(TypedDict): device_id: int ip_address: str factor: int + phase: int generation: Optional[int] @@ -29,6 +30,7 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.address: str = self.kwargs['ip_address'] self.factor: int = self.kwargs['factor'] + self.phase: int = self.kwargs['phase'] self.generation: Optional[int] = self.kwargs['generation'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") self.store = get_counter_value_store(self.component_config.id) @@ -42,69 +44,110 @@ def update(self) -> None: status_url = "http://" + self.address + "/rpc/Shelly.GetStatus" status = req.get_http_session().get(status_url, timeout=3).json() try: - if self.generation == 1: # shelly3EM - meters = status['emeters'] - # shelly3EM has three meters: - for meter in meters: - power = power + meter['power'] - power = power * self.factor + # GEN 1 + alphabetical_index = ['a', 'b', 'c'] + if "meters" in status: + powers = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + meters = status['meters'] # einphasiger shelly? + for i in range(len(meters)): + powers[(i+self.phase-1) % 3] = (float(meters[i]['power']) * self.factor + if meters[i].get('power') else 0) + voltages[(i+self.phase-1) % 3] = 230 + power = sum(powers) + elif "emeters" in status: + powers = [0.0, 0.0, 0.0] + currents = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + power_factors = [0.0, 0.0, 0.0] + meters = status['emeters'] # shellyEM & shelly3EM + # shellyEM has one meter, shelly3EM has three meters + for i in range(len(meters)): + powers[(i+self.phase-1) % 3] = (float(meters[i]['power']) * self.factor + if meters[i].get('power') else 0) + currents[(i+self.phase-1) % 3] = (float(meters[i]['current']) * self.factor + if meters[i].get('current') else 0) + voltages[(i+self.phase-1) % 3] = (float(meters[i]['voltage']) + if meters[i].get('voltage') else 0) + power_factors[(i+self.phase-1) % 3] = (float(meters[i]['pf']) + if meters[i].get('pf') else 0) + power = sum(powers) - voltages = [status['emeters'][i]['voltage'] for i in range(0, 3)] - currents = [status['emeters'][i]['current'] for i in range(0, 3)] - powers = [status['emeters'][i]['power'] for i in range(0, 3)] - power_factors = [status['emeters'][i]['pf'] for i in range(0, 3)] + # GEN 2+ + # shelly Pro3EM + elif "em:0" in status: + powers = [0.0, 0.0, 0.0] + currents = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + power_factors = [0.0, 0.0, 0.0] + meters = status['em:0'] + for i in range(0, 3): + powers[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_act_power']) * self.factor + if meters.get(f'{alphabetical_index[i]}_act_power') else 0) + voltages[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_voltage']) + if meters.get(f'{alphabetical_index[i]}_voltage') else 0) + currents[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_current']) * self.factor + if meters.get(f'{alphabetical_index[i]}_current') else 0) + power_factors[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_pf']) + if meters.get(f'{alphabetical_index[i]}_pf') else 0) + power = float(meters['total_act_power']) * self.factor + # Shelly MiniPM G3 + elif "pm1:0" in status: + log.debug("single phase shelly") + powers = [0.0, 0.0, 0.0] + currents = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + power_factors = [0.0, 0.0, 0.0] + meters = status['pm1:0'] + powers[self.phase-1] = meters['apower'] * self.factor + voltages[self.phase-1] = meters['voltage'] + currents[self.phase-1] = meters['current'] * self.factor + power_factors[self.phase-1] = meters['pf'] if meters.get('pf') else 0 + power = meters['apower'] * self.factor + frequency = meters['freq'] + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + powers = [0.0, 0.0, 0.0] + currents = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + power_factors = [0.0, 0.0, 0.0] + meters = status['switch:0'] + powers[self.phase-1] = meters['apower'] * self.factor + voltages[self.phase-1] = meters['voltage'] + currents[self.phase-1] = meters['current'] * self.factor + # power_factors[self.phase-1] = meters['pf'] + power = meters['apower'] * self.factor + frequency = meters['freq'] else: - # shelly Pro3EM - if "em:0" in status: - meter = status['em:0'] - voltages = [meter[f'{i}_voltage'] for i in 'abc'] - currents = [meter[f'{i}_current'] for i in 'abc'] - powers = [meter[f'{i}_act_power'] for i in 'abc'] - power_factors = [meter[f'{i}_pf'] for i in 'abc'] - power = meter['total_act_power'] * self.factor - # Shelly MiniPM G3 - elif "pm1:0" in status: - log.debug("single phase shelly") - meter = status['pm1:0'] - voltages = [meter['voltage'], 0, 0] - currents = [meter['current'], 0, 0] - power = meter['apower'] - frequency = meter['freq'] - powers = [meter['apower'], 0, 0] - elif 'switch:0' in status and 'apower' in status['switch:0']: - log.debug("single phase shelly") - meter = status['switch:0'] - power = meter['apower'] - voltages = [meter['voltage'], 0, 0] - currents = [meter['current'], 0, 0] - frequency = meter['freq'] - power_factors = [meter['pf'], 0, 0] - powers = [meter['apower'], 0, 0] - else: - log.debug("single phase shelly") - meter = status['em1:0'] - power = meter['act_power'] # shelly Pro EM Gen 2 - voltages = [meter['voltage'], 0, 0] - currents = [meter['current'], 0, 0] - frequency = meter['freq'] - power_factors = [meter['pf'], 0, 0] - powers = [meter['act_power'], 0, 0] + log.debug("single phase shelly") + powers = [0.0, 0.0, 0.0] + currents = [0.0, 0.0, 0.0] + voltages = [0.0, 0.0, 0.0] + power_factors = [0.0, 0.0, 0.0] + meters = status['em1:0'] + powers[self.phase-1] = meters['act_power'] + voltages[self.phase-1] = meters['voltage'] + currents[self.phase-1] = meters['current'] * self.factor + power_factors[self.phase-1] = meters['pf'] + power = meters['act_power'] # shelly Pro EM Gen 2 + frequency = meters['freq'] imported, exported = self.sim_counter.sim_count(power) counter_state = CounterState( - voltages=voltages, - currents=currents, imported=imported, exported=exported, + powers=powers, power=power ) if 'frequency' in locals(): counter_state.frequency = frequency if "power_factors" in locals(): counter_state.power_factors = power_factors - if "powers" in locals(): - counter_state.powers = powers + if "voltages" in locals(): + counter_state.voltages = voltages + if "currents" in locals(): + counter_state.currents = currents self.store.set(counter_state) except KeyError: log.exception("unsupported shelly device?") diff --git a/packages/modules/devices/shelly/shelly/device.py b/packages/modules/devices/shelly/shelly/device.py index 47097ad383..303f5b6818 100644 --- a/packages/modules/devices/shelly/shelly/device.py +++ b/packages/modules/devices/shelly/shelly/device.py @@ -22,6 +22,7 @@ def create_counter_component(component_config: ShellyCounterSetup) -> ShellyCoun device_id=device_config.id, ip_address=device_config.configuration.ip_address, factor=device_config.configuration.factor, + phase=device_config.configuration.phase, generation=generation) def create_inverter_component(component_config: ShellyInverterSetup) -> ShellyInverter: @@ -30,6 +31,7 @@ def create_inverter_component(component_config: ShellyInverterSetup) -> ShellyIn device_id=device_config.id, ip_address=device_config.configuration.ip_address, factor=device_config.configuration.factor, + phase=device_config.configuration.phase, generation=generation) def create_bat_component(component_config: ShellyBatSetup) -> ShellyBat: @@ -38,6 +40,7 @@ def create_bat_component(component_config: ShellyBatSetup) -> ShellyBat: device_id=device_config.id, ip_address=device_config.configuration.ip_address, factor=device_config.configuration.factor, + phase=device_config.configuration.phase, generation=generation) def initializer() -> None: diff --git a/packages/modules/devices/shelly/shelly/inverter.py b/packages/modules/devices/shelly/shelly/inverter.py index 96a7fa2662..6139fd1d3a 100644 --- a/packages/modules/devices/shelly/shelly/inverter.py +++ b/packages/modules/devices/shelly/shelly/inverter.py @@ -17,6 +17,7 @@ class KwargsDict(TypedDict): device_id: int ip_address: str factor: int + phase: int generation: Optional[int] @@ -29,6 +30,7 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.address: str = self.kwargs['ip_address'] self.factor: int = self.kwargs['factor'] + self.phase: int = self.kwargs['phase'] self.generation: Optional[int] = self.kwargs['generation'] self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") self.store = get_inverter_value_store(self.component_config.id) @@ -42,36 +44,53 @@ def update(self) -> None: status_url = "http://" + self.address + "/rpc/Shelly.GetStatus" status = req.get_http_session().get(status_url, timeout=3).json() try: - if self.generation == 1: - if 'meters' in status: - meters = status['meters'] # shelly - else: - meters = status['emeters'] # shellyEM & shelly3EM - # shellyEM has one meter, shelly3EM has three meters: - for meter in meters: - power = power + meter['power'] + alphabetical_index = ['a', 'b', 'c'] + currents = [0.0, 0.0, 0.0] + # GEN 1 + if "meters" in status: + meters = status['meters'] # einphasiger shelly? + for i in range(len(meters)): + currents[(i+self.phase-1) % 3] += ((float(meters[i]['power']) * self.factor) / 230 + if meters[i].get('power') else 0) + power = power + (float(meters[i]['power'] * self.factor)) + elif "emeters" in status: + meters = status['emeters'] # shellyEM & shelly3EM + # shellyEM has one meter, shelly3EM has three meters + for i in range(0, 3): + currents[(i+self.phase-1) % 3] = (float(meters[i]['current']) * self.factor + if meters[i].get('current') else 0) + power = power + (float(meters[i]['power'] * self.factor)) + # GEN 2+ + # shelly Pro3EM + elif "em:0" in status: + meters = status['em:0'] + for i in range(0, 3): + currents[(i+self.phase-1) % 3] = (float(meters[f'{alphabetical_index[i]}_current']) * self.factor + if meters.get(f'{alphabetical_index[i]}_current') else 0) + power = float(meters['total_act_power']) * self.factor + # Shelly MiniPM G3 + elif "pm1:0" in status: + log.debug("single phase shelly") + meters = status['pm1:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['apower'] * self.factor + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + meters = status['switch:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['apower'] * self.factor else: - if 'switch:0' in status and 'apower' in status['switch:0']: - power = status['switch:0']['apower'] - currents = [status['switch:0']['current'], 0, 0] - elif 'em1:0' in status: - power = status['em1:0']['act_power'] # shelly Pro EM Gen 2 - currents = [status['em1:0']['current'], 0, 0] - elif 'pm1:0' in status: - power = status['pm1:0']['apower'] # shelly PM Mini Gen 3 - currents = [status['pm1:0']['current'], 0, 0] - else: - power = status['em:0']['total_act_power'] # shelly Pro3EM - currents = [status['em:0'][f'{i}_current'] for i in 'abc'] - - power = power * self.factor + log.debug("single phase shelly") + meters = status['em1:0'] + currents[self.phase-1] = meters['current'] * self.factor + power = meters['act_power'] * self.factor # shelly Pro EM Gen 2 _, exported = self.sim_counter.sim_count(power) + inverter_state = InverterState( power=power, + currents=currents, exported=exported ) - if 'currents' in locals(): - inverter_state.currents = currents self.store.set(inverter_state) except KeyError: log.exception("unsupported shelly device.") diff --git a/packages/modules/devices/shelly/shelly/shelly_test.py b/packages/modules/devices/shelly/shelly/shelly_test.py index 8be2aa50e5..9721957ade 100644 --- a/packages/modules/devices/shelly/shelly/shelly_test.py +++ b/packages/modules/devices/shelly/shelly/shelly_test.py @@ -1,12 +1,97 @@ from unittest.mock import Mock import requests_mock -from modules.common.component_state import CounterState -from modules.conftest import SAMPLE_IP -from modules.devices.shelly.shelly.config import ShellyCounterSetup -from modules.devices.shelly.shelly import counter +from dataclasses import dataclass +from typing import Optional +import pytest +from modules.common.component_state import CounterState, InverterState, BatState +from modules.conftest import SAMPLE_IP +from modules.devices.shelly.shelly.config import ShellyCounterSetup, ShellyInverterSetup, ShellyBatSetup +from modules.devices.shelly.shelly import counter, inverter, bat +# Shelly PLUG G1 +DATA_PLUG_G1 = { + "meters": [{ + "power": 230, + "overpower": 0.00, + "is_valid": True, + "timestamp": 1756902511, + "counters": [400.467, 1127.491, 1046.330], + "total": 3981113 + }] +} +# Shelly EM 3 G1 +DATA_EM_3_G1 = { + # [...] + "emeters": [{ + "power": 2300.00, + "pf": 1.00, + "current": 10.00, + "voltage": 220.00, + "is_valid": True, + "total": 3000000.0, + "total_returned": 0.0 + }, { + "power": 460.00, + "pf": 1.00, + "current": 2.00, + "voltage": 230.00, + "is_valid": True, + "total": 1000000.0, + "total_returned": 0.0 + }, { + "power": 230.00, + "pf": 1.00, + "current": 1.00, + "voltage": 240.00, + "is_valid": True, + "total": 400000.0, + "total_returned": 0.0 + }], + "total_power": 2990.00, + # [...] +} +# Shelly Pro 3EM G2 +DATA_PRO_3EM_G2 = { + "em:0": { + "id": 0, + "a_current": 1.0, + "a_voltage": 220.0, + "a_act_power": 230.0, + "a_aprt_power": 71.2, + "a_pf": 0.50, + "a_freq": 50.0, + "b_current": 2.0, + "b_voltage": 230.0, + "b_act_power": 460.0, + "b_aprt_power": 58.8, + "b_pf": 1.00, + "b_freq": 50.0, + "c_current": 10.0, + "c_voltage": 240.0, + "c_act_power": 2300.0, + "c_aprt_power": 56.5, + "c_pf": 1.50, + "c_freq": 50.0, + "n_current": "null", + "total_current": 0.812, + "total_act_power": 2990.00, + "total_aprt_power": 186.512, + "user_calibrated_phase": [] + }, + "emdata:0": { + "id": 0, + "a_total_act_energy": 24169.51, + "a_total_act_ret_energy": 1356754.52, + "b_total_act_energy": 19485.50, + "b_total_act_ret_energy": 1256348.10, + "c_total_act_energy": 18670.00, + "c_total_act_ret_energy": 1211805.10, + "total_act": 62325.00, + "total_act_ret": 3824907.72 + }, +} # Shelly MiniPM G3 https://forum.openwb.de/viewtopic.php?p=117309#p117309 DATA_MINPM_G3 = { # [..] @@ -18,42 +103,262 @@ "freq": 51, "aenergy": { "total": 3195.88, - "by_minute": [ - 0, - 0, - 0 - ], + "by_minute": [0, 0, 0], "minute_ts": 1727857620 }, "ret_aenergy": { "total": 0, - "by_minute": [ - 0, - 0, - 0 - ], + "by_minute": [0, 0, 0], "minute_ts": 1727857620 } }, # [..] } +# Shelly 1PM G4 +DATA_1PM_G4 = { + # [...] + "switch:0": { + "id": 0, + "source": "init", + "output": True, + "apower": 117.9, + "voltage": 227.3, + "freq": 52.0, + "current": 0.65, + "aenergy": {"total": 185774.593, "by_minute": [2394.379, 1795.784, 1995.316], "minute_ts": 1756903980}, + "ret_aenergy": {"total": 185618.039, "by_minute": [2394.379, 1795.784, 1995.316], "minute_ts": 1756903980}, + "temperature": {"tC": 54.8, "tF": 130.6} + }, + # [...] +} + + +@dataclass +class CounterParams: + name: str + json_data: str + factor: int = 1 + phase: int = 1 + generation: int = 1 + expected_counter_state: Optional[CounterState] = None + +cases = [ + CounterParams(name="G1 - Shelly Plug Counter - Phase 1", + json_data=DATA_PLUG_G1, factor=1, phase=1, generation=1, + expected_counter_state=CounterState( + voltages=[230, 0, 0], power=230, currents=[1, 0, 0], + frequency=50, imported=100, exported=200, powers=[230, 0, 0])), + CounterParams(name="G1 - Shelly Plug Counter - Phase 2", + json_data=DATA_PLUG_G1, factor=1, phase=2, generation=1, + expected_counter_state=CounterState( + voltages=[0, 230, 0], power=230, currents=[0, 1, 0], + frequency=50, imported=100, exported=200, powers=[0, 230, 0])), + CounterParams(name="G1 - Shelly Plug Counter - Phase 3, Faktor -1", + json_data=DATA_PLUG_G1, factor=-1, phase=3, generation=1, + expected_counter_state=CounterState( + voltages=[0, 0, 230], power=-230, currents=[0, 0, -1], + frequency=50, imported=100, exported=200, powers=[0, 0, -230])), + CounterParams(name="G1 - Shelly EM3 Counter - Phase 1, Faktor -1", + json_data=DATA_EM_3_G1, factor=1, phase=1, generation=1, + expected_counter_state=CounterState( + voltages=[220.0, 230.0, 240.0], power=2990.00, currents=[10.0, 2.0, 1.0], frequency=50, + imported=100, exported=200, powers=[2300, 460, 230], power_factors=[1.0, 1.0, 1.0])), + CounterParams(name="G1 - Shelly EM3 Counter - Phase 2", + json_data=DATA_EM_3_G1, factor=-1, phase=2, generation=1, + expected_counter_state=CounterState( + voltages=[240.0, 220.0, 230.0], power=-2990.00, currents=[-1.0, -10.0, -2.0], frequency=50, + imported=100, exported=200, powers=[-230, -2300, -460], power_factors=[1.0, 1.0, 1.0])), + CounterParams(name="G1 - Shelly EM3 Counter - Phase 3", + json_data=DATA_EM_3_G1, factor=1, phase=3, generation=1, + expected_counter_state=CounterState( + voltages=[230.0, 240.0, 220.0], power=2990.00, currents=[2.0, 1.0, 10.0], frequency=50, + imported=100, exported=200, powers=[460, 230, 2300], power_factors=[1.0, 1.0, 1.0])), + CounterParams(name="G2 - Shelly Pro3 EM Counter - Phase 1", + json_data=DATA_PRO_3EM_G2, factor=1, phase=1, generation=2, + expected_counter_state=CounterState( + voltages=[220.0, 230.0, 240.0], power=2990.00, currents=[1.0, 2.0, 10.0], frequency=50, + imported=100, exported=200, powers=[230, 460, 2300], power_factors=[0.5, 1.0, 1.5])), + CounterParams(name="G2 - Shelly Pro3 EM Counter - Phase 2, Faktor -1", + json_data=DATA_PRO_3EM_G2, factor=-1, phase=2, generation=2, + expected_counter_state=CounterState( + voltages=[240.0, 220.0, 230.0], power=-2990.00, currents=[-10.0, -1.0, -2.0], frequency=50, + imported=100, exported=200, powers=[-2300, -230, -460], power_factors=[1.5, 0.5, 1.0])), + CounterParams(name="G2 - Shelly Pro3 EM Counter - Phase 3", + json_data=DATA_PRO_3EM_G2, factor=1, phase=3, generation=2, + expected_counter_state=CounterState( + voltages=[230.0, 240.0, 220.0], power=2990.00, currents=[2.0, 10.0, 1.0], frequency=50, + imported=100, exported=200, powers=[460, 2300, 230], power_factors=[1.0, 1.5, 0.5])), + CounterParams(name="G3 - Shelly Mini PM Counter - Phase 1", + json_data=DATA_MINPM_G3, factor=1, phase=1, generation=3, + expected_counter_state=CounterState( + voltages=[230.9, 0, 0], power=230, currents=[1, 0, 0], frequency=51, + imported=100, exported=200, powers=[230, 0, 0])), + CounterParams(name="G3 - Shelly Mini PM Counter - Phase 2", + json_data=DATA_MINPM_G3, factor=1, phase=2, generation=3, + expected_counter_state=CounterState( + voltages=[0, 230.9, 0], power=230, currents=[0, 1, 0], frequency=51, + imported=100, exported=200, powers=[0, 230, 0])), + CounterParams(name="G3 - Shelly Mini PM Counter - Phase 3", + json_data=DATA_MINPM_G3, factor=1, phase=3, generation=3, + expected_counter_state=CounterState( + voltages=[0, 0, 230.9], power=230, currents=[0, 0, 1], frequency=51, + imported=100, exported=200, powers=[0, 0, 230])), + CounterParams(name="G4 - Shelly 1PM Counter - Phase 1", + json_data=DATA_1PM_G4, factor=1, phase=1, generation=4, + expected_counter_state=CounterState( + voltages=[227.3, 0, 0], power=117.9, currents=[0.65, 0, 0], frequency=52, + imported=100, exported=200, powers=[117.9, 0, 0])), + CounterParams(name="G4 - Shelly 1PM Counter - Phase 2", + json_data=DATA_1PM_G4, factor=1, phase=2, generation=4, + expected_counter_state=CounterState( + voltages=[0, 227.3, 0], power=117.9, currents=[0, 0.65, 0], frequency=52, + imported=100, exported=200, powers=[0, 117.9, 0])), + CounterParams(name="G4 - Shelly 1PM Counter - Phase 3", + json_data=DATA_1PM_G4, factor=1, phase=3, generation=4, + expected_counter_state=CounterState( + voltages=[0, 0, 227.3], power=117.9, currents=[0, 0, 0.65], frequency=52, + imported=100, exported=200, powers=[0, 0, 117.9])), +] -def test_counter_shelly_minipm_g3(monkeypatch, requests_mock: requests_mock.mock): + +@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) +def test_counter(params: CounterParams, monkeypatch, requests_mock: requests_mock.mock): mock_counter_value_store = Mock() monkeypatch.setattr(counter, "get_counter_value_store", Mock(return_value=mock_counter_value_store)) - requests_mock.get(f"http://{SAMPLE_IP}/rpc/Shelly.GetStatus", json=DATA_MINPM_G3) + if params.generation == 1: + requests_mock.get(f"http://{SAMPLE_IP}/status", json=params.json_data) + else: + requests_mock.get(f"http://{SAMPLE_IP}/rpc/Shelly.GetStatus", json=params.json_data) mock_counter_value_store = Mock() monkeypatch.setattr(counter, "get_counter_value_store", Mock(return_value=mock_counter_value_store)) - c = counter.ShellyCounter(ShellyCounterSetup(), device_id=0, ip_address=SAMPLE_IP, factor=1, generation=2) + c = counter.ShellyCounter( + ShellyCounterSetup(), device_id=0, ip_address=SAMPLE_IP, + factor=params.factor, phase=params.phase, generation=params.generation) + c.initialize() + + # execution + c.update() + + # evaluation + assert vars(mock_counter_value_store.set.call_args[0][0]) == vars(params.expected_counter_state) + + +@dataclass +class InverterParams: + name: str + json_data: str + factor: int = 1 + phase: int = 1 + generation: int = 1 + expected_inverter_state: Optional[InverterState] = None + + +cases = [ + InverterParams(name="G1 - Shelly Plug Inverter - Phase 1", + json_data=DATA_PLUG_G1, factor=1, phase=1, generation=1, + expected_inverter_state=InverterState( + power=230, currents=[1, 0, 0], exported=200)), + InverterParams(name="G1 - Shelly Plug Inverter - Phase 3, Faktor -1", + json_data=DATA_PLUG_G1, factor=-1, phase=3, generation=1, + expected_inverter_state=InverterState( + power=-230, currents=[0, 0, -1], exported=200)), + InverterParams(name="G1 - Shelly EM3 Inverter - Phase 2, Faktor -1", + json_data=DATA_EM_3_G1, factor=-1, phase=2, generation=1, + expected_inverter_state=InverterState( + power=-2990.00, currents=[-1.0, -10.0, -2.0], exported=200)), + InverterParams(name="G3 - Shelly Mini PM Inverter - Phase 1, Faktor -1", + json_data=DATA_MINPM_G3, factor=-1, phase=1, generation=3, + expected_inverter_state=InverterState( + power=-230, currents=[-1, 0, 0], exported=200)), + InverterParams(name="G3 - Shelly Mini PM Inverter - Phase 3", + json_data=DATA_MINPM_G3, factor=1, phase=3, generation=3, + expected_inverter_state=InverterState( + power=230, currents=[0, 0, 1], exported=200)), + InverterParams(name="G4 - Shelly 1PM Inverter - Phase 2, Faktor -1", + json_data=DATA_1PM_G4, factor=-1, phase=2, generation=4, + expected_inverter_state=InverterState( + power=-117.9, currents=[0, -0.65, 0], exported=200)), +] + + +@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) +def test_inverter(params: InverterParams, monkeypatch, requests_mock: requests_mock.mock): + mock_inverter_value_store = Mock() + monkeypatch.setattr(inverter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) + if params.generation == 1: + requests_mock.get(f"http://{SAMPLE_IP}/status", json=params.json_data) + else: + requests_mock.get(f"http://{SAMPLE_IP}/rpc/Shelly.GetStatus", json=params.json_data) + mock_inverter_value_store = Mock() + monkeypatch.setattr(inverter, "get_inverter_value_store", Mock(return_value=mock_inverter_value_store)) + c = inverter.ShellyInverter( + ShellyInverterSetup(), device_id=0, ip_address=SAMPLE_IP, + factor=params.factor, phase=params.phase, generation=params.generation) c.initialize() # execution c.update() # evaluation - assert vars(mock_counter_value_store.set.call_args[0][0]) == vars(SAMPLE_COUNTER_STATE) + assert vars(mock_inverter_value_store.set.call_args[0][0]) == vars(params.expected_inverter_state) + +@dataclass +class BatParams: + name: str + json_data: str + factor: int = 1 + phase: int = 1 + generation: int = 1 + expected_bat_state: Optional[BatState] = None -SAMPLE_COUNTER_STATE = CounterState(voltages=[230.9, 0, 0], power=230, currents=[ - 1, 0, 0], frequency=51, imported=100, exported=200, powers=[230, 0, 0]) + +cases = [ + BatParams(name="G1 - Shelly Plug Bat - Phase 1", + json_data=DATA_PLUG_G1, factor=1, phase=1, generation=1, + expected_bat_state=BatState( + power=230, currents=[1, 0, 0], imported=100, exported=200)), + BatParams(name="G1 - Shelly Plug Bat - Phase 3, Faktor -1", + json_data=DATA_PLUG_G1, factor=-1, phase=3, generation=1, + expected_bat_state=BatState( + power=-230, currents=[0, 0, -1], imported=100, exported=200)), + BatParams(name="G1 - Shelly EM3 Bat - Phase 2, Faktor -1", + json_data=DATA_EM_3_G1, factor=-1, phase=2, generation=1, + expected_bat_state=BatState( + power=-2990.00, currents=[-1.0, -10.0, -2.0], imported=100, exported=200)), + BatParams(name="G3 - Shelly Mini PM Bat - Phase 1, Faktor -1", + json_data=DATA_MINPM_G3, factor=-1, phase=1, generation=3, + expected_bat_state=BatState( + power=-230, currents=[-1, 0, 0], imported=100, exported=200)), + BatParams(name="G3 - Shelly Mini PM Bat - Phase 3", + json_data=DATA_MINPM_G3, factor=1, phase=3, generation=3, + expected_bat_state=BatState( + power=230, currents=[0, 0, 1], imported=100, exported=200)), + BatParams(name="G4 - Shelly 1PM Bat - Phase 2, Faktor -1", + json_data=DATA_1PM_G4, factor=-1, phase=2, generation=4, + expected_bat_state=BatState( + power=-117.9, currents=[0, -0.65, 0], imported=100, exported=200)), +] + + +@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) +def test_bat(params: BatParams, monkeypatch, requests_mock: requests_mock.mock): + mock_bat_value_store = Mock() + monkeypatch.setattr(bat, "get_bat_value_store", Mock(return_value=mock_bat_value_store)) + if params.generation == 1: + requests_mock.get(f"http://{SAMPLE_IP}/status", json=params.json_data) + else: + requests_mock.get(f"http://{SAMPLE_IP}/rpc/Shelly.GetStatus", json=params.json_data) + mock_bat_value_store = Mock() + monkeypatch.setattr(bat, "get_bat_value_store", Mock(return_value=mock_bat_value_store)) + c = bat.ShellyBat( + ShellyBatSetup(), device_id=0, ip_address=SAMPLE_IP, + factor=params.factor, phase=params.phase, generation=params.generation) + c.initialize() + + # execution + c.update() + + # evaluation + assert vars(mock_bat_value_store.set.call_args[0][0]) == vars(params.expected_bat_state)