Move Google Assistant entity config out of customize (#11499)

* Move Google Assistant entity config out of customize

* CONF_ALIAS -> CONF_ALIASES

* Lint
This commit is contained in:
Paulus Schoutsen 2018-01-09 15:14:56 -08:00 committed by GitHub
parent 13042d5557
commit 8313225b40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 86 additions and 60 deletions

View file

@ -10,7 +10,7 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( 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 entityfilter
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -31,6 +31,7 @@ CONF_FILTER = 'filter'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id' CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
CONF_ALIASES = 'aliases'
MODE_DEV = 'development' MODE_DEV = 'development'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
@ -44,6 +45,12 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(alexa_sh.CONF_NAME): cv.string, 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({ ASSISTANT_SCHEMA = vol.Schema({
vol.Optional( vol.Optional(
CONF_FILTER, CONF_FILTER,
@ -55,6 +62,10 @@ ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA} 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
@ -65,7 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): ASSISTANT_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -79,14 +90,15 @@ def async_setup(hass, config):
kwargs = {CONF_MODE: DEFAULT_MODE} kwargs = {CONF_MODE: DEFAULT_MODE}
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({}) 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( kwargs[CONF_ALEXA] = alexa_sh.Config(
should_expose=alexa_conf[CONF_FILTER], should_expose=alexa_conf[CONF_FILTER],
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG), entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
) )
kwargs['gactions_should_expose'] = gactions_conf[CONF_FILTER]
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
success = yield from cloud.initialize() success = yield from cloud.initialize()
@ -101,14 +113,14 @@ def async_setup(hass, config):
class Cloud: class Cloud:
"""Store the configuration of the cloud connection.""" """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, cognito_client_id=None, user_pool_id=None, region=None,
relayer=None): relayer=None):
"""Create an instance of Cloud.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
self.alexa_config = alexa self.alexa_config = alexa
self._gactions_should_expose = gactions_should_expose self._google_actions = google_actions
self._gactions_config = None self._gactions_config = None
self.jwt_keyset = None self.jwt_keyset = None
self.id_token = None self.id_token = None
@ -161,13 +173,16 @@ class Cloud:
def gactions_config(self): def gactions_config(self):
"""Return the Google Assistant config.""" """Return the Google Assistant config."""
if self._gactions_config is None: if self._gactions_config is None:
conf = self._google_actions
def should_expose(entity): def should_expose(entity):
"""If an entity should be exposed.""" """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( self._gactions_config = ga_sh.Config(
should_expose=should_expose, 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 return self._gactions_config

View file

@ -17,6 +17,7 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # 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 import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
@ -25,10 +26,12 @@ from .const import (
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, 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 .auth import GoogleAssistantAuthView
from .http import async_register_http from .http import async_register_http
from .smart_home import MAPPING_COMPONENT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,6 +39,13 @@ DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant' 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( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
@ -48,7 +58,8 @@ CONFIG_SCHEMA = vol.Schema(
default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list,
vol.Optional(CONF_AGENT_USER_ID, vol.Optional(CONF_AGENT_USER_ID,
default=DEFAULT_AGENT_USER_ID): cv.string, 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) extra=vol.ALLOW_EXTRA)

View file

@ -6,10 +6,10 @@ import logging
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False: # if False:
from homeassistant.core import HomeAssistant # NOQA
from aiohttp.web import Request, Response # NOQA from aiohttp.web import Request, Response # NOQA
from typing import Dict, Any # NOQA from typing import Dict, Any # NOQA
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
HTTP_BAD_REQUEST, HTTP_BAD_REQUEST,

View file

@ -3,10 +3,8 @@ DOMAIN = 'google_assistant'
GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant'
ATTR_GOOGLE_ASSISTANT = 'google_assistant' CONF_EXPOSE = 'expose'
ATTR_GOOGLE_ASSISTANT_NAME = 'google_assistant_name' CONF_ENTITY_CONFIG = 'entity_config'
ATTR_GOOGLE_ASSISTANT_TYPE = 'google_assistant_type'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_EXPOSED_DOMAINS = 'exposed_domains'
CONF_PROJECT_ID = 'project_id' CONF_PROJECT_ID = 'project_id'

View file

@ -23,8 +23,9 @@ from .const import (
CONF_ACCESS_TOKEN, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS, 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 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) expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT)
exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS)
agent_user_id = cfg.get(CONF_AGENT_USER_ID) agent_user_id = cfg.get(CONF_AGENT_USER_ID)
entity_config = cfg.get(CONF_ENTITY_CONFIG)
def is_exposed(entity) -> bool: def is_exposed(entity) -> bool:
"""Determine if an entity should be exposed to Google Assistant.""" """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 # Ignore entities that are views
return False return False
domain = entity.domain.lower() explicit_expose = \
explicit_expose = entity.attributes.get(ATTR_GOOGLE_ASSISTANT, None) entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE)
domain_exposed_by_default = \ 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 # Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being # 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 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( hass.http.register_view(
GoogleAssistantView(access_token, gass_config)) GoogleAssistantView(access_token, gass_config))

View file

@ -1,6 +1,5 @@
"""Support for Google Assistant Smart Home API.""" """Support for Google Assistant Smart Home API."""
import asyncio import asyncio
from collections import namedtuple
import logging import logging
# Typing imports # Typing imports
@ -16,9 +15,9 @@ from homeassistant.util.decorator import Registry
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
CONF_FRIENDLY_NAME, STATE_OFF, STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_TURN_OFF, SERVICE_TURN_ON,
TEMP_FAHRENHEIT, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_NAME, CONF_TYPE
) )
from homeassistant.components import ( from homeassistant.components import (
switch, light, cover, media_player, group, fan, scene, script, climate 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 homeassistant.util.unit_system import METRIC_SYSTEM
from .const import ( from .const import (
ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR, COMMAND_COLOR,
ATTR_GOOGLE_ASSISTANT_TYPE,
COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE, COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE,
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE, COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE,
@ -69,13 +67,22 @@ MAPPING_COMPONENT = {
} # type: Dict[str, list] } # 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.""" """Convert a hass entity into an google actions device."""
entity_config = config.entity_config.get(entity.entity_id, {})
class_data = MAPPING_COMPONENT.get( 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: if class_data is None:
return None return None
@ -90,17 +97,12 @@ def entity_to_device(entity: Entity, units: UnitSystem):
device['traits'].append(class_data[1]) device['traits'].append(class_data[1])
# handle custom names # handle custom names
device['name']['name'] = \ device['name']['name'] = entity_config.get(CONF_NAME) or entity.name
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_NAME) or \
entity.attributes.get(CONF_FRIENDLY_NAME)
# use aliases # use aliases
aliases = entity.attributes.get(CONF_ALIASES) aliases = entity_config.get(CONF_ALIASES)
if aliases: if aliases:
if isinstance(aliases, list):
device['name']['nicknames'] = aliases device['name']['nicknames'] = aliases
else:
_LOGGER.warning("%s must be a list", CONF_ALIASES)
# add trait if entity supports feature # add trait if entity supports feature
if class_data[2]: if class_data[2]:
@ -322,7 +324,7 @@ def async_devices_sync(hass, config, payload):
if not config.should_expose(entity): if not config.should_expose(entity):
continue continue
device = entity_to_device(entity, hass.config.units) device = entity_to_device(entity, config, hass.config.units)
if device is None: if device is None:
_LOGGER.warning("No mapping for %s domain", entity.domain) _LOGGER.warning("No mapping for %s domain", entity.domain)
continue continue

View file

@ -317,6 +317,13 @@ def test_handler_google_actions(hass):
'filter': { 'filter': {
'exclude_entities': 'switch.test2' '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] device = devices[0]
assert device['id'] == 'switch.test' 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'

View file

@ -36,6 +36,15 @@ def assistant_client(loop, hass, test_client):
'project_id': PROJECT_ID, 'project_id': PROJECT_ID,
'client_id': CLIENT_ID, 'client_id': CLIENT_ID,
'access_token': ACCESS_TOKEN, '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 return hass