Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Google Assistant SDK."""
2 
3 from __future__ import annotations
4 
5 import dataclasses
6 
7 import aiohttp
8 from gassist_text import TextAssistant
9 from google.oauth2.credentials import Credentials
10 import voluptuous as vol
11 
12 from homeassistant.components import conversation
13 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
14 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
15 from homeassistant.core import (
16  HomeAssistant,
17  ServiceCall,
18  ServiceResponse,
19  SupportsResponse,
20 )
21 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
22 from homeassistant.helpers import config_validation as cv, discovery, intent
24  OAuth2Session,
25  async_get_config_entry_implementation,
26 )
27 from homeassistant.helpers.typing import ConfigType
28 
29 from .const import (
30  CONF_LANGUAGE_CODE,
31  DATA_MEM_STORAGE,
32  DATA_SESSION,
33  DOMAIN,
34  SUPPORTED_LANGUAGE_CODES,
35 )
36 from .helpers import (
37  GoogleAssistantSDKAudioView,
38  InMemoryStorage,
39  async_send_text_commands,
40  best_matching_language_code,
41 )
42 
43 SERVICE_SEND_TEXT_COMMAND = "send_text_command"
44 SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
45 SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
46 SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
47  {
48  vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
49  cv.ensure_list, [vol.All(str, vol.Length(min=1))]
50  ),
51  vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
52  },
53 )
54 
55 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
56 
57 
58 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
59  """Set up Google Assistant SDK component."""
60  hass.async_create_task(
61  discovery.async_load_platform(
62  hass, Platform.NOTIFY, DOMAIN, {CONF_NAME: DOMAIN}, config
63  )
64  )
65 
66  return True
67 
68 
69 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
70  """Set up Google Assistant SDK from a config entry."""
71  hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {}
72 
73  implementation = await async_get_config_entry_implementation(hass, entry)
74  session = OAuth2Session(hass, entry, implementation)
75  try:
76  await session.async_ensure_token_valid()
77  except aiohttp.ClientResponseError as err:
78  if 400 <= err.status < 500:
80  "OAuth session is not valid, reauth required"
81  ) from err
82  raise ConfigEntryNotReady from err
83  except aiohttp.ClientError as err:
84  raise ConfigEntryNotReady from err
85  hass.data[DOMAIN][entry.entry_id][DATA_SESSION] = session
86 
87  mem_storage = InMemoryStorage(hass)
88  hass.data[DOMAIN][entry.entry_id][DATA_MEM_STORAGE] = mem_storage
89  hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage))
90 
91  await async_setup_service(hass)
92 
93  agent = GoogleAssistantConversationAgent(hass, entry)
94  conversation.async_set_agent(hass, entry, agent)
95 
96  return True
97 
98 
99 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
100  """Unload a config entry."""
101  hass.data[DOMAIN].pop(entry.entry_id)
102  loaded_entries = [
103  entry
104  for entry in hass.config_entries.async_entries(DOMAIN)
105  if entry.state == ConfigEntryState.LOADED
106  ]
107  if len(loaded_entries) == 1:
108  for service_name in hass.services.async_services_for_domain(DOMAIN):
109  hass.services.async_remove(DOMAIN, service_name)
110 
111  conversation.async_unset_agent(hass, entry)
112 
113  return True
114 
115 
116 async def async_setup_service(hass: HomeAssistant) -> None:
117  """Add the services for Google Assistant SDK."""
118 
119  async def send_text_command(call: ServiceCall) -> ServiceResponse:
120  """Send a text command to Google Assistant SDK."""
121  commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
122  media_players: list[str] | None = call.data.get(
123  SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
124  )
125  command_response_list = await async_send_text_commands(
126  hass, commands, media_players
127  )
128  if call.return_response:
129  return {
130  "responses": [
131  dataclasses.asdict(command_response)
132  for command_response in command_response_list
133  ]
134  }
135  return None
136 
137  hass.services.async_register(
138  DOMAIN,
139  SERVICE_SEND_TEXT_COMMAND,
140  send_text_command,
141  schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
142  supports_response=SupportsResponse.OPTIONAL,
143  )
144 
145 
147  """Google Assistant SDK conversation agent."""
148 
149  def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
150  """Initialize the agent."""
151  self.hasshass = hass
152  self.entryentry = entry
153  self.assistantassistant: TextAssistant | None = None
154  self.sessionsession: OAuth2Session | None = None
155  self.languagelanguage: str | None = None
156 
157  @property
158  def supported_languages(self) -> list[str]:
159  """Return a list of supported languages."""
160  return SUPPORTED_LANGUAGE_CODES
161 
162  async def async_process(
163  self, user_input: conversation.ConversationInput
165  """Process a sentence."""
166  if self.sessionsession:
167  session = self.sessionsession
168  else:
169  session = self.hasshass.data[DOMAIN][self.entryentry.entry_id][DATA_SESSION]
170  self.sessionsession = session
171  if not session.valid_token:
172  await session.async_ensure_token_valid()
173  self.assistantassistant = None
174 
175  language = best_matching_language_code(
176  self.hasshass,
177  user_input.language,
178  self.entryentry.options.get(CONF_LANGUAGE_CODE),
179  )
180 
181  if not self.assistantassistant or language != self.languagelanguage:
182  credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
183  self.languagelanguage = language
184  self.assistantassistant = TextAssistant(credentials, self.languagelanguage)
185 
186  resp = await self.hasshass.async_add_executor_job(
187  self.assistantassistant.assist, user_input.text
188  )
189  text_response = resp[0] or "<empty response>"
190 
191  intent_response = intent.IntentResponse(language=language)
192  intent_response.async_set_speech(text_response)
194  response=intent_response, conversation_id=user_input.conversation_id
195  )
None __init__(self, HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:149
conversation.ConversationResult async_process(self, conversation.ConversationInput user_input)
Definition: __init__.py:164
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
None async_setup_service(HomeAssistant hass)
Definition: __init__.py:116
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:99
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:69
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:58
AbstractOAuth2Implementation async_get_config_entry_implementation(HomeAssistant hass, config_entries.ConfigEntry config_entry)