Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
RequestDataValidator)
from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as google_sh
from homeassistant.components.google_assistant import (
const as google_const)

from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
Expand Down Expand Up @@ -415,7 +416,7 @@ 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_sh.DOMAIN_TO_GOOGLE_TYPES),
'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,
Expand Down
46 changes: 46 additions & 0 deletions homeassistant/components/google_assistant/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
"""Constants for Google Assistant."""
from homeassistant.components import (
binary_sensor,
camera,
climate,
cover,
fan,
group,
input_boolean,
light,
lock,
media_player,
scene,
script,
switch,
vacuum,
)
DOMAIN = 'google_assistant'

GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant'
Expand Down Expand Up @@ -32,6 +48,7 @@
TYPE_BLINDS = PREFIX_TYPES + 'BLINDS'
TYPE_GARAGE = PREFIX_TYPES + 'GARAGE'
TYPE_OUTLET = PREFIX_TYPES + 'OUTLET'
TYPE_SENSOR = PREFIX_TYPES + 'SENSOR'

SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
Expand All @@ -51,3 +68,32 @@
EVENT_COMMAND_RECEIVED = 'google_assistant_command'
EVENT_QUERY_RECEIVED = 'google_assistant_query'
EVENT_SYNC_RECEIVED = 'google_assistant_sync'

DOMAIN_TO_GOOGLE_TYPES = {
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
cover.DOMAIN: TYPE_BLINDS,
fan.DOMAIN: TYPE_FAN,
group.DOMAIN: TYPE_SWITCH,
input_boolean.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
lock.DOMAIN: TYPE_LOCK,
media_player.DOMAIN: TYPE_SWITCH,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
switch.DOMAIN: TYPE_SWITCH,
vacuum.DOMAIN: TYPE_VACUUM,
}

DEVICE_CLASS_TO_GOOGLE_TYPES = {
(cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE,
(switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH,
(switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR):
TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,

}
13 changes: 13 additions & 0 deletions homeassistant/components/google_assistant/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Errors for Google Assistant."""


class SmartHomeError(Exception):
"""Google Assistant Smart Home errors.

https://developers.google.com/actions/smarthome/create-app#error_responses
"""

def __init__(self, code, msg):
"""Log error code."""
super().__init__(msg)
self.code = code
195 changes: 184 additions & 11 deletions homeassistant/components/google_assistant/helpers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""Helper classes for Google Assistant integration."""
from homeassistant.core import Context
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
)

class SmartHomeError(Exception):
"""Google Assistant Smart Home errors.

https://developers.google.com/actions/smarthome/create-app#error_responses
"""

def __init__(self, code, msg):
"""Log error code."""
super().__init__(msg)
self.code = code
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:
Expand All @@ -33,3 +35,174 @@ def __init__(self, config, user_id, request_id):
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
Loading