Coverage for icloudpy/services/findmyiphone.py: 63%

89 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-04-12 14:26 +0000

1"""Find my iPhone service.""" 

2import json 

3 

4from six import PY2 

5 

6from icloudpy.exceptions import ICloudPyNoDevicesException 

7 

8 

9class FindMyiPhoneServiceManager: 

10 """The 'Find my iPhone' iCloud service 

11 

12 This connects to iCloud and return phone data including the near-realtime 

13 latitude and longitude. 

14 """ 

15 

16 def __init__(self, service_root, session, params, with_family=False): 

17 self.session = session 

18 self.params = params 

19 self.with_family = with_family 

20 

21 fmip_endpoint = f"{service_root}/fmipservice/client/web" 

22 self._fmip_refresh_url = f"{fmip_endpoint}/refreshClient" 

23 self._fmip_sound_url = f"{fmip_endpoint}/playSound" 

24 self._fmip_message_url = f"{fmip_endpoint}/sendMessage" 

25 self._fmip_lost_url = f"{fmip_endpoint}/lostDevice" 

26 

27 self._devices = {} 

28 self.refresh_client() 

29 

30 def refresh_client(self): 

31 """Refreshes the FindMyiPhoneService endpoint, 

32 

33 This ensures that the location data is up-to-date. 

34 

35 """ 

36 req = self.session.post( 

37 self._fmip_refresh_url, 

38 params=self.params, 

39 data=json.dumps( 

40 { 

41 "clientContext": { 

42 "fmly": self.with_family, 

43 "shouldLocate": True, 

44 "selectedDevice": "all", 

45 "deviceListVersion": 1, 

46 } 

47 } 

48 ), 

49 ) 

50 self.response = req.json() 

51 

52 for device_info in self.response["content"]: 

53 device_id = device_info["id"] 

54 if device_id not in self._devices: 

55 self._devices[device_id] = AppleDevice( 

56 device_info, 

57 self.session, 

58 self.params, 

59 manager=self, 

60 sound_url=self._fmip_sound_url, 

61 lost_url=self._fmip_lost_url, 

62 message_url=self._fmip_message_url, 

63 ) 

64 else: 

65 self._devices[device_id].update(device_info) 

66 

67 if not self._devices: 

68 raise ICloudPyNoDevicesException() 

69 

70 def __getitem__(self, key): 

71 if isinstance(key, int): 

72 key = list(self.keys())[key] 

73 return self._devices[key] 

74 

75 def __getattr__(self, attr): 

76 return getattr(self._devices, attr) 

77 

78 def __unicode__(self): 

79 return str(self._devices) 

80 

81 def __str__(self): 

82 as_unicode = self.__unicode__() 

83 if PY2: 

84 return as_unicode.encode("utf-8", "ignore") 

85 return as_unicode 

86 

87 def __repr__(self): 

88 return str(self) 

89 

90 

91class AppleDevice: 

92 """Apple device.""" 

93 

94 def __init__( 

95 self, 

96 content, 

97 session, 

98 params, 

99 manager, 

100 sound_url=None, 

101 lost_url=None, 

102 message_url=None, 

103 ): 

104 self.content = content 

105 self.manager = manager 

106 self.session = session 

107 self.params = params 

108 

109 self.sound_url = sound_url 

110 self.lost_url = lost_url 

111 self.message_url = message_url 

112 

113 def update(self, data): 

114 """Updates the device data.""" 

115 self.content = data 

116 

117 def location(self): 

118 """Updates the device location.""" 

119 self.manager.refresh_client() 

120 return self.content["location"] 

121 

122 def status(self, additional=[]): # pylint: disable=dangerous-default-value 

123 """Returns status information for device. 

124 

125 This returns only a subset of possible properties. 

126 """ 

127 self.manager.refresh_client() 

128 fields = ["batteryLevel", "deviceDisplayName", "deviceStatus", "name"] 

129 fields += additional 

130 properties = {} 

131 for field in fields: 

132 properties[field] = self.content.get(field) 

133 return properties 

134 

135 def play_sound(self, subject="Find My iPhone Alert"): 

136 """Send a request to the device to play a sound. 

137 

138 It's possible to pass a custom message by changing the `subject`. 

139 """ 

140 data = json.dumps( 

141 { 

142 "device": self.content["id"], 

143 "subject": subject, 

144 "clientContext": {"fmly": True}, 

145 } 

146 ) 

147 self.session.post(self.sound_url, params=self.params, data=data) 

148 

149 def display_message( 

150 self, subject="Find My iPhone Alert", message="This is a note", sounds=False 

151 ): 

152 """Send a request to the device to play a sound. 

153 

154 It's possible to pass a custom message by changing the `subject`. 

155 """ 

156 data = json.dumps( 

157 { 

158 "device": self.content["id"], 

159 "subject": subject, 

160 "sound": sounds, 

161 "userText": True, 

162 "text": message, 

163 } 

164 ) 

165 self.session.post(self.message_url, params=self.params, data=data) 

166 

167 def lost_device( 

168 self, number, text="This iPhone has been lost. Please call me.", newpasscode="" 

169 ): 

170 """Send a request to the device to trigger 'lost mode'. 

171 

172 The device will show the message in `text`, and if a number has 

173 been passed, then the person holding the device can call 

174 the number without entering the passcode. 

175 """ 

176 data = json.dumps( 

177 { 

178 "text": text, 

179 "userText": True, 

180 "ownerNbr": number, 

181 "lostModeEnabled": True, 

182 "trackingEnabled": True, 

183 "device": self.content["id"], 

184 "passcode": newpasscode, 

185 } 

186 ) 

187 self.session.post(self.lost_url, params=self.params, data=data) 

188 

189 @property 

190 def data(self): 

191 """Gets the device data.""" 

192 return self.content 

193 

194 def __getitem__(self, key): 

195 return self.content[key] 

196 

197 def __getattr__(self, attr): 

198 return getattr(self.content, attr) 

199 

200 def __unicode__(self): 

201 display_name = self["deviceDisplayName"] 

202 name = self["name"] 

203 return f"{display_name}: {name}" 

204 

205 def __str__(self): 

206 as_unicode = self.__unicode__() 

207 if PY2: 

208 return as_unicode.encode("utf-8", "ignore") 

209 return as_unicode 

210 

211 def __repr__(self): 

212 return f"<AppleDevice({self})>"