Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to integrate the Home Assistant cloud."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Awaitable, Callable
7 from datetime import datetime, timedelta
8 from enum import Enum
9 from typing import cast
10 
11 from hass_nabucasa import Cloud
12 import voluptuous as vol
13 
14 from homeassistant.components import alexa, google_assistant
15 from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
16 from homeassistant.const import (
17  CONF_DESCRIPTION,
18  CONF_MODE,
19  CONF_NAME,
20  CONF_REGION,
21  EVENT_HOMEASSISTANT_STOP,
22  Platform,
23 )
24 from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback
25 from homeassistant.exceptions import HomeAssistantError
26 from homeassistant.helpers import config_validation as cv, entityfilter
27 from homeassistant.helpers.aiohttp_client import async_get_clientsession
28 from homeassistant.helpers.discovery import async_load_platform
30  async_dispatcher_connect,
31  async_dispatcher_send,
32 )
33 from homeassistant.helpers.event import async_call_later
34 from homeassistant.helpers.service import async_register_admin_service
35 from homeassistant.helpers.typing import ConfigType
36 from homeassistant.loader import bind_hass
37 from homeassistant.util.signal_type import SignalType
38 
39 from . import account_link, http_api
40 from .client import CloudClient
41 from .const import (
42  CONF_ACCOUNT_LINK_SERVER,
43  CONF_ACCOUNTS_SERVER,
44  CONF_ACME_SERVER,
45  CONF_ALEXA,
46  CONF_ALEXA_SERVER,
47  CONF_ALIASES,
48  CONF_CLOUDHOOK_SERVER,
49  CONF_COGNITO_CLIENT_ID,
50  CONF_ENTITY_CONFIG,
51  CONF_FILTER,
52  CONF_GOOGLE_ACTIONS,
53  CONF_RELAYER_SERVER,
54  CONF_REMOTESTATE_SERVER,
55  CONF_SERVICEHANDLERS_SERVER,
56  CONF_THINGTALK_SERVER,
57  CONF_USER_POOL_ID,
58  DATA_CLOUD,
59  DATA_PLATFORMS_SETUP,
60  DOMAIN,
61  MODE_DEV,
62  MODE_PROD,
63 )
64 from .prefs import CloudPreferences
65 from .repairs import async_manage_legacy_subscription_issue
66 from .subscription import async_subscription_info
67 
68 DEFAULT_MODE = MODE_PROD
69 
70 PLATFORMS = [Platform.BINARY_SENSOR, Platform.STT, Platform.TTS]
71 
72 SERVICE_REMOTE_CONNECT = "remote_connect"
73 SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
74 
75 SIGNAL_CLOUD_CONNECTION_STATE: SignalType[CloudConnectionState] = SignalType(
76  "CLOUD_CONNECTION_STATE"
77 )
78 
79 STARTUP_REPAIR_DELAY = 1 # 1 hour
80 
81 ALEXA_ENTITY_SCHEMA = vol.Schema(
82  {
83  vol.Optional(CONF_DESCRIPTION): cv.string,
84  vol.Optional(alexa.CONF_DISPLAY_CATEGORIES): cv.string,
85  vol.Optional(CONF_NAME): cv.string,
86  }
87 )
88 
89 GOOGLE_ENTITY_SCHEMA = vol.Schema(
90  {
91  vol.Optional(CONF_NAME): cv.string,
92  vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
93  vol.Optional(google_assistant.CONF_ROOM_HINT): cv.string,
94  }
95 )
96 
97 ASSISTANT_SCHEMA = vol.Schema(
98  {vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA}
99 )
100 
101 ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend(
102  {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}}
103 )
104 
105 GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend(
106  {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}}
107 )
108 
109 CONFIG_SCHEMA = vol.Schema(
110  {
111  DOMAIN: vol.Schema(
112  {
113  vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(
114  [MODE_DEV, MODE_PROD]
115  ),
116  vol.Optional(CONF_COGNITO_CLIENT_ID): str,
117  vol.Optional(CONF_USER_POOL_ID): str,
118  vol.Optional(CONF_REGION): str,
119  vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
120  vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
121  vol.Optional(CONF_ACCOUNT_LINK_SERVER): str,
122  vol.Optional(CONF_ACCOUNTS_SERVER): str,
123  vol.Optional(CONF_ACME_SERVER): str,
124  vol.Optional(CONF_ALEXA_SERVER): str,
125  vol.Optional(CONF_CLOUDHOOK_SERVER): str,
126  vol.Optional(CONF_RELAYER_SERVER): str,
127  vol.Optional(CONF_REMOTESTATE_SERVER): str,
128  vol.Optional(CONF_THINGTALK_SERVER): str,
129  vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
130  }
131  )
132  },
133  extra=vol.ALLOW_EXTRA,
134 )
135 
136 
138  """Raised when an action requires the cloud but it's not available."""
139 
140 
142  """Raised when an action requires the cloud but it's not connected."""
143 
144 
146  """Cloud connection state."""
147 
148  CLOUD_CONNECTED = "cloud_connected"
149  CLOUD_DISCONNECTED = "cloud_disconnected"
150 
151 
152 @bind_hass
153 @callback
154 def async_is_logged_in(hass: HomeAssistant) -> bool:
155  """Test if user is logged in.
156 
157  Note: This returns True even if not currently connected to the cloud.
158  """
159  return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].is_logged_in
160 
161 
162 @bind_hass
163 @callback
164 def async_is_connected(hass: HomeAssistant) -> bool:
165  """Test if connected to the cloud."""
166  return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].iot.connected
167 
168 
169 @callback
171  hass: HomeAssistant,
172  target: Callable[[CloudConnectionState], Awaitable[None] | None],
173 ) -> Callable[[], None]:
174  """Notify on connection state changes."""
175  return async_dispatcher_connect(hass, SIGNAL_CLOUD_CONNECTION_STATE, target)
176 
177 
178 @bind_hass
179 @callback
180 def async_active_subscription(hass: HomeAssistant) -> bool:
181  """Test if user has an active subscription."""
182  return async_is_logged_in(hass) and not hass.data[DATA_CLOUD].subscription_expired
183 
184 
185 async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
186  """Get or create a cloudhook."""
187  if not async_is_connected(hass):
188  raise CloudNotConnected
189 
190  if not async_is_logged_in(hass):
191  raise CloudNotAvailable
192 
193  cloud = hass.data[DATA_CLOUD]
194  cloudhooks = cloud.client.cloudhooks
195  if hook := cloudhooks.get(webhook_id):
196  return cast(str, hook["cloudhook_url"])
197 
198  return await async_create_cloudhook(hass, webhook_id)
199 
200 
201 @bind_hass
202 async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
203  """Create a cloudhook."""
204  if not async_is_connected(hass):
205  raise CloudNotConnected
206 
207  if not async_is_logged_in(hass):
208  raise CloudNotAvailable
209 
210  cloud = hass.data[DATA_CLOUD]
211  hook = await cloud.cloudhooks.async_create(webhook_id, True)
212  cloudhook_url: str = hook["cloudhook_url"]
213  return cloudhook_url
214 
215 
216 @bind_hass
217 async def async_delete_cloudhook(hass: HomeAssistant, webhook_id: str) -> None:
218  """Delete a cloudhook."""
219  if DATA_CLOUD not in hass.data:
220  raise CloudNotAvailable
221 
222  await hass.data[DATA_CLOUD].cloudhooks.async_delete(webhook_id)
223 
224 
225 @bind_hass
226 @callback
227 def async_remote_ui_url(hass: HomeAssistant) -> str:
228  """Get the remote UI URL."""
229  if not async_is_logged_in(hass):
230  raise CloudNotAvailable
231 
232  if not hass.data[DATA_CLOUD].client.prefs.remote_enabled:
233  raise CloudNotAvailable
234 
235  if not (remote_domain := hass.data[DATA_CLOUD].client.prefs.remote_domain):
236  raise CloudNotAvailable
237 
238  return f"https://{remote_domain}"
239 
240 
241 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
242  """Initialize the Home Assistant cloud."""
243  # Process configs
244  if DOMAIN in config:
245  kwargs = dict(config[DOMAIN])
246  else:
247  kwargs = {CONF_MODE: DEFAULT_MODE}
248 
249  # Alexa/Google custom config
250  alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
251  google_conf = kwargs.pop(CONF_GOOGLE_ACTIONS, None) or GACTIONS_SCHEMA({})
252 
253  # Cloud settings
254  prefs = CloudPreferences(hass)
255  await prefs.async_initialize()
256 
257  # Initialize Cloud
258  websession = async_get_clientsession(hass)
259  client = CloudClient(hass, prefs, websession, alexa_conf, google_conf)
260  cloud = hass.data[DATA_CLOUD] = Cloud(client, **kwargs)
261 
262  async def _shutdown(event: Event) -> None:
263  """Shutdown event."""
264  await cloud.stop()
265 
266  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
267 
269  _setup_services(hass, prefs)
270 
271  async def async_startup_repairs(_: datetime) -> None:
272  """Create repair issues after startup."""
273  if not cloud.is_logged_in:
274  return
275 
276  if subscription_info := await async_subscription_info(cloud):
277  async_manage_legacy_subscription_issue(hass, subscription_info)
278 
279  loaded = False
280  stt_platform_loaded = asyncio.Event()
281  tts_platform_loaded = asyncio.Event()
282  stt_tts_entities_added = asyncio.Event()
283  hass.data[DATA_PLATFORMS_SETUP] = {
284  Platform.STT: stt_platform_loaded,
285  Platform.TTS: tts_platform_loaded,
286  "stt_tts_entities_added": stt_tts_entities_added,
287  }
288 
289  async def _on_start() -> None:
290  """Handle cloud started after login."""
291  nonlocal loaded
292 
293  # Prevent multiple discovery
294  if loaded:
295  return
296  loaded = True
297 
298  await hass.config_entries.flow.async_init(
299  DOMAIN, context={"source": SOURCE_SYSTEM}
300  )
301 
302  async def _on_connect() -> None:
303  """Handle cloud connect."""
305  hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
306  )
307 
308  async def _on_disconnect() -> None:
309  """Handle cloud disconnect."""
311  hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
312  )
313 
314  async def _on_initialized() -> None:
315  """Update preferences."""
316  await prefs.async_update(remote_domain=cloud.remote.instance_domain)
317 
318  cloud.register_on_start(_on_start)
319  cloud.iot.register_on_connect(_on_connect)
320  cloud.iot.register_on_disconnect(_on_disconnect)
321  cloud.register_on_initialized(_on_initialized)
322 
323  await cloud.initialize()
324  http_api.async_setup(hass)
325 
326  account_link.async_setup(hass)
327 
328  # Load legacy tts platform for backwards compatibility.
329  hass.async_create_task(
331  hass,
332  Platform.TTS,
333  DOMAIN,
334  {"platform_loaded": tts_platform_loaded},
335  config,
336  ),
337  eager_start=True,
338  )
339 
341  hass=hass,
342  delay=timedelta(hours=STARTUP_REPAIR_DELAY),
343  action=HassJob(
344  async_startup_repairs, "cloud startup repairs", cancel_on_shutdown=True
345  ),
346  )
347 
348  return True
349 
350 
351 @callback
352 def _remote_handle_prefs_updated(cloud: Cloud[CloudClient]) -> None:
353  """Handle remote preferences updated."""
354  cur_pref = cloud.client.prefs.remote_enabled
355  lock = asyncio.Lock()
356 
357  # Sync remote connection with prefs
358  async def remote_prefs_updated(prefs: CloudPreferences) -> None:
359  """Update remote status."""
360  nonlocal cur_pref
361 
362  async with lock:
363  if prefs.remote_enabled == cur_pref:
364  return
365 
366  if cur_pref := prefs.remote_enabled:
367  await cloud.remote.connect()
368  else:
369  await cloud.remote.disconnect()
370 
371  cloud.client.prefs.async_listen_updates(remote_prefs_updated)
372 
373 
374 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
375  """Set up a config entry."""
376  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
377  stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
378  stt_tts_entities_added.set()
379 
380  return True
381 
382 
383 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
384  """Unload a config entry."""
385  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
386 
387 
388 @callback
389 def _setup_services(hass: HomeAssistant, prefs: CloudPreferences) -> None:
390  """Set up services for cloud component."""
391 
392  async def _service_handler(service: ServiceCall) -> None:
393  """Handle service for cloud."""
394  if service.service == SERVICE_REMOTE_CONNECT:
395  await prefs.async_update(remote_enabled=True)
396  elif service.service == SERVICE_REMOTE_DISCONNECT:
397  await prefs.async_update(remote_enabled=False)
398 
399  async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler)
401  hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
402  )
None async_manage_legacy_subscription_issue(HomeAssistant hass, dict[str, Any] subscription_info)
Definition: repairs.py:30
dict[str, Any]|None async_subscription_info(Cloud[CloudClient] cloud)
Definition: subscription.py:18
str async_remote_ui_url(HomeAssistant hass)
Definition: __init__.py:227
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:374
None async_delete_cloudhook(HomeAssistant hass, str webhook_id)
Definition: __init__.py:217
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:241
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:383
str async_create_cloudhook(HomeAssistant hass, str webhook_id)
Definition: __init__.py:202
bool async_is_logged_in(HomeAssistant hass)
Definition: __init__.py:154
bool async_active_subscription(HomeAssistant hass)
Definition: __init__.py:180
bool async_is_connected(HomeAssistant hass)
Definition: __init__.py:164
str async_get_or_create_cloudhook(HomeAssistant hass, str webhook_id)
Definition: __init__.py:185
None _remote_handle_prefs_updated(Cloud[CloudClient] cloud)
Definition: __init__.py:352
Callable[[], None] async_listen_connection_change(HomeAssistant hass, Callable[[CloudConnectionState], Awaitable[None]|None] target)
Definition: __init__.py:173
None _setup_services(HomeAssistant hass, CloudPreferences prefs)
Definition: __init__.py:389
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None async_load_platform(core.HomeAssistant hass, Platform|str component, str platform, DiscoveryInfoType|None discovered, ConfigType hass_config)
Definition: discovery.py:152
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
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
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121