mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 18:27:51 +00:00
Enable local fulfillment google assistant (#63218)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
f786237def
commit
25fe213f22
@ -65,16 +65,11 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
"""Return if states should be proactively reported."""
|
"""Return if states should be proactively reported."""
|
||||||
return self.enabled and self._prefs.google_report_state
|
return self.enabled and self._prefs.google_report_state
|
||||||
|
|
||||||
@property
|
def get_local_webhook_id(self, agent_user_id):
|
||||||
def local_sdk_webhook_id(self):
|
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
||||||
"""Return the local SDK webhook.
|
|
||||||
|
|
||||||
Return None to disable the local SDK.
|
|
||||||
"""
|
|
||||||
return self._prefs.google_local_webhook_id
|
return self._prefs.google_local_webhook_id
|
||||||
|
|
||||||
@property
|
def get_local_agent_user_id(self, webhook_id):
|
||||||
def local_sdk_user_id(self):
|
|
||||||
"""Return the user ID to be used for actions received via the local SDK."""
|
"""Return the user ID to be used for actions received via the local SDK."""
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
|
@ -177,6 +177,7 @@ CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded"
|
|||||||
CHALLENGE_PIN_NEEDED = "pinNeeded"
|
CHALLENGE_PIN_NEEDED = "pinNeeded"
|
||||||
|
|
||||||
STORE_AGENT_USER_IDS = "agent_user_ids"
|
STORE_AGENT_USER_IDS = "agent_user_ids"
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
|
||||||
|
|
||||||
SOURCE_CLOUD = "cloud"
|
SOURCE_CLOUD = "cloud"
|
||||||
SOURCE_LOCAL = "local"
|
SOURCE_LOCAL = "local"
|
||||||
|
@ -38,6 +38,7 @@ from .const import (
|
|||||||
NOT_EXPOSE_LOCAL,
|
NOT_EXPOSE_LOCAL,
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
STORE_AGENT_USER_IDS,
|
STORE_AGENT_USER_IDS,
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||||
)
|
)
|
||||||
from .error import SmartHomeError
|
from .error import SmartHomeError
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ class AbstractConfig(ABC):
|
|||||||
async def async_initialize(self):
|
async def async_initialize(self):
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
self._store = GoogleConfigStore(self.hass)
|
self._store = GoogleConfigStore(self.hass)
|
||||||
await self._store.async_load()
|
await self._store.async_initialize()
|
||||||
|
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
@ -148,18 +149,22 @@ class AbstractConfig(ABC):
|
|||||||
"""Return if states should be proactively reported."""
|
"""Return if states should be proactively reported."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
def get_local_agent_user_id(self, webhook_id):
|
||||||
def local_sdk_webhook_id(self):
|
"""Return the user ID to be used for actions received via the local SDK.
|
||||||
"""Return the local SDK webhook ID.
|
|
||||||
|
|
||||||
Return None to disable the local SDK.
|
Return None is no agent user id is found.
|
||||||
"""
|
"""
|
||||||
return None
|
found_agent_user_id = None
|
||||||
|
for agent_user_id, agent_user_data in self._store.agent_user_ids.items():
|
||||||
|
if agent_user_data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] == webhook_id:
|
||||||
|
found_agent_user_id = agent_user_id
|
||||||
|
break
|
||||||
|
|
||||||
@property
|
return found_agent_user_id
|
||||||
def local_sdk_user_id(self):
|
|
||||||
"""Return the user ID to be used for actions received via the local SDK."""
|
def get_local_webhook_id(self, agent_user_id):
|
||||||
raise NotImplementedError
|
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
||||||
|
return self._store.agent_user_ids[agent_user_id][STORE_GOOGLE_LOCAL_WEBHOOK_ID]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_agent_user_id(self, context):
|
def get_agent_user_id(self, context):
|
||||||
@ -267,22 +272,41 @@ class AbstractConfig(ABC):
|
|||||||
@callback
|
@callback
|
||||||
def async_enable_local_sdk(self):
|
def async_enable_local_sdk(self):
|
||||||
"""Enable the local SDK."""
|
"""Enable the local SDK."""
|
||||||
if (webhook_id := self.local_sdk_webhook_id) is None:
|
setup_successfull = True
|
||||||
return
|
setup_webhook_ids = []
|
||||||
|
|
||||||
try:
|
for user_agent_id, _ in self._store.agent_user_ids.items():
|
||||||
webhook.async_register(
|
|
||||||
self.hass,
|
if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None:
|
||||||
DOMAIN,
|
setup_successfull = False
|
||||||
"Local Support",
|
break
|
||||||
webhook_id,
|
|
||||||
self._handle_local_webhook,
|
try:
|
||||||
|
webhook.async_register(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
"Local Support for " + user_agent_id,
|
||||||
|
webhook_id,
|
||||||
|
self._handle_local_webhook,
|
||||||
|
)
|
||||||
|
setup_webhook_ids.append(webhook_id)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Webhook handler %s for agent user id %s is already defined!",
|
||||||
|
webhook_id,
|
||||||
|
user_agent_id,
|
||||||
|
)
|
||||||
|
setup_successfull = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if not setup_successfull:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Local fulfillment failed to setup, falling back to cloud fulfillment"
|
||||||
)
|
)
|
||||||
except ValueError:
|
for setup_webhook_id in setup_webhook_ids:
|
||||||
_LOGGER.info("Webhook handler is already defined!")
|
webhook.async_unregister(self.hass, setup_webhook_id)
|
||||||
return
|
|
||||||
|
|
||||||
self._local_sdk_active = True
|
self._local_sdk_active = setup_successfull
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_disable_local_sdk(self):
|
def async_disable_local_sdk(self):
|
||||||
@ -290,7 +314,11 @@ class AbstractConfig(ABC):
|
|||||||
if not self._local_sdk_active:
|
if not self._local_sdk_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
webhook.async_unregister(self.hass, self.local_sdk_webhook_id)
|
for agent_user_id in self._store.agent_user_ids:
|
||||||
|
webhook.async_unregister(
|
||||||
|
self.hass, self.get_local_webhook_id(agent_user_id)
|
||||||
|
)
|
||||||
|
|
||||||
self._local_sdk_active = False
|
self._local_sdk_active = False
|
||||||
|
|
||||||
async def _handle_local_webhook(self, hass, webhook_id, request):
|
async def _handle_local_webhook(self, hass, webhook_id, request):
|
||||||
@ -307,8 +335,23 @@ class AbstractConfig(ABC):
|
|||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return json_response(smart_home.turned_off_response(payload))
|
return json_response(smart_home.turned_off_response(payload))
|
||||||
|
|
||||||
|
if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None:
|
||||||
|
# No agent user linked to this webhook, means that the user has somehow unregistered
|
||||||
|
# removing webhook and stopping processing of this request.
|
||||||
|
_LOGGER.error(
|
||||||
|
"Cannot process request for webhook %s as no linked agent user is found:\n%s\n",
|
||||||
|
webhook_id,
|
||||||
|
pprint.pformat(payload),
|
||||||
|
)
|
||||||
|
webhook.async_unregister(self.hass, webhook_id)
|
||||||
|
return None
|
||||||
|
|
||||||
result = await smart_home.async_handle_message(
|
result = await smart_home.async_handle_message(
|
||||||
self.hass, self, self.local_sdk_user_id, payload, SOURCE_LOCAL
|
self.hass,
|
||||||
|
self,
|
||||||
|
agent_user_id,
|
||||||
|
payload,
|
||||||
|
SOURCE_LOCAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
@ -327,7 +370,32 @@ class GoogleConfigStore:
|
|||||||
"""Initialize a configuration store."""
|
"""Initialize a configuration store."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY)
|
self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY)
|
||||||
self._data = {STORE_AGENT_USER_IDS: {}}
|
self._data = None
|
||||||
|
|
||||||
|
async def async_initialize(self):
|
||||||
|
"""Finish initializing the ConfigStore."""
|
||||||
|
should_save_data = False
|
||||||
|
if (data := await self._store.async_load()) is None:
|
||||||
|
# if the store is not found create an empty one
|
||||||
|
# Note that the first request is always a cloud request,
|
||||||
|
# and that will store the correct agent user id to be used for local requests
|
||||||
|
data = {
|
||||||
|
STORE_AGENT_USER_IDS: {},
|
||||||
|
}
|
||||||
|
should_save_data = True
|
||||||
|
|
||||||
|
for agent_user_id, agent_user_data in data[STORE_AGENT_USER_IDS].items():
|
||||||
|
if STORE_GOOGLE_LOCAL_WEBHOOK_ID not in agent_user_data:
|
||||||
|
data[STORE_AGENT_USER_IDS][agent_user_id] = {
|
||||||
|
**agent_user_data,
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
|
||||||
|
}
|
||||||
|
should_save_data = True
|
||||||
|
|
||||||
|
if should_save_data:
|
||||||
|
await self._store.async_save(data)
|
||||||
|
|
||||||
|
self._data = data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agent_user_ids(self):
|
def agent_user_ids(self):
|
||||||
@ -338,7 +406,9 @@ class GoogleConfigStore:
|
|||||||
def add_agent_user_id(self, agent_user_id):
|
def add_agent_user_id(self, agent_user_id):
|
||||||
"""Add an agent user id to store."""
|
"""Add an agent user id to store."""
|
||||||
if agent_user_id not in self._data[STORE_AGENT_USER_IDS]:
|
if agent_user_id not in self._data[STORE_AGENT_USER_IDS]:
|
||||||
self._data[STORE_AGENT_USER_IDS][agent_user_id] = {}
|
self._data[STORE_AGENT_USER_IDS][agent_user_id] = {
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(),
|
||||||
|
}
|
||||||
self._store.async_delay_save(lambda: self._data, 1.0)
|
self._store.async_delay_save(lambda: self._data, 1.0)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -348,11 +418,6 @@ class GoogleConfigStore:
|
|||||||
self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None)
|
self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None)
|
||||||
self._store.async_delay_save(lambda: self._data, 1.0)
|
self._store.async_delay_save(lambda: self._data, 1.0)
|
||||||
|
|
||||||
async def async_load(self):
|
|
||||||
"""Store current configuration to disk."""
|
|
||||||
if data := await self._store.async_load():
|
|
||||||
self._data = data
|
|
||||||
|
|
||||||
|
|
||||||
class RequestData:
|
class RequestData:
|
||||||
"""Hold data associated with a particular request."""
|
"""Hold data associated with a particular request."""
|
||||||
@ -507,7 +572,7 @@ class GoogleEntity:
|
|||||||
if self.config.is_local_sdk_active and self.should_expose_local():
|
if self.config.is_local_sdk_active and self.should_expose_local():
|
||||||
device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
|
device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
|
||||||
device["customData"] = {
|
device["customData"] = {
|
||||||
"webhookId": self.config.local_sdk_webhook_id,
|
"webhookId": self.config.get_local_webhook_id(agent_user_id),
|
||||||
"httpPort": self.hass.http.server_port,
|
"httpPort": self.hass.http.server_port,
|
||||||
"httpSSL": self.hass.config.api.use_ssl,
|
"httpSSL": self.hass.config.api.use_ssl,
|
||||||
"uuid": await self.hass.helpers.instance_id.async_get(),
|
"uuid": await self.hass.helpers.instance_id.async_get(),
|
||||||
|
@ -12,9 +12,10 @@ from aiohttp import ClientError, ClientResponseError
|
|||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
# Typing imports
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORIES
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORIES
|
||||||
|
|
||||||
|
# Typing imports
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -84,6 +85,12 @@ class GoogleConfig(AbstractConfig):
|
|||||||
self._access_token = None
|
self._access_token = None
|
||||||
self._access_token_renew = None
|
self._access_token_renew = None
|
||||||
|
|
||||||
|
async def async_initialize(self):
|
||||||
|
"""Perform async initialization of config."""
|
||||||
|
await super().async_initialize()
|
||||||
|
|
||||||
|
self.async_enable_local_sdk()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
|
@ -83,6 +83,8 @@ async def async_devices_sync(hass, data, payload):
|
|||||||
)
|
)
|
||||||
|
|
||||||
agent_user_id = data.config.get_agent_user_id(data.context)
|
agent_user_id = data.config.get_agent_user_id(data.context)
|
||||||
|
await data.config.async_connect_agent_user(agent_user_id)
|
||||||
|
|
||||||
entities = async_get_entities(hass, data.config)
|
entities = async_get_entities(hass, data.config)
|
||||||
results = await asyncio.gather(
|
results = await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
@ -103,8 +105,6 @@ async def async_devices_sync(hass, data, payload):
|
|||||||
|
|
||||||
response = {"agentUserId": agent_user_id, "devices": devices}
|
response = {"agentUserId": agent_user_id, "devices": devices}
|
||||||
|
|
||||||
await data.config.async_connect_agent_user(agent_user_id)
|
|
||||||
|
|
||||||
_LOGGER.debug("Syncing entities response: %s", response)
|
_LOGGER.debug("Syncing entities response: %s", response)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -24,8 +24,6 @@ class MockConfig(helpers.AbstractConfig):
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
entity_config=None,
|
entity_config=None,
|
||||||
hass=None,
|
hass=None,
|
||||||
local_sdk_user_id=None,
|
|
||||||
local_sdk_webhook_id=None,
|
|
||||||
secure_devices_pin=None,
|
secure_devices_pin=None,
|
||||||
should_2fa=None,
|
should_2fa=None,
|
||||||
should_expose=None,
|
should_expose=None,
|
||||||
@ -35,8 +33,6 @@ class MockConfig(helpers.AbstractConfig):
|
|||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self._enabled = enabled
|
self._enabled = enabled
|
||||||
self._entity_config = entity_config or {}
|
self._entity_config = entity_config or {}
|
||||||
self._local_sdk_user_id = local_sdk_user_id
|
|
||||||
self._local_sdk_webhook_id = local_sdk_webhook_id
|
|
||||||
self._secure_devices_pin = secure_devices_pin
|
self._secure_devices_pin = secure_devices_pin
|
||||||
self._should_2fa = should_2fa
|
self._should_2fa = should_2fa
|
||||||
self._should_expose = should_expose
|
self._should_expose = should_expose
|
||||||
@ -58,16 +54,6 @@ class MockConfig(helpers.AbstractConfig):
|
|||||||
"""Return secure devices pin."""
|
"""Return secure devices pin."""
|
||||||
return self._entity_config
|
return self._entity_config
|
||||||
|
|
||||||
@property
|
|
||||||
def local_sdk_webhook_id(self):
|
|
||||||
"""Return local SDK webhook id."""
|
|
||||||
return self._local_sdk_webhook_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def local_sdk_user_id(self):
|
|
||||||
"""Return local SDK webhook id."""
|
|
||||||
return self._local_sdk_user_id
|
|
||||||
|
|
||||||
def get_agent_user_id(self, context):
|
def get_agent_user_id(self, context):
|
||||||
"""Get agent user ID making request."""
|
"""Get agent user ID making request."""
|
||||||
return context.user_id
|
return context.user_id
|
||||||
|
@ -9,6 +9,9 @@ from homeassistant.components.google_assistant import helpers
|
|||||||
from homeassistant.components.google_assistant.const import (
|
from homeassistant.components.google_assistant.const import (
|
||||||
EVENT_COMMAND_RECEIVED,
|
EVENT_COMMAND_RECEIVED,
|
||||||
NOT_EXPOSE_LOCAL,
|
NOT_EXPOSE_LOCAL,
|
||||||
|
SOURCE_CLOUD,
|
||||||
|
SOURCE_LOCAL,
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
@ -36,8 +39,11 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass):
|
|||||||
hass.http = Mock(server_port=1234)
|
hass.http = Mock(server_port=1234)
|
||||||
config = MockConfig(
|
config = MockConfig(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
local_sdk_webhook_id="mock-webhook-id",
|
agent_user_ids={
|
||||||
local_sdk_user_id="mock-user-id",
|
"mock-user-id": {
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights"))
|
entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights"))
|
||||||
|
|
||||||
@ -48,12 +54,12 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass):
|
|||||||
config.async_enable_local_sdk()
|
config.async_enable_local_sdk()
|
||||||
|
|
||||||
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"):
|
||||||
serialized = await entity.sync_serialize(None)
|
serialized = await entity.sync_serialize("mock-user-id")
|
||||||
assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}]
|
assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}]
|
||||||
assert serialized["customData"] == {
|
assert serialized["customData"] == {
|
||||||
"httpPort": 1234,
|
"httpPort": 1234,
|
||||||
"httpSSL": True,
|
"httpSSL": True,
|
||||||
"proxyDeviceId": None,
|
"proxyDeviceId": "mock-user-id",
|
||||||
"webhookId": "mock-webhook-id",
|
"webhookId": "mock-webhook-id",
|
||||||
"baseUrl": "https://hostname:1234",
|
"baseUrl": "https://hostname:1234",
|
||||||
"uuid": "abcdef",
|
"uuid": "abcdef",
|
||||||
@ -79,8 +85,11 @@ async def test_config_local_sdk(hass, hass_client):
|
|||||||
|
|
||||||
config = MockConfig(
|
config = MockConfig(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
local_sdk_webhook_id="mock-webhook-id",
|
agent_user_ids={
|
||||||
local_sdk_user_id="mock-user-id",
|
"mock-user-id": {
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@ -118,7 +127,7 @@ async def test_config_local_sdk(hass, hass_client):
|
|||||||
assert result["requestId"] == "mock-req-id"
|
assert result["requestId"] == "mock-req-id"
|
||||||
|
|
||||||
assert len(command_events) == 1
|
assert len(command_events) == 1
|
||||||
assert command_events[0].context.user_id == config.local_sdk_user_id
|
assert command_events[0].context.user_id == "mock-user-id"
|
||||||
|
|
||||||
assert len(turn_on_calls) == 1
|
assert len(turn_on_calls) == 1
|
||||||
assert turn_on_calls[0].context is command_events[0].context
|
assert turn_on_calls[0].context is command_events[0].context
|
||||||
@ -137,8 +146,11 @@ async def test_config_local_sdk_if_disabled(hass, hass_client):
|
|||||||
|
|
||||||
config = MockConfig(
|
config = MockConfig(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
local_sdk_webhook_id="mock-webhook-id",
|
agent_user_ids={
|
||||||
local_sdk_user_id="mock-user-id",
|
"mock-user-id": {
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
enabled=False,
|
enabled=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -171,35 +183,61 @@ async def test_agent_user_id_storage(hass, hass_storage):
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"minor_version": 1,
|
"minor_version": 1,
|
||||||
"key": "google_assistant",
|
"key": "google_assistant",
|
||||||
"data": {"agent_user_ids": {"agent_1": {}}},
|
"data": {
|
||||||
|
"agent_user_ids": {
|
||||||
|
"agent_1": {
|
||||||
|
"local_webhook_id": "test_webhook",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
store = helpers.GoogleConfigStore(hass)
|
store = helpers.GoogleConfigStore(hass)
|
||||||
await store.async_load()
|
await store.async_initialize()
|
||||||
|
|
||||||
assert hass_storage["google_assistant"] == {
|
assert hass_storage["google_assistant"] == {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"minor_version": 1,
|
"minor_version": 1,
|
||||||
"key": "google_assistant",
|
"key": "google_assistant",
|
||||||
"data": {"agent_user_ids": {"agent_1": {}}},
|
"data": {
|
||||||
|
"agent_user_ids": {
|
||||||
|
"agent_1": {
|
||||||
|
"local_webhook_id": "test_webhook",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _check_after_delay(data):
|
async def _check_after_delay(data):
|
||||||
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
|
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass_storage["google_assistant"] == {
|
assert (
|
||||||
"version": 1,
|
list(hass_storage["google_assistant"]["data"]["agent_user_ids"].keys())
|
||||||
"minor_version": 1,
|
== data
|
||||||
"key": "google_assistant",
|
)
|
||||||
"data": data,
|
|
||||||
}
|
|
||||||
|
|
||||||
store.add_agent_user_id("agent_2")
|
store.add_agent_user_id("agent_2")
|
||||||
await _check_after_delay({"agent_user_ids": {"agent_1": {}, "agent_2": {}}})
|
await _check_after_delay(["agent_1", "agent_2"])
|
||||||
|
|
||||||
store.pop_agent_user_id("agent_1")
|
store.pop_agent_user_id("agent_1")
|
||||||
await _check_after_delay({"agent_user_ids": {"agent_2": {}}})
|
await _check_after_delay(["agent_2"])
|
||||||
|
|
||||||
|
hass_storage["google_assistant"] = {
|
||||||
|
"version": 1,
|
||||||
|
"minor_version": 1,
|
||||||
|
"key": "google_assistant",
|
||||||
|
"data": {
|
||||||
|
"agent_user_ids": {"agent_1": {}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
store = helpers.GoogleConfigStore(hass)
|
||||||
|
await store.async_initialize()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID
|
||||||
|
in hass_storage["google_assistant"]["data"]["agent_user_ids"]["agent_1"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_agent_user_id_connect():
|
async def test_agent_user_id_connect():
|
||||||
@ -254,3 +292,17 @@ def test_supported_features_string(caplog):
|
|||||||
)
|
)
|
||||||
assert entity.is_supported() is False
|
assert entity.is_supported() is False
|
||||||
assert "Entity test.entity_id contains invalid supported_features value invalid"
|
assert "Entity test.entity_id contains invalid supported_features value invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_data():
|
||||||
|
"""Test request data properties."""
|
||||||
|
config = MockConfig()
|
||||||
|
data = helpers.RequestData(
|
||||||
|
config, "test_user", SOURCE_LOCAL, "test_request_id", None
|
||||||
|
)
|
||||||
|
assert data.is_local_request is True
|
||||||
|
|
||||||
|
data = helpers.RequestData(
|
||||||
|
config, "test_user", SOURCE_CLOUD, "test_request_id", None
|
||||||
|
)
|
||||||
|
assert data.is_local_request is False
|
||||||
|
@ -5,14 +5,23 @@ from unittest.mock import ANY, patch
|
|||||||
|
|
||||||
from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA
|
from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA
|
||||||
from homeassistant.components.google_assistant.const import (
|
from homeassistant.components.google_assistant.const import (
|
||||||
|
DOMAIN,
|
||||||
|
EVENT_COMMAND_RECEIVED,
|
||||||
HOMEGRAPH_TOKEN_URL,
|
HOMEGRAPH_TOKEN_URL,
|
||||||
REPORT_STATE_BASE_URL,
|
REPORT_STATE_BASE_URL,
|
||||||
|
STORE_AGENT_USER_IDS,
|
||||||
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.components.google_assistant.http import (
|
from homeassistant.components.google_assistant.http import (
|
||||||
GoogleConfig,
|
GoogleConfig,
|
||||||
_get_homegraph_jwt,
|
_get_homegraph_jwt,
|
||||||
_get_homegraph_token,
|
_get_homegraph_token,
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
|
from homeassistant.core import State
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import async_capture_events, async_mock_service
|
||||||
|
|
||||||
DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA(
|
DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA(
|
||||||
{
|
{
|
||||||
@ -97,7 +106,7 @@ async def test_update_access_token(hass):
|
|||||||
mock_get_token.assert_called_once()
|
mock_get_token.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_call_homegraph_api(hass, aioclient_mock, hass_storage):
|
async def test_call_homegraph_api(hass, aioclient_mock, hass_storage, caplog):
|
||||||
"""Test the function to call the homegraph api."""
|
"""Test the function to call the homegraph api."""
|
||||||
config = GoogleConfig(hass, DUMMY_CONFIG)
|
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||||
await config.async_initialize()
|
await config.async_initialize()
|
||||||
@ -164,3 +173,237 @@ async def test_report_state(hass, aioclient_mock, hass_storage):
|
|||||||
REPORT_STATE_BASE_URL,
|
REPORT_STATE_BASE_URL,
|
||||||
{"requestId": ANY, "agentUserId": agent_user_id, "payload": message},
|
{"requestId": ANY, "agentUserId": agent_user_id, "payload": message},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_google_config_local_fulfillment(hass, aioclient_mock, hass_storage):
|
||||||
|
"""Test the google config for local fulfillment."""
|
||||||
|
agent_user_id = "user"
|
||||||
|
local_webhook_id = "webhook"
|
||||||
|
|
||||||
|
hass_storage["google_assistant"] = {
|
||||||
|
"version": 1,
|
||||||
|
"minor_version": 1,
|
||||||
|
"key": "google_assistant",
|
||||||
|
"data": {
|
||||||
|
"agent_user_ids": {
|
||||||
|
agent_user_id: {
|
||||||
|
"local_webhook_id": local_webhook_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||||
|
await config.async_initialize()
|
||||||
|
|
||||||
|
with patch.object(config, "async_call_homegraph_api"):
|
||||||
|
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config.get_local_webhook_id(agent_user_id) == local_webhook_id
|
||||||
|
assert config.get_local_agent_user_id(local_webhook_id) == agent_user_id
|
||||||
|
assert config.get_local_agent_user_id("INCORRECT") is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_secure_device_pin_config(hass):
|
||||||
|
"""Test the setting of the secure device pin configuration."""
|
||||||
|
secure_pin = "TEST"
|
||||||
|
secure_config = GOOGLE_ASSISTANT_SCHEMA(
|
||||||
|
{
|
||||||
|
"project_id": "1234",
|
||||||
|
"service_account": {
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKYscIlwm7soDsHAz6L6YvUkCvkrX19rS6yeYOmovvhoK5WeYGWUsd8V72zmsyHB7XO94YgJVjvxfzn5K8bLePjFzwoSJjZvhBJ/ZQ05d8VmbvgyWUoPdG9oEa4fZ/lCYrXoaFdTot2xcJvrb/ZuiRl4s4eZpNeFYvVK/Am7UeFPAgMBAAECgYAUetOfzLYUudofvPCaKHu7tKZ5kQPfEa0w6BAPnBF1Mfl1JiDBRDMryFtKs6AOIAVwx00dY/Ex0BCbB3+Cr58H7t4NaPTJxCpmR09pK7o17B7xAdQv8+SynFNud9/5vQ5AEXMOLNwKiU7wpXT6Z7ZIibUBOR7ewsWgsHCDpN1iqQJBAOMODPTPSiQMwRAUHIc6GPleFSJnIz2PAoG3JOG9KFAL6RtIc19lob2ZXdbQdzKtjSkWo+O5W20WDNAl1k32h6MCQQC7W4ZCIY67mPbL6CxXfHjpSGF4Dr9VWJ7ZrKHr6XUoOIcEvsn/pHvWonjMdy93rQMSfOE8BKd/I1+GHRmNVgplAkAnSo4paxmsZVyfeKt7Jy2dMY+8tVZe17maUuQaAE7Sk00SgJYegwrbMYgQnWCTL39HBfj0dmYA2Zj8CCAuu6O7AkEAryFiYjaUAO9+4iNoL27+ZrFtypeeadyov7gKs0ZKaQpNyzW8A+Zwi7TbTeSqzic/E+z/bOa82q7p/6b7141xsQJBANCAcIwMcVb6KVCHlQbOtKspo5Eh4ZQi8bGl+IcwbQ6JSxeTx915IfAldgbuU047wOB04dYCFB2yLDiUGVXTifU=\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "dummy@dummy.iam.gserviceaccount.com",
|
||||||
|
},
|
||||||
|
"secure_devices_pin": secure_pin,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
config = GoogleConfig(hass, secure_config)
|
||||||
|
|
||||||
|
assert config.secure_devices_pin == secure_pin
|
||||||
|
|
||||||
|
|
||||||
|
async def test_should_expose(hass):
|
||||||
|
"""Test the google config should expose method."""
|
||||||
|
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||||
|
await config.async_initialize()
|
||||||
|
|
||||||
|
with patch.object(config, "async_call_homegraph_api"):
|
||||||
|
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
config.should_expose(State(DOMAIN + ".mock", "mock", {"view": "not None"}))
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(config, "async_call_homegraph_api"):
|
||||||
|
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config.should_expose(State(CLOUD_NEVER_EXPOSED_ENTITIES[0], "mock")) is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_missing_service_account(hass):
|
||||||
|
"""Test the google config _async_request_sync_devices."""
|
||||||
|
incorrect_config = GOOGLE_ASSISTANT_SCHEMA(
|
||||||
|
{
|
||||||
|
"project_id": "1234",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
config = GoogleConfig(hass, incorrect_config)
|
||||||
|
await config.async_initialize()
|
||||||
|
|
||||||
|
with patch.object(config, "async_call_homegraph_api"):
|
||||||
|
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
await config._async_request_sync_devices("mock")
|
||||||
|
is HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
renew = config._access_token_renew
|
||||||
|
await config._async_update_token()
|
||||||
|
assert config._access_token_renew is renew
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_enable_local_sdk(hass, hass_client, hass_storage, caplog):
|
||||||
|
"""Test the google config enable and disable local sdk."""
|
||||||
|
command_events = async_capture_events(hass, EVENT_COMMAND_RECEIVED)
|
||||||
|
turn_on_calls = async_mock_service(hass, "light", "turn_on")
|
||||||
|
hass.states.async_set("light.ceiling_lights", "off")
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "webhook", {})
|
||||||
|
|
||||||
|
hass_storage["google_assistant"] = {
|
||||||
|
"version": 1,
|
||||||
|
"minor_version": 1,
|
||||||
|
"key": "google_assistant",
|
||||||
|
"data": {
|
||||||
|
"agent_user_ids": {
|
||||||
|
"agent_1": {
|
||||||
|
"local_webhook_id": "mock_webhook_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||||
|
await config.async_initialize()
|
||||||
|
|
||||||
|
with patch.object(config, "async_call_homegraph_api"):
|
||||||
|
# Wait for google_assistant.helpers.async_initialize.sync_google to be called
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config.is_local_sdk_active is True
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/webhook/mock_webhook_id",
|
||||||
|
json={
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"context": {"locale_country": "US", "locale_language": "en"},
|
||||||
|
"intent": "action.devices.EXECUTE",
|
||||||
|
"payload": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"devices": [{"id": "light.ceiling_lights"}],
|
||||||
|
"execution": [
|
||||||
|
{
|
||||||
|
"command": "action.devices.commands.OnOff",
|
||||||
|
"params": {"on": True},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"structureData": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestId": "mock_req_id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["requestId"] == "mock_req_id"
|
||||||
|
|
||||||
|
assert len(command_events) == 1
|
||||||
|
assert command_events[0].context.user_id == "agent_1"
|
||||||
|
|
||||||
|
assert len(turn_on_calls) == 1
|
||||||
|
assert turn_on_calls[0].context is command_events[0].context
|
||||||
|
|
||||||
|
config.async_disable_local_sdk()
|
||||||
|
assert config.is_local_sdk_active is False
|
||||||
|
|
||||||
|
config._store._data = {
|
||||||
|
STORE_AGENT_USER_IDS: {
|
||||||
|
"agent_1": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock_webhook_id"},
|
||||||
|
"agent_2": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock_webhook_id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.async_enable_local_sdk()
|
||||||
|
assert config.is_local_sdk_active is False
|
||||||
|
|
||||||
|
config._store._data = {
|
||||||
|
STORE_AGENT_USER_IDS: {
|
||||||
|
"agent_1": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: None},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.async_enable_local_sdk()
|
||||||
|
assert config.is_local_sdk_active is False
|
||||||
|
|
||||||
|
config._store._data = {
|
||||||
|
STORE_AGENT_USER_IDS: {
|
||||||
|
"agent_2": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock_webhook_id"},
|
||||||
|
"agent_1": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: None},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.async_enable_local_sdk()
|
||||||
|
assert config.is_local_sdk_active is False
|
||||||
|
|
||||||
|
config.async_disable_local_sdk()
|
||||||
|
|
||||||
|
config._store._data = {
|
||||||
|
STORE_AGENT_USER_IDS: {
|
||||||
|
"agent_1": {STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock_webhook_id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.async_enable_local_sdk()
|
||||||
|
|
||||||
|
config._store.pop_agent_user_id("agent_1")
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/webhook/mock_webhook_id",
|
||||||
|
json={
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"context": {"locale_country": "US", "locale_language": "en"},
|
||||||
|
"intent": "action.devices.EXECUTE",
|
||||||
|
"payload": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"devices": [{"id": "light.ceiling_lights"}],
|
||||||
|
"execution": [
|
||||||
|
{
|
||||||
|
"command": "action.devices.commands.OnOff",
|
||||||
|
"params": {"on": True},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"structureData": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestId": "mock_req_id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
assert (
|
||||||
|
"Cannot process request for webhook mock_webhook_id as no linked agent user is found:"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
@ -53,6 +53,58 @@ def registries(hass):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_handle_message(hass):
|
||||||
|
"""Test the async handle message method."""
|
||||||
|
config = MockConfig(
|
||||||
|
should_expose=lambda state: state.entity_id != "light.not_expose",
|
||||||
|
entity_config={
|
||||||
|
"light.demo_light": {
|
||||||
|
const.CONF_ROOM_HINT: "Living Room",
|
||||||
|
const.CONF_ALIASES: ["Hello", "World"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await sh.async_handle_message(
|
||||||
|
hass,
|
||||||
|
config,
|
||||||
|
"test-agent",
|
||||||
|
{
|
||||||
|
"requestId": REQ_ID,
|
||||||
|
"inputs": [
|
||||||
|
{"intent": "action.devices.SYNC"},
|
||||||
|
{"intent": "action.devices.SYNC"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
const.SOURCE_CLOUD,
|
||||||
|
)
|
||||||
|
assert result == {
|
||||||
|
"requestId": REQ_ID,
|
||||||
|
"payload": {"errorCode": const.ERR_PROTOCOL_ERROR},
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await sh.async_handle_message(
|
||||||
|
hass,
|
||||||
|
config,
|
||||||
|
"test-agent",
|
||||||
|
{
|
||||||
|
"requestId": REQ_ID,
|
||||||
|
"inputs": [
|
||||||
|
{"intent": "action.devices.DOES_NOT_EXIST"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
const.SOURCE_CLOUD,
|
||||||
|
)
|
||||||
|
assert result == {
|
||||||
|
"requestId": REQ_ID,
|
||||||
|
"payload": {"errorCode": const.ERR_PROTOCOL_ERROR},
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_sync_message(hass):
|
async def test_sync_message(hass):
|
||||||
"""Test a sync message."""
|
"""Test a sync message."""
|
||||||
light = DemoLight(
|
light = DemoLight(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user