Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helper classes for Google Assistant SDK integration."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from http import HTTPStatus
7 import logging
8 from typing import Any
9 import uuid
10 
11 import aiohttp
12 from aiohttp import web
13 from gassist_text import TextAssistant
14 from google.oauth2.credentials import Credentials
15 
16 from homeassistant.components.http import HomeAssistantView
18  ATTR_MEDIA_ANNOUNCE,
19  ATTR_MEDIA_CONTENT_ID,
20  ATTR_MEDIA_CONTENT_TYPE,
21  DOMAIN as DOMAIN_MP,
22  SERVICE_PLAY_MEDIA,
23  MediaType,
24 )
25 from homeassistant.config_entries import ConfigEntry
26 from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
27 from homeassistant.core import HomeAssistant
28 from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
29 from homeassistant.helpers.event import async_call_later
30 
31 from .const import (
32  CONF_LANGUAGE_CODE,
33  DATA_MEM_STORAGE,
34  DATA_SESSION,
35  DOMAIN,
36  SUPPORTED_LANGUAGE_CODES,
37 )
38 
39 _LOGGER = logging.getLogger(__name__)
40 
41 DEFAULT_LANGUAGE_CODES = {
42  "de": "de-DE",
43  "en": "en-US",
44  "es": "es-ES",
45  "fr": "fr-FR",
46  "it": "it-IT",
47  "ja": "ja-JP",
48  "ko": "ko-KR",
49  "pt": "pt-BR",
50 }
51 
52 
53 @dataclass
55  """Response from a single command to Google Assistant Service."""
56 
57  text: str
58 
59 
61  hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None
62 ) -> list[CommandResponse]:
63  """Send text commands to Google Assistant Service."""
64  # There can only be 1 entry (config_flow has single_instance_allowed)
65  entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
66 
67  session: OAuth2Session = hass.data[DOMAIN][entry.entry_id][DATA_SESSION]
68  try:
69  await session.async_ensure_token_valid()
70  except aiohttp.ClientResponseError as err:
71  if 400 <= err.status < 500:
72  entry.async_start_reauth(hass)
73  raise
74 
75  credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
76  language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
77  with TextAssistant(
78  credentials, language_code, audio_out=bool(media_players)
79  ) as assistant:
80  command_response_list = []
81  for command in commands:
82  resp = await hass.async_add_executor_job(assistant.assist, command)
83  text_response = resp[0]
84  _LOGGER.debug("command: %s\nresponse: %s", command, text_response)
85  audio_response = resp[2]
86  if media_players and audio_response:
87  mem_storage: InMemoryStorage = hass.data[DOMAIN][entry.entry_id][
88  DATA_MEM_STORAGE
89  ]
90  audio_url = GoogleAssistantSDKAudioView.url.format(
91  filename=mem_storage.store_and_get_identifier(audio_response)
92  )
93  await hass.services.async_call(
94  DOMAIN_MP,
95  SERVICE_PLAY_MEDIA,
96  {
97  ATTR_ENTITY_ID: media_players,
98  ATTR_MEDIA_CONTENT_ID: audio_url,
99  ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
100  ATTR_MEDIA_ANNOUNCE: True,
101  },
102  blocking=True,
103  )
104  command_response_list.append(CommandResponse(text_response))
105  return command_response_list
106 
107 
108 def default_language_code(hass: HomeAssistant) -> str:
109  """Get default language code based on Home Assistant config."""
110  language_code = f"{hass.config.language}-{hass.config.country}"
111  if language_code in SUPPORTED_LANGUAGE_CODES:
112  return language_code
113  return DEFAULT_LANGUAGE_CODES.get(hass.config.language, "en-US")
114 
115 
117  hass: HomeAssistant, assist_language: str, agent_language: str | None = None
118 ) -> str:
119  """Get the best matching language, based on the preferred assist language and the configured agent language."""
120 
121  # Use the assist language if supported
122  if assist_language in SUPPORTED_LANGUAGE_CODES:
123  return assist_language
124  language = assist_language.split("-")[0]
125 
126  # Use the agent language if assist and agent start with the same language part
127  if agent_language is not None and agent_language.startswith(language):
128  return best_matching_language_code(hass, agent_language)
129 
130  # If assist and agent are not matching, try to find the default language
131  default_language = DEFAULT_LANGUAGE_CODES.get(language)
132  if default_language is not None:
133  return default_language
134 
135  # If no default agent is available, use the agent language
136  if agent_language is not None:
137  return best_matching_language_code(hass, agent_language)
138 
139  # Fallback to the system default language
140  return default_language_code(hass)
141 
142 
144  """Temporarily store and retrieve data from in memory storage."""
145 
146  def __init__(self, hass: HomeAssistant) -> None:
147  """Initialize InMemoryStorage."""
148  self.hass: HomeAssistant = hass
149  self.mem: dict[str, bytes] = {}
150 
151  def store_and_get_identifier(self, data: bytes) -> str:
152  """Temporarily store data and return identifier to be able to retrieve it.
153 
154  Data expires after 5 minutes.
155  """
156  identifier: str = uuid.uuid1().hex
157  self.mem[identifier] = data
158 
159  def async_remove_from_mem(*_: Any) -> None:
160  """Cleanup memory."""
161  self.mem.pop(identifier, None)
162 
163  # Remove the entry from memory 5 minutes later
164  async_call_later(self.hass, 5 * 60, async_remove_from_mem)
165 
166  return identifier
167 
168  def retrieve(self, identifier: str) -> bytes | None:
169  """Retrieve previously stored data."""
170  return self.mem.get(identifier)
171 
172 
173 class GoogleAssistantSDKAudioView(HomeAssistantView):
174  """Google Assistant SDK view to serve audio responses."""
175 
176  requires_auth = True
177  url = "/api/google_assistant_sdk/audio/{filename}"
178  name = "api:google_assistant_sdk:audio"
179 
180  def __init__(self, mem_storage: InMemoryStorage) -> None:
181  """Initialize GoogleAssistantSDKView."""
182  self.mem_storage: InMemoryStorage = mem_storage
183 
184  async def get(self, request: web.Request, filename: str) -> web.Response:
185  """Start a get request."""
186  audio = self.mem_storage.retrieve(filename)
187  if not audio:
188  return web.Response(status=HTTPStatus.NOT_FOUND)
189  return web.Response(body=audio, content_type="audio/mpeg")
web.Response get(self, web.Request request, str filename)
Definition: helpers.py:184
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[CommandResponse] async_send_text_commands(HomeAssistant hass, list[str] commands, list[str]|None media_players=None)
Definition: helpers.py:62
str best_matching_language_code(HomeAssistant hass, str assist_language, str|None agent_language=None)
Definition: helpers.py:118
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597