Skip to content

Commit 64347b9

Browse files
authored
Merge pull request #125 from pajowu/pajowu/aligment
Add option to align FindMyAccessories key generation
2 parents c0ab486 + 1d3affb commit 64347b9

File tree

2 files changed

+45
-15
lines changed

2 files changed

+45
-15
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
@@ -30,10 +31,15 @@
3031
logging.basicConfig(level=logging.INFO)
3132

3233

33-
def main(plist_path: str) -> int:
34+
def main(plist_path: Path, alignment_plist_path: Path | None) -> int:
3435
# Step 0: create an accessory key generator
35-
with Path(plist_path).open("rb") as f:
36-
airtag = FindMyAccessory.from_plist(f)
36+
with plist_path.open("rb") as f:
37+
f2 = alignment_plist_path.open("rb") if alignment_plist_path else None
38+
39+
airtag = FindMyAccessory.from_plist(f, f2)
40+
41+
if f2:
42+
f2.close()
3743

3844
# Step 1: log into an Apple account
3945
print("Logging into account")
@@ -56,10 +62,9 @@ def main(plist_path: str) -> int:
5662

5763

5864
if __name__ == "__main__":
59-
if len(sys.argv) < 2:
60-
print(f"Usage: {sys.argv[0]} <path to accessory plist>", file=sys.stderr)
61-
print(file=sys.stderr)
62-
print("The plist file should be dumped from MacOS's FindMy app.", file=sys.stderr)
63-
sys.exit(1)
65+
parser = argparse.ArgumentParser()
66+
parser.add_argument("plist_path", type=Path)
67+
parser.add_argument("--alignment_plist_path", default=None, type=Path)
68+
args = parser.parse_args()
6469

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

findmy/accessory.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ def __init__( # noqa: PLR0913
9595
name: str | None = None,
9696
model: str | None = None,
9797
identifier: str | None = None,
98+
alignment_date: datetime | None = None,
99+
alignment_index: int | None = None,
98100
) -> None:
99101
"""
100102
Initialize a FindMyAccessory. These values are usually obtained during pairing.
@@ -116,6 +118,14 @@ def __init__( # noqa: PLR0913
116118
self._name = name
117119
self._model = model
118120
self._identifier = identifier
121+
self._alignment_date = alignment_date if alignment_date is not None else paired_at
122+
self._alignment_index = alignment_index if alignment_index is not None else 0
123+
if self._alignment_date.tzinfo is None:
124+
self._alignment_date = self._alignment_date.astimezone()
125+
logger.warning(
126+
"Alignment datetime is timezone-naive. Assuming system tz: %s.",
127+
self._alignment_date.tzname(),
128+
)
119129

120130
@property
121131
def master_key(self) -> bytes:
@@ -173,25 +183,27 @@ def keys_at(self, ind: int | datetime) -> set[KeyPair]:
173183
secondary_offset = 0
174184

175185
if isinstance(ind, datetime):
176-
# number of 15-minute slots since pairing time
177-
ind = (
186+
# number of 15-minute slots since alignment
187+
slots_since_alignment = (
178188
int(
179-
(ind - self._paired_at).total_seconds() / (15 * 60),
189+
(ind - self._alignment_date).total_seconds() / (15 * 60),
180190
)
181191
+ 1
182192
)
193+
ind = self._alignment_index + slots_since_alignment
194+
183195
# number of slots until first 4 am
184-
first_rollover = self._paired_at.astimezone().replace(
196+
first_rollover = self._alignment_date.astimezone().replace(
185197
hour=4,
186198
minute=0,
187199
second=0,
188200
microsecond=0,
189201
)
190-
if first_rollover < self._paired_at: # we rolled backwards, so increment the day
202+
if first_rollover < self._alignment_date: # we rolled backwards, so increment the day
191203
first_rollover += timedelta(days=1)
192204
secondary_offset = (
193205
int(
194-
(first_rollover - self._paired_at).total_seconds() / (15 * 60),
206+
(first_rollover - self._alignment_date).total_seconds() / (15 * 60),
195207
)
196208
+ 1
197209
)
@@ -213,6 +225,7 @@ def keys_at(self, ind: int | datetime) -> set[KeyPair]:
213225
def from_plist(
214226
cls,
215227
plist: str | Path | dict | bytes | IO[bytes],
228+
key_alignment_plist: IO[bytes] | None = None,
216229
*,
217230
name: str | None = None,
218231
) -> FindMyAccessory:
@@ -247,6 +260,16 @@ def from_plist(
247260
model = device_data["model"]
248261
identifier = device_data["identifier"]
249262

263+
alignment_date = None
264+
index = None
265+
if key_alignment_plist:
266+
alignment_data = plistlib.load(key_alignment_plist)
267+
268+
alignment_date = alignment_data["lastIndexObservationDate"].replace(
269+
tzinfo=timezone.utc,
270+
)
271+
index = alignment_data["lastIndexObserved"]
272+
250273
return cls(
251274
master_key=master_key,
252275
skn=skn,
@@ -255,6 +278,8 @@ def from_plist(
255278
name=name,
256279
model=model,
257280
identifier=identifier,
281+
alignment_date=alignment_date,
282+
alignment_index=index,
258283
)
259284

260285
@override

0 commit comments

Comments
 (0)