Home Assistant Unofficial Reference 2024.12.1
smart_home.py
Go to the documentation of this file.
1 """Support for Google Assistant Smart Home API."""
2 
3 import asyncio
4 from collections.abc import Callable, Coroutine
5 from itertools import product
6 import logging
7 import pprint
8 from typing import Any
9 
10 from homeassistant.const import ATTR_ENTITY_ID, __version__
11 from homeassistant.core import HomeAssistant
12 from homeassistant.helpers import instance_id
13 from homeassistant.util.decorator import Registry
14 
15 from .const import (
16  ERR_DEVICE_OFFLINE,
17  ERR_PROTOCOL_ERROR,
18  ERR_UNKNOWN_ERROR,
19  EVENT_COMMAND_RECEIVED,
20  EVENT_QUERY_RECEIVED,
21  EVENT_SYNC_RECEIVED,
22 )
23 from .data_redaction import async_redact_msg
24 from .error import SmartHomeError
25 from .helpers import GoogleEntity, RequestData, async_get_entities
26 
27 EXECUTE_LIMIT = 2 # Wait 2 seconds for execute to finish
28 
29 HANDLERS: Registry[
30  str,
31  Callable[
32  [HomeAssistant, RequestData, dict[str, Any]],
33  Coroutine[Any, Any, dict[str, Any] | None],
34  ],
35 ] = Registry()
36 _LOGGER = logging.getLogger(__name__)
37 
38 
40  hass, config, agent_user_id, local_user_id, message, source
41 ):
42  """Handle incoming API messages."""
43  if _LOGGER.isEnabledFor(logging.DEBUG):
44  _LOGGER.debug(
45  "Processing message:\n%s",
46  pprint.pformat(async_redact_msg(message, agent_user_id)),
47  )
48 
49  data = RequestData(
50  config, local_user_id, source, message["requestId"], message.get("devices")
51  )
52 
53  response = await _process(hass, data, message)
54  if _LOGGER.isEnabledFor(logging.DEBUG):
55  if response:
56  _LOGGER.debug(
57  "Response:\n%s",
58  pprint.pformat(async_redact_msg(response["payload"], agent_user_id)),
59  )
60  else:
61  _LOGGER.debug("Empty response")
62 
63  if response and "errorCode" in response["payload"]:
64  _LOGGER.error(
65  "Error handling message\n:%s\nResponse:\n%s",
66  pprint.pformat(async_redact_msg(message, agent_user_id)),
67  pprint.pformat(async_redact_msg(response["payload"], agent_user_id)),
68  )
69 
70  return response
71 
72 
73 async def _process(hass, data, message):
74  """Process a message."""
75  inputs: list = message.get("inputs")
76 
77  if len(inputs) != 1:
78  return {
79  "requestId": data.request_id,
80  "payload": {"errorCode": ERR_PROTOCOL_ERROR},
81  }
82 
83  if (handler := HANDLERS.get(inputs[0].get("intent"))) is None:
84  return {
85  "requestId": data.request_id,
86  "payload": {"errorCode": ERR_PROTOCOL_ERROR},
87  }
88 
89  try:
90  result = await handler(hass, data, inputs[0].get("payload"))
91  except SmartHomeError as err:
92  return {"requestId": data.request_id, "payload": {"errorCode": err.code}}
93  except Exception:
94  _LOGGER.exception("Unexpected error")
95  return {
96  "requestId": data.request_id,
97  "payload": {"errorCode": ERR_UNKNOWN_ERROR},
98  }
99 
100  if result is None:
101  return None
102 
103  return {"requestId": data.request_id, "payload": result}
104 
105 
106 async def async_devices_sync_response(hass, config, agent_user_id):
107  """Generate the device serialization."""
108  entities = async_get_entities(hass, config)
109  instance_uuid = await instance_id.async_get(hass)
110  devices = []
111 
112  for entity in entities:
113  if not entity.should_expose():
114  continue
115 
116  try:
117  devices.append(entity.sync_serialize(agent_user_id, instance_uuid))
118  except Exception:
119  _LOGGER.exception("Error serializing %s", entity.entity_id)
120 
121  return devices
122 
123 
124 @HANDLERS.register("action.devices.SYNC")
126  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
127 ) -> dict[str, Any]:
128  """Handle action.devices.SYNC request.
129 
130  https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC
131  """
132  hass.bus.async_fire(
133  EVENT_SYNC_RECEIVED,
134  {"request_id": data.request_id, "source": data.source},
135  context=data.context,
136  )
137 
138  agent_user_id = data.config.get_agent_user_id_from_context(data.context)
139  await data.config.async_connect_agent_user(agent_user_id)
140 
141  devices = await async_devices_sync_response(hass, data.config, agent_user_id)
142  return create_sync_response(agent_user_id, devices)
143 
144 
145 @HANDLERS.register("action.devices.QUERY")
147  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
148 ) -> dict[str, Any]:
149  """Handle action.devices.QUERY request.
150 
151  https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY
152  """
153  payload_devices = payload.get("devices", [])
154 
155  hass.bus.async_fire(
156  EVENT_QUERY_RECEIVED,
157  {
158  "request_id": data.request_id,
159  ATTR_ENTITY_ID: [device["id"] for device in payload_devices],
160  "source": data.source,
161  },
162  context=data.context,
163  )
164 
165  return await async_devices_query_response(hass, data.config, payload_devices)
166 
167 
168 async def async_devices_query_response(hass, config, payload_devices):
169  """Generate the device serialization."""
170  devices = {}
171  for device in payload_devices:
172  devid = device["id"]
173 
174  if not (state := hass.states.get(devid)):
175  # If we can't find a state, the device is offline
176  devices[devid] = {"online": False}
177  continue
178 
179  entity = GoogleEntity(hass, config, state)
180  try:
181  devices[devid] = entity.query_serialize()
182  except Exception:
183  _LOGGER.exception("Unexpected error serializing query for %s", state)
184  devices[devid] = {"online": False}
185 
186  return {"devices": devices}
187 
188 
189 async def _entity_execute(entity, data, executions):
190  """Execute all commands for an entity.
191 
192  Returns a dict if a special result needs to be set.
193  """
194  for execution in executions:
195  try:
196  await entity.execute(data, execution)
197  except SmartHomeError as err:
198  return {
199  "ids": [entity.entity_id],
200  "status": "ERROR",
201  **err.to_response(),
202  }
203 
204  return None
205 
206 
207 @HANDLERS.register("action.devices.EXECUTE")
209  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
210 ) -> dict[str, Any]:
211  """Handle action.devices.EXECUTE request.
212 
213  https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE
214  """
215  entities: dict[str, GoogleEntity] = {}
216  executions: dict[str, list[Any]] = {}
217  results: dict[str, dict[str, Any]] = {}
218 
219  for command in payload["commands"]:
220  hass.bus.async_fire(
221  EVENT_COMMAND_RECEIVED,
222  {
223  "request_id": data.request_id,
224  ATTR_ENTITY_ID: [device["id"] for device in command["devices"]],
225  "execution": command["execution"],
226  "source": data.source,
227  },
228  context=data.context,
229  )
230 
231  for device, execution in product(command["devices"], command["execution"]):
232  entity_id = device["id"]
233 
234  # Happens if error occurred. Skip entity for further processing
235  if entity_id in results:
236  continue
237 
238  if entity_id in entities:
239  executions[entity_id].append(execution)
240  continue
241 
242  if (state := hass.states.get(entity_id)) is None:
243  results[entity_id] = {
244  "ids": [entity_id],
245  "status": "ERROR",
246  "errorCode": ERR_DEVICE_OFFLINE,
247  }
248  continue
249 
250  entities[entity_id] = GoogleEntity(hass, data.config, state)
251  executions[entity_id] = [execution]
252 
253  try:
254  execute_results = await asyncio.wait_for(
255  asyncio.shield(
256  asyncio.gather(
257  *(
258  _entity_execute(entities[entity_id], data, execution)
259  for entity_id, execution in executions.items()
260  )
261  )
262  ),
263  EXECUTE_LIMIT,
264  )
265  results.update(
266  {
267  entity_id: result
268  for entity_id, result in zip(executions, execute_results, strict=False)
269  if result is not None
270  }
271  )
272  except TimeoutError:
273  pass
274 
275  final_results = list(results.values())
276 
277  for entity in entities.values():
278  if entity.entity_id in results:
279  continue
280 
281  entity.async_update()
282 
283  final_results.append(
284  {
285  "ids": [entity.entity_id],
286  "status": "SUCCESS",
287  "states": entity.query_serialize(),
288  }
289  )
290 
291  return {"commands": final_results}
292 
293 
294 @HANDLERS.register("action.devices.DISCONNECT")
296  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
297 ) -> None:
298  """Handle action.devices.DISCONNECT request.
299 
300  https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT
301  """
302  assert data.context.user_id is not None
303  await data.config.async_disconnect_agent_user(data.context.user_id)
304 
305 
306 @HANDLERS.register("action.devices.IDENTIFY")
308  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
309 ) -> dict[str, Any]:
310  """Handle action.devices.IDENTIFY request.
311 
312  https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler
313  """
314  return {
315  "device": {
316  "id": data.config.get_agent_user_id_from_context(data.context),
317  "isLocalOnly": True,
318  "isProxy": True,
319  "deviceInfo": {
320  "hwVersion": "UNKNOWN_HW_VERSION",
321  "manufacturer": "Home Assistant",
322  "model": "Home Assistant",
323  "swVersion": __version__,
324  },
325  }
326  }
327 
328 
329 @HANDLERS.register("action.devices.REACHABLE_DEVICES")
331  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
332 ) -> dict[str, Any]:
333  """Handle action.devices.REACHABLE_DEVICES request.
334 
335  https://developers.google.com/assistant/smarthome/develop/local#implement_the_reachable_devices_handler_hub_integrations_only
336  """
337  google_ids = {dev["id"] for dev in (data.devices or [])}
338 
339  return {
340  "devices": [
341  entity.reachable_device_serialize()
342  for entity in async_get_entities(hass, data.config)
343  if entity.entity_id in google_ids and entity.should_expose_local()
344  ]
345  }
346 
347 
348 @HANDLERS.register("action.devices.PROXY_SELECTED")
350  hass: HomeAssistant, data: RequestData, payload: dict[str, Any]
351 ) -> dict[str, Any]:
352  """Handle action.devices.PROXY_SELECTED request.
353 
354  When selected for local SDK.
355  """
356  return {}
357 
358 
359 def create_sync_response(agent_user_id: str, devices: list):
360  """Return an empty sync response."""
361  return {
362  "agentUserId": agent_user_id,
363  "devices": devices,
364  }
365 
366 
367 def api_disabled_response(message, agent_user_id):
368  """Return a device turned off response."""
369  inputs: list = message.get("inputs")
370 
371  if inputs and inputs[0].get("intent") == "action.devices.SYNC":
372  payload = create_sync_response(agent_user_id, [])
373  else:
374  payload = {"errorCode": "deviceTurnedOff"}
375 
376  return {
377  "requestId": message.get("requestId"),
378  "payload": payload,
379  }
list[AlexaEntity] async_get_entities(HomeAssistant hass, AbstractConfig config)
Definition: entities.py:374
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, Any] async_redact_msg(dict[str, Any] msg, str agent_user_id)
def async_devices_sync_response(hass, config, agent_user_id)
Definition: smart_home.py:106
dict[str, Any] async_devices_query(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:148
def create_sync_response(str agent_user_id, list devices)
Definition: smart_home.py:359
dict[str, Any] async_devices_sync(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:127
dict[str, Any] async_devices_proxy_selected(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:351
dict[str, Any] async_devices_reachable(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:332
dict[str, Any] handle_devices_execute(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:210
def async_devices_query_response(hass, config, payload_devices)
Definition: smart_home.py:168
def api_disabled_response(message, agent_user_id)
Definition: smart_home.py:367
dict[str, Any] async_devices_identify(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:309
None async_devices_disconnect(HomeAssistant hass, RequestData data, dict[str, Any] payload)
Definition: smart_home.py:297
def _entity_execute(entity, data, executions)
Definition: smart_home.py:189
def async_handle_message(hass, config, agent_user_id, local_user_id, message, source)
Definition: smart_home.py:41