Clean up Google Config (#24663)

* Clean up Google Config

* Lint

* pylint

* pylint2
This commit is contained in:
Paulus Schoutsen 2019-06-21 02:17:21 -07:00 committed by Pascal Vizeli
parent d468d0f71b
commit 78b7ed0ebe
12 changed files with 460 additions and 387 deletions

View File

@ -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)

View File

@ -2,257 +2,24 @@
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 logging
import aiohttp import aiohttp
import async_timeout
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
from homeassistant.components.alexa import ( from homeassistant.components.google_assistant import smart_home as ga
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.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.helpers import entity_registry
from homeassistant.util.aiohttp import MockRequest 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 . import utils, alexa_config, google_config
from .const import ( from .const import DISPATCHER_REMOTE_UPDATE
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, RequireRelink)
from .prefs import CloudPreferences from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__) _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): class CloudClient(Interface):
@ -260,13 +27,14 @@ class CloudClient(Interface):
def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences, def __init__(self, hass: HomeAssistantType, prefs: CloudPreferences,
websession: aiohttp.ClientSession, 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.""" """Initialize client interface to Cloud."""
self._hass = hass self._hass = hass
self._prefs = prefs self._prefs = prefs
self._websession = websession self._websession = websession
self.google_user_config = google_config self.google_user_config = google_user_config
self.alexa_user_config = alexa_cfg self.alexa_user_config = alexa_user_config
self._alexa_config = None self._alexa_config = None
self._google_config = None self._google_config = None
self.cloud = None self.cloud = None
@ -307,53 +75,22 @@ class CloudClient(Interface):
return self._prefs.remote_enabled return self._prefs.remote_enabled
@property @property
def alexa_config(self) -> AlexaConfig: def alexa_config(self) -> alexa_config.AlexaConfig:
"""Return Alexa config.""" """Return Alexa config."""
if self._alexa_config is None: 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) self._hass, self.alexa_user_config, self._prefs, self.cloud)
return self._alexa_config return self._alexa_config
@property @property
def google_config(self) -> ga_h.Config: def google_config(self) -> google_config.CloudGoogleConfig:
"""Return Google config.""" """Return Google config."""
if not self._google_config: if not self._google_config:
google_conf = self.google_user_config assert self.cloud is not None
self._google_config = google_config.CloudGoogleConfig(
def should_expose(entity): self.google_user_config, self._prefs, self.cloud)
"""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
return self._google_config return self._google_config

View File

@ -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)

View File

@ -17,24 +17,32 @@ from .const import (
from .error import SmartHomeError from .error import SmartHomeError
class Config: class AbstractConfig:
"""Hold the configuration for Google Assistant.""" """Hold the configuration for Google Assistant."""
def __init__(self, should_expose, @property
entity_config=None, secure_devices_pin=None, def agent_user_id(self):
agent_user_id=None, should_2fa=None): """Return Agent User Id to use for query responses."""
"""Initialize the configuration.""" return None
self.should_expose = should_expose
self.entity_config = entity_config or {}
self.secure_devices_pin = secure_devices_pin
self._should_2fa = should_2fa
# Agent User Id to use for query responses @property
self.agent_user_id = agent_user_id 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): def should_2fa(self, state):
"""If an entity should have 2FA checked.""" """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: class RequestData:

View File

@ -17,33 +17,50 @@ from .const import (
CONF_SECURE_DEVICES_PIN, CONF_SECURE_DEVICES_PIN,
) )
from .smart_home import async_handle_message from .smart_home import async_handle_message
from .helpers import Config from .helpers import AbstractConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@callback class GoogleConfig(AbstractConfig):
def async_register_http(hass, cfg): """Config for manual setup of Google."""
"""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)
def is_exposed(entity) -> bool: def __init__(self, config):
"""Determine if an entity should be exposed to Google Assistant.""" """Initialize the config."""
if entity.attributes.get('view') is not None: 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 # Ignore entities that are views
return False return False
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False return False
explicit_expose = \ 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 = \ 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 # Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being # 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 return is_default_exposed or explicit_expose
config = Config( def should_2fa(self, state):
should_expose=is_exposed, """If an entity should have 2FA checked."""
entity_config=entity_config, return True
secure_devices_pin=secure_devices_pin
)
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): class GoogleAssistantView(HomeAssistantView):

View File

