Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New scanner device tracker and ZHA device tracker support #24584

Merged
merged 19 commits into from Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
dmulcahey marked this conversation as resolved.
Show resolved Hide resolved

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):
dmulcahey marked this conversation as resolved.
Show resolved Hide resolved
"""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