diff --git a/src/pyftms/client/backends/updater.py b/src/pyftms/client/backends/updater.py index dc51867a09..a37fd5a0db 100644 --- a/src/pyftms/client/backends/updater.py +++ b/src/pyftms/client/backends/updater.py @@ -24,11 +24,13 @@ def __init__( self._serializer = get_serializer(model) self._prev: dict[str, Any] = {} self._result: dict[str, Any] = {} + self._seen_nonzero = False def reset(self) -> None: """Resetting state. Call while disconnection event.""" self._prev.clear() self._result.clear() + self._seen_nonzero = False async def subscribe(self, cli: BleakClient, uuid: str) -> None: """Subscribe for notification.""" @@ -46,16 +48,23 @@ def _on_notify(self, c: BleakGATTCharacteristic, data: bytearray) -> None: _LOGGER.debug("'More Data' bit is set. Waiting for next data.") return - # My device sends a lot of null packets during wakeup and sleep mode. - # So I just filter null packets. - if any(self._result.values()): - update = self._result.items() ^ self._prev.items() - - if update := {k: self._result[k] for k, _ in update}: - _LOGGER.debug("Update data: %s", update) - update = cast(UpdateEventData, update) # unsafe casting - update = UpdateEvent(event_id="update", event_data=update) - self._cb(update) - self._prev = self._result.copy() + # Some devices send zero-only realtime packets during wakeup/sleep. + # Ignore those until we have seen a real nonzero packet, but still + # preserve valid nonzero-to-zero transitions after activity begins. + if self._result and all(value == 0 for value in self._result.values()): + if not self._seen_nonzero: + self._result.clear() + return + else: + self._seen_nonzero = True + + update = self._result.items() ^ self._prev.items() + + if update := {k: self._result[k] for k, _ in update}: + _LOGGER.debug("Update data: %s", update) + update = cast(UpdateEventData, update) # unsafe casting + update = UpdateEvent(event_id="update", event_data=update) + self._cb(update) + self._prev = self._result.copy() self._result.clear() diff --git a/tests/test_updater.py b/tests/test_updater.py new file mode 100644 index 0000000000..828ffe98fe --- /dev/null +++ b/tests/test_updater.py @@ -0,0 +1,108 @@ +from pyftms.client.backends import DataUpdater +from pyftms.models import IndoorBikeData + + +class _FakeRealtimeModel: + def __init__(self, data): + self._data = data + + def _asdict(self): + return self._data + + +class _FakeSerializer: + def __init__(self, *payloads): + self._payloads = iter(payloads) + + def deserialize(self, _data): + return _FakeRealtimeModel(next(self._payloads)) + + +def test_updater_emits_zero_value_changes(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 5.0, "cadence_instant": 50.0}, + {"speed_instant": 0.0, "cadence_instant": 0.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + updater._on_notify(None, bytearray(b"\x00")) + + assert len(events) == 2 + assert events[0].event_id == "update" + assert events[0].event_data == { + "speed_instant": 5.0, + "cadence_instant": 50.0, + } + assert events[1].event_id == "update" + assert events[1].event_data == { + "speed_instant": 0.0, + "cadence_instant": 0.0, + } + + +def test_updater_suppresses_zero_only_packets_before_first_activity(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 0.0}, + {"speed_instant": 0.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + updater._on_notify(None, bytearray(b"\x00")) + + assert events == [] + + +def test_updater_deduplicates_repeated_zero_packets(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 3.0}, + {"speed_instant": 0.0}, + {"speed_instant": 0.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + updater._on_notify(None, bytearray(b"\x00")) + updater._on_notify(None, bytearray(b"\x00")) + + assert len(events) == 2 + assert events[0].event_data == {"speed_instant": 3.0} + assert events[1].event_data == {"speed_instant": 0.0} + + +def test_updater_emits_mixed_packets_before_seen_nonzero(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 0.0, "cadence_instant": 50.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + + assert len(events) == 1 + assert events[0].event_data == { + "speed_instant": 0.0, + "cadence_instant": 50.0, + } + + +def test_updater_reset_restores_startup_zero_suppression(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 4.0}, + {"speed_instant": 0.0}, + {"speed_instant": 0.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + updater.reset() + updater._on_notify(None, bytearray(b"\x00")) + updater._on_notify(None, bytearray(b"\x00")) + + assert len(events) == 1 + assert events[0].event_data == {"speed_instant": 4.0}