@ -1,24 +1,22 @@
"""Tests for the cloud component.""" """Tests for the cloud component."""
from unittest.mock import patch from unittest.mock import patch
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.components import cloud from homeassistant.components import cloud
from homeassistant.components.cloud import const from homeassistant.components.cloud import const
from jose import jwt
from tests.common import mock_coro from tests.common import mock_coro
def mock_cloud(hass, config={}): async def mock_cloud(hass, config=None):
"""Mock cloud.""" """Mock cloud."""
with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): assert await async_setup_component(
assert hass.loop.run_until_complete(async_setup_component(
hass, cloud.DOMAIN, { hass, cloud.DOMAIN, {
'cloud': config 'cloud': config or {}
})) })
cloud_inst = hass.data['cloud']
hass.data[cloud.DOMAIN]._decode_claims = \ with patch('hass_nabucasa.Cloud.run_executor', return_value=mock_coro()):
lambda token: jwt.get_unverified_claims(token) await cloud_inst.start()
def mock_cloud_prefs(hass, prefs={}): def mock_cloud_prefs(hass, prefs={}):

View File

@ -18,7 +18,7 @@ def mock_user_data():
@pytest.fixture @pytest.fixture
def mock_cloud_fixture(hass): def mock_cloud_fixture(hass):
"""Fixture for cloud component.""" """Fixture for cloud component."""
mock_cloud(hass) hass.loop.run_until_complete(mock_cloud(hass))
return mock_cloud_prefs(hass) return mock_cloud_prefs(hass)

View File

