parent
3454b6fa87
commit
e866d769e8
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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://")):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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", {})
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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""
|
|
@ -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"}]},
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue