Google Assistant Local SDK (#27428)

* Local Google

* Fix test

* Fix tests
pull/27623/head
Paulus Schoutsen 2019-10-13 14:16:27 -07:00 committed by GitHub
parent 3454b6fa87
commit e866d769e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 512 additions and 52 deletions

View File

@ -110,14 +110,17 @@ class CloudClient(Interface):
if not self.cloud.is_logged_in:
return
if self.alexa_config.should_report_state:
if self.alexa_config.enabled and 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()
if self.google_config.enabled:
self.google_config.async_enable_local_sdk()
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

@ -16,6 +16,7 @@ PREF_OVERRIDE_NAME = "override_name"
PREF_DISABLE_2FA = "disable_2fa"
PREF_ALIASES = "aliases"
PREF_SHOULD_EXPOSE = "should_expose"
PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id"
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = False

View File

@ -63,6 +63,19 @@ class CloudGoogleConfig(AbstractConfig):
"""Return if states should be proactively reported."""
return self._prefs.google_report_state
@property
def local_sdk_webhook_id(self):
"""Return the local SDK webhook.
Return None to disable the local SDK.
"""
return self._prefs.google_local_webhook_id
@property
def local_sdk_user_id(self):
"""Return the user ID to be used for actions received via the local SDK."""
return self._prefs.cloud_user
def should_expose(self, state):
"""If a state object should be exposed."""
return self._should_expose_entity_id(state.entity_id)
@ -131,17 +144,19 @@ 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()
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
elif (
self._cur_entity_prefs is not prefs.google_entity_configs
and self._config["filter"].empty_filter
):
return
self.async_schedule_google_sync()
self.async_schedule_google_sync()
if self.enabled and not self.is_local_sdk_active:
self.async_enable_local_sdk()
elif not self.enabled and self.is_local_sdk_active:
self.async_disable_local_sdk()
async def _handle_entity_registry_updated(self, event):
"""Handle when entity registry updated."""

View File

@ -21,6 +21,7 @@ from .const import (
PREF_ALEXA_REPORT_STATE,
DEFAULT_ALEXA_REPORT_STATE,
PREF_GOOGLE_REPORT_STATE,
PREF_GOOGLE_LOCAL_WEBHOOK_ID,
DEFAULT_GOOGLE_REPORT_STATE,
InvalidTrustedNetworks,
InvalidTrustedProxies,
@ -59,6 +60,14 @@ class CloudPreferences:
self._prefs = prefs
if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs:
await self._save_prefs(
{
**self._prefs,
PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(),
}
)
@callback
def async_listen_updates(self, listener):
"""Listen for updates to the preferences."""
@ -79,6 +88,8 @@ class CloudPreferences:
google_report_state=_UNDEF,
):
"""Update user preferences."""
prefs = {**self._prefs}
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
@ -92,20 +103,17 @@ class CloudPreferences:
(PREF_GOOGLE_REPORT_STATE, google_report_state),
):
if value is not _UNDEF:
self._prefs[key] = value
prefs[key] = value
if remote_enabled is True and self._has_local_trusted_network:
self._prefs[PREF_ENABLE_REMOTE] = False
prefs[PREF_ENABLE_REMOTE] = False
raise InvalidTrustedNetworks
if remote_enabled is True and self._has_local_trusted_proxies:
self._prefs[PREF_ENABLE_REMOTE] = False
prefs[PREF_ENABLE_REMOTE] = False
raise InvalidTrustedProxies
await self._store.async_save(self._prefs)
for listener in self._listeners:
self._hass.async_create_task(async_create_catching_coro(listener(self)))
await self._save_prefs(prefs)
async def async_update_google_entity_config(
self,
@ -216,6 +224,11 @@ class CloudPreferences:
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
@property
def google_local_webhook_id(self):
"""Return Google webhook ID to receive local messages."""
return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
@property
def alexa_entity_configs(self):
"""Return Alexa Entity configurations."""
@ -262,3 +275,11 @@ class CloudPreferences:
return True
return False
async def _save_prefs(self, prefs):
"""Save preferences to disk."""
self._prefs = prefs
await self._store.async_save(self._prefs)
for listener in self._listeners:
self._hass.async_create_task(async_create_catching_coro(listener(self)))

View File

@ -1,10 +1,15 @@
"""Helper classes for Google Assistant integration."""
from asyncio import gather
from collections.abc import Mapping
from typing import List
import logging
import pprint
from typing import List, Optional
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.const import (
CONF_NAME,
STATE_UNAVAILABLE,
@ -15,6 +20,7 @@ from homeassistant.const import (
from . import trait
from .const import (
DOMAIN,
DOMAIN_TO_GOOGLE_TYPES,
CONF_ALIASES,
ERR_FUNCTION_NOT_SUPPORTED,
@ -24,6 +30,7 @@ from .const import (
from .error import SmartHomeError
SYNC_DELAY = 15
_LOGGER = logging.getLogger(__name__)
class AbstractConfig:
@ -35,6 +42,7 @@ class AbstractConfig:
"""Initialize abstract config."""
self.hass = hass
self._google_sync_unsub = None
self._local_sdk_active = False
@property
def enabled(self):
@ -61,12 +69,30 @@ class AbstractConfig:
"""Return if we're actively reporting states."""
return self._unsub_report_state is not None
@property
def is_local_sdk_active(self):
"""Return if we're actively accepting local messages."""
return self._local_sdk_active
@property
def should_report_state(self):
"""Return if states should be proactively reported."""
# pylint: disable=no-self-use
return False
@property
def local_sdk_webhook_id(self):
"""Return the local SDK webhook ID.
Return None to disable the local SDK.
"""
return None
@property
def local_sdk_user_id(self):
"""Return the user ID to be used for actions received via the local SDK."""
raise NotImplementedError
def should_expose(self, state) -> bool:
"""Return if entity should be exposed."""
raise NotImplementedError
@ -131,15 +157,66 @@ class AbstractConfig:
Called when the user disconnects their account from Google.
"""
@callback
def async_enable_local_sdk(self):
"""Enable the local SDK."""
webhook_id = self.local_sdk_webhook_id
if webhook_id is None:
return
webhook.async_register(
self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook
)
self._local_sdk_active = True
@callback
def async_disable_local_sdk(self):
"""Disable the local SDK."""
if not self._local_sdk_active:
return
webhook.async_unregister(self.hass, self.local_sdk_webhook_id)
self._local_sdk_active = False
async def _handle_local_webhook(self, hass, webhook_id, request):
"""Handle an incoming local SDK message."""
from . import smart_home
payload = await request.json()
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload))
if not self.enabled:
return json_response(smart_home.turned_off_response(payload))
result = await smart_home.async_handle_message(
self.hass, self, self.local_sdk_user_id, payload
)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result))
return json_response(result)
class RequestData:
"""Hold data associated with a particular request."""
def __init__(self, config: AbstractConfig, user_id: str, request_id: str):
def __init__(
self,
config: AbstractConfig,
user_id: str,
request_id: str,
devices: Optional[List[dict]],
):
"""Initialize the request data."""
self.config = config
self.request_id = request_id
self.context = Context(user_id=user_id)
self.devices = devices
def get_google_type(domain, device_class):
@ -234,6 +311,15 @@ class GoogleEntity:
if aliases:
device["name"]["nicknames"] = aliases
if self.config.is_local_sdk_active:
device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
device["customData"] = {
"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,
}
for trt in traits:
device["attributes"].update(trt.sync_attributes())
@ -280,6 +366,11 @@ class GoogleEntity:
return attrs
@callback
def reachable_device_serialize(self):
"""Serialize entity for a REACHABLE_DEVICE response."""
return {"verificationId": self.entity_id}
async def execute(self, data, command_payload):
"""Execute a command.

View File

@ -5,7 +5,7 @@ import logging
from homeassistant.util.decorator import Registry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, __version__
from .const import (
ERR_PROTOCOL_ERROR,
@ -24,9 +24,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages."""
request_id: str = message.get("requestId")
data = RequestData(config, user_id, request_id)
data = RequestData(config, user_id, message["requestId"], message.get("devices"))
response = await _process(hass, data, message)
@ -67,6 +65,7 @@ async def _process(hass, data, message):
if result is None:
return None
return {"requestId": data.request_id, "payload": result}
@ -74,7 +73,7 @@ async def _process(hass, data, message):
async def async_devices_sync(hass, data, payload):
"""Handle action.devices.SYNC request.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC
"""
hass.bus.async_fire(
EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context
@ -84,7 +83,7 @@ async def async_devices_sync(hass, data, payload):
*(
entity.sync_serialize()
for entity in async_get_entities(hass, data.config)
if data.config.should_expose(entity.state)
if entity.should_expose()
)
)
@ -100,7 +99,7 @@ async def async_devices_sync(hass, data, payload):
async def async_devices_query(hass, data, payload):
"""Handle action.devices.QUERY request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY
"""
devices = {}
for device in payload.get("devices", []):
@ -128,7 +127,7 @@ async def async_devices_query(hass, data, payload):
async def handle_devices_execute(hass, data, payload):
"""Handle action.devices.EXECUTE request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE
"""
entities = {}
results = {}
@ -196,12 +195,50 @@ async def handle_devices_execute(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
https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT
"""
await data.config.async_deactivate_report_state()
return None
@HANDLERS.register("action.devices.IDENTIFY")
async def async_devices_identify(hass, data: RequestData, payload):
"""Handle action.devices.IDENTIFY request.
https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler
"""
return {
"device": {
"id": data.config.agent_user_id,
"isLocalOnly": True,
"isProxy": True,
"deviceInfo": {
"hwVersion": "UNKNOWN_HW_VERSION",
"manufacturer": "Home Assistant",
"model": "Home Assistant",
"swVersion": __version__,
},
}
}
@HANDLERS.register("action.devices.REACHABLE_DEVICES")
async def async_devices_reachable(hass, data: RequestData, payload):
"""Handle action.devices.REACHABLE_DEVICES request.
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
"""
google_ids = set(dev["id"] for dev in (data.devices or []))
return {
"devices": [
entity.reachable_device_serialize()
for entity in async_get_entities(hass, data.config)
if entity.entity_id in google_ids and entity.should_expose()
]
}
def turned_off_response(message):
"""Return a device turned off response."""
return {

View File

@ -128,6 +128,7 @@ class ApiConfig:
"""Initialize a new API config object."""
self.host = host
self.port = port
self.use_ssl = use_ssl
host = host.rstrip("/")
if host.startswith(("http://", "https://")):

View File

@ -1,7 +1,7 @@
"""Webhooks for Home Assistant."""
import logging
from aiohttp.web import Response
from aiohttp.web import Response, Request
import voluptuous as vol
from homeassistant.core import callback
@ -98,9 +98,11 @@ class WebhookView(HomeAssistantView):
url = URL_WEBHOOK_PATH
name = "api:webhook"
requires_auth = False
cors_allowed = True
async def _handle(self, request, webhook_id):
async def _handle(self, request: Request, webhook_id):
"""Handle webhook call."""
_LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id)
hass = request.app["hass"]
return await async_handle_webhook(hass, webhook_id, request)

View File

@ -11,7 +11,11 @@ import voluptuous as vol
from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
from homeassistant import util
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START,
__version__,
)
from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT
_LOGGER = logging.getLogger(__name__)
@ -33,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up Zeroconf and make Home Assistant discoverable."""
zeroconf = Zeroconf()
zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}"
params = {
@ -58,9 +63,15 @@ def setup(hass, config):
properties=params,
)
zeroconf = Zeroconf()
def zeroconf_hass_start(_event):
"""Expose Home Assistant on zeroconf when it starts.
zeroconf.register_service(info)
Wait till started or otherwise HTTP is not up and running.
"""
_LOGGER.info("Starting Zeroconf broadcast")
zeroconf.register_service(info)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start)
def service_update(zeroconf, service_type, name, state_change):
"""Service state changed."""

View File

@ -230,7 +230,6 @@ def get_test_instance_port():
return _TEST_INSTANCE_PORT
@ha.callback
def async_mock_service(hass, domain, service, schema=None):
"""Set up a fake service & return a calls log list to this service."""
calls = []

View File

@ -25,4 +25,4 @@ def mock_cloud_prefs(hass, prefs={}):
}
prefs_to_set.update(prefs)
hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set
return prefs_to_set
return hass.data[cloud.DOMAIN].client._prefs

View File

@ -61,7 +61,7 @@ async def test_handler_alexa(hass):
async def test_handler_alexa_disabled(hass, mock_cloud_fixture):
"""Test handler Alexa when user has disabled it."""
mock_cloud_fixture[PREF_ENABLE_ALEXA] = False
mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False
cloud = hass.data["cloud"]
resp = await cloud.client.async_alexa_message(
@ -125,7 +125,7 @@ async def test_handler_google_actions(hass):
async def test_handler_google_actions_disabled(hass, mock_cloud_fixture):
"""Test handler Google Actions when user has disabled it."""
mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False
mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False
with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()):
assert await async_setup_component(hass, "cloud", {})

View File

@ -10,13 +10,7 @@ from hass_nabucasa.const import STATE_CONNECTED
from homeassistant.core import State
from homeassistant.auth.providers import trusted_networks as tn_auth
from homeassistant.components.cloud.const import (
PREF_ENABLE_GOOGLE,
PREF_ENABLE_ALEXA,
PREF_GOOGLE_SECURE_DEVICES_PIN,
DOMAIN,
RequireRelink,
)
from homeassistant.components.cloud.const import DOMAIN, RequireRelink
from homeassistant.components.google_assistant.helpers import GoogleEntity
from homeassistant.components.alexa.entities import LightCapabilities
from homeassistant.components.alexa import errors as alexa_errors
@ -474,9 +468,9 @@ async def test_websocket_update_preferences(
hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login
):
"""Test updating preference."""
assert setup_api[PREF_ENABLE_GOOGLE]
assert setup_api[PREF_ENABLE_ALEXA]
assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None
assert setup_api.google_enabled
assert setup_api.alexa_enabled
assert setup_api.google_secure_devices_pin is None
client = await hass_ws_client(hass)
await client.send_json(
{
@ -490,9 +484,9 @@ async def test_websocket_update_preferences(
response = await client.receive_json()
assert response["success"]
assert not setup_api[PREF_ENABLE_GOOGLE]
assert not setup_api[PREF_ENABLE_ALEXA]
assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == "1234"
assert not setup_api.google_enabled
assert not setup_api.alexa_enabled
assert setup_api.google_secure_devices_pin == "1234"
async def test_websocket_update_preferences_require_relink(

View File

@ -12,12 +12,23 @@ class MockConfig(helpers.AbstractConfig):
should_expose=None,
entity_config=None,
hass=None,
local_sdk_webhook_id=None,
local_sdk_user_id=None,
enabled=True,
):
"""Initialize config."""
super().__init__(hass)
self._should_expose = should_expose
self._secure_devices_pin = secure_devices_pin
self._entity_config = entity_config or {}
self._local_sdk_webhook_id = local_sdk_webhook_id
self._local_sdk_user_id = local_sdk_user_id
self._enabled = enabled
@property
def enabled(self):
"""Return if Google is enabled."""
return self._enabled
@property
def secure_devices_pin(self):
@ -29,6 +40,16 @@ 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 should_expose(self, state):
"""Expose it all."""
return self._should_expose is None or self._should_expose(state)

View File

@ -0,0 +1,130 @@
"""Test Google Assistant helpers."""
from unittest.mock import Mock
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 . import MockConfig
from tests.common import async_capture_events, async_mock_service
async def test_google_entity_sync_serialize_with_local_sdk(hass):
"""Test sync serialize attributes of a GoogleEntity."""
hass.states.async_set("light.ceiling_lights", "off")
hass.config.api = Mock(port=1234, use_ssl=True)
config = MockConfig(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
)
entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights"))
serialized = await entity.sync_serialize()
assert "otherDeviceIds" not in serialized
assert "customData" not in serialized
config.async_enable_local_sdk()
serialized = await entity.sync_serialize()
assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}]
assert serialized["customData"] == {
"httpPort": 1234,
"httpSSL": True,
"proxyDeviceId": None,
"webhookId": "mock-webhook-id",
}
async def test_config_local_sdk(hass, hass_client):
"""Test the 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", {})
config = MockConfig(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
)
client = await hass_client()
config.async_enable_local_sdk()
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 == 200
result = await resp.json()
assert result["requestId"] == "mock-req-id"
assert len(command_events) == 1
assert command_events[0].context.user_id == config.local_sdk_user_id
assert len(turn_on_calls) == 1
assert turn_on_calls[0].context is command_events[0].context
config.async_disable_local_sdk()
# Webhook is no longer active
resp = await client.post("/api/webhook/mock-webhook-id")
assert resp.status == 200
assert await resp.read() == b""
async def test_config_local_sdk_if_disabled(hass, hass_client):
"""Test the local SDK."""
assert await async_setup_component(hass, "webhook", {})
config = MockConfig(
hass=hass,
local_sdk_webhook_id="mock-webhook-id",
local_sdk_user_id="mock-user-id",
enabled=False,
)
client = await hass_client()
config.async_enable_local_sdk()
resp = await client.post(
"/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"}
)
assert resp.status == 200
result = await resp.json()
assert result == {
"payload": {"errorCode": "deviceTurnedOff"},
"requestId": "mock-req-id",
}
config.async_disable_local_sdk()
# Webhook is no longer active
resp = await client.post("/api/webhook/mock-webhook-id")
assert resp.status == 200
assert await resp.read() == b""

View File

@ -3,7 +3,7 @@ from unittest.mock import patch, Mock
import pytest
from homeassistant.core import State, EVENT_CALL_SERVICE
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__
from homeassistant.setup import async_setup_component
from homeassistant.components import camera
from homeassistant.components.climate.const import (
@ -734,3 +734,137 @@ async def test_trait_execute_adding_query_data(hass):
]
},
}
async def test_identify(hass):
"""Test identify message."""
result = await sh.async_handle_message(
hass,
BASIC_CONFIG,
None,
{
"requestId": REQ_ID,
"inputs": [
{
"intent": "action.devices.IDENTIFY",
"payload": {
"device": {
"mdnsScanData": {
"additionals": [
{
"type": "TXT",
"class": "IN",
"name": "devhome._home-assistant._tcp.local",
"ttl": 4500,
"data": [
"version=0.101.0.dev0",
"base_url=http://192.168.1.101:8123",
"requires_api_password=true",
],
}
]
}
},
"structureData": {},
},
}
],
"devices": [
{
"id": "light.ceiling_lights",
"customData": {
"httpPort": 8123,
"httpSSL": False,
"proxyDeviceId": BASIC_CONFIG.agent_user_id,
"webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
},
}
],
},
)
assert result == {
"requestId": REQ_ID,
"payload": {
"device": {
"id": BASIC_CONFIG.agent_user_id,
"isLocalOnly": True,
"isProxy": True,
"deviceInfo": {
"hwVersion": "UNKNOWN_HW_VERSION",
"manufacturer": "Home Assistant",
"model": "Home Assistant",
"swVersion": __version__,
},
}
},
}
async def test_reachable_devices(hass):
"""Test REACHABLE_DEVICES intent."""
# Matching passed in device.
hass.states.async_set("light.ceiling_lights", "on")
# Unsupported entity
hass.states.async_set("not_supported.entity", "something")
# Excluded via config
hass.states.async_set("light.not_expose", "on")
# Not passed in as google_id
hass.states.async_set("light.not_mentioned", "on")
config = MockConfig(
should_expose=lambda state: state.entity_id != "light.not_expose"
)
result = await sh.async_handle_message(
hass,
config,
None,
{
"requestId": REQ_ID,
"inputs": [
{
"intent": "action.devices.REACHABLE_DEVICES",
"payload": {
"device": {
"proxyDevice": {
"id": "6a04f0f7-6125-4356-a846-861df7e01497",
"customData": "{}",
"proxyData": "{}",
}
},
"structureData": {},
},
}
],
"devices": [
{
"id": "light.ceiling_lights",
"customData": {
"httpPort": 8123,
"httpSSL": False,
"proxyDeviceId": BASIC_CONFIG.agent_user_id,
"webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
},
},
{
"id": "light.not_expose",
"customData": {
"httpPort": 8123,
"httpSSL": False,
"proxyDeviceId": BASIC_CONFIG.agent_user_id,
"webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
},
},
{"id": BASIC_CONFIG.agent_user_id, "customData": {}},
],
},
)
assert result == {
"requestId": REQ_ID,
"payload": {"devices": [{"verificationId": "light.ceiling_lights"}]},
}

View File

@ -48,11 +48,11 @@ _LOGGER = logging.getLogger(__name__)
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID)
BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None)
PIN_CONFIG = MockConfig(secure_devices_pin="1234")
PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID)
PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None)
async def test_brightness_light(hass):