@@ -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
207245class AccessoryKeyGenerator (KeyGenerator [KeyPair ]):
0 commit comments