Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Intent integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, Protocol
7 
8 from aiohttp import web
9 import voluptuous as vol
10 
11 from homeassistant.components import http
13  ATTR_POSITION,
14  DOMAIN as COVER_DOMAIN,
15  SERVICE_CLOSE_COVER,
16  SERVICE_OPEN_COVER,
17  SERVICE_SET_COVER_POSITION,
18  CoverDeviceClass,
19 )
20 from homeassistant.components.http.data_validator import RequestDataValidator
22  DOMAIN as LOCK_DOMAIN,
23  SERVICE_LOCK,
24  SERVICE_UNLOCK,
25 )
26 from homeassistant.components.media_player import MediaPlayerDeviceClass
27 from homeassistant.components.switch import SwitchDeviceClass
29  DOMAIN as VALVE_DOMAIN,
30  SERVICE_CLOSE_VALVE,
31  SERVICE_OPEN_VALVE,
32  SERVICE_SET_VALVE_POSITION,
33  ValveDeviceClass,
34 )
35 from homeassistant.const import (
36  ATTR_ENTITY_ID,
37  SERVICE_TOGGLE,
38  SERVICE_TURN_OFF,
39  SERVICE_TURN_ON,
40 )
41 from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
42 from homeassistant.helpers import config_validation as cv, integration_platform, intent
43 from homeassistant.helpers.typing import ConfigType
44 from homeassistant.util import dt as dt_util
45 
46 from .const import DOMAIN, TIMER_DATA
47 from .timers import (
48  CancelAllTimersIntentHandler,
49  CancelTimerIntentHandler,
50  DecreaseTimerIntentHandler,
51  IncreaseTimerIntentHandler,
52  PauseTimerIntentHandler,
53  StartTimerIntentHandler,
54  TimerEventType,
55  TimerInfo,
56  TimerManager,
57  TimerStatusIntentHandler,
58  UnpauseTimerIntentHandler,
59  async_device_supports_timers,
60  async_register_timer_handler,
61 )
62 
63 _LOGGER = logging.getLogger(__name__)
64 
65 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
66 
67 __all__ = [
68  "async_register_timer_handler",
69  "async_device_supports_timers",
70  "TimerInfo",
71  "TimerEventType",
72  "DOMAIN",
73 ]
74 
75 ONOFF_DEVICE_CLASSES = {
76  CoverDeviceClass,
77  ValveDeviceClass,
78  SwitchDeviceClass,
79  MediaPlayerDeviceClass,
80 }
81 
82 
83 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
84  """Set up the Intent component."""
85  hass.data[TIMER_DATA] = TimerManager(hass)
86 
87  hass.http.register_view(IntentHandleView())
88 
89  await integration_platform.async_process_integration_platforms(
90  hass, DOMAIN, _async_process_intent
91  )
92 
93  intent.async_register(
94  hass,
96  intent.INTENT_TURN_ON,
97  HOMEASSISTANT_DOMAIN,
98  SERVICE_TURN_ON,
99  description="Turns on/opens a device or entity",
100  device_classes=ONOFF_DEVICE_CLASSES,
101  ),
102  )
103  intent.async_register(
104  hass,
106  intent.INTENT_TURN_OFF,
107  HOMEASSISTANT_DOMAIN,
108  SERVICE_TURN_OFF,
109  description="Turns off/closes a device or entity",
110  device_classes=ONOFF_DEVICE_CLASSES,
111  ),
112  )
113  intent.async_register(
114  hass,
115  intent.ServiceIntentHandler(
116  intent.INTENT_TOGGLE,
117  HOMEASSISTANT_DOMAIN,
118  SERVICE_TOGGLE,
119  description="Toggles a device or entity",
120  device_classes=ONOFF_DEVICE_CLASSES,
121  ),
122  )
123  intent.async_register(
124  hass,
126  )
127  intent.async_register(
128  hass,
130  )
131  intent.async_register(hass, SetPositionIntentHandler())
132  intent.async_register(hass, StartTimerIntentHandler())
133  intent.async_register(hass, CancelTimerIntentHandler())
134  intent.async_register(hass, CancelAllTimersIntentHandler())
135  intent.async_register(hass, IncreaseTimerIntentHandler())
136  intent.async_register(hass, DecreaseTimerIntentHandler())
137  intent.async_register(hass, PauseTimerIntentHandler())
138  intent.async_register(hass, UnpauseTimerIntentHandler())
139  intent.async_register(hass, TimerStatusIntentHandler())
140  intent.async_register(hass, GetCurrentDateIntentHandler())
141  intent.async_register(hass, GetCurrentTimeIntentHandler())
142  intent.async_register(hass, HelloIntentHandler())
143 
144  return True
145 
146 
147 class IntentPlatformProtocol(Protocol):
148  """Define the format that intent platforms can have."""
149 
150  async def async_setup_intents(self, hass: HomeAssistant) -> None:
151  """Set up platform intents."""
152 
153 
154 class OnOffIntentHandler(intent.ServiceIntentHandler):
155  """Intent handler for on/off that also supports covers, valves, locks, etc."""
156 
158  self, domain: str, service: str, intent_obj: intent.Intent, state: State
159  ) -> None:
160  """Call service on entity with handling for special cases."""
161  hass = intent_obj.hass
162 
163  if state.domain == COVER_DOMAIN:
164  # on = open
165  # off = close
166  if service == SERVICE_TURN_ON:
167  service_name = SERVICE_OPEN_COVER
168  else:
169  service_name = SERVICE_CLOSE_COVER
170 
171  await self._run_then_background(
172  hass.async_create_task(
173  hass.services.async_call(
174  COVER_DOMAIN,
175  service_name,
176  {ATTR_ENTITY_ID: state.entity_id},
177  context=intent_obj.context,
178  blocking=True,
179  )
180  )
181  )
182  return
183 
184  if state.domain == LOCK_DOMAIN:
185  # on = lock
186  # off = unlock
187  if service == SERVICE_TURN_ON:
188  service_name = SERVICE_LOCK
189  else:
190  service_name = SERVICE_UNLOCK
191 
192  await self._run_then_background(
193  hass.async_create_task(
194  hass.services.async_call(
195  LOCK_DOMAIN,
196  service_name,
197  {ATTR_ENTITY_ID: state.entity_id},
198  context=intent_obj.context,
199  blocking=True,
200  )
201  )
202  )
203  return
204 
205  if state.domain == VALVE_DOMAIN:
206  # on = opened
207  # off = closed
208  if service == SERVICE_TURN_ON:
209  service_name = SERVICE_OPEN_VALVE
210  else:
211  service_name = SERVICE_CLOSE_VALVE
212 
213  await self._run_then_background(
214  hass.async_create_task(
215  hass.services.async_call(
216  VALVE_DOMAIN,
217  service_name,
218  {ATTR_ENTITY_ID: state.entity_id},
219  context=intent_obj.context,
220  blocking=True,
221  )
222  )
223  )
224  return
225 
226  if not hass.services.has_service(state.domain, service):
227  raise intent.IntentHandleError(
228  f"Service {service} does not support entity {state.entity_id}"
229  )
230 
231  # Fall back to homeassistant.turn_on/off
232  await super().async_call_service(domain, service, intent_obj, state)
233 
234 
235 class GetStateIntentHandler(intent.IntentHandler):
236  """Answer questions about entity states."""
237 
238  intent_type = intent.INTENT_GET_STATE
239  description = "Gets or checks the state of a device or entity"
240  slot_schema = {
241  vol.Any("name", "area", "floor"): cv.string,
242  vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]),
243  vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]),
244  vol.Optional("state"): vol.All(cv.ensure_list, [cv.string]),
245  vol.Optional("preferred_area_id"): cv.string,
246  vol.Optional("preferred_floor_id"): cv.string,
247  }
248 
249  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
250  """Handle the hass intent."""
251  hass = intent_obj.hass
252  slots = self.async_validate_slots(intent_obj.slots)
253 
254  # Entity name to match
255  name_slot = slots.get("name", {})
256  entity_name: str | None = name_slot.get("value")
257 
258  # Get area/floor info
259  area_slot = slots.get("area", {})
260  area_id = area_slot.get("value")
261 
262  floor_slot = slots.get("floor", {})
263  floor_id = floor_slot.get("value")
264 
265  # Optional domain/device class filters.
266  # Convert to sets for speed.
267  domains: set[str] | None = None
268  device_classes: set[str] | None = None
269 
270  if "domain" in slots:
271  domains = set(slots["domain"]["value"])
272 
273  if "device_class" in slots:
274  device_classes = set(slots["device_class"]["value"])
275 
276  state_names: set[str] | None = None
277  if "state" in slots:
278  state_names = set(slots["state"]["value"])
279 
280  match_constraints = intent.MatchTargetsConstraints(
281  name=entity_name,
282  area_name=area_id,
283  floor_name=floor_id,
284  domains=domains,
285  device_classes=device_classes,
286  assistant=intent_obj.assistant,
287  )
288  match_preferences = intent.MatchTargetsPreferences(
289  area_id=slots.get("preferred_area_id", {}).get("value"),
290  floor_id=slots.get("preferred_floor_id", {}).get("value"),
291  )
292  match_result = intent.async_match_targets(
293  hass, match_constraints, match_preferences
294  )
295  if (
296  (not match_result.is_match)
297  and (match_result.no_match_reason is not None)
298  and (not match_result.no_match_reason.is_no_entities_reason())
299  ):
300  # Don't try to answer questions for certain errors.
301  # Other match failure reasons are OK.
302  raise intent.MatchFailedError(
303  result=match_result, constraints=match_constraints
304  )
305 
306  # Create response
307  response = intent_obj.create_response()
308  response.response_type = intent.IntentResponseType.QUERY_ANSWER
309 
310  success_results: list[intent.IntentResponseTarget] = []
311  if match_result.areas:
312  success_results.extend(
313  intent.IntentResponseTarget(
314  type=intent.IntentResponseTargetType.AREA,
315  name=area.name,
316  id=area.id,
317  )
318  for area in match_result.areas
319  )
320 
321  if match_result.floors:
322  success_results.extend(
323  intent.IntentResponseTarget(
324  type=intent.IntentResponseTargetType.FLOOR,
325  name=floor.name,
326  id=floor.floor_id,
327  )
328  for floor in match_result.floors
329  )
330 
331  # If we are matching a state name (e.g., "which lights are on?"), then
332  # we split the filtered states into two groups:
333  #
334  # 1. matched - entity states that match the requested state ("on")
335  # 2. unmatched - entity states that don't match ("off")
336  #
337  # In the response template, we can access these as query.matched and
338  # query.unmatched.
339  matched_states: list[State] = []
340  unmatched_states: list[State] = []
341 
342  for state in match_result.states:
343  success_results.append(
344  intent.IntentResponseTarget(
345  type=intent.IntentResponseTargetType.ENTITY,
346  name=state.name,
347  id=state.entity_id,
348  ),
349  )
350 
351  if (not state_names) or (state.state in state_names):
352  # If no state constraint, then all states will be "matched"
353  matched_states.append(state)
354  else:
355  unmatched_states.append(state)
356 
357  response.async_set_results(success_results=success_results)
358  response.async_set_states(matched_states, unmatched_states)
359 
360  return response
361 
362 
363 class NevermindIntentHandler(intent.IntentHandler):
364  """Takes no action."""
365 
366  intent_type = intent.INTENT_NEVERMIND
367  description = "Cancels the current request and does nothing"
368 
369  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
370  """Do nothing and produces an empty response."""
371  return intent_obj.create_response()
372 
373 
374 class SetPositionIntentHandler(intent.DynamicServiceIntentHandler):
375  """Intent handler for setting positions."""
376 
377  def __init__(self) -> None:
378  """Create set position handler."""
379  super().__init__(
380  intent.INTENT_SET_POSITION,
381  required_slots={
382  ATTR_POSITION: vol.All(vol.Coerce(int), vol.Range(min=0, max=100))
383  },
384  description="Sets the position of a device or entity",
385  platforms={COVER_DOMAIN, VALVE_DOMAIN},
386  device_classes={CoverDeviceClass, ValveDeviceClass},
387  )
388 
390  self, intent_obj: intent.Intent, state: State
391  ) -> tuple[str, str]:
392  """Get the domain and service name to call."""
393  if state.domain == COVER_DOMAIN:
394  return (COVER_DOMAIN, SERVICE_SET_COVER_POSITION)
395 
396  if state.domain == VALVE_DOMAIN:
397  return (VALVE_DOMAIN, SERVICE_SET_VALVE_POSITION)
398 
399  raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
400 
401 
402 class GetCurrentDateIntentHandler(intent.IntentHandler):
403  """Gets the current date."""
404 
405  intent_type = intent.INTENT_GET_CURRENT_DATE
406  description = "Gets the current date"
407 
408  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
409  response = intent_obj.create_response()
410  response.async_set_speech_slots({"date": dt_util.now().date()})
411  return response
412 
413 
414 class GetCurrentTimeIntentHandler(intent.IntentHandler):
415  """Gets the current time."""
416 
417  intent_type = intent.INTENT_GET_CURRENT_TIME
418  description = "Gets the current time"
419 
420  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
421  response = intent_obj.create_response()
422  response.async_set_speech_slots({"time": dt_util.now().time()})
423  return response
424 
425 
426 class HelloIntentHandler(intent.IntentHandler):
427  """Responds with no action."""
428 
429  intent_type = intent.INTENT_RESPOND
430  description = "Returns the provided response with no action."
431 
432  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
433  """Return the provided response, but take no action."""
434  return intent_obj.create_response()
435 
436 
438  hass: HomeAssistant, domain: str, platform: IntentPlatformProtocol
439 ) -> None:
440  """Process the intents of an integration."""
441  await platform.async_setup_intents(hass)
442 
443 
444 class IntentHandleView(http.HomeAssistantView):
445  """View to handle intents from JSON."""
446 
447  url = "/api/intent/handle"
448  name = "api:intent:handle"
449 
450  @RequestDataValidator( vol.Schema( { vol.Required("name"): cv.string,
451  vol.Optional("data"): vol.Schema({cv.string: object}),
452  }
453  )
454  )
455  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
456  """Handle intent with name/data."""
457  hass = request.app[http.KEY_HASS]
458  language = hass.config.language
459 
460  try:
461  intent_name = data["name"]
462  slots = {
463  key: {"value": value} for key, value in data.get("data", {}).items()
464  }
465  intent_result = await intent.async_handle(
466  hass, DOMAIN, intent_name, slots, "", self.context(request)
467  )
468  except intent.IntentHandleError as err:
469  intent_result = intent.IntentResponse(language=language)
470  intent_result.async_set_speech(str(err))
471 
472  if intent_result is None:
473  intent_result = intent.IntentResponse(language=language) # type: ignore[unreachable]
474  intent_result.async_set_speech("Sorry, I couldn't handle that")
475 
476  return self.json(intent_result)
477 
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:408
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:420
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:249
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:432
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: __init__.py:455
None async_setup_intents(self, HomeAssistant hass)
Definition: __init__.py:150
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:369
None async_call_service(self, str domain, str service, intent.Intent intent_obj, State state)
Definition: __init__.py:159
tuple[str, str] get_domain_and_service(self, intent.Intent intent_obj, State state)
Definition: __init__.py:391
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:83
None _async_process_intent(HomeAssistant hass, str domain, IntentPlatformProtocol platform)
Definition: __init__.py:439
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
Definition: condition.py:802