Cloud: Websocket API to manage Google assistant entity config (#24153)

* Extract exposed devices function

* Add might_2fa info to trait

* Do not filter with should_expose in Google helper func

* Cloud: allow setting if Google entity is exposed

* Allow disabling 2FA via config

* Cloud: allow disabling 2FA

* Lint

* More changes

* Fix typing
This commit is contained in:
Paulus Schoutsen 2019-05-29 08:39:12 -07:00 committed by GitHub
parent 85dfea1642
commit 6947f8cb2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 346 additions and 86 deletions

View File

@ -17,7 +17,9 @@ from homeassistant.util.aiohttp import MockRequest
from . import utils
from .const import (
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
from .prefs import CloudPreferences
@ -98,12 +100,26 @@ class CloudClient(Interface):
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
return google_conf['filter'](entity.entity_id)
if not google_conf['filter'].empty_filter:
return google_conf['filter'](entity.entity_id)
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return entity_config.get(
PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
def should_2fa(entity):
"""If an entity should be checked for 2FA."""
entity_configs = self.prefs.google_entity_configs
entity_config = entity_configs.get(entity.entity_id, {})
return not entity_config.get(
PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
username = self._hass.data[DOMAIN].claims["cognito:username"]
self._google_config = ga_h.Config(
should_expose=should_expose,
should_2fa=should_2fa,
secure_devices_pin=self._prefs.google_secure_devices_pin,
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
agent_user_id=username,

View File

@ -8,6 +8,13 @@ PREF_ENABLE_REMOTE = 'remote_enabled'
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
PREF_OVERRIDE_NAME = 'override_name'
PREF_DISABLE_2FA = 'disable_2fa'
PREF_ALIASES = 'aliases'
PREF_SHOULD_EXPOSE = 'should_expose'
DEFAULT_SHOULD_EXPOSE = True
DEFAULT_DISABLE_2FA = False
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'

View File

@ -14,8 +14,7 @@ from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import (
const as google_const)
from homeassistant.components.google_assistant import helpers as google_helpers
from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@ -81,6 +80,12 @@ async def async_setup(hass):
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
hass.components.websocket_api.async_register_command(
google_assistant_list)
hass.components.websocket_api.async_register_command(
google_assistant_update)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@ -411,7 +416,6 @@ def _account_data(cloud):
'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg):
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud))
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities'
})
async def google_assistant_list(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
entities = google_helpers.async_get_entities(
hass, cloud.client.google_config
)
result = []
for entity in entities:
result.append({
'entity_id': entity.entity_id,
'traits': [trait.name for trait in entity.traits()],
'might_2fa': entity.might_2fa(),
})
connection.send_result(msg['id'], result)
@websocket_api.require_admin
@_require_cloud_login
@websocket_api.async_response
@_ws_handle_cloud_errors
@websocket_api.websocket_command({
'type': 'cloud/google_assistant/entities/update',
'entity_id': str,
vol.Optional('should_expose'): bool,
vol.Optional('override_name'): str,
vol.Optional('aliases'): [str],
vol.Optional('disable_2fa'): bool,
})
async def google_assistant_update(hass, connection, msg):
"""List all google assistant entities."""
cloud = hass.data[DOMAIN]
changes = dict(msg)
changes.pop('type')
changes.pop('id')
await cloud.client.prefs.async_update_google_entity_config(**changes)
connection.send_result(
msg['id'],
cloud.client.prefs.google_entity_configs.get(msg['entity_id']))

View File

@ -4,6 +4,8 @@ from ipaddress import ip_address
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
PREF_ALIASES, PREF_SHOULD_EXPOSE,
InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN
@ -30,6 +32,7 @@ class CloudPreferences:
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None,
}
@ -39,7 +42,7 @@ class CloudPreferences:
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
cloud_user=_UNDEF):
cloud_user=_UNDEF, google_entity_configs=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
@ -48,6 +51,7 @@ class CloudPreferences:
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
):
if value is not _UNDEF:
self._prefs[key] = value
@ -57,9 +61,48 @@ class CloudPreferences:
await self._store.async_save(self._prefs)
async def async_update_google_entity_config(
self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
aliases=_UNDEF, should_expose=_UNDEF):
"""Update config for a Google entity."""
entities = self.google_entity_configs
entity = entities.get(entity_id, {})
changes = {}
for key, value in (
(PREF_OVERRIDE_NAME, override_name),
(PREF_DISABLE_2FA, disable_2fa),
(PREF_ALIASES, aliases),
(PREF_SHOULD_EXPOSE, should_expose),
):
if value is not _UNDEF:
changes[key] = value
if not changes:
return
updated_entity = {
**entity,
**changes,
}
updated_entities = {
**entities,
entity_id: updated_entity,
}
await self.async_update(google_entity_configs=updated_entities)
def as_dict(self):
"""Return dictionary version."""
return self._prefs
return {
PREF_ENABLE_ALEXA: self.alexa_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_CLOUD_USER: self.cloud_user,
}
@property
def remote_enabled(self):
@ -89,6 +132,11 @@ class CloudPreferences:
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
@property
def google_entity_configs(self):
"""Return Google Entity configurations."""
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""

