Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Handle intents with scripts."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, TypedDict
7 
8 import voluptuous as vol
9 
10 from homeassistant.components.script import CONF_MODE
11 from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
12 from homeassistant.core import HomeAssistant, ServiceCall
13 from homeassistant.helpers import (
14  config_validation as cv,
15  intent,
16  script,
17  service,
18  template,
19 )
20 from homeassistant.helpers.reload import async_integration_yaml_config
21 from homeassistant.helpers.typing import ConfigType
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 DOMAIN = "intent_script"
26 
27 CONF_PLATFORMS = "platforms"
28 CONF_INTENTS = "intents"
29 CONF_SPEECH = "speech"
30 CONF_REPROMPT = "reprompt"
31 
32 CONF_ACTION = "action"
33 CONF_CARD = "card"
34 CONF_TITLE = "title"
35 CONF_CONTENT = "content"
36 CONF_TEXT = "text"
37 CONF_ASYNC_ACTION = "async_action"
38 
39 DEFAULT_CONF_ASYNC_ACTION = False
40 
41 CONFIG_SCHEMA = vol.Schema(
42  {
43  DOMAIN: {
44  cv.string: {
45  vol.Optional(CONF_DESCRIPTION): cv.string,
46  vol.Optional(CONF_PLATFORMS): vol.All([cv.string], vol.Coerce(set)),
47  vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
48  vol.Optional(
49  CONF_ASYNC_ACTION, default=DEFAULT_CONF_ASYNC_ACTION
50  ): cv.boolean,
51  vol.Optional(CONF_MODE, default=script.DEFAULT_SCRIPT_MODE): vol.In(
52  script.SCRIPT_MODE_CHOICES
53  ),
54  vol.Optional(CONF_CARD): {
55  vol.Optional(CONF_TYPE, default="simple"): cv.string,
56  vol.Required(CONF_TITLE): cv.template,
57  vol.Required(CONF_CONTENT): cv.template,
58  },
59  vol.Optional(CONF_SPEECH): {
60  vol.Optional(CONF_TYPE, default="plain"): cv.string,
61  vol.Required(CONF_TEXT): cv.template,
62  },
63  vol.Optional(CONF_REPROMPT): {
64  vol.Optional(CONF_TYPE, default="plain"): cv.string,
65  vol.Required(CONF_TEXT): cv.template,
66  },
67  }
68  }
69  },
70  extra=vol.ALLOW_EXTRA,
71 )
72 
73 
74 async def async_reload(hass: HomeAssistant, service_call: ServiceCall) -> None:
75  """Handle reload Intent Script service call."""
76  new_config = await async_integration_yaml_config(hass, DOMAIN)
77  existing_intents = hass.data[DOMAIN]
78 
79  for intent_type in existing_intents:
80  intent.async_remove(hass, intent_type)
81 
82  if not new_config or DOMAIN not in new_config:
83  hass.data[DOMAIN] = {}
84  return
85 
86  new_intents = new_config[DOMAIN]
87 
88  async_load_intents(hass, new_intents)
89 
90 
91 def async_load_intents(hass: HomeAssistant, intents: dict[str, ConfigType]) -> None:
92  """Load YAML intents into the intent system."""
93  hass.data[DOMAIN] = intents
94 
95  for intent_type, conf in intents.items():
96  if CONF_ACTION in conf:
97  script_mode: str = conf.get(CONF_MODE, script.DEFAULT_SCRIPT_MODE)
98  conf[CONF_ACTION] = script.Script(
99  hass,
100  conf[CONF_ACTION],
101  f"Intent Script {intent_type}",
102  DOMAIN,
103  script_mode=script_mode,
104  )
105  intent.async_register(hass, ScriptIntentHandler(intent_type, conf))
106 
107 
108 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
109  """Set up the intent script component."""
110  intents = config[DOMAIN]
111 
112  async_load_intents(hass, intents)
113 
114  async def _handle_reload(service_call: ServiceCall) -> None:
115  return await async_reload(hass, service_call)
116 
117  service.async_register_admin_service(
118  hass,
119  DOMAIN,
120  SERVICE_RELOAD,
121  _handle_reload,
122  )
123 
124  return True
125 
126 
127 class _IntentSpeechRepromptData(TypedDict):
128  """Intent config data type for speech or reprompt info."""
129 
130  content: template.Template
131  title: template.Template
132  text: template.Template
133  type: str
134 
135 
136 class _IntentCardData(TypedDict):
137  """Intent config data type for card info."""
138 
139  type: str
140  title: template.Template
141  content: template.Template
142 
143 
144 class ScriptIntentHandler(intent.IntentHandler):
145  """Respond to an intent with a script."""
146 
147  slot_schema = {
148  vol.Any("name", "area", "floor"): cv.string,
149  vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]),
150  vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]),
151  }
152 
153  def __init__(self, intent_type: str, config: ConfigType) -> None:
154  """Initialize the script intent handler."""
155  self.intent_typeintent_type = intent_type
156  self.configconfig = config
157  self.descriptiondescription = config.get(CONF_DESCRIPTION)
158  self.platformsplatforms = config.get(CONF_PLATFORMS)
159 
160  async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
161  """Handle the intent."""
162  speech: _IntentSpeechRepromptData | None = self.configconfig.get(CONF_SPEECH)
163  reprompt: _IntentSpeechRepromptData | None = self.configconfig.get(CONF_REPROMPT)
164  card: _IntentCardData | None = self.configconfig.get(CONF_CARD)
165  action: script.Script | None = self.configconfig.get(CONF_ACTION)
166  is_async_action: bool = self.configconfig[CONF_ASYNC_ACTION]
167  hass: HomeAssistant = intent_obj.hass
168  intent_slots = self.async_validate_slots(intent_obj.slots)
169  slots: dict[str, Any] = {
170  key: value["value"] for key, value in intent_slots.items()
171  }
172 
173  _LOGGER.debug(
174  "Intent named %s received with slots: %s",
175  intent_obj.intent_type,
176  {
177  key: value
178  for key, value in slots.items()
179  if not key.startswith("_") and not key.endswith("_raw_value")
180  },
181  )
182 
183  entity_name = slots.get("name")
184  area_name = slots.get("area")
185  floor_name = slots.get("floor")
186 
187  # Optional domain/device class filters.
188  # Convert to sets for speed.
189  domains: set[str] | None = None
190  device_classes: set[str] | None = None
191 
192  if "domain" in slots:
193  domains = set(slots["domain"])
194 
195  if "device_class" in slots:
196  device_classes = set(slots["device_class"])
197 
198  match_constraints = intent.MatchTargetsConstraints(
199  name=entity_name,
200  area_name=area_name,
201  floor_name=floor_name,
202  domains=domains,
203  device_classes=device_classes,
204  assistant=intent_obj.assistant,
205  )
206 
207  if match_constraints.has_constraints:
208  match_result = intent.async_match_targets(hass, match_constraints)
209  if match_result.is_match:
210  targets = {}
211 
212  if match_result.states:
213  targets["entities"] = [
214  state.entity_id for state in match_result.states
215  ]
216 
217  if match_result.areas:
218  targets["areas"] = [area.id for area in match_result.areas]
219 
220  if match_result.floors:
221  targets["floors"] = [
222  floor.floor_id for floor in match_result.floors
223  ]
224 
225  if targets:
226  slots["targets"] = targets
227 
228  if action is not None:
229  if is_async_action:
230  intent_obj.hass.async_create_task(
231  action.async_run(slots, intent_obj.context)
232  )
233  else:
234  action_res = await action.async_run(slots, intent_obj.context)
235 
236  # if the action returns a response, make it available to the speech/reprompt templates below
237  if action_res and action_res.service_response is not None:
238  slots["action_response"] = action_res.service_response
239 
240  response = intent_obj.create_response()
241 
242  if speech is not None:
243  response.async_set_speech(
244  speech["text"].async_render(slots, parse_result=False),
245  speech["type"],
246  )
247 
248  if reprompt is not None:
249  text_reprompt = reprompt["text"].async_render(slots, parse_result=False)
250  if text_reprompt:
251  response.async_set_reprompt(
252  text_reprompt,
253  reprompt["type"],
254  )
255 
256  if card is not None:
257  response.async_set_card(
258  card["title"].async_render(slots, parse_result=False),
259  card["content"].async_render(slots, parse_result=False),
260  card["type"],
261  )
262 
263  return response
None __init__(self, str intent_type, ConfigType config)
Definition: __init__.py:153
intent.IntentResponse async_handle(self, intent.Intent intent_obj)
Definition: __init__.py:160
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:108
None async_reload(HomeAssistant hass, ServiceCall service_call)
Definition: __init__.py:74
None async_load_intents(HomeAssistant hass, dict[str, ConfigType] intents)
Definition: __init__.py:91
ConfigType|None async_integration_yaml_config(HomeAssistant hass, str integration_name)
Definition: reload.py:142