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

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

2 

3import json 

4 

5from six import PY2 

6 

7from icloudpy.exceptions import ICloudPyNoDevicesException 

8 

9 

10class FindMyiPhoneServiceManager: 

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

12 

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

14 latitude and longitude. 

15 """ 

16 

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 

21 

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" 

27 

28 self._devices = {} 

29 self.refresh_client() 

30 

31 def refresh_client(self): 

32 """Refreshes the FindMyiPhoneService endpoint, 

33 

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

35 

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() 

52 

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) 

67 

68 if not self._devices: 

69 raise ICloudPyNoDevicesException 

70 

71 def __getitem__(self, key): 

72 if isinstance(key, int): 

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

74 return self._devices[key] 

75 

76 def __getattr__(self, attr): 

77 return getattr(self._devices, attr) 

78 

79 def __unicode__(self): 

80 return str(self._devices) 

81 

82 def __str__(self): 

83 as_unicode = self.__unicode__() 

84 if PY2: 

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

86 return as_unicode 

87 

88 def __repr__(self): 

89 return str(self) 

90 

91 

92class AppleDevice: 

93 """Apple device.""" 

94 

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 

109 

110 self.sound_url = sound_url 

111 self.lost_url = lost_url 

112 self.message_url = message_url 

113 

114 def update(self, data): 

115 """Updates the device data.""" 

116 self.content = data 

117 

118 def location(self): 

119 """Updates the device location.""" 

120 self.manager.refresh_client() 

121 return self.content["location"] 

122 

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

124 """Returns status information for device. 

125 

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 

135 

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

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

138 

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) 

149 

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. 

157 

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) 

170 

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'. 

178 

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) 

195 

196 @property 

197 def data(self): 

198 """Gets the device data.""" 

199 return self.content 

200 

201 def __getitem__(self, key): 

202 return self.content[key] 

203 

204 def __getattr__(self, attr): 

205 return getattr(self.content, attr) 

206 

207 def __unicode__(self): 

208 display_name = self["deviceDisplayName"] 

209 name = self["name"] 

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

211 

212 def __str__(self): 

213 as_unicode = self.__unicode__() 

214 if PY2: 

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

216 return as_unicode 

217 

218 def __repr__(self): 

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