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
Changes from 18 commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -37,7 +37,7 @@
return await hass.data[DOMAIN].async_unload_entry(entry)


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

@property
@@ -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.
@@ -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."""
@@ -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
@@ -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):
@@ -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__(
@@ -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):
@@ -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 @@
return True


class MobileAppEntity(DeviceTrackerEntity, RestoreEntity):
class MobileAppEntity(TrackerEntity, RestoreEntity):
"""Represent a tracked device."""

def __init__(self, entry, data=None):
@@ -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):
@@ -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,
@@ -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
@@ -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
This conversation was marked as resolved by dmulcahey

This comment has been minimized.

Copy link
@balloob

balloob Jul 1, 2019

Member

Let's stick to a consistent number format ?

This comment has been minimized.

Copy link
@dmulcahey

dmulcahey Jul 1, 2019

Author Contributor

I’ll convert the other 2. Zigbee is generally all in hex


DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
@@ -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
}


@@ -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({
@@ -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)
@@ -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):
This conversation was marked as resolved by dmulcahey

This comment has been minimized.

Copy link
@balloob

balloob Jun 17, 2019

Member

Instead of relying on polling, use async_call_later to automatically update the state to be not home.

This comment has been minimized.

Copy link
@dmulcahey

dmulcahey Jun 18, 2019

Author Contributor

how would that work? This needs to happen continually. we're checking the last seen on the device in zigpy (local property check... nothing remote) against a threshold. The way the device works is by sending battery reports very frequently once it connects to the network.

This comment has been minimized.

Copy link
@balloob

balloob Jun 18, 2019

Member

Something like this:

self.state = STATE_HOME
if self._unsub_state_check:
    self._unsub_state_check()
self._unsub_state_check = async_call_later(self.hass, 10, self._mark_not_home)

This comment has been minimized.

Copy link
@dmulcahey

dmulcahey Jun 18, 2019

Author Contributor

Updated. How about something like this? Polling seemed cleaner. Is there an advantage to doing it this way?

This comment has been minimized.

Copy link
@balloob

balloob Jul 1, 2019

Member

Polling feels weird if we know the exact time it would be marked as stale.

This comment has been minimized.

Copy link
@dmulcahey

dmulcahey Jul 1, 2019

Author Contributor

Ok. So is the updated code acceptable?

"""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
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.