From 3d68e0b513976aa1dcc7afb32c8bd0815be45d9c Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Sun, 16 Jun 2019 12:10:54 -0400 Subject: [PATCH 01/19] initial implementation for zha device trackers --- .../components/device_tracker/__init__.py | 4 +- .../components/device_tracker/const.py | 1 + homeassistant/components/zha/core/const.py | 2 + .../components/zha/core/registries.py | 11 +- .../components/zha/device_tracker.py | 115 ++++++++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zha/device_tracker.py diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4c67e6fa65d3..45b778453291 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -41,6 +41,7 @@ SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, + SOURCE_TYPE_ZIGBEE, ) ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') @@ -48,7 +49,8 @@ SERVICE_SEE = 'see' SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) + SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_ZIGBEE) NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 18ec486e693c..013193a8d715 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -13,6 +13,7 @@ SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_BLUETOOTH = 'bluetooth' +SOURCE_TYPE_ZIGBEE = 'zigbee' SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' CONF_SCAN_INTERVAL = 'interval_seconds' diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b40f1cf5ff4d..f86a5ee9f452 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK @@ -25,6 +26,7 @@ COMPONENTS = ( BINARY_SENSOR, + DEVICE_TRACKER, FAN, LIGHT, LOCK, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e710b0cc856d..d8978811aa1a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -6,6 +6,7 @@ """ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK @@ -39,12 +40,14 @@ BINDABLE_CLUSTERS = [] INPUT_BIND_ONLY_CLUSTERS = [] BINARY_SENSOR_CLUSTERS = set() +DEVICE_TRACKER_CLUSTERS = set() LIGHT_CLUSTERS = set() SWITCH_CLUSTERS = set() COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, LIGHT: LIGHT_CLUSTERS, - SWITCH: SWITCH_CLUSTERS + SWITCH: SWITCH_CLUSTERS, + DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS } @@ -134,7 +137,8 @@ def get_deconz_radio(): zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT + zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + 0x8000: DEVICE_TRACKER }) DEVICE_CLASS[zll.PROFILE_ID].update({ @@ -323,6 +327,9 @@ def get_deconz_radio(): zcl.clusters.measurement.OccupancySensing.cluster_id) BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + DEVICE_TRACKER_CLUSTERS.add( + zcl.clusters.general.PowerConfiguration.cluster_id) + LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py new file mode 100644 index 000000000000..00e24edf3766 --- /dev/null +++ b/homeassistant/components/zha/device_tracker.py @@ -0,0 +1,115 @@ +"""Support for the ZHA platform.""" +import logging +import time +from homeassistant.components.device_tracker import ( + SOURCE_TYPE_ZIGBEE, DOMAIN +) +from homeassistant.components.device_tracker.config_entry import ( + DeviceTrackerEntity +) +from homeassistant.const import ( + STATE_NOT_HOME, + STATE_HOME +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .core.const import ( + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, + POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR, + SIGNAL_ATTR_UPDATED +) +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation device tracker from config entry.""" + async def async_discover(discovery_info): + await _async_setup_entities(hass, config_entry, async_add_entities, + [discovery_info]) + + unsub = async_dispatcher_connect( + hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + device_trackers = hass.data.get(DATA_ZHA, {}).get(DOMAIN) + if device_trackers is not None: + await _async_setup_entities(hass, config_entry, async_add_entities, + device_trackers.values()) + del hass.data[DATA_ZHA][DOMAIN] + + return True + + +async def _async_setup_entities(hass, config_entry, async_add_entities, + discovery_infos): + """Set up the ZHA device trackers.""" + entities = [] + for discovery_info in discovery_infos: + entities.append(ZHADeviceTrackerEntity(**discovery_info)) + + async_add_entities(entities, update_before_add=True) + + +class ZHADeviceTrackerEntity(DeviceTrackerEntity, ZhaEntity): + """Represent a tracked device.""" + + def __init__(self, **kwargs): + """Initialize the ZHA device tracker.""" + super().__init__(**kwargs) + self._battery_channel = self.cluster_channels.get( + POWER_CONFIGURATION_CHANNEL) + self._last_seen = None + self._seen = False + self._keepalive_interval = 60 + self._should_poll = True + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + if self._battery_channel: + await self.async_accept_signal( + self._battery_channel, SIGNAL_STATE_ATTR, + self.async_attribute_updated) + await self.async_accept_signal( + self._battery_channel, SIGNAL_ATTR_UPDATED, + self.async_attribute_updated) + + async def async_update(self): + """Handle polling.""" + if self._last_seen is None: + self._seen = False + else: + difference = time.time() - self._last_seen + if difference > self._keepalive_interval: + self._seen = False + else: + self._seen = True + + @property + def state(self): + """Return the state of the device.""" + if self._seen: + return STATE_HOME + return STATE_NOT_HOME + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return None + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return None + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_ZIGBEE + + @callback + def async_attribute_updated(self, attribute, value): + """Handle tracking.""" + self._last_seen = time.time() From 9a76c80ffeb770725da812d1ebe978ac04302295 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Mon, 17 Jun 2019 17:32:34 -0400 Subject: [PATCH 02/19] constant --- homeassistant/components/zha/core/registries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index d8978811aa1a..f9d2fba73af6 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -24,6 +24,7 @@ SMARTTHINGS_HUMIDITY_CLUSTER = 64581 SMARTTHINGS_ACCELERATION_CLUSTER = 64514 +SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 DEVICE_CLASS = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} @@ -138,7 +139,7 @@ def get_deconz_radio(): zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - 0x8000: DEVICE_TRACKER + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER }) DEVICE_CLASS[zll.PROFILE_ID].update({ From 3919beb91775b0ac045ab2e27ee561a0df5819cb Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 18 Jun 2019 07:53:50 -0400 Subject: [PATCH 03/19] review comments --- .../components/zha/device_tracker.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 00e24edf3766..5340d0a7cb80 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -13,6 +13,7 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_call_later from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR, @@ -63,7 +64,7 @@ def __init__(self, **kwargs): self._last_seen = None self._seen = False self._keepalive_interval = 60 - self._should_poll = True + self._unsub_state_check = None async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -76,17 +77,6 @@ async def async_added_to_hass(self): self._battery_channel, SIGNAL_ATTR_UPDATED, self.async_attribute_updated) - async def async_update(self): - """Handle polling.""" - if self._last_seen is None: - self._seen = False - else: - difference = time.time() - self._last_seen - if difference > self._keepalive_interval: - self._seen = False - else: - self._seen = True - @property def state(self): """Return the state of the device.""" @@ -113,3 +103,22 @@ def source_type(self): def async_attribute_updated(self, attribute, value): """Handle tracking.""" self._last_seen = time.time() + self._seen = True + self.async_schedule_update_ha_state() + if self._unsub_state_check: + self._unsub_state_check() + self._unsub_state_check = async_call_later( + self.hass, 60, self._async_maybe_mark_not_home + ) + + @callback + def _async_maybe_mark_not_home(self, *_): + if self._last_seen is None: + self._seen = False + else: + difference = time.time() - self._last_seen + if difference > self._keepalive_interval: + self._seen = False + else: + self._seen = True + self.async_schedule_update_ha_state() From 4efdd994089a1b5ec19ebf9ed83333686a5b53bd Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 13:43:29 -0400 Subject: [PATCH 04/19] Revert "review comments" This reverts commit 2130823566820dfc114dbeda08fcdf76ed47a4e7. --- .../components/zha/device_tracker.py | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 5340d0a7cb80..00e24edf3766 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -13,7 +13,6 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_call_later from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR, @@ -64,7 +63,7 @@ def __init__(self, **kwargs): self._last_seen = None self._seen = False self._keepalive_interval = 60 - self._unsub_state_check = None + self._should_poll = True async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -77,6 +76,17 @@ async def async_added_to_hass(self): self._battery_channel, SIGNAL_ATTR_UPDATED, self.async_attribute_updated) + async def async_update(self): + """Handle polling.""" + if self._last_seen is None: + self._seen = False + else: + difference = time.time() - self._last_seen + if difference > self._keepalive_interval: + self._seen = False + else: + self._seen = True + @property def state(self): """Return the state of the device.""" @@ -103,22 +113,3 @@ def source_type(self): def async_attribute_updated(self, attribute, value): """Handle tracking.""" self._last_seen = time.time() - self._seen = True - self.async_schedule_update_ha_state() - if self._unsub_state_check: - self._unsub_state_check() - self._unsub_state_check = async_call_later( - self.hass, 60, self._async_maybe_mark_not_home - ) - - @callback - def _async_maybe_mark_not_home(self, *_): - if self._last_seen is None: - self._seen = False - else: - difference = time.time() - self._last_seen - if difference > self._keepalive_interval: - self._seen = False - else: - self._seen = True - self.async_schedule_update_ha_state() From bed09661a18377b39a6dc328746962c44b06fb64 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 13:49:14 -0400 Subject: [PATCH 05/19] rename device tracker entity --- homeassistant/components/device_tracker/config_entry.py | 2 +- homeassistant/components/geofency/device_tracker.py | 4 ++-- homeassistant/components/gpslogger/device_tracker.py | 4 ++-- homeassistant/components/locative/device_tracker.py | 4 ++-- homeassistant/components/mobile_app/device_tracker.py | 4 ++-- homeassistant/components/owntracks/device_tracker.py | 4 ++-- homeassistant/components/zha/device_tracker.py | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 59f6c0c49c17..f520bb9cb011 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -37,7 +37,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class DeviceTrackerEntity(Entity): +class TrackerEntity(Entity): """Represent a tracked device.""" @property diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index f9a7df638eb8..3400e7ea35d1 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -8,7 +8,7 @@ from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity @@ -52,7 +52,7 @@ def _receive_data(device, gps, location_name, attributes): return True -class GeofencyEntity(DeviceTrackerEntity, RestoreEntity): +class GeofencyEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" def __init__(self, device, gps=None, location_name=None, attributes=None): diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index d4b6b3c53cc8..254c9d2b391e 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -10,7 +10,7 @@ ) from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -67,7 +67,7 @@ def _receive_data(device, gps, battery, accuracy, attrs): async_add_entities(entities) -class GPSLoggerEntity(DeviceTrackerEntity, RestoreEntity): +class GPSLoggerEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" def __init__( diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 38efab7e8c01..aa6d056c7866 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -4,7 +4,7 @@ from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,7 +33,7 @@ def _receive_data(device, location, location_name): return True -class LocativeEntity(DeviceTrackerEntity): +class LocativeEntity(TrackerEntity): """Represent a tracked device.""" def __init__(self, device, location, location_name): diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 7fb76f3af413..62eb575fcb8c 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -9,7 +9,7 @@ ) from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -44,7 +44,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return True -class MobileAppEntity(DeviceTrackerEntity, RestoreEntity): +class MobileAppEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" def __init__(self, entry, data=None): diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index b573e390a12f..4ef0cb8d699d 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -11,7 +11,7 @@ from homeassistant.components.device_tracker.const import ( ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS) from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers import device_registry @@ -62,7 +62,7 @@ def _receive_data(dev_id, **data): return True -class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity): +class OwnTracksEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" def __init__(self, dev_id, data=None): diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 00e24edf3766..bd830013fc44 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -5,7 +5,7 @@ SOURCE_TYPE_ZIGBEE, DOMAIN ) from homeassistant.components.device_tracker.config_entry import ( - DeviceTrackerEntity + TrackerEntity ) from homeassistant.const import ( STATE_NOT_HOME, @@ -52,7 +52,7 @@ async def _async_setup_entities(hass, config_entry, async_add_entities, async_add_entities(entities, update_before_add=True) -class ZHADeviceTrackerEntity(DeviceTrackerEntity, ZhaEntity): +class ZHADeviceTrackerEntity(TrackerEntity, ZhaEntity): """Represent a tracked device.""" def __init__(self, **kwargs): From 34cae689a00fbc2d9e101b08cf318e9f352f96c0 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 14:46:50 -0400 Subject: [PATCH 06/19] update trackers --- .../components/device_tracker/config_entry.py | 47 +++++++++++++---- .../components/zha/device_tracker.py | 52 +++++++++---------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index f520bb9cb011..55a470ddf75f 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -37,7 +37,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class TrackerEntity(Entity): +class BaseTrackerEntity(Entity): """Represent a tracked device.""" @property @@ -48,6 +48,25 @@ def battery_level(self): """ return None + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + raise NotImplementedError + + @property + def state_attributes(self): + """Return the device state attributes.""" + attr = {} + + if self.battery_level: + attr[ATTR_BATTERY_LEVEL] = self.battery_level + + return attr + + +class TrackerEntity(BaseTrackerEntity): + """Represent a tracked device.""" + @property def location_accuracy(self): """Return the location accuracy of the device. @@ -71,11 +90,6 @@ def longitude(self) -> float: """Return longitude value of the device.""" return NotImplementedError - @property - def source_type(self): - """Return the source type, eg gps or router, of the device.""" - raise NotImplementedError - @property def state(self): """Return the state of the device.""" @@ -102,13 +116,26 @@ def state_attributes(self): attr = { ATTR_SOURCE_TYPE: self.source_type } - + attr.update(super.state_attributes) if self.latitude is not None: attr[ATTR_LATITUDE] = self.latitude attr[ATTR_LONGITUDE] = self.longitude attr[ATTR_GPS_ACCURACY] = self.location_accuracy - if self.battery_level: - attr[ATTR_BATTERY_LEVEL] = self.battery_level - return attr + + +class ScannerEntity(BaseTrackerEntity): + """Represent a tracked device that is on a scaned network.""" + + @property + def state(self): + """Return the state of the device.""" + if self.is_connected: + return STATE_HOME + return STATE_NOT_HOME + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return False diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index bd830013fc44..6830c7422a73 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,15 +1,12 @@ """Support for the ZHA platform.""" import logging +import numbers import time from homeassistant.components.device_tracker import ( SOURCE_TYPE_ZIGBEE, DOMAIN ) from homeassistant.components.device_tracker.config_entry import ( - TrackerEntity -) -from homeassistant.const import ( - STATE_NOT_HOME, - STATE_HOME + ScannerEntity ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -47,12 +44,12 @@ async def _async_setup_entities(hass, config_entry, async_add_entities, """Set up the ZHA device trackers.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZHADeviceTrackerEntity(**discovery_info)) + entities.append(ZHADeviceScannerEntity(**discovery_info)) async_add_entities(entities, update_before_add=True) -class ZHADeviceTrackerEntity(TrackerEntity, ZhaEntity): +class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Represent a tracked device.""" def __init__(self, **kwargs): @@ -61,7 +58,7 @@ def __init__(self, **kwargs): self._battery_channel = self.cluster_channels.get( POWER_CONFIGURATION_CHANNEL) self._last_seen = None - self._seen = False + self._connected = False self._keepalive_interval = 60 self._should_poll = True @@ -79,30 +76,18 @@ async def async_added_to_hass(self): async def async_update(self): """Handle polling.""" if self._last_seen is None: - self._seen = False + self._connected = False else: difference = time.time() - self._last_seen if difference > self._keepalive_interval: - self._seen = False + self._connected = False else: - self._seen = True - - @property - def state(self): - """Return the state of the device.""" - if self._seen: - return STATE_HOME - return STATE_NOT_HOME + self._connected = True @property - def latitude(self) -> float: - """Return latitude value of the device.""" - return None - - @property - def longitude(self) -> float: - """Return longitude value of the device.""" - return None + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._connected @property def source_type(self): @@ -113,3 +98,18 @@ def source_type(self): def async_attribute_updated(self, attribute, value): """Handle tracking.""" self._last_seen = time.time() + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + value = self._battery_channel.get_attribute_value( + 'battery_percentage_remaining', from_cache=True) + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if not isinstance(value, numbers.Number) or value == -1: + return None + value = value / 2 + value = int(round(value)) + return value From 9d9fe7358bd2ea9102608a525ace99619151efc9 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 14:51:26 -0400 Subject: [PATCH 07/19] raise when not implemented --- homeassistant/components/device_tracker/config_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 55a470ddf75f..2f1a990aafe7 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -138,4 +138,4 @@ def state(self): @property def is_connected(self): """Return true if the device is connected to the network.""" - return False + raise NotImplementedError From f20bf73d0c28ea8e1d492928073f41d41b650fdc Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 16:28:38 -0400 Subject: [PATCH 08/19] move source type to base state attrs --- homeassistant/components/device_tracker/config_entry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 2f1a990aafe7..b22ae51892e7 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -56,7 +56,9 @@ def source_type(self): @property def state_attributes(self): """Return the device state attributes.""" - attr = {} + attr = { + ATTR_SOURCE_TYPE: self.source_type + } if self.battery_level: attr[ATTR_BATTERY_LEVEL] = self.battery_level @@ -113,9 +115,7 @@ def state(self): @property def state_attributes(self): """Return the device state attributes.""" - attr = { - ATTR_SOURCE_TYPE: self.source_type - } + attr = {} attr.update(super.state_attributes) if self.latitude is not None: attr[ATTR_LATITUDE] = self.latitude From 7877e1fd49eaff911cf1f674364b5f26a603b095 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 16:31:12 -0400 Subject: [PATCH 09/19] review comments --- homeassistant/components/zha/core/registries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index f9d2fba73af6..1997f130278a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -22,8 +22,8 @@ CONTROLLER, BATTERY ) -SMARTTHINGS_HUMIDITY_CLUSTER = 64581 -SMARTTHINGS_ACCELERATION_CLUSTER = 64514 +SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 +SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 DEVICE_CLASS = {} From 3d336d85de407a4013c8106d8cddee2828f1b748 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 16:31:27 -0400 Subject: [PATCH 10/19] review comments --- homeassistant/components/zha/device_tracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 6830c7422a73..726d6e76e5c8 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -36,8 +36,6 @@ async def async_discover(discovery_info): device_trackers.values()) del hass.data[DATA_ZHA][DOMAIN] - return True - async def _async_setup_entities(hass, config_entry, async_add_entities, discovery_infos): From 9e414f8a81a493724e43ed25eef2d660c80b5061 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Jul 2019 15:10:18 -0400 Subject: [PATCH 11/19] Update homeassistant/components/device_tracker/config_entry.py Review comment Co-Authored-By: Martin Hjelmare --- homeassistant/components/device_tracker/config_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index b22ae51892e7..7a223a7e402d 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -126,7 +126,7 @@ def state_attributes(self): class ScannerEntity(BaseTrackerEntity): - """Represent a tracked device that is on a scaned network.""" + """Represent a tracked device that is on a scanned network.""" @property def state(self): From eccfe4c957064fe921dd07d75c8d89c6a9e5e9c7 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 2 Jul 2019 16:40:46 -0400 Subject: [PATCH 12/19] review comments --- homeassistant/components/device_tracker/__init__.py | 4 +--- homeassistant/components/device_tracker/const.py | 1 - homeassistant/components/zha/device_tracker.py | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 45b778453291..4c67e6fa65d3 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -41,7 +41,6 @@ SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, - SOURCE_TYPE_ZIGBEE, ) ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') @@ -49,8 +48,7 @@ SERVICE_SEE = 'see' SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_ZIGBEE) + SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE) NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 013193a8d715..18ec486e693c 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -13,7 +13,6 @@ SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_BLUETOOTH = 'bluetooth' -SOURCE_TYPE_ZIGBEE = 'zigbee' SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' CONF_SCAN_INTERVAL = 'interval_seconds' diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 726d6e76e5c8..e6aaccfc635b 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -3,7 +3,7 @@ import numbers import time from homeassistant.components.device_tracker import ( - SOURCE_TYPE_ZIGBEE, DOMAIN + SOURCE_TYPE_ROUTER, DOMAIN ) from homeassistant.components.device_tracker.config_entry import ( ScannerEntity @@ -90,7 +90,7 @@ def is_connected(self): @property def source_type(self): """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_ZIGBEE + return SOURCE_TYPE_ROUTER @callback def async_attribute_updated(self, attribute, value): From 0c8441c6434716c74da782cb26bc558774b1361d Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 06:59:50 -0400 Subject: [PATCH 13/19] fix super call --- homeassistant/components/device_tracker/config_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 7a223a7e402d..872d982618c1 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -116,7 +116,7 @@ def state(self): def state_attributes(self): """Return the device state attributes.""" attr = {} - attr.update(super.state_attributes) + attr.update(super().state_attributes) if self.latitude is not None: attr[ATTR_LATITUDE] = self.latitude attr[ATTR_LONGITUDE] = self.longitude From 98b344ebc8e844ff190e8a0826d25f2a68c57bd5 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 09:03:12 -0400 Subject: [PATCH 14/19] fix battery and use last seen from device --- .../components/zha/device_tracker.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index e6aaccfc635b..f3cca8755cab 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -55,10 +55,10 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self._battery_channel = self.cluster_channels.get( POWER_CONFIGURATION_CHANNEL) - self._last_seen = None self._connected = False self._keepalive_interval = 60 self._should_poll = True + self._battery_level = None async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -73,14 +73,24 @@ async def async_added_to_hass(self): async def async_update(self): """Handle polling.""" - if self._last_seen is None: + if self.zha_device.last_seen is None: self._connected = False else: - difference = time.time() - self._last_seen + difference = time.time() - self.zha_device.last_seen if difference > self._keepalive_interval: self._connected = False else: self._connected = True + # replace with power formatter import once the other PR merges + value = await self._battery_channel.get_attribute_value( + 'battery_percentage_remaining', from_cache=True) + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if not isinstance(value, numbers.Number) or value == -1: + self._battery_level = None + else: + value = value / 2 + value = int(round(value)) + self._battery_level = value @property def is_connected(self): @@ -95,7 +105,7 @@ def source_type(self): @callback def async_attribute_updated(self, attribute, value): """Handle tracking.""" - self._last_seen = time.time() + pass @property def battery_level(self): @@ -103,11 +113,4 @@ def battery_level(self): Percentage from 0-100. """ - value = self._battery_channel.get_attribute_value( - 'battery_percentage_remaining', from_cache=True) - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: - return None - value = value / 2 - value = int(round(value)) - return value + return self._battery_level From c0fc04f01d179644d821ea03e52e02acca820c28 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 09:03:33 -0400 Subject: [PATCH 15/19] add test --- tests/components/zha/test_device_tracker.py | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/components/zha/test_device_tracker.py diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py new file mode 100644 index 000000000000..2c2947f3ebd2 --- /dev/null +++ b/tests/components/zha/test_device_tracker.py @@ -0,0 +1,74 @@ +"""Test ZHA Device Tracker.""" +from datetime import timedelta +import time +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE +from homeassistant.components.zha.core.registries import \ + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE +import homeassistant.util.dt as dt_util +from .common import ( + async_init_zigpy_device, make_attribute, make_entity_id, + async_test_device_join, async_enable_traffic +) +from tests.common import async_fire_time_changed + + +async def test_device_tracker(hass, config_entry, zha_gateway): + """Test zha device tracker platform.""" + from zigpy.zcl.clusters.general import ( + Basic, PowerConfiguration, BinaryInput, Identify, Ota, PollControl) + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + BinaryInput.cluster_id + ], + [ + Identify.cluster_id, + Ota.cluster_id + ], + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, + zha_gateway + ) + + # load up device tracker domain + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() + + cluster = zigpy_device.endpoints.get(1).power + entity_id = make_entity_id(DOMAIN, zigpy_device, cluster, use_suffix=False) + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # test that the device tracker was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + zigpy_device.last_seen = time.time() - 120 + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the state has changed from unavailable to not home + assert hass.states.get(entity_id).state == STATE_NOT_HOME + + # turn state flip + attr = make_attribute(21, 23) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + zigpy_device.last_seen = time.time() + 10 + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + + # test adding device tracker to the network and HA + await async_test_device_join( + hass, zha_gateway, PowerConfiguration.cluster_id, DOMAIN, + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE) From 0f0e3b0a917587cb7f35acc273d60aee5d86cedf Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 11:27:05 -0400 Subject: [PATCH 16/19] cleanup and add more to test --- .../components/zha/device_tracker.py | 24 ++++++++++--------- tests/components/zha/test_device_tracker.py | 21 +++++++++++++--- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index f3cca8755cab..fcac5596420d 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -81,16 +81,6 @@ async def async_update(self): self._connected = False else: self._connected = True - # replace with power formatter import once the other PR merges - value = await self._battery_channel.get_attribute_value( - 'battery_percentage_remaining', from_cache=True) - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: - self._battery_level = None - else: - value = value / 2 - value = int(round(value)) - self._battery_level = value @property def is_connected(self): @@ -105,7 +95,19 @@ def source_type(self): @callback def async_attribute_updated(self, attribute, value): """Handle tracking.""" - pass + _LOGGER.debug('Attribute updated: %s - %s', attribute, value) + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + # Fix this after the other PR merges and replace with power formatter + # import once the other PR merges + self._connected = True + if attribute in ('battery_level', 'battery_percentage_remaining'): + if not isinstance(value, numbers.Number) or value == -1: + self._battery_level = None + else: + value = value / 2 + value = int(round(value)) + self._battery_level = value + self.async_schedule_update_ha_state() @property def battery_level(self): diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 2c2947f3ebd2..3fbad7fd6d42 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,8 +1,12 @@ """Test ZHA Device Tracker.""" from datetime import timedelta import time -from homeassistant.components.device_tracker import DOMAIN -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE +from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER +from homeassistant.const import ( + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE +) from homeassistant.components.zha.core.registries import \ SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE import homeassistant.util.dt as dt_util @@ -60,14 +64,25 @@ async def test_device_tracker(hass, config_entry, zha_gateway): assert hass.states.get(entity_id).state == STATE_NOT_HOME # turn state flip - attr = make_attribute(21, 23) + attr = make_attribute(0x0020, 23) + cluster.handle_message(False, 1, 0x0a, [[attr]]) + + attr = make_attribute(0x0021, 200) cluster.handle_message(False, 1, 0x0a, [[attr]]) + zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + entity = hass.data[DOMAIN].get_entity(entity_id) + + assert entity.is_connected is True + assert entity.source_type == SOURCE_TYPE_ROUTER + assert entity.battery_level == 100 + # test adding device tracker to the network and HA await async_test_device_join( hass, zha_gateway, PowerConfiguration.cluster_id, DOMAIN, From 107d52eb60d084f6742cb9b168a8459bab4fe84b Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 13:45:03 -0400 Subject: [PATCH 17/19] cleanup post zha entity removal PR --- .../components/zha/device_tracker.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index fcac5596420d..677b1bc1f275 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,6 +1,5 @@ """Support for the ZHA platform.""" import logging -import numbers import time from homeassistant.components.device_tracker import ( SOURCE_TYPE_ROUTER, DOMAIN @@ -12,10 +11,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, - POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR, - SIGNAL_ATTR_UPDATED + POWER_CONFIGURATION_CHANNEL, SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity +from .sensor import battery_percentage_remaining_formatter _LOGGER = logging.getLogger(__name__) @@ -64,12 +63,9 @@ async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() if self._battery_channel: - await self.async_accept_signal( - self._battery_channel, SIGNAL_STATE_ATTR, - self.async_attribute_updated) await self.async_accept_signal( self._battery_channel, SIGNAL_ATTR_UPDATED, - self.async_attribute_updated) + self.async_battery_percentage_remaining_updated) async def async_update(self): """Handle polling.""" @@ -93,20 +89,11 @@ def source_type(self): return SOURCE_TYPE_ROUTER @callback - def async_attribute_updated(self, attribute, value): + def async_battery_percentage_remaining_updated(self, value): """Handle tracking.""" - _LOGGER.debug('Attribute updated: %s - %s', attribute, value) - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - # Fix this after the other PR merges and replace with power formatter - # import once the other PR merges + _LOGGER.debug('battery_percentage_remaining updated: %s', value) self._connected = True - if attribute in ('battery_level', 'battery_percentage_remaining'): - if not isinstance(value, numbers.Number) or value == -1: - self._battery_level = None - else: - value = value / 2 - value = int(round(value)) - self._battery_level = value + self._battery_level = battery_percentage_remaining_formatter(value) self.async_schedule_update_ha_state() @property From 51e6f499ea15e5f152988ecbdb3db9143025064f Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 14:22:50 -0400 Subject: [PATCH 18/19] add tests for base entities --- .../device_tracker/test_entities.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/components/device_tracker/test_entities.py diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py new file mode 100644 index 000000000000..c25f8a086745 --- /dev/null +++ b/tests/components/device_tracker/test_entities.py @@ -0,0 +1,110 @@ +"""Tests for fan platforms.""" + +import unittest +import pytest + +from homeassistant.components.device_tracker.config_entry import ( + BaseTrackerEntity, ScannerEntity +) +from homeassistant.components.device_tracker.const import ( + SOURCE_TYPE_ROUTER, ATTR_SOURCE_TYPE +) +from homeassistant.const import ( + STATE_HOME, + STATE_NOT_HOME, + ATTR_BATTERY_LEVEL +) + + +class TestScannerEntity(unittest.TestCase): + """Test coverage for base ScannerEntity entity class.""" + + def setUp(self): + """Set up test data.""" + self.entity = ScannerEntity() + + def tearDown(self): + """Tear down unit test data.""" + self.entity = None + + def test_scannerentity(self): + """Test scanner entity methods.""" + with pytest.raises(NotImplementedError): + assert self.entity.source_type is None + with pytest.raises(NotImplementedError): + assert self.entity.is_connected is None + with pytest.raises(NotImplementedError): + assert self.entity.state == STATE_NOT_HOME + assert self.entity.battery_level is None + + +class TestScannerImplementation(ScannerEntity): + """Test implementation of a ScannerEntity.""" + + def __init__(self): + """Init.""" + self.connected = False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_ROUTER + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return 100 + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self.connected + + +class TestScannerEntityImpl(unittest.TestCase): + """Test coverage for base ScannerEntity entity class.""" + + def setUp(self): + """Set up test data.""" + self.entity = TestScannerImplementation() + + def tearDown(self): + """Tear down unit test data.""" + self.entity = None + + def test_scannerentity(self): + """Test scanner entity methods.""" + assert self.entity.source_type == SOURCE_TYPE_ROUTER + assert self.entity.is_connected is False + assert self.entity.state == STATE_NOT_HOME + self.entity.connected = True + assert self.entity.is_connected is True + assert self.entity.state == STATE_HOME + assert self.entity.battery_level == 100 + assert self.entity.state_attributes == { + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_BATTERY_LEVEL: 100 + } + + +class TestBaseTrackerEntity(unittest.TestCase): + """Test coverage for base BaseTrackerEntity entity class.""" + + def setUp(self): + """Set up test data.""" + self.entity = BaseTrackerEntity() + + def tearDown(self): + """Tear down unit test data.""" + self.entity = None + + def test_basetrackerentity(self): + """Test BaseTrackerEntity entity methods.""" + with pytest.raises(NotImplementedError): + assert self.entity.source_type is None + assert self.entity.battery_level is None + with pytest.raises(NotImplementedError): + assert self.entity.state_attributes is None From fbeeb9e8bff8f1761561310f6116842538b2f57d Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 3 Jul 2019 23:29:41 -0400 Subject: [PATCH 19/19] rework entity tests --- .../device_tracker/test_entities.py | 124 ++++++------------ .../custom_components/test/device_tracker.py | 39 ++++++ 2 files changed, 77 insertions(+), 86 deletions(-) diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index c25f8a086745..a338a03f2081 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -1,110 +1,62 @@ -"""Tests for fan platforms.""" - -import unittest +"""Tests for device tracker entities.""" import pytest from homeassistant.components.device_tracker.config_entry import ( BaseTrackerEntity, ScannerEntity ) from homeassistant.components.device_tracker.const import ( - SOURCE_TYPE_ROUTER, ATTR_SOURCE_TYPE + SOURCE_TYPE_ROUTER, ATTR_SOURCE_TYPE, DOMAIN ) from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_BATTERY_LEVEL ) +from tests.common import MockConfigEntry -class TestScannerEntity(unittest.TestCase): - """Test coverage for base ScannerEntity entity class.""" - - def setUp(self): - """Set up test data.""" - self.entity = ScannerEntity() - - def tearDown(self): - """Tear down unit test data.""" - self.entity = None - - def test_scannerentity(self): - """Test scanner entity methods.""" - with pytest.raises(NotImplementedError): - assert self.entity.source_type is None - with pytest.raises(NotImplementedError): - assert self.entity.is_connected is None - with pytest.raises(NotImplementedError): - assert self.entity.state == STATE_NOT_HOME - assert self.entity.battery_level is None - - -class TestScannerImplementation(ScannerEntity): - """Test implementation of a ScannerEntity.""" +async def test_scanner_entity_device_tracker(hass): + """Test ScannerEntity based device tracker.""" + config_entry = MockConfigEntry(domain='test') + config_entry.add_to_hass(hass) - def __init__(self): - """Init.""" - self.connected = False + await hass.config_entries.async_forward_entry_setup( + config_entry, DOMAIN) + await hass.async_block_till_done() - @property - def source_type(self): - """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_ROUTER + entity_id = 'device_tracker.unnamed_device' + entity_state = hass.states.get(entity_id) + assert entity_state.attributes == { + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_BATTERY_LEVEL: 100 + } + assert entity_state.state == STATE_NOT_HOME - @property - def battery_level(self): - """Return the battery level of the device. + entity = hass.data[DOMAIN].get_entity(entity_id) + entity.set_connected() + await hass.async_block_till_done() - Percentage from 0-100. - """ - return 100 + entity_state = hass.states.get(entity_id) + assert entity_state.state == STATE_HOME - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - return self.connected - -class TestScannerEntityImpl(unittest.TestCase): +def test_scanner_entity(): """Test coverage for base ScannerEntity entity class.""" - - def setUp(self): - """Set up test data.""" - self.entity = TestScannerImplementation() - - def tearDown(self): - """Tear down unit test data.""" - self.entity = None - - def test_scannerentity(self): - """Test scanner entity methods.""" - assert self.entity.source_type == SOURCE_TYPE_ROUTER - assert self.entity.is_connected is False - assert self.entity.state == STATE_NOT_HOME - self.entity.connected = True - assert self.entity.is_connected is True - assert self.entity.state == STATE_HOME - assert self.entity.battery_level == 100 - assert self.entity.state_attributes == { - ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, - ATTR_BATTERY_LEVEL: 100 - } + entity = ScannerEntity() + with pytest.raises(NotImplementedError): + assert entity.source_type is None + with pytest.raises(NotImplementedError): + assert entity.is_connected is None + with pytest.raises(NotImplementedError): + assert entity.state == STATE_NOT_HOME + assert entity.battery_level is None -class TestBaseTrackerEntity(unittest.TestCase): +def test_base_tracker_entity(): """Test coverage for base BaseTrackerEntity entity class.""" - - def setUp(self): - """Set up test data.""" - self.entity = BaseTrackerEntity() - - def tearDown(self): - """Tear down unit test data.""" - self.entity = None - - def test_basetrackerentity(self): - """Test BaseTrackerEntity entity methods.""" - with pytest.raises(NotImplementedError): - assert self.entity.source_type is None - assert self.entity.battery_level is None - with pytest.raises(NotImplementedError): - assert self.entity.state_attributes is None + entity = BaseTrackerEntity() + with pytest.raises(NotImplementedError): + assert entity.source_type is None + assert entity.battery_level is None + with pytest.raises(NotImplementedError): + assert entity.state_attributes is None diff --git a/tests/testing_config/custom_components/test/device_tracker.py b/tests/testing_config/custom_components/test/device_tracker.py index 6f4314b767dd..960f9eb47da9 100644 --- a/tests/testing_config/custom_components/test/device_tracker.py +++ b/tests/testing_config/custom_components/test/device_tracker.py @@ -1,6 +1,8 @@ """Provide a mock device scanner.""" from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER def get_scanner(hass, config): @@ -8,6 +10,43 @@ def get_scanner(hass, config): return SCANNER +class MockScannerEntity(ScannerEntity): + """Test implementation of a ScannerEntity.""" + + def __init__(self): + """Init.""" + self.connected = False + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_ROUTER + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return 100 + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self.connected + + def set_connected(self): + """Set connected to True.""" + self.connected = True + self.async_schedule_update_ha_state() + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the config entry.""" + entity = MockScannerEntity() + async_add_entities([entity]) + + class MockScanner(DeviceScanner): """Mock device scanner."""