Config entry device tracker (#24040)
* Move zone helpers to zone root * Add config entry support to device tracker * Convert Geofency * Convert GPSLogger * Track unsub per entry * Convert locative * Migrate OwnTracks * Lint * location -> latitude, longitude props * Lint * lint * Fix testpull/24117/head
parent
144b530045
commit
e6d7f6ed71
|
@ -3,10 +3,8 @@ import asyncio
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group
|
||||||
from homeassistant.config import config_without_domain
|
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||||
|
@ -15,6 +13,9 @@ from homeassistant.helpers.event import async_track_utc_time_change
|
||||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
|
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
|
||||||
|
|
||||||
from . import legacy, setup
|
from . import legacy, setup
|
||||||
|
from .config_entry import ( # noqa # pylint: disable=unused-import
|
||||||
|
async_setup_entry, async_unload_entry
|
||||||
|
)
|
||||||
from .legacy import DeviceScanner # noqa # pylint: disable=unused-import
|
from .legacy import DeviceScanner # noqa # pylint: disable=unused-import
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ATTRIBUTES,
|
ATTR_ATTRIBUTES,
|
||||||
|
@ -35,9 +36,7 @@ from .const import (
|
||||||
DEFAULT_CONSIDER_HOME,
|
DEFAULT_CONSIDER_HOME,
|
||||||
DEFAULT_TRACK_NEW,
|
DEFAULT_TRACK_NEW,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
|
||||||
PLATFORM_TYPE_LEGACY,
|
PLATFORM_TYPE_LEGACY,
|
||||||
SCAN_INTERVAL,
|
|
||||||
SOURCE_TYPE_BLUETOOTH_LE,
|
SOURCE_TYPE_BLUETOOTH_LE,
|
||||||
SOURCE_TYPE_BLUETOOTH,
|
SOURCE_TYPE_BLUETOOTH,
|
||||||
SOURCE_TYPE_GPS,
|
SOURCE_TYPE_GPS,
|
||||||
|
@ -113,36 +112,13 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
"""Set up the device tracker."""
|
"""Set up the device tracker."""
|
||||||
tracker = await legacy.get_tracker(hass, config)
|
tracker = await legacy.get_tracker(hass, config)
|
||||||
|
|
||||||
async def setup_entry_helper(entry):
|
legacy_platforms = await setup.async_extract_config(hass, config)
|
||||||
"""Set up a config entry."""
|
|
||||||
platform = await setup.async_create_platform_type(
|
|
||||||
hass, config, entry.domain, entry)
|
|
||||||
|
|
||||||
if platform is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
await platform.async_setup_legacy(hass, tracker)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = setup_entry_helper
|
|
||||||
component = EntityComponent(
|
|
||||||
LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
|
||||||
|
|
||||||
legacy_platforms, entity_platforms = \
|
|
||||||
await setup.async_extract_config(hass, config)
|
|
||||||
|
|
||||||
setup_tasks = [
|
setup_tasks = [
|
||||||
legacy_platform.async_setup_legacy(hass, tracker)
|
legacy_platform.async_setup_legacy(hass, tracker)
|
||||||
for legacy_platform in legacy_platforms
|
for legacy_platform in legacy_platforms
|
||||||
]
|
]
|
||||||
|
|
||||||
if entity_platforms:
|
|
||||||
setup_tasks.append(component.async_setup({
|
|
||||||
**config_without_domain(config, DOMAIN),
|
|
||||||
DOMAIN: [platform.config for platform in entity_platforms]
|
|
||||||
}))
|
|
||||||
|
|
||||||
if setup_tasks:
|
if setup_tasks:
|
||||||
await asyncio.wait(setup_tasks)
|
await asyncio.wait(setup_tasks)
|
||||||
|
|
||||||
|
@ -178,8 +154,3 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
# restore
|
# restore
|
||||||
await tracker.async_setup_tracked_device()
|
await tracker.async_setup_tracked_device()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
|
||||||
"""Set up an entry."""
|
|
||||||
return await hass.data[DOMAIN](entry)
|
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
"""Code to set up a device tracker platform using a config entry."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_NOT_HOME,
|
||||||
|
STATE_HOME,
|
||||||
|
ATTR_GPS_ACCURACY,
|
||||||
|
ATTR_LATITUDE,
|
||||||
|
ATTR_LONGITUDE,
|
||||||
|
ATTR_BATTERY_LEVEL,
|
||||||
|
)
|
||||||
|
from homeassistant.components import zone
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_SOURCE_TYPE,
|
||||||
|
DOMAIN,
|
||||||
|
LOGGER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up an entry."""
|
||||||
|
component = hass.data.get(DOMAIN) # type: Optional[EntityComponent]
|
||||||
|
|
||||||
|
if component is None:
|
||||||
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
|
LOGGER, DOMAIN, hass
|
||||||
|
)
|
||||||
|
|
||||||
|
return await component.async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload an entry."""
|
||||||
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTrackerEntity(Entity):
|
||||||
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self):
|
||||||
|
"""Return the battery level of the device.
|
||||||
|
|
||||||
|
Percentage from 0-100.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_accuracy(self):
|
||||||
|
"""Return the location accuracy of the device.
|
||||||
|
|
||||||
|
Value in meters.
|
||||||
|
"""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_name(self) -> str:
|
||||||
|
"""Return a location name for the current location of the device."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self) -> float:
|
||||||
|
"""Return latitude value of the device."""
|
||||||
|
return NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
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."""
|
||||||
|
if self.location_name:
|
||||||
|
return self.location_name
|
||||||
|
|
||||||
|
if self.latitude is not None:
|
||||||
|
zone_state = zone.async_active_zone(
|
||||||
|
self.hass, self.latitude, self.longitude,
|
||||||
|
self.location_accuracy)
|
||||||
|
if zone_state is None:
|
||||||
|
state = STATE_NOT_HOME
|
||||||
|
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||||
|
state = STATE_HOME
|
||||||
|
else:
|
||||||
|
state = zone_state.name
|
||||||
|
return state
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the device state attributes."""
|
||||||
|
attr = {
|
||||||
|
ATTR_SOURCE_TYPE: self.source_type
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
|
@ -10,7 +10,7 @@ from homeassistant.components import zone
|
||||||
from homeassistant.components.group import (
|
from homeassistant.components.group import (
|
||||||
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
|
ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE,
|
||||||
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
|
DOMAIN as DOMAIN_GROUP, SERVICE_SET)
|
||||||
from homeassistant.components.zone.zone import async_active_zone
|
from homeassistant.components.zone import async_active_zone
|
||||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
|
@ -20,7 +20,6 @@ from homeassistant.const import (
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORM_TYPE_ENTITY,
|
|
||||||
PLATFORM_TYPE_LEGACY,
|
PLATFORM_TYPE_LEGACY,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
SCAN_INTERVAL,
|
SCAN_INTERVAL,
|
||||||
|
@ -38,14 +37,7 @@ class DeviceTrackerPlatform:
|
||||||
'get_scanner',
|
'get_scanner',
|
||||||
'async_setup_scanner',
|
'async_setup_scanner',
|
||||||
'setup_scanner',
|
'setup_scanner',
|
||||||
# Small steps, initially just legacy setup supported.
|
|
||||||
'async_setup_entry'
|
|
||||||
)
|
)
|
||||||
# ENTITY_PLATFORM_SETUP = (
|
|
||||||
# 'setup_platform',
|
|
||||||
# 'async_setup_platform',
|
|
||||||
# 'async_setup_entry'
|
|
||||||
# )
|
|
||||||
|
|
||||||
name = attr.ib(type=str)
|
name = attr.ib(type=str)
|
||||||
platform = attr.ib(type=ModuleType)
|
platform = attr.ib(type=ModuleType)
|
||||||
|
@ -56,7 +48,6 @@ class DeviceTrackerPlatform:
|
||||||
"""Return platform type."""
|
"""Return platform type."""
|
||||||
for methods, platform_type in (
|
for methods, platform_type in (
|
||||||
(self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),
|
(self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),
|
||||||
# (self.ENTITY_PLATFORM_SETUP, PLATFORM_TYPE_ENTITY),
|
|
||||||
):
|
):
|
||||||
for meth in methods:
|
for meth in methods:
|
||||||
if hasattr(self.platform, meth):
|
if hasattr(self.platform, meth):
|
||||||
|
@ -83,9 +74,6 @@ class DeviceTrackerPlatform:
|
||||||
setup = await hass.async_add_job(
|
setup = await hass.async_add_job(
|
||||||
self.platform.setup_scanner, hass, self.config,
|
self.platform.setup_scanner, hass, self.config,
|
||||||
tracker.see, discovery_info)
|
tracker.see, discovery_info)
|
||||||
elif hasattr(self.platform, 'async_setup_entry'):
|
|
||||||
setup = await self.platform.async_setup_entry(
|
|
||||||
hass, self.config, tracker.async_see)
|
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Invalid legacy device_tracker platform.")
|
"Invalid legacy device_tracker platform.")
|
||||||
|
@ -106,7 +94,6 @@ class DeviceTrackerPlatform:
|
||||||
async def async_extract_config(hass, config):
|
async def async_extract_config(hass, config):
|
||||||
"""Extract device tracker config and split between legacy and modern."""
|
"""Extract device tracker config and split between legacy and modern."""
|
||||||
legacy = []
|
legacy = []
|
||||||
entity_platform = []
|
|
||||||
|
|
||||||
for platform in await asyncio.gather(*[
|
for platform in await asyncio.gather(*[
|
||||||
async_create_platform_type(hass, config, p_type, p_config)
|
async_create_platform_type(hass, config, p_type, p_config)
|
||||||
|
@ -115,15 +102,13 @@ async def async_extract_config(hass, config):
|
||||||
if platform is None:
|
if platform is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if platform.type == PLATFORM_TYPE_ENTITY:
|
if platform.type == PLATFORM_TYPE_LEGACY:
|
||||||
entity_platform.append(platform)
|
|
||||||
elif platform.type == PLATFORM_TYPE_LEGACY:
|
|
||||||
legacy.append(platform)
|
legacy.append(platform)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unable to determine type for {}: {}".format(
|
raise ValueError("Unable to determine type for {}: {}".format(
|
||||||
platform.name, platform.type))
|
platform.name, platform.type))
|
||||||
|
|
||||||
return (legacy, entity_platform)
|
return legacy
|
||||||
|
|
||||||
|
|
||||||
async def async_create_platform_type(hass, config, p_type, p_config) \
|
async def async_create_platform_type(hass, config, p_type, p_config) \
|
||||||
|
|
|
@ -63,7 +63,11 @@ async def async_setup(hass, hass_config):
|
||||||
"""Set up the Geofency component."""
|
"""Set up the Geofency component."""
|
||||||
config = hass_config.get(DOMAIN, {})
|
config = hass_config.get(DOMAIN, {})
|
||||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
|
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
|
||||||
hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons]
|
hass.data[DOMAIN] = {
|
||||||
|
'beacons': [slugify(beacon) for beacon in mobile_beacons],
|
||||||
|
'devices': set(),
|
||||||
|
'unsub_device_tracker': {}
|
||||||
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +81,7 @@ async def handle_webhook(hass, webhook_id, request):
|
||||||
status=HTTP_UNPROCESSABLE_ENTITY
|
status=HTTP_UNPROCESSABLE_ENTITY
|
||||||
)
|
)
|
||||||
|
|
||||||
if _is_mobile_beacon(data, hass.data[DOMAIN]):
|
if _is_mobile_beacon(data, hass.data[DOMAIN]['beacons']):
|
||||||
return _set_location(hass, data, None)
|
return _set_location(hass, data, None)
|
||||||
if data['entry'] == LOCATION_ENTRY:
|
if data['entry'] == LOCATION_ENTRY:
|
||||||
location_name = data['name']
|
location_name = data['name']
|
||||||
|
@ -128,7 +132,7 @@ async def async_setup_entry(hass, entry):
|
||||||
async def async_unload_entry(hass, entry):
|
async def async_unload_entry(hass, entry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||||
|
hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)()
|
||||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,100 @@
|
||||||
"""Support for the Geofency device tracker platform."""
|
"""Support for the Geofency device tracker platform."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.core import callback
|
||||||
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
|
DeviceTrackerEntity
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from . import DOMAIN as GEOFENCY_DOMAIN, TRACKER_UPDATE
|
from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_KEY = '{}.{}'.format(GEOFENCY_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
async def async_setup_entry(hass, entry, async_see):
|
"""Set up Geofency config entry."""
|
||||||
"""Configure a dispatcher connection based on a config entry."""
|
@callback
|
||||||
async def _set_location(device, gps, location_name, attributes):
|
def _receive_data(device, gps, location_name, attributes):
|
||||||
"""Fire HA event to set location."""
|
"""Fire HA event to set location."""
|
||||||
await async_see(
|
if device in hass.data[GF_DOMAIN]['devices']:
|
||||||
dev_id=device,
|
return
|
||||||
gps=gps,
|
|
||||||
location_name=location_name,
|
hass.data[GF_DOMAIN]['devices'].add(device)
|
||||||
attributes=attributes
|
|
||||||
)
|
async_add_entities([GeofencyEntity(
|
||||||
|
device, gps, location_name, attributes
|
||||||
|
)])
|
||||||
|
|
||||||
|
hass.data[GF_DOMAIN]['unsub_device_tracker'][config_entry.entry_id] = \
|
||||||
|
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
|
||||||
|
|
||||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
|
||||||
hass, TRACKER_UPDATE, _set_location
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass, entry):
|
class GeofencyEntity(DeviceTrackerEntity):
|
||||||
"""Unload the config entry and remove the dispatcher connection."""
|
"""Represent a tracked device."""
|
||||||
hass.data[DATA_KEY]()
|
|
||||||
return True
|
def __init__(self, device, gps, location_name, attributes):
|
||||||
|
"""Set up Geofency entity."""
|
||||||
|
self._attributes = attributes
|
||||||
|
self._name = device
|
||||||
|
self._location_name = location_name
|
||||||
|
self._gps = gps
|
||||||
|
self._unsub_dispatcher = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return self._attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self):
|
||||||
|
"""Return latitude value of the device."""
|
||||||
|
return self._gps[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def longitude(self):
|
||||||
|
"""Return longitude value of the device."""
|
||||||
|
return self._gps[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_name(self):
|
||||||
|
"""Return a location name for the current location of the device."""
|
||||||
|
return self._location_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_type(self):
|
||||||
|
"""Return the source type, eg gps or router, of the device."""
|
||||||
|
return SOURCE_TYPE_GPS
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register state update callback."""
|
||||||
|
self._unsub_dispatcher = async_dispatcher_connect(
|
||||||
|
self.hass, TRACKER_UPDATE, self._async_receive_data)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Clean up after entity before removal."""
|
||||||
|
self._unsub_dispatcher()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_receive_data(self, device, gps, location_name, attributes):
|
||||||
|
"""Mark the device as seen."""
|
||||||
|
if device != self.name:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._attributes.update(attributes)
|
||||||
|
self._location_name = location_name
|
||||||
|
self._gps = gps
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -50,6 +50,10 @@ WEBHOOK_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
async def async_setup(hass, hass_config):
|
async def async_setup(hass, hass_config):
|
||||||
"""Set up the GPSLogger component."""
|
"""Set up the GPSLogger component."""
|
||||||
|
hass.data[DOMAIN] = {
|
||||||
|
'devices': set(),
|
||||||
|
'unsub_device_tracker': {},
|
||||||
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +102,7 @@ async def async_setup_entry(hass, entry):
|
||||||
async def async_unload_entry(hass, entry):
|
async def async_unload_entry(hass, entry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||||
|
hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)()
|
||||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,109 @@
|
||||||
"""Support for the GPSLogger device tracking."""
|
"""Support for the GPSLogger device tracking."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.core import callback
|
||||||
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
|
DeviceTrackerEntity
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import DOMAIN as GPSLOGGER_DOMAIN, TRACKER_UPDATE
|
from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_KEY = '{}.{}'.format(GPSLOGGER_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistantType, entry,
|
||||||
async def async_setup_entry(hass: HomeAssistantType, entry, async_see):
|
async_add_entities):
|
||||||
"""Configure a dispatcher connection based on a config entry."""
|
"""Configure a dispatcher connection based on a config entry."""
|
||||||
async def _set_location(device, gps_location, battery, accuracy, attrs):
|
@callback
|
||||||
"""Fire HA event to set location."""
|
def _receive_data(device, gps, battery, accuracy, attrs):
|
||||||
await async_see(
|
"""Receive set location."""
|
||||||
dev_id=device,
|
if device in hass.data[GPL_DOMAIN]['devices']:
|
||||||
gps=gps_location,
|
return
|
||||||
battery=battery,
|
|
||||||
gps_accuracy=accuracy,
|
|
||||||
attributes=attrs
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
hass.data[GPL_DOMAIN]['devices'].add(device)
|
||||||
hass, TRACKER_UPDATE, _set_location
|
|
||||||
)
|
async_add_entities([GPSLoggerEntity(
|
||||||
return True
|
device, gps, battery, accuracy, attrs
|
||||||
|
)])
|
||||||
|
|
||||||
|
hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \
|
||||||
|
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistantType, entry):
|
class GPSLoggerEntity(DeviceTrackerEntity):
|
||||||
"""Unload the config entry and remove the dispatcher connection."""
|
"""Represent a tracked device."""
|
||||||
hass.data[DATA_KEY]()
|
|
||||||
return True
|
def __init__(
|
||||||
|
self, device, location, battery, accuracy, attributes):
|
||||||
|
"""Set up Geofency entity."""
|
||||||
|
self._accuracy = accuracy
|
||||||
|
self._attributes = attributes
|
||||||
|
self._name = device
|
||||||
|
self._battery = battery
|
||||||
|
self._location = location
|
||||||
|
self._unsub_dispatcher = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self):
|
||||||
|
"""Return battery value of the device."""
|
||||||
|
return self._battery
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return self._attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self):
|
||||||
|
"""Return latitude value of the device."""
|
||||||
|
return self._location[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def longitude(self):
|
||||||
|
"""Return longitude value of the device."""
|
||||||
|
return self._location[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_accuracy(self):
|
||||||
|
"""Return the gps accuracy of the device."""
|
||||||
|
return self._accuracy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_type(self):
|
||||||
|
"""Return the source type, eg gps or router, of the device."""
|
||||||
|
return SOURCE_TYPE_GPS
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register state update callback."""
|
||||||
|
self._unsub_dispatcher = async_dispatcher_connect(
|
||||||
|
self.hass, TRACKER_UPDATE, self._async_receive_data)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Clean up after entity before removal."""
|
||||||
|
self._unsub_dispatcher()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_receive_data(self, device, location, battery, accuracy,
|
||||||
|
attributes):
|
||||||
|
"""Mark the device as seen."""
|
||||||
|
if device != self.name:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._location = location
|
||||||
|
self._battery = battery
|
||||||
|
self._accuracy = accuracy
|
||||||
|
self._attributes.update(attributes)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -10,12 +10,13 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||||
from homeassistant.components.device_tracker.const import (
|
from homeassistant.components.device_tracker.const import (
|
||||||
DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
|
DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
|
||||||
from homeassistant.components.device_tracker.legacy import DeviceScanner
|
from homeassistant.components.device_tracker.legacy import DeviceScanner
|
||||||
from homeassistant.components.zone.zone import active_zone
|
from homeassistant.components.zone import async_active_zone
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.location import distance
|
from homeassistant.util.location import distance
|
||||||
|
from homeassistant.util.async_ import run_callback_threadsafe
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -330,7 +331,10 @@ class Icloud(DeviceScanner):
|
||||||
|
|
||||||
def determine_interval(self, devicename, latitude, longitude, battery):
|
def determine_interval(self, devicename, latitude, longitude, battery):
|
||||||
"""Calculate new interval."""
|
"""Calculate new interval."""
|
||||||
currentzone = active_zone(self.hass, latitude, longitude)
|
currentzone = run_callback_threadsafe(
|
||||||
|
self.hass.loop,
|
||||||
|
async_active_zone, self.hass, latitude, longitude
|
||||||
|
).result()
|
||||||
|
|
||||||
if ((currentzone is not None and
|
if ((currentzone is not None and
|
||||||
currentzone == self._overridestates.get(devicename)) or
|
currentzone == self._overridestates.get(devicename)) or
|
||||||
|
@ -472,10 +476,13 @@ class Icloud(DeviceScanner):
|
||||||
devicestate = self.hass.states.get(devid)
|
devicestate = self.hass.states.get(devid)
|
||||||
if interval is not None:
|
if interval is not None:
|
||||||
if devicestate is not None:
|
if devicestate is not None:
|
||||||
self._overridestates[device] = active_zone(
|
self._overridestates[device] = run_callback_threadsafe(
|
||||||
|
self.hass.loop,
|
||||||
|
async_active_zone,
|
||||||
self.hass,
|
self.hass,
|
||||||
float(devicestate.attributes.get('latitude', 0)),
|
float(devicestate.attributes.get('latitude', 0)),
|
||||||
float(devicestate.attributes.get('longitude', 0)))
|
float(devicestate.attributes.get('longitude', 0))
|
||||||
|
).result()
|
||||||
if self._overridestates[device] is None:
|
if self._overridestates[device] is None:
|
||||||
self._overridestates[device] = 'away'
|
self._overridestates[device] = 'away'
|
||||||
self._intervals[device] = interval
|
self._intervals[device] = interval
|
||||||
|
|
|
@ -49,6 +49,10 @@ WEBHOOK_SCHEMA = vol.All(
|
||||||
|
|
||||||
async def async_setup(hass, hass_config):
|
async def async_setup(hass, hass_config):
|
||||||
"""Set up the Locative component."""
|
"""Set up the Locative component."""
|
||||||
|
hass.data[DOMAIN] = {
|
||||||
|
'devices': set(),
|
||||||
|
'unsub_device_tracker': {},
|
||||||
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,6 +143,7 @@ async def async_setup_entry(hass, entry):
|
||||||
async def async_unload_entry(hass, entry):
|
async def async_unload_entry(hass, entry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||||
|
hass.data[DOMAIN]['unsub_device_tracker'].pop(entry.entry_id)()
|
||||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,90 @@
|
||||||
"""Support for the Locative platform."""
|
"""Support for the Locative platform."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.core import callback
|
||||||
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||||
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
|
DeviceTrackerEntity
|
||||||
|
)
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import DOMAIN as LOCATIVE_DOMAIN, TRACKER_UPDATE
|
from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_KEY = '{}.{}'.format(LOCATIVE_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
async def async_setup_entry(hass, entry, async_see):
|
|
||||||
"""Configure a dispatcher connection based on a config entry."""
|
"""Configure a dispatcher connection based on a config entry."""
|
||||||
async def _set_location(device, gps_location, location_name):
|
@callback
|
||||||
"""Fire HA event to set location."""
|
def _receive_data(device, location, location_name):
|
||||||
await async_see(
|
"""Receive set location."""
|
||||||
dev_id=slugify(device),
|
if device in hass.data[LT_DOMAIN]['devices']:
|
||||||
gps=gps_location,
|
return
|
||||||
location_name=location_name
|
|
||||||
)
|
hass.data[LT_DOMAIN]['devices'].add(device)
|
||||||
|
|
||||||
|
async_add_entities([LocativeEntity(
|
||||||
|
device, location, location_name
|
||||||
|
)])
|
||||||
|
|
||||||
|
hass.data[LT_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \
|
||||||
|
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
|
||||||
|
|
||||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
|
||||||
hass, TRACKER_UPDATE, _set_location
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass, entry):
|
class LocativeEntity(DeviceTrackerEntity):
|
||||||
"""Unload the config entry and remove the dispatcher connection."""
|
"""Represent a tracked device."""
|
||||||
hass.data[DATA_KEY]()
|
|
||||||
return True
|
def __init__(self, device, location, location_name):
|
||||||
|
"""Set up Locative entity."""
|
||||||
|
self._name = device
|
||||||
|
self._location = location
|
||||||
|
self._location_name = location_name
|
||||||
|
self._unsub_dispatcher = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self):
|
||||||
|
"""Return latitude value of the device."""
|
||||||
|
return self._location[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def longitude(self):
|
||||||
|
"""Return longitude value of the device."""
|
||||||
|
return self._location[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_name(self):
|
||||||
|
"""Return a location name for the current location of the device."""
|
||||||
|
return self._location_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_type(self):
|
||||||
|
"""Return the source type, eg gps or router, of the device."""
|
||||||
|
return SOURCE_TYPE_GPS
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register state update callback."""
|
||||||
|
self._unsub_dispatcher = async_dispatcher_connect(
|
||||||
|
self.hass, TRACKER_UPDATE, self._async_receive_data)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Clean up after entity before removal."""
|
||||||
|
self._unsub_dispatcher()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_receive_data(self, device, location, location_name):
|
||||||
|
"""Update device data."""
|
||||||
|
self._location_name = location_name
|
||||||
|
self._location = location
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -15,6 +15,7 @@ import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.setup import async_when_setup
|
from homeassistant.setup import async_when_setup
|
||||||
|
|
||||||
from .config_flow import CONF_SECRET
|
from .config_flow import CONF_SECRET
|
||||||
|
from .messages import async_handle_message
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -50,7 +51,9 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Initialize OwnTracks component."""
|
"""Initialize OwnTracks component."""
|
||||||
hass.data[DOMAIN] = {
|
hass.data[DOMAIN] = {
|
||||||
'config': config[DOMAIN]
|
'config': config[DOMAIN],
|
||||||
|
'devices': {},
|
||||||
|
'unsub': None,
|
||||||
}
|
}
|
||||||
if not hass.config_entries.async_entries(DOMAIN):
|
if not hass.config_entries.async_entries(DOMAIN):
|
||||||
hass.async_create_task(hass.config_entries.flow.async_init(
|
hass.async_create_task(hass.config_entries.flow.async_init(
|
||||||
|
@ -88,6 +91,10 @@ async def async_setup_entry(hass, entry):
|
||||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
entry, 'device_tracker'))
|
entry, 'device_tracker'))
|
||||||
|
|
||||||
|
hass.data[DOMAIN]['unsub'] = \
|
||||||
|
hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
|
DOMAIN, async_handle_message)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,6 +103,8 @@ async def async_unload_entry(hass, entry):
|
||||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||||
await hass.config_entries.async_forward_entry_unload(
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
entry, 'device_tracker')
|
entry, 'device_tracker')
|
||||||
|
hass.data[DOMAIN]['unsub']()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -213,11 +222,13 @@ class OwnTracksContext:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def async_see(self, **data):
|
@callback
|
||||||
|
def async_see(self, **data):
|
||||||
"""Send a see message to the device tracker."""
|
"""Send a see message to the device tracker."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_see_beacons(self, hass, dev_id, kwargs_param):
|
@callback
|
||||||
|
def async_see_beacons(self, hass, dev_id, kwargs_param):
|
||||||
"""Set active beacons to the current location."""
|
"""Set active beacons to the current location."""
|
||||||
kwargs = kwargs_param.copy()
|
kwargs = kwargs_param.copy()
|
||||||
|
|
||||||
|
@ -231,8 +242,13 @@ class OwnTracksContext:
|
||||||
acc = device_tracker_state.attributes.get("gps_accuracy")
|
acc = device_tracker_state.attributes.get("gps_accuracy")
|
||||||
lat = device_tracker_state.attributes.get("latitude")
|
lat = device_tracker_state.attributes.get("latitude")
|
||||||
lon = device_tracker_state.attributes.get("longitude")
|
lon = device_tracker_state.attributes.get("longitude")
|
||||||
kwargs['gps_accuracy'] = acc
|
|
||||||
kwargs['gps'] = (lat, lon)
|
if lat is not None and lon is not None:
|
||||||
|
kwargs['gps'] = (lat, lon)
|
||||||
|
kwargs['gps_accuracy'] = acc
|
||||||
|
else:
|
||||||
|
kwargs['gps'] = None
|
||||||
|
kwargs['gps_accuracy'] = None
|
||||||
|
|
||||||
# the battery state applies to the tracking device, not the beacon
|
# the battery state applies to the tracking device, not the beacon
|
||||||
# kwargs location is the beacon's configured lat/lon
|
# kwargs location is the beacon's configured lat/lon
|
||||||
|
@ -240,4 +256,4 @@ class OwnTracksContext:
|
||||||
for beacon in self.mobile_beacons_active[dev_id]:
|
for beacon in self.mobile_beacons_active[dev_id]:
|
||||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||||
kwargs['host_name'] = beacon
|
kwargs['host_name'] = beacon
|
||||||
await self.async_see(**kwargs)
|
self.async_see(**kwargs)
|
||||||
|
|
|
@ -1,351 +1,142 @@
|
||||||
"""Device tracker platform that adds support for OwnTracks over MQTT."""
|
"""Device tracker platform that adds support for OwnTracks over MQTT."""
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components import zone as zone_comp
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT
|
||||||
ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS)
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
from homeassistant.const import STATE_HOME
|
DeviceTrackerEntity
|
||||||
from homeassistant.util import decorator, slugify
|
)
|
||||||
|
|
||||||
from . import DOMAIN as OT_DOMAIN
|
from . import DOMAIN as OT_DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HANDLERS = decorator.Registry()
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
async def async_setup_entry(hass, entry, async_see):
|
|
||||||
"""Set up OwnTracks based off an entry."""
|
"""Set up OwnTracks based off an entry."""
|
||||||
hass.data[OT_DOMAIN]['context'].async_see = async_see
|
@callback
|
||||||
hass.helpers.dispatcher.async_dispatcher_connect(
|
def _receive_data(dev_id, host_name, gps, attributes, gps_accuracy=None,
|
||||||
OT_DOMAIN, async_handle_message)
|
battery=None, source_type=None, location_name=None):
|
||||||
|
"""Receive set location."""
|
||||||
|
device = hass.data[OT_DOMAIN]['devices'].get(dev_id)
|
||||||
|
|
||||||
|
if device is not None:
|
||||||
|
device.update_data(
|
||||||
|
host_name=host_name,
|
||||||
|
gps=gps,
|
||||||
|
attributes=attributes,
|
||||||
|
gps_accuracy=gps_accuracy,
|
||||||
|
battery=battery,
|
||||||
|
source_type=source_type,
|
||||||
|
location_name=location_name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
device = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
|
||||||
|
dev_id=dev_id,
|
||||||
|
host_name=host_name,
|
||||||
|
gps=gps,
|
||||||
|
attributes=attributes,
|
||||||
|
gps_accuracy=gps_accuracy,
|
||||||
|
battery=battery,
|
||||||
|
source_type=source_type,
|
||||||
|
location_name=location_name,
|
||||||
|
)
|
||||||
|
async_add_entities([device])
|
||||||
|
|
||||||
|
hass.data[OT_DOMAIN]['context'].async_see = _receive_data
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_cipher():
|
class OwnTracksEntity(DeviceTrackerEntity):
|
||||||
"""Return decryption function and length of key.
|
"""Represent a tracked device."""
|
||||||
|
|
||||||
Async friendly.
|
def __init__(self, dev_id, host_name, gps, attributes, gps_accuracy,
|
||||||
"""
|
battery, source_type, location_name):
|
||||||
from nacl.secret import SecretBox
|
"""Set up OwnTracks entity."""
|
||||||
from nacl.encoding import Base64Encoder
|
self._dev_id = dev_id
|
||||||
|
self._host_name = host_name
|
||||||
|
self._gps = gps
|
||||||
|
self._gps_accuracy = gps_accuracy
|
||||||
|
self._location_name = location_name
|
||||||
|
self._attributes = attributes
|
||||||
|
self._battery = battery
|
||||||
|
self._source_type = source_type
|
||||||
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||||
|
|
||||||
def decrypt(ciphertext, key):
|
@property
|
||||||
"""Decrypt ciphertext using key."""
|
def unique_id(self):
|
||||||
return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder)
|
"""Return the unique ID."""
|
||||||
return (SecretBox.KEY_SIZE, decrypt)
|
return self._dev_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self):
|
||||||
|
"""Return the battery level of the device."""
|
||||||
|
return self._battery
|
||||||
|
|
||||||
def _parse_topic(topic, subscribe_topic):
|
@property
|
||||||
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific attributes."""
|
||||||
|
return self._attributes
|
||||||
|
|
||||||
Async friendly.
|
@property
|
||||||
"""
|
def location_accuracy(self):
|
||||||
subscription = subscribe_topic.split('/')
|
"""Return the gps accuracy of the device."""
|
||||||
try:
|
return self._gps_accuracy
|
||||||
user_index = subscription.index('#')
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic)
|
|
||||||
raise
|
|
||||||
|
|
||||||
topic_list = topic.split('/')
|
@property
|
||||||
try:
|
def latitude(self):
|
||||||
user, device = topic_list[user_index], topic_list[user_index + 1]
|
"""Return latitude value of the device."""
|
||||||
except IndexError:
|
if self._gps is not None:
|
||||||
_LOGGER.error("Can't parse topic: '%s'", topic)
|
return self._gps[0]
|
||||||
raise
|
|
||||||
|
|
||||||
return user, device
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_see_args(message, subscribe_topic):
|
|
||||||
"""Parse the OwnTracks location parameters, into the format see expects.
|
|
||||||
|
|
||||||
Async friendly.
|
|
||||||
"""
|
|
||||||
user, device = _parse_topic(message['topic'], subscribe_topic)
|
|
||||||
dev_id = slugify('{}_{}'.format(user, device))
|
|
||||||
kwargs = {
|
|
||||||
'dev_id': dev_id,
|
|
||||||
'host_name': user,
|
|
||||||
'gps': (message['lat'], message['lon']),
|
|
||||||
'attributes': {}
|
|
||||||
}
|
|
||||||
if 'acc' in message:
|
|
||||||
kwargs['gps_accuracy'] = message['acc']
|
|
||||||
if 'batt' in message:
|
|
||||||
kwargs['battery'] = message['batt']
|
|
||||||
if 'vel' in message:
|
|
||||||
kwargs['attributes']['velocity'] = message['vel']
|
|
||||||
if 'tid' in message:
|
|
||||||
kwargs['attributes']['tid'] = message['tid']
|
|
||||||
if 'addr' in message:
|
|
||||||
kwargs['attributes']['address'] = message['addr']
|
|
||||||
if 'cog' in message:
|
|
||||||
kwargs['attributes']['course'] = message['cog']
|
|
||||||
if 't' in message:
|
|
||||||
if message['t'] == 'c':
|
|
||||||
kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS
|
|
||||||
if message['t'] == 'b':
|
|
||||||
kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_BLUETOOTH_LE
|
|
||||||
|
|
||||||
return dev_id, kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def _set_gps_from_zone(kwargs, location, zone):
|
|
||||||
"""Set the see parameters from the zone parameters.
|
|
||||||
|
|
||||||
Async friendly.
|
|
||||||
"""
|
|
||||||
if zone is not None:
|
|
||||||
kwargs['gps'] = (
|
|
||||||
zone.attributes['latitude'],
|
|
||||||
zone.attributes['longitude'])
|
|
||||||
kwargs['gps_accuracy'] = zone.attributes['radius']
|
|
||||||
kwargs['location_name'] = location
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def _decrypt_payload(secret, topic, ciphertext):
|
|
||||||
"""Decrypt encrypted payload."""
|
|
||||||
try:
|
|
||||||
keylen, decrypt = get_cipher()
|
|
||||||
except OSError:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Ignoring encrypted payload because libsodium not installed")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(secret, dict):
|
@property
|
||||||
key = secret.get(topic)
|
def longitude(self):
|
||||||
else:
|
"""Return longitude value of the device."""
|
||||||
key = secret
|
if self._gps is not None:
|
||||||
|
return self._gps[1]
|
||||||
|
|
||||||
if key is None:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Ignoring encrypted payload because no decryption key known "
|
|
||||||
"for topic %s", topic)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
key = key.encode("utf-8")
|
@property
|
||||||
key = key[:keylen]
|
def location_name(self):
|
||||||
key = key.ljust(keylen, b'\0')
|
"""Return a location name for the current location of the device."""
|
||||||
|
return self._location_name
|
||||||
|
|
||||||
try:
|
@property
|
||||||
message = decrypt(ciphertext, key)
|
def name(self):
|
||||||
message = message.decode("utf-8")
|
"""Return the name of the device."""
|
||||||
_LOGGER.debug("Decrypted payload: %s", message)
|
return self._host_name
|
||||||
return message
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Ignoring encrypted payload because unable to decrypt using "
|
|
||||||
"key for topic %s", topic)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
@HANDLERS.register('location')
|
@property
|
||||||
async def async_handle_location_message(hass, context, message):
|
def source_type(self):
|
||||||
"""Handle a location message."""
|
"""Return the source type, eg gps or router, of the device."""
|
||||||
if not context.async_valid_accuracy(message):
|
return self._source_type
|
||||||
return
|
|
||||||
|
|
||||||
if context.events_only:
|
@property
|
||||||
_LOGGER.debug("Location update ignored due to events_only setting")
|
def device_info(self):
|
||||||
return
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
'name': self._host_name,
|
||||||
|
'identifiers': {(OT_DOMAIN, self._dev_id)},
|
||||||
|
}
|
||||||
|
|
||||||
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
@callback
|
||||||
|
def update_data(self, host_name, gps, attributes, gps_accuracy,
|
||||||
|
battery, source_type, location_name):
|
||||||
|
"""Mark the device as seen."""
|
||||||
|
self._host_name = host_name
|
||||||
|
self._gps = gps
|
||||||
|
self._gps_accuracy = gps_accuracy
|
||||||
|
self._location_name = location_name
|
||||||
|
self._attributes = attributes
|
||||||
|
self._battery = battery
|
||||||
|
self._source_type = source_type
|
||||||
|
|
||||||
if context.regions_entered[dev_id]:
|
self.async_write_ha_state()
|
||||||
_LOGGER.debug(
|
|
||||||
"Location update ignored, inside region %s",
|
|
||||||
context.regions_entered[-1])
|
|
||||||
return
|
|
||||||
|
|
||||||
await context.async_see(**kwargs)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_transition_message_enter(hass, context, message, location):
|
|
||||||
"""Execute enter event."""
|
|
||||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
|
||||||
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
|
||||||
|
|
||||||
if zone is None and message.get('t') == 'b':
|
|
||||||
# Not a HA zone, and a beacon so mobile beacon.
|
|
||||||
# kwargs will contain the lat/lon of the beacon
|
|
||||||
# which is not where the beacon actually is
|
|
||||||
# and is probably set to 0/0
|
|
||||||
beacons = context.mobile_beacons_active[dev_id]
|
|
||||||
if location not in beacons:
|
|
||||||
beacons.add(location)
|
|
||||||
_LOGGER.info("Added beacon %s", location)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
else:
|
|
||||||
# Normal region
|
|
||||||
regions = context.regions_entered[dev_id]
|
|
||||||
if location not in regions:
|
|
||||||
regions.append(location)
|
|
||||||
_LOGGER.info("Enter region %s", location)
|
|
||||||
_set_gps_from_zone(kwargs, location, zone)
|
|
||||||
await context.async_see(**kwargs)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_transition_message_leave(hass, context, message, location):
|
|
||||||
"""Execute leave event."""
|
|
||||||
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
|
||||||
regions = context.regions_entered[dev_id]
|
|
||||||
|
|
||||||
if location in regions:
|
|
||||||
regions.remove(location)
|
|
||||||
|
|
||||||
beacons = context.mobile_beacons_active[dev_id]
|
|
||||||
if location in beacons:
|
|
||||||
beacons.remove(location)
|
|
||||||
_LOGGER.info("Remove beacon %s", location)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
else:
|
|
||||||
new_region = regions[-1] if regions else None
|
|
||||||
if new_region:
|
|
||||||
# Exit to previous region
|
|
||||||
zone = hass.states.get(
|
|
||||||
"zone.{}".format(slugify(new_region)))
|
|
||||||
_set_gps_from_zone(kwargs, new_region, zone)
|
|
||||||
_LOGGER.info("Exit to %s", new_region)
|
|
||||||
await context.async_see(**kwargs)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.info("Exit to GPS")
|
|
||||||
|
|
||||||
# Check for GPS accuracy
|
|
||||||
if context.async_valid_accuracy(message):
|
|
||||||
await context.async_see(**kwargs)
|
|
||||||
await context.async_see_beacons(hass, dev_id, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('transition')
|
|
||||||
async def async_handle_transition_message(hass, context, message):
|
|
||||||
"""Handle a transition message."""
|
|
||||||
if message.get('desc') is None:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Location missing from `Entering/Leaving` message - "
|
|
||||||
"please turn `Share` on in OwnTracks app")
|
|
||||||
return
|
|
||||||
# OwnTracks uses - at the start of a beacon zone
|
|
||||||
# to switch on 'hold mode' - ignore this
|
|
||||||
location = message['desc'].lstrip("-")
|
|
||||||
|
|
||||||
# Create a layer of indirection for Owntracks instances that may name
|
|
||||||
# regions differently than their HA names
|
|
||||||
if location in context.region_mapping:
|
|
||||||
location = context.region_mapping[location]
|
|
||||||
|
|
||||||
if location.lower() == 'home':
|
|
||||||
location = STATE_HOME
|
|
||||||
|
|
||||||
if message['event'] == 'enter':
|
|
||||||
await _async_transition_message_enter(
|
|
||||||
hass, context, message, location)
|
|
||||||
elif message['event'] == 'leave':
|
|
||||||
await _async_transition_message_leave(
|
|
||||||
hass, context, message, location)
|
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Misformatted mqtt msgs, _type=transition, event=%s",
|
|
||||||
message['event'])
|
|
||||||
|
|
||||||
|
|
||||||
async def async_handle_waypoint(hass, name_base, waypoint):
|
|
||||||
"""Handle a waypoint."""
|
|
||||||
name = waypoint['desc']
|
|
||||||
pretty_name = '{} - {}'.format(name_base, name)
|
|
||||||
lat = waypoint['lat']
|
|
||||||
lon = waypoint['lon']
|
|
||||||
rad = waypoint['rad']
|
|
||||||
|
|
||||||
# check zone exists
|
|
||||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
|
||||||
|
|
||||||
# Check if state already exists
|
|
||||||
if hass.states.get(entity_id) is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
|
||||||
zone_comp.ICON_IMPORT, False)
|
|
||||||
zone.entity_id = entity_id
|
|
||||||
await zone.async_update_ha_state()
|
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('waypoint')
|
|
||||||
@HANDLERS.register('waypoints')
|
|
||||||
async def async_handle_waypoints_message(hass, context, message):
|
|
||||||
"""Handle a waypoints message."""
|
|
||||||
if not context.import_waypoints:
|
|
||||||
return
|
|
||||||
|
|
||||||
if context.waypoint_whitelist is not None:
|
|
||||||
user = _parse_topic(message['topic'], context.mqtt_topic)[0]
|
|
||||||
|
|
||||||
if user not in context.waypoint_whitelist:
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'waypoints' in message:
|
|
||||||
wayps = message['waypoints']
|
|
||||||
else:
|
|
||||||
wayps = [message]
|
|
||||||
|
|
||||||
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
|
|
||||||
|
|
||||||
name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic))
|
|
||||||
|
|
||||||
for wayp in wayps:
|
|
||||||
await async_handle_waypoint(hass, name_base, wayp)
|
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('encrypted')
|
|
||||||
async def async_handle_encrypted_message(hass, context, message):
|
|
||||||
"""Handle an encrypted message."""
|
|
||||||
if 'topic' not in message and isinstance(context.secret, dict):
|
|
||||||
_LOGGER.error("You cannot set per topic secrets when using HTTP")
|
|
||||||
return
|
|
||||||
|
|
||||||
plaintext_payload = _decrypt_payload(context.secret, message.get('topic'),
|
|
||||||
message['data'])
|
|
||||||
|
|
||||||
if plaintext_payload is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
decrypted = json.loads(plaintext_payload)
|
|
||||||
if 'topic' in message and 'topic' not in decrypted:
|
|
||||||
decrypted['topic'] = message['topic']
|
|
||||||
|
|
||||||
await async_handle_message(hass, context, decrypted)
|
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('lwt')
|
|
||||||
@HANDLERS.register('configuration')
|
|
||||||
@HANDLERS.register('beacon')
|
|
||||||
@HANDLERS.register('cmd')
|
|
||||||
@HANDLERS.register('steps')
|
|
||||||
@HANDLERS.register('card')
|
|
||||||
async def async_handle_not_impl_msg(hass, context, message):
|
|
||||||
"""Handle valid but not implemented message types."""
|
|
||||||
_LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_handle_unsupported_msg(hass, context, message):
|
|
||||||
"""Handle an unsupported or invalid message type."""
|
|
||||||
_LOGGER.warning('Received unsupported message type: %s.',
|
|
||||||
message.get('_type'))
|
|
||||||
|
|
||||||
|
|
||||||
async def async_handle_message(hass, context, message):
|
|
||||||
"""Handle an OwnTracks message."""
|
|
||||||
msgtype = message.get('_type')
|
|
||||||
|
|
||||||
_LOGGER.debug("Received %s", message)
|
|
||||||
|
|
||||||
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
|
||||||
|
|
||||||
await handler(hass, context, message)
|
|
||||||
|
|
|
@ -0,0 +1,348 @@
|
||||||
|
"""OwnTracks Message handlers."""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import zone as zone_comp
|
||||||
|
from homeassistant.components.device_tracker import (
|
||||||
|
SOURCE_TYPE_GPS, SOURCE_TYPE_BLUETOOTH_LE
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_HOME
|
||||||
|
from homeassistant.util import decorator, slugify
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HANDLERS = decorator.Registry()
|
||||||
|
|
||||||
|
|
||||||
|
def get_cipher():
|
||||||
|
"""Return decryption function and length of key.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
from nacl.secret import SecretBox
|
||||||
|
from nacl.encoding import Base64Encoder
|
||||||
|
|
||||||
|
def decrypt(ciphertext, key):
|
||||||
|
"""Decrypt ciphertext using key."""
|
||||||
|
return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder)
|
||||||
|
return (SecretBox.KEY_SIZE, decrypt)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_topic(topic, subscribe_topic):
|
||||||
|
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
subscription = subscribe_topic.split('/')
|
||||||
|
try:
|
||||||
|
user_index = subscription.index('#')
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("Can't parse subscription topic: '%s'", subscribe_topic)
|
||||||
|
raise
|
||||||
|
|
||||||
|
topic_list = topic.split('/')
|
||||||
|
try:
|
||||||
|
user, device = topic_list[user_index], topic_list[user_index + 1]
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error("Can't parse topic: '%s'", topic)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return user, device
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_see_args(message, subscribe_topic):
|
||||||
|
"""Parse the OwnTracks location parameters, into the format see expects.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
user, device = _parse_topic(message['topic'], subscribe_topic)
|
||||||
|
dev_id = slugify('{}_{}'.format(user, device))
|
||||||
|
kwargs = {
|
||||||
|
'dev_id': dev_id,
|
||||||
|
'host_name': user,
|
||||||
|
'attributes': {}
|
||||||
|
}
|
||||||
|
if message['lat'] is not None and message['lon'] is not None:
|
||||||
|
kwargs['gps'] = (message['lat'], message['lon'])
|
||||||
|
else:
|
||||||
|
kwargs['gps'] = None
|
||||||
|
|
||||||
|
if 'acc' in message:
|
||||||
|
kwargs['gps_accuracy'] = message['acc']
|
||||||
|
if 'batt' in message:
|
||||||
|
kwargs['battery'] = message['batt']
|
||||||
|
if 'vel' in message:
|
||||||
|
kwargs['attributes']['velocity'] = message['vel']
|
||||||
|
if 'tid' in message:
|
||||||
|
kwargs['attributes']['tid'] = message['tid']
|
||||||
|
if 'addr' in message:
|
||||||
|
kwargs['attributes']['address'] = message['addr']
|
||||||
|
if 'cog' in message:
|
||||||
|
kwargs['attributes']['course'] = message['cog']
|
||||||
|
if 't' in message:
|
||||||
|
if message['t'] in ('c', 'u'):
|
||||||
|
kwargs['source_type'] = SOURCE_TYPE_GPS
|
||||||
|
if message['t'] == 'b':
|
||||||
|
kwargs['source_type'] = SOURCE_TYPE_BLUETOOTH_LE
|
||||||
|
|
||||||
|
return dev_id, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _set_gps_from_zone(kwargs, location, zone):
|
||||||
|
"""Set the see parameters from the zone parameters.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
if zone is not None:
|
||||||
|
kwargs['gps'] = (
|
||||||
|
zone.attributes['latitude'],
|
||||||
|
zone.attributes['longitude'])
|
||||||
|
kwargs['gps_accuracy'] = zone.attributes['radius']
|
||||||
|
kwargs['location_name'] = location
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt_payload(secret, topic, ciphertext):
|
||||||
|
"""Decrypt encrypted payload."""
|
||||||
|
try:
|
||||||
|
keylen, decrypt = get_cipher()
|
||||||
|
except OSError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring encrypted payload because libsodium not installed")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(secret, dict):
|
||||||
|
key = secret.get(topic)
|
||||||
|
else:
|
||||||
|
key = secret
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring encrypted payload because no decryption key known "
|
||||||
|
"for topic %s", topic)
|
||||||
|
return None
|
||||||
|
|
||||||
|
key = key.encode("utf-8")
|
||||||
|
key = key[:keylen]
|
||||||
|
key = key.ljust(keylen, b'\0')
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = decrypt(ciphertext, key)
|
||||||
|
message = message.decode("utf-8")
|
||||||
|
_LOGGER.debug("Decrypted payload: %s", message)
|
||||||
|
return message
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring encrypted payload because unable to decrypt using "
|
||||||
|
"key for topic %s", topic)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('location')
|
||||||
|
async def async_handle_location_message(hass, context, message):
|
||||||
|
"""Handle a location message."""
|
||||||
|
if not context.async_valid_accuracy(message):
|
||||||
|
return
|
||||||
|
|
||||||
|
if context.events_only:
|
||||||
|
_LOGGER.debug("Location update ignored due to events_only setting")
|
||||||
|
return
|
||||||
|
|
||||||
|
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
||||||
|
|
||||||
|
if context.regions_entered[dev_id]:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Location update ignored, inside region %s",
|
||||||
|
context.regions_entered[-1])
|
||||||
|
return
|
||||||
|
|
||||||
|
context.async_see(**kwargs)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_transition_message_enter(hass, context, message, location):
|
||||||
|
"""Execute enter event."""
|
||||||
|
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||||
|
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
||||||
|
|
||||||
|
if zone is None and message.get('t') == 'b':
|
||||||
|
# Not a HA zone, and a beacon so mobile beacon.
|
||||||
|
# kwargs will contain the lat/lon of the beacon
|
||||||
|
# which is not where the beacon actually is
|
||||||
|
# and is probably set to 0/0
|
||||||
|
beacons = context.mobile_beacons_active[dev_id]
|
||||||
|
if location not in beacons:
|
||||||
|
beacons.add(location)
|
||||||
|
_LOGGER.info("Added beacon %s", location)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
else:
|
||||||
|
# Normal region
|
||||||
|
regions = context.regions_entered[dev_id]
|
||||||
|
if location not in regions:
|
||||||
|
regions.append(location)
|
||||||
|
_LOGGER.info("Enter region %s", location)
|
||||||
|
_set_gps_from_zone(kwargs, location, zone)
|
||||||
|
context.async_see(**kwargs)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_transition_message_leave(hass, context, message, location):
|
||||||
|
"""Execute leave event."""
|
||||||
|
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
||||||
|
regions = context.regions_entered[dev_id]
|
||||||
|
|
||||||
|
if location in regions:
|
||||||
|
regions.remove(location)
|
||||||
|
|
||||||
|
beacons = context.mobile_beacons_active[dev_id]
|
||||||
|
if location in beacons:
|
||||||
|
beacons.remove(location)
|
||||||
|
_LOGGER.info("Remove beacon %s", location)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
else:
|
||||||
|
new_region = regions[-1] if regions else None
|
||||||
|
if new_region:
|
||||||
|
# Exit to previous region
|
||||||
|
zone = hass.states.get(
|
||||||
|
"zone.{}".format(slugify(new_region)))
|
||||||
|
_set_gps_from_zone(kwargs, new_region, zone)
|
||||||
|
_LOGGER.info("Exit to %s", new_region)
|
||||||
|
context.async_see(**kwargs)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info("Exit to GPS")
|
||||||
|
|
||||||
|
# Check for GPS accuracy
|
||||||
|
if context.async_valid_accuracy(message):
|
||||||
|
context.async_see(**kwargs)
|
||||||
|
context.async_see_beacons(hass, dev_id, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('transition')
|
||||||
|
async def async_handle_transition_message(hass, context, message):
|
||||||
|
"""Handle a transition message."""
|
||||||
|
if message.get('desc') is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Location missing from `Entering/Leaving` message - "
|
||||||
|
"please turn `Share` on in OwnTracks app")
|
||||||
|
return
|
||||||
|
# OwnTracks uses - at the start of a beacon zone
|
||||||
|
# to switch on 'hold mode' - ignore this
|
||||||
|
location = message['desc'].lstrip("-")
|
||||||
|
|
||||||
|
# Create a layer of indirection for Owntracks instances that may name
|
||||||
|
# regions differently than their HA names
|
||||||
|
if location in context.region_mapping:
|
||||||
|
location = context.region_mapping[location]
|
||||||
|
|
||||||
|
if location.lower() == 'home':
|
||||||
|
location = STATE_HOME
|
||||||
|
|
||||||
|
if message['event'] == 'enter':
|
||||||
|
await _async_transition_message_enter(
|
||||||
|
hass, context, message, location)
|
||||||
|
elif message['event'] == 'leave':
|
||||||
|
await _async_transition_message_leave(
|
||||||
|
hass, context, message, location)
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Misformatted mqtt msgs, _type=transition, event=%s",
|
||||||
|
message['event'])
|
||||||
|
|
||||||
|
|
||||||
|
async def async_handle_waypoint(hass, name_base, waypoint):
|
||||||
|
"""Handle a waypoint."""
|
||||||
|
name = waypoint['desc']
|
||||||
|
pretty_name = '{} - {}'.format(name_base, name)
|
||||||
|
lat = waypoint['lat']
|
||||||
|
lon = waypoint['lon']
|
||||||
|
rad = waypoint['rad']
|
||||||
|
|
||||||
|
# check zone exists
|
||||||
|
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||||
|
|
||||||
|
# Check if state already exists
|
||||||
|
if hass.states.get(entity_id) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||||
|
zone_comp.ICON_IMPORT, False)
|
||||||
|
zone.entity_id = entity_id
|
||||||
|
await zone.async_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('waypoint')
|
||||||
|
@HANDLERS.register('waypoints')
|
||||||
|
async def async_handle_waypoints_message(hass, context, message):
|
||||||
|
"""Handle a waypoints message."""
|
||||||
|
if not context.import_waypoints:
|
||||||
|
return
|
||||||
|
|
||||||
|
if context.waypoint_whitelist is not None:
|
||||||
|
user = _parse_topic(message['topic'], context.mqtt_topic)[0]
|
||||||
|
|
||||||
|
if user not in context.waypoint_whitelist:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'waypoints' in message:
|
||||||
|
wayps = message['waypoints']
|
||||||
|
else:
|
||||||
|
wayps = [message]
|
||||||
|
|
||||||
|
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
|
||||||
|
|
||||||
|
name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic))
|
||||||
|
|
||||||
|
for wayp in wayps:
|
||||||
|
await async_handle_waypoint(hass, name_base, wayp)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('encrypted')
|
||||||
|
async def async_handle_encrypted_message(hass, context, message):
|
||||||
|
"""Handle an encrypted message."""
|
||||||
|
if 'topic' not in message and isinstance(context.secret, dict):
|
||||||
|
_LOGGER.error("You cannot set per topic secrets when using HTTP")
|
||||||
|
return
|
||||||
|
|
||||||
|
plaintext_payload = _decrypt_payload(context.secret, message.get('topic'),
|
||||||
|
message['data'])
|
||||||
|
|
||||||
|
if plaintext_payload is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
decrypted = json.loads(plaintext_payload)
|
||||||
|
if 'topic' in message and 'topic' not in decrypted:
|
||||||
|
decrypted['topic'] = message['topic']
|
||||||
|
|
||||||
|
await async_handle_message(hass, context, decrypted)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('lwt')
|
||||||
|
@HANDLERS.register('configuration')
|
||||||
|
@HANDLERS.register('beacon')
|
||||||
|
@HANDLERS.register('cmd')
|
||||||
|
@HANDLERS.register('steps')
|
||||||
|
@HANDLERS.register('card')
|
||||||
|
async def async_handle_not_impl_msg(hass, context, message):
|
||||||
|
"""Handle valid but not implemented message types."""
|
||||||
|
_LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_handle_unsupported_msg(hass, context, message):
|
||||||
|
"""Handle an unsupported or invalid message type."""
|
||||||
|
_LOGGER.warning('Received unsupported message type: %s.',
|
||||||
|
message.get('_type'))
|
||||||
|
|
||||||
|
|
||||||
|
async def async_handle_message(hass, context, message):
|
||||||
|
"""Handle an OwnTracks message."""
|
||||||
|
msgtype = message.get('_type')
|
||||||
|
|
||||||
|
_LOGGER.debug("Received %s", message)
|
||||||
|
|
||||||
|
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
||||||
|
|
||||||
|
await handler(hass, context, message)
|
|
@ -3,15 +3,19 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.loader import bind_hass
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS)
|
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS)
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform
|
||||||
from homeassistant.helpers.entity import async_generate_entity_id
|
from homeassistant.helpers.entity import async_generate_entity_id
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
||||||
|
from homeassistant.util.location import distance
|
||||||
|
|
||||||
|
|
||||||
from .config_flow import configured_zones
|
from .config_flow import configured_zones
|
||||||
from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE
|
from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS
|
||||||
from .zone import Zone
|
from .zone import Zone
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -37,6 +41,40 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
def async_active_zone(hass, latitude, longitude, radius=0):
|
||||||
|
"""Find the active zone for given latitude, longitude.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
|
||||||
|
zones = (hass.states.get(entity_id) for entity_id
|
||||||
|
in sorted(hass.states.async_entity_ids(DOMAIN)))
|
||||||
|
|
||||||
|
min_dist = None
|
||||||
|
closest = None
|
||||||
|
|
||||||
|
for zone in zones:
|
||||||
|
if zone.attributes.get(ATTR_PASSIVE):
|
||||||
|
continue
|
||||||
|
|
||||||
|
zone_dist = distance(
|
||||||
|
latitude, longitude,
|
||||||
|
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
||||||
|
|
||||||
|
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
||||||
|
closer_zone = closest is None or zone_dist < min_dist
|
||||||
|
smaller_zone = (zone_dist == min_dist and
|
||||||
|
zone.attributes[ATTR_RADIUS] <
|
||||||
|
closest.attributes[ATTR_RADIUS])
|
||||||
|
|
||||||
|
if within_zone and (closer_zone or smaller_zone):
|
||||||
|
min_dist = zone_dist
|
||||||
|
closest = zone
|
||||||
|
|
||||||
|
return closest
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up configured zones as well as home assistant zone if necessary."""
|
"""Set up configured zones as well as home assistant zone if necessary."""
|
||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {}
|
||||||
|
|
|
@ -3,3 +3,5 @@
|
||||||
CONF_PASSIVE = 'passive'
|
CONF_PASSIVE = 'passive'
|
||||||
DOMAIN = 'zone'
|
DOMAIN = 'zone'
|
||||||
HOME_ZONE = 'home'
|
HOME_ZONE = 'home'
|
||||||
|
ATTR_PASSIVE = 'passive'
|
||||||
|
ATTR_RADIUS = 'radius'
|
||||||
|
|
|
@ -1,60 +1,13 @@
|
||||||
"""Zone entity and functionality."""
|
"""Zone entity and functionality."""
|
||||||
from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE
|
from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.loader import bind_hass
|
|
||||||
from homeassistant.util.async_ import run_callback_threadsafe
|
|
||||||
from homeassistant.util.location import distance
|
from homeassistant.util.location import distance
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import ATTR_PASSIVE, ATTR_RADIUS
|
||||||
|
|
||||||
ATTR_PASSIVE = 'passive'
|
|
||||||
ATTR_RADIUS = 'radius'
|
|
||||||
|
|
||||||
STATE = 'zoning'
|
STATE = 'zoning'
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
def active_zone(hass, latitude, longitude, radius=0):
|
|
||||||
"""Find the active zone for given latitude, longitude."""
|
|
||||||
return run_callback_threadsafe(
|
|
||||||
hass.loop, async_active_zone, hass, latitude, longitude, radius
|
|
||||||
).result()
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
|
||||||
def async_active_zone(hass, latitude, longitude, radius=0):
|
|
||||||
"""Find the active zone for given latitude, longitude.
|
|
||||||
|
|
||||||
This method must be run in the event loop.
|
|
||||||
"""
|
|
||||||
# Sort entity IDs so that we are deterministic if equal distance to 2 zones
|
|
||||||
zones = (hass.states.get(entity_id) for entity_id
|
|
||||||
in sorted(hass.states.async_entity_ids(DOMAIN)))
|
|
||||||
|
|
||||||
min_dist = None
|
|
||||||
closest = None
|
|
||||||
|
|
||||||
for zone in zones:
|
|
||||||
if zone.attributes.get(ATTR_PASSIVE):
|
|
||||||
continue
|
|
||||||
|
|
||||||
zone_dist = distance(
|
|
||||||
latitude, longitude,
|
|
||||||
zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE])
|
|
||||||
|
|
||||||
within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS]
|
|
||||||
closer_zone = closest is None or zone_dist < min_dist
|
|
||||||
smaller_zone = (zone_dist == min_dist and
|
|
||||||
zone.attributes[ATTR_RADIUS] <
|
|
||||||
closest.attributes[ATTR_RADIUS])
|
|
||||||
|
|
||||||
if within_zone and (closer_zone or smaller_zone):
|
|
||||||
min_dist = zone_dist
|
|
||||||
closest = zone
|
|
||||||
|
|
||||||
return closest
|
|
||||||
|
|
||||||
|
|
||||||
def in_zone(zone, latitude, longitude, radius=0) -> bool:
|
def in_zone(zone, latitude, longitude, radius=0) -> bool:
|
||||||
"""Test if given latitude, longitude is in given zone.
|
"""Test if given latitude, longitude is in given zone.
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id):
|
||||||
data['device']))
|
data['device']))
|
||||||
assert state.state == STATE_NOT_HOME
|
assert state.state == STATE_NOT_HOME
|
||||||
assert state.attributes['gps_accuracy'] == 10.5
|
assert state.attributes['gps_accuracy'] == 10.5
|
||||||
assert state.attributes['battery'] == 10.0
|
assert state.attributes['battery_level'] == 10.0
|
||||||
assert state.attributes['speed'] == 100.0
|
assert state.attributes['speed'] == 100.0
|
||||||
assert state.attributes['direction'] == 105.32
|
assert state.attributes['direction'] == 105.32
|
||||||
assert state.attributes['altitude'] == 102.0
|
assert state.attributes['altitude'] == 102.0
|
||||||
|
|
|
@ -861,10 +861,9 @@ async def test_event_beacon_unknown_zone_no_location(hass, context):
|
||||||
# the Device during test case setup.
|
# the Device during test case setup.
|
||||||
assert_location_state(hass, 'None')
|
assert_location_state(hass, 'None')
|
||||||
|
|
||||||
# home is the state of a Device constructed through
|
# We have had no location yet, so the beacon status
|
||||||
# the normal code path on it's first observation with
|
# set to unknown.
|
||||||
# the conditions I pass along.
|
assert_mobile_tracker_state(hass, 'unknown', 'unknown')
|
||||||
assert_mobile_tracker_state(hass, 'home', 'unknown')
|
|
||||||
|
|
||||||
|
|
||||||
async def test_event_beacon_unknown_zone(hass, context):
|
async def test_event_beacon_unknown_zone(hass, context):
|
||||||
|
@ -1276,7 +1275,7 @@ async def test_single_waypoint_import(hass, context):
|
||||||
async def test_not_implemented_message(hass, context):
|
async def test_not_implemented_message(hass, context):
|
||||||
"""Handle not implemented message type."""
|
"""Handle not implemented message type."""
|
||||||
patch_handler = patch('homeassistant.components.owntracks.'
|
patch_handler = patch('homeassistant.components.owntracks.'
|
||||||
'device_tracker.async_handle_not_impl_msg',
|
'messages.async_handle_not_impl_msg',
|
||||||
return_value=mock_coro(False))
|
return_value=mock_coro(False))
|
||||||
patch_handler.start()
|
patch_handler.start()
|
||||||
assert not await send_message(hass, LWT_TOPIC, LWT_MESSAGE)
|
assert not await send_message(hass, LWT_TOPIC, LWT_MESSAGE)
|
||||||
|
@ -1286,7 +1285,7 @@ async def test_not_implemented_message(hass, context):
|
||||||
async def test_unsupported_message(hass, context):
|
async def test_unsupported_message(hass, context):
|
||||||
"""Handle not implemented message type."""
|
"""Handle not implemented message type."""
|
||||||
patch_handler = patch('homeassistant.components.owntracks.'
|
patch_handler = patch('homeassistant.components.owntracks.'
|
||||||
'device_tracker.async_handle_unsupported_msg',
|
'messages.async_handle_unsupported_msg',
|
||||||
return_value=mock_coro(False))
|
return_value=mock_coro(False))
|
||||||
patch_handler.start()
|
patch_handler.start()
|
||||||
assert not await send_message(hass, BAD_TOPIC, BAD_MESSAGE)
|
assert not await send_message(hass, BAD_TOPIC, BAD_MESSAGE)
|
||||||
|
@ -1374,7 +1373,7 @@ def config_context(hass, setup_comp):
|
||||||
patch_save.stop()
|
patch_save.stop()
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload(hass, setup_comp):
|
async def test_encrypted_payload(hass, setup_comp):
|
||||||
"""Test encrypted payload."""
|
"""Test encrypted payload."""
|
||||||
|
@ -1385,7 +1384,7 @@ async def test_encrypted_payload(hass, setup_comp):
|
||||||
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload_topic_key(hass, setup_comp):
|
async def test_encrypted_payload_topic_key(hass, setup_comp):
|
||||||
"""Test encrypted payload with a topic key."""
|
"""Test encrypted payload with a topic key."""
|
||||||
|
@ -1398,7 +1397,7 @@ async def test_encrypted_payload_topic_key(hass, setup_comp):
|
||||||
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload_no_key(hass, setup_comp):
|
async def test_encrypted_payload_no_key(hass, setup_comp):
|
||||||
"""Test encrypted payload with no key, ."""
|
"""Test encrypted payload with no key, ."""
|
||||||
|
@ -1411,7 +1410,7 @@ async def test_encrypted_payload_no_key(hass, setup_comp):
|
||||||
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload_wrong_key(hass, setup_comp):
|
async def test_encrypted_payload_wrong_key(hass, setup_comp):
|
||||||
"""Test encrypted payload with wrong key."""
|
"""Test encrypted payload with wrong key."""
|
||||||
|
@ -1422,7 +1421,7 @@ async def test_encrypted_payload_wrong_key(hass, setup_comp):
|
||||||
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload_wrong_topic_key(hass, setup_comp):
|
async def test_encrypted_payload_wrong_topic_key(hass, setup_comp):
|
||||||
"""Test encrypted payload with wrong topic key."""
|
"""Test encrypted payload with wrong topic key."""
|
||||||
|
@ -1435,7 +1434,7 @@ async def test_encrypted_payload_wrong_topic_key(hass, setup_comp):
|
||||||
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
assert hass.states.get(DEVICE_TRACKER_STATE) is None
|
||||||
|
|
||||||
|
|
||||||
@patch('homeassistant.components.owntracks.device_tracker.get_cipher',
|
@patch('homeassistant.components.owntracks.messages.get_cipher',
|
||||||
mock_cipher)
|
mock_cipher)
|
||||||
async def test_encrypted_payload_no_topic_key(hass, setup_comp):
|
async def test_encrypted_payload_no_topic_key(hass, setup_comp):
|
||||||
"""Test encrypted payload with no topic key."""
|
"""Test encrypted payload with no topic key."""
|
||||||
|
|
|
@ -142,7 +142,7 @@ class TestComponentZone(unittest.TestCase):
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
active = zone.zone.active_zone(self.hass, 32.880600, -117.237561)
|
active = zone.async_active_zone(self.hass, 32.880600, -117.237561)
|
||||||
assert active is None
|
assert active is None
|
||||||
|
|
||||||
def test_active_zone_skips_passive_zones_2(self):
|
def test_active_zone_skips_passive_zones_2(self):
|
||||||
|
@ -158,7 +158,7 @@ class TestComponentZone(unittest.TestCase):
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
active = zone.zone.active_zone(self.hass, 32.880700, -117.237561)
|
active = zone.async_active_zone(self.hass, 32.880700, -117.237561)
|
||||||
assert 'zone.active_zone' == active.entity_id
|
assert 'zone.active_zone' == active.entity_id
|
||||||
|
|
||||||
def test_active_zone_prefers_smaller_zone_if_same_distance(self):
|
def test_active_zone_prefers_smaller_zone_if_same_distance(self):
|
||||||
|
@ -182,7 +182,7 @@ class TestComponentZone(unittest.TestCase):
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
active = zone.zone.active_zone(self.hass, latitude, longitude)
|
active = zone.async_active_zone(self.hass, latitude, longitude)
|
||||||
assert 'zone.small_zone' == active.entity_id
|
assert 'zone.small_zone' == active.entity_id
|
||||||
|
|
||||||
def test_active_zone_prefers_smaller_zone_if_same_distance_2(self):
|
def test_active_zone_prefers_smaller_zone_if_same_distance_2(self):
|
||||||
|
@ -200,7 +200,7 @@ class TestComponentZone(unittest.TestCase):
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
active = zone.zone.active_zone(self.hass, latitude, longitude)
|
active = zone.async_active_zone(self.hass, latitude, longitude)
|
||||||
assert 'zone.smallest_zone' == active.entity_id
|
assert 'zone.smallest_zone' == active.entity_id
|
||||||
|
|
||||||
def test_in_zone_works_for_passive_zones(self):
|
def test_in_zone_works_for_passive_zones(self):
|
||||||
|
|
Loading…
Reference in New Issue