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