mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
New scanner device tracker and ZHA device tracker support (#24584)
* initial implementation for zha device trackers * constant * review comments * Revert "review comments" This reverts commit 2130823566820dfc114dbeda08fcdf76ed47a4e7. * 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
This commit is contained in:
parent
e824c553ca
commit
3c487928d4
@ -37,7 +37,7 @@ async def async_unload_entry(hass, entry):
|
|||||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTrackerEntity(Entity):
|
class BaseTrackerEntity(Entity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -48,6 +48,27 @@ class DeviceTrackerEntity(Entity):
|
|||||||
"""
|
"""
|
||||||
return None
|
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
|
@property
|
||||||
def location_accuracy(self):
|
def location_accuracy(self):
|
||||||
"""Return the location accuracy of the device.
|
"""Return the location accuracy of the device.
|
||||||
@ -71,11 +92,6 @@ class DeviceTrackerEntity(Entity):
|
|||||||
"""Return longitude value of the device."""
|
"""Return longitude value of the device."""
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|
||||||
@property
|
|
||||||
def source_type(self):
|
|
||||||
"""Return the source type, eg gps or router, of the device."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
@ -99,16 +115,27 @@ class DeviceTrackerEntity(Entity):
|
|||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
attr = {
|
attr = {}
|
||||||
ATTR_SOURCE_TYPE: self.source_type
|
attr.update(super().state_attributes)
|
||||||
}
|
|
||||||
|
|
||||||
if self.latitude is not None:
|
if self.latitude is not None:
|
||||||
attr[ATTR_LATITUDE] = self.latitude
|
attr[ATTR_LATITUDE] = self.latitude
|
||||||
attr[ATTR_LONGITUDE] = self.longitude
|
attr[ATTR_LONGITUDE] = self.longitude
|
||||||
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
|
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
|
||||||
|
|
||||||
if self.battery_level:
|
|
||||||
attr[ATTR_BATTERY_LEVEL] = self.battery_level
|
|
||||||
|
|
||||||
return attr
|
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.const import (
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
from homeassistant.components.device_tracker.config_entry import (
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
DeviceTrackerEntity
|
TrackerEntity
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
@ -52,7 +52,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class GeofencyEntity(DeviceTrackerEntity, RestoreEntity):
|
class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
def __init__(self, device, gps=None, location_name=None, attributes=None):
|
def __init__(self, device, gps=None, location_name=None, attributes=None):
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
from homeassistant.components.device_tracker.config_entry import (
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
DeviceTrackerEntity
|
TrackerEntity
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import device_registry
|
from homeassistant.helpers import device_registry
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry,
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class GPSLoggerEntity(DeviceTrackerEntity, RestoreEntity):
|
class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
from homeassistant.components.device_tracker.config_entry import (
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
DeviceTrackerEntity
|
TrackerEntity
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class LocativeEntity(DeviceTrackerEntity):
|
class LocativeEntity(TrackerEntity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
def __init__(self, device, location, location_name):
|
def __init__(self, device, location, location_name):
|
||||||
|
@ -9,7 +9,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
|
from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
|
||||||
from homeassistant.components.device_tracker.config_entry import (
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
DeviceTrackerEntity
|
TrackerEntity
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -44,7 +44,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class MobileAppEntity(DeviceTrackerEntity, RestoreEntity):
|
class MobileAppEntity(TrackerEntity, RestoreEntity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
def __init__(self, entry, data=None):
|
def __init__(self, entry, data=None):
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.components.device_tracker.const import (
|
from homeassistant.components.device_tracker.const import (
|
||||||
ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS)
|
ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS)
|
||||||
from homeassistant.components.device_tracker.config_entry import (
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
DeviceTrackerEntity
|
TrackerEntity
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers import device_registry
|
from homeassistant.helpers import device_registry
|
||||||
@ -62,7 +62,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity):
|
class OwnTracksEntity(TrackerEntity, RestoreEntity):
|
||||||
"""Represent a tracked device."""
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
def __init__(self, dev_id, data=None):
|
def __init__(self, dev_id, data=None):
|
||||||
|
@ -3,6 +3,7 @@ import enum
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
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.fan import DOMAIN as FAN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT
|
from homeassistant.components.light import DOMAIN as LIGHT
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK
|
from homeassistant.components.lock import DOMAIN as LOCK
|
||||||
@ -25,6 +26,7 @@ ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
|
|||||||
|
|
||||||
COMPONENTS = (
|
COMPONENTS = (
|
||||||
BINARY_SENSOR,
|
BINARY_SENSOR,
|
||||||
|
DEVICE_TRACKER,
|
||||||
FAN,
|
FAN,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
LOCK,
|
LOCK,
|
||||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/zha/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
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.fan import DOMAIN as FAN
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT
|
from homeassistant.components.light import DOMAIN as LIGHT
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK
|
from homeassistant.components.lock import DOMAIN as LOCK
|
||||||
@ -21,8 +22,9 @@ from .const import (
|
|||||||
CONTROLLER, BATTERY
|
CONTROLLER, BATTERY
|
||||||
)
|
)
|
||||||
|
|
||||||
SMARTTHINGS_HUMIDITY_CLUSTER = 64581
|
SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45
|
||||||
SMARTTHINGS_ACCELERATION_CLUSTER = 64514
|
SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02
|
||||||
|
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000
|
||||||
|
|
||||||
DEVICE_CLASS = {}
|
DEVICE_CLASS = {}
|
||||||
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
@ -39,12 +41,14 @@ OUTPUT_CHANNEL_ONLY_CLUSTERS = []
|
|||||||
BINDABLE_CLUSTERS = []
|
BINDABLE_CLUSTERS = []
|
||||||
INPUT_BIND_ONLY_CLUSTERS = []
|
INPUT_BIND_ONLY_CLUSTERS = []
|
||||||
BINARY_SENSOR_CLUSTERS = set()
|
BINARY_SENSOR_CLUSTERS = set()
|
||||||
|
DEVICE_TRACKER_CLUSTERS = set()
|
||||||
LIGHT_CLUSTERS = set()
|
LIGHT_CLUSTERS = set()
|
||||||
SWITCH_CLUSTERS = set()
|
SWITCH_CLUSTERS = set()
|
||||||
COMPONENT_CLUSTERS = {
|
COMPONENT_CLUSTERS = {
|
||||||
BINARY_SENSOR: BINARY_SENSOR_CLUSTERS,
|
BINARY_SENSOR: BINARY_SENSOR_CLUSTERS,
|
||||||
LIGHT: LIGHT_CLUSTERS,
|
LIGHT: LIGHT_CLUSTERS,
|
||||||
SWITCH: SWITCH_CLUSTERS
|
SWITCH: SWITCH_CLUSTERS,
|
||||||
|
DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -134,7 +138,8 @@ def establish_device_mappings():
|
|||||||
zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH,
|
zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH,
|
||||||
zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT,
|
zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT,
|
||||||
zha.DeviceType.COLOR_TEMPERATURE_LIGHT: 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({
|
DEVICE_CLASS[zll.PROFILE_ID].update({
|
||||||
@ -323,6 +328,9 @@ def establish_device_mappings():
|
|||||||
zcl.clusters.measurement.OccupancySensing.cluster_id)
|
zcl.clusters.measurement.OccupancySensing.cluster_id)
|
||||||
BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER)
|
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.OnOff.cluster_id)
|
||||||
LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id)
|
LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id)
|
||||||
LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id)
|
LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id)
|
||||||
|
105
homeassistant/components/zha/device_tracker.py
Normal file
105
homeassistant/components/zha/device_tracker.py
Normal file
@ -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
|
62
tests/components/device_tracker/test_entities.py
Normal file
62
tests/components/device_tracker/test_entities.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""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, DOMAIN
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_HOME,
|
||||||
|
STATE_NOT_HOME,
|
||||||
|
ATTR_BATTERY_LEVEL
|
||||||
|
)
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_scanner_entity_device_tracker(hass):
|
||||||
|
"""Test ScannerEntity based device tracker."""
|
||||||
|
config_entry = MockConfigEntry(domain='test')
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
|
config_entry, DOMAIN)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
entity = hass.data[DOMAIN].get_entity(entity_id)
|
||||||
|
entity.set_connected()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_state = hass.states.get(entity_id)
|
||||||
|
assert entity_state.state == STATE_HOME
|
||||||
|
|
||||||
|
|
||||||
|
def test_scanner_entity():
|
||||||
|
"""Test coverage for base ScannerEntity entity class."""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_tracker_entity():
|
||||||
|
"""Test coverage for base BaseTrackerEntity entity class."""
|
||||||
|
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
|
89
tests/components/zha/test_device_tracker.py
Normal file
89
tests/components/zha/test_device_tracker.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""Test ZHA Device Tracker."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import time
|
||||||
|
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
|
||||||
|
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(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,
|
||||||
|
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE)
|
@ -1,6 +1,8 @@
|
|||||||
"""Provide a mock device scanner."""
|
"""Provide a mock device scanner."""
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import DeviceScanner
|
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):
|
def get_scanner(hass, config):
|
||||||
@ -8,6 +10,43 @@ def get_scanner(hass, config):
|
|||||||
return SCANNER
|
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):
|
class MockScanner(DeviceScanner):
|
||||||
"""Mock device scanner."""
|
"""Mock device scanner."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user