1 """The Intent integration."""
3 from __future__
import annotations
6 from typing
import Any, Protocol
8 from aiohttp
import web
9 import voluptuous
as vol
14 DOMAIN
as COVER_DOMAIN,
17 SERVICE_SET_COVER_POSITION,
22 DOMAIN
as LOCK_DOMAIN,
29 DOMAIN
as VALVE_DOMAIN,
32 SERVICE_SET_VALVE_POSITION,
46 from .const
import DOMAIN, TIMER_DATA
48 CancelAllTimersIntentHandler,
49 CancelTimerIntentHandler,
50 DecreaseTimerIntentHandler,
51 IncreaseTimerIntentHandler,
52 PauseTimerIntentHandler,
53 StartTimerIntentHandler,
57 TimerStatusIntentHandler,
58 UnpauseTimerIntentHandler,
59 async_device_supports_timers,
60 async_register_timer_handler,
63 _LOGGER = logging.getLogger(__name__)
65 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
68 "async_register_timer_handler",
69 "async_device_supports_timers",
75 ONOFF_DEVICE_CLASSES = {
79 MediaPlayerDeviceClass,
83 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
84 """Set up the Intent component."""
89 await integration_platform.async_process_integration_platforms(
90 hass, DOMAIN, _async_process_intent
93 intent.async_register(
96 intent.INTENT_TURN_ON,
99 description=
"Turns on/opens a device or entity",
100 device_classes=ONOFF_DEVICE_CLASSES,
103 intent.async_register(
106 intent.INTENT_TURN_OFF,
107 HOMEASSISTANT_DOMAIN,
109 description=
"Turns off/closes a device or entity",
110 device_classes=ONOFF_DEVICE_CLASSES,
113 intent.async_register(
115 intent.ServiceIntentHandler(
116 intent.INTENT_TOGGLE,
117 HOMEASSISTANT_DOMAIN,
119 description=
"Toggles a device or entity",
120 device_classes=ONOFF_DEVICE_CLASSES,
123 intent.async_register(
127 intent.async_register(
148 """Define the format that intent platforms can have."""
151 """Set up platform intents."""
155 """Intent handler for on/off that also supports covers, valves, locks, etc."""
158 self, domain: str, service: str, intent_obj: intent.Intent, state: State
160 """Call service on entity with handling for special cases."""
161 hass = intent_obj.hass
163 if state.domain == COVER_DOMAIN:
166 if service == SERVICE_TURN_ON:
167 service_name = SERVICE_OPEN_COVER
169 service_name = SERVICE_CLOSE_COVER
171 await self._run_then_background(
172 hass.async_create_task(
173 hass.services.async_call(
176 {ATTR_ENTITY_ID: state.entity_id},
177 context=intent_obj.context,
184 if state.domain == LOCK_DOMAIN:
187 if service == SERVICE_TURN_ON:
188 service_name = SERVICE_LOCK
190 service_name = SERVICE_UNLOCK
192 await self._run_then_background(
193 hass.async_create_task(
194 hass.services.async_call(
197 {ATTR_ENTITY_ID: state.entity_id},
198 context=intent_obj.context,
205 if state.domain == VALVE_DOMAIN:
208 if service == SERVICE_TURN_ON:
209 service_name = SERVICE_OPEN_VALVE
211 service_name = SERVICE_CLOSE_VALVE
213 await self._run_then_background(
214 hass.async_create_task(
215 hass.services.async_call(
218 {ATTR_ENTITY_ID: state.entity_id},
219 context=intent_obj.context,
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}"
236 """Answer questions about entity states."""
238 intent_type = intent.INTENT_GET_STATE
239 description =
"Gets or checks the state of a device or entity"
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,
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)
255 name_slot = slots.get(
"name", {})
256 entity_name: str |
None = name_slot.get(
"value")
259 area_slot = slots.get(
"area", {})
260 area_id = area_slot.get(
"value")
262 floor_slot = slots.get(
"floor", {})
263 floor_id = floor_slot.get(
"value")
267 domains: set[str] |
None =
None
268 device_classes: set[str] |
None =
None
270 if "domain" in slots:
271 domains = set(slots[
"domain"][
"value"])
273 if "device_class" in slots:
274 device_classes = set(slots[
"device_class"][
"value"])
276 state_names: set[str] |
None =
None
278 state_names = set(slots[
"state"][
"value"])
280 match_constraints = intent.MatchTargetsConstraints(
285 device_classes=device_classes,
286 assistant=intent_obj.assistant,
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"),
292 match_result = intent.async_match_targets(
293 hass, match_constraints, match_preferences
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())
302 raise intent.MatchFailedError(
303 result=match_result, constraints=match_constraints
307 response = intent_obj.create_response()
308 response.response_type = intent.IntentResponseType.QUERY_ANSWER
310 success_results: list[intent.IntentResponseTarget] = []
311 if match_result.areas:
312 success_results.extend(
313 intent.IntentResponseTarget(
314 type=intent.IntentResponseTargetType.AREA,
318 for area
in match_result.areas
321 if match_result.floors:
322 success_results.extend(
323 intent.IntentResponseTarget(
324 type=intent.IntentResponseTargetType.FLOOR,
328 for floor
in match_result.floors
339 matched_states: list[State] = []
340 unmatched_states: list[State] = []
342 for state
in match_result.states:
343 success_results.append(
344 intent.IntentResponseTarget(
345 type=intent.IntentResponseTargetType.ENTITY,
351 if (
not state_names)
or (state.state
in state_names):
353 matched_states.append(state)
355 unmatched_states.append(state)
357 response.async_set_results(success_results=success_results)
358 response.async_set_states(matched_states, unmatched_states)
364 """Takes no action."""
366 intent_type = intent.INTENT_NEVERMIND
367 description =
"Cancels the current request and does nothing"
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()
375 """Intent handler for setting positions."""
378 """Create set position handler."""
380 intent.INTENT_SET_POSITION,
382 ATTR_POSITION: vol.All(vol.Coerce(int), vol.Range(min=0, max=100))
384 description=
"Sets the position of a device or entity",
385 platforms={COVER_DOMAIN, VALVE_DOMAIN},
386 device_classes={CoverDeviceClass, ValveDeviceClass},
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)
396 if state.domain == VALVE_DOMAIN:
397 return (VALVE_DOMAIN, SERVICE_SET_VALVE_POSITION)
399 raise intent.IntentHandleError(f
"Domain not supported: {state.domain}")
403 """Gets the current date."""
405 intent_type = intent.INTENT_GET_CURRENT_DATE
406 description =
"Gets the current date"
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()})
415 """Gets the current time."""
417 intent_type = intent.INTENT_GET_CURRENT_TIME
418 description =
"Gets the current time"
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()})
427 """Responds with no action."""
429 intent_type = intent.INTENT_RESPOND
430 description =
"Returns the provided response with no action."
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()
438 hass: HomeAssistant, domain: str, platform: IntentPlatformProtocol
440 """Process the intents of an integration."""
441 await platform.async_setup_intents(hass)
445 """View to handle intents from JSON."""
447 url =
"/api/intent/handle"
448 name =
"api:intent:handle"
450 @RequestDataValidator(
vol.Schema(
{
vol.Required("name"): cv.string,
451 vol.Optional(
"data"): vol.Schema({cv.string: object}),
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
461 intent_name = data[
"name"]
463 key: {
"value": value}
for key, value
in data.get(
"data", {}).items()
465 intent_result = await intent.async_handle(
466 hass, DOMAIN, intent_name, slots,
"", self.context(request)
468 except intent.IntentHandleError
as err:
469 intent_result = intent.IntentResponse(language=language)
470 intent_result.async_set_speech(
str(err))
472 if intent_result
is None:
473 intent_result = intent.IntentResponse(language=language)
474 intent_result.async_set_speech(
"Sorry, I couldn't handle that")
476 return self.json(intent_result)
477
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
web.Response post(self, web.Request request, dict[str, Any] data)
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
None async_call_service(self, str domain, str service, intent.Intent intent_obj, State state)
tuple[str, str] get_domain_and_service(self, intent.Intent intent_obj, State state)
web.Response get(self, web.Request request, str config_key)
bool async_setup(HomeAssistant hass, ConfigType config)
None _async_process_intent(HomeAssistant hass, str domain, IntentPlatformProtocol platform)
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)