3 from __future__
import annotations
5 from datetime
import timedelta
10 from pyicloud
import PyiCloudService
11 from pyicloud.exceptions
import (
12 PyiCloudFailedLoginException,
13 PyiCloudNoDevicesException,
14 PyiCloudServiceNotActivatedException,
16 from pyicloud.services.findmyiphone
import AppleDevice
33 DEVICE_BATTERY_STATUS,
38 DEVICE_LOCATION_HORIZONTAL_ACCURACY,
39 DEVICE_LOCATION_LATITUDE,
40 DEVICE_LOCATION_LONGITUDE,
41 DEVICE_LOST_MODE_CAPABLE,
42 DEVICE_LOW_POWER_MODE,
45 DEVICE_RAW_DEVICE_MODEL,
53 ATTR_ACCOUNT_FETCH_INTERVAL =
"account_fetch_interval"
54 ATTR_BATTERY =
"battery"
55 ATTR_BATTERY_STATUS =
"battery_status"
56 ATTR_DEVICE_NAME =
"device_name"
57 ATTR_DEVICE_STATUS =
"device_status"
58 ATTR_LOW_POWER_MODE =
"low_power_mode"
59 ATTR_OWNER_NAME =
"owner_fullname"
62 SERVICE_ICLOUD_PLAY_SOUND =
"play_sound"
63 SERVICE_ICLOUD_DISPLAY_MESSAGE =
"display_message"
64 SERVICE_ICLOUD_LOST_DEVICE =
"lost_device"
65 SERVICE_ICLOUD_UPDATE =
"update"
66 ATTR_ACCOUNT =
"account"
67 ATTR_LOST_DEVICE_MESSAGE =
"message"
68 ATTR_LOST_DEVICE_NUMBER =
"number"
69 ATTR_LOST_DEVICE_SOUND =
"sound"
71 _LOGGER = logging.getLogger(__name__)
75 """Representation of an iCloud account."""
85 gps_accuracy_threshold: int,
86 config_entry: ConfigEntry,
88 """Initialize an iCloud account."""
99 self.
apiapi: PyiCloudService |
None =
None
102 self.
_devices_devices: dict[str, IcloudDevice] = {}
106 self.listeners: list[CALLBACK_TYPE] = []
109 """Set up an iCloud account."""
111 self.
apiapi = PyiCloudService(
118 if self.
apiapi.requires_2fa:
120 raise PyiCloudFailedLoginException
122 except PyiCloudFailedLoginException:
127 "Your password for '%s' is no longer working; Go to the "
128 "Integrations menu and click on Configure on the discovered Apple "
129 "iCloud card to login again"
138 api_devices = self.
apiapi.devices
140 user_info = api_devices.response[
"userInfo"]
142 PyiCloudServiceNotActivatedException,
143 PyiCloudNoDevicesException,
145 _LOGGER.error(
"No iCloud device found")
146 raise ConfigEntryNotReady
from err
148 self.
_owner_fullname_owner_fullname = f
"{user_info['firstName']} {user_info['lastName']}"
151 if user_info.get(
"membersInfo")
is not None:
152 for prs_id, member
in user_info[
"membersInfo"].items():
154 f
"{member['firstName']} {member['lastName']}"
161 """Update iCloud devices."""
162 if self.
apiapi
is None:
164 _LOGGER.debug(
"Updating devices")
166 if self.
apiapi.requires_2fa:
172 api_devices = self.
apiapi.devices
173 except Exception
as err:
174 _LOGGER.error(
"Unknown iCloud error: %s", err)
182 for device
in api_devices:
183 status = device.status(DEVICE_STATUS_SET)
184 device_id = status[DEVICE_ID]
185 device_name = status[DEVICE_NAME]
188 status[DEVICE_BATTERY_STATUS] ==
"Unknown"
189 or status.get(DEVICE_BATTERY_LEVEL)
is None
193 if self.
_devices_devices.
get(device_id)
is not None:
195 _LOGGER.debug(
"Updating iCloud device: %s", device_name)
200 "Adding iCloud device: %s [model: %s]",
202 status[DEVICE_RAW_DEVICE_MODEL],
209 DEVICE_STATUS_CODES.get(
list(api_devices)[0][DEVICE_STATUS]) ==
"pending"
212 _LOGGER.debug(
"Pending devices, trying again in 15s")
226 """Require the user to log in again."""
230 """Calculate new interval between two API fetch (in minutes)."""
232 for device
in self.
_devices_devices.values():
234 if device.location
is None:
237 current_zone = run_callback_threadsafe(
241 device.location[DEVICE_LOCATION_LATITUDE],
242 device.location[DEVICE_LOCATION_LONGITUDE],
243 device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY],
247 if current_zone
is not None:
251 self.
hasshass.states.get(entity_id)
252 for entity_id
in sorted(self.
hasshass.states.entity_ids(
"zone"))
256 for zone_state
in zones:
257 if zone_state
is None:
259 zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE]
260 zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE]
262 device.location[DEVICE_LOCATION_LATITUDE],
263 device.location[DEVICE_LOCATION_LONGITUDE],
267 if zone_distance
is not None:
268 distances.append(round(zone_distance / 1000, 1))
273 mindistance =
min(distances)
277 interval = round(mindistance / 2)
280 interval =
max(interval, 1)
288 device.battery_level
is not None
289 and device.battery_level <= 33
293 interval = interval * 2
295 intervals[device.name] = interval
298 int(
min(intervals.items(), key=operator.itemgetter(1))[1]),
311 """Keep the API alive."""
312 if self.
apiapi
is None:
315 if self.
apiapi
is None:
322 """Get devices by name."""
323 name_slug =
slugify(name.replace(
" ",
"", 99))
326 for device
in self.
devicesdevices.values()
327 if slugify(device.name.replace(
" ",
"", 99)) == name_slug
330 raise ValueError(f
"No device with name {name}")
335 """Return the account username."""
340 """Return the account owner fullname."""
345 """Return the account family members fullname."""
350 """Return the account fetch interval."""
355 """Return the account devices."""
360 """Event specific per Freebox entry to signal new device."""
361 return f
"{DOMAIN}-{self._username}-device-new"
365 """Event specific per Freebox entry to signal updates in devices."""
366 return f
"{DOMAIN}-{self._username}-device-update"
370 """Representation of a iCloud device."""
372 _attr_attribution =
"Data provided by Apple iCloud"
374 def __init__(self, account: IcloudAccount, device: AppleDevice, status) ->
None:
375 """Initialize the iCloud device."""
391 ATTR_ACCOUNT_FETCH_INTERVAL: self.
_account_account.fetch_interval,
393 ATTR_DEVICE_STATUS:
None,
395 if self.
_status_status[DEVICE_PERSON_ID]:
396 self.
_attrs_attrs[ATTR_OWNER_NAME] = account.family_members_fullname[
397 self.
_status_status[DEVICE_PERSON_ID]
399 elif account.owner_fullname
is not None:
400 self.
_attrs_attrs[ATTR_OWNER_NAME] = account.owner_fullname
403 """Update the iCloud device."""
406 self.
_status_status[ATTR_ACCOUNT_FETCH_INTERVAL] = self.
_account_account.fetch_interval
408 device_status = DEVICE_STATUS_CODES.get(self.
_status_status[DEVICE_STATUS],
"error")
409 self.
_attrs_attrs[ATTR_DEVICE_STATUS] = device_status
413 device_battery_level = self.
_status_status.
get(DEVICE_BATTERY_LEVEL, 0)
414 if self.
_battery_status_battery_status !=
"Unknown" and device_battery_level
is not None:
417 self.
_attrs_attrs[ATTR_LOW_POWER_MODE] = self.
_status_status[DEVICE_LOW_POWER_MODE]
420 self.
_status_status[DEVICE_LOCATION]
421 and self.
_status_status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE]
423 location = self.
_status_status[DEVICE_LOCATION]
429 """Play sound on the device."""
430 if self.
_account_account.api
is None:
433 self.
_account_account.api.authenticate()
434 _LOGGER.debug(
"Playing sound for %s", self.
namename)
438 """Display a message on the device."""
439 if self.
_account_account.api
is None:
442 self.
_account_account.api.authenticate()
443 _LOGGER.debug(
"Displaying message for %s", self.
namename)
447 """Make the device in lost state."""
448 if self.
_account_account.api
is None:
451 self.
_account_account.api.authenticate()
452 if self.
_status_status[DEVICE_LOST_MODE_CAPABLE]:
453 _LOGGER.debug(
"Make device lost for %s", self.
namename)
456 _LOGGER.error(
"Cannot make device lost for %s", self.
namename)
460 """Return a unique ID."""
465 """Return the Apple device name."""
466 return self.
_name_name
470 """Return the Apple device."""
475 """Return the Apple device class."""
480 """Return the Apple device model."""
485 """Return the Apple device battery level."""
490 """Return the Apple device battery status."""
495 """Return the Apple device location."""
500 """Return the attributes."""
dict[str, Any] devices(self)
str signal_device_update(self)
str signal_device_new(self)
dict[str, str] family_members_fullname(self)
None __init__(self, HomeAssistant hass, str username, str password, Store icloud_dir, bool with_family, int max_interval, int gps_accuracy_threshold, ConfigEntry config_entry)
None keep_alive(self, now=None)
def _require_reauth(self)
list[Any] get_devices_with_name(self, str name)
None _schedule_next_fetch(self)
float fetch_interval(self)
int _determine_interval(self)
str|None owner_fullname(self)
None update_devices(self)
None display_message(self, str message, bool sound=False)
dict[str, Any]|None location(self)
str|None battery_status(self)
None update(self, status)
dict[str, Any] extra_state_attributes(self)
None lost_device(self, str number, str message)
None __init__(self, IcloudAccount account, AppleDevice device, status)
int|None battery_level(self)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
def authenticate(HomeAssistant hass, host, port, servers)
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
def distance(hass, *args)