1 """Support for OwnTracks."""
3 from collections
import defaultdict
8 from aiohttp
import web
9 import voluptuous
as vol
23 async_dispatcher_connect,
24 async_dispatcher_send,
30 from .config_flow
import CONF_SECRET
31 from .const
import DOMAIN
32 from .messages
import async_handle_message, encrypt_message
34 _LOGGER = logging.getLogger(__name__)
36 CONF_MAX_GPS_ACCURACY =
"max_gps_accuracy"
37 CONF_WAYPOINT_IMPORT =
"waypoints"
38 CONF_WAYPOINT_WHITELIST =
"waypoint_whitelist"
39 CONF_MQTT_TOPIC =
"mqtt_topic"
40 CONF_REGION_MAPPING =
"region_mapping"
41 CONF_EVENTS_ONLY =
"events_only"
42 BEACON_DEV_ID =
"beacon"
43 PLATFORMS = [Platform.DEVICE_TRACKER]
45 DEFAULT_OWNTRACKS_TOPIC =
"owntracks/#"
47 CONFIG_SCHEMA = vol.All(
48 cv.removed(CONF_WEBHOOK_ID),
51 vol.Optional(DOMAIN, default={}): {
52 vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
53 vol.Optional(CONF_WAYPOINT_IMPORT, default=
True): cv.boolean,
54 vol.Optional(CONF_EVENTS_ONLY, default=
False): cv.boolean,
56 CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC
57 ): mqtt.valid_subscribe_topic,
58 vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
59 cv.ensure_list, [cv.string]
61 vol.Optional(CONF_SECRET): vol.Any(
62 vol.Schema({vol.Optional(cv.string): cv.string}), cv.string
64 vol.Optional(CONF_REGION_MAPPING, default={}): dict,
67 extra=vol.ALLOW_EXTRA,
72 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
73 """Initialize OwnTracks component."""
74 hass.data[DOMAIN] = {
"config": config[DOMAIN],
"devices": {},
"unsub":
None}
79 """Set up OwnTracks entry."""
80 config = hass.data[DOMAIN][
"config"]
81 max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
82 waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
83 waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
84 secret = config.get(CONF_SECRET)
or entry.data[CONF_SECRET]
85 region_mapping = config.get(CONF_REGION_MAPPING)
86 events_only = config.get(CONF_EVENTS_ONLY)
87 mqtt_topic = config.get(CONF_MQTT_TOPIC)
100 webhook_id = config.get(CONF_WEBHOOK_ID)
or entry.data[CONF_WEBHOOK_ID]
102 hass.data[DOMAIN][
"context"] = context
106 webhook.async_register(hass, DOMAIN,
"OwnTracks", webhook_id, handle_webhook)
108 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
111 hass, DOMAIN, async_handle_message
118 """Unload an OwnTracks config entry."""
119 webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
120 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
121 hass.data[DOMAIN][
"unsub"]()
127 """Remove an OwnTracks config entry."""
128 if not entry.data.get(
"cloudhook"):
131 await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
135 """Subscribe to MQTT topic."""
136 context = hass.data[DOMAIN][
"context"]
139 def async_handle_mqtt_message(msg):
140 """Handle incoming OwnTracks message."""
145 _LOGGER.error(
"Unable to parse payload as JSON: %s", msg.payload)
148 message[
"topic"] = msg.topic
151 await mqtt.async_subscribe(hass, context.mqtt_topic, async_handle_mqtt_message, 1)
157 hass: HomeAssistant, webhook_id: str, request: web.Request
159 """Handle webhook callback.
161 iOS sets the "topic" as part of the payload.
162 Android does not set a topic but adds headers to the request.
164 context = hass.data[DOMAIN][
"context"]
165 topic_base = re.sub(
"/#$",
"", context.mqtt_topic)
168 message = await request.json()
170 _LOGGER.warning(
"Received invalid JSON from OwnTracks")
171 return web.json_response([])
174 if "topic" not in message:
175 headers = request.headers
176 user = headers.get(
"X-Limit-U")
177 device = headers.get(
"X-Limit-D", user)
180 message[
"topic"] = f
"{topic_base}/{user}/{device}"
182 elif message[
"_type"] !=
"encrypted":
184 "No topic or user found in message. If on Android,"
185 " set a username in Connection -> Identification"
188 return web.json_response([])
195 "lat": person.attributes[
"latitude"],
196 "lon": person.attributes[
"longitude"],
197 "tid":
"".join(p[0]
for p
in person.name.split(
" ")[:2]),
198 "tst":
int(person.last_updated.timestamp()),
200 for person
in hass.states.async_all(
"person")
201 if "latitude" in person.attributes
and "longitude" in person.attributes
204 if message[
"_type"] ==
"encrypted" and context.secret:
205 return web.json_response(
207 "_type":
"encrypted",
209 context.secret, message[
"topic"], json.dumps(response)
214 return web.json_response(response)
218 """Hold the current OwnTracks context."""
231 """Initialize an OwnTracks context."""
246 """Check if we should ignore this message."""
247 if (acc := message.get(
"acc"))
is None:
257 "Ignoring %s update because GPS accuracy is zero: %s",
265 "Ignoring %s update because expected GPS accuracy %s is not met: %s",
276 """Set a new async_see function."""
284 """Send a see message to the device tracker."""
289 """Set active beacons to the current location."""
290 kwargs = kwargs_param.copy()
295 device_tracker_state = hass.states.get(f
"device_tracker.{dev_id}")
297 if device_tracker_state
is not None:
298 acc = device_tracker_state.attributes.get(ATTR_GPS_ACCURACY)
299 lat = device_tracker_state.attributes.get(ATTR_LATITUDE)
300 lon = device_tracker_state.attributes.get(ATTR_LONGITUDE)
302 if lat
is not None and lon
is not None:
303 kwargs[
"gps"] = (lat, lon)
304 kwargs[
"gps_accuracy"] = acc
307 kwargs[
"gps_accuracy"] =
None
311 kwargs.pop(
"battery",
None)
313 kwargs[
"dev_id"] = f
"{BEACON_DEV_ID}_{beacon}"
314 kwargs[
"host_name"] = beacon
def async_see_beacons(self, hass, dev_id, kwargs_param)
def __init__(self, hass, secret, max_gps_accuracy, import_waypoints, waypoint_whitelist, region_mapping, events_only, mqtt_topic)
def async_valid_accuracy(self, message)
def async_see(self, **data)
def set_async_see(self, func)
def encrypt_message(secret, topic, message)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
web.Response handle_webhook(HomeAssistant hass, str webhook_id, web.Request request)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup(HomeAssistant hass, ConfigType config)
def async_connect_mqtt(hass, component)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
None async_when_setup(core.HomeAssistant hass, str component, Callable[[core.HomeAssistant, str], Awaitable[None]] when_setup_cb)