Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Locative."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 import logging
7 
8 from aiohttp import web
9 import voluptuous as vol
10 
11 from homeassistant.components import webhook
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import (
14  ATTR_ID,
15  ATTR_LATITUDE,
16  ATTR_LONGITUDE,
17  CONF_WEBHOOK_ID,
18  STATE_NOT_HOME,
19  Platform,
20 )
21 from homeassistant.core import HomeAssistant
22 from homeassistant.helpers import config_entry_flow
24 from homeassistant.helpers.dispatcher import async_dispatcher_send
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 DOMAIN = "locative"
29 TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
30 
31 PLATFORMS = [Platform.DEVICE_TRACKER]
32 
33 ATTR_DEVICE_ID = "device"
34 ATTR_TRIGGER = "trigger"
35 
36 
37 def _id(value: str) -> str:
38  """Coerce id by removing '-'."""
39  return value.replace("-", "")
40 
41 
42 def _validate_test_mode(obj: dict) -> dict:
43  """Validate that id is provided outside of test mode."""
44  if ATTR_ID not in obj and obj[ATTR_TRIGGER] != "test":
45  raise vol.Invalid("Location id not specified")
46  return obj
47 
48 
49 WEBHOOK_SCHEMA = vol.All(
50  vol.Schema(
51  {
52  vol.Required(ATTR_LATITUDE): cv.latitude,
53  vol.Required(ATTR_LONGITUDE): cv.longitude,
54  vol.Required(ATTR_DEVICE_ID): cv.string,
55  vol.Required(ATTR_TRIGGER): cv.string,
56  vol.Optional(ATTR_ID): vol.All(cv.string, _id),
57  },
58  extra=vol.ALLOW_EXTRA,
59  ),
60  _validate_test_mode,
61 )
62 
63 
64 async def handle_webhook(
65  hass: HomeAssistant, webhook_id: str, request: web.Request
66 ) -> web.Response:
67  """Handle incoming webhook from Locative."""
68  try:
69  data = WEBHOOK_SCHEMA(dict(await request.post()))
70  except vol.MultipleInvalid as error:
71  return web.Response(
72  text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
73  )
74 
75  device = data[ATTR_DEVICE_ID]
76  location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower()
77  direction = data[ATTR_TRIGGER]
78  gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
79 
80  if direction == "enter":
81  async_dispatcher_send(hass, TRACKER_UPDATE, device, gps_location, location_name)
82  return web.Response(text=f"Setting location to {location_name}")
83 
84  if direction == "exit":
85  current_state = hass.states.get(f"{Platform.DEVICE_TRACKER}.{device}")
86 
87  if current_state is None or current_state.state == location_name:
88  location_name = STATE_NOT_HOME
90  hass, TRACKER_UPDATE, device, gps_location, location_name
91  )
92  return web.Response(text="Setting location to not home")
93 
94  # Ignore the message if it is telling us to exit a zone that we
95  # aren't currently in. This occurs when a zone is entered
96  # before the previous zone was exited. The enter message will
97  # be sent first, then the exit message will be sent second.
98  return web.Response(
99  text=f"Ignoring exit from {location_name} (already in {current_state})",
100  )
101 
102  if direction == "test":
103  # In the app, a test message can be sent. Just return something to
104  # the user to let them know that it works.
105  return web.Response(text="Received test message.")
106 
107  _LOGGER.error("Received unidentified message from Locative: %s", direction)
108  return web.Response(
109  text=f"Received unidentified message: {direction}",
110  status=HTTPStatus.UNPROCESSABLE_ENTITY,
111  )
112 
113 
114 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
115  """Configure based on config entry."""
116  if DOMAIN not in hass.data:
117  hass.data[DOMAIN] = {"devices": set(), "unsub_device_tracker": {}}
118  webhook.async_register(
119  hass, DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook
120  )
121 
122  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
123  return True
124 
125 
126 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
127  """Unload a config entry."""
128  webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
129  hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
130  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
131 
132 
133 async_remove_entry = config_entry_flow.webhook_async_remove_entry
dict _validate_test_mode(dict obj)
Definition: __init__.py:42
web.Response handle_webhook(HomeAssistant hass, str webhook_id, web.Request request)
Definition: __init__.py:66
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:126
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:114
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193