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
16 changes: 12 additions & 4 deletions src/pyftms/client/backends/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
8 changes: 4 additions & 4 deletions src/pyftms/client/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...]:
Expand All @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions tests/test_updater.py
Original file line number Diff line number Diff line change
@@ -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}