2015-09-20 07:27:50 +00:00
|
|
|
"""
|
2017-09-28 07:49:35 +00:00
|
|
|
Device tracker platform that adds support for OwnTracks over MQTT.
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2015-10-13 18:55:15 +00:00
|
|
|
For more details about this platform, please refer to the documentation at
|
2015-11-09 12:12:18 +00:00
|
|
|
https://home-assistant.io/components/device_tracker.owntracks/
|
2015-09-20 07:27:50 +00:00
|
|
|
"""
|
2017-03-03 08:23:58 +00:00
|
|
|
import asyncio
|
2017-10-12 16:13:43 +00:00
|
|
|
import base64
|
2015-09-20 07:27:50 +00:00
|
|
|
import json
|
2015-09-21 03:09:53 +00:00
|
|
|
import logging
|
2016-01-29 09:39:00 +00:00
|
|
|
from collections import defaultdict
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2016-09-04 17:10:20 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2015-09-20 07:27:50 +00:00
|
|
|
import homeassistant.components.mqtt as mqtt
|
2017-10-12 16:13:43 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-08-25 16:03:07 +00:00
|
|
|
from homeassistant.components import zone as zone_comp
|
2018-01-18 22:00:20 +00:00
|
|
|
from homeassistant.components.device_tracker import (
|
|
|
|
PLATFORM_SCHEMA, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE,
|
|
|
|
SOURCE_TYPE_GPS
|
|
|
|
)
|
2017-10-12 16:13:43 +00:00
|
|
|
from homeassistant.const import STATE_HOME
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.util import slugify, decorator
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2017-10-27 06:01:32 +00:00
|
|
|
REQUIREMENTS = ['libnacl==1.6.1']
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2016-10-06 00:32:29 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2016-01-29 11:49:44 +00:00
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
HANDLERS = decorator.Registry()
|
|
|
|
|
2016-01-29 11:49:44 +00:00
|
|
|
BEACON_DEV_ID = 'beacon'
|
2016-01-29 09:39:00 +00:00
|
|
|
|
2016-10-06 00:32:29 +00:00
|
|
|
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
|
|
|
CONF_SECRET = 'secret'
|
|
|
|
CONF_WAYPOINT_IMPORT = 'waypoints'
|
|
|
|
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
2018-01-08 07:16:45 +00:00
|
|
|
CONF_MQTT_TOPIC = 'mqtt_topic'
|
|
|
|
CONF_REGION_MAPPING = 'region_mapping'
|
|
|
|
CONF_EVENTS_ONLY = 'events_only'
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2017-10-12 16:13:43 +00:00
|
|
|
DEPENDENCIES = ['mqtt']
|
|
|
|
|
2018-01-08 07:16:45 +00:00
|
|
|
DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#'
|
|
|
|
REGION_MAPPING = {}
|
2016-08-26 14:22:08 +00:00
|
|
|
|
2016-09-07 01:35:10 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
2016-09-04 17:10:20 +00:00
|
|
|
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
|
2018-01-08 07:16:45 +00:00
|
|
|
vol.Optional(CONF_EVENTS_ONLY, default=False): cv.boolean,
|
|
|
|
vol.Optional(CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC):
|
|
|
|
mqtt.valid_subscribe_topic,
|
2016-10-04 07:57:37 +00:00
|
|
|
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
|
|
|
|
cv.ensure_list, [cv.string]),
|
|
|
|
vol.Optional(CONF_SECRET): vol.Any(
|
|
|
|
vol.Schema({vol.Optional(cv.string): cv.string}),
|
2018-01-08 07:16:45 +00:00
|
|
|
cv.string),
|
|
|
|
vol.Optional(CONF_REGION_MAPPING, default=REGION_MAPPING): dict
|
2016-09-04 17:10:20 +00:00
|
|
|
})
|
|
|
|
|
2016-08-25 11:17:34 +00:00
|
|
|
|
2016-10-04 07:57:37 +00:00
|
|
|
def get_cipher():
|
2017-03-03 08:23:58 +00:00
|
|
|
"""Return decryption function and length of key.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-10-04 07:57:37 +00:00
|
|
|
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
|
|
|
from libnacl.secret import SecretBox
|
|
|
|
|
|
|
|
def decrypt(ciphertext, key):
|
|
|
|
"""Decrypt ciphertext using key."""
|
|
|
|
return SecretBox(key).decrypt(ciphertext)
|
|
|
|
return (KEYLEN, decrypt)
|
|
|
|
|
|
|
|
|
2017-03-03 08:23:58 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
2016-10-06 00:32:29 +00:00
|
|
|
"""Set up an OwnTracks tracker."""
|
2017-09-28 07:49:35 +00:00
|
|
|
context = context_from_config(async_see, config)
|
2016-10-04 07:57:37 +00:00
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle_mqtt_message(topic, payload, qos):
|
|
|
|
"""Handle incoming OwnTracks message."""
|
2015-09-20 18:46:01 +00:00
|
|
|
try:
|
2017-09-25 16:05:09 +00:00
|
|
|
message = json.loads(payload)
|
2015-09-20 18:46:01 +00:00
|
|
|
except ValueError:
|
|
|
|
# If invalid JSON
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
|
2017-10-14 22:46:06 +00:00
|
|
|
return
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
message['topic'] = topic
|
2016-09-24 07:04:03 +00:00
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
yield from async_handle_message(hass, context, message)
|
2016-01-29 11:49:44 +00:00
|
|
|
|
2017-03-03 08:23:58 +00:00
|
|
|
yield from mqtt.async_subscribe(
|
2018-01-08 07:16:45 +00:00
|
|
|
hass, context.mqtt_topic, async_handle_mqtt_message, 1)
|
2016-08-12 09:18:28 +00:00
|
|
|
|
2016-01-29 09:39:00 +00:00
|
|
|
return True
|
2016-01-03 16:12:11 +00:00
|
|
|
|
2015-09-20 07:27:50 +00:00
|
|
|
|
2018-01-08 07:16:45 +00:00
|
|
|
def _parse_topic(topic, subscribe_topic):
|
|
|
|
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
|
2017-03-03 08:23:58 +00:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2018-01-08 07:16:45 +00:00
|
|
|
subscription = subscribe_topic.split('/')
|
2017-10-14 22:46:06 +00:00
|
|
|
try:
|
2018-01-08 07:16:45 +00:00
|
|
|
user_index = subscription.index('#')
|
2017-10-14 22:46:06 +00:00
|
|
|
except ValueError:
|
2018-01-08 07:16:45 +00:00
|
|
|
_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:
|
2017-10-14 22:46:06 +00:00
|
|
|
_LOGGER.error("Can't parse topic: '%s'", topic)
|
|
|
|
raise
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
return user, device
|
2016-08-28 07:48:30 +00:00
|
|
|
|
|
|
|
|
2018-01-08 07:16:45 +00:00
|
|
|
def _parse_see_args(message, subscribe_topic):
|
2017-03-03 08:23:58 +00:00
|
|
|
"""Parse the OwnTracks location parameters, into the format see expects.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2018-01-08 07:16:45 +00:00
|
|
|
user, device = _parse_topic(message['topic'], subscribe_topic)
|
2017-09-25 16:05:09 +00:00
|
|
|
dev_id = slugify('{}_{}'.format(user, device))
|
2016-01-29 09:39:00 +00:00
|
|
|
kwargs = {
|
|
|
|
'dev_id': dev_id,
|
2017-09-25 16:05:09 +00:00
|
|
|
'host_name': user,
|
|
|
|
'gps': (message['lat'], message['lon']),
|
2017-07-16 19:43:47 +00:00
|
|
|
'attributes': {}
|
2016-01-29 09:39:00 +00:00
|
|
|
}
|
2017-09-25 16:05:09 +00:00
|
|
|
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']
|
2018-02-09 22:06:31 +00:00
|
|
|
if 'cog' in message:
|
|
|
|
kwargs['attributes']['course'] = message['cog']
|
2018-01-18 22:00:20 +00:00
|
|
|
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
|
2017-07-16 19:43:47 +00:00
|
|
|
|
2016-01-29 09:39:00 +00:00
|
|
|
return dev_id, kwargs
|
|
|
|
|
|
|
|
|
2016-08-25 16:03:07 +00:00
|
|
|
def _set_gps_from_zone(kwargs, location, zone):
|
2017-03-03 08:23:58 +00:00
|
|
|
"""Set the see parameters from the zone parameters.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-08-25 16:03:07 +00:00
|
|
|
if zone is not None:
|
2016-01-29 09:39:00 +00:00
|
|
|
kwargs['gps'] = (
|
2016-08-25 16:03:07 +00:00
|
|
|
zone.attributes['latitude'],
|
|
|
|
zone.attributes['longitude'])
|
|
|
|
kwargs['gps_accuracy'] = zone.attributes['radius']
|
2016-03-13 18:01:29 +00:00
|
|
|
kwargs['location_name'] = location
|
2016-01-29 09:39:00 +00:00
|
|
|
return kwargs
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
ciphertext = base64.b64decode(ciphertext)
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2017-09-28 07:49:35 +00:00
|
|
|
def context_from_config(async_see, config):
|
|
|
|
"""Create an async context from Home Assistant config."""
|
|
|
|
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
|
|
|
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
|
|
|
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
|
|
|
secret = config.get(CONF_SECRET)
|
2018-01-08 07:16:45 +00:00
|
|
|
region_mapping = config.get(CONF_REGION_MAPPING)
|
|
|
|
events_only = config.get(CONF_EVENTS_ONLY)
|
|
|
|
mqtt_topic = config.get(CONF_MQTT_TOPIC)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
|
|
|
return OwnTracksContext(async_see, secret, max_gps_accuracy,
|
2018-01-08 07:16:45 +00:00
|
|
|
waypoint_import, waypoint_whitelist,
|
|
|
|
region_mapping, events_only, mqtt_topic)
|
2017-09-28 07:49:35 +00:00
|
|
|
|
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
class OwnTracksContext:
|
|
|
|
"""Hold the current OwnTracks context."""
|
|
|
|
|
|
|
|
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
|
2018-01-08 07:16:45 +00:00
|
|
|
waypoint_whitelist, region_mapping, events_only, mqtt_topic):
|
2017-09-25 16:05:09 +00:00
|
|
|
"""Initialize an OwnTracks context."""
|
|
|
|
self.async_see = async_see
|
|
|
|
self.secret = secret
|
|
|
|
self.max_gps_accuracy = max_gps_accuracy
|
2017-10-31 07:18:45 +00:00
|
|
|
self.mobile_beacons_active = defaultdict(set)
|
2017-09-25 16:05:09 +00:00
|
|
|
self.regions_entered = defaultdict(list)
|
|
|
|
self.import_waypoints = import_waypoints
|
|
|
|
self.waypoint_whitelist = waypoint_whitelist
|
2018-01-08 07:16:45 +00:00
|
|
|
self.region_mapping = region_mapping
|
|
|
|
self.events_only = events_only
|
|
|
|
self.mqtt_topic = mqtt_topic
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_valid_accuracy(self, message):
|
|
|
|
"""Check if we should ignore this message."""
|
|
|
|
acc = message.get('acc')
|
|
|
|
|
|
|
|
if acc is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
|
|
acc = float(acc)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if acc == 0:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Ignoring %s update because GPS accuracy is zero: %s",
|
|
|
|
message['_type'], message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
if self.max_gps_accuracy is not None and \
|
|
|
|
acc > self.max_gps_accuracy:
|
|
|
|
_LOGGER.info("Ignoring %s update because expected GPS "
|
|
|
|
"accuracy %s is not met: %s",
|
|
|
|
message['_type'], self.max_gps_accuracy,
|
|
|
|
message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
2017-10-31 07:18:45 +00:00
|
|
|
def async_see_beacons(self, hass, dev_id, kwargs_param):
|
2017-09-25 16:05:09 +00:00
|
|
|
"""Set active beacons to the current location."""
|
|
|
|
kwargs = kwargs_param.copy()
|
2017-10-31 07:18:45 +00:00
|
|
|
|
|
|
|
# Mobile beacons should always be set to the location of the
|
|
|
|
# tracking device. I get the device state and make the necessary
|
|
|
|
# changes to kwargs.
|
|
|
|
device_tracker_state = hass.states.get(
|
|
|
|
"device_tracker.{}".format(dev_id))
|
|
|
|
|
|
|
|
if device_tracker_state is not None:
|
|
|
|
acc = device_tracker_state.attributes.get("gps_accuracy")
|
|
|
|
lat = device_tracker_state.attributes.get("latitude")
|
|
|
|
lon = device_tracker_state.attributes.get("longitude")
|
|
|
|
kwargs['gps_accuracy'] = acc
|
|
|
|
kwargs['gps'] = (lat, lon)
|
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
# the battery state applies to the tracking device, not the beacon
|
2017-10-31 07:18:45 +00:00
|
|
|
# kwargs location is the beacon's configured lat/lon
|
2017-09-25 16:05:09 +00:00
|
|
|
kwargs.pop('battery', None)
|
|
|
|
for beacon in self.mobile_beacons_active[dev_id]:
|
|
|
|
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
|
|
|
kwargs['host_name'] = beacon
|
|
|
|
yield from self.async_see(**kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register('location')
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle_location_message(hass, context, message):
|
|
|
|
"""Handle a location message."""
|
|
|
|
if not context.async_valid_accuracy(message):
|
|
|
|
return
|
|
|
|
|
2018-01-08 07:16:45 +00:00
|
|
|
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)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
if context.regions_entered[dev_id]:
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Location update ignored, inside region %s",
|
|
|
|
context.regions_entered[-1])
|
|
|
|
return
|
|
|
|
|
|
|
|
yield from context.async_see(**kwargs)
|
2017-10-31 07:18:45 +00:00
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def _async_transition_message_enter(hass, context, message, location):
|
|
|
|
"""Execute enter event."""
|
|
|
|
zone = hass.states.get("zone.{}".format(slugify(location)))
|
2018-01-08 07:16:45 +00:00
|
|
|
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
if zone is None and message.get('t') == 'b':
|
2017-10-31 07:18:45 +00:00
|
|
|
# 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
|
2017-09-25 16:05:09 +00:00
|
|
|
beacons = context.mobile_beacons_active[dev_id]
|
|
|
|
if location not in beacons:
|
2017-10-31 07:18:45 +00:00
|
|
|
beacons.add(location)
|
2017-09-25 16:05:09 +00:00
|
|
|
_LOGGER.info("Added beacon %s", location)
|
2017-10-31 07:18:45 +00:00
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
2017-09-25 16:05:09 +00:00
|
|
|
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)
|
2017-10-31 07:18:45 +00:00
|
|
|
yield from context.async_see(**kwargs)
|
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def _async_transition_message_leave(hass, context, message, location):
|
|
|
|
"""Execute leave event."""
|
2018-01-08 07:16:45 +00:00
|
|
|
dev_id, kwargs = _parse_see_args(message, context.mqtt_topic)
|
2017-09-25 16:05:09 +00:00
|
|
|
regions = context.regions_entered[dev_id]
|
|
|
|
|
|
|
|
if location in regions:
|
|
|
|
regions.remove(location)
|
|
|
|
|
2017-10-31 07:18:45 +00:00
|
|
|
beacons = context.mobile_beacons_active[dev_id]
|
|
|
|
if location in beacons:
|
|
|
|
beacons.remove(location)
|
|
|
|
_LOGGER.info("Remove beacon %s", location)
|
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
2017-09-25 16:05:09 +00:00
|
|
|
else:
|
2017-10-31 07:18:45 +00:00
|
|
|
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)
|
|
|
|
yield from context.async_see(**kwargs)
|
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
|
|
|
return
|
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
_LOGGER.info("Exit to GPS")
|
|
|
|
|
|
|
|
# Check for GPS accuracy
|
|
|
|
if context.async_valid_accuracy(message):
|
|
|
|
yield from context.async_see(**kwargs)
|
2017-10-31 07:18:45 +00:00
|
|
|
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register('transition')
|
|
|
|
@asyncio.coroutine
|
|
|
|
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("-")
|
2018-01-08 07:16:45 +00:00
|
|
|
|
|
|
|
# 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]
|
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
if location.lower() == 'home':
|
|
|
|
location = STATE_HOME
|
|
|
|
|
|
|
|
if message['event'] == 'enter':
|
|
|
|
yield from _async_transition_message_enter(
|
|
|
|
hass, context, message, location)
|
|
|
|
elif message['event'] == 'leave':
|
|
|
|
yield from _async_transition_message_leave(
|
|
|
|
hass, context, message, location)
|
|
|
|
else:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Misformatted mqtt msgs, _type=transition, event=%s",
|
|
|
|
message['event'])
|
|
|
|
|
|
|
|
|
2017-11-10 17:29:21 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
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
|
|
|
|
yield from zone.async_update_ha_state()
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register('waypoint')
|
2017-09-25 16:05:09 +00:00
|
|
|
@HANDLERS.register('waypoints')
|
|
|
|
@asyncio.coroutine
|
|
|
|
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:
|
2018-01-08 07:16:45 +00:00
|
|
|
user = _parse_topic(message['topic'], context.mqtt_topic)[0]
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
if user not in context.waypoint_whitelist:
|
|
|
|
return
|
|
|
|
|
2017-11-10 17:29:21 +00:00
|
|
|
if 'waypoints' in message:
|
|
|
|
wayps = message['waypoints']
|
|
|
|
else:
|
|
|
|
wayps = [message]
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
|
|
|
|
|
2018-01-08 07:16:45 +00:00
|
|
|
name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic))
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
for wayp in wayps:
|
2017-11-10 17:29:21 +00:00
|
|
|
yield from async_handle_waypoint(hass, name_base, wayp)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register('encrypted')
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle_encrypted_message(hass, context, message):
|
|
|
|
"""Handle an encrypted message."""
|
|
|
|
plaintext_payload = _decrypt_payload(context.secret, message['topic'],
|
|
|
|
message['data'])
|
|
|
|
|
|
|
|
if plaintext_payload is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
decrypted = json.loads(plaintext_payload)
|
|
|
|
decrypted['topic'] = message['topic']
|
|
|
|
|
|
|
|
yield from async_handle_message(hass, context, decrypted)
|
|
|
|
|
|
|
|
|
2017-10-12 15:25:18 +00:00
|
|
|
@HANDLERS.register('lwt')
|
2017-11-10 17:29:21 +00:00
|
|
|
@HANDLERS.register('configuration')
|
|
|
|
@HANDLERS.register('beacon')
|
|
|
|
@HANDLERS.register('cmd')
|
|
|
|
@HANDLERS.register('steps')
|
|
|
|
@HANDLERS.register('card')
|
|
|
|
@asyncio.coroutine
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2017-10-12 15:25:18 +00:00
|
|
|
@asyncio.coroutine
|
2017-11-10 17:29:21 +00:00
|
|
|
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'))
|
2017-10-12 15:25:18 +00:00
|
|
|
|
|
|
|
|
2017-09-25 16:05:09 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_handle_message(hass, context, message):
|
|
|
|
"""Handle an OwnTracks message."""
|
|
|
|
msgtype = message.get('_type')
|
|
|
|
|
2017-11-10 17:29:21 +00:00
|
|
|
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
2017-09-25 16:05:09 +00:00
|
|
|
|
|
|
|
yield from handler(hass, context, message)
|