From 8313225b401a0327f0c132de34d92042c21b1c43 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Jan 2018 15:14:56 -0800 Subject: [PATCH] Move Google Assistant entity config out of customize (#11499) * Move Google Assistant entity config out of customize * CONF_ALIAS -> CONF_ALIASES * Lint --- homeassistant/components/cloud/__init__.py | 33 ++++++++++++----- .../components/google_assistant/__init__.py | 15 ++++++-- .../components/google_assistant/auth.py | 2 +- .../components/google_assistant/const.py | 6 ++-- .../components/google_assistant/http.py | 14 ++++---- .../components/google_assistant/smart_home.py | 36 ++++++++++--------- tests/components/cloud/test_iot.py | 11 +++++- .../google_assistant/test_google_assistant.py | 29 +++++---------- 8 files changed, 86 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index e93eb086fd0..e497f4677e4 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -10,7 +10,7 @@ import async_timeout import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE) + EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE) from homeassistant.helpers import entityfilter from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -31,6 +31,7 @@ CONF_FILTER = 'filter' CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' +CONF_ALIASES = 'aliases' MODE_DEV = 'development' DEFAULT_MODE = 'production' @@ -44,6 +45,12 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({ vol.Optional(alexa_sh.CONF_NAME): cv.string, }) +GOOGLE_ENTITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT), + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) +}) + ASSISTANT_SCHEMA = vol.Schema({ vol.Optional( CONF_FILTER, @@ -55,6 +62,10 @@ ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({ vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} }) +GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({ + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA} +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MODE, default=DEFAULT_MODE): @@ -65,7 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, - vol.Optional(CONF_GOOGLE_ACTIONS): ASSISTANT_SCHEMA, + vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) @@ -79,14 +90,15 @@ def async_setup(hass, config): kwargs = {CONF_MODE: DEFAULT_MODE} alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({}) - gactions_conf = (kwargs.pop(CONF_GOOGLE_ACTIONS, None) or - ASSISTANT_SCHEMA({})) + + if CONF_GOOGLE_ACTIONS not in kwargs: + kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({}) kwargs[CONF_ALEXA] = alexa_sh.Config( should_expose=alexa_conf[CONF_FILTER], entity_config=alexa_conf.get(CONF_ENTITY_CONFIG), ) - kwargs['gactions_should_expose'] = gactions_conf[CONF_FILTER] + cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) success = yield from cloud.initialize() @@ -101,14 +113,14 @@ def async_setup(hass, config): class Cloud: """Store the configuration of the cloud connection.""" - def __init__(self, hass, mode, alexa, gactions_should_expose, + def __init__(self, hass, mode, alexa, google_actions, cognito_client_id=None, user_pool_id=None, region=None, relayer=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode self.alexa_config = alexa - self._gactions_should_expose = gactions_should_expose + self._google_actions = google_actions self._gactions_config = None self.jwt_keyset = None self.id_token = None @@ -161,13 +173,16 @@ class Cloud: def gactions_config(self): """Return the Google Assistant config.""" if self._gactions_config is None: + conf = self._google_actions + def should_expose(entity): """If an entity should be exposed.""" - return self._gactions_should_expose(entity.entity_id) + return conf['filter'](entity.entity_id) self._gactions_config = ga_sh.Config( should_expose=should_expose, - agent_user_id=self.claims['cognito:username'] + agent_user_id=self.claims['cognito:username'], + entity_config=conf.get(CONF_ENTITY_CONFIG), ) return self._gactions_config diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 0f9bd858d7e..aac258b4e93 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -17,6 +17,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant # NOQA from typing import Dict, Any # NOQA +from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.loader import bind_hass @@ -25,10 +26,12 @@ from .const import ( DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, - SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL + SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, + CONF_EXPOSE, CONF_ALIASES ) from .auth import GoogleAssistantAuthView from .http import async_register_http +from .smart_home import MAPPING_COMPONENT _LOGGER = logging.getLogger(__name__) @@ -36,6 +39,13 @@ DEPENDENCIES = ['http'] DEFAULT_AGENT_USER_ID = 'home-assistant' +ENTITY_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.In(MAPPING_COMPONENT), + vol.Optional(CONF_EXPOSE): cv.boolean, + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) +}) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { @@ -48,7 +58,8 @@ CONFIG_SCHEMA = vol.Schema( default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_AGENT_USER_ID, default=DEFAULT_AGENT_USER_ID): cv.string, - vol.Optional(CONF_API_KEY): cv.string + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} } }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index 4ef30ff53c8..1ed27403797 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -6,10 +6,10 @@ import logging # Typing imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports # if False: -from homeassistant.core import HomeAssistant # NOQA from aiohttp.web import Request, Response # NOQA from typing import Dict, Any # NOQA +from homeassistant.core import HomeAssistant # NOQA from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( HTTP_BAD_REQUEST, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index c15f14bccdb..fc250c4b655 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -3,10 +3,8 @@ DOMAIN = 'google_assistant' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' -ATTR_GOOGLE_ASSISTANT = 'google_assistant' -ATTR_GOOGLE_ASSISTANT_NAME = 'google_assistant_name' -ATTR_GOOGLE_ASSISTANT_TYPE = 'google_assistant_type' - +CONF_EXPOSE = 'expose' +CONF_ENTITY_CONFIG = 'entity_config' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_PROJECT_ID = 'project_id' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 93c5b3d4f8e..47bdd0acb68 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -23,8 +23,9 @@ from .const import ( CONF_ACCESS_TOKEN, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - ATTR_GOOGLE_ASSISTANT, - CONF_AGENT_USER_ID + CONF_AGENT_USER_ID, + CONF_ENTITY_CONFIG, + CONF_EXPOSE, ) from .smart_home import async_handle_message, Config @@ -38,6 +39,7 @@ def async_register_http(hass, cfg): expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) agent_user_id = cfg.get(CONF_AGENT_USER_ID) + entity_config = cfg.get(CONF_ENTITY_CONFIG) def is_exposed(entity) -> bool: """Determine if an entity should be exposed to Google Assistant.""" @@ -45,11 +47,11 @@ def async_register_http(hass, cfg): # Ignore entities that are views return False - domain = entity.domain.lower() - explicit_expose = entity.attributes.get(ATTR_GOOGLE_ASSISTANT, None) + explicit_expose = \ + entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = \ - expose_by_default and domain in exposed_domains + expose_by_default and entity.domain in exposed_domains # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being @@ -59,7 +61,7 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - gass_config = Config(is_exposed, agent_user_id) + gass_config = Config(is_exposed, agent_user_id, entity_config) hass.http.register_view( GoogleAssistantView(access_token, gass_config)) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 9ba77434c47..d6d5a4fd877 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,6 +1,5 @@ """Support for Google Assistant Smart Home API.""" import asyncio -from collections import namedtuple import logging # Typing imports @@ -16,9 +15,9 @@ from homeassistant.util.decorator import Registry from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, - CONF_FRIENDLY_NAME, STATE_OFF, - SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS, + CONF_NAME, CONF_TYPE ) from homeassistant.components import ( switch, light, cover, media_player, group, fan, scene, script, climate @@ -26,8 +25,7 @@ from homeassistant.components import ( from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( - ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR, - ATTR_GOOGLE_ASSISTANT_TYPE, + COMMAND_COLOR, COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE, COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE, @@ -69,13 +67,22 @@ MAPPING_COMPONENT = { } # type: Dict[str, list] -Config = namedtuple('GoogleAssistantConfig', 'should_expose,agent_user_id') +class Config: + """Hold the configuration for Google Assistant.""" + + def __init__(self, should_expose, agent_user_id, entity_config=None): + """Initialize the configuration.""" + self.should_expose = should_expose + self.agent_user_id = agent_user_id + self.entity_config = entity_config or {} -def entity_to_device(entity: Entity, units: UnitSystem): +def entity_to_device(entity: Entity, config: Config, units: UnitSystem): """Convert a hass entity into an google actions device.""" + entity_config = config.entity_config.get(entity.entity_id, {}) class_data = MAPPING_COMPONENT.get( - entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain) + entity_config.get(CONF_TYPE) or entity.domain) + if class_data is None: return None @@ -90,17 +97,12 @@ def entity_to_device(entity: Entity, units: UnitSystem): device['traits'].append(class_data[1]) # handle custom names - device['name']['name'] = \ - entity.attributes.get(ATTR_GOOGLE_ASSISTANT_NAME) or \ - entity.attributes.get(CONF_FRIENDLY_NAME) + device['name']['name'] = entity_config.get(CONF_NAME) or entity.name # use aliases - aliases = entity.attributes.get(CONF_ALIASES) + aliases = entity_config.get(CONF_ALIASES) if aliases: - if isinstance(aliases, list): - device['name']['nicknames'] = aliases - else: - _LOGGER.warning("%s must be a list", CONF_ALIASES) + device['name']['nicknames'] = aliases # add trait if entity supports feature if class_data[2]: @@ -322,7 +324,7 @@ def async_devices_sync(hass, config, payload): if not config.should_expose(entity): continue - device = entity_to_device(entity, hass.config.units) + device = entity_to_device(entity, config, hass.config.units) if device is None: _LOGGER.warning("No mapping for %s domain", entity.domain) continue diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index d829134eb21..529559f56af 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -317,6 +317,13 @@ def test_handler_google_actions(hass): 'filter': { 'exclude_entities': 'switch.test2' }, + 'entity_config': { + 'switch.test': { + 'name': 'Config name', + 'type': 'light', + 'aliases': 'Config alias' + } + } } } }) @@ -340,4 +347,6 @@ def test_handler_google_actions(hass): device = devices[0] assert device['id'] == 'switch.test' - assert device['name']['name'] == 'Test switch' + assert device['name']['name'] == 'Config name' + assert device['name']['nicknames'] == ['Config alias'] + assert device['type'] == 'action.devices.types.LIGHT' diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index ff6f53cf1a0..3b9ad7f3ef7 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -36,6 +36,15 @@ def assistant_client(loop, hass, test_client): 'project_id': PROJECT_ID, 'client_id': CLIENT_ID, 'access_token': ACCESS_TOKEN, + 'entity_config': { + 'light.ceiling_lights': { + 'aliases': ['top lights', 'ceiling lights'], + 'name': 'Roof Lights', + }, + 'switch.decorative_lights': { + 'type': 'light' + } + } } })) @@ -88,26 +97,6 @@ def hass_fixture(loop, hass): }] })) - # Kitchen light is explicitly excluded from being exposed - ceiling_lights_entity = hass.states.get('light.ceiling_lights') - attrs = dict(ceiling_lights_entity.attributes) - attrs[ga.const.ATTR_GOOGLE_ASSISTANT_NAME] = "Roof Lights" - attrs[ga.const.CONF_ALIASES] = ['top lights', 'ceiling lights'] - hass.states.async_set( - ceiling_lights_entity.entity_id, - ceiling_lights_entity.state, - attributes=attrs) - - # By setting the google_assistant_type = 'light' - # we can override how a device is reported to GA - switch_light = hass.states.get('switch.decorative_lights') - attrs = dict(switch_light.attributes) - attrs[ga.const.ATTR_GOOGLE_ASSISTANT_TYPE] = "light" - hass.states.async_set( - switch_light.entity_id, - switch_light.state, - attributes=attrs) - return hass