diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 687c6bf93de..1131a4eecc9 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -123,10 +123,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold, ) await hass.async_add_executor_job(account.setup) - if not account.devices: - return False - hass.data[DOMAIN][username] = account + hass.data[DOMAIN][entry.unique_id] = account for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 5d681539668..789ae563482 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -10,6 +10,7 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.storage import Store @@ -37,7 +38,7 @@ from .const import ( DEVICE_STATUS, DEVICE_STATUS_CODES, DEVICE_STATUS_SET, - SERVICE_UPDATE, + DOMAIN, ) ATTRIBUTION = "Data provided by Apple iCloud" @@ -91,7 +92,7 @@ class IcloudAccount: self._family_members_fullname = {} self._devices = {} - self.unsub_device_tracker = None + self.listeners = [] def setup(self) -> None: """Set up an iCloud account.""" @@ -104,13 +105,17 @@ class IcloudAccount: _LOGGER.error("Error logging into iCloud Service: %s", error) return - user_info = None try: + api_devices = self.api.devices # Gets device owners infos - user_info = self.api.devices.response["userInfo"] - except PyiCloudNoDevicesException: + user_info = api_devices.response["userInfo"] + except (KeyError, PyiCloudNoDevicesException): _LOGGER.error("No iCloud device found") - return + raise ConfigEntryNotReady + + if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": + _LOGGER.warning("Pending devices, trying again ...") + raise ConfigEntryNotReady self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" @@ -132,13 +137,21 @@ class IcloudAccount: api_devices = {} try: api_devices = self.api.devices - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud device found") - return except Exception as err: # pylint: disable=broad-except _LOGGER.error("Unknown iCloud error: %s", err) - self._fetch_interval = 5 - dispatcher_send(self.hass, SERVICE_UPDATE) + self._fetch_interval = 2 + dispatcher_send(self.hass, self.signal_device_update) + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + return + + if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": + _LOGGER.warning("Pending devices, trying again in 15s") + self._fetch_interval = 0.25 + dispatcher_send(self.hass, self.signal_device_update) track_point_in_utc_time( self.hass, self.keep_alive, @@ -147,10 +160,19 @@ class IcloudAccount: return # Gets devices infos + new_device = False for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] device_name = status[DEVICE_NAME] + device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error") + + if ( + device_status == "pending" + or status[DEVICE_BATTERY_STATUS] == "Unknown" + or status.get(DEVICE_BATTERY_LEVEL) is None + ): + continue if self._devices.get(device_id, None) is not None: # Seen device -> updating @@ -165,9 +187,14 @@ class IcloudAccount: ) self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) + new_device = True self._fetch_interval = self._determine_interval() - dispatcher_send(self.hass, SERVICE_UPDATE) + + dispatcher_send(self.hass, self.signal_device_update) + if new_device: + dispatcher_send(self.hass, self.signal_device_new) + track_point_in_utc_time( self.hass, self.keep_alive, @@ -291,6 +318,16 @@ class IcloudAccount: """Return the account devices.""" return self._devices + @property + def signal_device_new(self) -> str: + """Event specific per Freebox entry to signal new device.""" + return f"{DOMAIN}-{self._username}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per Freebox entry to signal updates in devices.""" + return f"{DOMAIN}-{self._username}-device-update" + class IcloudDevice: """Representation of a iCloud device.""" @@ -348,6 +385,8 @@ class IcloudDevice: and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] ): location = self._status[DEVICE_LOCATION] + if self._location is None: + dispatcher_send(self._account.hass, self._account.signal_device_new) self._location = location def play_sound(self) -> None: diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 3349615ed57..14bd4e498bd 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,7 +1,6 @@ """iCloud component constants.""" DOMAIN = "icloud" -SERVICE_UPDATE = f"{DOMAIN}_update" CONF_MAX_INTERVAL = "max_interval" CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 4248485e11b..47a302e2f26 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -5,17 +5,16 @@ from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .account import IcloudDevice +from .account import IcloudAccount, IcloudDevice from .const import ( DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_LATITUDE, DEVICE_LOCATION_LONGITUDE, DOMAIN, - SERVICE_UPDATE, ) _LOGGER = logging.getLogger(__name__) @@ -30,25 +29,45 @@ async def async_setup_scanner( async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities -): - """Configure a dispatcher connection based on a config entry.""" - username = entry.data[CONF_USERNAME] +) -> None: + """Set up device tracker for iCloud component.""" + account = hass.data[DOMAIN][entry.unique_id] + tracked = set() - for device in hass.data[DOMAIN][username].devices.values(): - if device.location is None: - _LOGGER.debug("No position found for %s", device.name) + @callback + def update_account(): + """Update the values of the account.""" + add_entities(account, async_add_entities, tracked) + + account.listeners.append( + async_dispatcher_connect(hass, account.signal_device_new, update_account) + ) + + update_account() + + +@callback +def add_entities(account, async_add_entities, tracked): + """Add new tracker entities from the account.""" + new_tracked = [] + + for dev_id, device in account.devices.items(): + if dev_id in tracked or device.location is None: continue - _LOGGER.debug("Adding device_tracker for %s", device.name) + new_tracked.append(IcloudTrackerEntity(account, device)) + tracked.add(dev_id) - async_add_entities([IcloudTrackerEntity(device)]) + if new_tracked: + async_add_entities(new_tracked, True) class IcloudTrackerEntity(TrackerEntity): """Represent a tracked device.""" - def __init__(self, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice): """Set up the iCloud tracker entity.""" + self._account = account self._device = device self._unsub_dispatcher = None @@ -110,7 +129,7 @@ class IcloudTrackerEntity(TrackerEntity): async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, SERVICE_UPDATE, self.async_write_ha_state + self.hass, self._account.signal_device_update, self.async_write_ha_state ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 5438b9ce810..b2e8b4ead1e 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -3,14 +3,15 @@ import logging from typing import Dict from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import HomeAssistantType -from .account import IcloudDevice -from .const import DOMAIN, SERVICE_UPDATE +from .account import IcloudAccount, IcloudDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,23 +19,44 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up iCloud devices sensors based on a config entry.""" - username = entry.data[CONF_USERNAME] + """Set up device tracker for iCloud component.""" + account = hass.data[DOMAIN][entry.unique_id] + tracked = set() - entities = [] - for device in hass.data[DOMAIN][username].devices.values(): - if device.battery_level is not None: - _LOGGER.debug("Adding battery sensor for %s", device.name) - entities.append(IcloudDeviceBatterySensor(device)) + @callback + def update_account(): + """Update the values of the account.""" + add_entities(account, async_add_entities, tracked) - async_add_entities(entities, True) + account.listeners.append( + async_dispatcher_connect(hass, account.signal_device_new, update_account) + ) + + update_account() + + +@callback +def add_entities(account, async_add_entities, tracked): + """Add new tracker entities from the account.""" + new_tracked = [] + + for dev_id, device in account.devices.items(): + if dev_id in tracked or device.battery_level is None: + continue + + new_tracked.append(IcloudDeviceBatterySensor(account, device)) + tracked.add(dev_id) + + if new_tracked: + async_add_entities(new_tracked, True) class IcloudDeviceBatterySensor(Entity): """Representation of a iCloud device battery sensor.""" - def __init__(self, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice): """Initialize the battery sensor.""" + self._account = account self._device = device self._unsub_dispatcher = None @@ -94,7 +116,7 @@ class IcloudDeviceBatterySensor(Entity): async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, SERVICE_UPDATE, self.async_write_ha_state + self.hass, self._account.signal_device_update, self.async_write_ha_state ) async def async_will_remove_from_hass(self):