From 327fe63047ce7fb7b25f3393cab1b27f40b0cfd0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Jun 2019 02:17:21 -0700 Subject: [PATCH] Clean up Google Config (#24663) * Clean up Google Config * Lint * pylint * pylint2 --- .../components/cloud/alexa_config.py | 244 +++++++++++++++ homeassistant/components/cloud/client.py | 293 +----------------- .../components/cloud/google_config.py | 52 ++++ .../components/google_assistant/helpers.py | 32 +- .../components/google_assistant/http.py | 59 ++-- tests/components/cloud/__init__.py | 20 +- tests/components/cloud/conftest.py | 2 +- tests/components/cloud/test_client.py | 80 +++-- tests/components/cloud/test_http_api.py | 14 +- tests/components/google_assistant/__init__.py | 31 +- .../google_assistant/test_smart_home.py | 11 +- .../components/google_assistant/test_trait.py | 9 +- 12 files changed, 460 insertions(+), 387 deletions(-) create mode 100644 homeassistant/components/cloud/alexa_config.py create mode 100644 homeassistant/components/cloud/google_config.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py new file mode 100644 index 00000000000..746f01dd04b --- /dev/null +++ b/homeassistant/components/cloud/alexa_config.py @@ -0,0 +1,244 @@ +"""Alexa configuration for Home Assistant Cloud.""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +from hass_nabucasa import cloud_api + +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers import entity_registry +from homeassistant.helpers.event import async_call_later +from homeassistant.util.dt import utcnow +from homeassistant.components.alexa import ( + config as alexa_config, + errors as alexa_errors, + entities as alexa_entities, + state_report as alexa_state_report, +) + + +from .const import ( + CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + RequireRelink +) + +_LOGGER = logging.getLogger(__name__) + +# Time to wait when entity preferences have changed before syncing it to +# the cloud. +SYNC_DELAY = 1 + + +class AlexaConfig(alexa_config.AbstractConfig): + """Alexa Configuration.""" + + def __init__(self, hass, config, prefs, cloud): + """Initialize the Alexa config.""" + super().__init__(hass) + self._config = config + self._prefs = prefs + self._cloud = cloud + self._token = None + self._token_valid = None + self._cur_entity_prefs = prefs.alexa_entity_configs + self._alexa_sync_unsub = None + self._endpoint = None + + prefs.async_listen_updates(self._async_prefs_updated) + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated + ) + + @property + def enabled(self): + """Return if Alexa is enabled.""" + return self._prefs.alexa_enabled + + @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 + def endpoint(self): + """Endpoint for report state.""" + if self._endpoint is None: + raise ValueError("No endpoint available. Fetch access token first") + + return self._endpoint + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + def should_expose(self, entity_id): + """If an entity should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + return False + + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_configs = self._prefs.alexa_entity_configs + entity_config = entity_configs.get(entity_id, {}) + return entity_config.get( + 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 + + raise alexa_errors.NoTokenAvailable + + self._token = body['access_token'] + self._endpoint = body['event_endpoint'] + 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: + if self.should_report_state: + await self.async_enable_proactive_mode() + else: + await self.async_disable_proactive_mode() + + # If entity prefs are the same or we have filter in config.yaml, + # don't sync. + if (self._cur_entity_prefs is prefs.alexa_entity_configs or + not self._config[CONF_FILTER].empty_filter): + return + + if self._alexa_sync_unsub: + self._alexa_sync_unsub() + + self._alexa_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._sync_prefs) + + async def _sync_prefs(self, _now): + """Sync the updated preferences to Alexa.""" + self._alexa_sync_unsub = None + old_prefs = self._cur_entity_prefs + new_prefs = self._prefs.alexa_entity_configs + + seen = set() + to_update = [] + to_remove = [] + + for entity_id, info in old_prefs.items(): + seen.add(entity_id) + old_expose = info.get(PREF_SHOULD_EXPOSE) + + if entity_id in new_prefs: + new_expose = new_prefs[entity_id].get(PREF_SHOULD_EXPOSE) + else: + new_expose = None + + if old_expose == new_expose: + continue + + if new_expose: + to_update.append(entity_id) + else: + to_remove.append(entity_id) + + # Now all the ones that are in new prefs but never were in old prefs + for entity_id, info in new_prefs.items(): + if entity_id in seen: + continue + + new_expose = info.get(PREF_SHOULD_EXPOSE) + + if new_expose is None: + continue + + # Only test if we should expose. It can never be a remove action, + # as it didn't exist in old prefs object. + if new_expose: + to_update.append(entity_id) + + # We only set the prefs when update is successful, that way we will + # retry when next change comes in. + if await self._sync_helper(to_update, to_remove): + self._cur_entity_prefs = new_prefs + + async def async_sync_entities(self): + """Sync all entities to Alexa.""" + to_update = [] + to_remove = [] + + for entity in alexa_entities.async_get_entities(self.hass, self): + if self.should_expose(entity.entity_id): + to_update.append(entity.entity_id) + else: + to_remove.append(entity.entity_id) + + return await self._sync_helper(to_update, to_remove) + + async def _sync_helper(self, to_update, to_remove) -> bool: + """Sync entities to Alexa. + + Return boolean if it was successful. + """ + if not to_update and not to_remove: + return True + + tasks = [] + + if to_update: + tasks.append(alexa_state_report.async_send_add_or_update_message( + self.hass, self, to_update + )) + + if to_remove: + tasks.append(alexa_state_report.async_send_delete_message( + self.hass, self, to_remove + )) + + try: + with async_timeout.timeout(10): + await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) + + return True + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout trying to sync entitites to Alexa") + return False + + except aiohttp.ClientError as err: + _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) + return False + + async def _handle_entity_registry_updated(self, event): + """Handle when entity registry updated.""" + if not self.enabled or not self._cloud.is_logged_in: + return + + action = event.data['action'] + entity_id = event.data['entity_id'] + to_update = [] + to_remove = [] + + if action == 'create' and self.should_expose(entity_id): + to_update.append(entity_id) + elif action == 'remove' and self.should_expose(entity_id): + to_remove.append(entity_id) + + await self._sync_helper(to_update, to_remove) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f8cfc255aa4..16a05b0d127 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -2,257 +2,24 @@ import asyncio from pathlib import Path from typing import Any, Dict -from datetime import timedelta import logging import aiohttp -import async_timeout -from hass_nabucasa import cloud_api from hass_nabucasa.client import CloudClient as Interface from homeassistant.core import callback -from homeassistant.components.alexa import ( - config as alexa_config, - errors as alexa_errors, - smart_home as alexa_sh, - entities as alexa_entities, - state_report as alexa_state_report, -) -from homeassistant.components.google_assistant import ( - helpers as ga_h, smart_home as ga) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES -from homeassistant.helpers.event import async_call_later +from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers import entity_registry from homeassistant.util.aiohttp import MockRequest -from homeassistant.util.dt import utcnow +from homeassistant.components.alexa import smart_home as alexa_sh -from . import utils -from .const import ( - CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE, - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, RequireRelink) +from . import utils, alexa_config, google_config +from .const import DISPATCHER_REMOTE_UPDATE from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) -# Time to wait when entity preferences have changed before syncing it to -# the cloud. -SYNC_DELAY = 1 - - -class AlexaConfig(alexa_config.AbstractConfig): - """Alexa Configuration.""" - - def __init__(self, hass, config, prefs, cloud): - """Initialize the Alexa config.""" - super().__init__(hass) - self._config = config - self._prefs = prefs - self._cloud = cloud - self._token = None - self._token_valid = None - self._cur_entity_prefs = prefs.alexa_entity_configs - self._alexa_sync_unsub = None - self._endpoint = None - - prefs.async_listen_updates(self._async_prefs_updated) - hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - self._handle_entity_registry_updated - ) - - @property - def enabled(self): - """Return if Alexa is enabled.""" - return self._prefs.alexa_enabled - - @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 - def endpoint(self): - """Endpoint for report state.""" - if self._endpoint is None: - raise ValueError("No endpoint available. Fetch access token first") - - return self._endpoint - - @property - def entity_config(self): - """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG, {}) - - def should_expose(self, entity_id): - """If an entity should be exposed.""" - if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: - return False - - if not self._config[CONF_FILTER].empty_filter: - return self._config[CONF_FILTER](entity_id) - - entity_configs = self._prefs.alexa_entity_configs - entity_config = entity_configs.get(entity_id, {}) - return entity_config.get( - 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 - - raise alexa_errors.NoTokenAvailable - - self._token = body['access_token'] - self._endpoint = body['event_endpoint'] - 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: - if self.should_report_state: - await self.async_enable_proactive_mode() - else: - await self.async_disable_proactive_mode() - - # If entity prefs are the same or we have filter in config.yaml, - # don't sync. - if (self._cur_entity_prefs is prefs.alexa_entity_configs or - not self._config[CONF_FILTER].empty_filter): - return - - if self._alexa_sync_unsub: - self._alexa_sync_unsub() - - self._alexa_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._sync_prefs) - - async def _sync_prefs(self, _now): - """Sync the updated preferences to Alexa.""" - self._alexa_sync_unsub = None - old_prefs = self._cur_entity_prefs - new_prefs = self._prefs.alexa_entity_configs - - seen = set() - to_update = [] - to_remove = [] - - for entity_id, info in old_prefs.items(): - seen.add(entity_id) - old_expose = info.get(PREF_SHOULD_EXPOSE) - - if entity_id in new_prefs: - new_expose = new_prefs[entity_id].get(PREF_SHOULD_EXPOSE) - else: - new_expose = None - - if old_expose == new_expose: - continue - - if new_expose: - to_update.append(entity_id) - else: - to_remove.append(entity_id) - - # Now all the ones that are in new prefs but never were in old prefs - for entity_id, info in new_prefs.items(): - if entity_id in seen: - continue - - new_expose = info.get(PREF_SHOULD_EXPOSE) - - if new_expose is None: - continue - - # Only test if we should expose. It can never be a remove action, - # as it didn't exist in old prefs object. - if new_expose: - to_update.append(entity_id) - - # We only set the prefs when update is successful, that way we will - # retry when next change comes in. - if await self._sync_helper(to_update, to_remove): - self._cur_entity_prefs = new_prefs - - async def async_sync_entities(self): - """Sync all entities to Alexa.""" - to_update = [] - to_remove = [] - - for entity in alexa_entities.async_get_entities(self.hass, self): - if self.should_expose(entity.entity_id): - to_update.append(entity.entity_id) - else: - to_remove.append(entity.entity_id) - - return await self._sync_helper(to_update, to_remove) - - async def _sync_helper(self, to_update, to_remove) -> bool: - """Sync entities to Alexa. - - Return boolean if it was successful. - """ - if not to_update and not to_remove: - return True - - tasks = [] - - if to_update: - tasks.append(alexa_state_report.async_send_add_or_update_message( - self.hass, self, to_update - )) - - if to_remove: - tasks.append(alexa_state_report.async_send_delete_message( - self.hass, self, to_remove - )) - - try: - with async_timeout.timeout(10): - await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - - return True - - except asyncio.TimeoutError: - _LOGGER.warning("Timeout trying to sync entitites to Alexa") - return False - - except aiohttp.ClientError as err: - _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) - return False - - async def _handle_entity_registry_updated(self, event): - """Handle when entity registry updated.""" - if not self.enabled or not self._cloud.is_logged_in: - return - - action = event.data['action'] - entity_id = event.data['entity_id'] - to_update = [] - to_remove = [] - - if action == 'create' and self.should_expose(entity_id): - to_update.append(entity_id) - elif action == 'remove' and self.should_expose(entity_id): - to_remove.append(entity_id) - - await self._sync_helper(to_update, to_remove) class CloudClient(Interface): @@ -260,13 +27,14 @@ class CloudClient(Interface): def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences, websession: aiohttp.ClientSession, - alexa_cfg: Dict[str, Any], google_config: Dict[str, Any]): + alexa_user_config: Dict[str, Any], + google_user_config: Dict[str, Any]): """Initialize client interface to Cloud.""" self._hass = hass self._prefs = prefs self._websession = websession - self.google_user_config = google_config - self.alexa_user_config = alexa_cfg + self.google_user_config = google_user_config + self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None self.cloud = None @@ -307,53 +75,22 @@ class CloudClient(Interface): return self._prefs.remote_enabled @property - def alexa_config(self) -> AlexaConfig: + def alexa_config(self) -> alexa_config.AlexaConfig: """Return Alexa config.""" if self._alexa_config is None: - self._alexa_config = AlexaConfig( + assert self.cloud is not None + self._alexa_config = alexa_config.AlexaConfig( self._hass, self.alexa_user_config, self._prefs, self.cloud) return self._alexa_config @property - def google_config(self) -> ga_h.Config: + def google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: - google_conf = self.google_user_config - - def should_expose(entity): - """If an entity should be exposed.""" - if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: - return False - - if not google_conf['filter'].empty_filter: - return google_conf['filter'](entity.entity_id) - - entity_configs = self.prefs.google_entity_configs - entity_config = entity_configs.get(entity.entity_id, {}) - return entity_config.get( - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) - - def should_2fa(entity): - """If an entity should be checked for 2FA.""" - entity_configs = self.prefs.google_entity_configs - entity_config = entity_configs.get(entity.entity_id, {}) - return not entity_config.get( - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) - - username = self._hass.data[DOMAIN].claims["cognito:username"] - - self._google_config = ga_h.Config( - should_expose=should_expose, - should_2fa=should_2fa, - secure_devices_pin=self._prefs.google_secure_devices_pin, - entity_config=google_conf.get(CONF_ENTITY_CONFIG), - agent_user_id=username, - ) - - # Set it to the latest. - self._google_config.secure_devices_pin = \ - self._prefs.google_secure_devices_pin + assert self.cloud is not None + self._google_config = google_config.CloudGoogleConfig( + self.google_user_config, self._prefs, self.cloud) return self._google_config diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py new file mode 100644 index 00000000000..b047d25ee49 --- /dev/null +++ b/homeassistant/components/cloud/google_config.py @@ -0,0 +1,52 @@ +"""Google config for Cloud.""" +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.components.google_assistant.helpers import AbstractConfig + +from .const import ( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + + +class CloudGoogleConfig(AbstractConfig): + """HA Cloud Configuration for Google Assistant.""" + + def __init__(self, config, prefs, cloud): + """Initialize the Alexa config.""" + self._config = config + self._prefs = prefs + self._cloud = cloud + + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return self._cloud.claims["cognito:username"] + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG) + + @property + def secure_devices_pin(self): + """Return entity config.""" + return self._prefs.google_secure_devices_pin + + def should_expose(self, state): + """If an entity should be exposed.""" + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + return False + + if not self._config['filter'].empty_filter: + return self._config['filter'](state.entity_id) + + entity_configs = self._prefs.google_entity_configs + entity_config = entity_configs.get(state.entity_id, {}) + return entity_config.get( + PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + + def should_2fa(self, state): + """If an entity should be checked for 2FA.""" + entity_configs = self._prefs.google_entity_configs + entity_config = entity_configs.get(state.entity_id, {}) + return not entity_config.get( + PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 770a502ad5d..87c4fb78f3a 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -17,24 +17,32 @@ from .const import ( from .error import SmartHomeError -class Config: +class AbstractConfig: """Hold the configuration for Google Assistant.""" - def __init__(self, should_expose, - entity_config=None, secure_devices_pin=None, - agent_user_id=None, should_2fa=None): - """Initialize the configuration.""" - self.should_expose = should_expose - self.entity_config = entity_config or {} - self.secure_devices_pin = secure_devices_pin - self._should_2fa = should_2fa + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return None - # Agent User Id to use for query responses - self.agent_user_id = agent_user_id + @property + def entity_config(self): + """Return entity config.""" + return {} + + @property + def secure_devices_pin(self): + """Return entity config.""" + return None + + def should_expose(self, state) -> bool: + """Return if entity should be exposed.""" + raise NotImplementedError def should_2fa(self, state): """If an entity should have 2FA checked.""" - return self._should_2fa is None or self._should_2fa(state) + # pylint: disable=no-self-use + return True class RequestData: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d385d742c7d..95528eea3ca 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -17,33 +17,50 @@ from .const import ( CONF_SECURE_DEVICES_PIN, ) from .smart_home import async_handle_message -from .helpers import Config +from .helpers import AbstractConfig _LOGGER = logging.getLogger(__name__) -@callback -def async_register_http(hass, cfg): - """Register HTTP views for Google Assistant.""" - expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) - exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) - entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} - secure_devices_pin = cfg.get(CONF_SECURE_DEVICES_PIN) +class GoogleConfig(AbstractConfig): + """Config for manual setup of Google.""" - def is_exposed(entity) -> bool: - """Determine if an entity should be exposed to Google Assistant.""" - if entity.attributes.get('view') is not None: + def __init__(self, config): + """Initialize the config.""" + self._config = config + + @property + def agent_user_id(self): + """Return Agent User Id to use for query responses.""" + return None + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG, {}) + + @property + def secure_devices_pin(self): + """Return entity config.""" + return self._config.get(CONF_SECURE_DEVICES_PIN) + + def should_expose(self, state) -> bool: + """Return if entity should be exposed.""" + expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) + exposed_domains = self._config.get(CONF_EXPOSED_DOMAINS) + + if state.attributes.get('view') is not None: # Ignore entities that are views return False - if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False explicit_expose = \ - entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) + self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - expose_by_default and entity.domain in exposed_domains + expose_by_default and state.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being @@ -53,13 +70,15 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - config = Config( - should_expose=is_exposed, - entity_config=entity_config, - secure_devices_pin=secure_devices_pin - ) + def should_2fa(self, state): + """If an entity should have 2FA checked.""" + return True - hass.http.register_view(GoogleAssistantView(config)) + +@callback +def async_register_http(hass, cfg): + """Register HTTP views for Google Assistant.""" + hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg))) class GoogleAssistantView(HomeAssistantView): diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 08ab5324b97..3f2b8f034cd 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,24 +1,22 @@ """Tests for the cloud component.""" from unittest.mock import patch + from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.cloud import const -from jose import jwt - from tests.common import mock_coro -def mock_cloud(hass, config={}): +async def mock_cloud(hass, config=None): """Mock cloud.""" - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - assert hass.loop.run_until_complete(async_setup_component( - hass, cloud.DOMAIN, { - 'cloud': config - })) - - hass.data[cloud.DOMAIN]._decode_claims = \ - lambda token: jwt.get_unverified_claims(token) + assert await async_setup_component( + hass, cloud.DOMAIN, { + 'cloud': config or {} + }) + cloud_inst = hass.data['cloud'] + with patch('hass_nabucasa.Cloud.run_executor', return_value=mock_coro()): + await cloud_inst.start() def mock_cloud_prefs(hass, prefs={}): diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index c9fd6360929..87ef6809fdd 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -18,7 +18,7 @@ def mock_user_data(): @pytest.fixture def mock_cloud_fixture(hass): """Fixture for cloud component.""" - mock_cloud(hass) + hass.loop.run_until_complete(mock_cloud(hass)) return mock_cloud_prefs(hass) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 7d1afda7e6a..fa42bda32db 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -9,7 +9,7 @@ import pytest from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.components.cloud import ( - DOMAIN, ALEXA_SCHEMA, client) + DOMAIN, ALEXA_SCHEMA, alexa_config) from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) from homeassistant.util.dt import utcnow @@ -17,11 +17,11 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro, async_fire_time_changed -from . import mock_cloud_prefs +from . import mock_cloud_prefs, mock_cloud @pytest.fixture -def mock_cloud(): +def mock_cloud_inst(): """Mock cloud class.""" return MagicMock(subscription_expired=False) @@ -29,10 +29,7 @@ def mock_cloud(): @pytest.fixture async def mock_cloud_setup(hass): """Set up the cloud.""" - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - assert await async_setup_component(hass, 'cloud', { - 'cloud': {} - }) + await mock_cloud(hass) @pytest.fixture @@ -52,24 +49,20 @@ async def test_handler_alexa(hass): hass.states.async_set( 'switch.test2', 'on', {'friendly_name': "Test switch 2"}) - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - setup = await async_setup_component(hass, 'cloud', { - 'cloud': { - 'alexa': { - 'filter': { - 'exclude_entities': 'switch.test2' - }, - 'entity_config': { - 'switch.test': { - 'name': 'Config name', - 'description': 'Config description', - 'display_categories': 'LIGHT' - } - } + await mock_cloud(hass, { + 'alexa': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'description': 'Config description', + 'display_categories': 'LIGHT' } } - }) - assert setup + } + }) mock_cloud_prefs(hass) cloud = hass.data['cloud'] @@ -110,24 +103,20 @@ async def test_handler_google_actions(hass): hass.states.async_set( 'group.all_locks', 'on', {'friendly_name': "Evil locks"}) - with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): - setup = await async_setup_component(hass, 'cloud', { - 'cloud': { - 'google_actions': { - 'filter': { - 'exclude_entities': 'switch.test2' - }, - 'entity_config': { - 'switch.test': { - 'name': 'Config name', - 'aliases': 'Config alias', - 'room': 'living room' - } - } + await mock_cloud(hass, { + 'google_actions': { + 'filter': { + 'exclude_entities': 'switch.test2' + }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'aliases': 'Config alias', + 'room': 'living room' } } - }) - assert setup + } + }) mock_cloud_prefs(hass) cloud = hass.data['cloud'] @@ -265,7 +254,7 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): await cloud_prefs.async_update(alexa_entity_configs={ 'light.kitchen': entity_conf }) - conf = client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) assert not conf.should_expose('light.kitchen') entity_conf['should_expose'] = True @@ -274,7 +263,7 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): 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) + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) assert cloud_prefs.alexa_report_state is False assert conf.should_report_state is False @@ -307,9 +296,9 @@ def patch_sync_helper(): to_remove = [] with patch( - 'homeassistant.components.cloud.client.SYNC_DELAY', 0 + 'homeassistant.components.cloud.alexa_config.SYNC_DELAY', 0 ), patch( - 'homeassistant.components.cloud.client.AlexaConfig._sync_helper', + 'homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper', side_effect=mock_coro ) as mock_helper: yield to_update, to_remove @@ -321,7 +310,7 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" - client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -354,7 +343,8 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" - client.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 60346dc6ea1..55cd9e9e2e5 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -14,10 +14,11 @@ from homeassistant.components.cloud.const import ( PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, DOMAIN) from homeassistant.components.google_assistant.helpers import ( - GoogleEntity, Config) + GoogleEntity) from homeassistant.components.alexa.entities import LightCapabilities from tests.common import mock_coro +from tests.components.google_assistant import MockConfig from . import mock_cloud, mock_cloud_prefs @@ -45,7 +46,7 @@ def mock_cloud_login(hass, setup_api): @pytest.fixture(autouse=True) def setup_api(hass, aioclient_mock): """Initialize HTTP API.""" - mock_cloud(hass, { + hass.loop.run_until_complete(mock_cloud(hass, { 'mode': 'development', 'cognito_client_id': 'cognito_client_id', 'user_pool_id': 'user_pool_id', @@ -63,7 +64,7 @@ def setup_api(hass, aioclient_mock): 'include_entities': ['light.kitchen', 'switch.ac'] } } - }) + })) return mock_cloud_prefs(hass) @@ -709,9 +710,10 @@ async def test_list_google_entities( hass, hass_ws_client, setup_api, mock_cloud_login): """Test that we can list Google entities.""" client = await hass_ws_client(hass) - entity = GoogleEntity(hass, Config(lambda *_: False), State( - 'light.kitchen', 'on' - )) + entity = GoogleEntity( + hass, MockConfig(should_expose=lambda *_: False), State( + 'light.kitchen', 'on' + )) with patch('homeassistant.components.google_assistant.helpers' '.async_get_entities', return_value=[entity]): await client.send_json({ diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f3732c12213..c7930f3c62f 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,6 +1,33 @@ - - """Tests for the Google Assistant integration.""" +from homeassistant.components.google_assistant import helpers + + +class MockConfig(helpers.AbstractConfig): + """Fake config that always exposes everything.""" + + def __init__(self, *, secure_devices_pin=None, should_expose=None, + entity_config=None): + """Initialize config.""" + self._should_expose = should_expose + self._secure_devices_pin = secure_devices_pin + self._entity_config = entity_config or {} + + @property + def secure_devices_pin(self): + """Return secure devices pin.""" + return self._secure_devices_pin + + @property + def entity_config(self): + """Return secure devices pin.""" + return self._entity_config + + def should_expose(self, state): + """Expose it all.""" + return self._should_expose is None or self._should_expose(state) + + +BASIC_CONFIG = MockConfig() DEMO_DEVICES = [{ 'id': diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index a65387d48a2..cfe7b946611 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -11,7 +11,7 @@ from homeassistant.components.climate.const import ( ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE ) from homeassistant.components.google_assistant import ( - const, trait, helpers, smart_home as sh, + const, trait, smart_home as sh, EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover @@ -23,9 +23,8 @@ from homeassistant.helpers import device_registry from tests.common import (mock_device_registry, mock_registry, mock_area_registry, mock_coro) -BASIC_CONFIG = helpers.Config( - should_expose=lambda state: True, -) +from . import BASIC_CONFIG, MockConfig + REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -57,7 +56,7 @@ async def test_sync_message(hass): # Excluded via config hass.states.async_set('light.not_expose', 'on') - config = helpers.Config( + config = MockConfig( should_expose=lambda state: state.entity_id != 'light.not_expose', entity_config={ 'light.demo_light': { @@ -145,7 +144,7 @@ async def test_sync_in_area(hass, registries): light.entity_id = entity.entity_id await light.async_update_ha_state() - config = helpers.Config( + config = MockConfig( should_expose=lambda _: True, entity_config={} ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 6b1b6a7c9f4..d2d216a9fc5 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -29,10 +29,8 @@ from homeassistant.const import ( from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE from homeassistant.util import color from tests.common import async_mock_service, mock_coro +from . import BASIC_CONFIG, MockConfig -BASIC_CONFIG = helpers.Config( - should_expose=lambda state: True, -) REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -42,8 +40,7 @@ BASIC_DATA = helpers.RequestData( REQ_ID, ) -PIN_CONFIG = helpers.Config( - should_expose=lambda state: True, +PIN_CONFIG = MockConfig( secure_devices_pin='1234' ) @@ -927,7 +924,7 @@ async def test_lock_unlock_unlock(hass): # Test with 2FA override with patch('homeassistant.components.google_assistant.helpers' - '.Config.should_2fa', return_value=False): + '.AbstractConfig.should_2fa', return_value=False): await trt.execute( trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {}) assert len(calls) == 2