1 """OwnTracks Message handlers."""
6 from nacl.encoding
import Base64Encoder
7 from nacl.secret
import SecretBox
14 from .helper
import supports_encryption
16 _LOGGER = logging.getLogger(__name__)
18 HANDLERS = decorator.Registry()
22 """Return decryption function and length of key.
27 def decrypt(ciphertext, key):
28 """Decrypt ciphertext using key."""
29 return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder)
31 return (SecretBox.KEY_SIZE, decrypt)
35 """Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
39 subscription = subscribe_topic.split(
"/")
41 user_index = subscription.index(
"#")
43 _LOGGER.error(
"Can't parse subscription topic: '%s'", subscribe_topic)
46 topic_list = topic.split(
"/")
48 user, device = topic_list[user_index], topic_list[user_index + 1]
50 _LOGGER.error(
"Can't parse topic: '%s'", topic)
57 """Parse the OwnTracks location parameters, into the format see expects.
61 user, device =
_parse_topic(message[
"topic"], subscribe_topic)
62 dev_id =
slugify(f
"{user}_{device}")
63 kwargs = {
"dev_id": dev_id,
"host_name": user,
"attributes": {}}
64 if message[
"lat"]
is not None and message[
"lon"]
is not None:
65 kwargs[
"gps"] = (message[
"lat"], message[
"lon"])
70 kwargs[
"gps_accuracy"] = message[
"acc"]
72 kwargs[
"battery"] = message[
"batt"]
74 kwargs[
"attributes"][
"velocity"] = message[
"vel"]
76 kwargs[
"attributes"][
"tid"] = message[
"tid"]
78 kwargs[
"attributes"][
"address"] = message[
"addr"]
80 kwargs[
"attributes"][
"course"] = message[
"cog"]
82 kwargs[
"attributes"][
"battery_status"] = message[
"bs"]
84 if message[
"t"]
in (
"c",
"u"):
85 kwargs[
"source_type"] = SourceType.GPS
86 if message[
"t"] ==
"b":
87 kwargs[
"source_type"] = SourceType.BLUETOOTH_LE
93 """Set the see parameters from the zone parameters.
99 zone.attributes[ATTR_LATITUDE],
100 zone.attributes[ATTR_LONGITUDE],
102 kwargs[
"gps_accuracy"] = zone.attributes[
"radius"]
103 kwargs[
"location_name"] = location
108 """Decrypt encrypted payload."""
113 _LOGGER.warning(
"Ignoring encrypted payload because nacl not installed")
116 _LOGGER.warning(
"Ignoring encrypted payload because nacl not installed")
119 if isinstance(secret, dict):
120 key = secret.get(topic)
126 "Ignoring encrypted payload because no decryption key known for topic %s",
131 key = key.encode(
"utf-8")
133 key = key.ljust(keylen, b
"\0")
136 message = decrypt(ciphertext, key)
137 message = message.decode(
"utf-8")
141 "Ignoring encrypted payload because unable to decrypt using key for"
147 _LOGGER.debug(
"Decrypted payload: %s", message)
152 """Encrypt message."""
154 keylen = SecretBox.KEY_SIZE
156 if isinstance(secret, dict):
157 key = secret.get(topic)
163 "Unable to encrypt payload because no decryption key known for topic %s",
168 key = key.encode(
"utf-8")
170 key = key.ljust(keylen, b
"\0")
173 message = message.encode(
"utf-8")
174 payload = SecretBox(key).encrypt(message, encoder=Base64Encoder)
175 _LOGGER.debug(
"Encrypted message: %s to %s", message, payload)
176 return payload.decode(
"utf-8")
178 _LOGGER.warning(
"Unable to encrypt message for topic %s", topic)
182 @HANDLERS.register("location")
184 """Handle a location message."""
185 if not context.async_valid_accuracy(message):
188 if context.events_only:
189 _LOGGER.debug(
"Location update ignored due to events_only setting")
194 if context.regions_entered[dev_id]:
196 "Location update ignored, inside region %s", context.regions_entered[-1]
200 context.async_see(**kwargs)
201 context.async_see_beacons(hass, dev_id, kwargs)
205 """Execute enter event."""
206 zone = hass.states.get(f
"zone.{slugify(location)}")
209 if zone
is None and message.get(
"t") ==
"b":
214 beacons = context.mobile_beacons_active[dev_id]
215 if location
not in beacons:
216 beacons.add(location)
217 _LOGGER.debug(
"Added beacon %s", location)
218 context.async_see_beacons(hass, dev_id, kwargs)
221 regions = context.regions_entered[dev_id]
222 if location
not in regions:
223 regions.append(location)
224 _LOGGER.debug(
"Enter region %s", location)
226 context.async_see(**kwargs)
227 context.async_see_beacons(hass, dev_id, kwargs)
231 """Execute leave event."""
233 regions = context.regions_entered[dev_id]
235 if location
in regions:
236 regions.remove(location)
238 beacons = context.mobile_beacons_active[dev_id]
239 if location
in beacons:
240 beacons.remove(location)
241 _LOGGER.debug(
"Remove beacon %s", location)
242 context.async_see_beacons(hass, dev_id, kwargs)
244 new_region = regions[-1]
if regions
else None
247 zone = hass.states.get(f
"zone.{slugify(new_region)}")
249 _LOGGER.debug(
"Exit to %s", new_region)
250 context.async_see(**kwargs)
251 context.async_see_beacons(hass, dev_id, kwargs)
254 _LOGGER.debug(
"Exit to GPS")
257 if context.async_valid_accuracy(message):
258 context.async_see(**kwargs)
259 context.async_see_beacons(hass, dev_id, kwargs)
262 @HANDLERS.register("transition")
264 """Handle a transition message."""
265 if message.get(
"desc")
is None:
267 "Location missing from `Entering/Leaving` message - "
268 "please turn `Share` on in OwnTracks app"
273 location = message[
"desc"].lstrip(
"-")
277 if location
in context.region_mapping:
278 location = context.region_mapping[location]
280 if location.lower() ==
"home":
281 location = STATE_HOME
283 if message[
"event"] ==
"enter":
285 elif message[
"event"] ==
"leave":
289 "Misformatted mqtt msgs, _type=transition, event=%s", message[
"event"]
294 """Handle a waypoint."""
295 name = waypoint[
"desc"]
296 pretty_name = f
"{name_base} - {name}"
297 lat = waypoint[
"lat"]
298 lon = waypoint[
"lon"]
299 rad = waypoint[
"rad"]
302 entity_id = zone_comp.ENTITY_ID_FORMAT.format(
slugify(pretty_name))
305 if hass.states.get(entity_id)
is not None:
308 zone = zone_comp.Zone.from_yaml(
310 zone_comp.CONF_NAME: pretty_name,
311 zone_comp.CONF_LATITUDE: lat,
312 zone_comp.CONF_LONGITUDE: lon,
313 zone_comp.CONF_RADIUS: rad,
314 zone_comp.CONF_ICON: zone_comp.ICON_IMPORT,
315 zone_comp.CONF_PASSIVE:
False,
319 zone.entity_id = entity_id
320 zone.async_write_ha_state()
323 @HANDLERS.register("waypoint")
324 @HANDLERS.register("waypoints")
326 """Handle a waypoints message."""
327 if not context.import_waypoints:
330 if context.waypoint_whitelist
is not None:
331 user =
_parse_topic(message[
"topic"], context.mqtt_topic)[0]
333 if user
not in context.waypoint_whitelist:
336 wayps = message.get(
"waypoints", [message])
338 _LOGGER.debug(
"Got %d waypoints from %s", len(wayps), message[
"topic"])
340 name_base =
" ".join(
_parse_topic(message[
"topic"], context.mqtt_topic))
346 @HANDLERS.register("encrypted")
348 """Handle an encrypted message."""
349 if "topic" not in message
and isinstance(context.secret, dict):
350 _LOGGER.error(
"You cannot set per topic secrets when using HTTP")
354 context.secret, message.get(
"topic"), message[
"data"]
357 if plaintext_payload
is None:
360 decrypted = json.loads(plaintext_payload)
361 if "topic" in message
and "topic" not in decrypted:
362 decrypted[
"topic"] = message[
"topic"]
367 @HANDLERS.register("lwt")
368 @HANDLERS.register("configuration")
369 @HANDLERS.register("beacon")
370 @HANDLERS.register("cmd")
371 @HANDLERS.register("steps")
372 @HANDLERS.register("card")
374 """Handle valid but not implemented message types."""
375 _LOGGER.debug(
"Not handling %s message: %s", message.get(
"_type"), message)
379 """Handle an unsupported or invalid message type."""
380 _LOGGER.warning(
"Received unsupported message type: %s", message.get(
"_type"))
384 """Handle an OwnTracks message."""
385 msgtype = message.get(
"_type")
387 _LOGGER.debug(
"Received %s", message)
389 handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
391 await handler(hass, context, message)
bool supports_encryption()
def async_handle_unsupported_msg(hass, context, message)
def _parse_see_args(message, subscribe_topic)
def _parse_topic(topic, subscribe_topic)
def async_handle_waypoint(hass, name_base, waypoint)
def _set_gps_from_zone(kwargs, location, zone)
def async_handle_transition_message(hass, context, message)
def async_handle_message(hass, context, message)
def _async_transition_message_leave(hass, context, message, location)
def async_handle_encrypted_message(hass, context, message)
def async_handle_waypoints_message(hass, context, message)
def _decrypt_payload(secret, topic, ciphertext)
def _async_transition_message_enter(hass, context, message, location)
def async_handle_location_message(hass, context, message)
def async_handle_not_impl_msg(hass, context, message)
def encrypt_message(secret, topic, message)