Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/pyftms/client/backends/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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()
108 changes: 108 additions & 0 deletions tests/test_updater.py
Original file line number Diff line number Diff line change
@@ -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}