@ -9,7 +9,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, client) DOMAIN, ALEXA_SCHEMA, alexa_config)
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 homeassistant.util.dt import utcnow 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.components.alexa import test_smart_home as test_alexa
from tests.common import mock_coro, async_fire_time_changed from tests.common import mock_coro, async_fire_time_changed
from . import mock_cloud_prefs from . import mock_cloud_prefs, mock_cloud
@pytest.fixture @pytest.fixture
def mock_cloud(): def mock_cloud_inst():
"""Mock cloud class.""" """Mock cloud class."""
return MagicMock(subscription_expired=False) return MagicMock(subscription_expired=False)
@ -29,10 +29,7 @@ def mock_cloud():
@pytest.fixture @pytest.fixture
async def mock_cloud_setup(hass): async def mock_cloud_setup(hass):
"""Set up the cloud.""" """Set up the cloud."""
with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): await mock_cloud(hass)
assert await async_setup_component(hass, 'cloud', {
'cloud': {}
})
@pytest.fixture @pytest.fixture
@ -52,9 +49,7 @@ async def test_handler_alexa(hass):
hass.states.async_set( hass.states.async_set(
'switch.test2', 'on', {'friendly_name': "Test switch 2"}) 'switch.test2', 'on', {'friendly_name': "Test switch 2"})
with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): await mock_cloud(hass, {
setup = await async_setup_component(hass, 'cloud', {
'cloud': {
'alexa': { 'alexa': {
'filter': { 'filter': {
'exclude_entities': 'switch.test2' 'exclude_entities': 'switch.test2'
@ -67,9 +62,7 @@ async def test_handler_alexa(hass):
} }
} }
} }
}
}) })
assert setup
mock_cloud_prefs(hass) mock_cloud_prefs(hass)
cloud = hass.data['cloud'] cloud = hass.data['cloud']
@ -110,9 +103,7 @@ async def test_handler_google_actions(hass):
hass.states.async_set( hass.states.async_set(
'group.all_locks', 'on', {'friendly_name': "Evil locks"}) 'group.all_locks', 'on', {'friendly_name': "Evil locks"})
with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): await mock_cloud(hass, {
setup = await async_setup_component(hass, 'cloud', {
'cloud': {
'google_actions': { 'google_actions': {
'filter': { 'filter': {
'exclude_entities': 'switch.test2' 'exclude_entities': 'switch.test2'
@ -125,9 +116,7 @@ async def test_handler_google_actions(hass):
} }
} }
} }
}
}) })
assert setup
mock_cloud_prefs(hass) mock_cloud_prefs(hass)
cloud = hass.data['cloud'] 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={ await cloud_prefs.async_update(alexa_entity_configs={
'light.kitchen': entity_conf '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') assert not conf.should_expose('light.kitchen')
entity_conf['should_expose'] = True 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): async def test_alexa_config_report_state(hass, cloud_prefs):
"""Test Alexa config should expose using 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 cloud_prefs.alexa_report_state is False
assert conf.should_report_state is False assert conf.should_report_state is False
@ -307,9 +296,9 @@ def patch_sync_helper():
to_remove = [] to_remove = []
with patch( with patch(
'homeassistant.components.cloud.client.SYNC_DELAY', 0 'homeassistant.components.cloud.alexa_config.SYNC_DELAY', 0
), patch( ), patch(
'homeassistant.components.cloud.client.AlexaConfig._sync_helper', 'homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper',
side_effect=mock_coro side_effect=mock_coro
) as mock_helper: ) as mock_helper:
yield to_update, to_remove yield to_update, to_remove
@ -321,7 +310,7 @@ def patch_sync_helper():
async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs):
"""Test Alexa config responds to updating exposed entities.""" """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): with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update_alexa_entity_config( 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): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
"""Test Alexa config responds to entity registry.""" """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): with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, {

View File

@ -14,10 +14,11 @@ from homeassistant.components.cloud.const import (
PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN,
DOMAIN) DOMAIN)
from homeassistant.components.google_assistant.helpers import ( from homeassistant.components.google_assistant.helpers import (
GoogleEntity, Config) GoogleEntity)
from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.alexa.entities import LightCapabilities
from tests.common import mock_coro from tests.common import mock_coro
from tests.components.google_assistant import MockConfig
from . import mock_cloud, mock_cloud_prefs from . import mock_cloud, mock_cloud_prefs
@ -45,7 +46,7 @@ def mock_cloud_login(hass, setup_api):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_api(hass, aioclient_mock): def setup_api(hass, aioclient_mock):
"""Initialize HTTP API.""" """Initialize HTTP API."""
mock_cloud(hass, { hass.loop.run_until_complete(mock_cloud(hass, {
'mode': 'development', 'mode': 'development',
'cognito_client_id': 'cognito_client_id', 'cognito_client_id': 'cognito_client_id',
'user_pool_id': 'user_pool_id', 'user_pool_id': 'user_pool_id',
@ -63,7 +64,7 @@ def setup_api(hass, aioclient_mock):
'include_entities': ['light.kitchen', 'switch.ac'] 'include_entities': ['light.kitchen', 'switch.ac']
} }
} }
}) }))
return mock_cloud_prefs(hass) return mock_cloud_prefs(hass)
@ -709,7 +710,8 @@ async def test_list_google_entities(
hass, hass_ws_client, setup_api, mock_cloud_login): hass, hass_ws_client, setup_api, mock_cloud_login):
"""Test that we can list Google entities.""" """Test that we can list Google entities."""
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
entity = GoogleEntity(hass, Config(lambda *_: False), State( entity = GoogleEntity(
hass, MockConfig(should_expose=lambda *_: False), State(
'light.kitchen', 'on' 'light.kitchen', 'on'
)) ))
with patch('homeassistant.components.google_assistant.helpers' with patch('homeassistant.components.google_assistant.helpers'

View File

@ -1,6 +1,33 @@
"""Tests for the Google Assistant integration.""" """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 = [{ DEMO_DEVICES = [{
'id': 'id':

View File

@ -11,7 +11,7 @@ from homeassistant.components.climate.const import (
ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE
) )
from homeassistant.components.google_assistant import ( 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) EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED)
from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.binary_sensor import DemoBinarySensor
from homeassistant.components.demo.cover import DemoCover 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, from tests.common import (mock_device_registry, mock_registry,
mock_area_registry, mock_coro) mock_area_registry, mock_coro)
BASIC_CONFIG = helpers.Config( from . import BASIC_CONFIG, MockConfig
should_expose=lambda state: True,
)
REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
@ -57,7 +56,7 @@ async def test_sync_message(hass):
# Excluded via config # Excluded via config
hass.states.async_set('light.not_expose', 'on') hass.states.async_set('light.not_expose', 'on')
config = helpers.Config( config = MockConfig(
should_expose=lambda state: state.entity_id != 'light.not_expose', should_expose=lambda state: state.entity_id != 'light.not_expose',
entity_config={ entity_config={
'light.demo_light': { 'light.demo_light': {
@ -145,7 +144,7 @@ async def test_sync_in_area(hass, registries):
light.entity_id = entity.entity_id light.entity_id = entity.entity_id
await light.async_update_ha_state() await light.async_update_ha_state()
config = helpers.Config( config = MockConfig(
should_expose=lambda _: True, should_expose=lambda _: True,
entity_config={} entity_config={}
) )

View File

@ -29,10 +29,8 @@ from homeassistant.const import (
from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE
from homeassistant.util import color from homeassistant.util import color
from tests.common import async_mock_service, mock_coro 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' REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
@ -42,8 +40,7 @@ BASIC_DATA = helpers.RequestData(
REQ_ID, REQ_ID,
) )
PIN_CONFIG = helpers.Config( PIN_CONFIG = MockConfig(
should_expose=lambda state: True,
secure_devices_pin='1234' secure_devices_pin='1234'
) )
@ -927,7 +924,7 @@ async def test_lock_unlock_unlock(hass):
# Test with 2FA override # Test with 2FA override
with patch('homeassistant.components.google_assistant.helpers' with patch('homeassistant.components.google_assistant.helpers'
'.Config.should_2fa', return_value=False): '.AbstractConfig.should_2fa', return_value=False):
await trt.execute( await trt.execute(
trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {}) trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {})
assert len(calls) == 2 assert len(calls) == 2