Cloud: Add Alexa report state (#24536)
* Cloud: Add Alexa report state * Lint * Lint * Only track state changes when we are logged in
This commit is contained in:
parent
5ab1996d3f
commit
a02b69db38
17 changed files with 207 additions and 33 deletions
|
@ -1,14 +1,26 @@
|
||||||
"""Config helpers for Alexa."""
|
"""Config helpers for Alexa."""
|
||||||
|
from .state_report import async_enable_proactive_mode
|
||||||
|
|
||||||
|
|
||||||
class AbstractConfig:
|
class AbstractConfig:
|
||||||
"""Hold the configuration for Alexa."""
|
"""Hold the configuration for Alexa."""
|
||||||
|
|
||||||
|
_unsub_proactive_report = None
|
||||||
|
|
||||||
|
def __init__(self, hass):
|
||||||
|
"""Initialize abstract config."""
|
||||||
|
self.hass = hass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_auth(self):
|
def supports_auth(self):
|
||||||
"""Return if config supports auth."""
|
"""Return if config supports auth."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_report_state(self):
|
||||||
|
"""Return if states should be proactively reported."""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
"""Endpoint for report state."""
|
"""Endpoint for report state."""
|
||||||
|
@ -19,6 +31,30 @@ class AbstractConfig:
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_reporting_states(self):
|
||||||
|
"""Return if proactive mode is enabled."""
|
||||||
|
return self._unsub_proactive_report is not None
|
||||||
|
|
||||||
|
async def async_enable_proactive_mode(self):
|
||||||
|
"""Enable proactive mode."""
|
||||||
|
if self._unsub_proactive_report is None:
|
||||||
|
self._unsub_proactive_report = self.hass.async_create_task(
|
||||||
|
async_enable_proactive_mode(self.hass, self)
|
||||||
|
)
|
||||||
|
resp = await self._unsub_proactive_report
|
||||||
|
|
||||||
|
# Failed to start reporting.
|
||||||
|
if resp is None:
|
||||||
|
self._unsub_proactive_report = None
|
||||||
|
|
||||||
|
async def async_disable_proactive_mode(self):
|
||||||
|
"""Disable proactive mode."""
|
||||||
|
unsub_func = await self._unsub_proactive_report
|
||||||
|
if unsub_func:
|
||||||
|
unsub_func()
|
||||||
|
self._unsub_proactive_report = None
|
||||||
|
|
||||||
def should_expose(self, entity_id):
|
def should_expose(self, entity_id):
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
|
@ -87,7 +87,9 @@ async def async_api_accept_grant(hass, config, directive, context):
|
||||||
|
|
||||||
if config.supports_auth:
|
if config.supports_auth:
|
||||||
await config.async_accept_grant(auth_code)
|
await config.async_accept_grant(auth_code)
|
||||||
await async_enable_proactive_mode(hass, config)
|
|
||||||
|
if config.should_report_state:
|
||||||
|
await async_enable_proactive_mode(hass, config)
|
||||||
|
|
||||||
return directive.response(
|
return directive.response(
|
||||||
name='AcceptGrant.Response',
|
name='AcceptGrant.Response',
|
||||||
|
|
|
@ -25,6 +25,7 @@ class AlexaConfig(AbstractConfig):
|
||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize Alexa config."""
|
"""Initialize Alexa config."""
|
||||||
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
|
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
|
||||||
|
@ -38,6 +39,11 @@ class AlexaConfig(AbstractConfig):
|
||||||
"""Return if config supports auth."""
|
"""Return if config supports auth."""
|
||||||
return self._auth is not None
|
return self._auth is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_report_state(self):
|
||||||
|
"""Return if we should proactively report states."""
|
||||||
|
return self._auth is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
"""Endpoint for report state."""
|
"""Endpoint for report state."""
|
||||||
|
@ -73,7 +79,7 @@ async def async_setup(hass, config):
|
||||||
smart_home_config = AlexaConfig(hass, config)
|
smart_home_config = AlexaConfig(hass, config)
|
||||||
hass.http.register_view(SmartHomeView(smart_home_config))
|
hass.http.register_view(SmartHomeView(smart_home_config))
|
||||||
|
|
||||||
if smart_home_config.supports_auth:
|
if smart_home_config.should_report_state:
|
||||||
await async_enable_proactive_mode(hass, smart_home_config)
|
await async_enable_proactive_mode(hass, smart_home_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,24 +21,23 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||||
|
|
||||||
Proactive mode makes this component report state changes to Alexa.
|
Proactive mode makes this component report state changes to Alexa.
|
||||||
"""
|
"""
|
||||||
if smart_home_config.async_get_access_token is None:
|
|
||||||
# no function to call to get token
|
|
||||||
return
|
|
||||||
|
|
||||||
if await smart_home_config.async_get_access_token() is None:
|
if await smart_home_config.async_get_access_token() is None:
|
||||||
# not ready yet
|
# not ready yet
|
||||||
return
|
return
|
||||||
|
|
||||||
async def async_entity_state_listener(changed_entity, old_state,
|
async def async_entity_state_listener(changed_entity, old_state,
|
||||||
new_state):
|
new_state):
|
||||||
if not smart_home_config.should_expose(changed_entity):
|
if not new_state:
|
||||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
|
||||||
changed_entity)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_state.domain not in ENTITY_ADAPTERS:
|
if new_state.domain not in ENTITY_ADAPTERS:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not smart_home_config.should_expose(changed_entity):
|
||||||
|
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||||
|
changed_entity)
|
||||||
|
return
|
||||||
|
|
||||||
alexa_changed_entity = \
|
alexa_changed_entity = \
|
||||||
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
|
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
|
||||||
new_state)
|
new_state)
|
||||||
|
@ -49,7 +48,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||||
alexa_changed_entity)
|
alexa_changed_entity)
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.helpers.event.async_track_state_change(
|
return hass.helpers.event.async_track_state_change(
|
||||||
MATCH_ALL, async_entity_state_listener
|
MATCH_ALL, async_entity_state_listener
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,7 +93,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
|
||||||
allow_redirects=True)
|
allow_redirects=True)
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Timeout calling LWA to get auth token.")
|
_LOGGER.error("Timeout sending report to Alexa.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
response_text = await response.text()
|
response_text = await response.text()
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from hass_nabucasa import cloud_api
|
||||||
from hass_nabucasa.client import CloudClient as Interface
|
from hass_nabucasa.client import CloudClient as Interface
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -17,22 +20,41 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.util.aiohttp import MockRequest
|
from homeassistant.util.aiohttp import MockRequest
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
|
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
|
||||||
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
|
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
|
||||||
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, RequireRelink)
|
||||||
from .prefs import CloudPreferences
|
from .prefs import CloudPreferences
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AlexaConfig(alexa_config.AbstractConfig):
|
class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
"""Alexa Configuration."""
|
"""Alexa Configuration."""
|
||||||
|
|
||||||
def __init__(self, config, prefs):
|
def __init__(self, hass, config, prefs, cloud):
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Alexa config."""
|
||||||
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
|
self._cloud = cloud
|
||||||
|
self._token = None
|
||||||
|
self._token_valid = None
|
||||||
|
prefs.async_listen_updates(self.async_prefs_updated)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supports_auth(self):
|
||||||
|
"""Return if config supports auth."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_report_state(self):
|
||||||
|
"""Return if states should be proactively reported."""
|
||||||
|
return self._prefs.alexa_report_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
|
@ -57,6 +79,34 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
return entity_config.get(
|
return entity_config.get(
|
||||||
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
|
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
|
||||||
|
|
||||||
|
async def async_get_access_token(self):
|
||||||
|
"""Get an access token."""
|
||||||
|
if self._token_valid is not None and self._token_valid < utcnow():
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
resp = await cloud_api.async_alexa_access_token(self._cloud)
|
||||||
|
body = await resp.json()
|
||||||
|
|
||||||
|
if resp.status == 400:
|
||||||
|
if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'):
|
||||||
|
raise RequireRelink
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._token = body['access_token']
|
||||||
|
self._token_valid = utcnow() + timedelta(seconds=body['expires_in'])
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
async def async_prefs_updated(self, prefs):
|
||||||
|
"""Handle updated preferences."""
|
||||||
|
if self.should_report_state == self.is_reporting_states:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.should_report_state:
|
||||||
|
await self.async_enable_proactive_mode()
|
||||||
|
else:
|
||||||
|
await self.async_disable_proactive_mode()
|
||||||
|
|
||||||
|
|
||||||
class CloudClient(Interface):
|
class CloudClient(Interface):
|
||||||
"""Interface class for Home Assistant Cloud."""
|
"""Interface class for Home Assistant Cloud."""
|
||||||
|
@ -70,9 +120,9 @@ class CloudClient(Interface):
|
||||||
self._websession = websession
|
self._websession = websession
|
||||||
self.google_user_config = google_config
|
self.google_user_config = google_config
|
||||||
self.alexa_user_config = alexa_cfg
|
self.alexa_user_config = alexa_cfg
|
||||||
|
self._alexa_config = None
|
||||||
self.alexa_config = AlexaConfig(alexa_cfg, prefs)
|
|
||||||
self._google_config = None
|
self._google_config = None
|
||||||
|
self.cloud = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_path(self) -> Path:
|
def base_path(self) -> Path:
|
||||||
|
@ -109,6 +159,15 @@ class CloudClient(Interface):
|
||||||
"""Return true if we want start a remote connection."""
|
"""Return true if we want start a remote connection."""
|
||||||
return self._prefs.remote_enabled
|
return self._prefs.remote_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alexa_config(self) -> AlexaConfig:
|
||||||
|
"""Return Alexa config."""
|
||||||
|
if self._alexa_config is None:
|
||||||
|
self._alexa_config = AlexaConfig(
|
||||||
|
self._hass, self.alexa_user_config, self._prefs, self.cloud)
|
||||||
|
|
||||||
|
return self._alexa_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_config(self) -> ga_h.Config:
|
def google_config(self) -> ga_h.Config:
|
||||||
"""Return Google config."""
|
"""Return Google config."""
|
||||||
|
@ -151,6 +210,13 @@ class CloudClient(Interface):
|
||||||
|
|
||||||
return self._google_config
|
return self._google_config
|
||||||
|
|
||||||
|
async def async_initialize(self, cloud) -> None:
|
||||||
|
"""Initialize the client."""
|
||||||
|
self.cloud = cloud
|
||||||
|
|
||||||
|
if self.alexa_config.should_report_state and self.cloud.is_logged_in:
|
||||||
|
await self.alexa_config.async_enable_proactive_mode()
|
||||||
|
|
||||||
async def cleanups(self) -> None:
|
async def cleanups(self) -> None:
|
||||||
"""Cleanup some stuff after logout."""
|
"""Cleanup some stuff after logout."""
|
||||||
self._google_config = None
|
self._google_config = None
|
||||||
|
|
|
@ -10,12 +10,14 @@ PREF_CLOUDHOOKS = 'cloudhooks'
|
||||||
PREF_CLOUD_USER = 'cloud_user'
|
PREF_CLOUD_USER = 'cloud_user'
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
|
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
|
||||||
PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs'
|
PREF_ALEXA_ENTITY_CONFIGS = 'alexa_entity_configs'
|
||||||
|
PREF_ALEXA_REPORT_STATE = 'alexa_report_state'
|
||||||
PREF_OVERRIDE_NAME = 'override_name'
|
PREF_OVERRIDE_NAME = 'override_name'
|
||||||
PREF_DISABLE_2FA = 'disable_2fa'
|
PREF_DISABLE_2FA = 'disable_2fa'
|
||||||
PREF_ALIASES = 'aliases'
|
PREF_ALIASES = 'aliases'
|
||||||
PREF_SHOULD_EXPOSE = 'should_expose'
|
PREF_SHOULD_EXPOSE = 'should_expose'
|
||||||
DEFAULT_SHOULD_EXPOSE = True
|
DEFAULT_SHOULD_EXPOSE = True
|
||||||
DEFAULT_DISABLE_2FA = False
|
DEFAULT_DISABLE_2FA = False
|
||||||
|
DEFAULT_ALEXA_REPORT_STATE = False
|
||||||
|
|
||||||
CONF_ALEXA = 'alexa'
|
CONF_ALEXA = 'alexa'
|
||||||
CONF_ALIASES = 'aliases'
|
CONF_ALIASES = 'aliases'
|
||||||
|
@ -43,3 +45,7 @@ class InvalidTrustedNetworks(Exception):
|
||||||
|
|
||||||
class InvalidTrustedProxies(Exception):
|
class InvalidTrustedProxies(Exception):
|
||||||
"""Raised when invalid trusted proxies config."""
|
"""Raised when invalid trusted proxies config."""
|
||||||
|
|
||||||
|
|
||||||
|
class RequireRelink(Exception):
|
||||||
|
"""The skill needs to be relinked."""
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.components.google_assistant import helpers as google_helpers
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks,
|
PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks,
|
||||||
InvalidTrustedProxies)
|
InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -363,6 +363,7 @@ async def websocket_subscription(hass, connection, msg):
|
||||||
vol.Required('type'): 'cloud/update_prefs',
|
vol.Required('type'): 'cloud/update_prefs',
|
||||||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||||
|
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
||||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||||
})
|
})
|
||||||
async def websocket_update_prefs(hass, connection, msg):
|
async def websocket_update_prefs(hass, connection, msg):
|
||||||
|
@ -424,7 +425,6 @@ def _account_data(cloud):
|
||||||
'prefs': client.prefs.as_dict(),
|
'prefs': client.prefs.as_dict(),
|
||||||
'google_entities': client.google_user_config['filter'].config,
|
'google_entities': client.google_user_config['filter'].config,
|
||||||
'alexa_entities': client.alexa_user_config['filter'].config,
|
'alexa_entities': client.alexa_user_config['filter'].config,
|
||||||
'alexa_domains': list(alexa_entities.ENTITY_ADAPTERS),
|
|
||||||
'remote_domain': remote.instance_domain,
|
'remote_domain': remote.instance_domain,
|
||||||
'remote_connected': remote.is_connected,
|
'remote_connected': remote.is_connected,
|
||||||
'remote_certificate': certificate,
|
'remote_certificate': certificate,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Cloud",
|
"name": "Cloud",
|
||||||
"documentation": "https://www.home-assistant.io/components/cloud",
|
"documentation": "https://www.home-assistant.io/components/cloud",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"hass-nabucasa==0.14"
|
"hass-nabucasa==0.15"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"http",
|
"http",
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
"""Preference management for cloud."""
|
"""Preference management for cloud."""
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.util.logging import async_create_catching_coro
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
|
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
|
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
|
||||||
PREF_ALIASES, PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS,
|
PREF_ALIASES, PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS,
|
||||||
|
PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE,
|
||||||
InvalidTrustedNetworks, InvalidTrustedProxies)
|
InvalidTrustedNetworks, InvalidTrustedProxies)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
|
@ -21,6 +25,7 @@ class CloudPreferences:
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
self._prefs = None
|
self._prefs = None
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self):
|
||||||
"""Finish initializing the preferences."""
|
"""Finish initializing the preferences."""
|
||||||
|
@ -40,11 +45,17 @@ class CloudPreferences:
|
||||||
|
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_listen_updates(self, listener):
|
||||||
|
"""Listen for updates to the preferences."""
|
||||||
|
self._listeners.append(listener)
|
||||||
|
|
||||||
async def async_update(self, *, google_enabled=_UNDEF,
|
async def async_update(self, *, google_enabled=_UNDEF,
|
||||||
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
|
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
|
||||||
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
|
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
|
||||||
cloud_user=_UNDEF, google_entity_configs=_UNDEF,
|
cloud_user=_UNDEF, google_entity_configs=_UNDEF,
|
||||||
alexa_entity_configs=_UNDEF):
|
alexa_entity_configs=_UNDEF,
|
||||||
|
alexa_report_state=_UNDEF):
|
||||||
"""Update user preferences."""
|
"""Update user preferences."""
|
||||||
for key, value in (
|
for key, value in (
|
||||||
(PREF_ENABLE_GOOGLE, google_enabled),
|
(PREF_ENABLE_GOOGLE, google_enabled),
|
||||||
|
@ -55,18 +66,26 @@ class CloudPreferences:
|
||||||
(PREF_CLOUD_USER, cloud_user),
|
(PREF_CLOUD_USER, cloud_user),
|
||||||
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
||||||
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
||||||
|
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
|
||||||
):
|
):
|
||||||
if value is not _UNDEF:
|
if value is not _UNDEF:
|
||||||
self._prefs[key] = value
|
self._prefs[key] = value
|
||||||
|
|
||||||
if remote_enabled is True and self._has_local_trusted_network:
|
if remote_enabled is True and self._has_local_trusted_network:
|
||||||
|
self._prefs[PREF_ENABLE_REMOTE] = False
|
||||||
raise InvalidTrustedNetworks
|
raise InvalidTrustedNetworks
|
||||||
|
|
||||||
if remote_enabled is True and self._has_local_trusted_proxies:
|
if remote_enabled is True and self._has_local_trusted_proxies:
|
||||||
|
self._prefs[PREF_ENABLE_REMOTE] = False
|
||||||
raise InvalidTrustedProxies
|
raise InvalidTrustedProxies
|
||||||
|
|
||||||
await self._store.async_save(self._prefs)
|
await self._store.async_save(self._prefs)
|
||||||
|
|
||||||
|
for listener in self._listeners:
|
||||||
|
self._hass.async_create_task(
|
||||||
|
async_create_catching_coro(listener(self))
|
||||||
|
)
|
||||||
|
|
||||||
async def async_update_google_entity_config(
|
async def async_update_google_entity_config(
|
||||||
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
|
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
|
||||||
aliases=_UNDEF, should_expose=_UNDEF):
|
aliases=_UNDEF, should_expose=_UNDEF):
|
||||||
|
@ -134,6 +153,7 @@ class CloudPreferences:
|
||||||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
||||||
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
||||||
|
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
|
||||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||||
PREF_CLOUD_USER: self.cloud_user,
|
PREF_CLOUD_USER: self.cloud_user,
|
||||||
}
|
}
|
||||||
|
@ -156,6 +176,12 @@ class CloudPreferences:
|
||||||
"""Return if Alexa is enabled."""
|
"""Return if Alexa is enabled."""
|
||||||
return self._prefs[PREF_ENABLE_ALEXA]
|
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
|
@property
|
||||||
def google_enabled(self):
|
def google_enabled(self):
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
|
|
|
@ -9,7 +9,7 @@ bcrypt==3.1.6
|
||||||
certifi>=2018.04.16
|
certifi>=2018.04.16
|
||||||
cryptography==2.6.1
|
cryptography==2.6.1
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
hass-nabucasa==0.14
|
hass-nabucasa==0.15
|
||||||
home-assistant-frontend==20190614.0
|
home-assistant-frontend==20190614.0
|
||||||
importlib-metadata==0.15
|
importlib-metadata==0.15
|
||||||
jinja2>=2.10
|
jinja2>=2.10
|
||||||
|
|
|
@ -562,7 +562,7 @@ habitipy==0.2.0
|
||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.14
|
hass-nabucasa==0.15
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.4
|
hbmqtt==0.9.4
|
||||||
|
|
|
@ -145,7 +145,7 @@ ha-ffmpeg==2.0
|
||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.14
|
hass-nabucasa==0.15
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.4
|
hbmqtt==0.9.4
|
||||||
|
|
|
@ -38,7 +38,7 @@ class MockConfig(config.AbstractConfig):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = MockConfig()
|
DEFAULT_CONFIG = MockConfig(None)
|
||||||
|
|
||||||
|
|
||||||
def get_new_request(namespace, name, endpoint=None):
|
def get_new_request(namespace, name, endpoint=None):
|
||||||
|
|
|
@ -1012,7 +1012,7 @@ async def test_exclude_filters(hass):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
|
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
|
||||||
|
|
||||||
alexa_config = MockConfig()
|
alexa_config = MockConfig(hass)
|
||||||
alexa_config.should_expose = entityfilter.generate_filter(
|
alexa_config.should_expose = entityfilter.generate_filter(
|
||||||
include_domains=[],
|
include_domains=[],
|
||||||
include_entities=[],
|
include_entities=[],
|
||||||
|
@ -1045,7 +1045,7 @@ async def test_include_filters(hass):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
||||||
|
|
||||||
alexa_config = MockConfig()
|
alexa_config = MockConfig(hass)
|
||||||
alexa_config.should_expose = entityfilter.generate_filter(
|
alexa_config.should_expose = entityfilter.generate_filter(
|
||||||
include_domains=['automation', 'group'],
|
include_domains=['automation', 'group'],
|
||||||
include_entities=['script.deny'],
|
include_entities=['script.deny'],
|
||||||
|
@ -1072,7 +1072,7 @@ async def test_never_exposed_entities(hass):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
||||||
|
|
||||||
alexa_config = MockConfig()
|
alexa_config = MockConfig(hass)
|
||||||
alexa_config.should_expose = entityfilter.generate_filter(
|
alexa_config.should_expose = entityfilter.generate_filter(
|
||||||
include_domains=['group'],
|
include_domains=['group'],
|
||||||
include_entities=[],
|
include_entities=[],
|
||||||
|
@ -1155,7 +1155,7 @@ async def test_entity_config(hass):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
'light.test_1', 'on', {'friendly_name': "Test light 1"})
|
'light.test_1', 'on', {'friendly_name': "Test light 1"})
|
||||||
|
|
||||||
alexa_config = MockConfig()
|
alexa_config = MockConfig(hass)
|
||||||
alexa_config.entity_config = {
|
alexa_config.entity_config = {
|
||||||
'light.test_1': {
|
'light.test_1': {
|
||||||
'name': 'Config name',
|
'name': 'Config name',
|
||||||
|
|
|
@ -3,6 +3,8 @@ import pytest
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.cloud import prefs
|
||||||
|
|
||||||
from . import mock_cloud, mock_cloud_prefs
|
from . import mock_cloud, mock_cloud_prefs
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,3 +20,11 @@ def mock_cloud_fixture(hass):
|
||||||
"""Fixture for cloud component."""
|
"""Fixture for cloud component."""
|
||||||
mock_cloud(hass)
|
mock_cloud(hass)
|
||||||
return mock_cloud_prefs(hass)
|
return mock_cloud_prefs(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def cloud_prefs(hass):
|
||||||
|
"""Fixture for cloud preferences."""
|
||||||
|
cloud_prefs = prefs.CloudPreferences(hass)
|
||||||
|
await cloud_prefs.async_initialize()
|
||||||
|
return cloud_prefs
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components.cloud import (
|
from homeassistant.components.cloud import (
|
||||||
DOMAIN, ALEXA_SCHEMA, prefs, client)
|
DOMAIN, ALEXA_SCHEMA, client)
|
||||||
from homeassistant.components.cloud.const import (
|
from homeassistant.components.cloud.const import (
|
||||||
PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE)
|
PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE)
|
||||||
from tests.components.alexa import test_smart_home as test_alexa
|
from tests.components.alexa import test_smart_home as test_alexa
|
||||||
|
@ -254,18 +254,41 @@ async def test_google_config_should_2fa(
|
||||||
assert not cloud_client.google_config.should_2fa(state)
|
assert not cloud_client.google_config.should_2fa(state)
|
||||||
|
|
||||||
|
|
||||||
async def test_alexa_config_expose_entity_prefs(hass):
|
async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs):
|
||||||
"""Test Alexa config should expose using prefs."""
|
"""Test Alexa config should expose using prefs."""
|
||||||
cloud_prefs = prefs.CloudPreferences(hass)
|
|
||||||
await cloud_prefs.async_initialize()
|
|
||||||
entity_conf = {
|
entity_conf = {
|
||||||
'should_expose': False
|
'should_expose': False
|
||||||
}
|
}
|
||||||
await cloud_prefs.async_update(alexa_entity_configs={
|
await cloud_prefs.async_update(alexa_entity_configs={
|
||||||
'light.kitchen': entity_conf
|
'light.kitchen': entity_conf
|
||||||
})
|
})
|
||||||
conf = client.AlexaConfig(ALEXA_SCHEMA({}), cloud_prefs)
|
conf = client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
||||||
|
|
||||||
assert not conf.should_expose('light.kitchen')
|
assert not conf.should_expose('light.kitchen')
|
||||||
entity_conf['should_expose'] = True
|
entity_conf['should_expose'] = True
|
||||||
assert conf.should_expose('light.kitchen')
|
assert conf.should_expose('light.kitchen')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_alexa_config_report_state(hass, cloud_prefs):
|
||||||
|
"""Test Alexa config should expose using prefs."""
|
||||||
|
conf = client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is False
|
||||||
|
assert conf.should_report_state is False
|
||||||
|
assert conf.is_reporting_states is False
|
||||||
|
|
||||||
|
with patch.object(conf, 'async_get_access_token',
|
||||||
|
return_value=mock_coro("hello")):
|
||||||
|
await cloud_prefs.async_update(alexa_report_state=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is True
|
||||||
|
assert conf.should_report_state is True
|
||||||
|
assert conf.is_reporting_states is True
|
||||||
|
|
||||||
|
await cloud_prefs.async_update(alexa_report_state=False)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is False
|
||||||
|
assert conf.should_report_state is False
|
||||||
|
assert conf.is_reporting_states is False
|
||||||
|
|
|
@ -363,6 +363,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
|
||||||
'google_entity_configs': {},
|
'google_entity_configs': {},
|
||||||
'google_secure_devices_pin': None,
|
'google_secure_devices_pin': None,
|
||||||
'alexa_entity_configs': {},
|
'alexa_entity_configs': {},
|
||||||
|
'alexa_report_state': False,
|
||||||
'remote_enabled': False,
|
'remote_enabled': False,
|
||||||
},
|
},
|
||||||
'alexa_entities': {
|
'alexa_entities': {
|
||||||
|
@ -371,7 +372,6 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
|
||||||
'exclude_domains': [],
|
'exclude_domains': [],
|
||||||
'exclude_entities': [],
|
'exclude_entities': [],
|
||||||
},
|
},
|
||||||
'alexa_domains': ['switch'],
|
|
||||||
'google_entities': {
|
'google_entities': {
|
||||||
'include_domains': ['light'],
|
'include_domains': ['light'],
|
||||||
'include_entities': [],
|
'include_entities': [],
|
||||||
|
|
Loading…
Add table
Reference in a new issue