1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00
ha-core/homeassistant/components/cloud/prefs.py
2022-01-21 09:42:34 -08:00

335 lines
11 KiB
Python

"""Preference management for cloud."""
from __future__ import annotations
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
from homeassistant.components import webhook
from homeassistant.core import callback
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.util.logging import async_create_catching_coro
from .const import (
DEFAULT_ALEXA_REPORT_STATE,
DEFAULT_EXPOSED_DOMAINS,
DEFAULT_GOOGLE_REPORT_STATE,
DEFAULT_TTS_DEFAULT_VOICE,
DOMAIN,
PREF_ALEXA_DEFAULT_EXPOSE,
PREF_ALEXA_ENTITY_CONFIGS,
PREF_ALEXA_REPORT_STATE,
PREF_ALIASES,
PREF_CLOUD_USER,
PREF_CLOUDHOOKS,
PREF_DISABLE_2FA,
PREF_ENABLE_ALEXA,
PREF_ENABLE_GOOGLE,
PREF_ENABLE_REMOTE,
PREF_GOOGLE_DEFAULT_EXPOSE,
PREF_GOOGLE_ENTITY_CONFIGS,
PREF_GOOGLE_LOCAL_WEBHOOK_ID,
PREF_GOOGLE_REPORT_STATE,
PREF_GOOGLE_SECURE_DEVICES_PIN,
PREF_OVERRIDE_NAME,
PREF_REMOTE_DOMAIN,
PREF_SHOULD_EXPOSE,
PREF_TTS_DEFAULT_VOICE,
PREF_USERNAME,
)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
class CloudPreferences:
"""Handle cloud preferences."""
def __init__(self, hass):
"""Initialize cloud prefs."""
self._hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._prefs = None
self._listeners = []
async def async_initialize(self):
"""Finish initializing the preferences."""
if (prefs := await self._store.async_load()) is None:
prefs = self._empty_config("")
self._prefs = prefs
if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs:
await self._save_prefs(
{
**self._prefs,
PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
}
)
@callback
def async_listen_updates(self, listener):
"""Listen for updates to the preferences."""
self._listeners.append(listener)
async def async_update(
self,
*,
google_enabled=UNDEFINED,
alexa_enabled=UNDEFINED,
remote_enabled=UNDEFINED,
google_secure_devices_pin=UNDEFINED,
cloudhooks=UNDEFINED,
cloud_user=UNDEFINED,
google_entity_configs=UNDEFINED,
alexa_entity_configs=UNDEFINED,
alexa_report_state=UNDEFINED,
google_report_state=UNDEFINED,
alexa_default_expose=UNDEFINED,
google_default_expose=UNDEFINED,
tts_default_voice=UNDEFINED,
remote_domain=UNDEFINED,
):
"""Update user preferences."""
prefs = {**self._prefs}
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_ENABLE_REMOTE, remote_enabled),
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
(PREF_GOOGLE_REPORT_STATE, google_report_state),
(PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose),
(PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose),
(PREF_TTS_DEFAULT_VOICE, tts_default_voice),
(PREF_REMOTE_DOMAIN, remote_domain),
):
if value is not UNDEFINED:
prefs[key] = value
await self._save_prefs(prefs)
async def async_update_google_entity_config(
self,
*,
entity_id,
override_name=UNDEFINED,
disable_2fa=UNDEFINED,
aliases=UNDEFINED,
should_expose=UNDEFINED,
):
"""Update config for a Google entity."""
entities = self.google_entity_configs
entity = entities.get(entity_id, {})
changes = {}
for key, value in (
(PREF_OVERRIDE_NAME, override_name),
(PREF_DISABLE_2FA, disable_2fa),
(PREF_ALIASES, aliases),
(PREF_SHOULD_EXPOSE, should_expose),
):
if value is not UNDEFINED:
changes[key] = value
if not changes:
return
updated_entity = {**entity, **changes}
updated_entities = {**entities, entity_id: updated_entity}
await self.async_update(google_entity_configs=updated_entities)
async def async_update_alexa_entity_config(
self, *, entity_id, should_expose=UNDEFINED
):
"""Update config for an Alexa entity."""
entities = self.alexa_entity_configs
entity = entities.get(entity_id, {})
changes = {}
for key, value in ((PREF_SHOULD_EXPOSE, should_expose),):
if value is not UNDEFINED:
changes[key] = value
if not changes:
return
updated_entity = {**entity, **changes}
updated_entities = {**entities, entity_id: updated_entity}
await self.async_update(alexa_entity_configs=updated_entities)
async def async_set_username(self, username) -> bool:
"""Set the username that is logged in."""
# Logging out.
if username is None:
user = await self._load_cloud_user()
if user is not None:
await self._hass.auth.async_remove_user(user)
await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None})
return False
cur_username = self._prefs.get(PREF_USERNAME)
if cur_username == username:
return False
if cur_username is None:
await self._save_prefs({**self._prefs, PREF_USERNAME: username})
else:
await self._save_prefs(self._empty_config(username))
return True
def as_dict(self):
"""Return dictionary version."""
return {
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_ENABLE_ALEXA: self.alexa_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose,
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
}
@property
def remote_enabled(self):
"""Return if remote is enabled on start."""
if not self._prefs.get(PREF_ENABLE_REMOTE, False):
return False
return True
@property
def remote_domain(self):
"""Return remote domain."""
return self._prefs.get(PREF_REMOTE_DOMAIN)
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
return self._prefs[PREF_ENABLE_ALEXA]
@property
def alexa_report_state(self):
"""Return if Alexa report state is enabled."""
return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE)
@property
def alexa_default_expose(self) -> list[str] | None:
"""Return array of entity domains that are exposed by default to Alexa.
Can return None, in which case for backwards should be interpreted as allow all domains.
"""
return self._prefs.get(PREF_ALEXA_DEFAULT_EXPOSE)
@property
def alexa_entity_configs(self):
"""Return Alexa Entity configurations."""
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
@property
def google_enabled(self):
"""Return if Google is enabled."""
return self._prefs[PREF_ENABLE_GOOGLE]
@property
def google_report_state(self):
"""Return if Google report state is enabled."""
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
@property
def google_secure_devices_pin(self):
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
@property
def google_entity_configs(self):
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
@property
def google_local_webhook_id(self):
"""Return Google webhook ID to receive local messages."""
return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
@property
def google_default_expose(self) -> list[str] | None:
"""Return array of entity domains that are exposed by default to Google.
Can return None, in which case for backwards should be interpreted as allow all domains.
"""
return self._prefs.get(PREF_GOOGLE_DEFAULT_EXPOSE)
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""
return self._prefs.get(PREF_CLOUDHOOKS, {})
@property
def tts_default_voice(self):
"""Return the default TTS voice."""
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE)
async def get_cloud_user(self) -> str:
"""Return ID of Home Assistant Cloud system user."""
user = await self._load_cloud_user()
if user:
return user.id
user = await self._hass.auth.async_create_system_user(
"Home Assistant Cloud", group_ids=[GROUP_ID_ADMIN], local_only=True
)
assert user is not None
await self.async_update(cloud_user=user.id)
return user.id
async def _load_cloud_user(self) -> User | None:
"""Load cloud user if available."""
if (user_id := self._prefs.get(PREF_CLOUD_USER)) is None:
return None
# Fetch the user. It can happen that the user no longer exists if
# an image was restored without restoring the cloud prefs.
return await self._hass.auth.async_get_user(user_id)
async def _save_prefs(self, prefs):
"""Save preferences to disk."""
self._prefs = prefs
await self._store.async_save(self._prefs)
for listener in self._listeners:
self._hass.async_create_task(async_create_catching_coro(listener(self)))
@callback
@staticmethod
def _empty_config(username):
"""Return an empty config."""
return {
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
PREF_ALEXA_ENTITY_CONFIGS: {},
PREF_CLOUD_USER: None,
PREF_CLOUDHOOKS: {},
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_REMOTE_DOMAIN: None,
PREF_USERNAME: username,
}