Coverage for icloudpy/services/findmyiphone.py: 67%
82 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-12 17:36 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-12 17:36 +0000
1"""Find my iPhone service."""
3import json
5from icloudpy.exceptions import ICloudPyNoDevicesException
8class FindMyiPhoneServiceManager:
9 """The 'Find my iPhone' iCloud service
11 This connects to iCloud and return phone data including the near-realtime
12 latitude and longitude.
13 """
15 def __init__(self, service_root, session, params, with_family=False):
16 self.session = session
17 self.params = params
18 self.with_family = with_family
20 fmip_endpoint = f"{service_root}/fmipservice/client/web"
21 self._fmip_refresh_url = f"{fmip_endpoint}/refreshClient"
22 self._fmip_sound_url = f"{fmip_endpoint}/playSound"
23 self._fmip_message_url = f"{fmip_endpoint}/sendMessage"
24 self._fmip_lost_url = f"{fmip_endpoint}/lostDevice"
26 self._devices = {}
27 self.refresh_client()
29 def refresh_client(self):
30 """Refreshes the FindMyiPhoneService endpoint,
32 This ensures that the location data is up-to-date.
34 """
35 req = self.session.post(
36 self._fmip_refresh_url,
37 params=self.params,
38 data=json.dumps(
39 {
40 "clientContext": {
41 "fmly": self.with_family,
42 "shouldLocate": True,
43 "selectedDevice": "all",
44 "deviceListVersion": 1,
45 },
46 },
47 ),
48 )
49 self.response = req.json()
51 for device_info in self.response["content"]:
52 device_id = device_info["id"]
53 if device_id not in self._devices:
54 self._devices[device_id] = AppleDevice(
55 device_info,
56 self.session,
57 self.params,
58 manager=self,
59 sound_url=self._fmip_sound_url,
60 lost_url=self._fmip_lost_url,
61 message_url=self._fmip_message_url,
62 )
63 else:
64 self._devices[device_id].update(device_info)
66 if not self._devices:
67 raise ICloudPyNoDevicesException
69 def __getitem__(self, key):
70 if isinstance(key, int):
71 key = list(self.keys())[key]
72 return self._devices[key]
74 def __getattr__(self, attr):
75 return getattr(self._devices, attr)
77 def __unicode__(self):
78 return str(self._devices)
80 def __str__(self):
81 return self.__unicode__()
83 def __repr__(self):
84 return str(self)
87class AppleDevice:
88 """Apple device."""
90 def __init__(
91 self,
92 content,
93 session,
94 params,
95 manager,
96 sound_url=None,
97 lost_url=None,
98 message_url=None,
99 ):
100 self.content = content
101 self.manager = manager
102 self.session = session
103 self.params = params
105 self.sound_url = sound_url
106 self.lost_url = lost_url
107 self.message_url = message_url
109 def update(self, data):
110 """Updates the device data."""
111 self.content = data
113 def location(self):
114 """Updates the device location."""
115 self.manager.refresh_client()
116 return self.content["location"]
118 def status(self, additional=[]): # pylint: disable=dangerous-default-value
119 """Returns status information for device.
121 This returns only a subset of possible properties.
122 """
123 self.manager.refresh_client()
124 fields = ["batteryLevel", "deviceDisplayName", "deviceStatus", "name"]
125 fields += additional
126 properties = {}
127 for field in fields:
128 properties[field] = self.content.get(field)
129 return properties
131 def play_sound(self, subject="Find My iPhone Alert"):
132 """Send a request to the device to play a sound.
134 It's possible to pass a custom message by changing the `subject`.
135 """
136 data = json.dumps(
137 {
138 "device": self.content["id"],
139 "subject": subject,
140 "clientContext": {"fmly": True},
141 },
142 )
143 self.session.post(self.sound_url, params=self.params, data=data)
145 def display_message(
146 self,
147 subject="Find My iPhone Alert",
148 message="This is a note",
149 sounds=False,
150 ):
151 """Send a request to the device to play a sound.
153 It's possible to pass a custom message by changing the `subject`.
154 """
155 data = json.dumps(
156 {
157 "device": self.content["id"],
158 "subject": subject,
159 "sound": sounds,
160 "userText": True,
161 "text": message,
162 },
163 )
164 self.session.post(self.message_url, params=self.params, data=data)
166 def lost_device(
167 self,
168 number,
169 text="This iPhone has been lost. Please call me.",
170 newpasscode="",
171 ):
172 """Send a request to the device to trigger 'lost mode'.
174 The device will show the message in `text`, and if a number has
175 been passed, then the person holding the device can call
176 the number without entering the passcode.
177 """
178 data = json.dumps(
179 {
180 "text": text,
181 "userText": True,
182 "ownerNbr": number,
183 "lostModeEnabled": True,
184 "trackingEnabled": True,
185 "device": self.content["id"],
186 "passcode": newpasscode,
187 },
188 )
189 self.session.post(self.lost_url, params=self.params, data=data)
191 @property
192 def data(self):
193 """Gets the device data."""
194 return self.content
196 def __getitem__(self, key):
197 return self.content[key]
199 def __getattr__(self, attr):
200 return getattr(self.content, attr)
202 def __unicode__(self):
203 display_name = self["deviceDisplayName"]
204 name = self["name"]
205 return f"{display_name}: {name}"
207 def __str__(self):
208 return self.__unicode__()
210 def __repr__(self):
211 return f"<AppleDevice({self})>"