Home Assistant Unofficial Reference 2024.12.1
smart_home.py
Go to the documentation of this file.
1 """Support for alexa Smart Home Skill API."""
2 
3 import logging
4 from typing import Any
5 
6 from aiohttp import web
7 from yarl import URL
8 
9 from homeassistant import core
10 from homeassistant.auth.models import User
12  KEY_HASS,
13  HomeAssistantRequest,
14  HomeAssistantView,
15 )
16 from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
17 from homeassistant.core import Context, HomeAssistant
18 from homeassistant.helpers import entity_registry as er
19 from homeassistant.helpers.typing import ConfigType
20 
21 from .auth import Auth
22 from .config import AbstractConfig
23 from .const import (
24  API_DIRECTIVE,
25  API_HEADER,
26  CONF_ENDPOINT,
27  CONF_ENTITY_CONFIG,
28  CONF_FILTER,
29  CONF_LOCALE,
30  EVENT_ALEXA_SMART_HOME,
31 )
32 from .diagnostics import async_redact_auth_data
33 from .errors import AlexaBridgeUnreachableError, AlexaError
34 from .handlers import HANDLERS
35 from .state_report import AlexaDirective
36 
37 _LOGGER = logging.getLogger(__name__)
38 SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
39 
40 
42  """Alexa config."""
43 
44  _auth: Auth | None
45 
46  def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
47  """Initialize Alexa config."""
48  super().__init__(hass)
49  self._config_config = config
50 
51  if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
52  self._auth_auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET])
53  else:
54  self._auth_auth = None
55 
56  @property
57  def supports_auth(self) -> bool:
58  """Return if config supports auth."""
59  return self._auth_auth is not None
60 
61  @property
62  def should_report_state(self) -> bool:
63  """Return if we should proactively report states."""
64  return self._auth_auth is not None and self.authorizedauthorized
65 
66  @property
67  def endpoint(self) -> str | URL | None:
68  """Endpoint for report state."""
69  return self._config_config.get(CONF_ENDPOINT)
70 
71  @property
72  def entity_config(self) -> dict[str, Any]:
73  """Return entity config."""
74  return self._config_config.get(CONF_ENTITY_CONFIG) or {}
75 
76  @property
77  def locale(self) -> str | None:
78  """Return config locale."""
79  return self._config_config.get(CONF_LOCALE)
80 
81  @core.callback
82  def user_identifier(self) -> str:
83  """Return an identifier for the user that represents this config."""
84  return ""
85 
86  @core.callback
87  def should_expose(self, entity_id: str) -> bool:
88  """If an entity should be exposed."""
89  if not self._config_config[CONF_FILTER].empty_filter:
90  return bool(self._config_config[CONF_FILTER](entity_id))
91 
92  entity_registry = er.async_get(self.hasshass)
93  if registry_entry := entity_registry.async_get(entity_id):
94  auxiliary_entity = (
95  registry_entry.entity_category is not None
96  or registry_entry.hidden_by is not None
97  )
98  else:
99  auxiliary_entity = False
100  return not auxiliary_entity
101 
102  @core.callback
103  def async_invalidate_access_token(self) -> None:
104  """Invalidate access token."""
105  assert self._auth_auth is not None
107 
108  async def async_get_access_token(self) -> str | None:
109  """Get an access token."""
110  assert self._auth_auth is not None
111  return await self._auth_auth.async_get_access_token()
112 
113  async def async_accept_grant(self, code: str) -> str | None:
114  """Accept a grant."""
115  assert self._auth_auth is not None
116  return await self._auth_auth.async_do_auth(code)
117 
118 
119 async def async_setup(hass: HomeAssistant, config: ConfigType) -> None:
120  """Activate Smart Home functionality of Alexa component.
121 
122  This is optional, triggered by having a `smart_home:` sub-section in the
123  alexa configuration.
124 
125  Even if that's disabled, the functionality in this module may still be used
126  by the cloud component which will call async_handle_message directly.
127  """
128  smart_home_config = AlexaConfig(hass, config)
129  await smart_home_config.async_initialize()
130  hass.http.register_view(SmartHomeView(smart_home_config))
131 
132  if smart_home_config.should_report_state:
133  await smart_home_config.async_enable_proactive_mode()
134 
135 
136 class SmartHomeView(HomeAssistantView):
137  """Expose Smart Home v3 payload interface via HTTP POST."""
138 
139  url = SMART_HOME_HTTP_ENDPOINT
140  name = "api:alexa:smart_home"
141 
142  def __init__(self, smart_home_config: AlexaConfig) -> None:
143  """Initialize."""
144  self.smart_home_configsmart_home_config = smart_home_config
145 
146  async def post(self, request: HomeAssistantRequest) -> web.Response | bytes:
147  """Handle Alexa Smart Home requests.
148 
149  The Smart Home API requires the endpoint to be implemented in AWS
150  Lambda, which will need to forward the requests to here and pass back
151  the response.
152  """
153  hass = request.app[KEY_HASS]
154  user: User = request["hass_user"]
155  message: dict[str, Any] = await request.json()
156 
157  if _LOGGER.isEnabledFor(logging.DEBUG):
158  _LOGGER.debug(
159  "Received Alexa Smart Home request: %s",
160  async_redact_auth_data(message),
161  )
162 
163  response = await async_handle_message(
164  hass, self.smart_home_configsmart_home_config, message, context=core.Context(user_id=user.id)
165  )
166  if _LOGGER.isEnabledFor(logging.DEBUG):
167  _LOGGER.debug(
168  "Sending Alexa Smart Home response: %s",
169  async_redact_auth_data(response),
170  )
171 
172  return b"" if response is None else self.json(response)
173 
174 
176  hass: HomeAssistant,
177  config: AbstractConfig,
178  request: dict[str, Any],
179  context: Context | None = None,
180  enabled: bool = True,
181 ) -> dict[str, Any]:
182  """Handle incoming API messages.
183 
184  If enabled is False, the response to all messages will be a
185  BRIDGE_UNREACHABLE error. This can be used if the API has been disabled in
186  configuration.
187  """
188  assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3"
189 
190  if context is None:
191  context = Context()
192 
193  directive = AlexaDirective(request)
194 
195  try:
196  if not enabled:
197  raise AlexaBridgeUnreachableError( # noqa: TRY301
198  "Alexa API not enabled in Home Assistant configuration"
199  )
200 
201  await config.set_authorized(True)
202 
203  if directive.has_endpoint:
204  directive.load_entity(hass, config)
205 
206  funct_ref = HANDLERS.get((directive.namespace, directive.name))
207  if funct_ref:
208  response = await funct_ref(hass, config, directive, context)
209  if directive.has_endpoint:
210  response.merge_context_properties(directive.endpoint)
211  else:
212  _LOGGER.warning(
213  "Unsupported API request %s/%s", directive.namespace, directive.name
214  )
215  response = directive.error()
216  except AlexaError as err:
217  response = directive.error(
218  error_type=str(err.error_type),
219  error_message=err.error_message,
220  payload=err.payload,
221  )
222  except Exception:
223  _LOGGER.exception(
224  "Uncaught exception processing Alexa %s/%s request (%s)",
225  directive.namespace,
226  directive.name,
227  directive.entity_id or "-",
228  )
229  response = directive.error(error_message="Unknown error")
230 
231  request_info: dict[str, Any] = {
232  "namespace": directive.namespace,
233  "name": directive.name,
234  }
235 
236  if directive.has_endpoint:
237  assert directive.entity_id is not None
238  request_info["entity_id"] = directive.entity_id
239 
240  hass.bus.async_fire(
241  EVENT_ALEXA_SMART_HOME,
242  {
243  "request": request_info,
244  "response": {"namespace": response.namespace, "name": response.name},
245  },
246  context=context,
247  )
248 
249  return response.serialize()
None __init__(self, HomeAssistant hass, ConfigType config)
Definition: smart_home.py:46
None __init__(self, AlexaConfig smart_home_config)
Definition: smart_home.py:142
web.Response|bytes post(self, HomeAssistantRequest request)
Definition: smart_home.py:146
dict[str, str] async_redact_auth_data(Mapping[Any, Any] mapping)
Definition: diagnostics.py:32
dict[str, Any] async_handle_message(HomeAssistant hass, AbstractConfig config, dict[str, Any] request, Context|None context=None, bool enabled=True)
Definition: smart_home.py:181
None async_setup(HomeAssistant hass, ConfigType config)
Definition: smart_home.py:119
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88