Home Assistant Unofficial Reference 2024.12.1
trigger.py
Go to the documentation of this file.
1 """Offer sentence based automation rules."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from hassil.recognize import RecognizeResult
8 from hassil.util import PUNCTUATION_ALL
9 import voluptuous as vol
10 
11 from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
12 from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
14 from homeassistant.helpers.script import ScriptRunResult
15 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
16 from homeassistant.helpers.typing import UNDEFINED, ConfigType
17 
18 from .const import DATA_DEFAULT_ENTITY, DOMAIN
19 
20 
21 def has_no_punctuation(value: list[str]) -> list[str]:
22  """Validate result does not contain punctuation."""
23  for sentence in value:
24  if PUNCTUATION_ALL.search(sentence):
25  raise vol.Invalid("sentence should not contain punctuation")
26 
27  return value
28 
29 
30 def has_one_non_empty_item(value: list[str]) -> list[str]:
31  """Validate result has at least one item."""
32  if len(value) < 1:
33  raise vol.Invalid("at least one sentence is required")
34 
35  for sentence in value:
36  if not sentence:
37  raise vol.Invalid(f"sentence too short: '{sentence}'")
38 
39  return value
40 
41 
42 TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
43  {
44  vol.Required(CONF_PLATFORM): DOMAIN,
45  vol.Required(CONF_COMMAND): vol.All(
46  cv.ensure_list, [cv.string], has_one_non_empty_item, has_no_punctuation
47  ),
48  }
49 )
50 
51 
53  hass: HomeAssistant,
54  config: ConfigType,
55  action: TriggerActionType,
56  trigger_info: TriggerInfo,
57 ) -> CALLBACK_TYPE:
58  """Listen for events based on configuration."""
59  trigger_data = trigger_info["trigger_data"]
60  sentences = config.get(CONF_COMMAND, [])
61 
62  job = HassJob(action)
63 
64  async def call_action(
65  sentence: str, result: RecognizeResult, device_id: str | None
66  ) -> str | None:
67  """Call action with right context."""
68 
69  # Add slot values as extra trigger data
70  details = {
71  entity_name: {
72  "name": entity_name,
73  "text": entity.text.strip(), # remove whitespace
74  "value": (
75  entity.value.strip()
76  if isinstance(entity.value, str)
77  else entity.value
78  ),
79  }
80  for entity_name, entity in result.entities.items()
81  }
82 
83  trigger_input: dict[str, Any] = { # Satisfy type checker
84  **trigger_data,
85  "platform": DOMAIN,
86  "sentence": sentence,
87  "details": details,
88  "slots": { # direct access to values
89  entity_name: entity["value"] for entity_name, entity in details.items()
90  },
91  "device_id": device_id,
92  }
93 
94  # Wait for the automation to complete
95  if future := hass.async_run_hass_job(
96  job,
97  {"trigger": trigger_input},
98  ):
99  automation_result = await future
100  if isinstance(
101  automation_result, ScriptRunResult
102  ) and automation_result.conversation_response not in (None, UNDEFINED):
103  # mypy does not understand the type narrowing, unclear why
104  return automation_result.conversation_response # type: ignore[return-value]
105 
106  # It's important to return None here instead of a string.
107  #
108  # When editing in the UI, a copy of this trigger is registered.
109  # If we return a string from here, there is a race condition between the
110  # two trigger copies for who will provide a response.
111  return None
112 
113  return hass.data[DATA_DEFAULT_ENTITY].register_trigger(sentences, call_action)
list[str] has_no_punctuation(list[str] value)
Definition: trigger.py:21
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
Definition: trigger.py:57
list[str] has_one_non_empty_item(list[str] value)
Definition: trigger.py:30