66
77from __future__ import annotations
88
9- import json
109import logging
1110import plistlib
1211from abc import ABC , abstractmethod
1312from datetime import datetime , timedelta , timezone
1413from pathlib import Path
15- from typing import IO , TYPE_CHECKING , overload
14+ from typing import IO , TYPE_CHECKING , Literal , TypedDict , overload
1615
1716from typing_extensions import override
1817
18+ from findmy .util .abc import Serializable
19+ from findmy .util .files import read_data_json , save_and_return_json
20+
1921from .keys import KeyGenerator , KeyPair , KeyType
2022from .util import crypto
2123
2224if TYPE_CHECKING :
23- from collections .abc import Generator , Mapping
25+ from collections .abc import Generator
2426
2527logger = logging .getLogger (__name__ )
2628
2729
30+ class FindMyAccessoryMapping (TypedDict ):
31+ """JSON mapping representing state of a FindMyAccessory instance."""
32+
33+ type : Literal ["accessory" ]
34+ master_key : str
35+ skn : str
36+ sks : str
37+ paired_at : str
38+ name : str | None
39+ model : str | None
40+ identifier : str | None
41+
42+
2843class RollingKeyPairSource (ABC ):
2944 """A class that generates rolling `KeyPair`s."""
3045
@@ -67,7 +82,7 @@ def keys_between(self, start: int | datetime, end: int | datetime) -> set[KeyPai
6782 return keys
6883
6984
70- class FindMyAccessory (RollingKeyPairSource ):
85+ class FindMyAccessory (RollingKeyPairSource , Serializable [ FindMyAccessoryMapping ] ):
7186 """A findable Find My-accessory using official key rollover."""
7287
7388 def __init__ ( # noqa: PLR0913
@@ -242,9 +257,10 @@ def from_plist(
242257 identifier = identifier ,
243258 )
244259
245- def to_json (self , path : str | Path | None = None ) -> dict [str , str | int | None ]:
246- """Convert the accessory to a JSON-serializable dictionary."""
247- d = {
260+ @override
261+ def to_json (self , path : str | Path | None = None , / ) -> FindMyAccessoryMapping :
262+ res : FindMyAccessoryMapping = {
263+ "type" : "accessory" ,
248264 "master_key" : self ._primary_gen .master_key .hex (),
249265 "skn" : self .skn .hex (),
250266 "sks" : self .sks .hex (),
@@ -253,23 +269,32 @@ def to_json(self, path: str | Path | None = None) -> dict[str, str | int | None]
253269 "model" : self .model ,
254270 "identifier" : self .identifier ,
255271 }
256- if path is not None :
257- Path (path ).write_text (json .dumps (d , indent = 4 ))
258- return d
272+
273+ return save_and_return_json (res , path )
259274
260275 @classmethod
261- def from_json (cls , json_ : str | Path | Mapping , / ) -> FindMyAccessory :
262- """Create a FindMyAccessory from a JSON file."""
263- data = json .loads (Path (json_ ).read_text ()) if isinstance (json_ , (str , Path )) else json_
264- return cls (
265- master_key = bytes .fromhex (data ["master_key" ]),
266- skn = bytes .fromhex (data ["skn" ]),
267- sks = bytes .fromhex (data ["sks" ]),
268- paired_at = datetime .fromisoformat (data ["paired_at" ]),
269- name = data ["name" ],
270- model = data ["model" ],
271- identifier = data ["identifier" ],
272- )
276+ @override
277+ def from_json (
278+ cls ,
279+ val : str | Path | FindMyAccessoryMapping ,
280+ / ,
281+ ) -> FindMyAccessory :
282+ val = read_data_json (val )
283+ assert val ["type" ] == "accessory"
284+
285+ try :
286+ return cls (
287+ master_key = bytes .fromhex (val ["master_key" ]),
288+ skn = bytes .fromhex (val ["skn" ]),
289+ sks = bytes .fromhex (val ["sks" ]),
290+ paired_at = datetime .fromisoformat (val ["paired_at" ]),
291+ name = val ["name" ],
292+ model = val ["model" ],
293+ identifier = val ["identifier" ],
294+ )
295+ except KeyError as e :
296+ msg = f"Failed to restore account data: { e } "
297+ raise ValueError (msg ) from None
273298
274299
275300class AccessoryKeyGenerator (KeyGenerator [KeyPair ]):
0 commit comments