From 51690e0dcd0a9a6e9b6a4ec99bd95f57ef4c0f18 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 9 Dec 2019 23:13:25 +0100 Subject: [PATCH 1/4] Add battery sensor to iCloud --- homeassistant/components/icloud/__init__.py | 5 - homeassistant/components/icloud/const.py | 3 +- .../components/icloud/device_tracker.py | 26 ++--- homeassistant/components/icloud/sensor.py | 96 +++++++++++++++++++ 4 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/icloud/sensor.py diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 2012f69193803b..c59f4098951ad3 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -560,11 +560,6 @@ def unique_id(self) -> str: """Return a unique ID.""" return self._device_id - @property - def dev_id(self) -> str: - """Return the device ID.""" - return self._device_id - @property def name(self) -> str: """Return the Apple device name.""" diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 4e99a378077c9a..ed2fc78fe6d254 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -14,8 +14,7 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -# Next PR will add sensor -ICLOUD_COMPONENTS = ["device_tracker"] +ICLOUD_COMPONENTS = ["device_tracker", "sensor"] # pyicloud.AppleDevice status DEVICE_BATTERY_LEVEL = "batteryLevel" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 4be34728c6dfde..bf0e8b80ab91b3 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,8 +1,10 @@ """Support for tracking for iCloud devices.""" import logging +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.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType @@ -26,7 +28,9 @@ async def async_setup_scanner( pass -async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): +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] @@ -49,12 +53,12 @@ def __init__(self, device: IcloudDevice): self._unsub_dispatcher = None @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" - return f"{self._device.unique_id}_tracker" + return self._device.unique_id @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._device.name @@ -74,36 +78,36 @@ def longitude(self): return self._device.location[DEVICE_LOCATION_LONGITUDE] @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed.""" return False @property - def battery_level(self): + def battery_level(self) -> int: """Return the battery level of the device.""" return self._device.battery_level @property - def source_type(self): + def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return icon_for_icloud_device(self._device) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, any]: """Return the device state attributes.""" return self._device.state_attributes @property - def device_info(self): + def device_info(self) -> Dict[str, any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, + "name": self._device.name, "manufacturer": "Apple", "model": self._device.device_model, } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py new file mode 100644 index 00000000000000..83ecde4ceeb95f --- /dev/null +++ b/homeassistant/components/icloud/sensor.py @@ -0,0 +1,96 @@ +"""Support for iCloud sensors.""" +import logging +from typing import Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.typing import HomeAssistantType + +from . import 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] + icloud = hass.data[DOMAIN][username] + + devices = [] + for device_name, icloud_device in icloud.devices.items(): + if icloud_device.battery_level is not None: + _LOGGER.debug("Adding sensors from iCloud device=%s", device_name) + devices.append( + IcloudDeviceBatterySensor(hass, icloud.username, icloud_device) + ) + + async_add_entities(devices, True) + + +class IcloudDeviceBatterySensor(Entity): + """Representation of a iCloud device battery sensor.""" + + def __init__(self, hass: HomeAssistantType, username: str, device: IcloudDevice): + """Initialize the battery sensor.""" + self._hass = hass + self._username = username + self._device = device + + def update(self): + """Fetch new state data for the sensor.""" + self._device = self._hass.data[DOMAIN][self._username].devices[ + self._device.unique_id + ] + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device.unique_id + + @property + def name(self) -> str: + """Sensor name.""" + return self._device.name + " battery state" + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self) -> int: + """Battery state percentage.""" + return self._device.battery_level + + @property + def unit_of_measurement(self) -> str: + """Battery state measured in percentage.""" + return "%" + + @property + def icon(self) -> str: + """Battery state icon handling.""" + return icon_for_battery_level( + battery_level=self._device.battery_level, + charging=self._device.battery_status == "Charging", + ) + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return default attributes for the iCloud device entity.""" + return self._device.state_attributes + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } From 906da977ba18b63bc42b58e8670ce0304ee5d18a Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 12 Dec 2019 10:33:47 +0100 Subject: [PATCH 2/4] Update .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index f4794b593816f1..c6e7182d326d88 100644 --- a/.coveragerc +++ b/.coveragerc @@ -321,6 +321,7 @@ omit = homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/__init__.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/sensor.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py From c61d7f2c532389e515e3ed6c4e6b9ffb83926af7 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 13 Dec 2019 21:07:27 +0100 Subject: [PATCH 3/4] Review: @balloob & @MartinHjelmare --- .../components/icloud/device_tracker.py | 4 +-- homeassistant/components/icloud/sensor.py | 29 ++++++------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index bf0e8b80ab91b3..511ce7f94470a2 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -36,7 +36,7 @@ async def async_setup_entry( for device in hass.data[DOMAIN][username].devices.values(): if device.location is None: - _LOGGER.debug("No position found for device %s", device.name) + _LOGGER.debug("No position found for %s", device.name) continue _LOGGER.debug("Adding device_tracker for %s", device.name) @@ -106,7 +106,7 @@ def device_state_attributes(self) -> Dict[str, any]: def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, + "identifiers": {(DOMAIN, self._device.unique_id)}, "name": self._device.name, "manufacturer": "Apple", "model": self._device.device_model, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 83ecde4ceeb95f..f972103b7b1a92 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -19,38 +19,27 @@ async def async_setup_entry( ) -> None: """Set up iCloud devices sensors based on a config entry.""" username = entry.data[CONF_USERNAME] - icloud = hass.data[DOMAIN][username] - devices = [] - for device_name, icloud_device in icloud.devices.items(): - if icloud_device.battery_level is not None: - _LOGGER.debug("Adding sensors from iCloud device=%s", device_name) - devices.append( - IcloudDeviceBatterySensor(hass, icloud.username, icloud_device) - ) + 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)) - async_add_entities(devices, True) + async_add_entities(entities, True) class IcloudDeviceBatterySensor(Entity): """Representation of a iCloud device battery sensor.""" - def __init__(self, hass: HomeAssistantType, username: str, device: IcloudDevice): + def __init__(self, device: IcloudDevice): """Initialize the battery sensor.""" - self._hass = hass - self._username = username self._device = device - def update(self): - """Fetch new state data for the sensor.""" - self._device = self._hass.data[DOMAIN][self._username].devices[ - self._device.unique_id - ] - @property def unique_id(self) -> str: """Return a unique ID.""" - return self._device.unique_id + return f"{self._device.unique_id}_battery" @property def name(self) -> str: @@ -89,7 +78,7 @@ def device_state_attributes(self) -> Dict[str, any]: def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, + "identifiers": {(DOMAIN, self._device.unique_id)}, "name": self._device.name, "manufacturer": "Apple", "model": self._device.device_model, From c7154d0947caf99d1cdcdde7fcdb7e4a27f67f45 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sat, 14 Dec 2019 17:41:38 +0100 Subject: [PATCH 4/4] Review: use f string --- homeassistant/components/icloud/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index f972103b7b1a92..4351d4ffa1983c 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -44,7 +44,7 @@ def unique_id(self) -> str: @property def name(self) -> str: """Sensor name.""" - return self._device.name + " battery state" + return f"{self._device.name} battery state" @property def device_class(self) -> str: