mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
parent
aa17481c94
commit
3e70154695
@ -4,14 +4,15 @@ Support the OwnTracks platform.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.owntracks/
|
https://home-assistant.io/components/device_tracker.owntracks/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
import base64
|
import base64
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.const import STATE_HOME
|
from homeassistant.const import STATE_HOME
|
||||||
@ -19,6 +20,7 @@ from homeassistant.util import convert, slugify
|
|||||||
from homeassistant.components import zone as zone_comp
|
from homeassistant.components import zone as zone_comp
|
||||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mqtt']
|
||||||
REQUIREMENTS = ['libnacl==1.5.0']
|
REQUIREMENTS = ['libnacl==1.5.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -30,16 +32,9 @@ CONF_SECRET = 'secret'
|
|||||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
|
||||||
|
|
||||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||||
|
|
||||||
LOCATION_TOPIC = 'owntracks/+/+'
|
LOCATION_TOPIC = 'owntracks/+/+'
|
||||||
LOCK = threading.Lock()
|
|
||||||
|
|
||||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
|
||||||
|
|
||||||
REGIONS_ENTERED = defaultdict(list)
|
|
||||||
|
|
||||||
VALIDATE_LOCATION = 'location'
|
VALIDATE_LOCATION = 'location'
|
||||||
VALIDATE_TRANSITION = 'transition'
|
VALIDATE_TRANSITION = 'transition'
|
||||||
@ -60,8 +55,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def get_cipher():
|
def get_cipher():
|
||||||
"""Return decryption function and length of key."""
|
"""Return decryption function and length of key.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
||||||
from libnacl.secret import SecretBox
|
from libnacl.secret import SecretBox
|
||||||
|
|
||||||
@ -71,13 +70,18 @@ def get_cipher():
|
|||||||
return (KEYLEN, decrypt)
|
return (KEYLEN, decrypt)
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see, discovery_info=None):
|
@asyncio.coroutine
|
||||||
|
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||||
"""Set up an OwnTracks tracker."""
|
"""Set up an OwnTracks tracker."""
|
||||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||||
secret = config.get(CONF_SECRET)
|
secret = config.get(CONF_SECRET)
|
||||||
|
|
||||||
|
mobile_beacons_active = defaultdict(list)
|
||||||
|
regions_entered = defaultdict(list)
|
||||||
|
|
||||||
|
@callback
|
||||||
def decrypt_payload(topic, ciphertext):
|
def decrypt_payload(topic, ciphertext):
|
||||||
"""Decrypt encrypted payload."""
|
"""Decrypt encrypted payload."""
|
||||||
try:
|
try:
|
||||||
@ -115,6 +119,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
|
@callback
|
||||||
def validate_payload(topic, payload, data_type):
|
def validate_payload(topic, payload, data_type):
|
||||||
"""Validate the OwnTracks payload."""
|
"""Validate the OwnTracks payload."""
|
||||||
try:
|
try:
|
||||||
@ -154,7 +159,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def owntracks_location_update(topic, payload, qos):
|
@callback
|
||||||
|
def async_owntracks_location_update(topic, payload, qos):
|
||||||
"""MQTT message received."""
|
"""MQTT message received."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||||
@ -164,18 +170,17 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
|
|
||||||
dev_id, kwargs = _parse_see_args(topic, data)
|
dev_id, kwargs = _parse_see_args(topic, data)
|
||||||
|
|
||||||
# Block updates if we're in a region
|
if regions_entered[dev_id]:
|
||||||
with LOCK:
|
|
||||||
if REGIONS_ENTERED[dev_id]:
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"location update ignored - inside region %s",
|
"location update ignored - inside region %s",
|
||||||
REGIONS_ENTERED[-1])
|
regions_entered[-1])
|
||||||
return
|
return
|
||||||
|
|
||||||
see(**kwargs)
|
hass.async_add_job(async_see(**kwargs))
|
||||||
see_beacons(dev_id, kwargs)
|
async_see_beacons(dev_id, kwargs)
|
||||||
|
|
||||||
def owntracks_event_update(topic, payload, qos):
|
@callback
|
||||||
|
def async_owntracks_event_update(topic, payload, qos):
|
||||||
"""MQTT event (geofences) received."""
|
"""MQTT event (geofences) received."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||||
@ -196,31 +201,31 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
|
|
||||||
dev_id, kwargs = _parse_see_args(topic, data)
|
dev_id, kwargs = _parse_see_args(topic, data)
|
||||||
|
|
||||||
|
@callback
|
||||||
def enter_event():
|
def enter_event():
|
||||||
"""Execute enter event."""
|
"""Execute enter event."""
|
||||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||||
with LOCK:
|
|
||||||
if zone is None and data.get('t') == 'b':
|
if zone is None and data.get('t') == 'b':
|
||||||
# Not a HA zone, and a beacon so assume mobile
|
# Not a HA zone, and a beacon so assume mobile
|
||||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
beacons = mobile_beacons_active[dev_id]
|
||||||
if location not in beacons:
|
if location not in beacons:
|
||||||
beacons.append(location)
|
beacons.append(location)
|
||||||
_LOGGER.info("Added beacon %s", location)
|
_LOGGER.info("Added beacon %s", location)
|
||||||
else:
|
else:
|
||||||
# Normal region
|
# Normal region
|
||||||
regions = REGIONS_ENTERED[dev_id]
|
regions = regions_entered[dev_id]
|
||||||
if location not in regions:
|
if location not in regions:
|
||||||
regions.append(location)
|
regions.append(location)
|
||||||
_LOGGER.info("Enter region %s", location)
|
_LOGGER.info("Enter region %s", location)
|
||||||
_set_gps_from_zone(kwargs, location, zone)
|
_set_gps_from_zone(kwargs, location, zone)
|
||||||
|
|
||||||
see(**kwargs)
|
hass.async_add_job(async_see(**kwargs))
|
||||||
see_beacons(dev_id, kwargs)
|
async_see_beacons(dev_id, kwargs)
|
||||||
|
|
||||||
|
@callback
|
||||||
def leave_event():
|
def leave_event():
|
||||||
"""Execute leave event."""
|
"""Execute leave event."""
|
||||||
with LOCK:
|
regions = regions_entered[dev_id]
|
||||||
regions = REGIONS_ENTERED[dev_id]
|
|
||||||
if location in regions:
|
if location in regions:
|
||||||
regions.remove(location)
|
regions.remove(location)
|
||||||
new_region = regions[-1] if regions else None
|
new_region = regions[-1] if regions else None
|
||||||
@ -231,8 +236,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
"zone.{}".format(slugify(new_region)))
|
"zone.{}".format(slugify(new_region)))
|
||||||
_set_gps_from_zone(kwargs, new_region, zone)
|
_set_gps_from_zone(kwargs, new_region, zone)
|
||||||
_LOGGER.info("Exit to %s", new_region)
|
_LOGGER.info("Exit to %s", new_region)
|
||||||
see(**kwargs)
|
hass.async_add_job(async_see(**kwargs))
|
||||||
see_beacons(dev_id, kwargs)
|
async_see_beacons(dev_id, kwargs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Exit to GPS")
|
_LOGGER.info("Exit to GPS")
|
||||||
@ -253,10 +258,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
'GPS accuracy %s is not met: %s',
|
'GPS accuracy %s is not met: %s',
|
||||||
max_gps_accuracy, payload)
|
max_gps_accuracy, payload)
|
||||||
if valid_gps:
|
if valid_gps:
|
||||||
see(**kwargs)
|
hass.async_add_job(async_see(**kwargs))
|
||||||
see_beacons(dev_id, kwargs)
|
async_see_beacons(dev_id, kwargs)
|
||||||
|
|
||||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
beacons = mobile_beacons_active[dev_id]
|
||||||
if location in beacons:
|
if location in beacons:
|
||||||
beacons.remove(location)
|
beacons.remove(location)
|
||||||
_LOGGER.info("Remove beacon %s", location)
|
_LOGGER.info("Remove beacon %s", location)
|
||||||
@ -271,7 +276,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
data['event'])
|
data['event'])
|
||||||
return
|
return
|
||||||
|
|
||||||
def owntracks_waypoint_update(topic, payload, qos):
|
@callback
|
||||||
|
def async_owntracks_waypoint_update(topic, payload, qos):
|
||||||
"""List of waypoints published by a user."""
|
"""List of waypoints published by a user."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||||
@ -298,36 +304,44 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||||
zone_comp.ICON_IMPORT, False)
|
zone_comp.ICON_IMPORT, False)
|
||||||
zone.entity_id = entity_id
|
zone.entity_id = entity_id
|
||||||
zone.update_ha_state()
|
hass.async_add_job(zone.async_update_ha_state())
|
||||||
|
|
||||||
def see_beacons(dev_id, kwargs_param):
|
@callback
|
||||||
|
def async_see_beacons(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()
|
||||||
# the battery state applies to the tracking device, not the beacon
|
# the battery state applies to the tracking device, not the beacon
|
||||||
kwargs.pop('battery', None)
|
kwargs.pop('battery', None)
|
||||||
for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
|
for beacon in 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
|
||||||
see(**kwargs)
|
hass.async_add_job(async_see(**kwargs))
|
||||||
|
|
||||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
yield from mqtt.async_subscribe(
|
||||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
hass, LOCATION_TOPIC, async_owntracks_location_update, 1)
|
||||||
|
yield from mqtt.async_subscribe(
|
||||||
|
hass, EVENT_TOPIC, async_owntracks_event_update, 1)
|
||||||
|
|
||||||
if waypoint_import:
|
if waypoint_import:
|
||||||
if waypoint_whitelist is None:
|
if waypoint_whitelist is None:
|
||||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'),
|
yield from mqtt.async_subscribe(
|
||||||
owntracks_waypoint_update, 1)
|
hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||||
|
async_owntracks_waypoint_update, 1)
|
||||||
else:
|
else:
|
||||||
for whitelist_user in waypoint_whitelist:
|
for whitelist_user in waypoint_whitelist:
|
||||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user,
|
yield from mqtt.async_subscribe(
|
||||||
'+'),
|
hass, WAYPOINT_TOPIC.format(whitelist_user, '+'),
|
||||||
owntracks_waypoint_update, 1)
|
async_owntracks_waypoint_update, 1)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def parse_topic(topic, pretty=False):
|
def parse_topic(topic, pretty=False):
|
||||||
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple."""
|
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
parts = topic.split('/')
|
parts = topic.split('/')
|
||||||
dev_id_format = ''
|
dev_id_format = ''
|
||||||
if pretty:
|
if pretty:
|
||||||
@ -339,8 +353,12 @@ def parse_topic(topic, pretty=False):
|
|||||||
return (host_name, dev_id)
|
return (host_name, dev_id)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def _parse_see_args(topic, data):
|
def _parse_see_args(topic, data):
|
||||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
"""Parse the OwnTracks location parameters, into the format see expects.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
(host_name, dev_id) = parse_topic(topic, False)
|
(host_name, dev_id) = parse_topic(topic, False)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'dev_id': dev_id,
|
'dev_id': dev_id,
|
||||||
@ -354,8 +372,12 @@ def _parse_see_args(topic, data):
|
|||||||
return dev_id, kwargs
|
return dev_id, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def _set_gps_from_zone(kwargs, location, zone):
|
def _set_gps_from_zone(kwargs, location, zone):
|
||||||
"""Set the see parameters from the zone parameters."""
|
"""Set the see parameters from the zone parameters.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
if zone is not None:
|
if zone is not None:
|
||||||
kwargs['gps'] = (
|
kwargs['gps'] = (
|
||||||
zone.attributes['latitude'],
|
zone.attributes['latitude'],
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""The tests for the Owntracks device tracker."""
|
"""The tests for the Owntracks device tracker."""
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
@ -12,6 +13,7 @@ import homeassistant.components.device_tracker.owntracks as owntracks
|
|||||||
from homeassistant.bootstrap import setup_component
|
from homeassistant.bootstrap import setup_component
|
||||||
from homeassistant.components import device_tracker
|
from homeassistant.components import device_tracker
|
||||||
from homeassistant.const import CONF_PLATFORM, STATE_NOT_HOME
|
from homeassistant.const import CONF_PLATFORM, STATE_NOT_HOME
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
USER = 'greg'
|
USER = 'greg'
|
||||||
DEVICE = 'phone'
|
DEVICE = 'phone'
|
||||||
@ -640,6 +642,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
|
|||||||
|
|
||||||
def test_waypoint_import_no_whitelist(self):
|
def test_waypoint_import_no_whitelist(self):
|
||||||
"""Test import of list of waypoints with no whitelist set."""
|
"""Test import of list of waypoints with no whitelist set."""
|
||||||
|
@asyncio.coroutine
|
||||||
def mock_see(**kwargs):
|
def mock_see(**kwargs):
|
||||||
"""Fake see method for owntracks."""
|
"""Fake see method for owntracks."""
|
||||||
return
|
return
|
||||||
@ -649,7 +652,8 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
|
|||||||
CONF_MAX_GPS_ACCURACY: 200,
|
CONF_MAX_GPS_ACCURACY: 200,
|
||||||
CONF_WAYPOINT_IMPORT: True
|
CONF_WAYPOINT_IMPORT: True
|
||||||
}
|
}
|
||||||
owntracks.setup_scanner(self.hass, test_config, mock_see)
|
run_coroutine_threadsafe(owntracks.async_setup_scanner(
|
||||||
|
self.hass, test_config, mock_see), self.hass.loop).result()
|
||||||
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
|
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
|
||||||
self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message)
|
self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message)
|
||||||
# Check if it made it into states
|
# Check if it made it into states
|
||||||
|
Loading…
x
Reference in New Issue
Block a user