core/homeassistant/components/google_assistant/smart_home.py

210 lines
5.9 KiB
Python
Raw Normal View History

"""Support for Google Assistant Smart Home API."""
import asyncio
from itertools import product
import logging
from homeassistant.util.decorator import Registry
from homeassistant.const import ATTR_ENTITY_ID
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,
)
from .helpers import RequestData, GoogleEntity, async_get_entities
from .error import SmartHomeError
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages."""
request_id: str = message.get("requestId")
data = RequestData(config, user_id, request_id)
response = await _process(hass, data, message)
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"])
return response
async def _process(hass, data, message):
"""Process a message."""
inputs: list = message.get("inputs")
if len(inputs) != 1:
return {
2019-07-31 19:25:30 +00:00
"requestId": data.request_id,
"payload": {"errorCode": ERR_PROTOCOL_ERROR},
}
2019-07-31 19:25:30 +00:00
handler = HANDLERS.get(inputs[0].get("intent"))
if handler is None:
return {
2019-07-31 19:25:30 +00:00
"requestId": data.request_id,
"payload": {"errorCode": ERR_PROTOCOL_ERROR},
}
try:
2019-07-31 19:25:30 +00:00
result = await handler(hass, data, inputs[0].get("payload"))
except SmartHomeError as err:
2019-07-31 19:25:30 +00:00
return {"requestId": data.request_id, "payload": {"errorCode": err.code}}
except Exception: # pylint: disable=broad-except
2019-07-31 19:25:30 +00:00
_LOGGER.exception("Unexpected error")
return {
2019-07-31 19:25:30 +00:00
"requestId": data.request_id,
"payload": {"errorCode": ERR_UNKNOWN_ERROR},
}
if result is None:
return None
2019-07-31 19:25:30 +00:00
return {"requestId": data.request_id, "payload": result}
2019-07-31 19:25:30 +00:00
@HANDLERS.register("action.devices.SYNC")
async def async_devices_sync(hass, data, payload):
"""Handle action.devices.SYNC request.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
hass.bus.async_fire(
2019-07-31 19:25:30 +00:00
EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context
)
2019-07-31 19:25:30 +00:00
devices = await asyncio.gather(
*(
entity.sync_serialize()
for entity in async_get_entities(hass, data.config)
if data.config.should_expose(entity.state)
)
)
response = {
2019-07-31 19:25:30 +00:00
"agentUserId": data.config.agent_user_id or data.context.user_id,
"devices": devices,
}
return response
2019-07-31 19:25:30 +00:00
@HANDLERS.register("action.devices.QUERY")
async def async_devices_query(hass, data, payload):
"""Handle action.devices.QUERY request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
devices = {}
2019-07-31 19:25:30 +00:00
for device in payload.get("devices", []):
devid = device["id"]
state = hass.states.get(devid)
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,
)
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}
continue
entity = GoogleEntity(hass, data.config, state)
devices[devid] = entity.query_serialize()
2019-07-31 19:25:30 +00:00
return {"devices": devices}
2019-07-31 19:25:30 +00:00
@HANDLERS.register("action.devices.EXECUTE")
async def handle_devices_execute(hass, data, payload):
"""Handle action.devices.EXECUTE request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
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"]
hass.bus.async_fire(
EVENT_COMMAND_RECEIVED,
{
2019-07-31 19:25:30 +00:00
"request_id": data.request_id,
ATTR_ENTITY_ID: entity_id,
2019-07-31 19:25:30 +00:00
"execution": execution,
},
2019-07-31 19:25:30 +00:00
context=data.context,
)
# 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,
}
continue
entities[entity_id] = GoogleEntity(hass, data.config, state)
try:
await entities[entity_id].execute(data, execution)
except SmartHomeError as err:
results[entity_id] = {
2019-07-31 19:25:30 +00:00
"ids": [entity_id],
"status": "ERROR",
**err.to_response(),
}
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(),
}
)
2019-07-31 19:25:30 +00:00
return {"commands": final_results}
2019-07-31 19:25:30 +00:00
@HANDLERS.register("action.devices.DISCONNECT")
async def async_devices_disconnect(hass, data, payload):
"""Handle action.devices.DISCONNECT request.
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
"""
return None
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"},
}