2017-10-18 05:00:59 +00:00
|
|
|
"""Support for Google Assistant Smart Home API."""
|
2019-05-29 15:39:12 +00:00
|
|
|
import asyncio
|
2018-03-08 22:39:10 +00:00
|
|
|
from itertools import product
|
2017-10-18 05:00:59 +00:00
|
|
|
import logging
|
|
|
|
|
2017-12-31 23:04:49 +00:00
|
|
|
from homeassistant.util.decorator import Registry
|
2017-10-18 05:00:59 +00:00
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
from homeassistant.const import ATTR_ENTITY_ID, __version__
|
2017-10-18 05:00:59 +00:00
|
|
|
|
|
|
|
from .const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ERR_PROTOCOL_ERROR,
|
|
|
|
ERR_DEVICE_OFFLINE,
|
|
|
|
ERR_UNKNOWN_ERROR,
|
|
|
|
EVENT_COMMAND_RECEIVED,
|
|
|
|
EVENT_SYNC_RECEIVED,
|
|
|
|
EVENT_QUERY_RECEIVED,
|
2017-10-18 05:00:59 +00:00
|
|
|
)
|
2019-05-29 15:39:12 +00:00
|
|
|
from .helpers import RequestData, GoogleEntity, async_get_entities
|
2019-04-18 05:37:39 +00:00
|
|
|
from .error import SmartHomeError
|
2017-10-18 05:00:59 +00:00
|
|
|
|
2017-12-31 23:04:49 +00:00
|
|
|
HANDLERS = Registry()
|
2017-10-18 05:00:59 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-01-30 09:19:24 +00:00
|
|
|
|
2019-03-06 04:00:53 +00:00
|
|
|
async def async_handle_message(hass, config, user_id, message):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Handle incoming API messages."""
|
2019-10-13 21:16:27 +00:00
|
|
|
data = RequestData(config, user_id, message["requestId"], message.get("devices"))
|
2019-03-06 04:00:53 +00:00
|
|
|
|
|
|
|
response = await _process(hass, data, message)
|
2018-01-30 09:19:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if response and "errorCode" in response["payload"]:
|
|
|
|
_LOGGER.error("Error handling message %s: %s", message, response["payload"])
|
2017-11-16 07:00:43 +00:00
|
|
|
|
2018-01-30 09:19:24 +00:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
2019-03-06 04:00:53 +00:00
|
|
|
async def _process(hass, data, message):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Process a message."""
|
2019-09-07 06:48:58 +00:00
|
|
|
inputs: list = message.get("inputs")
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
if len(inputs) != 1:
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"requestId": data.request_id,
|
|
|
|
"payload": {"errorCode": ERR_PROTOCOL_ERROR},
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
handler = HANDLERS.get(inputs[0].get("intent"))
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
if handler is None:
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"requestId": data.request_id,
|
|
|
|
"payload": {"errorCode": ERR_PROTOCOL_ERROR},
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2018-03-08 22:39:10 +00:00
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
result = await handler(hass, data, inputs[0].get("payload"))
|
2018-03-08 22:39:10 +00:00
|
|
|
except SmartHomeError as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"requestId": data.request_id, "payload": {"errorCode": err.code}}
|
2018-10-25 20:15:20 +00:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.exception("Unexpected error")
|
2018-03-08 22:39:10 +00:00
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"requestId": data.request_id,
|
|
|
|
"payload": {"errorCode": ERR_UNKNOWN_ERROR},
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-02-25 18:35:03 +00:00
|
|
|
if result is None:
|
|
|
|
return None
|
2019-10-13 21:16:27 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"requestId": data.request_id, "payload": result}
|
2019-02-25 18:35:03 +00:00
|
|
|
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@HANDLERS.register("action.devices.SYNC")
|
2019-03-06 04:00:53 +00:00
|
|
|
async def async_devices_sync(hass, data, payload):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Handle action.devices.SYNC request.
|
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC
|
2018-03-08 22:39:10 +00:00
|
|
|
"""
|
2019-03-06 04:00:53 +00:00
|
|
|
hass.bus.async_fire(
|
2019-07-31 19:25:30 +00:00
|
|
|
EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context
|
|
|
|
)
|
2019-02-27 19:33:34 +00:00
|
|
|
|
2019-12-03 06:05:59 +00:00
|
|
|
agent_user_id = data.context.user_id
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
devices = await asyncio.gather(
|
|
|
|
*(
|
2019-12-03 06:05:59 +00:00
|
|
|
entity.sync_serialize(agent_user_id)
|
2019-07-31 19:25:30 +00:00
|
|
|
for entity in async_get_entities(hass, data.config)
|
2019-10-13 21:16:27 +00:00
|
|
|
if entity.should_expose()
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
)
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-12-03 06:05:59 +00:00
|
|
|
response = {"agentUserId": agent_user_id, "devices": devices}
|
|
|
|
|
|
|
|
await data.config.async_connect_agent_user(agent_user_id)
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-02-27 19:33:34 +00:00
|
|
|
return response
|
|
|
|
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@HANDLERS.register("action.devices.QUERY")
|
2019-03-06 04:00:53 +00:00
|
|
|
async def async_devices_query(hass, data, payload):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Handle action.devices.QUERY request.
|
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY
|
2018-03-08 22:39:10 +00:00
|
|
|
"""
|
2017-12-31 23:04:49 +00:00
|
|
|
devices = {}
|
2019-07-31 19:25:30 +00:00
|
|
|
for device in payload.get("devices", []):
|
|
|
|
devid = device["id"]
|
2017-12-31 23:04:49 +00:00
|
|
|
state = hass.states.get(devid)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2019-03-06 04:00:53 +00:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_QUERY_RECEIVED,
|
2019-07-31 19:25:30 +00:00
|
|
|
{"request_id": data.request_id, ATTR_ENTITY_ID: devid},
|
|
|
|
context=data.context,
|
|
|
|
)
|
2019-02-27 19:33:34 +00:00
|
|
|
|
2017-12-31 23:04:49 +00:00
|
|
|
if not state:
|
|
|
|
# If we can't find a state, the device is offline
|
2019-07-31 19:25:30 +00:00
|
|
|
devices[devid] = {"online": False}
|
2018-03-08 22:39:10 +00:00
|
|
|
continue
|
|
|
|
|
2019-04-18 05:37:39 +00:00
|
|
|
entity = GoogleEntity(hass, data.config, state)
|
2019-03-06 04:00:53 +00:00
|
|
|
devices[devid] = entity.query_serialize()
|
2017-12-31 23:04:49 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"devices": devices}
|
2017-12-31 23:04:49 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@HANDLERS.register("action.devices.EXECUTE")
|
2019-03-06 04:00:53 +00:00
|
|
|
async def handle_devices_execute(hass, data, payload):
|
2018-03-08 22:39:10 +00:00
|
|
|
"""Handle action.devices.EXECUTE request.
|
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE
|
2018-03-08 22:39:10 +00:00
|
|
|
"""
|
|
|
|
entities = {}
|
|
|
|
results = {}
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
for command in payload["commands"]:
|
|
|
|
for device, execution in product(command["devices"], command["execution"]):
|
|
|
|
entity_id = device["id"]
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2019-03-06 04:00:53 +00:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_COMMAND_RECEIVED,
|
|
|
|
{
|
2019-07-31 19:25:30 +00:00
|
|
|
"request_id": data.request_id,
|
2019-03-06 04:00:53 +00:00
|
|
|
ATTR_ENTITY_ID: entity_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
"execution": execution,
|
2019-03-06 04:00:53 +00:00
|
|
|
},
|
2019-07-31 19:25:30 +00:00
|
|
|
context=data.context,
|
|
|
|
)
|
2019-02-27 19:33:34 +00:00
|
|
|
|
2018-03-08 22:39:10 +00: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] = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"ids": [entity_id],
|
|
|
|
"status": "ERROR",
|
|
|
|
"errorCode": ERR_DEVICE_OFFLINE,
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
|
|
|
continue
|
|
|
|
|
2019-04-18 05:37:39 +00:00
|
|
|
entities[entity_id] = GoogleEntity(hass, data.config, state)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
|
|
|
try:
|
2019-04-19 21:50:21 +00:00
|
|
|
await entities[entity_id].execute(data, execution)
|
2018-03-08 22:39:10 +00:00
|
|
|
except SmartHomeError as err:
|
|
|
|
results[entity_id] = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"ids": [entity_id],
|
|
|
|
"status": "ERROR",
|
|
|
|
**err.to_response(),
|
2018-03-08 22:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
final_results = list(results.values())
|
|
|
|
|
|
|
|
for entity in entities.values():
|
|
|
|
if entity.entity_id in results:
|
|
|
|
continue
|
|
|
|
|
|
|
|
entity.async_update()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
final_results.append(
|
|
|
|
{
|
|
|
|
"ids": [entity.entity_id],
|
|
|
|
"status": "SUCCESS",
|
|
|
|
"states": entity.query_serialize(),
|
|
|
|
}
|
|
|
|
)
|
2018-03-08 22:39:10 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"commands": final_results}
|
2018-09-20 21:46:51 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@HANDLERS.register("action.devices.DISCONNECT")
|
2019-10-03 11:02:38 +00:00
|
|
|
async def async_devices_disconnect(hass, data: RequestData, payload):
|
2019-02-25 18:35:03 +00:00
|
|
|
"""Handle action.devices.DISCONNECT request.
|
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT
|
2019-02-25 18:35:03 +00:00
|
|
|
"""
|
2019-12-03 06:05:59 +00:00
|
|
|
await data.config.async_disconnect_agent_user(data.context.user_id)
|
2019-02-25 18:35:03 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2019-10-13 21:16:27 +00:00
|
|
|
@HANDLERS.register("action.devices.IDENTIFY")
|
|
|
|
async def async_devices_identify(hass, data: RequestData, payload):
|
|
|
|
"""Handle action.devices.IDENTIFY request.
|
|
|
|
|
|
|
|
https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
"device": {
|
2019-12-03 06:05:59 +00:00
|
|
|
"id": data.context.user_id,
|
2019-10-13 21:16:27 +00:00
|
|
|
"isLocalOnly": True,
|
|
|
|
"isProxy": True,
|
|
|
|
"deviceInfo": {
|
|
|
|
"hwVersion": "UNKNOWN_HW_VERSION",
|
|
|
|
"manufacturer": "Home Assistant",
|
|
|
|
"model": "Home Assistant",
|
|
|
|
"swVersion": __version__,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@HANDLERS.register("action.devices.REACHABLE_DEVICES")
|
|
|
|
async def async_devices_reachable(hass, data: RequestData, payload):
|
|
|
|
"""Handle action.devices.REACHABLE_DEVICES request.
|
|
|
|
|
|
|
|
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
|
|
|
|
"""
|
|
|
|
google_ids = set(dev["id"] for dev in (data.devices or []))
|
|
|
|
|
|
|
|
return {
|
|
|
|
"devices": [
|
|
|
|
entity.reachable_device_serialize()
|
|
|
|
for entity in async_get_entities(hass, data.config)
|
|
|
|
if entity.entity_id in google_ids and entity.should_expose()
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-09-20 21:46:51 +00:00
|
|
|
def turned_off_response(message):
|
|
|
|
"""Return a device turned off response."""
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"requestId": message.get("requestId"),
|
|
|
|
"payload": {"errorCode": "deviceTurnedOff"},
|
2018-09-20 21:46:51 +00:00
|
|
|
}
|