From c78944452dee19a85059c9e133da1a8634aeba3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Bia=C5=82ek?= Date: Mon, 16 Sep 2024 21:55:06 +0200 Subject: [PATCH] Add data readiness check to mitigate publishing zero values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Krzysztof BiaƂek --- src/deye_inverter_state.py | 15 +++++- src/deye_sensor.py | 21 +++++++- src/deye_sensors.py | 2 +- tests/deye_inverter_state_test.py | 82 +++++++++++++++++++++++++++++-- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/deye_inverter_state.py b/src/deye_inverter_state.py index a4c9eda..bd17647 100644 --- a/src/deye_inverter_state.py +++ b/src/deye_inverter_state.py @@ -54,7 +54,11 @@ def read_from_logger(self): regs |= self.__modbus.read_registers(reg_range.first_reg_address, reg_range.last_reg_address) events = DeyeEventList(logger_index=self.__logger_config.index) events.append(DeyeLoggerStatusEvent(len(regs) > 0)) - events += self.__get_observations_from_reg_values(regs) + observation_events = self.__get_observations_from_reg_values(regs) + data_is_ready = self.__is_data_ready(observation_events) + self.__log.debug(f"Data readiness check result: {data_is_ready}") + if data_is_ready: + events += observation_events if not self.__config.publish_on_change or self.__is_device_observation_changed(events): for processor in self.__processors: processor.process(events) @@ -101,3 +105,12 @@ def __is_device_observation_changed(self, events: DeyeEventList) -> bool: self.__event_updated = time.time() self.__last_observations = events return True + + def __is_data_ready(self, observation_events: list[DeyeObservationEvent]) -> bool: + readiness_check_observations = [ + event.observation for event in observation_events if event.observation.sensor.is_readiness_check + ] + self.__log.debug(f"Data readiness observations: {readiness_check_observations}") + if not readiness_check_observations: + return True + return len([o for o in readiness_check_observations if o.value != 0]) == len(readiness_check_observations) diff --git a/src/deye_sensor.py b/src/deye_sensor.py index f03ae1e..1ccc37b 100644 --- a/src/deye_sensor.py +++ b/src/deye_sensor.py @@ -59,6 +59,10 @@ def data_type(self) -> str: def scale_factor(self) -> float: pass + @abstractproperty + def is_readiness_check(self) -> bool: + pass + @abstractmethod def read_value(self, registers: dict[int, bytearray]): """ @@ -127,14 +131,18 @@ def print_format(self) -> str: def groups(self) -> [str]: return self.__delegate.groups - @abstractproperty + @property def data_type(self) -> str: return self.__delegate.data_type - @abstractproperty + @property def scale_factor(self) -> float: return self.__delegate.scale_factor + @property + def is_readiness_check(self) -> bool: + return self.__delegate.is_readiness_check + def read_value(self, registers: dict[int, bytearray]): now = datetime.now() value = self.__delegate.read_value(registers) @@ -170,6 +178,7 @@ def __init__(self, name: str, mqtt_topic_suffix="", unit="", print_format="{:s}" self.__print_format = print_format assert len(groups) > 0, f"Sensor {name} must belong to at least one group" self.__groups = groups + self.__is_readiness_check = False @property def name(self) -> str: @@ -199,6 +208,14 @@ def data_type(self) -> str: def scale_factor(self) -> float: return 1 + @property + def is_readiness_check(self) -> bool: + return self.__is_readiness_check + + def use_as_readiness_check(self) -> Sensor: + self.__is_readiness_check = True + return self + class SingleRegisterSensor(AbstractSensor): """ diff --git a/src/deye_sensors.py b/src/deye_sensors.py index 7d6eb09..0794ec2 100644 --- a/src/deye_sensors.py +++ b/src/deye_sensors.py @@ -204,7 +204,7 @@ ) production_total_sensor = DoubleRegisterSensor( "Production Total", 0x3F, 0.1, mqtt_topic_suffix="total_energy", unit="kWh", groups=["string", "micro"] -) +).use_as_readiness_check() # Temperature sensors string_radiator_temp_sensor = SingleRegisterSensor( diff --git a/tests/deye_inverter_state_test.py b/tests/deye_inverter_state_test.py index 9b0b890..442eb6c 100644 --- a/tests/deye_inverter_state_test.py +++ b/tests/deye_inverter_state_test.py @@ -20,18 +20,24 @@ from unittest.mock import MagicMock, patch from deye_inverter_state import DeyeInverterState -from deye_events import DeyeEventList, DeyeLoggerStatusEvent, DeyeObservationEvent, Observation -from deye_sensor import AbstractSensor, SensorRegisterRanges +from deye_events import DeyeEventList, DeyeLoggerStatusEvent, DeyeObservationEvent, Observation, DeyeEventProcessor +from deye_sensor import AbstractSensor, SensorRegisterRanges, SensorRegisterRange +from deye_modbus import DeyeModbus class FakeSensor(AbstractSensor): - def __init__(self, name: str, value: float): + def __init__(self, name: str, value: float, is_readiness_check=False): super().__init__(name, groups=["float"], print_format="{:0.1f}") self.value = value + self.__is_readiness_check = is_readiness_check def read_value(self, registers): return self.value + @property + def is_readiness_check(self): + return self.__is_readiness_check + class TestInverterState(unittest.TestCase): def test_no_last_observation(self): @@ -69,7 +75,7 @@ def test_is_device_offline(self): self.assertFalse(inverter_state._DeyeInverterState__is_device_observation_changed(events_new)) @patch("time.time") - def test_is_evenets_unchanged(self, time): + def test_is_events_unchanged(self, time): # Create the InverterState instance with a mock configuration config_mock = MagicMock() config_mock.logger_config.protocol = "tcp" @@ -146,3 +152,71 @@ def test_is_events_changed(self): ] ) self.assertTrue(inverter_state._DeyeInverterState__is_device_observation_changed(events_new)) + + def test_readiness_test_success(self): + # given: create processor + processor: DeyeEventProcessor = MagicMock() + + # Create the InverterState instance with a mock configuration + config_mock = MagicMock() + config_mock.logger_config.protocol = "tcp" + config_mock.publish_on_change = False + + # and + modbus: DeyeModbus = MagicMock() + + # and + reg_ranges = SensorRegisterRanges([], [], 0) + + # and + sensors = [ + FakeSensor("Energy", 0, is_readiness_check=True), + FakeSensor("Power", 0), + ] + + # and + inverter_state = DeyeInverterState( + config_mock, config_mock.logger_config, reg_ranges, modbus, sensors, [processor] + ) + + # when + inverter_state.read_from_logger() + + # then + processor.process.assert_called_once() + published_events = processor.process.call_args.args[0] + assert len(published_events) == 1 # as only online event is published + + def test_readiness_test_failure(self): + # given: create processor + processor: DeyeEventProcessor = MagicMock() + + # Create the InverterState instance with a mock configuration + config_mock = MagicMock() + config_mock.logger_config.protocol = "tcp" + config_mock.publish_on_change = False + + # and + modbus: DeyeModbus = MagicMock() + + # and + reg_ranges = SensorRegisterRanges([], [], 0) + + # and + sensors = [ + FakeSensor("Energy", 1, is_readiness_check=True), + FakeSensor("Power", 0), + ] + + # and + inverter_state = DeyeInverterState( + config_mock, config_mock.logger_config, reg_ranges, modbus, sensors, [processor] + ) + + # when + inverter_state.read_from_logger() + + # then + processor.process.assert_called_once() + published_events = processor.process.call_args.args[0] + assert len(published_events) == 3 # as online event and two observation events are published