Add Google Report State (#27112)

* Add Google Report State

* UPDATE codeowners"

* Add config option for dev mode

* update library

* lint

* Bug fixes
This commit is contained in:
Paulus Schoutsen 2019-10-03 04:02:38 -07:00 committed by Pascal Vizeli
parent 3e99743244
commit f184bf4d85
26 changed files with 510 additions and 56 deletions

View file

@ -16,6 +16,7 @@ homeassistant/scripts/check_config.py @kellerza
homeassistant/components/adguard/* @frenck
homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alexa/* @home-assistant/cloud
homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480
homeassistant/components/ambiclimate/* @danielhiversen
@ -106,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff
homeassistant/components/gntp/* @robbiet480
homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton
homeassistant/components/google_translate/* @awarecan
homeassistant/components/google_travel_time/* @robbiet480

View file

@ -3,8 +3,6 @@
"name": "Alexa",
"documentation": "https://www.home-assistant.io/integrations/alexa",
"requirements": [],
"dependencies": [
"http"
],
"codeowners": []
"dependencies": ["http"],
"codeowners": ["@home-assistant/cloud"]
}

View file

@ -34,6 +34,7 @@ from .const import (
CONF_REMOTE_API_URL,
CONF_SUBSCRIPTION_INFO_URL,
CONF_USER_POOL_ID,
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL,
DOMAIN,
MODE_DEV,
MODE_PROD,
@ -96,7 +97,8 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str,
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(),
vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(),
}
)
},

View file

@ -98,7 +98,7 @@ class CloudClient(Interface):
if not self._google_config:
assert self.cloud is not None
self._google_config = google_config.CloudGoogleConfig(
self.google_user_config, self._prefs, self.cloud
self._hass, self.google_user_config, self._prefs, self.cloud
)
return self._google_config
@ -107,13 +107,17 @@ class CloudClient(Interface):
"""Initialize the client."""
self.cloud = cloud
if not self.alexa_config.should_report_state or not self.cloud.is_logged_in:
if not self.cloud.is_logged_in:
return
try:
await self.alexa_config.async_enable_proactive_mode()
except alexa_errors.NoTokenAvailable:
pass
if self.alexa_config.should_report_state:
try:
await self.alexa_config.async_enable_proactive_mode()
except alexa_errors.NoTokenAvailable:
pass
if self.google_config.should_report_state:
self.google_config.async_enable_report_state()
async def cleanups(self) -> None:
"""Cleanup some stuff after logout."""

View file

@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin"
PREF_CLOUDHOOKS = "cloudhooks"
PREF_CLOUD_USER = "cloud_user"
PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs"
PREF_GOOGLE_REPORT_STATE = "google_report_state"
PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs"
PREF_ALEXA_REPORT_STATE = "alexa_report_state"
PREF_OVERRIDE_NAME = "override_name"
@ -18,6 +19,7 @@ PREF_SHOULD_EXPOSE = "should_expose"
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = False
DEFAULT_GOOGLE_REPORT_STATE = False
CONF_ALEXA = "alexa"
CONF_ALIASES = "aliases"
@ -33,6 +35,7 @@ CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url"
CONF_REMOTE_API_URL = "remote_api_url"
CONF_ACME_DIRECTORY_SERVER = "acme_directory_server"
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url"
MODE_DEV = "development"
MODE_PROD = "production"

View file

@ -1,6 +1,13 @@
"""Google config for Cloud."""
import asyncio
import logging
import async_timeout
from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.helpers import entity_registry
from .const import (
PREF_SHOULD_EXPOSE,
@ -10,15 +17,31 @@ from .const import (
DEFAULT_DISABLE_2FA,
)
_LOGGER = logging.getLogger(__name__)
class CloudGoogleConfig(AbstractConfig):
"""HA Cloud Configuration for Google Assistant."""
def __init__(self, config, prefs, cloud):
"""Initialize the Alexa config."""
def __init__(self, hass, config, prefs, cloud):
"""Initialize the Google config."""
super().__init__(hass)
self._config = config
self._prefs = prefs
self._cloud = cloud
self._cur_entity_prefs = self._prefs.google_entity_configs
self._sync_entities_lock = asyncio.Lock()
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 Google is enabled."""
return self._prefs.google_enabled
@property
def agent_user_id(self):
@ -35,16 +58,25 @@ class CloudGoogleConfig(AbstractConfig):
"""Return entity config."""
return self._prefs.google_secure_devices_pin
@property
def should_report_state(self):
"""Return if states should be proactively reported."""
return self._prefs.google_report_state
def should_expose(self, state):
"""If an entity should be exposed."""
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
"""If a state object should be exposed."""
return self._should_expose_entity_id(state.entity_id)
def _should_expose_entity_id(self, entity_id):
"""If an entity ID should be exposed."""
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
if not self._config["filter"].empty_filter:
return self._config["filter"](state.entity_id)
return self._config["filter"](entity_id)
entity_configs = self._prefs.google_entity_configs
entity_config = entity_configs.get(state.entity_id, {})
entity_config = entity_configs.get(entity_id, {})
return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
def should_2fa(self, state):
@ -52,3 +84,72 @@ class CloudGoogleConfig(AbstractConfig):
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)
async def async_report_state(self, message):
"""Send a state report to Google."""
try:
await self._cloud.google_report_state.async_send_message(message)
except ErrorResponse as err:
_LOGGER.warning("Error reporting state - %s: %s", err.code, err.message)
async def _async_request_sync_devices(self):
"""Trigger a sync with Google."""
if self._sync_entities_lock.locked():
return 200
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
async with self._sync_entities_lock:
with async_timeout.timeout(10):
await self._cloud.auth.async_check_token()
_LOGGER.debug("Requesting sync")
with async_timeout.timeout(30):
req = await websession.post(
self._cloud.google_actions_sync_url,
headers={"authorization": self._cloud.id_token},
)
_LOGGER.debug("Finished requesting syncing: %s", req.status)
return req.status
async def async_deactivate_report_state(self):
"""Turn off report state and disable further state reporting.
Called when the user disconnects their account from Google.
"""
await self._prefs.async_update(google_report_state=False)
async def _async_prefs_updated(self, prefs):
"""Handle updated preferences."""
if self.should_report_state != self.is_reporting_state:
if self.should_report_state:
self.async_enable_report_state()
else:
self.async_disable_report_state()
# State reporting is reported as a property on entities.
# So when we change it, we need to sync all entities.
await self.async_sync_entities()
return
# If entity prefs are the same or we have filter in config.yaml,
# don't sync.
if (
self._cur_entity_prefs is prefs.google_entity_configs
or not self._config["filter"].empty_filter
):
return
self.async_schedule_google_sync()
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
entity_id = event.data["entity_id"]
# Schedule a sync if a change was made to an entity that Google knows about
if self._should_expose_entity_id(entity_id):
await self.async_sync_entities()

View file

@ -7,6 +7,7 @@ import attr
import aiohttp
import async_timeout
import voluptuous as vol
from hass_nabucasa import Cloud
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
@ -28,6 +29,7 @@ from .const import (
InvalidTrustedNetworks,
InvalidTrustedProxies,
PREF_ALEXA_REPORT_STATE,
PREF_GOOGLE_REPORT_STATE,
RequireRelink,
)
@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView):
async def post(self, request):
"""Trigger a Google Actions sync."""
hass = request.app["hass"]
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT):
await hass.async_add_job(cloud.auth.check_token)
with async_timeout.timeout(REQUEST_TIMEOUT):
req = await websession.post(
cloud.google_actions_sync_url, headers={"authorization": cloud.id_token}
)
return self.json({}, status_code=req.status)
cloud: Cloud = hass.data[DOMAIN]
status = await cloud.client.google_config.async_sync_entities()
return self.json({}, status_code=status)
class CloudLoginView(HomeAssistantView):
@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg):
vol.Optional(PREF_ENABLE_GOOGLE): bool,
vol.Optional(PREF_ENABLE_ALEXA): bool,
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
}
)

View file

@ -2,7 +2,7 @@
"domain": "cloud",
"name": "Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud",
"requirements": ["hass-nabucasa==0.17"],
"requirements": ["hass-nabucasa==0.22"],
"dependencies": ["http", "webhook"],
"codeowners": ["@home-assistant/cloud"]
}

View file

@ -20,6 +20,8 @@ from .const import (
PREF_ALEXA_ENTITY_CONFIGS,
PREF_ALEXA_REPORT_STATE,
DEFAULT_ALEXA_REPORT_STATE,
PREF_GOOGLE_REPORT_STATE,
DEFAULT_GOOGLE_REPORT_STATE,
InvalidTrustedNetworks,
InvalidTrustedProxies,
)
@ -74,6 +76,7 @@ class CloudPreferences:
google_entity_configs=_UNDEF,
alexa_entity_configs=_UNDEF,
alexa_report_state=_UNDEF,
google_report_state=_UNDEF,
):
"""Update user preferences."""
for key, value in (
@ -86,6 +89,7 @@ class CloudPreferences:
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
(PREF_GOOGLE_REPORT_STATE, google_report_state),
):
if value is not _UNDEF:
self._prefs[key] = value
@ -164,6 +168,7 @@ class CloudPreferences:
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_CLOUD_USER: self.cloud_user,
}
@ -196,6 +201,11 @@ class CloudPreferences:
"""Return if Google is enabled."""
return self._prefs[PREF_ENABLE_GOOGLE]
@property
def google_report_state(self):
"""Return if Google report state is enabled."""
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
@property
def google_secure_devices_pin(self):
"""Return if Google is allowed to unlock locks."""

View file

@ -83,6 +83,13 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
try:
with async_timeout.timeout(15):
agent_user_id = call.data.get("agent_user_id") or call.context.user_id
if agent_user_id is None:
_LOGGER.warning(
"No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id."
)
return
res = await websession.post(
REQUEST_SYNC_BASE_URL,
params={"key": api_key},

View file

@ -3,7 +3,8 @@ from asyncio import gather
from collections.abc import Mapping
from typing import List
from homeassistant.core import Context, callback
from homeassistant.core import Context, callback, HomeAssistant, State
from homeassistant.helpers.event import async_call_later
from homeassistant.const import (
CONF_NAME,
STATE_UNAVAILABLE,
@ -22,10 +23,24 @@ from .const import (
)
from .error import SmartHomeError
SYNC_DELAY = 15
class AbstractConfig:
"""Hold the configuration for Google Assistant."""
_unsub_report_state = None
def __init__(self, hass):
"""Initialize abstract config."""
self.hass = hass
self._google_sync_unsub = None
@property
def enabled(self):
"""Return if Google is enabled."""
return False
@property
def agent_user_id(self):
"""Return Agent User Id to use for query responses."""
@ -41,6 +56,17 @@ class AbstractConfig:
"""Return entity config."""
return None
@property
def is_reporting_state(self):
"""Return if we're actively reporting states."""
return self._unsub_report_state is not None
@property
def should_report_state(self):
"""Return if states should be proactively reported."""
# pylint: disable=no-self-use
return False
def should_expose(self, state) -> bool:
"""Return if entity should be exposed."""
raise NotImplementedError
@ -50,11 +76,66 @@ class AbstractConfig:
# pylint: disable=no-self-use
return True
async def async_report_state(self, message):
"""Send a state report to Google."""
raise NotImplementedError
def async_enable_report_state(self):
"""Enable proactive mode."""
# Circular dep
from .report_state import async_enable_report_state
if self._unsub_report_state is None:
self._unsub_report_state = async_enable_report_state(self.hass, self)
def async_disable_report_state(self):
"""Disable report state."""
if self._unsub_report_state is not None:
self._unsub_report_state()
self._unsub_report_state = None
async def async_sync_entities(self):
"""Sync all entities to Google."""
# Remove any pending sync
if self._google_sync_unsub:
self._google_sync_unsub()
self._google_sync_unsub = None
return await self._async_request_sync_devices()
async def _schedule_callback(self, _now):
"""Handle a scheduled sync callback."""
self._google_sync_unsub = None
await self.async_sync_entities()
@callback
def async_schedule_google_sync(self):
"""Schedule a sync."""
if self._google_sync_unsub:
self._google_sync_unsub()
self._google_sync_unsub = async_call_later(
self.hass, SYNC_DELAY, self._schedule_callback
)
async def _async_request_sync_devices(self) -> int:
"""Trigger a sync with Google.
Return value is the HTTP status code of the sync request.
"""
raise NotImplementedError
async def async_deactivate_report_state(self):
"""Turn off report state and disable further state reporting.
Called when the user disconnects their account from Google.
"""
class RequestData:
"""Hold data associated with a particular request."""
def __init__(self, config, user_id, request_id):
def __init__(self, config: AbstractConfig, user_id: str, request_id: str):
"""Initialize the request data."""
self.config = config
self.request_id = request_id
@ -71,7 +152,7 @@ def get_google_type(domain, device_class):
class GoogleEntity:
"""Adaptation of Entity expressed in Google's terms."""
def __init__(self, hass, config, state):
def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State):
"""Initialize a Google entity."""
self.hass = hass
self.config = config
@ -139,7 +220,7 @@ class GoogleEntity:
"name": {"name": name},
"attributes": {},
"traits": [trait.name for trait in traits],
"willReportState": False,
"willReportState": self.config.should_report_state,
"type": device_type,
}

View file

@ -25,10 +25,16 @@ _LOGGER = logging.getLogger(__name__)
class GoogleConfig(AbstractConfig):
"""Config for manual setup of Google."""
def __init__(self, config):
def __init__(self, hass, config):
"""Initialize the config."""
super().__init__(hass)
self._config = config
@property
def enabled(self):
"""Return if Google is enabled."""
return True
@property
def agent_user_id(self):
"""Return Agent User Id to use for query responses."""
@ -77,7 +83,7 @@ class GoogleConfig(AbstractConfig):
@callback
def async_register_http(hass, cfg):
"""Register HTTP views for Google Assistant."""
hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg)))
hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg)))
class GoogleAssistantView(HomeAssistantView):

View file

@ -3,8 +3,6 @@
"name": "Google assistant",
"documentation": "https://www.home-assistant.io/integrations/google_assistant",
"requirements": [],
"dependencies": [
"http"
],
"codeowners": []
"dependencies": ["http"],
"codeowners": ["@home-assistant/cloud"]
}

View file

@ -0,0 +1,39 @@
"""Google Report State implementation."""
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import MATCH_ALL
from .helpers import AbstractConfig, GoogleEntity
@callback
def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig):
"""Enable state reporting."""
async def async_entity_state_listener(changed_entity, old_state, new_state):
if not new_state:
return
if not google_config.should_expose(new_state):
return
entity = GoogleEntity(hass, google_config, new_state)
if not entity.is_supported():
return
entity_data = entity.query_serialize()
if old_state:
old_entity = GoogleEntity(hass, google_config, old_state)
# Only report to Google if data that Google cares about has changed
if entity_data == old_entity.query_serialize():
return
await google_config.async_report_state(
{"devices": {"states": {changed_entity: entity_data}}}
)
return hass.helpers.event.async_track_state_change(
MATCH_ALL, async_entity_state_listener
)

View file

@ -193,11 +193,12 @@ async def handle_devices_execute(hass, data, payload):
@HANDLERS.register("action.devices.DISCONNECT")
async def async_devices_disconnect(hass, data, payload):
async def async_devices_disconnect(hass, data: RequestData, payload):
"""Handle action.devices.DISCONNECT request.
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
"""
await data.config.async_deactivate_report_state()
return None

View file

@ -10,7 +10,7 @@ certifi>=2019.6.16
contextvars==2.4;python_version<"3.7"
cryptography==2.7
distro==1.4.0
hass-nabucasa==0.17
hass-nabucasa==0.22
home-assistant-frontend==20191002.0
importlib-metadata==0.23
jinja2>=2.10.1

View file

@ -607,7 +607,7 @@ habitipy==0.2.0
hangups==0.4.9
# homeassistant.components.cloud
hass-nabucasa==0.17
hass-nabucasa==0.22
# homeassistant.components.mqtt
hbmqtt==0.9.5

View file

@ -164,7 +164,7 @@ ha-ffmpeg==2.0
hangups==0.4.9
# homeassistant.components.cloud
hass-nabucasa==0.17
hass-nabucasa==0.22
# homeassistant.components.mqtt
hbmqtt==0.9.5

View file

@ -59,7 +59,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
cloud_prefs,
Mock(
alexa_access_token_url="http://example/alexa_token",
run_executor=Mock(side_effect=mock_coro),
auth=Mock(async_check_token=Mock(side_effect=mock_coro)),
websession=hass.helpers.aiohttp_client.async_get_clientsession(),
),
)
@ -160,7 +160,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
with patch_sync_helper() as (to_update, to_remove):
hass.bus.async_fire(
EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "update", "entity_id": "light.kitchen"},
{
"action": "update",
"entity_id": "light.kitchen",
"changes": ["entity_id"],
},
)
await hass.async_block_till_done()

View file

@ -0,0 +1,121 @@
"""Test the Cloud Google Config."""
from unittest.mock import patch, Mock
from homeassistant.components.google_assistant import helpers as ga_helpers
from homeassistant.components.cloud import GACTIONS_SCHEMA
from homeassistant.components.cloud.google_config import CloudGoogleConfig
from homeassistant.util.dt import utcnow
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from tests.common import mock_coro, async_fire_time_changed
async def test_google_update_report_state(hass, cloud_prefs):
"""Test Google config responds to updating preference."""
config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None)
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch(
"homeassistant.components.google_assistant.report_state.async_enable_report_state"
) as mock_report_state:
await cloud_prefs.async_update(google_report_state=True)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
assert len(mock_report_state.mock_calls) == 1
async def test_sync_entities(aioclient_mock, hass, cloud_prefs):
"""Test sync devices."""
aioclient_mock.post("http://example.com", status=404)
config = CloudGoogleConfig(
hass,
GACTIONS_SCHEMA({}),
cloud_prefs,
Mock(
google_actions_sync_url="http://example.com",
auth=Mock(async_check_token=Mock(side_effect=mock_coro)),
),
)
assert await config.async_sync_entities() == 404
async def test_google_update_expose_trigger_sync(hass, cloud_prefs):
"""Test Google config responds to updating exposed entities."""
config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None)
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
await cloud_prefs.async_update_google_entity_config(
entity_id="light.kitchen", should_expose=True
)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow())
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
await cloud_prefs.async_update_google_entity_config(
entity_id="light.kitchen", should_expose=False
)
await cloud_prefs.async_update_google_entity_config(
entity_id="binary_sensor.door", should_expose=True
)
await cloud_prefs.async_update_google_entity_config(
entity_id="sensor.temp", should_expose=True
)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow())
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
"""Test Google config responds to entity registry."""
config = CloudGoogleConfig(
hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"]
)
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
hass.bus.async_fire(
EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "create", "entity_id": "light.kitchen"},
)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
hass.bus.async_fire(
EVENT_ENTITY_REGISTRY_UPDATED,
{"action": "remove", "entity_id": "light.kitchen"},
)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
with patch.object(
config, "async_sync_entities", side_effect=mock_coro
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
hass.bus.async_fire(
EVENT_ENTITY_REGISTRY_UPDATED,
{
"action": "update",
"entity_id": "light.kitchen",
"changes": ["entity_id"],
},
)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1

View file

@ -33,7 +33,9 @@ SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info"
@pytest.fixture()
def mock_auth():
"""Mock check token."""
with patch("hass_nabucasa.auth.CognitoAuth.check_token"):
with patch(
"hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro
):
yield
@ -357,6 +359,7 @@ async def test_websocket_status(
"google_secure_devices_pin": None,
"alexa_entity_configs": {},
"alexa_report_state": False,
"google_report_state": False,
"remote_enabled": False,
},
"alexa_entities": {

View file

@ -28,6 +28,13 @@ async def test_constructor_loads_info_from_config(hass):
"user_pool_id": "test-user_pool_id",
"region": "test-region",
"relayer": "test-relayer",
"google_actions_sync_url": "http://test-google_actions_sync_url",
"subscription_info_url": "http://test-subscription-info-url",
"cloudhook_create_url": "http://test-cloudhook_create_url",
"remote_api_url": "http://test-remote_api_url",
"alexa_access_token_url": "http://test-alexa-token-url",
"acme_directory_server": "http://test-acme-directory-server",
"google_actions_report_state_url": "http://test-google-actions-report-state-url",
},
},
)
@ -39,6 +46,16 @@ async def test_constructor_loads_info_from_config(hass):
assert cl.user_pool_id == "test-user_pool_id"
assert cl.region == "test-region"
assert cl.relayer == "test-relayer"
assert cl.google_actions_sync_url == "http://test-google_actions_sync_url"
assert cl.subscription_info_url == "http://test-subscription-info-url"
assert cl.cloudhook_create_url == "http://test-cloudhook_create_url"
assert cl.remote_api_url == "http://test-remote_api_url"
assert cl.alexa_access_token_url == "http://test-alexa-token-url"
assert cl.acme_directory_server == "http://test-acme-directory-server"
assert (
cl.google_actions_report_state_url
== "http://test-google-actions-report-state-url"
)
async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user):

View file

@ -6,9 +6,15 @@ class MockConfig(helpers.AbstractConfig):
"""Fake config that always exposes everything."""
def __init__(
self, *, secure_devices_pin=None, should_expose=None, entity_config=None
self,
*,
secure_devices_pin=None,
should_expose=None,
entity_config=None,
hass=None,
):
"""Initialize config."""
super().__init__(hass)
self._should_expose = should_expose
self._secure_devices_pin = secure_devices_pin
self._entity_config = entity_config or {}

View file

@ -1,6 +1,7 @@
"""The tests for google-assistant init."""
import asyncio
from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from homeassistant.components import google_assistant as ga
@ -20,7 +21,10 @@ def test_request_sync_service(aioclient_mock, hass):
assert aioclient_mock.call_count == 0
yield from hass.services.async_call(
ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True
ga.const.DOMAIN,
ga.const.SERVICE_REQUEST_SYNC,
blocking=True,
context=Context(user_id="123"),
)
assert aioclient_mock.call_count == 1

View file

@ -0,0 +1,47 @@
"""Test Google report state."""
from unittest.mock import patch
from homeassistant.components.google_assistant.report_state import (
async_enable_report_state,
)
from . import BASIC_CONFIG
from tests.common import mock_coro
async def test_report_state(hass):
"""Test report state works."""
unsub = async_enable_report_state(hass, BASIC_CONFIG)
with patch.object(
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
) as mock_report:
hass.states.async_set("light.kitchen", "on")
await hass.async_block_till_done()
assert len(mock_report.mock_calls) == 1
assert mock_report.mock_calls[0][1][0] == {
"devices": {"states": {"light.kitchen": {"on": True, "online": True}}}
}
# Test that state changes that change something that Google doesn't care about
# do not trigger a state report.
with patch.object(
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
) as mock_report:
hass.states.async_set(
"light.kitchen", "on", {"irrelevant": "should_be_ignored"}
)
await hass.async_block_till_done()
assert len(mock_report.mock_calls) == 0
unsub()
with patch.object(
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
) as mock_report:
hass.states.async_set("light.kitchen", "on")
await hass.async_block_till_done()
assert len(mock_report.mock_calls) == 0

View file

@ -657,14 +657,20 @@ async def test_device_media_player(hass, device_class, google_type):
async def test_query_disconnect(hass):
"""Test a disconnect message."""
result = await sh.async_handle_message(
hass,
BASIC_CONFIG,
"test-agent",
{"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
)
config = MockConfig(hass=hass)
config.async_enable_report_state()
assert config._unsub_report_state is not None
with patch.object(
config, "async_deactivate_report_state", side_effect=mock_coro
) as mock_deactivate:
result = await sh.async_handle_message(
hass,
config,
"test-agent",
{"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
)
assert result is None
assert len(mock_deactivate.mock_calls) == 1
async def test_trait_execute_adding_query_data(hass):