1 """Component to integrate the Home Assistant cloud."""
3 from __future__
import annotations
6 from collections.abc
import Awaitable, Callable
7 from datetime
import datetime, timedelta
9 from typing
import cast
11 from hass_nabucasa
import Cloud
12 import voluptuous
as vol
21 EVENT_HOMEASSISTANT_STOP,
30 async_dispatcher_connect,
31 async_dispatcher_send,
39 from .
import account_link, http_api
40 from .client
import CloudClient
42 CONF_ACCOUNT_LINK_SERVER,
48 CONF_CLOUDHOOK_SERVER,
49 CONF_COGNITO_CLIENT_ID,
54 CONF_REMOTESTATE_SERVER,
55 CONF_SERVICEHANDLERS_SERVER,
56 CONF_THINGTALK_SERVER,
64 from .prefs
import CloudPreferences
65 from .repairs
import async_manage_legacy_subscription_issue
66 from .subscription
import async_subscription_info
68 DEFAULT_MODE = MODE_PROD
70 PLATFORMS = [Platform.BINARY_SENSOR, Platform.STT, Platform.TTS]
72 SERVICE_REMOTE_CONNECT =
"remote_connect"
73 SERVICE_REMOTE_DISCONNECT =
"remote_disconnect"
75 SIGNAL_CLOUD_CONNECTION_STATE: SignalType[CloudConnectionState] =
SignalType(
76 "CLOUD_CONNECTION_STATE"
79 STARTUP_REPAIR_DELAY = 1
81 ALEXA_ENTITY_SCHEMA = vol.Schema(
83 vol.Optional(CONF_DESCRIPTION): cv.string,
84 vol.Optional(alexa.CONF_DISPLAY_CATEGORIES): cv.string,
85 vol.Optional(CONF_NAME): cv.string,
89 GOOGLE_ENTITY_SCHEMA = vol.Schema(
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,
97 ASSISTANT_SCHEMA = vol.Schema(
98 {vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA}
101 ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend(
102 {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}}
105 GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend(
106 {vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}}
109 CONFIG_SCHEMA = vol.Schema(
113 vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(
114 [MODE_DEV, MODE_PROD]
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,
133 extra=vol.ALLOW_EXTRA,
138 """Raised when an action requires the cloud but it's not available."""
142 """Raised when an action requires the cloud but it's not connected."""
146 """Cloud connection state."""
148 CLOUD_CONNECTED =
"cloud_connected"
149 CLOUD_DISCONNECTED =
"cloud_disconnected"
155 """Test if user is logged in.
157 Note: This returns True even if not currently connected to the cloud.
159 return DATA_CLOUD
in hass.data
and hass.data[DATA_CLOUD].is_logged_in
165 """Test if connected to the cloud."""
166 return DATA_CLOUD
in hass.data
and hass.data[DATA_CLOUD].iot.connected
172 target: Callable[[CloudConnectionState], Awaitable[
None] |
None],
173 ) -> Callable[[],
None]:
174 """Notify on connection state changes."""
181 """Test if user has an active subscription."""
186 """Get or create a cloudhook."""
188 raise CloudNotConnected
191 raise CloudNotAvailable
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"])
203 """Create a cloudhook."""
205 raise CloudNotConnected
208 raise CloudNotAvailable
210 cloud = hass.data[DATA_CLOUD]
211 hook = await cloud.cloudhooks.async_create(webhook_id,
True)
212 cloudhook_url: str = hook[
"cloudhook_url"]
218 """Delete a cloudhook."""
219 if DATA_CLOUD
not in hass.data:
220 raise CloudNotAvailable
222 await hass.data[DATA_CLOUD].cloudhooks.async_delete(webhook_id)
228 """Get the remote UI URL."""
230 raise CloudNotAvailable
232 if not hass.data[DATA_CLOUD].client.prefs.remote_enabled:
233 raise CloudNotAvailable
235 if not (remote_domain := hass.data[DATA_CLOUD].client.prefs.remote_domain):
236 raise CloudNotAvailable
238 return f
"https://{remote_domain}"
241 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
242 """Initialize the Home Assistant cloud."""
245 kwargs =
dict(config[DOMAIN])
247 kwargs = {CONF_MODE: DEFAULT_MODE}
250 alexa_conf = kwargs.pop(CONF_ALEXA,
None)
or ALEXA_SCHEMA({})
251 google_conf = kwargs.pop(CONF_GOOGLE_ACTIONS,
None)
or GACTIONS_SCHEMA({})
255 await prefs.async_initialize()
259 client =
CloudClient(hass, prefs, websession, alexa_conf, google_conf)
260 cloud = hass.data[DATA_CLOUD] = Cloud(client, **kwargs)
262 async
def _shutdown(event: Event) ->
None:
263 """Shutdown event."""
266 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
271 async
def async_startup_repairs(_: datetime) ->
None:
272 """Create repair issues after startup."""
273 if not cloud.is_logged_in:
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,
289 async
def _on_start() -> None:
290 """Handle cloud started after login."""
298 await hass.config_entries.flow.async_init(
299 DOMAIN, context={
"source": SOURCE_SYSTEM}
302 async
def _on_connect() -> None:
303 """Handle cloud connect."""
305 hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
308 async
def _on_disconnect() -> None:
309 """Handle cloud disconnect."""
311 hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
314 async
def _on_initialized() -> None:
315 """Update preferences."""
316 await prefs.async_update(remote_domain=cloud.remote.instance_domain)
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)
323 await cloud.initialize()
324 http_api.async_setup(hass)
326 account_link.async_setup(hass)
329 hass.async_create_task(
334 {
"platform_loaded": tts_platform_loaded},
342 delay=
timedelta(hours=STARTUP_REPAIR_DELAY),
344 async_startup_repairs,
"cloud startup repairs", cancel_on_shutdown=
True
353 """Handle remote preferences updated."""
354 cur_pref = cloud.client.prefs.remote_enabled
355 lock = asyncio.Lock()
358 async
def remote_prefs_updated(prefs: CloudPreferences) ->
None:
359 """Update remote status."""
363 if prefs.remote_enabled == cur_pref:
366 if cur_pref := prefs.remote_enabled:
367 await cloud.remote.connect()
369 await cloud.remote.disconnect()
371 cloud.client.prefs.async_listen_updates(remote_prefs_updated)
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()
384 """Unload a config entry."""
385 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
390 """Set up services for cloud component."""
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)
401 hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
None async_manage_legacy_subscription_issue(HomeAssistant hass, dict[str, Any] subscription_info)
dict[str, Any]|None async_subscription_info(Cloud[CloudClient] cloud)
str async_remote_ui_url(HomeAssistant hass)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_delete_cloudhook(HomeAssistant hass, str webhook_id)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
str async_create_cloudhook(HomeAssistant hass, str webhook_id)
bool async_is_logged_in(HomeAssistant hass)
bool async_active_subscription(HomeAssistant hass)
bool async_is_connected(HomeAssistant hass)
str async_get_or_create_cloudhook(HomeAssistant hass, str webhook_id)
None _remote_handle_prefs_updated(Cloud[CloudClient] cloud)
Callable[[], None] async_listen_connection_change(HomeAssistant hass, Callable[[CloudConnectionState], Awaitable[None]|None] target)
None _setup_services(HomeAssistant hass, CloudPreferences prefs)
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)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
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)
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))