Skip to content

Commit

Permalink
Merge pull request #200 from kbialek/feature/wait-until-data-is-ready
Browse files Browse the repository at this point in the history
Add data readiness check to mitigate publishing zero values
  • Loading branch information
kbialek authored Sep 19, 2024
2 parents 70a37d7 + c789444 commit 8a5fce7
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 8 deletions.
15 changes: 14 additions & 1 deletion src/deye_inverter_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
21 changes: 19 additions & 2 deletions src/deye_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
"""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/deye_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
82 changes: 78 additions & 4 deletions tests/deye_inverter_state_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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

0 comments on commit 8a5fce7

Please sign in to comment.