Home Assistant Unofficial Reference 2024.12.1
http_api.py
Go to the documentation of this file.
1 """The HTTP api to control the cloud integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable, Coroutine, Mapping
7 from contextlib import suppress
8 import dataclasses
9 from functools import wraps
10 from http import HTTPStatus
11 import logging
12 from typing import Any, Concatenate
13 
14 import aiohttp
15 from aiohttp import web
16 import attr
17 from hass_nabucasa import Cloud, auth, thingtalk
18 from hass_nabucasa.const import STATE_DISCONNECTED
19 from hass_nabucasa.voice import TTS_VOICES
20 import voluptuous as vol
21 
22 from homeassistant.components import websocket_api
24  entities as alexa_entities,
25  errors as alexa_errors,
26 )
27 from homeassistant.components.google_assistant import helpers as google_helpers
28 from homeassistant.components.homeassistant import exposed_entities
29 from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
30 from homeassistant.components.http.data_validator import RequestDataValidator
31 from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
32 from homeassistant.core import HomeAssistant, callback
33 from homeassistant.exceptions import HomeAssistantError
34 from homeassistant.helpers.aiohttp_client import async_get_clientsession
35 from homeassistant.util.location import async_detect_location_info
36 
37 from .alexa_config import entity_supported as entity_supported_by_alexa
38 from .assist_pipeline import async_create_cloud_pipeline
39 from .client import CloudClient
40 from .const import (
41  DATA_CLOUD,
42  PREF_ALEXA_REPORT_STATE,
43  PREF_DISABLE_2FA,
44  PREF_ENABLE_ALEXA,
45  PREF_ENABLE_CLOUD_ICE_SERVERS,
46  PREF_ENABLE_GOOGLE,
47  PREF_GOOGLE_REPORT_STATE,
48  PREF_GOOGLE_SECURE_DEVICES_PIN,
49  PREF_REMOTE_ALLOW_REMOTE_ENABLE,
50  PREF_TTS_DEFAULT_VOICE,
51  REQUEST_TIMEOUT,
52 )
53 from .google_config import CLOUD_GOOGLE
54 from .repairs import async_manage_legacy_subscription_issue
55 from .subscription import async_subscription_info
56 
57 _LOGGER = logging.getLogger(__name__)
58 
59 
60 _CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = {
61  TimeoutError: (
62  HTTPStatus.BAD_GATEWAY,
63  "Unable to reach the Home Assistant cloud.",
64  ),
65  aiohttp.ClientError: (
66  HTTPStatus.INTERNAL_SERVER_ERROR,
67  "Error making internal request",
68  ),
69 }
70 
71 
72 @callback
73 def async_setup(hass: HomeAssistant) -> None:
74  """Initialize the HTTP API."""
75  websocket_api.async_register_command(hass, websocket_cloud_remove_data)
76  websocket_api.async_register_command(hass, websocket_cloud_status)
77  websocket_api.async_register_command(hass, websocket_subscription)
78  websocket_api.async_register_command(hass, websocket_update_prefs)
79  websocket_api.async_register_command(hass, websocket_hook_create)
80  websocket_api.async_register_command(hass, websocket_hook_delete)
81  websocket_api.async_register_command(hass, websocket_remote_connect)
82  websocket_api.async_register_command(hass, websocket_remote_disconnect)
83 
84  websocket_api.async_register_command(hass, google_assistant_get)
85  websocket_api.async_register_command(hass, google_assistant_list)
86  websocket_api.async_register_command(hass, google_assistant_update)
87 
88  websocket_api.async_register_command(hass, alexa_get)
89  websocket_api.async_register_command(hass, alexa_list)
90  websocket_api.async_register_command(hass, alexa_sync)
91 
92  websocket_api.async_register_command(hass, thingtalk_convert)
93  websocket_api.async_register_command(hass, tts_info)
94 
95  hass.http.register_view(GoogleActionsSyncView)
96  hass.http.register_view(CloudLoginView)
97  hass.http.register_view(CloudLogoutView)
98  hass.http.register_view(CloudRegisterView)
99  hass.http.register_view(CloudResendConfirmView)
100  hass.http.register_view(CloudForgotPasswordView)
101 
102  _CLOUD_ERRORS.update(
103  {
104  auth.UserNotFound: (HTTPStatus.BAD_REQUEST, "User does not exist."),
105  auth.UserNotConfirmed: (HTTPStatus.BAD_REQUEST, "Email not confirmed."),
106  auth.UserExists: (
107  HTTPStatus.BAD_REQUEST,
108  "An account with the given email already exists.",
109  ),
110  auth.Unauthenticated: (HTTPStatus.UNAUTHORIZED, "Authentication failed."),
111  auth.PasswordChangeRequired: (
112  HTTPStatus.BAD_REQUEST,
113  "Password change required.",
114  ),
115  }
116  )
117 
118 
119 def _handle_cloud_errors[_HassViewT: HomeAssistantView, **_P](
120  handler: Callable[
121  Concatenate[_HassViewT, web.Request, _P], Awaitable[web.Response]
122  ],
123 ) -> Callable[
124  Concatenate[_HassViewT, web.Request, _P], Coroutine[Any, Any, web.Response]
125 ]:
126  """Webview decorator to handle auth errors."""
127 
128  @wraps(handler)
129  async def error_handler(
130  view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs
131  ) -> web.Response:
132  """Handle exceptions that raise from the wrapped request handler."""
133  try:
134  result = await handler(view, request, *args, **kwargs)
135  except Exception as err: # noqa: BLE001
136  status, msg = _process_cloud_exception(err, request.path)
137  return view.json_message(
138  msg, status_code=status, message_code=err.__class__.__name__.lower()
139  )
140  return result
141 
142  return error_handler
143 
144 
146  handler: Callable[
147  [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
148  Coroutine[None, None, None],
149  ],
150 ) -> Callable[
151  [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
152  Coroutine[None, None, None],
153 ]:
154  """Websocket decorator to handle auth errors."""
155 
156  @wraps(handler)
157  async def error_handler(
158  hass: HomeAssistant,
159  connection: websocket_api.ActiveConnection,
160  msg: dict[str, Any],
161  ) -> None:
162  """Handle exceptions that raise from the wrapped handler."""
163  try:
164  return await handler(hass, connection, msg)
165 
166  except Exception as err: # noqa: BLE001
167  err_status, err_msg = _process_cloud_exception(err, msg["type"])
168  connection.send_error(msg["id"], str(err_status), err_msg)
169 
170  return error_handler
171 
172 
173 def _process_cloud_exception(exc: Exception, where: str) -> tuple[HTTPStatus, str]:
174  """Process a cloud exception."""
175  err_info: tuple[HTTPStatus, str] | None = None
176 
177  for err, value_info in _CLOUD_ERRORS.items():
178  if isinstance(exc, err):
179  err_info = value_info
180  break
181 
182  if err_info is None:
183  _LOGGER.exception("Unexpected error processing request for %s", where)
184  err_info = (HTTPStatus.BAD_GATEWAY, f"Unexpected error: {exc}")
185 
186  return err_info
187 
188 
189 class GoogleActionsSyncView(HomeAssistantView):
190  """Trigger a Google Actions Smart Home Sync."""
191 
192  url = "/api/cloud/google_actions/sync"
193  name = "api:cloud:google_actions/sync"
194 
195  @require_admin
196  @_handle_cloud_errors
197  async def post(self, request: web.Request) -> web.Response:
198  """Trigger a Google Actions sync."""
199  hass = request.app[KEY_HASS]
200  cloud = hass.data[DATA_CLOUD]
201  gconf = await cloud.client.get_google_config()
202  status = await gconf.async_sync_entities(gconf.agent_user_id)
203  return self.json({}, status_code=status)
204 
205 
206 class CloudLoginView(HomeAssistantView):
207  """Login to Home Assistant cloud."""
208 
209  url = "/api/cloud/login"
210  name = "api:cloud:login"
211 
212  @require_admin
213  @_handle_cloud_errors
214  @RequestDataValidator( vol.Schema({vol.Required("email"): str, vol.Required("password"): str})
215  )
216  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
217  """Handle login request."""
218  hass = request.app[KEY_HASS]
219  cloud = hass.data[DATA_CLOUD]
220  await cloud.login(data["email"], data["password"])
221 
222  if "assist_pipeline" in hass.config.components:
223  new_cloud_pipeline_id = await async_create_cloud_pipeline(hass)
224  else:
225  new_cloud_pipeline_id = None
226  return self.json({"success": True, "cloud_pipeline": new_cloud_pipeline_id})
227 
228 
229 class CloudLogoutView(HomeAssistantView):
230  """Log out of the Home Assistant cloud."""
231 
232  url = "/api/cloud/logout"
233  name = "api:cloud:logout"
234 
235  @require_admin
236  @_handle_cloud_errors
237  async def post(self, request: web.Request) -> web.Response:
238  """Handle logout request."""
239  hass = request.app[KEY_HASS]
240  cloud = hass.data[DATA_CLOUD]
241 
242  async with asyncio.timeout(REQUEST_TIMEOUT):
243  await cloud.logout()
244 
245  return self.json_message("ok")
246 
247 
248 class CloudRegisterView(HomeAssistantView):
249  """Register on the Home Assistant cloud."""
250 
251  url = "/api/cloud/register"
252  name = "api:cloud:register"
253 
254  @require_admin
255  @_handle_cloud_errors
256  @RequestDataValidator( vol.Schema( { vol.Required("email"): str,
257  vol.Required("password"): vol.All(str, vol.Length(min=6)),
258  }
259  )
260  )
261  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
262  """Handle registration request."""
263  hass = request.app[KEY_HASS]
264  cloud = hass.data[DATA_CLOUD]
265 
266  client_metadata = None
267 
268  if (
269  location_info := await async_detect_location_info(
271  )
272  ) and location_info.country_code is not None:
273  client_metadata = {"NC_COUNTRY_CODE": location_info.country_code}
274  if location_info.region_code is not None:
275  client_metadata["NC_REGION_CODE"] = location_info.region_code
276  if location_info.zip_code is not None:
277  client_metadata["NC_ZIP_CODE"] = location_info.zip_code
278 
279  async with asyncio.timeout(REQUEST_TIMEOUT):
280  await cloud.auth.async_register(
281  data["email"],
282  data["password"],
283  client_metadata=client_metadata,
284  )
285 
286  return self.json_message("ok")
287 
288 
289 class CloudResendConfirmView(HomeAssistantView):
290  """Resend email confirmation code."""
291 
292  url = "/api/cloud/resend_confirm"
293  name = "api:cloud:resend_confirm"
294 
295  @require_admin
296  @_handle_cloud_errors
297  @RequestDataValidator(vol.Schema({vol.Required("email"): str}))
298  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
299  """Handle resending confirm email code request."""
300  hass = request.app[KEY_HASS]
301  cloud = hass.data[DATA_CLOUD]
302 
303  async with asyncio.timeout(REQUEST_TIMEOUT):
304  await cloud.auth.async_resend_email_confirm(data["email"])
305 
306  return self.json_message("ok")
307 
308 
309 class CloudForgotPasswordView(HomeAssistantView):
310  """View to start Forgot Password flow.."""
311 
312  url = "/api/cloud/forgot_password"
313  name = "api:cloud:forgot_password"
314 
315  @require_admin
316  @_handle_cloud_errors
317  @RequestDataValidator(vol.Schema({vol.Required("email"): str}))
318  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
319  """Handle forgot password request."""
320  hass = request.app[KEY_HASS]
321  cloud = hass.data[DATA_CLOUD]
322 
323  async with asyncio.timeout(REQUEST_TIMEOUT):
324  await cloud.auth.async_forgot_password(data["email"])
325 
326  return self.json_message("ok")
327 
328 
329 @websocket_api.require_admin
330 @websocket_api.websocket_command({vol.Required("type"): "cloud/remove_data"})
331 @websocket_api.async_response
333  hass: HomeAssistant,
334  connection: websocket_api.ActiveConnection,
335  msg: dict[str, Any],
336 ) -> None:
337  """Handle request for account info.
338 
339  Async friendly.
340  """
341  cloud = hass.data[DATA_CLOUD]
342  if cloud.is_logged_in:
343  connection.send_message(
344  websocket_api.error_message(
345  msg["id"], "logged_in", "Can't remove data when logged in."
346  )
347  )
348  return
349 
350  await cloud.remove_data()
351  await cloud.client.prefs.async_erase_config()
352 
353  connection.send_message(websocket_api.result_message(msg["id"]))
354 
355 
356 @websocket_api.websocket_command({vol.Required("type"): "cloud/status"})
357 @websocket_api.async_response
358 async def websocket_cloud_status(
359  hass: HomeAssistant,
360  connection: websocket_api.ActiveConnection,
361  msg: dict[str, Any],
362 ) -> None:
363  """Handle request for account info.
364 
365  Async friendly.
366  """
367  cloud = hass.data[DATA_CLOUD]
368  connection.send_message(
369  websocket_api.result_message(msg["id"], await _account_data(hass, cloud))
370  )
371 
372 
374  handler: Callable[
375  [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
376  None,
377  ],
378 ) -> Callable[
379  [HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
380  None,
381 ]:
382  """Websocket decorator that requires cloud to be logged in."""
383 
384  @wraps(handler)
385  def with_cloud_auth(
386  hass: HomeAssistant,
387  connection: websocket_api.ActiveConnection,
388  msg: dict[str, Any],
389  ) -> None:
390  """Require to be logged into the cloud."""
391  cloud = hass.data[DATA_CLOUD]
392  if not cloud.is_logged_in:
393  connection.send_message(
394  websocket_api.error_message(
395  msg["id"], "not_logged_in", "You need to be logged in to the cloud."
396  )
397  )
398  return
399 
400  handler(hass, connection, msg)
401 
402  return with_cloud_auth
403 
404 
405 @_require_cloud_login
406 @websocket_api.websocket_command({vol.Required("type"): "cloud/subscription"})
407 @websocket_api.async_response
408 async def websocket_subscription(
409  hass: HomeAssistant,
410  connection: websocket_api.ActiveConnection,
411  msg: dict[str, Any],
412 ) -> None:
413  """Handle request for account info."""
414  cloud = hass.data[DATA_CLOUD]
415  if (data := await async_subscription_info(cloud)) is None:
416  connection.send_error(
417  msg["id"], "request_failed", "Failed to request subscription"
418  )
419  return
420 
421  connection.send_result(msg["id"], data)
423 
424 
425 def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
426  """Validate language and voice."""
427  language, voice = value
428  if language not in TTS_VOICES:
429  raise vol.Invalid(f"Invalid language {language}")
430  if voice not in TTS_VOICES[language]:
431  raise vol.Invalid(f"Invalid voice {voice} for language {language}")
432  return value
433 
434 
435 @_require_cloud_login
436 @websocket_api.websocket_command( { vol.Required("type"): "cloud/update_prefs",
437  vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
438  vol.Optional(PREF_ENABLE_ALEXA): bool,
439  vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool,
440  vol.Optional(PREF_ENABLE_GOOGLE): bool,
441  vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
442  vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
443  vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
444  vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
445  vol.Coerce(tuple), validate_language_voice
446  ),
447  }
448 )
449 @websocket_api.async_response
450 async def websocket_update_prefs(
451  hass: HomeAssistant,
452  connection: websocket_api.ActiveConnection,
453  msg: dict[str, Any],
454 ) -> None:
455  """Handle request for account info."""
456  cloud = hass.data[DATA_CLOUD]
457 
458  changes = dict(msg)
459  changes.pop("id")
460  changes.pop("type")
461 
462  # If we turn alexa linking on, validate that we can fetch access token
463  if changes.get(PREF_ALEXA_REPORT_STATE):
464  alexa_config = await cloud.client.get_alexa_config()
465  try:
466  async with asyncio.timeout(10):
467  await alexa_config.async_get_access_token()
468  except TimeoutError:
469  connection.send_error(
470  msg["id"], "alexa_timeout", "Timeout validating Alexa access token."
471  )
472  return
473  except (alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink):
474  connection.send_error(
475  msg["id"],
476  "alexa_relink",
477  (
478  "Please go to the Alexa app and re-link the Home Assistant "
479  "skill and then try to enable state reporting."
480  ),
481  )
482  await alexa_config.set_authorized(False)
483  return
484 
485  await alexa_config.set_authorized(True)
486 
487  await cloud.client.prefs.async_update(**changes)
488 
489  connection.send_message(websocket_api.result_message(msg["id"]))
490 
491 
492 @_require_cloud_login
493 @websocket_api.websocket_command( { vol.Required("type"): "cloud/cloudhook/create",
494  vol.Required("webhook_id"): str,
495  }
496 )
497 @websocket_api.async_response
498 @_ws_handle_cloud_errors
499 async def websocket_hook_create(
500  hass: HomeAssistant,
501  connection: websocket_api.ActiveConnection,
502  msg: dict[str, Any],
503 ) -> None:
504  """Handle request for account info."""
505  cloud = hass.data[DATA_CLOUD]
506  hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False)
507  connection.send_message(websocket_api.result_message(msg["id"], hook))
508 
509 
510 @_require_cloud_login
511 @websocket_api.websocket_command( { vol.Required("type"): "cloud/cloudhook/delete",
512  vol.Required("webhook_id"): str,
513  }
514 )
515 @websocket_api.async_response
516 @_ws_handle_cloud_errors
517 async def websocket_hook_delete(
518  hass: HomeAssistant,
519  connection: websocket_api.ActiveConnection,
520  msg: dict[str, Any],
521 ) -> None:
522  """Handle request for account info."""
523  cloud = hass.data[DATA_CLOUD]
524  await cloud.cloudhooks.async_delete(msg["webhook_id"])
525  connection.send_message(websocket_api.result_message(msg["id"]))
526 
527 
528 async def _account_data(
529  hass: HomeAssistant, cloud: Cloud[CloudClient]
530 ) -> dict[str, Any]:
531  """Generate the auth data JSON response."""
532 
533  assert hass.config.api
534  if not cloud.is_logged_in:
535  return {
536  "logged_in": False,
537  "cloud": STATE_DISCONNECTED,
538  "http_use_ssl": hass.config.api.use_ssl,
539  }
540 
541  claims = cloud.claims
542  client = cloud.client
543  remote = cloud.remote
544 
545  alexa_config = await client.get_alexa_config()
546  google_config = await client.get_google_config()
547 
548  # Load remote certificate
549  if remote.certificate:
550  certificate = attr.asdict(remote.certificate)
551  else:
552  certificate = None
553 
554  if cloud.iot.last_disconnect_reason:
555  cloud_last_disconnect_reason = dataclasses.asdict(
556  cloud.iot.last_disconnect_reason
557  )
558  else:
559  cloud_last_disconnect_reason = None
560 
561  return {
562  "alexa_entities": client.alexa_user_config["filter"].config,
563  "alexa_registered": alexa_config.authorized,
564  "cloud": cloud.iot.state,
565  "cloud_last_disconnect_reason": cloud_last_disconnect_reason,
566  "email": claims["email"],
567  "google_entities": client.google_user_config["filter"].config,
568  "google_registered": google_config.has_registered_user_agent,
569  "google_local_connected": google_config.is_local_connected,
570  "logged_in": True,
571  "prefs": client.prefs.as_dict(),
572  "remote_certificate": certificate,
573  "remote_certificate_status": remote.certificate_status,
574  "remote_connected": remote.is_connected,
575  "remote_domain": remote.instance_domain,
576  "http_use_ssl": hass.config.api.use_ssl,
577  "active_subscription": not cloud.subscription_expired,
578  }
579 
580 
581 @websocket_api.require_admin
582 @_require_cloud_login
583 @websocket_api.websocket_command({"type": "cloud/remote/connect"})
584 @websocket_api.async_response
585 @_ws_handle_cloud_errors
586 async def websocket_remote_connect(
587  hass: HomeAssistant,
588  connection: websocket_api.ActiveConnection,
589  msg: dict[str, Any],
590 ) -> None:
591  """Handle request for connect remote."""
592  cloud = hass.data[DATA_CLOUD]
593  await cloud.client.prefs.async_update(remote_enabled=True)
594  connection.send_result(msg["id"], await _account_data(hass, cloud))
595 
596 
597 @websocket_api.require_admin
598 @_require_cloud_login
599 @websocket_api.websocket_command({"type": "cloud/remote/disconnect"})
600 @websocket_api.async_response
601 @_ws_handle_cloud_errors
603  hass: HomeAssistant,
604  connection: websocket_api.ActiveConnection,
605  msg: dict[str, Any],
606 ) -> None:
607  """Handle request for disconnect remote."""
608  cloud = hass.data[DATA_CLOUD]
609  await cloud.client.prefs.async_update(remote_enabled=False)
610  connection.send_result(msg["id"], await _account_data(hass, cloud))
611 
612 
613 @websocket_api.require_admin
614 @_require_cloud_login
615 @websocket_api.websocket_command( { "type": "cloud/google_assistant/entities/get", "entity_id": str, } )
616 @websocket_api.async_response
617 @_ws_handle_cloud_errors
618 async def google_assistant_get(
619  hass: HomeAssistant,
620  connection: websocket_api.ActiveConnection,
621  msg: dict[str, Any],
622 ) -> None:
623  """Get data for a single google assistant entity."""
624  cloud = hass.data[DATA_CLOUD]
625  gconf = await cloud.client.get_google_config()
626  entity_id: str = msg["entity_id"]
627  state = hass.states.get(entity_id)
628 
629  if not state:
630  connection.send_error(
631  msg["id"],
632  websocket_api.ERR_NOT_FOUND,
633  f"{entity_id} unknown",
634  )
635  return
636 
637  entity = google_helpers.GoogleEntity(hass, gconf, state)
638  if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported():
639  connection.send_error(
640  msg["id"],
641  websocket_api.ERR_NOT_SUPPORTED,
642  f"{entity_id} not supported by Google assistant",
643  )
644  return
645 
646  assistant_options: Mapping[str, Any] = {}
647  with suppress(HomeAssistantError, KeyError):
648  settings = exposed_entities.async_get_entity_settings(hass, entity_id)
649  assistant_options = settings[CLOUD_GOOGLE]
650 
651  result = {
652  "entity_id": entity.entity_id,
653  "traits": [trait.name for trait in entity.traits()],
654  "might_2fa": entity.might_2fa_traits(),
655  PREF_DISABLE_2FA: assistant_options.get(PREF_DISABLE_2FA),
656  }
657 
658  connection.send_result(msg["id"], result)
659 
660 
661 @websocket_api.require_admin
662 @_require_cloud_login
663 @websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
664 @websocket_api.async_response
665 @_ws_handle_cloud_errors
666 async def google_assistant_list(
667  hass: HomeAssistant,
668  connection: websocket_api.ActiveConnection,
669  msg: dict[str, Any],
670 ) -> None:
671  """List all google assistant entities."""
672  cloud = hass.data[DATA_CLOUD]
673  gconf = await cloud.client.get_google_config()
674  entities = google_helpers.async_get_entities(hass, gconf)
675 
676  result = [
677  {
678  "entity_id": entity.entity_id,
679  "traits": [trait.name for trait in entity.traits()],
680  "might_2fa": entity.might_2fa_traits(),
681  }
682  for entity in entities
683  ]
684 
685  connection.send_result(msg["id"], result)
686 
687 
688 @websocket_api.require_admin
689 @_require_cloud_login
690 @websocket_api.websocket_command( { "type": "cloud/google_assistant/entities/update", "entity_id": str, vol.Optional(PREF_DISABLE_2FA): bool,
691  }
692 )
693 @websocket_api.async_response
694 @_ws_handle_cloud_errors
695 async def google_assistant_update(
696  hass: HomeAssistant,
697  connection: websocket_api.ActiveConnection,
698  msg: dict[str, Any],
699 ) -> None:
700  """Update google assistant entity config."""
701  entity_id: str = msg["entity_id"]
702 
703  assistant_options: Mapping[str, Any] = {}
704  with suppress(HomeAssistantError, KeyError):
705  settings = exposed_entities.async_get_entity_settings(hass, entity_id)
706  assistant_options = settings[CLOUD_GOOGLE]
707 
708  disable_2fa = msg[PREF_DISABLE_2FA]
709  if assistant_options.get(PREF_DISABLE_2FA) == disable_2fa:
710  return
711 
712  exposed_entities.async_set_assistant_option(
713  hass, CLOUD_GOOGLE, entity_id, PREF_DISABLE_2FA, disable_2fa
714  )
715  connection.send_result(msg["id"])
716 
717 
718 @websocket_api.require_admin
719 @_require_cloud_login
720 @websocket_api.websocket_command( { "type": "cloud/alexa/entities/get", "entity_id": str, } )
721 @websocket_api.async_response
722 @_ws_handle_cloud_errors
723 async def alexa_get(
724  hass: HomeAssistant,
725  connection: websocket_api.ActiveConnection,
726  msg: dict[str, Any],
727 ) -> None:
728  """Get data for a single alexa entity."""
729  entity_id: str = msg["entity_id"]
730 
731  if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
732  hass, entity_id
733  ):
734  connection.send_error(
735  msg["id"],
736  websocket_api.ERR_NOT_SUPPORTED,
737  f"{entity_id} not supported by Alexa",
738  )
739  return
740 
741  connection.send_result(msg["id"])
742 
743 
744 @websocket_api.require_admin
745 @_require_cloud_login
746 @websocket_api.websocket_command({"type": "cloud/alexa/entities"})
747 @websocket_api.async_response
748 @_ws_handle_cloud_errors
749 async def alexa_list(
750  hass: HomeAssistant,
751  connection: websocket_api.ActiveConnection,
752  msg: dict[str, Any],
753 ) -> None:
754  """List all alexa entities."""
755  cloud = hass.data[DATA_CLOUD]
756  alexa_config = await cloud.client.get_alexa_config()
757  entities = alexa_entities.async_get_entities(hass, alexa_config)
758 
759  result = [
760  {
761  "entity_id": entity.entity_id,
762  "display_categories": entity.default_display_categories(),
763  "interfaces": [ifc.name() for ifc in entity.interfaces()],
764  }
765  for entity in entities
766  ]
767 
768  connection.send_result(msg["id"], result)
769 
770 
771 @websocket_api.require_admin
772 @_require_cloud_login
773 @websocket_api.websocket_command({"type": "cloud/alexa/sync"})
774 @websocket_api.async_response
775 async def alexa_sync(
776  hass: HomeAssistant,
777  connection: websocket_api.ActiveConnection,
778  msg: dict[str, Any],
779 ) -> None:
780  """Sync with Alexa."""
781  cloud = hass.data[DATA_CLOUD]
782  alexa_config = await cloud.client.get_alexa_config()
783 
784  async with asyncio.timeout(10):
785  try:
786  success = await alexa_config.async_sync_entities()
787  except alexa_errors.NoTokenAvailable:
788  connection.send_error(
789  msg["id"],
790  "alexa_relink",
791  "Please go to the Alexa app and re-link the Home Assistant skill.",
792  )
793  return
794 
795  if success:
796  connection.send_result(msg["id"])
797  else:
798  connection.send_error(
799  msg["id"], websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"
800  )
801 
802 
803 @websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
804 @websocket_api.async_response
805 async def thingtalk_convert(
806  hass: HomeAssistant,
807  connection: websocket_api.ActiveConnection,
808  msg: dict[str, Any],
809 ) -> None:
810  """Convert a query."""
811  cloud = hass.data[DATA_CLOUD]
812 
813  async with asyncio.timeout(10):
814  try:
815  connection.send_result(
816  msg["id"], await thingtalk.async_convert(cloud, msg["query"])
817  )
818  except thingtalk.ThingTalkConversionError as err:
819  connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
820 
821 
822 @websocket_api.websocket_command({"type": "cloud/tts/info"})
823 def tts_info(
824  hass: HomeAssistant,
825  connection: websocket_api.ActiveConnection,
826  msg: dict[str, Any],
827 ) -> None:
828  """Fetch available tts info."""
829  connection.send_result(
830  msg["id"],
831  {
832  "languages": [
833  (language, voice)
834  for language, voices in TTS_VOICES.items()
835  for voice in voices
836  ]
837  },
838  )
839 
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: http_api.py:322
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: http_api.py:216
web.Response post(self, web.Request request)
Definition: http_api.py:238
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: http_api.py:262
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: http_api.py:302
web.Response post(self, web.Request request)
Definition: http_api.py:197
str|None async_create_cloud_pipeline(HomeAssistant hass)
None tts_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:851
tuple[HTTPStatus, str] _process_cloud_exception(Exception exc, str where)
Definition: http_api.py:173
None websocket_cloud_status(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:366
None thingtalk_convert(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:833
Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], None,] _require_cloud_login(Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], None,] handler)
Definition: http_api.py:385
None alexa_list(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:777
Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], Coroutine[None, None, None],] _ws_handle_cloud_errors(Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], Coroutine[None, None, None],] handler)
Definition: http_api.py:153
None websocket_hook_delete(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:531
tuple[str, str] validate_language_voice(tuple[str, str] value)
Definition: http_api.py:429
None websocket_hook_create(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:511
None websocket_subscription(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:416
None google_assistant_list(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:685
None alexa_get(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:751
None websocket_cloud_remove_data(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:340
None async_setup(HomeAssistant hass)
Definition: http_api.py:73
dict[str, Any] _account_data(HomeAssistant hass, Cloud[CloudClient] cloud)
Definition: http_api.py:540
None google_assistant_get(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:637
None google_assistant_update(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:718
None websocket_remote_connect(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:600
None websocket_update_prefs(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:460
None alexa_sync(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:803
None websocket_remote_disconnect(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: http_api.py:616
None async_manage_legacy_subscription_issue(HomeAssistant hass, dict[str, Any] subscription_info)
Definition: repairs.py:30
dict[str, Any]|None async_subscription_info(Cloud[CloudClient] cloud)
Definition: subscription.py:18
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
LocationInfo|None async_detect_location_info(aiohttp.ClientSession session)
Definition: location.py:51