Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Snips on-device ASR and NLU."""
2 
3 from datetime import timedelta
4 import json
5 import logging
6 
7 import voluptuous as vol
8 
9 from homeassistant.components import mqtt
10 from homeassistant.core import HomeAssistant, ServiceCall
11 from homeassistant.helpers import config_validation as cv, intent
12 from homeassistant.helpers.typing import ConfigType
13 
14 DOMAIN = "snips"
15 CONF_INTENTS = "intents"
16 CONF_ACTION = "action"
17 CONF_FEEDBACK = "feedback_sounds"
18 CONF_PROBABILITY = "probability_threshold"
19 CONF_SITE_IDS = "site_ids"
20 
21 SERVICE_SAY = "say"
22 SERVICE_SAY_ACTION = "say_action"
23 SERVICE_FEEDBACK_ON = "feedback_on"
24 SERVICE_FEEDBACK_OFF = "feedback_off"
25 
26 INTENT_TOPIC = "hermes/intent/#"
27 FEEDBACK_ON_TOPIC = "hermes/feedback/sound/toggleOn"
28 FEEDBACK_OFF_TOPIC = "hermes/feedback/sound/toggleOff"
29 
30 ATTR_TEXT = "text"
31 ATTR_SITE_ID = "site_id"
32 ATTR_CUSTOM_DATA = "custom_data"
33 ATTR_CAN_BE_ENQUEUED = "can_be_enqueued"
34 ATTR_INTENT_FILTER = "intent_filter"
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 CONFIG_SCHEMA = vol.Schema(
39  {
40  DOMAIN: vol.Schema(
41  {
42  vol.Optional(CONF_FEEDBACK): cv.boolean,
43  vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float),
44  vol.Optional(CONF_SITE_IDS, default=["default"]): vol.All(
45  cv.ensure_list, [cv.string]
46  ),
47  }
48  )
49  },
50  extra=vol.ALLOW_EXTRA,
51 )
52 
53 INTENT_SCHEMA = vol.Schema(
54  {
55  vol.Required("input"): str,
56  vol.Required("intent"): {vol.Required("intentName"): str},
57  vol.Optional("slots"): [
58  {
59  vol.Required("slotName"): str,
60  vol.Required("value"): {
61  vol.Required("kind"): str,
62  vol.Optional("value"): cv.match_all,
63  vol.Optional("rawValue"): cv.match_all,
64  },
65  }
66  ],
67  },
68  extra=vol.ALLOW_EXTRA,
69 )
70 
71 SERVICE_SCHEMA_SAY = vol.Schema(
72  {
73  vol.Required(ATTR_TEXT): str,
74  vol.Optional(ATTR_SITE_ID, default="default"): str,
75  vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
76  }
77 )
78 SERVICE_SCHEMA_SAY_ACTION = vol.Schema(
79  {
80  vol.Required(ATTR_TEXT): str,
81  vol.Optional(ATTR_SITE_ID, default="default"): str,
82  vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
83  vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean,
84  vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list),
85  }
86 )
87 SERVICE_SCHEMA_FEEDBACK = vol.Schema(
88  {vol.Optional(ATTR_SITE_ID, default="default"): str}
89 )
90 
91 
92 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
93  """Activate Snips component."""
94 
95  # Make sure MQTT integration is enabled and the client is available
96  if not await mqtt.async_wait_for_mqtt_client(hass):
97  _LOGGER.error("MQTT integration is not available")
98  return False
99 
100  async def async_set_feedback(site_ids, state):
101  """Set Feedback sound state."""
102  site_ids = site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS)
103  topic = FEEDBACK_ON_TOPIC if state else FEEDBACK_OFF_TOPIC
104  for site_id in site_ids:
105  payload = json.dumps({"siteId": site_id})
106  await mqtt.async_publish(hass, FEEDBACK_ON_TOPIC, "", qos=0, retain=False)
107  await mqtt.async_publish(hass, topic, payload, qos=int(state), retain=state)
108 
109  if CONF_FEEDBACK in config[DOMAIN]:
110  await async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK])
111 
112  async def message_received(msg):
113  """Handle new messages on MQTT."""
114  _LOGGER.debug("New intent: %s", msg.payload)
115 
116  try:
117  request = json.loads(msg.payload)
118  except TypeError:
119  _LOGGER.error("Received invalid JSON: %s", msg.payload)
120  return
121 
122  if request["intent"]["confidenceScore"] < config[DOMAIN].get(CONF_PROBABILITY):
123  _LOGGER.warning(
124  "Intent below probaility threshold %s < %s",
125  request["intent"]["confidenceScore"],
126  config[DOMAIN].get(CONF_PROBABILITY),
127  )
128  return
129 
130  try:
131  request = INTENT_SCHEMA(request)
132  except vol.Invalid as err:
133  _LOGGER.error("Intent has invalid schema: %s. %s", err, request)
134  return
135 
136  if request["intent"]["intentName"].startswith("user_"):
137  intent_type = request["intent"]["intentName"].split("__")[-1]
138  else:
139  intent_type = request["intent"]["intentName"].split(":")[-1]
140  slots = {}
141  for slot in request.get("slots", []):
142  slots[slot["slotName"]] = {"value": resolve_slot_values(slot)}
143  slots[f"{slot['slotName']}_raw"] = {"value": slot["rawValue"]}
144  slots["site_id"] = {"value": request.get("siteId")}
145  slots["session_id"] = {"value": request.get("sessionId")}
146  slots["confidenceScore"] = {"value": request["intent"]["confidenceScore"]}
147 
148  try:
149  intent_response = await intent.async_handle(
150  hass, DOMAIN, intent_type, slots, request["input"]
151  )
152  notification = {"sessionId": request.get("sessionId", "default")}
153 
154  if "plain" in intent_response.speech:
155  notification["text"] = intent_response.speech["plain"]["speech"]
156 
157  _LOGGER.debug("send_response %s", json.dumps(notification))
158  await mqtt.async_publish(
159  hass, "hermes/dialogueManager/endSession", json.dumps(notification)
160  )
161  except intent.UnknownIntent:
162  _LOGGER.warning(
163  "Received unknown intent %s", request["intent"]["intentName"]
164  )
165  except intent.IntentError:
166  _LOGGER.exception("Error while handling intent: %s", intent_type)
167 
168  await mqtt.async_subscribe(hass, INTENT_TOPIC, message_received)
169 
170  async def snips_say(call: ServiceCall) -> None:
171  """Send a Snips notification message."""
172  notification = {
173  "siteId": call.data.get(ATTR_SITE_ID, "default"),
174  "customData": call.data.get(ATTR_CUSTOM_DATA, ""),
175  "init": {"type": "notification", "text": call.data.get(ATTR_TEXT)},
176  }
177  await mqtt.async_publish(
178  hass, "hermes/dialogueManager/startSession", json.dumps(notification)
179  )
180 
181  async def snips_say_action(call: ServiceCall) -> None:
182  """Send a Snips action message."""
183  notification = {
184  "siteId": call.data.get(ATTR_SITE_ID, "default"),
185  "customData": call.data.get(ATTR_CUSTOM_DATA, ""),
186  "init": {
187  "type": "action",
188  "text": call.data.get(ATTR_TEXT),
189  "canBeEnqueued": call.data.get(ATTR_CAN_BE_ENQUEUED, True),
190  "intentFilter": call.data.get(ATTR_INTENT_FILTER, []),
191  },
192  }
193  await mqtt.async_publish(
194  hass, "hermes/dialogueManager/startSession", json.dumps(notification)
195  )
196 
197  async def feedback_on(call: ServiceCall) -> None:
198  """Turn feedback sounds on."""
199  await async_set_feedback(call.data.get(ATTR_SITE_ID), True)
200 
201  async def feedback_off(call: ServiceCall) -> None:
202  """Turn feedback sounds off."""
203  await async_set_feedback(call.data.get(ATTR_SITE_ID), False)
204 
205  hass.services.async_register(
206  DOMAIN, SERVICE_SAY, snips_say, schema=SERVICE_SCHEMA_SAY
207  )
208  hass.services.async_register(
209  DOMAIN, SERVICE_SAY_ACTION, snips_say_action, schema=SERVICE_SCHEMA_SAY_ACTION
210  )
211  hass.services.async_register(
212  DOMAIN, SERVICE_FEEDBACK_ON, feedback_on, schema=SERVICE_SCHEMA_FEEDBACK
213  )
214  hass.services.async_register(
215  DOMAIN, SERVICE_FEEDBACK_OFF, feedback_off, schema=SERVICE_SCHEMA_FEEDBACK
216  )
217 
218  return True
219 
220 
222  """Convert snips builtin types to usable values."""
223  if "value" in slot["value"]:
224  value = slot["value"]["value"]
225  else:
226  value = slot["rawValue"]
227 
228  if slot.get("entity") == "snips/duration":
229  delta = timedelta(
230  weeks=slot["value"]["weeks"],
231  days=slot["value"]["days"],
232  hours=slot["value"]["hours"],
233  minutes=slot["value"]["minutes"],
234  seconds=slot["value"]["seconds"],
235  )
236  value = delta.total_seconds()
237 
238  return value
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:92