Home Assistant Unofficial Reference 2024.12.1
webhook.py
Go to the documentation of this file.
1 """Webhook handlers for mobile_app."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable, Coroutine
7 from contextlib import suppress
8 from functools import lru_cache, wraps
9 from http import HTTPStatus
10 import logging
11 import secrets
12 from typing import Any
13 
14 from aiohttp.web import HTTPBadRequest, Request, Response, json_response
15 from nacl.exceptions import CryptoError
16 from nacl.secret import SecretBox
17 import voluptuous as vol
18 
19 from homeassistant.components import (
20  camera,
21  cloud,
22  conversation,
23  notify as hass_notify,
24  tag,
25 )
26 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
27 from homeassistant.components.camera import CameraEntityFeature
29  ATTR_BATTERY,
30  ATTR_GPS,
31  ATTR_GPS_ACCURACY,
32  ATTR_LOCATION_NAME,
33 )
34 from homeassistant.components.frontend import MANIFEST_JSON
35 from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
36 from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
37 from homeassistant.config_entries import ConfigEntry
38 from homeassistant.const import (
39  ATTR_DEVICE_ID,
40  ATTR_DOMAIN,
41  ATTR_SERVICE,
42  ATTR_SERVICE_DATA,
43  ATTR_SUPPORTED_FEATURES,
44  CONF_NAME,
45  CONF_UNIQUE_ID,
46  CONF_WEBHOOK_ID,
47  EntityCategory,
48 )
49 from homeassistant.core import EventOrigin, HomeAssistant
50 from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
51 from homeassistant.helpers import (
52  config_validation as cv,
53  device_registry as dr,
54  entity_registry as er,
55  template,
56 )
57 from homeassistant.helpers.dispatcher import async_dispatcher_send
58 from homeassistant.util.decorator import Registry
59 
60 from .const import (
61  ATTR_ALTITUDE,
62  ATTR_APP_DATA,
63  ATTR_APP_VERSION,
64  ATTR_CAMERA_ENTITY_ID,
65  ATTR_COURSE,
66  ATTR_DEVICE_NAME,
67  ATTR_EVENT_DATA,
68  ATTR_EVENT_TYPE,
69  ATTR_MANUFACTURER,
70  ATTR_MODEL,
71  ATTR_NO_LEGACY_ENCRYPTION,
72  ATTR_OS_VERSION,
73  ATTR_SENSOR_ATTRIBUTES,
74  ATTR_SENSOR_DEVICE_CLASS,
75  ATTR_SENSOR_DISABLED,
76  ATTR_SENSOR_ENTITY_CATEGORY,
77  ATTR_SENSOR_ICON,
78  ATTR_SENSOR_NAME,
79  ATTR_SENSOR_STATE,
80  ATTR_SENSOR_STATE_CLASS,
81  ATTR_SENSOR_TYPE,
82  ATTR_SENSOR_TYPE_BINARY_SENSOR,
83  ATTR_SENSOR_TYPE_SENSOR,
84  ATTR_SENSOR_UNIQUE_ID,
85  ATTR_SENSOR_UOM,
86  ATTR_SPEED,
87  ATTR_SUPPORTS_ENCRYPTION,
88  ATTR_TEMPLATE,
89  ATTR_TEMPLATE_VARIABLES,
90  ATTR_VERTICAL_ACCURACY,
91  ATTR_WEBHOOK_DATA,
92  ATTR_WEBHOOK_ENCRYPTED,
93  ATTR_WEBHOOK_ENCRYPTED_DATA,
94  ATTR_WEBHOOK_TYPE,
95  CONF_CLOUDHOOK_URL,
96  CONF_REMOTE_UI_URL,
97  CONF_SECRET,
98  DATA_CONFIG_ENTRIES,
99  DATA_DELETED_IDS,
100  DATA_DEVICES,
101  DOMAIN,
102  ERR_ENCRYPTION_ALREADY_ENABLED,
103  ERR_ENCRYPTION_REQUIRED,
104  ERR_INVALID_FORMAT,
105  ERR_SENSOR_NOT_REGISTERED,
106  SCHEMA_APP_DATA,
107  SIGNAL_LOCATION_UPDATE,
108  SIGNAL_SENSOR_UPDATE,
109 )
110 from .helpers import (
111  decrypt_payload,
112  decrypt_payload_legacy,
113  empty_okay_response,
114  error_response,
115  registration_context,
116  safe_registration,
117  webhook_response,
118 )
119 
120 _LOGGER = logging.getLogger(__name__)
121 
122 DELAY_SAVE = 10
123 
124 WEBHOOK_COMMANDS: Registry[
125  str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]]
126 ] = Registry()
127 
128 SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)
129 
130 WEBHOOK_PAYLOAD_SCHEMA = vol.Any(
131  vol.Schema(
132  {
133  vol.Required(ATTR_WEBHOOK_TYPE): cv.string,
134  vol.Optional(ATTR_WEBHOOK_DATA): vol.Any(dict, list),
135  }
136  ),
137  vol.Schema(
138  {
139  vol.Required(ATTR_WEBHOOK_TYPE): cv.string,
140  vol.Required(ATTR_WEBHOOK_ENCRYPTED): True,
141  vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string,
142  }
143  ),
144 )
145 
146 SENSOR_SCHEMA_FULL = vol.Schema(
147  {
148  vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
149  vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any(None, cv.icon),
150  vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str),
151  vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
152  vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
153  }
154 )
155 
156 
157 def validate_schema(schema):
158  """Decorate a webhook function with a schema."""
159  if isinstance(schema, dict):
160  schema = vol.Schema(schema)
161 
162  def wrapper(func):
163  """Wrap function so we validate schema."""
164 
165  @wraps(func)
166  async def validate_and_run(hass, config_entry, data):
167  """Validate input and call handler."""
168  try:
169  data = schema(data)
170  except vol.Invalid as ex:
171  err = vol.humanize.humanize_error(data, ex)
172  _LOGGER.error("Received invalid webhook payload: %s", err)
173  return empty_okay_response()
174 
175  return await func(hass, config_entry, data)
176 
177  return validate_and_run
178 
179  return wrapper
180 
181 
182 async def handle_webhook(
183  hass: HomeAssistant, webhook_id: str, request: Request
184 ) -> Response:
185  """Handle webhook callback."""
186  if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
187  return Response(status=410)
188 
189  config_entry: ConfigEntry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
190 
191  device_name: str = config_entry.data[ATTR_DEVICE_NAME]
192 
193  try:
194  req_data = await request.json()
195  except ValueError:
196  _LOGGER.warning("Received invalid JSON from mobile_app device: %s", device_name)
197  return empty_okay_response(status=HTTPStatus.BAD_REQUEST)
198 
199  if (
200  ATTR_WEBHOOK_ENCRYPTED not in req_data
201  and config_entry.data[ATTR_SUPPORTS_ENCRYPTION]
202  ):
203  _LOGGER.warning(
204  "Refusing to accept unencrypted webhook from %s",
205  device_name,
206  )
207  return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required")
208 
209  try:
210  req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data)
211  except vol.Invalid as ex:
212  err = vol.humanize.humanize_error(req_data, ex)
213  _LOGGER.error(
214  "Received invalid webhook from %s with payload: %s", device_name, err
215  )
216  return empty_okay_response()
217 
218  webhook_type = req_data[ATTR_WEBHOOK_TYPE]
219 
220  webhook_payload = None
221 
222  if ATTR_WEBHOOK_ENCRYPTED in req_data:
223  enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA]
224  try:
225  webhook_payload = decrypt_payload(config_entry.data[CONF_SECRET], enc_data)
226  if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data:
227  data = {**config_entry.data, ATTR_NO_LEGACY_ENCRYPTION: True}
228  hass.config_entries.async_update_entry(config_entry, data=data)
229  except CryptoError:
230  if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data:
231  try:
232  webhook_payload = decrypt_payload_legacy(
233  config_entry.data[CONF_SECRET], enc_data
234  )
235  except CryptoError:
236  _LOGGER.warning(
237  "Ignoring encrypted payload because unable to decrypt"
238  )
239  except ValueError:
240  _LOGGER.warning("Ignoring invalid JSON in encrypted payload")
241  else:
242  _LOGGER.warning("Ignoring encrypted payload because unable to decrypt")
243  except ValueError as err:
244  _LOGGER.warning("Ignoring invalid JSON in encrypted payload: %s", err)
245  else:
246  webhook_payload = req_data.get(ATTR_WEBHOOK_DATA, {})
247 
248  if webhook_payload is None:
249  return empty_okay_response()
250 
251  if webhook_type not in WEBHOOK_COMMANDS:
252  _LOGGER.error(
253  "Received invalid webhook from %s of type: %s", device_name, webhook_type
254  )
255  return empty_okay_response()
256 
257  _LOGGER.debug(
258  "Received webhook payload from %s for type %s: %s",
259  device_name,
260  webhook_type,
261  webhook_payload,
262  )
263 
264  # Shield so we make sure we finish the webhook, even if sender hangs up.
265  return await asyncio.shield(
266  WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload)
267  )
268 
269 
270 @WEBHOOK_COMMANDS.register("call_service")
271 @validate_schema( { vol.Required(ATTR_DOMAIN): cv.string,
272  vol.Required(ATTR_SERVICE): cv.string,
273  vol.Optional(ATTR_SERVICE_DATA, default={}): dict,
274  }
275 )
276 async def webhook_call_service(
277  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
278 ) -> Response:
279  """Handle a call service webhook."""
280  try:
281  await hass.services.async_call(
282  data[ATTR_DOMAIN],
283  data[ATTR_SERVICE],
284  data[ATTR_SERVICE_DATA],
285  blocking=True,
286  context=registration_context(config_entry.data),
287  )
288  except (vol.Invalid, ServiceNotFound, Exception) as ex:
289  _LOGGER.error(
290  (
291  "Error when calling service during mobile_app "
292  "webhook (device name: %s): %s"
293  ),
294  config_entry.data[ATTR_DEVICE_NAME],
295  ex,
296  )
297  raise HTTPBadRequest from ex
298 
299  return empty_okay_response()
300 
301 
302 @WEBHOOK_COMMANDS.register("fire_event")
303 @validate_schema( { vol.Required(ATTR_EVENT_TYPE): cv.string,
304  vol.Optional(ATTR_EVENT_DATA, default={}): dict,
305  }
306 )
307 async def webhook_fire_event(
308  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
309 ) -> Response:
310  """Handle a fire event webhook."""
311  event_type: str = data[ATTR_EVENT_TYPE]
312  hass.bus.async_fire(
313  event_type,
314  data[ATTR_EVENT_DATA],
315  EventOrigin.remote,
316  context=registration_context(config_entry.data),
317  )
318  return empty_okay_response()
319 
320 
321 @WEBHOOK_COMMANDS.register("conversation_process")
322 @validate_schema( { vol.Required("text"): cv.string,
323  vol.Optional("language"): cv.string,
324  vol.Optional("conversation_id"): cv.string,
325  }
326 )
328  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
329 ) -> Response:
330  """Handle a conversation process webhook."""
331  result = await conversation.async_converse(
332  hass,
333  text=data["text"],
334  language=data.get("language"),
335  conversation_id=data.get("conversation_id"),
336  context=registration_context(config_entry.data),
337  )
338  return webhook_response(result.as_dict(), registration=config_entry.data)
339 
340 
341 @WEBHOOK_COMMANDS.register("stream_camera")
342 @validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string})
343 async def webhook_stream_camera(
344  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, str]
345 ) -> Response:
346  """Handle a request to HLS-stream a camera."""
347  if (camera_state := hass.states.get(data[ATTR_CAMERA_ENTITY_ID])) is None:
348  return webhook_response(
349  {"success": False},
350  registration=config_entry.data,
351  status=HTTPStatus.BAD_REQUEST,
352  )
353 
354  resp: dict[str, Any] = {
355  "mjpeg_path": f"/api/camera_proxy_stream/{camera_state.entity_id}"
356  }
357 
358  if camera_state.attributes[ATTR_SUPPORTED_FEATURES] & CameraEntityFeature.STREAM:
359  try:
360  resp["hls_path"] = await camera.async_request_stream(
361  hass, camera_state.entity_id, "hls"
362  )
363  except HomeAssistantError:
364  resp["hls_path"] = None
365  else:
366  resp["hls_path"] = None
367 
368  return webhook_response(resp, registration=config_entry.data)
369 
370 
371 @lru_cache
372 def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template:
373  """Return a cached template."""
374  return template.Template(template_str, hass)
375 
376 
377 @WEBHOOK_COMMANDS.register("render_template")
378 @validate_schema( { str: { vol.Required(ATTR_TEMPLATE): cv.string,
379  vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict,
380  }
381  }
382 )
383 async def webhook_render_template(
384  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
385 ) -> Response:
386  """Handle a render template webhook."""
387  resp = {}
388  for key, item in data.items():
389  try:
390  tpl = _cached_template(item[ATTR_TEMPLATE], hass)
391  resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES))
392  except TemplateError as ex:
393  resp[key] = {"error": str(ex)}
394 
395  return webhook_response(resp, registration=config_entry.data)
396 
397 
398 @WEBHOOK_COMMANDS.register("update_location")
399 @validate_schema( vol.All( cv.key_dependency(ATTR_GPS, ATTR_GPS_ACCURACY),
400  vol.Schema(
401  {
402  vol.Optional(ATTR_LOCATION_NAME): cv.string,
403  vol.Optional(ATTR_GPS): cv.gps,
404  vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int,
405  vol.Optional(ATTR_BATTERY): cv.positive_int,
406  vol.Optional(ATTR_SPEED): cv.positive_int,
407  vol.Optional(ATTR_ALTITUDE): vol.Coerce(float),
408  vol.Optional(ATTR_COURSE): cv.positive_int,
409  vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int,
410  },
411  ),
412  )
413 )
414 async def webhook_update_location(
415  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
416 ) -> Response:
417  """Handle an update location webhook."""
419  hass, SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data
420  )
421  return empty_okay_response()
422 
423 
424 @WEBHOOK_COMMANDS.register("update_registration")
425 @validate_schema( { vol.Optional(ATTR_APP_DATA): SCHEMA_APP_DATA,
426  vol.Required(ATTR_APP_VERSION): cv.string,
427  vol.Required(ATTR_DEVICE_NAME): cv.string,
428  vol.Required(ATTR_MANUFACTURER): cv.string,
429  vol.Required(ATTR_MODEL): cv.string,
430  vol.Optional(ATTR_OS_VERSION): cv.string,
431  }
432 )
434  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
435 ) -> Response:
436  """Handle an update registration webhook."""
437  new_registration = {**config_entry.data, **data}
438 
439  device_registry = dr.async_get(hass)
440 
441  device_registry.async_get_or_create(
442  config_entry_id=config_entry.entry_id,
443  identifiers={(DOMAIN, config_entry.data[ATTR_DEVICE_ID])},
444  manufacturer=new_registration[ATTR_MANUFACTURER],
445  model=new_registration[ATTR_MODEL],
446  name=new_registration[ATTR_DEVICE_NAME],
447  sw_version=new_registration[ATTR_OS_VERSION],
448  )
449 
450  hass.config_entries.async_update_entry(config_entry, data=new_registration)
451 
452  await hass_notify.async_reload(hass, DOMAIN)
453 
454  return webhook_response(
455  safe_registration(new_registration),
456  registration=new_registration,
457  )
458 
459 
460 @WEBHOOK_COMMANDS.register("enable_encryption")
461 async def webhook_enable_encryption(
462  hass: HomeAssistant, config_entry: ConfigEntry, data: Any
463 ) -> Response:
464  """Handle a encryption enable webhook."""
465  if config_entry.data[ATTR_SUPPORTS_ENCRYPTION]:
466  _LOGGER.warning(
467  "Refusing to enable encryption for %s because it is already enabled!",
468  config_entry.data[ATTR_DEVICE_NAME],
469  )
470  return error_response(
471  ERR_ENCRYPTION_ALREADY_ENABLED, "Encryption already enabled"
472  )
473 
474  secret = secrets.token_hex(SecretBox.KEY_SIZE)
475 
476  update_data = {
477  **config_entry.data,
478  ATTR_SUPPORTS_ENCRYPTION: True,
479  CONF_SECRET: secret,
480  }
481 
482  hass.config_entries.async_update_entry(config_entry, data=update_data)
483 
484  return json_response({"secret": secret})
485 
486 
487 def _validate_state_class_sensor(value: dict[str, Any]) -> dict[str, Any]:
488  """Validate we only set state class for sensors."""
489  if (
490  ATTR_SENSOR_STATE_CLASS in value
491  and value[ATTR_SENSOR_TYPE] != ATTR_SENSOR_TYPE_SENSOR
492  ):
493  raise vol.Invalid("state_class only allowed for sensors")
494 
495  return value
496 
497 
498 def _gen_unique_id(webhook_id: str, sensor_unique_id: str) -> str:
499  """Return a unique sensor ID."""
500  return f"{webhook_id}_{sensor_unique_id}"
501 
502 
503 def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str:
504  """Return a unique sensor ID."""
505  return unique_id[len(webhook_id) + 1 :]
506 
507 
508 @WEBHOOK_COMMANDS.register("register_sensor")
509 @validate_schema( vol.All( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
510  vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any(
511  None,
512  vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)),
513  vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)),
514  ),
515  vol.Required(ATTR_SENSOR_NAME): cv.string,
516  vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
517  vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
518  vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string),
519  vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any(
520  None, bool, int, float, str
521  ),
522  vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any(
523  None, vol.Coerce(EntityCategory)
524  ),
525  vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any(
526  None, cv.icon
527  ),
528  vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any(
529  None, vol.Coerce(SensorStateClass)
530  ),
531  vol.Optional(ATTR_SENSOR_DISABLED): bool,
532  },
533  _validate_state_class_sensor,
534  )
535 )
536 async def webhook_register_sensor(
537  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any]
538 ) -> Response:
539  """Handle a register sensor webhook."""
540  entity_type: str = data[ATTR_SENSOR_TYPE]
541  unique_id: str = data[ATTR_SENSOR_UNIQUE_ID]
542  device_name: str = config_entry.data[ATTR_DEVICE_NAME]
543 
544  unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id)
545  entity_registry = er.async_get(hass)
546  existing_sensor = entity_registry.async_get_entity_id(
547  entity_type, DOMAIN, unique_store_key
548  )
549 
550  data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
551 
552  # If sensor already is registered, update current state instead
553  if existing_sensor:
554  _LOGGER.debug(
555  "Re-register for %s of existing sensor %s", device_name, unique_id
556  )
557 
558  entry = entity_registry.async_get(existing_sensor)
559  assert entry is not None
560  changes: dict[str, Any] = {}
561 
562  if (
563  new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}"
564  ) != entry.original_name:
565  changes["original_name"] = new_name
566 
567  if (
568  should_be_disabled := data.get(ATTR_SENSOR_DISABLED)
569  ) is None or should_be_disabled == entry.disabled:
570  pass
571  elif should_be_disabled:
572  changes["disabled_by"] = er.RegistryEntryDisabler.INTEGRATION
573  else:
574  changes["disabled_by"] = None
575 
576  for ent_reg_key, data_key in (
577  ("device_class", ATTR_SENSOR_DEVICE_CLASS),
578  ("unit_of_measurement", ATTR_SENSOR_UOM),
579  ("entity_category", ATTR_SENSOR_ENTITY_CATEGORY),
580  ("original_icon", ATTR_SENSOR_ICON),
581  ):
582  if data_key in data and getattr(entry, ent_reg_key) != data[data_key]:
583  changes[ent_reg_key] = data[data_key]
584 
585  if changes:
586  entity_registry.async_update_entity(existing_sensor, **changes)
587 
588  async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data)
589  else:
590  data[CONF_UNIQUE_ID] = unique_store_key
591  data[CONF_NAME] = (
592  f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}"
593  )
594 
595  register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
596  async_dispatcher_send(hass, register_signal, data)
597 
598  return webhook_response(
599  {"success": True},
600  registration=config_entry.data,
601  status=HTTPStatus.CREATED,
602  )
603 
604 
605 @WEBHOOK_COMMANDS.register("update_sensor_states")
606 @validate_schema( vol.All( cv.ensure_list, [ # Partial schema, enough to identify schema. # We don't validate everything because otherwise 1 invalid sensor # will invalidate all sensors. vol.Schema( { vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
607  vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
608  },
609  extra=vol.ALLOW_EXTRA,
610  )
611  ],
612  )
613 )
615  hass: HomeAssistant, config_entry: ConfigEntry, data: list[dict[str, Any]]
616 ) -> Response:
617  """Handle an update sensor states webhook."""
618  device_name: str = config_entry.data[ATTR_DEVICE_NAME]
619  resp: dict[str, Any] = {}
620  entity_registry = er.async_get(hass)
621 
622  for sensor in data:
623  entity_type: str = sensor[ATTR_SENSOR_TYPE]
624 
625  unique_id: str = sensor[ATTR_SENSOR_UNIQUE_ID]
626 
627  unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id)
628 
629  if not (
630  entity_id := entity_registry.async_get_entity_id(
631  entity_type, DOMAIN, unique_store_key
632  )
633  ):
634  _LOGGER.debug(
635  "Refusing to update %s non-registered sensor: %s",
636  device_name,
637  unique_store_key,
638  )
639  err_msg = f"{entity_type} {unique_id} is not registered"
640  resp[unique_id] = {
641  "success": False,
642  "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg},
643  }
644  continue
645 
646  try:
647  sensor = SENSOR_SCHEMA_FULL(sensor)
648  except vol.Invalid as err:
649  err_msg = vol.humanize.humanize_error(sensor, err)
650  _LOGGER.error(
651  "Received invalid sensor payload from %s for %s: %s",
652  device_name,
653  unique_id,
654  err_msg,
655  )
656  resp[unique_id] = {
657  "success": False,
658  "error": {"code": ERR_INVALID_FORMAT, "message": err_msg},
659  }
660  continue
661 
662  sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
664  hass,
665  f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}",
666  sensor,
667  )
668 
669  resp[unique_id] = {"success": True}
670 
671  # Check if disabled
672  entry = entity_registry.async_get(entity_id)
673 
674  if entry and entry.disabled_by:
675  resp[unique_id]["is_disabled"] = True
676 
677  return webhook_response(resp, registration=config_entry.data)
678 
679 
680 @WEBHOOK_COMMANDS.register("get_zones")
681 async def webhook_get_zones(
682  hass: HomeAssistant, config_entry: ConfigEntry, data: Any
683 ) -> Response:
684  """Handle a get zones webhook."""
685  zones = [
686  hass.states.get(entity_id)
687  for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN))
688  ]
689  return webhook_response(zones, registration=config_entry.data)
690 
691 
692 @WEBHOOK_COMMANDS.register("get_config")
693 async def webhook_get_config(
694  hass: HomeAssistant, config_entry: ConfigEntry, data: Any
695 ) -> Response:
696  """Handle a get config webhook."""
697  hass_config = hass.config.as_dict()
698 
699  device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][
700  config_entry.data[CONF_WEBHOOK_ID]
701  ]
702 
703  resp = {
704  "latitude": hass_config["latitude"],
705  "longitude": hass_config["longitude"],
706  "elevation": hass_config["elevation"],
707  "hass_device_id": device.id,
708  "unit_system": hass_config["unit_system"],
709  "location_name": hass_config["location_name"],
710  "time_zone": hass_config["time_zone"],
711  "components": hass_config["components"],
712  "version": hass_config["version"],
713  "theme_color": MANIFEST_JSON["theme_color"],
714  }
715 
716  if CONF_CLOUDHOOK_URL in config_entry.data:
717  resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL]
718 
719  if cloud.async_active_subscription(hass):
720  with suppress(cloud.CloudNotAvailable):
721  resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass)
722 
723  webhook_id = config_entry.data[CONF_WEBHOOK_ID]
724 
725  entities = {}
726  for entry in er.async_entries_for_config_entry(
727  er.async_get(hass), config_entry.entry_id
728  ):
729  if entry.domain in ("binary_sensor", "sensor"):
730  unique_id = _extract_sensor_unique_id(webhook_id, entry.unique_id)
731  else:
732  unique_id = entry.unique_id
733 
734  entities[unique_id] = {"disabled": entry.disabled}
735 
736  resp["entities"] = entities
737 
738  return webhook_response(resp, registration=config_entry.data)
739 
740 
741 @WEBHOOK_COMMANDS.register("scan_tag")
742 @validate_schema({vol.Required("tag_id"): cv.string})
743 async def webhook_scan_tag(
744  hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, str]
745 ) -> Response:
746  """Handle a fire event webhook."""
747  await tag.async_scan_tag(
748  hass,
749  data["tag_id"],
750  hass.data[DOMAIN][DATA_DEVICES][config_entry.data[CONF_WEBHOOK_ID]].id,
751  registration_context(config_entry.data),
752  )
753  return empty_okay_response()
754 
JsonValueType|None decrypt_payload(str key, bytes ciphertext)
Definition: helpers.py:97
Response error_response(str code, str message, HTTPStatus status=HTTPStatus.BAD_REQUEST, dict|None headers=None)
Definition: helpers.py:136
dict safe_registration(dict registration)
Definition: helpers.py:145
Context registration_context(Mapping[str, Any] registration)
Definition: helpers.py:117
Response empty_okay_response(dict|None headers=None, HTTPStatus status=HTTPStatus.OK)
Definition: helpers.py:124
Response webhook_response(Any data, *Mapping[str, Any] registration, HTTPStatus status=HTTPStatus.OK, Mapping[str, str]|None headers=None)
Definition: helpers.py:174
JsonValueType|None decrypt_payload_legacy(str key, bytes ciphertext)
Definition: helpers.py:110
str _extract_sensor_unique_id(str webhook_id, str unique_id)
Definition: webhook.py:516
Response webhook_enable_encryption(HomeAssistant hass, ConfigEntry config_entry, Any data)
Definition: webhook.py:476
Response webhook_conversation_process(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:335
Response webhook_stream_camera(HomeAssistant hass, ConfigEntry config_entry, dict[str, str] data)
Definition: webhook.py:351
Response webhook_update_location(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:427
Response webhook_render_template(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:394
Response webhook_update_sensor_states(HomeAssistant hass, ConfigEntry config_entry, list[dict[str, Any]] data)
Definition: webhook.py:641
str _gen_unique_id(str webhook_id, str sensor_unique_id)
Definition: webhook.py:511
Response handle_webhook(HomeAssistant hass, str webhook_id, Request request)
Definition: webhook.py:184
Response webhook_register_sensor(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:554
Response webhook_scan_tag(HomeAssistant hass, ConfigEntry config_entry, dict[str, str] data)
Definition: webhook.py:770
Response webhook_get_config(HomeAssistant hass, ConfigEntry config_entry, Any data)
Definition: webhook.py:720
dict[str, Any] _validate_state_class_sensor(dict[str, Any] value)
Definition: webhook.py:500
Response webhook_update_registration(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:448
template.Template _cached_template(str template_str, HomeAssistant hass)
Definition: webhook.py:378
Response webhook_call_service(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:280
Response webhook_get_zones(HomeAssistant hass, ConfigEntry config_entry, Any data)
Definition: webhook.py:708
Response webhook_fire_event(HomeAssistant hass, ConfigEntry config_entry, dict[str, Any] data)
Definition: webhook.py:313
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193