View File

@ -1,17 +1,18 @@
"""Helper classes for Google Assistant integration."""
from asyncio import gather
from collections.abc import Mapping
from typing import List
from homeassistant.core import Context, callback
from homeassistant.const import (
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES,
ATTR_DEVICE_CLASS
ATTR_DEVICE_CLASS, CLOUD_NEVER_EXPOSED_ENTITIES
)
from . import trait
from .const import (
DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED,
DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT,
DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT
)
from .error import SmartHomeError
@ -21,15 +22,20 @@ class Config:
def __init__(self, should_expose,
entity_config=None, secure_devices_pin=None,
agent_user_id=None):
agent_user_id=None, should_2fa=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.entity_config = entity_config or {}
self.secure_devices_pin = secure_devices_pin
self._should_2fa = should_2fa
# Agent User Id to use for query responses
self.agent_user_id = agent_user_id
def should_2fa(self, state):
"""If an entity should have 2FA checked."""
return self._should_2fa is None or self._should_2fa(state)
class RequestData:
"""Hold data associated with a particular request."""
@ -79,6 +85,22 @@ class GoogleEntity:
if Trait.supported(domain, features, device_class)]
return self._traits
@callback
def is_supported(self) -> bool:
"""Return if the entity is supported by Google."""
return self.state.state != STATE_UNAVAILABLE and bool(self.traits())
@callback
def might_2fa(self) -> bool:
"""Return if the entity might encounter 2FA."""
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
return any(trait.might_2fa(domain, features, device_class)
for trait in self.traits())
async def sync_serialize(self):
"""Serialize entity for a SYNC response.
@ -86,27 +108,13 @@ class GoogleEntity:
"""
state = self.state
# When a state is unavailable, the attributes that describe
# capabilities will be stripped. For example, a light entity will miss
# the min/max mireds. Therefore they will be excluded from a sync.
if state.state == STATE_UNAVAILABLE:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
domain = state.domain
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
# If an empty string
if not name:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None
device_type = get_google_type(domain,
device_class)
@ -213,3 +221,19 @@ def deep_update(target, source):
else:
target[key] = value
return target
@callback
def async_get_entities(hass, config) -> List[GoogleEntity]:
"""Return all entities that are supported by Google."""
entities = []
for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue
entity = GoogleEntity(hass, config, state)
if entity.is_supported():
entities.append(entity)
return entities

View File

