Skip to content

Commit ed098cc

Browse files
committed
Add option to align FindMyAccessories key generation
Sometimes the key generation diverges for example if the accessory has no power. FindMy solves this be re-aligning the key generation if a btle connection is established. FindMy.app stores there alignment records in the `KeyAlignmentRecord` directory. This PR extends the FindMyAccessory class to read those records during `from_plist`-generation and re-sync the key generation by this
1 parent 4b7a9cb commit ed098cc

File tree

2 files changed

+60
-17
lines changed

2 files changed

+60
-17
lines changed

examples/real_airtag.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import annotations
66

7+
import argparse
78
import logging
89
import sys
910
from pathlib import Path
@@ -19,10 +20,15 @@
1920
logging.basicConfig(level=logging.INFO)
2021

2122

22-
def main(plist_path: str) -> int:
23+
def main(plist_path: Path, alignment_plist_path: Path | None) -> int:
2324
# Step 0: create an accessory key generator
24-
with Path(plist_path).open("rb") as f:
25-
airtag = FindMyAccessory.from_plist(f)
25+
with plist_path.open("rb") as f:
26+
f2 = alignment_plist_path.open("rb") if alignment_plist_path else None
27+
28+
airtag = FindMyAccessory.from_plist(f, f2)
29+
30+
if f2:
31+
f2.close()
2632

2733
# Step 1: log into an Apple account
2834
print("Logging into account")
@@ -43,10 +49,9 @@ def main(plist_path: str) -> int:
4349

4450

4551
if __name__ == "__main__":
46-
if len(sys.argv) < 2:
47-
print(f"Usage: {sys.argv[0]} <path to accessory plist>", file=sys.stderr)
48-
print(file=sys.stderr)
49-
print("The plist file should be dumped from MacOS's FindMy app.", file=sys.stderr)
50-
sys.exit(1)
52+
parser = argparse.ArgumentParser()
53+
parser.add_argument("plist_path", type=Path)
54+
parser.add_argument("--alignment_plist_path", default=None, type=Path)
55+
args = parser.parse_args()
5156

52-
sys.exit(main(sys.argv[1]))
57+
sys.exit(main(args.plist_path, args.alignment_plist_path))

findmy/accessory.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ def __init__( # noqa: PLR0913
7777
name: str | None = None,
7878
model: str | None = None,
7979
identifier: str | None = None,
80+
alignment_date: datetime | None = None,
81+
alignment_index: int | None = None,
8082
) -> None:
8183
"""
8284
Initialize a FindMyAccessory. These values are usually obtained during pairing.
@@ -98,6 +100,16 @@ def __init__( # noqa: PLR0913
98100
self._name = name
99101
self._model = model
100102
self._identifier = identifier
103+
self._alignment_date = (
104+
alignment_date if alignment_date is not None else paired_at
105+
)
106+
self._alignment_index = alignment_index if alignment_index is not None else 0
107+
if self._alignment_date.tzinfo is None:
108+
self._alignment_date = self._alignment_date.astimezone()
109+
logging.warning(
110+
"Alignment datetime is timezone-naive. Assuming system tz: %s.",
111+
self._alignment_date.tzname(),
112+
)
101113

102114
@property
103115
def paired_at(self) -> datetime:
@@ -140,25 +152,29 @@ def keys_at(self, ind: int | datetime) -> set[KeyPair]:
140152
secondary_offset = 0
141153

142154
if isinstance(ind, datetime):
143-
# number of 15-minute slots since pairing time
144-
ind = (
155+
# number of 15-minute slots since alignment
156+
slots_since_alignment = (
145157
int(
146-
(ind - self._paired_at).total_seconds() / (15 * 60),
158+
(ind - self._alignment_date).total_seconds() / (15 * 60),
147159
)
148160
+ 1
149161
)
162+
ind = self._alignment_index + slots_since_alignment
163+
150164
# number of slots until first 4 am
151-
first_rollover = self._paired_at.astimezone().replace(
165+
first_rollover = self._alignment_date.astimezone().replace(
152166
hour=4,
153167
minute=0,
154168
second=0,
155169
microsecond=0,
156170
)
157-
if first_rollover < self._paired_at: # we rolled backwards, so increment the day
171+
if (
172+
first_rollover < self._alignment_date
173+
): # we rolled backwards, so increment the day
158174
first_rollover += timedelta(days=1)
159175
secondary_offset = (
160176
int(
161-
(first_rollover - self._paired_at).total_seconds() / (15 * 60),
177+
(first_rollover - self._alignment_date).total_seconds() / (15 * 60),
162178
)
163179
+ 1
164180
)
@@ -177,7 +193,9 @@ def keys_at(self, ind: int | datetime) -> set[KeyPair]:
177193
return possible_keys
178194

179195
@classmethod
180-
def from_plist(cls, plist: IO[bytes]) -> FindMyAccessory:
196+
def from_plist(
197+
cls, plist: IO[bytes], key_alignment_plist: IO[bytes] | None = None
198+
) -> FindMyAccessory:
181199
"""Create a FindMyAccessory from a .plist file dumped from the FindMy app."""
182200
device_data = plistlib.load(plist)
183201

@@ -201,7 +219,27 @@ def from_plist(cls, plist: IO[bytes]) -> FindMyAccessory:
201219
model = device_data["model"]
202220
identifier = device_data["identifier"]
203221

204-
return cls(master_key, skn, sks, paired_at, None, model, identifier)
222+
alignment_date = None
223+
index = None
224+
if key_alignment_plist:
225+
alignment_data = plistlib.load(key_alignment_plist)
226+
227+
alignment_date = alignment_data["lastIndexObservationDate"].replace(
228+
tzinfo=timezone.utc,
229+
)
230+
index = alignment_data["lastIndexObserved"]
231+
232+
return cls(
233+
master_key,
234+
skn,
235+
sks,
236+
paired_at,
237+
None,
238+
model,
239+
identifier,
240+
alignment_date,
241+
index,
242+
)
205243

206244

207245
class AccessoryKeyGenerator(KeyGenerator[KeyPair]):

0 commit comments

Comments
 (0)