diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 00acf930f86..956d35caf2d 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -101,6 +101,7 @@ class CloudClient(Interface): self._google_config = google_config.CloudGoogleConfig( self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) + await self._google_config.async_initialize() return self._google_config diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 3d6511ffc3d..3df06c140a0 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -42,12 +42,7 @@ class CloudGoogleConfig(AbstractConfig): @property def enabled(self): """Return if Google is enabled.""" - return self._prefs.google_enabled - - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return self._cloud.username + return self._cloud.is_logged_in and self._prefs.google_enabled @property def entity_config(self): @@ -62,7 +57,7 @@ class CloudGoogleConfig(AbstractConfig): @property def should_report_state(self): """Return if states should be proactively reported.""" - return self._prefs.google_report_state + return self._cloud.is_logged_in and self._prefs.google_report_state @property def local_sdk_webhook_id(self): @@ -104,7 +99,7 @@ class CloudGoogleConfig(AbstractConfig): 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): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" try: await self._cloud.google_report_state.async_send_message(message) @@ -132,13 +127,6 @@ class CloudGoogleConfig(AbstractConfig): _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: @@ -149,7 +137,7 @@ class CloudGoogleConfig(AbstractConfig): # 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(self.agent_user_id) + await self.async_sync_entities_all() # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -157,7 +145,7 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs is not prefs.google_entity_configs and self._config["filter"].empty_filter ): - self.async_schedule_google_sync(self.agent_user_id) + self.async_schedule_google_sync_all() if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() @@ -173,4 +161,4 @@ class CloudGoogleConfig(AbstractConfig): # 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(self.agent_user_id) + await self.async_sync_entities_all() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d808fe72d39..c68f24172f0 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -175,7 +175,7 @@ class GoogleActionsSyncView(HomeAssistantView): hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] gconf = await cloud.client.get_google_config() - status = await gconf.async_sync_entities(gconf.agent_user_id) + status = await gconf.async_sync_entities(gconf.cloud_user) return self.json({}, status_code=status) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index c156bf0176b..ecb6d767817 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -93,7 +93,9 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EX async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) + google_config = GoogleConfig(hass, config) + await google_config.async_initialize() hass.http.register_view(GoogleAssistantView(google_config)) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 5209f6040c1..35a04e0e08e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -141,3 +141,5 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { CHALLENGE_ACK_NEEDED = "ackNeeded" CHALLENGE_PIN_NEEDED = "pinNeeded" CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" + +STORE_AGENT_USER_IDS = "agent_user_ids" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index a128b6c9bcb..bc076533c69 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -10,6 +10,7 @@ from aiohttp.web import json_response from homeassistant.core import Context, callback, HomeAssistant, State from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook +from homeassistant.helpers.storage import Store from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -26,6 +27,7 @@ from .const import ( ERR_FUNCTION_NOT_SUPPORTED, DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, + STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -41,19 +43,20 @@ class AbstractConfig: def __init__(self, hass): """Initialize abstract config.""" self.hass = hass + self._store = None self._google_sync_unsub = {} self._local_sdk_active = False + async def async_initialize(self): + """Perform async initialization of config.""" + self._store = GoogleConfigStore(self.hass) + await self._store.async_load() + @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.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -101,10 +104,18 @@ class AbstractConfig: # pylint: disable=no-self-use return True - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" raise NotImplementedError + async def async_report_state_all(self, message): + """Send a state report to Google for all previously synced users.""" + jobs = [ + self.async_report_state(message, agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + await gather(*jobs) + def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep @@ -123,9 +134,18 @@ class AbstractConfig: """Sync all entities to Google.""" # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() - return await self._async_request_sync_devices(agent_user_id) + async def async_sync_entities_all(self): + """Sync all entities to Google for all registered agents.""" + res = await gather( + *[ + self.async_sync_entities(agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + ) + return max(res, default=204) + @callback def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" @@ -141,6 +161,12 @@ class AbstractConfig: self.hass, SYNC_DELAY, _schedule_callback ) + @callback + def async_schedule_google_sync_all(self): + """Schedule a sync for all registered agents.""" + for agent_user_id in self._store.agent_user_ids: + self.async_schedule_google_sync(agent_user_id) + async def _async_request_sync_devices(self, agent_user_id: str) -> int: """Trigger a sync with Google. @@ -148,11 +174,19 @@ class AbstractConfig: """ raise NotImplementedError - async def async_deactivate_report_state(self): + async def async_connect_agent_user(self, agent_user_id: str): + """Add an synced and known agent_user_id. + + Called when a completed sync response have been sent to Google. + """ + self._store.add_agent_user_id(agent_user_id) + + async def async_disconnect_agent_user(self, agent_user_id: str): """Turn off report state and disable further state reporting. Called when the user disconnects their account from Google. """ + self._store.pop_agent_user_id(agent_user_id) @callback def async_enable_local_sdk(self): @@ -199,6 +233,44 @@ class AbstractConfig: return json_response(result) +class GoogleConfigStore: + """A configuration store for google assistant.""" + + _STORAGE_VERSION = 1 + _STORAGE_KEY = DOMAIN + + def __init__(self, hass): + """Initialize a configuration store.""" + self._hass = hass + self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) + self._data = {STORE_AGENT_USER_IDS: {}} + + @property + def agent_user_ids(self): + """Return a list of connected agent user_ids.""" + return self._data[STORE_AGENT_USER_IDS] + + @callback + def add_agent_user_id(self, agent_user_id): + """Add an agent user id to store.""" + if agent_user_id not in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS][agent_user_id] = {} + self._store.async_delay_save(lambda: self._data, 1.0) + + @callback + def pop_agent_user_id(self, agent_user_id): + """Remove agent user id from store.""" + if agent_user_id in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None) + self._store.async_delay_save(lambda: self._data, 1.0) + + async def async_load(self): + """Store current configuration to disk.""" + data = await self._store.async_load() + if data: + self._data = data + + class RequestData: """Hold data associated with a particular request.""" @@ -278,7 +350,7 @@ class GoogleEntity: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self): + async def sync_serialize(self, agent_user_id): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync @@ -314,7 +386,7 @@ class GoogleEntity: "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.config.api.port, "httpSSL": self.hass.config.api.use_ssl, - "proxyDeviceId": self.config.agent_user_id, + "proxyDeviceId": agent_user_id, } for trt in traits: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index abf931bd969..c3d0dd493a8 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -81,11 +81,6 @@ class GoogleConfig(AbstractConfig): """Return if Google is enabled.""" return True - @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.""" @@ -214,11 +209,11 @@ class GoogleConfig(AbstractConfig): _LOGGER.error("Could not contact %s", url) return 500 - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" data = { "requestId": uuid4().hex, - "agentUserId": (await self.hass.auth.async_get_owner()).id, + "agentUserId": agent_user_id, "payload": message, } await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index aacb90e9d2b..78a0f50e277 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -45,7 +45,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if entity_data == old_entity.query_serialize(): return - await google_config.async_report_state( + await google_config.async_report_state_all( {"devices": {"states": {changed_entity: entity_data}}} ) @@ -62,7 +62,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig except SmartHomeError: continue - await google_config.async_report_state({"devices": {"states": entities}}) + await google_config.async_report_state_all({"devices": {"states": entities}}) async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0944c9532ef..0e5037ce13a 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -79,18 +79,19 @@ async def async_devices_sync(hass, data, payload): EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context ) + agent_user_id = data.context.user_id + devices = await asyncio.gather( *( - entity.sync_serialize() + entity.sync_serialize(agent_user_id) for entity in async_get_entities(hass, data.config) if entity.should_expose() ) ) - response = { - "agentUserId": data.config.agent_user_id or data.context.user_id, - "devices": devices, - } + response = {"agentUserId": agent_user_id, "devices": devices} + + await data.config.async_connect_agent_user(agent_user_id) return response @@ -197,7 +198,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ - await data.config.async_deactivate_report_state() + await data.config.async_disconnect_agent_user(data.context.user_id) return None @@ -209,7 +210,7 @@ async def async_devices_identify(hass, data: RequestData, payload): """ return { "device": { - "id": data.config.agent_user_id, + "id": data.context.user_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index a9c4ade668d..955923c1e68 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -102,16 +102,13 @@ async def test_handler_google_actions(hass): reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - with patch( - "hass_nabucasa.Cloud._decode_claims", - return_value={"cognito:username": "myUserName"}, - ): - resp = await cloud.client.async_google_message(data) + config = await cloud.client.get_google_config() + resp = await cloud.client.async_google_message(data) assert resp["requestId"] == reqid payload = resp["payload"] - assert payload["agentUserId"] == "myUserName" + assert payload["agentUserId"] == config.cloud_user devices = payload["devices"] assert len(devices) == 1 diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 0284a2c3851..3510b4b8abd 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -19,6 +19,8 @@ async def test_google_update_report_state(hass, cloud_prefs): cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -58,6 +60,8 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -95,6 +99,8 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 09522e9c86f..657bf930ed6 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,18 @@ """Tests for the Google Assistant integration.""" +from asynctest.mock import MagicMock from homeassistant.components.google_assistant import helpers +def mock_google_config_store(agent_user_ids=None): + """Fake a storage for google assistant.""" + store = MagicMock(spec=helpers.GoogleConfigStore) + if agent_user_ids is not None: + store.agent_user_ids = agent_user_ids + else: + store.agent_user_ids = {} + return store + + class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" @@ -15,6 +26,7 @@ class MockConfig(helpers.AbstractConfig): local_sdk_webhook_id=None, local_sdk_user_id=None, enabled=True, + agent_user_ids=None, ): """Initialize config.""" super().__init__(hass) @@ -24,6 +36,7 @@ class MockConfig(helpers.AbstractConfig): self._local_sdk_webhook_id = local_sdk_webhook_id self._local_sdk_user_id = local_sdk_user_id self._enabled = enabled + self._store = mock_google_config_store(agent_user_ids) @property def enabled(self): diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 497b7b1f0ae..eb479a3b6b5 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,11 +1,18 @@ """Test Google Assistant helpers.""" -from unittest.mock import Mock +from asynctest.mock import Mock, patch, call +from datetime import timedelta +import pytest from homeassistant.setup import async_setup_component from homeassistant.components.google_assistant import helpers from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from homeassistant.util import dt from . import MockConfig -from tests.common import async_capture_events, async_mock_service +from tests.common import ( + async_capture_events, + async_mock_service, + async_fire_time_changed, +) async def test_google_entity_sync_serialize_with_local_sdk(hass): @@ -19,13 +26,13 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "httpPort": 1234, @@ -128,3 +135,84 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): resp = await client.post("/api/webhook/mock-webhook-id") assert resp.status == 200 assert await resp.read() == b"" + + +async def test_agent_user_id_storage(hass, hass_storage): + """Test a disconnect message.""" + + hass_storage["google_assistant"] = { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + store = helpers.GoogleConfigStore(hass) + await store.async_load() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + async def _check_after_delay(data): + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": data, + } + + store.add_agent_user_id("agent_2") + await _check_after_delay({"agent_user_ids": {"agent_1": {}, "agent_2": {}}}) + + store.pop_agent_user_id("agent_1") + await _check_after_delay({"agent_user_ids": {"agent_2": {}}}) + + +async def test_agent_user_id_connect(): + """Test the connection and disconnection of users.""" + config = MockConfig() + store = config._store + + await config.async_connect_agent_user("agent_2") + assert store.add_agent_user_id.call_args == call("agent_2") + + await config.async_connect_agent_user("agent_1") + assert store.add_agent_user_id.call_args == call("agent_1") + + await config.async_disconnect_agent_user("agent_2") + assert store.pop_agent_user_id.call_args == call("agent_2") + + await config.async_disconnect_agent_user("agent_1") + assert store.pop_agent_user_id.call_args == call("agent_1") + + +@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}]) +async def test_report_state_all(agents): + """Test a disconnect message.""" + config = MockConfig(agent_user_ids=agents) + data = {} + with patch.object(config, "async_report_state") as mock: + await config.async_report_state_all(data) + assert sorted(mock.mock_calls) == sorted( + [call(data, agent) for agent in agents] + ) + + +@pytest.mark.parametrize( + "agents, result", [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], +) +async def test_sync_entities_all(agents, result): + """Test sync entities .""" + config = MockConfig(agent_user_ids=set(agents.keys())) + with patch.object( + config, + "async_sync_entities", + side_effect=lambda agent_user_id: agents[agent_user_id], + ) as mock: + res = await config.async_sync_entities_all() + assert sorted(mock.mock_calls) == sorted([call(agent) for agent in agents]) + assert res == result diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 42706a470ab..86ffcc87ac0 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -12,7 +12,6 @@ from homeassistant.components.google_assistant.const import ( REPORT_STATE_BASE_URL, HOMEGRAPH_TOKEN_URL, ) -from homeassistant.auth.models import User DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { @@ -67,6 +66,7 @@ async def test_update_access_token(hass): jwt = "dummyjwt" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) with patch( @@ -99,6 +99,8 @@ async def test_update_access_token(hass): async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -120,6 +122,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): """Test the that the calls get retried with new token on 401.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -143,8 +147,10 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), ) + await config.async_initialize() + aioclient_mock.post(MOCK_URL, status=200, json={}) res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) @@ -159,8 +165,10 @@ async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), ) + await config.async_initialize() + aioclient_mock.post(MOCK_URL, status=666, json={}) res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) @@ -170,17 +178,16 @@ async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" + agent_user_id = "user" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + + await config.async_connect_agent_user(agent_user_id) message = {"devices": {}} - owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) - with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( - hass.auth, "async_get_owner" - ) as mock_get_owner: - mock_get_owner.return_value = owner - - await config.async_report_state(message) + with patch.object(config, "async_call_homegraph_api") as mock_call: + await config.async_report_state(message, agent_user_id) mock_call.assert_called_once_with( REPORT_STATE_BASE_URL, - {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + {"requestId": ANY, "agentUserId": agent_user_id, "payload": message}, ) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 6ab88286a69..ce624c9ca95 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -16,7 +16,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -35,7 +35,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -48,7 +48,7 @@ async def test_report_state(hass, caplog): # 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 + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -59,7 +59,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -73,7 +73,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 250d611b602..3bd38315218 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -455,7 +455,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize() + result = await entity.sync_serialize(None) assert result == { "id": "input_boolean.bla", "attributes": {}, @@ -664,8 +664,8 @@ async def test_query_disconnect(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: + config, "async_disconnect_agent_user", side_effect=mock_coro + ) as mock_disconnect: result = await sh.async_handle_message( hass, config, @@ -673,7 +673,7 @@ async def test_query_disconnect(hass): {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, ) assert result is None - assert len(mock_deactivate.mock_calls) == 1 + assert len(mock_disconnect.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): @@ -741,10 +741,12 @@ async def test_trait_execute_adding_query_data(hass): async def test_identify(hass): """Test identify message.""" + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id result = await sh.async_handle_message( hass, BASIC_CONFIG, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -778,7 +780,7 @@ async def test_identify(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, } @@ -790,7 +792,7 @@ async def test_identify(hass): "requestId": REQ_ID, "payload": { "device": { - "id": BASIC_CONFIG.agent_user_id, + "id": proxy_device_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { @@ -822,10 +824,13 @@ async def test_reachable_devices(hass): should_expose=lambda state: state.entity_id != "light.not_expose" ) + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id + result = await sh.async_handle_message( hass, config, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -834,7 +839,7 @@ async def test_reachable_devices(hass): "payload": { "device": { "proxyDevice": { - "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "id": proxy_device_id, "customData": "{}", "proxyData": "{}", } @@ -849,7 +854,7 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, @@ -858,11 +863,11 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, - {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + {"id": proxy_device_id, "customData": {}}, ], }, )