2017-10-17 22:00:59 -07:00
|
|
|
"""Support for Google Assistant Smart Home API."""
|
2018-03-08 14:39:10 -08:00
|
|
|
from itertools import product
|
2017-10-17 22:00:59 -07:00
|
|
|
import logging
|
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
from homeassistant.util.decorator import Registry
|
2017-10-17 22:00:59 -07:00
|
|
|
|
|
|
|
from homeassistant.const import (
|
2019-04-18 07:37:39 +02:00
|
|
|
CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID)
|
2017-10-17 22:00:59 -07:00
|
|
|
|
|
|
|
from .const import (
|
2019-04-18 07:37:39 +02:00
|
|
|
ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR,
|
2019-02-28 03:33:34 +08:00
|
|
|
EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
|
2017-10-17 22:00:59 -07:00
|
|
|
)
|
2019-04-18 07:37:39 +02:00
|
|
|
from .helpers import RequestData, GoogleEntity
|
|
|
|
from .error import SmartHomeError
|
2017-10-17 22:00:59 -07:00
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
HANDLERS = Registry()
|
2017-10-17 22:00:59 -07:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-01-30 01:19:24 -08:00
|
|
|
|
2019-03-06 12:00:53 +08:00
|
|
|
async def async_handle_message(hass, config, user_id, message):
|
2018-03-08 14:39:10 -08:00
|
|
|
"""Handle incoming API messages."""
|
2019-03-06 12:00:53 +08:00
|
|
|
request_id = message.get('requestId') # type: str
|
|
|
|
|
|
|
|
data = RequestData(config, user_id, request_id)
|
|
|
|
|
|
|
|
response = await _process(hass, data, message)
|
2018-01-30 01:19:24 -08:00
|
|
|
|
2019-02-25 10:35:03 -08:00
|
|
|
if response and 'errorCode' in response['payload']:
|
2018-03-08 14:39:10 -08:00
|
|
|
_LOGGER.error('Error handling message %s: %s',
|
|
|
|
message, response['payload'])
|
2017-11-16 08:00:43 +01:00
|
|
|
|
2018-01-30 01:19:24 -08:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
2019-03-06 12:00:53 +08:00
|
|
|
async def _process(hass, data, message):
|
2018-03-08 14:39:10 -08:00
|
|
|
"""Process a message."""
|
2017-12-31 15:04:49 -08:00
|
|
|
inputs = message.get('inputs') # type: list
|
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
if len(inputs) != 1:
|
|
|
|
return {
|
2019-03-06 12:00:53 +08:00
|
|
|
'requestId': data.request_id,
|
2018-03-08 14:39:10 -08:00
|
|
|
'payload': {'errorCode': ERR_PROTOCOL_ERROR}
|
|
|
|
}
|
2017-12-31 15:04:49 -08:00
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
handler = HANDLERS.get(inputs[0].get('intent'))
|
2017-12-31 15:04:49 -08:00
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
if handler is None:
|
|
|
|
return {
|
2019-03-06 12:00:53 +08:00
|
|
|
'requestId': data.request_id,
|
2018-03-08 14:39:10 -08:00
|
|
|
'payload': {'errorCode': ERR_PROTOCOL_ERROR}
|
|
|
|
}
|
2017-12-31 15:04:49 -08:00
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
try:
|
2019-03-06 12:00:53 +08:00
|
|
|
result = await handler(hass, data, inputs[0].get('payload'))
|
2018-03-08 14:39:10 -08:00
|
|
|
except SmartHomeError as err:
|
|
|
|
return {
|
2019-03-06 12:00:53 +08:00
|
|
|
'requestId': data.request_id,
|
2018-03-08 14:39:10 -08:00
|
|
|
'payload': {'errorCode': err.code}
|
|
|
|
}
|
2018-10-25 23:15:20 +03:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2018-03-08 14:39:10 -08:00
|
|
|
_LOGGER.exception('Unexpected error')
|
|
|
|
return {
|
2019-03-06 12:00:53 +08:00
|
|
|
'requestId': data.request_id,
|
2018-03-08 14:39:10 -08:00
|
|
|
'payload': {'errorCode': ERR_UNKNOWN_ERROR}
|
|
|
|
}
|
2017-12-31 15:04:49 -08:00
|
|
|
|
2019-02-25 10:35:03 -08:00
|
|
|
if result is None:
|
|
|
|
return None
|
2019-03-06 12:00:53 +08:00
|
|
|
return {'requestId': data.request_id, 'payload': result}
|
2019-02-25 10:35:03 -08:00
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
|
|
|
|
@HANDLERS.register('action.devices.SYNC')
|
2019-03-06 12:00:53 +08:00
|
|
|
async def async_devices_sync(hass, data, payload):
|
2018-03-08 14:39:10 -08:00
|
|
|
"""Handle action.devices.SYNC request.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
|
|
|
|
"""
|
2019-03-06 12:00:53 +08:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_SYNC_RECEIVED,
|
|
|
|
{'request_id': data.request_id},
|
|
|
|
context=data.context)
|
2019-02-28 03:33:34 +08:00
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
devices = []
|
2018-03-08 14:39:10 -08:00
|
|
|
for state in hass.states.async_all():
|
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 03:39:10 -06:00
|
|
|
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
|
|
|
continue
|
|
|
|
|
2019-03-06 12:00:53 +08:00
|
|
|
if not data.config.should_expose(state):
|
2017-12-31 15:04:49 -08:00
|
|
|
continue
|
|
|
|
|
2019-04-18 07:37:39 +02:00
|
|
|
entity = GoogleEntity(hass, data.config, state)
|
2019-03-02 15:31:57 +08:00
|
|
|
serialized = await entity.sync_serialize()
|
2018-03-08 14:39:10 -08:00
|
|
|
|
|
|
|
if serialized is None:
|
|
|
|
_LOGGER.debug("No mapping for %s domain", entity.state)
|
2017-12-31 15:04:49 -08:00
|
|
|
continue
|
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
devices.append(serialized)
|
2017-12-31 15:04:49 -08:00
|
|
|
|
2019-02-28 03:33:34 +08:00
|
|
|
response = {
|
2019-03-06 12:00:53 +08:00
|
|
|
'agentUserId': data.context.user_id,
|
2017-12-31 15:04:49 -08:00
|
|
|
'devices': devices,
|
|
|
|
}
|
|
|
|
|
2019-02-28 03:33:34 +08:00
|
|
|
return response
|
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
|
|
|
|
@HANDLERS.register('action.devices.QUERY')
|
2019-03-06 12:00:53 +08:00
|
|
|
async def async_devices_query(hass, data, payload):
|
2018-03-08 14:39:10 -08:00
|
|
|
"""Handle action.devices.QUERY request.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
|
|
|
|
"""
|
2017-12-31 15:04:49 -08:00
|
|
|
devices = {}
|
|
|
|
for device in payload.get('devices', []):
|
2018-03-08 14:39:10 -08:00
|
|
|
devid = device['id']
|
2017-12-31 15:04:49 -08:00
|
|
|
state = hass.states.get(devid)
|
2018-03-08 14:39:10 -08:00
|
|
|
|
2019-03-06 12:00:53 +08:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_QUERY_RECEIVED,
|
|
|
|
{
|
|
|
|
'request_id': data.request_id,
|
|
|
|
ATTR_ENTITY_ID: devid,
|
|
|
|
},
|
|
|
|
context=data.context)
|
2019-02-28 03:33:34 +08:00
|
|
|
|
2017-12-31 15:04:49 -08:00
|
|
|
if not state:
|
|
|
|
# If we can't find a state, the device is offline
|
|
|
|
devices[devid] = {'online': False}
|
2018-03-08 14:39:10 -08:00
|
|
|
continue
|
|
|
|
|
2019-04-18 07:37:39 +02:00
|
|
|
entity = GoogleEntity(hass, data.config, state)
|
2019-03-06 12:00:53 +08:00
|
|
|
devices[devid] = entity.query_serialize()
|
2017-12-31 15:04:49 -08:00
|
|
|
|
|
|
|
return {'devices': devices}
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register('action.devices.EXECUTE')
|
2019-03-06 12:00:53 +08:00
|
|
|
async def handle_devices_execute(hass, data, payload):
|
2018-03-08 14:39:10 -08:00
|
|
|
"""Handle action.devices.EXECUTE request.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
|
|
|
|
"""
|
|
|
|
entities = {}
|
|
|
|
results = {}
|
|
|
|
|
|
|
|
for command in payload['commands']:
|
|
|
|
for device, execution in product(command['devices'],
|
|
|
|
command['execution']):
|
|
|
|
entity_id = device['id']
|
|
|
|
|
2019-03-06 12:00:53 +08:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_COMMAND_RECEIVED,
|
|
|
|
{
|
|
|
|
'request_id': data.request_id,
|
|
|
|
ATTR_ENTITY_ID: entity_id,
|
|
|
|
'execution': execution
|
|
|
|
},
|
|
|
|
context=data.context)
|
2019-02-28 03:33:34 +08:00
|
|
|
|
2018-03-08 14:39:10 -08:00
|
|
|
# Happens if error occurred. Skip entity for further processing
|
|
|
|
if entity_id in results:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if entity_id not in entities:
|
|
|
|
state = hass.states.get(entity_id)
|
|
|
|
|
|
|
|
if state is None:
|
|
|
|
results[entity_id] = {
|
|
|
|
'ids': [entity_id],
|
|
|
|
'status': 'ERROR',
|
|
|
|
'errorCode': ERR_DEVICE_OFFLINE
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
|
2019-04-18 07:37:39 +02:00
|
|
|
entities[entity_id] = GoogleEntity(hass, data.config, state)
|
2018-03-08 14:39:10 -08:00
|
|
|
|
|
|
|
try:
|
|
|
|
await entities[entity_id].execute(execution['command'],
|
2019-03-06 12:00:53 +08:00
|
|
|
data,
|
2018-03-08 14:39:10 -08:00
|
|
|
execution.get('params', {}))
|
|
|
|
except SmartHomeError as err:
|
|
|
|
results[entity_id] = {
|
|
|
|
'ids': [entity_id],
|
|
|
|
'status': 'ERROR',
|
|
|
|
'errorCode': err.code
|
|
|
|
}
|
|
|
|
|
|
|
|
final_results = list(results.values())
|
|
|
|
|
|
|
|
for entity in entities.values():
|
|
|
|
if entity.entity_id in results:
|
|
|
|
continue
|
|
|
|
|
|
|
|
entity.async_update()
|
|
|
|
|
|
|
|
final_results.append({
|
|
|
|
'ids': [entity.entity_id],
|
|
|
|
'status': 'SUCCESS',
|
|
|
|
'states': entity.query_serialize(),
|
|
|
|
})
|
|
|
|
|
|
|
|
return {'commands': final_results}
|
2018-09-20 23:46:51 +02:00
|
|
|
|
|
|
|
|
2019-02-25 10:35:03 -08:00
|
|
|
@HANDLERS.register('action.devices.DISCONNECT')
|
2019-03-06 12:00:53 +08:00
|
|
|
async def async_devices_disconnect(hass, data, payload):
|
2019-02-25 10:35:03 -08:00
|
|
|
"""Handle action.devices.DISCONNECT request.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
|
|
|
|
"""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2018-09-20 23:46:51 +02:00
|
|
|
def turned_off_response(message):
|
|
|
|
"""Return a device turned off response."""
|
|
|
|
return {
|
|
|
|
'requestId': message.get('requestId'),
|
|
|
|
'payload': {'errorCode': 'deviceTurnedOff'}
|
|
|
|
}
|