@ -1,17 +1,17 @@
"""Support for Google Assistant Smart Home API."""
import asyncio
from itertools import product
import logging
from homeassistant.util.decorator import Registry
from homeassistant.const import (
CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID)
from homeassistant.const import ATTR_ENTITY_ID
from .const import (
ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR,
EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
)
from .helpers import RequestData, GoogleEntity
from .helpers import RequestData, GoogleEntity, async_get_entities
from .error import SmartHomeError
HANDLERS = Registry()
@ -81,22 +81,11 @@ async def async_devices_sync(hass, data, payload):
{'request_id': data.request_id},
context=data.context)
devices = []
for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue
if not data.config.should_expose(state):
continue
entity = GoogleEntity(hass, data.config, state)
serialized = await entity.sync_serialize()
if serialized is None:
_LOGGER.debug("No mapping for %s domain", entity.state)
continue
devices.append(serialized)
devices = await asyncio.gather(*[
entity.sync_serialize() for entity in
async_get_entities(hass, data.config)
if data.config.should_expose(entity.state)
])
response = {
'agentUserId': data.config.agent_user_id or data.context.user_id,

View File

@ -104,6 +104,11 @@ class _Trait:
commands = []
@staticmethod
def might_2fa(domain, features, device_class):
"""Return if the trait might ask for 2FA."""
return False
def __init__(self, hass, state, config):
"""Initialize a trait for a state."""
self.hass = hass
@ -732,6 +737,11 @@ class LockUnlockTrait(_Trait):
"""Test if state is supported."""
return domain == lock.DOMAIN
@staticmethod
def might_2fa(domain, features, device_class):
"""Return if the trait might ask for 2FA."""
return True
def sync_attributes(self):
"""Return LockUnlock attributes for a sync request."""
return {}
@ -745,7 +755,7 @@ class LockUnlockTrait(_Trait):
if params['lock']:
service = lock.SERVICE_LOCK
else:
_verify_pin_challenge(data, challenge)
_verify_pin_challenge(data, self.state, challenge)
service = lock.SERVICE_UNLOCK
await self.hass.services.async_call(lock.DOMAIN, service, {
@ -1021,6 +1031,9 @@ class OpenCloseTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/openclose
"""
# Cover device classes that require 2FA
COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE)
name = TRAIT_OPENCLOSE
commands = [
COMMAND_OPENCLOSE
@ -1042,6 +1055,12 @@ class OpenCloseTrait(_Trait):
binary_sensor.DEVICE_CLASS_WINDOW,
)
@staticmethod
def might_2fa(domain, features, device_class):
"""Return if the trait might ask for 2FA."""
return (domain == cover.DOMAIN and
device_class in OpenCloseTrait.COVER_2FA)
def sync_attributes(self):
"""Return opening direction."""
response = {}
@ -1114,9 +1133,8 @@ class OpenCloseTrait(_Trait):
if (should_verify and
self.state.attributes.get(ATTR_DEVICE_CLASS)
in (cover.DEVICE_CLASS_DOOR,
cover.DEVICE_CLASS_GARAGE)):
_verify_pin_challenge(data, challenge)
in OpenCloseTrait.COVER_2FA):
_verify_pin_challenge(data, self.state, challenge)
await self.hass.services.async_call(
cover.DOMAIN, service, svc_params,
@ -1202,8 +1220,11 @@ class VolumeTrait(_Trait):
ERR_NOT_SUPPORTED, 'Command not supported')
def _verify_pin_challenge(data, challenge):
def _verify_pin_challenge(data, state, challenge):
"""Verify a pin challenge."""
if not data.config.should_2fa(state):
return
if not data.config.secure_devices_pin:
raise SmartHomeError(
ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up')
@ -1217,7 +1238,7 @@ def _verify_pin_challenge(data, challenge):
raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED)
def _verify_ack_challenge(data, challenge):
def _verify_ack_challenge(data, state, challenge):
"""Verify a pin challenge."""
if not challenge or not challenge.get('ack'):
raise ChallengeNeeded(CHALLENGE_ACK_NEEDED)

View File

@ -1,5 +1,5 @@
"""Helper class to implement include/exclude of entities and domains."""
from typing import Callable, Dict, Iterable
from typing import Callable, Dict, List
import voluptuous as vol
@ -12,7 +12,7 @@ CONF_EXCLUDE_DOMAINS = 'exclude_domains'
CONF_EXCLUDE_ENTITIES = 'exclude_entities'
def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]:
def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]:
filt = generate_filter(
config[CONF_INCLUDE_DOMAINS],
config[CONF_INCLUDE_ENTITIES],
@ -20,6 +20,8 @@ def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]:
config[CONF_EXCLUDE_ENTITIES],
)
setattr(filt, 'config', config)
setattr(
filt, 'empty_filter', sum(len(val) for val in config.values()) == 0)
return filt
@ -34,10 +36,10 @@ FILTER_SCHEMA = vol.All(
}), _convert_filter)
def generate_filter(include_domains: Iterable[str],
include_entities: Iterable[str],
exclude_domains: Iterable[str],
exclude_entities: Iterable[str]) -> Callable[[str], bool]:
def generate_filter(include_domains: List[str],
include_entities: List[str],
exclude_domains: List[str],
exclude_entities: List[str]) -> Callable[[str], bool]:
"""Return a function that will filter entities based on the args."""
include_d = set(include_domains)
include_e = set(include_entities)

View File

@ -2,9 +2,12 @@
from unittest.mock import patch, MagicMock
from aiohttp import web
import jwt
import pytest
from homeassistant.core import State
from homeassistant.setup import async_setup_component
from homeassistant.components.cloud import DOMAIN
from homeassistant.components.cloud.const import (
PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE)
from tests.components.alexa import test_smart_home as test_alexa
@ -19,6 +22,25 @@ def mock_cloud():
return MagicMock(subscription_expired=False)
@pytest.fixture
async def mock_cloud_setup(hass):
"""Set up the cloud."""
with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()):
assert await async_setup_component(hass, 'cloud', {
'cloud': {}
})
@pytest.fixture
def mock_cloud_login(hass, mock_cloud_setup):
"""Mock cloud is logged in."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03',
'cognito:username': 'abcdefghjkl',
}, 'test')
async def test_handler_alexa(hass):
"""Test handler Alexa."""
hass.states.async_set(
@ -197,3 +219,35 @@ async def test_webhook_msg(hass):
assert await received[0].json() == {
'hello': 'world'
}
async def test_google_config_expose_entity(
hass, mock_cloud_setup, mock_cloud_login):
"""Test Google config exposing entity method uses latest config."""
cloud_client = hass.data[DOMAIN].client
state = State('light.kitchen', 'on')
assert cloud_client.google_config.should_expose(state)
await cloud_client.prefs.async_update_google_entity_config(
entity_id='light.kitchen',
should_expose=False,
)
assert not cloud_client.google_config.should_expose(state)
async def test_google_config_should_2fa(
hass, mock_cloud_setup, mock_cloud_login):
"""Test Google config disabling 2FA method uses latest config."""
cloud_client = hass.data[DOMAIN].client
state = State('light.kitchen', 'on')
assert cloud_client.google_config.should_2fa(state)
await cloud_client.prefs.async_update_google_entity_config(
entity_id='light.kitchen',
disable_2fa=True,
)
assert not cloud_client.google_config.should_2fa(state)

View File

@ -7,10 +7,13 @@ from jose import jwt
from hass_nabucasa.auth import Unauthenticated, UnknownError
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)
from homeassistant.components.google_assistant.helpers import (
GoogleEntity, Config)
from tests.common import mock_coro
@ -32,7 +35,8 @@ def mock_cloud_login(hass, setup_api):
"""Mock cloud is logged in."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
'custom:sub-exp': '2018-01-03'
'custom:sub-exp': '2018-01-03',
'cognito:username': 'abcdefghjkl',
}, 'test')
@ -349,7 +353,15 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
'logged_in': True,
'email': 'hello@home-assistant.io',
'cloud': 'connected',
'prefs': mock_cloud_fixture,
'prefs': {
'alexa_enabled': True,
'cloud_user': None,
'cloudhooks': {},
'google_enabled': True,
'google_entity_configs': {},
'google_secure_devices_pin': None,
'remote_enabled': False,
},
'alexa_entities': {
'include_domains': [],
'include_entities': ['light.kitchen', 'switch.ac'],
@ -363,7 +375,6 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
'exclude_domains': [],
'exclude_entities': [],
},
'google_domains': ['light'],
'remote_domain': None,
'remote_connected': False,
'remote_certificate': None,
@ -689,3 +700,52 @@ async def test_enabling_remote_trusted_networks_other(
assert cloud.client.remote_autostart
assert len(mock_connect.mock_calls) == 1
async def test_list_google_entities(
hass, hass_ws_client, setup_api, mock_cloud_login):
"""Test that we can list Google entities."""
client = await hass_ws_client(hass)
entity = GoogleEntity(hass, Config(lambda *_: False), State(
'light.kitchen', 'on'
))
with patch('homeassistant.components.google_assistant.helpers'
'.async_get_entities', return_value=[entity]):
await client.send_json({
'id': 5,
'type': 'cloud/google_assistant/entities',
})
response = await client.receive_json()
assert response['success']
assert len(response['result']) == 1
assert response['result'][0] == {
'entity_id': 'light.kitchen',
'might_2fa': False,
'traits': ['action.devices.traits.OnOff'],
}
async def test_update_google_entity(
hass, hass_ws_client, setup_api, mock_cloud_login):
"""Test that we can update config of a Google entity."""
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
'type': 'cloud/google_assistant/entities/update',
'entity_id': 'light.kitchen',
'should_expose': False,
'override_name': 'updated name',
'aliases': ['lefty', 'righty'],
'disable_2fa': False,
})
response = await client.receive_json()
assert response['success']
prefs = hass.data[DOMAIN].client.prefs
assert prefs.google_entity_configs['light.kitchen'] == {
'should_expose': False,
'override_name': 'updated name',
'aliases': ['lefty', 'righty'],
'disable_2fa': False,
}

View File

@ -530,34 +530,6 @@ async def test_unavailable_state_doesnt_sync(hass):
}
async def test_empty_name_doesnt_sync(hass):
"""Test that an entity with empty name does not sync over."""
light = DemoLight(
None, ' ',
state=False,
)
light.hass = hass
light.entity_id = 'light.demo_light'
await light.async_update_ha_state()
result = await sh.async_handle_message(
hass, BASIC_CONFIG, 'test-agent',
{
"requestId": REQ_ID,
"inputs": [{
"intent": "action.devices.SYNC"
}]
})
assert result == {
'requestId': REQ_ID,
'payload': {
'agentUserId': 'test-agent',
'devices': []
}
}
@pytest.mark.parametrize("device_class,google_type", [
('non_existing_class', 'action.devices.types.SWITCH'),
('switch', 'action.devices.types.SWITCH'),

View File

@ -843,6 +843,8 @@ async def test_lock_unlock_lock(hass):
assert helpers.get_google_type(lock.DOMAIN, None) is not None
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN,
None)
assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN,
None)
trt = trait.LockUnlockTrait(hass,
State('lock.front_door', lock.STATE_LOCKED),
@ -922,6 +924,13 @@ async def test_lock_unlock_unlock(hass):
assert len(calls) == 1
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
# Test with 2FA override
with patch('homeassistant.components.google_assistant.helpers'
'.Config.should_2fa', return_value=False):
await trt.execute(
trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {})
assert len(calls) == 2
async def test_fan_speed(hass):
"""Test FanSpeed trait speed control support for fan domain."""
@ -1216,6 +1225,8 @@ async def test_openclose_cover_secure(hass, device_class):
assert helpers.get_google_type(cover.DOMAIN, device_class) is not None
assert trait.OpenCloseTrait.supported(
cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class)
assert trait.OpenCloseTrait.might_2fa(
cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class)
trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, {
ATTR_DEVICE_CLASS: device_class,