Skip to content

Commit

Permalink
Get pending iCloud devices when available + request again when needs …
Browse files Browse the repository at this point in the history
…an update (home-assistant#32400)

* Fetch iCloud devices again if the status is pending

* Remove "No iCloud device found" double check

* fix default api_devices value

* Remove useless unitialisation declarations
  • Loading branch information
Quentame authored Mar 5, 2020
1 parent 521cc72 commit 85ba469
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 42 deletions.
4 changes: 1 addition & 3 deletions homeassistant/components/icloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
63 changes: 51 additions & 12 deletions homeassistant/components/icloud/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

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
Expand Down Expand Up @@ -37,7 +38,7 @@
DEVICE_STATUS,
DEVICE_STATUS_CODES,
DEVICE_STATUS_SET,
SERVICE_UPDATE,
DOMAIN,
)

ATTRIBUTION = "Data provided by Apple iCloud"
Expand Down Expand Up @@ -91,7 +92,7 @@ def __init__(
self._family_members_fullname = {}
self._devices = {}

self.unsub_device_tracker = None
self.listeners = []

def setup(self) -> None:
"""Set up an iCloud account."""
Expand All @@ -104,13 +105,17 @@ def setup(self) -> None:
_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']}"

Expand All @@ -132,13 +137,21 @@ def update_devices(self) -> None:
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,
Expand All @@ -147,10 +160,19 @@ def update_devices(self) -> None:
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
Expand All @@ -165,9 +187,14 @@ def update_devices(self) -> None:
)
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,
Expand Down Expand Up @@ -291,6 +318,16 @@ def devices(self) -> Dict[str, any]:
"""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."""
Expand Down Expand Up @@ -348,6 +385,8 @@ def update(self, status) -> None:
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:
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/icloud/const.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
45 changes: 32 additions & 13 deletions homeassistant/components/icloud/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
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__)
Expand All @@ -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()

@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 device in hass.data[DOMAIN][username].devices.values():
if device.location is None:
_LOGGER.debug("No position found for %s", device.name)
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

Expand Down Expand Up @@ -110,7 +129,7 @@ def device_info(self) -> Dict[str, any]:
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):
Expand Down
48 changes: 35 additions & 13 deletions homeassistant/components/icloud/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,60 @@
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__)


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

Expand Down Expand Up @@ -94,7 +116,7 @@ def should_poll(self) -> bool:
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):
Expand Down

0 comments on commit 85ba469

Please sign in to comment.