Enable local fulfillment google assistant (#63218)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Loek Sangers 2022-01-05 21:09:59 +01:00 committed by GitHub
parent f786237def
commit 25fe213f22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 480 additions and 79 deletions

View file

@ -24,8 +24,6 @@ class MockConfig(helpers.AbstractConfig):
enabled=True,
entity_config=None,
hass=None,
local_sdk_user_id=None,
local_sdk_webhook_id=None,
secure_devices_pin=None,
should_2fa=None,
should_expose=None,
@ -35,8 +33,6 @@ class MockConfig(helpers.AbstractConfig):
super().__init__(hass)
self._enabled = enabled
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._should_2fa = should_2fa
self._should_expose = should_expose
@ -58,16 +54,6 @@ class MockConfig(helpers.AbstractConfig):
"""Return secure devices pin."""
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):
"""Get agent user ID making request."""
return context.user_id

View file

@ -9,6 +9,9 @@ from homeassistant.components.google_assistant import helpers
from homeassistant.components.google_assistant.const import (
EVENT_COMMAND_RECEIVED,
NOT_EXPOSE_LOCAL,
SOURCE_CLOUD,
SOURCE_LOCAL,
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
)
from homeassistant.config import async_process_ha_core_config
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)
config = MockConfig(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
agent_user_ids={
"mock-user-id": {
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
},
},
)
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()
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["customData"] == {
"httpPort": 1234,
"httpSSL": True,
"proxyDeviceId": None,
"proxyDeviceId": "mock-user-id",
"webhookId": "mock-webhook-id",
"baseUrl": "https://hostname:1234",
"uuid": "abcdef",
@ -79,8 +85,11 @@ async def test_config_local_sdk(hass, hass_client):
config = MockConfig(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
agent_user_ids={
"mock-user-id": {
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
},
},
)
client = await hass_client()
@ -118,7 +127,7 @@ async def test_config_local_sdk(hass, hass_client):
assert result["requestId"] == "mock-req-id"
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 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(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
agent_user_ids={
"mock-user-id": {
STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id",
},
},
enabled=False,
)
@ -171,35 +183,61 @@ async def test_agent_user_id_storage(hass, hass_storage):
"version": 1,
"minor_version": 1,
"key": "google_assistant",
"data": {"agent_user_ids": {"agent_1": {}}},
"data": {
"agent_user_ids": {
"agent_1": {
"local_webhook_id": "test_webhook",
}
},
},
}
store = helpers.GoogleConfigStore(hass)
await store.async_load()
await store.async_initialize()
assert hass_storage["google_assistant"] == {
"version": 1,
"minor_version": 1,
"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_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert hass_storage["google_assistant"] == {
"version": 1,
"minor_version": 1,
"key": "google_assistant",
"data": data,
}
assert (
list(hass_storage["google_assistant"]["data"]["agent_user_ids"].keys())
== data
)
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")
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():
@ -254,3 +292,17 @@ def test_supported_features_string(caplog):
)
assert entity.is_supported() is False
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

View file

@ -5,14 +5,23 @@ from unittest.mock import ANY, patch
from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA
from homeassistant.components.google_assistant.const import (
DOMAIN,
EVENT_COMMAND_RECEIVED,
HOMEGRAPH_TOKEN_URL,
REPORT_STATE_BASE_URL,
STORE_AGENT_USER_IDS,
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
)
from homeassistant.components.google_assistant.http import (
GoogleConfig,
_get_homegraph_jwt,
_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(
{
@ -97,7 +106,7 @@ async def test_update_access_token(hass):
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."""
config = GoogleConfig(hass, DUMMY_CONFIG)
await config.async_initialize()
@ -164,3 +173,237 @@ async def test_report_state(hass, aioclient_mock, hass_storage):
REPORT_STATE_BASE_URL,
{"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
)

View file

@ -53,6 +53,58 @@ def registries(hass):
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):
"""Test a sync message."""
light = DemoLight(