From f0e31e1b5a07037952f1a98cde0f1b2904d56694 Mon Sep 17 00:00:00 2001 From: Cherry Date: Thu, 25 Jun 2026 10:41:21 +1200 Subject: [PATCH] Fix rolling accessory location history fetching --- findmy/reports/account.py | 9 ++--- findmy/reports/reports.py | 12 +++++- tests/test_reports.py | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 tests/test_reports.py diff --git a/findmy/reports/account.py b/findmy/reports/account.py index 5077549..93eeaaf 100644 --- a/findmy/reports/account.py +++ b/findmy/reports/account.py @@ -785,7 +785,7 @@ async def fetch_location( | None ): """See :meth:`BaseAppleAccount.fetch_location`.""" - hist = await self.fetch_location_history(keys) + hist = await self._reports.fetch_location_history(keys, only_latest=True) if isinstance(hist, list): return sorted(hist)[-1] if hist else None @@ -1188,11 +1188,8 @@ def fetch_location( | None ): """See :meth:`BaseAppleAccount.fetch_location`.""" - hist = self.fetch_location_history(keys) - if isinstance(hist, list): - return sorted(hist)[-1] if hist else None - - return {dev: sorted(reports)[-1] if reports else None for dev, reports in hist.items()} + coro = self._asyncacc.fetch_location(keys) + return self._evt_loop.run_until_complete(coro) @override def get_anisette_headers( diff --git a/findmy/reports/reports.py b/findmy/reports/reports.py index 3094d73..f7b5697 100644 --- a/findmy/reports/reports.py +++ b/findmy/reports/reports.py @@ -342,18 +342,24 @@ def __init__(self, account: AsyncAppleAccount) -> None: async def fetch_location_history( self, device: HasHashedPublicKey, + *, + only_latest: bool = False, ) -> list[LocationReport]: ... @overload async def fetch_location_history( self, device: RollingKeyPairSource, + *, + only_latest: bool = False, ) -> list[LocationReport]: ... @overload async def fetch_location_history( self, device: Sequence[HasHashedPublicKey | RollingKeyPairSource], + *, + only_latest: bool = False, ) -> dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]: ... async def fetch_location_history( @@ -361,6 +367,8 @@ async def fetch_location_history( device: HasHashedPublicKey | RollingKeyPairSource | Sequence[HasHashedPublicKey | RollingKeyPairSource], + *, + only_latest: bool = False, ) -> ( list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]] ): @@ -386,7 +394,7 @@ async def fetch_location_history( if isinstance(device, RollingKeyPairSource): # key generator - return await self._fetch_accessory_reports(device, only_latest=True) + return await self._fetch_accessory_reports(device, only_latest=only_latest) if not isinstance(device, list) or not all( isinstance(x, HasHashedPublicKey | RollingKeyPairSource) for x in device @@ -408,7 +416,7 @@ async def fetch_location_history( static_keys.append(dev) elif isinstance(dev, RollingKeyPairSource): # query immediately - reports[dev] = await self._fetch_accessory_reports(dev, only_latest=True) + reports[dev] = await self._fetch_accessory_reports(dev, only_latest=only_latest) if static_keys: # batch request for static keys key_reports = await self._fetch_key_reports(static_keys) diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 0000000..46f27e5 --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,79 @@ +"""Location report fetcher tests.""" + +import asyncio +from datetime import datetime + +from typing_extensions import override + +from findmy import KeyPair +from findmy.accessory import RollingKeyPairSource +from findmy.reports.reports import LocationReport, LocationReportsFetcher + + +class DummyAccessory(RollingKeyPairSource): + """Minimal rolling key source for dispatch tests.""" + + @override + def get_min_index(self, dt: datetime) -> int: + """Return a dummy minimum key index.""" + _ = dt + return 0 + + @override + def get_max_index(self, dt: datetime) -> int: + """Return a dummy maximum key index.""" + _ = dt + return 0 + + @override + def update_alignment(self, dt: datetime, index: int) -> None: + """Ignore alignment updates.""" + _ = (dt, index) + + @override + def keys_at(self, ind: int) -> set[KeyPair]: + """Return no keys.""" + _ = ind + return set() + + +class RecordingFetcher(LocationReportsFetcher): + """Fetcher that records only_latest values passed to accessory fetching.""" + + def __init__(self, accessory: RollingKeyPairSource) -> None: + """Initialize the recording fetcher.""" + super().__init__(account=None) # type: ignore[arg-type] + self.accessory = accessory + self.calls: list[bool] = [] + + @override + async def _fetch_accessory_reports( + self, + accessory: RollingKeyPairSource, + only_latest: bool = False, + ) -> list[LocationReport]: + assert accessory is self.accessory + self.calls.append(only_latest) + return [] + + +def test_fetch_location_history_fetches_full_rolling_history_by_default() -> None: + """Rolling-key history should not stop after the latest report.""" + accessory = DummyAccessory() + fetcher = RecordingFetcher(accessory) + + result = asyncio.run(fetcher.fetch_location_history(accessory)) + + assert result == [] + assert fetcher.calls == [False] + + +def test_fetch_location_history_can_fetch_latest_rolling_report() -> None: + """Internal latest-location callers can still stop after the latest report.""" + accessory = DummyAccessory() + fetcher = RecordingFetcher(accessory) + + result = asyncio.run(fetcher.fetch_location_history([accessory], only_latest=True)) + + assert result == {accessory: []} + assert fetcher.calls == [True]