From 307f452951846fe0da21943de919786688be9ced Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Tue, 2 Sep 2025 14:52:54 +0200 Subject: [PATCH 1/7] Phase selection for number indices --- .../modules/devices/shelly/shelly/counter.py | 115 +++++++++++------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/packages/modules/devices/shelly/shelly/counter.py b/packages/modules/devices/shelly/shelly/counter.py index 3c4ebadf41..cffab8e649 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,53 +44,76 @@ 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 + letter_index = ['a', 'b', 'c'] + 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] + if "meters" in status: + meters = status['meters'] # einphasiger shelly? + for i in range(0, 3): + powers[i] = [float(meters[(i+(self.phase-1) % 3)]['power']) * self.factor + if meters[(i+(self.phase-1) % 3)].get('power') else 0] + power = sum(powers) + elif "emeters" in status: + meters = status['emeters'] # shellyEM & shelly3EM + # shellyEM has one meter, shelly3EM has three meters + for i in range(0, 3): + powers[i] = [float(meters[(i+(self.phase-1) % 3)]['power']) * self.factor + if meters[(i+(self.phase-1) % 3)].get('power') else 0] + currents[i] = [float(meters[(i+(self.phase-1) % 3)]['current']) + if meters[(i+(self.phase-1) % 3)].get('current') else 0] + voltages[i] = [float(meters[(i+(self.phase-1) % 3)]['voltage']) + if meters[(i+(self.phase-1) % 3)].get('voltage') else 0] + power_factors[i] = [float(meters[(i+(self.phase-1) % 3)]['pf']) + if meters[(i+(self.phase-1) % 3)].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: + meters = status['em:0'] + powers = [float(meters[f'{i}_act_power']) + if meters.get(f'{i}_act_power') else 0 + for i in 'abc'] + power = float(meters['total_act_power']) * self.factor + voltages = [float(meters[f'{i}_voltage']) + if meters.get(f'{i}_voltage') else 0 + for i in 'abc'] + currents = [float(meters[f'{i}_current']) + if meters.get(f'{i}_current') else 0 + for i in 'abc'] + power_factors = [float(meters[f'{i}_pf']) + if meters.get(f'{i}_pf') else 0 + for i in 'abc'] + # Shelly MiniPM G3 + elif "pm1:0" in status: + log.debug("single phase shelly") + meters = status['pm1:0'] + powers = [meters['apower'], 0, 0] + power = meters['apower'] + voltages = [meters['voltage'], 0, 0] + currents = [meters['current'], 0, 0] + frequency = meters['freq'] + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + meters = status['switch:0'] + powers = [meters['apower'], 0, 0] + power = meters['apower'] + voltages = [meters['voltage'], 0, 0] + currents = [meters['current'], 0, 0] + power_factors = [meters['pf'], 0, 0] + 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") + meters = status['em1:0'] + powers = [meters['act_power'], 0, 0] + power = meters['act_power'] # shelly Pro EM Gen 2 + voltages = [meters['voltage'], 0, 0] + currents = [meters['current'], 0, 0] + power_factors = [meters['pf'], 0, 0] + frequency = meters['freq'] imported, exported = self.sim_counter.sim_count(power) From 8b38dd6bd5753026bf10b5d6d5ca7d82d017ac54 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 4 Sep 2025 12:23:52 +0200 Subject: [PATCH 2/7] add tests --- packages/modules/devices/shelly/shelly/bat.py | 73 +++--- .../modules/devices/shelly/shelly/config.py | 3 +- .../modules/devices/shelly/shelly/counter.py | 110 +++++---- .../modules/devices/shelly/shelly/device.py | 3 + .../modules/devices/shelly/shelly/inverter.py | 72 ++++-- .../devices/shelly/shelly/shelly_test.py | 208 ++++++++++++++++-- 6 files changed, 354 insertions(+), 115 deletions(-) diff --git a/packages/modules/devices/shelly/shelly/bat.py b/packages/modules/devices/shelly/shelly/bat.py index abbcb15ab3..3993c48eac 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,59 @@ 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'] + # GEN 1 + if "meters" in status: + currents = [0.0, 0.0, 0.0] + meters = status['meters'] # einphasiger shelly? + for i in range(0, 3): + 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: + currents = [0.0, 0.0, 0.0] + 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']) + if meters[i].get('current') else 0] + power = power + (float(meters[i]['power'] * self.factor)) + # GEN 2+ + # shelly Pro3EM + elif "em:0" in status: + currents = [0.0, 0.0, 0.0] + meters = status['em:0'] + for i in range(0, 3): + currents[(i+self.phase-1) % 3] = [float(meters[f'{alphabetical_index[i]}_current']) + 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") + currents = [0.0, 0.0, 0.0] + meters = status['pm1:0'] + currents[self.phase-1] = meters['current'] + power = meters['apower'] * self.factor + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + currents = [0.0, 0.0, 0.0] + meters = status['switch:0'] + currents[self.phase-1] = meters['current'] + 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 = [meter[f'{i}_current'] for i in 'abc'] - - power = power * self.factor + log.debug("single phase shelly") + currents = [0.0, 0.0, 0.0] + meters = status['em1:0'] + currents[self.phase-1] = meters['current'] + 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 cffab8e649..d942a722c8 100644 --- a/packages/modules/devices/shelly/shelly/counter.py +++ b/packages/modules/devices/shelly/shelly/counter.py @@ -45,91 +45,109 @@ def update(self) -> None: status = req.get_http_session().get(status_url, timeout=3).json() try: # GEN 1 - letter_index = ['a', 'b', 'c'] - 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] + 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(0, 3): - powers[i] = [float(meters[(i+(self.phase-1) % 3)]['power']) * self.factor - if meters[(i+(self.phase-1) % 3)].get('power') else 0] + 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(0, 3): - powers[i] = [float(meters[(i+(self.phase-1) % 3)]['power']) * self.factor - if meters[(i+(self.phase-1) % 3)].get('power') else 0] - currents[i] = [float(meters[(i+(self.phase-1) % 3)]['current']) - if meters[(i+(self.phase-1) % 3)].get('current') else 0] - voltages[i] = [float(meters[(i+(self.phase-1) % 3)]['voltage']) - if meters[(i+(self.phase-1) % 3)].get('voltage') else 0] - power_factors[i] = [float(meters[(i+(self.phase-1) % 3)]['pf']) - if meters[(i+(self.phase-1) % 3)].get('pf') else 0] + 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) # 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'] - powers = [float(meters[f'{i}_act_power']) - if meters.get(f'{i}_act_power') else 0 - for i in 'abc'] + 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 - voltages = [float(meters[f'{i}_voltage']) - if meters.get(f'{i}_voltage') else 0 - for i in 'abc'] - currents = [float(meters[f'{i}_current']) - if meters.get(f'{i}_current') else 0 - for i in 'abc'] - power_factors = [float(meters[f'{i}_pf']) - if meters.get(f'{i}_pf') else 0 - for i in 'abc'] # 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 = [meters['apower'], 0, 0] - power = meters['apower'] - voltages = [meters['voltage'], 0, 0] - currents = [meters['current'], 0, 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 = [meters['apower'], 0, 0] - power = meters['apower'] - voltages = [meters['voltage'], 0, 0] - currents = [meters['current'], 0, 0] - power_factors = [meters['pf'], 0, 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: 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 = [meters['act_power'], 0, 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 - voltages = [meters['voltage'], 0, 0] - currents = [meters['current'], 0, 0] - power_factors = [meters['pf'], 0, 0] 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 cc57a0959f..39893fe5ff 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,58 @@ 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'] + # GEN 1 + if "meters" in status: + currents = [0.0, 0.0, 0.0] + 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: + currents = [0.0, 0.0, 0.0] + 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']) + if meters[i].get('current') else 0] + power = power + (float(meters[i]['power'] * self.factor)) + # GEN 2+ + # shelly Pro3EM + elif "em:0" in status: + currents = [0.0, 0.0, 0.0] + meters = status['em:0'] + for i in range(0, 3): + currents[(i+self.phase-1) % 3] = [float(meters[f'{alphabetical_index[i]}_current']) + 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") + currents = [0.0, 0.0, 0.0] + meters = status['pm1:0'] + currents[self.phase-1] = meters['current'] + power = meters['apower'] * self.factor + elif 'switch:0' in status and 'apower' in status['switch:0']: + log.debug("single phase shelly") + currents = [0.0, 0.0, 0.0] + meters = status['switch:0'] + currents[self.phase-1] = meters['current'] + 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 = [meter[f'{i}_current'] for i in 'abc'] - - power = power * self.factor + log.debug("single phase shelly") + currents = [0.0, 0.0, 0.0] + meters = status['em1:0'] + currents[self.phase-1] = meters['current'] + 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..54f9f7cc9a 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 dataclasses import dataclass +from typing import Optional +import pytest + 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 - +# 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,127 @@ "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 Params: + name: str + json_data: str + factor: int = 1 + phase: int = 1 + generation: int = 1 + expected_counter_state: Optional[CounterState] = None -def test_counter_shelly_minipm_g3(monkeypatch, requests_mock: requests_mock.mock): +cases = [ + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(name="G2 - Shelly Pro3 EM Counter - Phase 2", 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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), + Params(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])), +] + + +@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) +def test_counter(params: Params, 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(SAMPLE_COUNTER_STATE) - - -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]) + assert vars(mock_counter_value_store.set.call_args[0][0]) == vars(params.expected_counter_state) From 9832cacd66a6423c55f78fc62d19b5357c8e54ea Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 4 Sep 2025 12:34:18 +0200 Subject: [PATCH 3/7] counter tests --- .../modules/devices/shelly/shelly/counter.py | 2 +- .../devices/shelly/shelly/shelly_test.py | 139 ++++++++++-------- 2 files changed, 78 insertions(+), 63 deletions(-) diff --git a/packages/modules/devices/shelly/shelly/counter.py b/packages/modules/devices/shelly/shelly/counter.py index d942a722c8..558d2249ec 100644 --- a/packages/modules/devices/shelly/shelly/counter.py +++ b/packages/modules/devices/shelly/shelly/counter.py @@ -101,7 +101,7 @@ def update(self) -> None: 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 + 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'] diff --git a/packages/modules/devices/shelly/shelly/shelly_test.py b/packages/modules/devices/shelly/shelly/shelly_test.py index 54f9f7cc9a..d72396c592 100644 --- a/packages/modules/devices/shelly/shelly/shelly_test.py +++ b/packages/modules/devices/shelly/shelly/shelly_test.py @@ -134,7 +134,7 @@ @dataclass -class Params: +class CounterParams: name: str json_data: str factor: int = 1 @@ -144,71 +144,86 @@ class Params: cases = [ - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(name="G2 - Shelly Pro3 EM Counter - Phase 2", 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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), - Params(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])), + 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])), ] @pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_counter(params: Params, monkeypatch, requests_mock: requests_mock.mock): +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)) if params.generation == 1: From 615a1802a0e1f00dbe875327de8ca43724fa6e4e Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 4 Sep 2025 13:53:49 +0200 Subject: [PATCH 4/7] fix bat and inverter, add tests --- packages/modules/devices/shelly/shelly/bat.py | 22 +-- .../modules/devices/shelly/shelly/inverter.py | 14 +- .../devices/shelly/shelly/shelly_test.py | 126 +++++++++++++++++- 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/packages/modules/devices/shelly/shelly/bat.py b/packages/modules/devices/shelly/shelly/bat.py index 3993c48eac..98ecc2edfc 100644 --- a/packages/modules/devices/shelly/shelly/bat.py +++ b/packages/modules/devices/shelly/shelly/bat.py @@ -50,17 +50,17 @@ def update(self) -> None: if "meters" in status: currents = [0.0, 0.0, 0.0] meters = status['meters'] # einphasiger shelly? - for i in range(0, 3): - currents[(i+self.phase-1) % 3] = [(float(meters[i]['power']) * self.factor) / 230 - if meters[i].get('power') else 0] + 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: currents = [0.0, 0.0, 0.0] 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']) - if meters[i].get('current') else 0] + 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 @@ -68,27 +68,27 @@ def update(self) -> None: currents = [0.0, 0.0, 0.0] meters = status['em:0'] for i in range(0, 3): - currents[(i+self.phase-1) % 3] = [float(meters[f'{alphabetical_index[i]}_current']) - if meters.get(f'{alphabetical_index[i]}_current') 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 = float(meters['total_act_power']) * self.factor # Shelly MiniPM G3 elif "pm1:0" in status: log.debug("single phase shelly") currents = [0.0, 0.0, 0.0] meters = status['pm1:0'] - currents[self.phase-1] = meters['current'] + 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") currents = [0.0, 0.0, 0.0] meters = status['switch:0'] - currents[self.phase-1] = meters['current'] + currents[self.phase-1] = meters['current'] * self.factor power = meters['apower'] * self.factor else: log.debug("single phase shelly") currents = [0.0, 0.0, 0.0] meters = status['em1:0'] - currents[self.phase-1] = meters['current'] + 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) diff --git a/packages/modules/devices/shelly/shelly/inverter.py b/packages/modules/devices/shelly/shelly/inverter.py index 39893fe5ff..1debc108ce 100644 --- a/packages/modules/devices/shelly/shelly/inverter.py +++ b/packages/modules/devices/shelly/shelly/inverter.py @@ -58,8 +58,8 @@ def update(self) -> None: 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']) - if meters[i].get('current') else 0] + 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 @@ -67,27 +67,27 @@ def update(self) -> None: currents = [0.0, 0.0, 0.0] meters = status['em:0'] for i in range(0, 3): - currents[(i+self.phase-1) % 3] = [float(meters[f'{alphabetical_index[i]}_current']) - if meters.get(f'{alphabetical_index[i]}_current') 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 = float(meters['total_act_power']) * self.factor # Shelly MiniPM G3 elif "pm1:0" in status: log.debug("single phase shelly") currents = [0.0, 0.0, 0.0] meters = status['pm1:0'] - currents[self.phase-1] = meters['current'] + 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") currents = [0.0, 0.0, 0.0] meters = status['switch:0'] - currents[self.phase-1] = meters['current'] + currents[self.phase-1] = meters['current'] * self.factor power = meters['apower'] * self.factor else: log.debug("single phase shelly") currents = [0.0, 0.0, 0.0] meters = status['em1:0'] - currents[self.phase-1] = meters['current'] + 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) diff --git a/packages/modules/devices/shelly/shelly/shelly_test.py b/packages/modules/devices/shelly/shelly/shelly_test.py index d72396c592..9721957ade 100644 --- a/packages/modules/devices/shelly/shelly/shelly_test.py +++ b/packages/modules/devices/shelly/shelly/shelly_test.py @@ -5,10 +5,10 @@ from typing import Optional import pytest -from modules.common.component_state import CounterState +from modules.common.component_state import CounterState, InverterState, BatState from modules.conftest import SAMPLE_IP -from modules.devices.shelly.shelly.config import ShellyCounterSetup -from modules.devices.shelly.shelly import counter +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 = { @@ -242,3 +242,123 @@ def test_counter(params: CounterParams, monkeypatch, requests_mock: requests_moc # 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_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 + + +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) From 4b69c8801509cc580c3afc8d5187acd377d6b9af Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 11 Sep 2025 11:21:34 +0200 Subject: [PATCH 5/7] move currents to top --- packages/modules/devices/shelly/shelly/bat.py | 7 +------ packages/modules/devices/shelly/shelly/inverter.py | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/modules/devices/shelly/shelly/bat.py b/packages/modules/devices/shelly/shelly/bat.py index 98ecc2edfc..caf4a9760c 100644 --- a/packages/modules/devices/shelly/shelly/bat.py +++ b/packages/modules/devices/shelly/shelly/bat.py @@ -46,16 +46,15 @@ def update(self) -> None: try: alphabetical_index = ['a', 'b', 'c'] + currents = [0.0, 0.0, 0.0] # GEN 1 if "meters" in status: - currents = [0.0, 0.0, 0.0] 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: - currents = [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)): @@ -65,7 +64,6 @@ def update(self) -> None: # GEN 2+ # shelly Pro3EM elif "em:0" in status: - currents = [0.0, 0.0, 0.0] 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 @@ -74,19 +72,16 @@ def update(self) -> None: # Shelly MiniPM G3 elif "pm1:0" in status: log.debug("single phase shelly") - currents = [0.0, 0.0, 0.0] 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") - currents = [0.0, 0.0, 0.0] meters = status['switch:0'] currents[self.phase-1] = meters['current'] * self.factor power = meters['apower'] * self.factor else: log.debug("single phase shelly") - currents = [0.0, 0.0, 0.0] meters = status['em1:0'] currents[self.phase-1] = meters['current'] * self.factor power = meters['act_power'] * self.factor # shelly Pro EM Gen 2 diff --git a/packages/modules/devices/shelly/shelly/inverter.py b/packages/modules/devices/shelly/shelly/inverter.py index 1debc108ce..6139fd1d3a 100644 --- a/packages/modules/devices/shelly/shelly/inverter.py +++ b/packages/modules/devices/shelly/shelly/inverter.py @@ -45,16 +45,15 @@ def update(self) -> None: status = req.get_http_session().get(status_url, timeout=3).json() try: alphabetical_index = ['a', 'b', 'c'] + currents = [0.0, 0.0, 0.0] # GEN 1 if "meters" in status: - currents = [0.0, 0.0, 0.0] 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: - currents = [0.0, 0.0, 0.0] meters = status['emeters'] # shellyEM & shelly3EM # shellyEM has one meter, shelly3EM has three meters for i in range(0, 3): @@ -64,7 +63,6 @@ def update(self) -> None: # GEN 2+ # shelly Pro3EM elif "em:0" in status: - currents = [0.0, 0.0, 0.0] 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 @@ -73,19 +71,16 @@ def update(self) -> None: # Shelly MiniPM G3 elif "pm1:0" in status: log.debug("single phase shelly") - currents = [0.0, 0.0, 0.0] 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") - currents = [0.0, 0.0, 0.0] meters = status['switch:0'] currents[self.phase-1] = meters['current'] * self.factor power = meters['apower'] * self.factor else: log.debug("single phase shelly") - currents = [0.0, 0.0, 0.0] meters = status['em1:0'] currents[self.phase-1] = meters['current'] * self.factor power = meters['act_power'] * self.factor # shelly Pro EM Gen 2 From 0b70685a115bd8c4081ef5aba02fc12574d16e15 Mon Sep 17 00:00:00 2001 From: ndrsnhs Date: Thu, 11 Sep 2025 11:37:37 +0200 Subject: [PATCH 6/7] update config --- packages/helpermodules/update_config.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 094067adcd..62f44b2519 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 96 + DATASTORE_VERSION = 97 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2553,3 +2553,14 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: payload} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 96) + + def upgrade_datastore_96(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/system/device/[0-9]+", 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", 97) From 891f7433dfde0119f3f73b3fe73415fe29d72fbd Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 27 Oct 2025 11:04:52 +0100 Subject: [PATCH 7/7] fix update config --- packages/helpermodules/update_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 96048d9e76..017af2dca8 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -2575,7 +2575,7 @@ def upgrade_datastore_96(self) -> None: def upgrade_datastore_97(self) -> None: def upgrade(topic: str, payload) -> None: - if re.search("openWB/system/device/[0-9]+", topic) is not 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"]: