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

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

2 

3import json 

4 

5from icloudpy.exceptions import ICloudPyNoDevicesException 

6 

7 

8class FindMyiPhoneServiceManager: 

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

10 

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

12 latitude and longitude. 

13 """ 

14 

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 

19 

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" 

25 

26 self._devices = {} 

27 self.refresh_client() 

28 

29 def refresh_client(self): 

30 """Refreshes the FindMyiPhoneService endpoint, 

31 

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

33 

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

50 

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) 

65 

66 if not self._devices: 

67 raise ICloudPyNoDevicesException 

68 

69 def __getitem__(self, key): 

70 if isinstance(key, int): 

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

72 return self._devices[key] 

73 

74 def __getattr__(self, attr): 

75 return getattr(self._devices, attr) 

76 

77 def __unicode__(self): 

78 return str(self._devices) 

79 

80 def __str__(self): 

81 return self.__unicode__() 

82 

83 def __repr__(self): 

84 return str(self) 

85 

86 

87class AppleDevice: 

88 """Apple device.""" 

89 

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 

104 

105 self.sound_url = sound_url 

106 self.lost_url = lost_url 

107 self.message_url = message_url 

108 

109 def update(self, data): 

110 """Updates the device data.""" 

111 self.content = data 

112 

113 def location(self): 

114 """Updates the device location.""" 

115 self.manager.refresh_client() 

116 return self.content["location"] 

117 

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

119 """Returns status information for device. 

120 

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 

130 

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

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

133 

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) 

144 

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. 

152 

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) 

165 

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

173 

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) 

190 

191 @property 

192 def data(self): 

193 """Gets the device data.""" 

194 return self.content 

195 

196 def __getitem__(self, key): 

197 return self.content[key] 

198 

199 def __getattr__(self, attr): 

200 return getattr(self.content, attr) 

201 

202 def __unicode__(self): 

203 display_name = self["deviceDisplayName"] 

204 name = self["name"] 

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

206 

207 def __str__(self): 

208 return self.__unicode__() 

209 

210 def __repr__(self): 

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