1 """Interface implementation for cloud client."""
3 from __future__
import annotations
6 from collections.abc
import Callable
7 from datetime
import datetime
8 from http
import HTTPStatus
10 from pathlib
import Path
11 from typing
import Any, Literal
14 from hass_nabucasa.client
import CloudClient
as Interface, RemoteActivationNotAllowed
15 from webrtc_models
import RTCIceServer
19 errors
as alexa_errors,
20 smart_home
as alexa_smart_home,
32 from .
import alexa_config, google_config
33 from .const
import DISPATCHER_REMOTE_UPDATE, DOMAIN, PREF_ENABLE_CLOUD_ICE_SERVERS
34 from .prefs
import CloudPreferences
36 _LOGGER = logging.getLogger(__name__)
38 VALID_REPAIR_TRANSLATION_KEYS = {
39 "warn_bad_custom_domain_configuration",
40 "reset_bad_custom_domain_configuration",
45 """Interface class for Home Assistant Cloud."""
50 prefs: CloudPreferences,
51 websession: aiohttp.ClientSession,
52 alexa_user_config: dict[str, Any],
53 google_user_config: dict[str, Any],
55 """Initialize client interface to Cloud."""
70 """Return path to base dir."""
71 return Path(self.
_hass_hass.config.config_dir)
74 def prefs(self) -> CloudPreferences:
75 """Return Cloud preferences."""
79 def loop(self) -> asyncio.AbstractEventLoop:
80 """Return client loop."""
81 return self.
_hass_hass.loop
85 """Return client session for aiohttp."""
90 """Return client webinterface aiohttp application."""
91 return self.
_hass_hass.http.runner
94 def cloudhooks(self) -> dict[str, dict[str, str | bool]]:
95 """Return list of cloudhooks."""
96 return self.
_prefs_prefs.cloudhooks
100 """Return true if we want start a remote connection."""
101 return self.
_prefs_prefs.remote_enabled
105 """Return the client name that will be used for API calls."""
106 return SERVER_SOFTWARE
110 """Return the connected relayer region."""
114 """Return Alexa config."""
122 cloud_user = await self.
_prefs_prefs.get_cloud_user()
131 await alexa_conf.async_initialize()
137 """Return Google config."""
143 cloud_user = await self.
_prefs_prefs.get_cloud_user()
152 await google_conf.async_initialize()
158 """When cloud is connected."""
159 _LOGGER.debug(
"cloud_connected")
160 is_new_user = await self.
prefsprefs.async_set_username(self.cloud.username)
162 async
def enable_alexa(_: Any) ->
None:
166 await aconf.async_enable_proactive_mode()
167 except aiohttp.ClientError
as err:
168 if self.
_hass_hass.is_running:
169 logging.getLogger(__package__).warning(
171 "Unable to activate Alexa Report State: %s. Retrying in 30"
177 except (alexa_errors.NoTokenAvailable, alexa_errors.RequireRelink):
180 enable_alexa_job =
HassJob(enable_alexa, cancel_on_shutdown=
True)
182 async
def enable_google(_: datetime) ->
None:
186 gconf.async_enable_local_sdk()
188 if gconf.should_report_state:
189 gconf.async_enable_report_state()
192 await gconf.async_sync_entities(gconf.agent_user_id)
194 async
def setup_cloud_ice_servers(_: datetime) ->
None:
195 async
def register_cloud_ice_server(
196 ice_servers: list[RTCIceServer],
197 ) -> Callable[[],
None]:
198 """Register cloud ice server."""
200 def get_ice_servers() -> list[RTCIceServer]:
205 async
def async_register_cloud_ice_servers_listener(
206 prefs: CloudPreferences,
208 is_cloud_ice_servers_enabled = (
209 self.cloud.is_logged_in
210 and not self.cloud.subscription_expired
211 and prefs.cloud_ice_servers_enabled
213 if is_cloud_ice_servers_enabled:
216 register_cloud_ice_server
222 async
def async_prefs_updated(prefs: CloudPreferences) ->
None:
223 updated_prefs = prefs.last_updated
226 updated_prefs
is None
227 or PREF_ENABLE_CLOUD_ICE_SERVERS
not in updated_prefs
231 await async_register_cloud_ice_servers_listener(prefs)
233 await async_register_cloud_ice_servers_listener(self.
_prefs_prefs)
235 self.
_prefs_prefs.async_listen_updates(async_prefs_updated)
239 if self.
_prefs_prefs.alexa_enabled
and self.
_prefs_prefs.alexa_report_state:
240 tasks.append(enable_alexa)
242 if self.
_prefs_prefs.google_enabled:
243 tasks.append(enable_google)
245 tasks.append(setup_cloud_ice_servers)
248 await asyncio.gather(*(task(
None)
for task
in tasks))
251 """When cloud disconnected."""
252 _LOGGER.debug(
"cloud_disconnected")
257 """When cloud is started."""
260 """When the cloud is stopped."""
263 """Cleanup some stuff after logout."""
264 await self.prefs.async_set_username(
None)
266 if self._alexa_config:
267 self._alexa_config.async_deinitialize()
268 self._alexa_config =
None
270 if self._google_config:
271 self._google_config.async_deinitialize()
272 self._google_config =
None
274 if self._cloud_ice_servers_listener:
275 self._cloud_ice_servers_listener()
276 self._cloud_ice_servers_listener =
None
279 def user_message(self, identifier: str, title: str, message: str) ->
None:
280 """Create a message for user to UI."""
281 persistent_notification.async_create(self.
_hass_hass, message, title, identifier)
285 """Match cloud notification to dispatcher."""
286 if identifier.startswith(
"remote_"):
290 """Process cloud remote message to client."""
291 if not self.
_prefs_prefs.remote_allow_remote_enable:
292 raise RemoteActivationNotAllowed
296 self, payload: dict[str, Any]
298 """Process cloud connection info message to client."""
301 "can_enable": self.
_prefs_prefs.remote_allow_remote_enable,
302 "connected": self.cloud.remote.is_connected,
303 "enabled": self.
_prefs_prefs.remote_enabled,
304 "instance_domain": self.cloud.remote.instance_domain,
305 "alias": self.cloud.remote.alias,
307 "version": HA_VERSION,
308 "instance_id": self.
prefsprefs.instance_id,
312 """Process cloud alexa message to client."""
313 cloud_user = await self.
_prefs_prefs.get_cloud_user()
315 return await alexa_smart_home.async_handle_message(
319 context=
Context(user_id=cloud_user),
320 enabled=self.
_prefs_prefs.alexa_enabled,
324 """Process cloud google message to client."""
327 msgid: Any =
"<UNKNOWN>"
328 if isinstance(payload, dict):
329 msgid = payload.get(
"requestId")
330 _LOGGER.debug(
"Received cloud message %s", msgid)
332 if not self.
_prefs_prefs.google_enabled:
333 return ga.api_disabled_response(
334 payload, gconf.agent_user_id
337 return await ga.async_handle_message(
343 google_assistant.SOURCE_CLOUD,
347 """Process cloud webhook message to client."""
348 cloudhook_id = payload[
"cloudhook_id"]
351 for cloudhook
in self.
_prefs_prefs.cloudhooks.values():
352 if cloudhook[
"cloudhook_id"] == cloudhook_id:
357 return {
"status": HTTPStatus.OK}
360 content=payload[
"body"].encode(
"utf-8"),
361 headers=payload[
"headers"],
362 method=payload[
"method"],
363 query_string=payload[
"query"],
367 response = await webhook.async_handle_webhook(
368 self.
_hass_hass, found[
"webhook_id"], request
372 body = response_dict.get(
"body")
376 "status": response_dict[
"status"],
377 "headers": {
"Content-Type": response.content_type},
381 """Handle system messages."""
382 if payload
and (region := payload.get(
"region")):
386 self, data: dict[str, dict[str, str | bool]]
388 """Update local list of cloudhooks."""
394 translation_key: str,
396 placeholders: dict[str, str] |
None =
None,
397 severity: Literal[
"error",
"warning"] =
"warning",
399 """Create a repair issue."""
400 if translation_key
not in VALID_REPAIR_TRANSLATION_KEYS:
401 raise ValueError(f
"Invalid translation key {translation_key}")
403 hass=self.
_hass_hass,
406 translation_key=translation_key,
407 translation_placeholders=placeholders,
dict[Any, Any] async_alexa_message(self, dict[Any, Any] payload)
dict[Any, Any] async_webhook_message(self, dict[Any, Any] payload)
CloudPreferences prefs(self)
aiohttp.web.AppRunner|None aiohttp_runner(self)
dict[str, dict[str, str|bool]] cloudhooks(self)
aiohttp.ClientSession websession(self)
None async_create_repair_issue(self, str identifier, str translation_key, *dict[str, str]|None placeholders=None, Literal["error", "warning"] severity="warning")
None cloud_disconnected(self)
google_config.CloudGoogleConfig get_google_config(self)
bool remote_autostart(self)
dict[Any, Any] async_google_message(self, dict[Any, Any] payload)
_cloud_ice_servers_listener
asyncio.AbstractEventLoop loop(self)
None user_message(self, str identifier, str title, str message)
None cloud_connected(self)
None dispatcher_message(self, str identifier, Any data=None)
None logout_cleanups(self)
None async_cloudhooks_update(self, dict[str, dict[str, str|bool]] data)
None __init__(self, HomeAssistant hass, CloudPreferences prefs, aiohttp.ClientSession websession, dict[str, Any] alexa_user_config, dict[str, Any] google_user_config)
None async_system_message(self, dict[Any, Any]|None payload)
dict[str, Any] async_cloud_connection_info(self, dict[str, Any] payload)
str|None relayer_region(self)
None async_cloud_connect_update(self, bool connect)
alexa_config.CloudAlexaConfig get_alexa_config(self)
Callable[[], None] async_register_ice_servers(HomeAssistant hass, Callable[[], Iterable[RTCIceServer]] get_ice_server_fn)
None async_create_issue(HomeAssistant hass, str entry_id)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
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)
dict[str, Any] serialize_response(web.Response response)