diff --git a/src/pyftms/client/backends/updater.py b/src/pyftms/client/backends/updater.py index dc51867a09..7d322b625f 100644 --- a/src/pyftms/client/backends/updater.py +++ b/src/pyftms/client/backends/updater.py @@ -46,16 +46,24 @@ def _on_notify(self, c: BleakGATTCharacteristic, data: bytearray) -> None: _LOGGER.debug("'More Data' bit is set. Waiting for next data.") return + result = self._result.copy() + prev = self._prev.copy() + # 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 any(result.values()): + missing = object() + update = { + key: value + for key, value in result.items() + if prev.get(key, missing) != value + } - if update := {k: self._result[k] for k, _ in update}: + if 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._prev = result self._result.clear() diff --git a/src/pyftms/client/manager.py b/src/pyftms/client/manager.py index a62dd8cc26..0fa664eac2 100644 --- a/src/pyftms/client/manager.py +++ b/src/pyftms/client/manager.py @@ -61,8 +61,8 @@ def get_setting(self, name: str) -> Any: @property def properties(self) -> UpdateEventData: - """Read-only updateable properties mapping.""" - return cast(UpdateEventData, MappingProxyType(self._properties)) + """Read-only snapshot of properties mapping.""" + return cast(UpdateEventData, MappingProxyType(self._properties.copy())) @property def live_properties(self) -> tuple[str, ...]: @@ -75,8 +75,8 @@ def live_properties(self) -> tuple[str, ...]: @property def settings(self) -> SetupEventData: - """Read-only updateable settings mapping.""" - return cast(SetupEventData, MappingProxyType(self._settings)) + """Read-only snapshot of settings mapping.""" + return cast(SetupEventData, MappingProxyType(self._settings.copy())) @property def training_status(self) -> TrainingStatusCode: diff --git a/tests/test_manager.py b/tests/test_manager.py new file mode 100644 index 0000000000..769c51b8c2 --- /dev/null +++ b/tests/test_manager.py @@ -0,0 +1,44 @@ +from pyftms.client.backends import SetupEvent, UpdateEvent +from pyftms.client.manager import PropertiesManager + + +def test_properties_returns_stable_snapshot(): + manager = PropertiesManager() + manager._on_event(UpdateEvent("update", {"speed_instant": 3.0})) + + properties = manager.properties + iterator = iter(properties.items()) + assert next(iterator) == ("speed_instant", 3.0) + + manager._on_event(UpdateEvent("update", {"cadence_instant": 90.0})) + + assert list(iterator) == [] + assert dict(properties) == {"speed_instant": 3.0} + assert manager.properties["cadence_instant"] == 90.0 + + +def test_settings_returns_stable_snapshot(): + manager = PropertiesManager() + manager._on_event( + SetupEvent( + event_id="setup", + event_data={"target_resistance": 5.0}, + event_source="callback", + ) + ) + + settings = manager.settings + iterator = iter(settings.items()) + assert next(iterator) == ("target_resistance", 5.0) + + manager._on_event( + SetupEvent( + event_id="setup", + event_data={"target_power": 120}, + event_source="callback", + ) + ) + + assert list(iterator) == [] + assert dict(settings) == {"target_resistance": 5.0} + assert manager.settings["target_power"] == 120 diff --git a/tests/test_updater.py b/tests/test_updater.py new file mode 100644 index 0000000000..2319582506 --- /dev/null +++ b/tests/test_updater.py @@ -0,0 +1,35 @@ +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_uses_stable_snapshot_for_callback_event(): + events = [] + updater = DataUpdater(IndoorBikeData, events.append) + updater._serializer = _FakeSerializer( + {"speed_instant": 5.0}, + {"cadence_instant": 90.0}, + ) + + updater._on_notify(None, bytearray(b"\x00")) + first_event_data = events[0].event_data + + updater._on_notify(None, bytearray(b"\x00")) + + assert first_event_data == {"speed_instant": 5.0} + assert events[1].event_data == {"cadence_instant": 90.0}