core/homeassistant/components/google_assistant/helpers.py

209 lines
6.3 KiB
Python
Raw Normal View History

"""Helper classes for Google Assistant integration."""
from asyncio import gather
from collections.abc import Mapping
from homeassistant.core import Context, callback
from homeassistant.const import (
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES,
ATTR_DEVICE_CLASS
)
from . import trait
from .const import (
DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED,
DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT,
)
from .error import SmartHomeError
class Config:
"""Hold the configuration for Google Assistant."""
def __init__(self, should_expose, allow_unlock,
entity_config=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.entity_config = entity_config or {}
Add support for locks in google assistant component (#18233) * Add support for locks in google assistant component This is supported by the smarthome API, but there is no documentation for it. This work is based on an article I found with screenshots of documentation that was erroneously uploaded: https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/ Google Assistant now supports unlocking certain locks - Nest and August come to mind - via this API, and this commit allows Home Assistant to do so as well. Notably, I've added a config option `allow_unlock` that controls whether we actually honor requests to unlock a lock via the google assistant. It defaults to false. Additionally, we add the functionNotSupported error, which makes a little more sense when we're unable to execute the desired state transition. https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list * Fix linter warnings * Ensure that certain groups are never exposed to cloud entities For example, the group.all_locks entity - we should probably never expose this to third party cloud integrations. It's risky. This is not configurable, but can be extended by adding to the cloud.const.NEVER_EXPOSED_ENTITIES array. It's implemented in a modestly hacky fashion, because we determine whether or not a entity should be excluded/included in several ways. Notably, we define this array in the top level const.py, to avoid circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
self.allow_unlock = allow_unlock
class RequestData:
"""Hold data associated with a particular request."""
def __init__(self, config, user_id, request_id):
"""Initialize the request data."""
self.config = config
self.request_id = request_id
self.context = Context(user_id=user_id)
def get_google_type(domain, device_class):
"""Google type based on domain and device class."""
typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class))
return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES[domain]
class GoogleEntity:
"""Adaptation of Entity expressed in Google's terms."""
def __init__(self, hass, config, state):
"""Initialize a Google entity."""
self.hass = hass
self.config = config
self.state = state
self._traits = None
@property
def entity_id(self):
"""Return entity ID."""
return self.state.entity_id
@callback
def traits(self):
"""Return traits for entity."""
if self._traits is not None:
return self._traits
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._traits = [Trait(self.hass, state, self.config)
for Trait in trait.TRAITS
if Trait.supported(domain, features, device_class)]
return self._traits
async def sync_serialize(self):
"""Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
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)
device = {
'id': state.entity_id,
'name': {
'name': name
},
'attributes': {},
'traits': [trait.name for trait in traits],
'willReportState': False,
'type': device_type,
}
# use aliases
aliases = entity_config.get(CONF_ALIASES)
if aliases:
device['name']['nicknames'] = aliases
for trt in traits:
device['attributes'].update(trt.sync_attributes())
room = entity_config.get(CONF_ROOM_HINT)
if room:
device['roomHint'] = room
return device
dev_reg, ent_reg, area_reg = await gather(
self.hass.helpers.device_registry.async_get_registry(),
self.hass.helpers.entity_registry.async_get_registry(),
self.hass.helpers.area_registry.async_get_registry(),
)
entity_entry = ent_reg.async_get(state.entity_id)
if not (entity_entry and entity_entry.device_id):
return device
device_entry = dev_reg.devices.get(entity_entry.device_id)
if not (device_entry and device_entry.area_id):
return device
area_entry = area_reg.areas.get(device_entry.area_id)
if area_entry and area_entry.name:
device['roomHint'] = area_entry.name
return device
@callback
def query_serialize(self):
"""Serialize entity for a QUERY response.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
state = self.state
if state.state == STATE_UNAVAILABLE:
return {'online': False}
attrs = {'online': True}
for trt in self.traits():
deep_update(attrs, trt.query_attributes())
return attrs
async def execute(self, command, data, params):
"""Execute a command.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
executed = False
for trt in self.traits():
if trt.can_execute(command, params):
await trt.execute(command, data, params)
executed = True
break
if not executed:
raise SmartHomeError(
ERR_FUNCTION_NOT_SUPPORTED,
'Unable to execute {} for {}'.format(command,
self.state.entity_id))
@callback
def async_update(self):
"""Update the entity with latest info from Home Assistant."""
self.state = self.hass.states.get(self.entity_id)
if self._traits is None:
return
for trt in self._traits:
trt.state = self.state
def deep_update(target, source):
"""Update a nested dictionary with another nested dictionary."""
for key, value in source.items():
if isinstance(value, Mapping):
target[key] = deep_update(target.get(key, {}), value)
else:
target[key] = value
return target