Skip to content

Commit

Permalink
New scanner device tracker and ZHA device tracker support (#24584)
Browse files Browse the repository at this point in the history
* initial implementation for zha device trackers

* constant

* review comments

* Revert "review comments"

This reverts commit 2130823.

* rename device tracker entity

* update trackers

* raise when not implemented

* Update homeassistant/components/device_tracker/config_entry.py

Review comment

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* move source type to base state attrs

* review comments

* review comments

* review comments

* fix super call

* fix battery and use last seen from device

* add test

* cleanup and add more to test

* cleanup post zha entity removal PR

* add tests for base entities

* rework entity tests
  • Loading branch information
dmulcahey authored and MartinHjelmare committed Jul 4, 2019
1 parent e824c55 commit 3c48792
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 27 deletions.
53 changes: 40 additions & 13 deletions homeassistant/components/device_tracker/config_entry.py
Expand Up @@ -37,7 +37,7 @@ async def async_unload_entry(hass, entry):
return await hass.data[DOMAIN].async_unload_entry(entry)


class DeviceTrackerEntity(Entity):
class BaseTrackerEntity(Entity):
"""Represent a tracked device."""

@property
Expand All @@ -48,6 +48,27 @@ 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 = {
ATTR_SOURCE_TYPE: self.source_type
}

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.
Expand All @@ -71,11 +92,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."""
Expand All @@ -99,16 +115,27 @@ 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
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 scanned 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."""
raise NotImplementedError
4 changes: 2 additions & 2 deletions homeassistant/components/geofency/device_tracker.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/gpslogger/device_tracker.py
Expand Up @@ -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
Expand Down Expand Up @@ -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__(
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/locative/device_tracker.py
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/mobile_app/device_tracker.py
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/owntracks/device_tracker.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/zha/core/const.py
Expand Up @@ -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
Expand All @@ -25,6 +26,7 @@

COMPONENTS = (
BINARY_SENSOR,
DEVICE_TRACKER,
FAN,
LIGHT,
LOCK,
Expand Down
16 changes: 12 additions & 4 deletions homeassistant/components/zha/core/registries.py
Expand Up @@ -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
Expand All @@ -21,8 +22,9 @@
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 = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
Expand All @@ -39,12 +41,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
}


Expand Down Expand Up @@ -134,7 +138,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,
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER
})

DEVICE_CLASS[zll.PROFILE_ID].update({
Expand Down Expand Up @@ -323,6 +328,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)
Expand Down
105 changes: 105 additions & 0 deletions homeassistant/components/zha/device_tracker.py
@@ -0,0 +1,105 @@
"""Support for the ZHA platform."""
import logging
import time
from homeassistant.components.device_tracker import (
SOURCE_TYPE_ROUTER, DOMAIN
)
from homeassistant.components.device_tracker.config_entry import (
ScannerEntity
)
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_ATTR_UPDATED
)
from .entity import ZhaEntity
from .sensor import battery_percentage_remaining_formatter

_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]


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(ZHADeviceScannerEntity(**discovery_info))

async_add_entities(entities, update_before_add=True)


class ZHADeviceScannerEntity(ScannerEntity, 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._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."""
await super().async_added_to_hass()
if self._battery_channel:
await self.async_accept_signal(
self._battery_channel, SIGNAL_ATTR_UPDATED,
self.async_battery_percentage_remaining_updated)

async def async_update(self):
"""Handle polling."""
if self.zha_device.last_seen is None:
self._connected = False
else:
difference = time.time() - self.zha_device.last_seen
if difference > self._keepalive_interval:
self._connected = False
else:
self._connected = True

@property
def is_connected(self):
"""Return true if the device is connected to the network."""
return self._connected

@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
return SOURCE_TYPE_ROUTER

@callback
def async_battery_percentage_remaining_updated(self, value):
"""Handle tracking."""
_LOGGER.debug('battery_percentage_remaining updated: %s', value)
self._connected = True
self._battery_level = battery_percentage_remaining_formatter(value)
self.async_schedule_update_ha_state()

@property
def battery_level(self):
"""Return the battery level of the device.
Percentage from 0-100.
"""
return self._battery_level

0 comments on commit 3c48792

Please sign in to comment.