core/homeassistant/components/api/__init__.py

421 lines
12 KiB
Python
Raw Normal View History

"""Rest API for Home Assistant."""
import asyncio
2016-02-19 05:27:50 +00:00
import json
2015-01-30 16:26:06 +00:00
import logging
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadRequest
import async_timeout
import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.bootstrap import DATA_LOGGING
2018-05-27 18:16:30 +00:00
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
EVENT_HOMEASSISTANT_STOP,
EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST,
HTTP_CREATED,
HTTP_NOT_FOUND,
HTTP_OK,
2019-07-31 19:25:30 +00:00
MATCH_ALL,
URL_API,
URL_API_COMPONENTS,
URL_API_CONFIG,
URL_API_DISCOVERY_INFO,
URL_API_ERROR_LOG,
URL_API_EVENTS,
URL_API_SERVICES,
URL_API_STATES,
URL_API_STREAM,
URL_API_TEMPLATE,
__version__,
)
2018-05-27 18:16:30 +00:00
import homeassistant.core as ha
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
2016-02-23 20:06:50 +00:00
from homeassistant.helpers import template
from homeassistant.helpers.json import JSONEncoder
2018-05-27 18:16:30 +00:00
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
ATTR_BASE_URL = "base_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_VERSION = "version"
2019-07-31 19:25:30 +00:00
DOMAIN = "api"
STREAM_PING_PAYLOAD = "ping"
2015-02-19 08:15:21 +00:00
STREAM_PING_INTERVAL = 50 # seconds
def setup(hass, config):
2016-02-23 20:06:50 +00:00
"""Register the API with the HTTP interface."""
hass.http.register_view(APIStatusView)
hass.http.register_view(APIEventStream)
hass.http.register_view(APIConfigView)
hass.http.register_view(APIDiscoveryView)
hass.http.register_view(APIStatesView)
hass.http.register_view(APIEntityStateView)
hass.http.register_view(APIEventListenersView)
hass.http.register_view(APIEventView)
hass.http.register_view(APIServicesView)
hass.http.register_view(APIDomainServicesView)
hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView)
if DATA_LOGGING in hass.data:
hass.http.register_view(APIErrorLog)
return True
2015-01-30 16:26:06 +00:00
class APIStatusView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Status requests."""
url = URL_API
2019-07-31 19:25:30 +00:00
name = "api:status"
@ha.callback
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Retrieve if API is running."""
2018-05-27 18:16:30 +00:00
return self.json_message("API running.")
class APIEventStream(HomeAssistantView):
2016-05-17 02:47:15 +00:00
"""View to handle EventStream requests."""
2015-02-14 02:59:42 +00:00
2016-05-14 07:58:36 +00:00
url = URL_API_STREAM
2019-07-31 19:25:30 +00:00
name = "api:stream"
2015-09-24 04:35:23 +00:00
async def get(self, request):
2016-05-14 07:58:36 +00:00
"""Provide a streaming interface for the event bus."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized()
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
2016-05-14 07:58:36 +00:00
stop_obj = object()
to_write = asyncio.Queue()
2016-05-14 07:58:36 +00:00
2019-07-31 19:25:30 +00:00
restrict = request.query.get("restrict")
2016-05-14 07:58:36 +00:00
if restrict:
2019-07-31 19:25:30 +00:00
restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP]
2016-05-14 07:58:36 +00:00
async def forward_events(event):
2016-05-14 07:58:36 +00:00
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
2016-05-16 06:54:14 +00:00
return
if restrict and event.event_type not in restrict:
return
2018-05-27 18:16:30 +00:00
_LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
2016-05-16 06:54:14 +00:00
if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj
2016-05-14 07:58:36 +00:00
else:
data = json.dumps(event, cls=JSONEncoder)
2016-05-16 06:54:14 +00:00
await to_write.put(data)
2016-05-14 07:58:36 +00:00
response = web.StreamResponse()
2019-07-31 19:25:30 +00:00
response.content_type = "text/event-stream"
await response.prepare(request)
2016-05-14 07:58:36 +00:00
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
2015-02-19 08:15:21 +00:00
try:
2018-05-27 18:16:30 +00:00
_LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
2016-05-17 02:47:15 +00:00
# Fire off one message so browsers fire open event right away
await to_write.put(STREAM_PING_PAYLOAD)
2015-02-14 02:59:42 +00:00
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL):
payload = await to_write.get()
2015-02-14 02:59:42 +00:00
if payload is stop_obj:
2016-09-07 13:59:59 +00:00
break
msg = f"data: {payload}\n\n"
2019-07-31 19:25:30 +00:00
_LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
await response.write(msg.encode("UTF-8"))
except asyncio.TimeoutError:
await to_write.put(STREAM_PING_PAYLOAD)
2017-01-15 16:36:24 +00:00
except asyncio.CancelledError:
2018-05-27 18:16:30 +00:00
_LOGGER.debug("STREAM %s ABORT", id(stop_obj))
2017-01-15 16:36:24 +00:00
finally:
2018-05-27 18:16:30 +00:00
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
unsub_stream()
2015-02-14 02:59:42 +00:00
return response
2015-02-14 02:59:42 +00:00
class APIConfigView(HomeAssistantView):
2018-05-27 18:16:30 +00:00
"""View to handle Configuration requests."""
2016-05-14 07:58:36 +00:00
url = URL_API_CONFIG
2019-07-31 19:25:30 +00:00
name = "api:config"
@ha.callback
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current configuration."""
2019-07-31 19:25:30 +00:00
return self.json(request.app["hass"].config.as_dict())
2015-05-02 01:24:32 +00:00
class APIDiscoveryView(HomeAssistantView):
2018-05-27 18:16:30 +00:00
"""View to provide Discovery information."""
2016-05-14 07:58:36 +00:00
requires_auth = False
url = URL_API_DISCOVERY_INFO
2019-07-31 19:25:30 +00:00
name = "api:discovery"
@ha.callback
def get(self, request):
2018-05-27 18:16:30 +00:00
"""Get discovery information."""
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
return self.json(
{
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,
}
)
class APIStatesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle States requests."""
url = URL_API_STATES
name = "api:states"
@ha.callback
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current states."""
2019-07-31 19:25:30 +00:00
user = request["hass_user"]
entity_perm = user.permissions.check_entity
states = [
2019-07-31 19:25:30 +00:00
state
for state in request.app["hass"].states.async_all()
if entity_perm(state.entity_id, "read")
]
return self.json(states)
class APIEntityStateView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EntityState requests."""
2019-07-31 19:25:30 +00:00
url = "/api/states/{entity_id}"
name = "api:entity-state"
@ha.callback
def get(self, request, entity_id):
2016-05-14 07:58:36 +00:00
"""Retrieve state of entity."""
2019-07-31 19:25:30 +00:00
user = request["hass_user"]
if not user.permissions.check_entity(entity_id, POLICY_READ):
raise Unauthorized(entity_id=entity_id)
2019-07-31 19:25:30 +00:00
state = request.app["hass"].states.get(entity_id)
if state:
2016-05-14 07:58:36 +00:00
return self.json(state)
2018-05-27 18:16:30 +00:00
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
async def post(self, request, entity_id):
2016-05-14 07:58:36 +00:00
"""Update state of entity."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized(entity_id=entity_id)
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
try:
data = await request.json()
except ValueError:
2019-07-31 19:25:30 +00:00
return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
2019-07-31 19:25:30 +00:00
new_state = data.get("state")
if new_state is None:
2018-05-27 18:16:30 +00:00
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
2019-07-31 19:25:30 +00:00
attributes = data.get("attributes")
force_update = data.get("force_update", False)
is_new_state = hass.states.get(entity_id) is None
# Write state
2019-07-31 19:25:30 +00:00
hass.states.async_set(
entity_id, new_state, attributes, force_update, self.context(request)
)
# Read the state back for our response
status_code = HTTP_CREATED if is_new_state else HTTP_OK
resp = self.json(hass.states.get(entity_id), status_code)
resp.headers.add("Location", f"/api/states/{entity_id}")
return resp
@ha.callback
def delete(self, request, entity_id):
2016-05-14 07:58:36 +00:00
"""Remove entity."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized(entity_id=entity_id)
2019-07-31 19:25:30 +00:00
if request.app["hass"].states.async_remove(entity_id):
2018-05-27 18:16:30 +00:00
return self.json_message("Entity removed.")
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
class APIEventListenersView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EventListeners requests."""
url = URL_API_EVENTS
2019-07-31 19:25:30 +00:00
name = "api:event-listeners"
@ha.callback
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get event listeners."""
2019-07-31 19:25:30 +00:00
return self.json(async_events_json(request.app["hass"]))
class APIEventView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Event requests."""
2019-07-31 19:25:30 +00:00
url = "/api/events/{event_type}"
name = "api:event"
async def post(self, request, event_type):
2016-05-14 07:58:36 +00:00
"""Fire events."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized()
body = await request.text()
try:
event_data = json.loads(body) if body else None
except ValueError:
2018-05-27 18:16:30 +00:00
return self.json_message(
2019-07-31 19:25:30 +00:00
"Event data should be valid JSON.", HTTP_BAD_REQUEST
)
2016-05-14 07:58:36 +00:00
if event_data is not None and not isinstance(event_data, dict):
2018-05-27 18:16:30 +00:00
return self.json_message(
2019-07-31 19:25:30 +00:00
"Event data should be a JSON object", HTTP_BAD_REQUEST
)
# Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects
if event_type == ha.EVENT_STATE_CHANGED and event_data:
2019-07-31 19:25:30 +00:00
for key in ("old_state", "new_state"):
state = ha.State.from_dict(event_data.get(key))
if state:
event_data[key] = state
2019-07-31 19:25:30 +00:00
request.app["hass"].bus.async_fire(
event_type, event_data, ha.EventOrigin.remote, self.context(request)
)
return self.json_message(f"Event {event_type} fired.")
class APIServicesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Services requests."""
url = URL_API_SERVICES
2019-07-31 19:25:30 +00:00
name = "api:services"
async def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get registered services."""
2019-07-31 19:25:30 +00:00
services = await async_services_json(request.app["hass"])
return self.json(services)
class APIDomainServicesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle DomainServices requests."""
2019-07-31 19:25:30 +00:00
url = "/api/services/{domain}/{service}"
name = "api:domain-services"
async def post(self, request, domain, service):
2016-05-14 07:58:36 +00:00
"""Call a service.
2016-05-14 07:58:36 +00:00
Returns a list of changed states.
"""
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
body = await request.text()
try:
data = json.loads(body) if body else None
except ValueError:
2019-07-31 19:25:30 +00:00
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states:
try:
await hass.services.async_call(
2019-07-31 19:25:30 +00:00
domain, service, data, True, self.context(request)
)
except (vol.Invalid, ServiceNotFound):
raise HTTPBadRequest()
2016-05-14 07:58:36 +00:00
return self.json(changed_states)
class APIComponentsView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Components requests."""
url = URL_API_COMPONENTS
2019-07-31 19:25:30 +00:00
name = "api:components"
@ha.callback
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current loaded components."""
2019-07-31 19:25:30 +00:00
return self.json(request.app["hass"].config.components)
2015-05-02 02:02:29 +00:00
2015-05-02 03:56:10 +00:00
class APITemplateView(HomeAssistantView):
2018-05-27 18:16:30 +00:00
"""View to handle Template requests."""
2016-05-14 07:58:36 +00:00
url = URL_API_TEMPLATE
2019-07-31 19:25:30 +00:00
name = "api:template"
async def post(self, request):
2016-05-14 07:58:36 +00:00
"""Render a template."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized()
2016-05-14 07:58:36 +00:00
try:
data = await request.json()
2019-07-31 19:25:30 +00:00
tpl = template.Template(data["template"], request.app["hass"])
return tpl.async_render(data.get("variables"))
except (ValueError, TemplateError) as ex:
2018-05-27 18:16:30 +00:00
return self.json_message(
f"Error rendering template: {ex}", HTTP_BAD_REQUEST
2019-07-31 19:25:30 +00:00
)
2015-12-10 07:56:20 +00:00
class APIErrorLog(HomeAssistantView):
2018-05-27 18:16:30 +00:00
"""View to fetch the API error log."""
url = URL_API_ERROR_LOG
2019-07-31 19:25:30 +00:00
name = "api:error_log"
async def get(self, request):
"""Retrieve API error log."""
2019-07-31 19:25:30 +00:00
if not request["hass_user"].is_admin:
raise Unauthorized()
2019-07-31 19:25:30 +00:00
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
async def async_services_json(hass):
2016-02-23 20:06:50 +00:00
"""Generate services data to JSONify."""
descriptions = await async_get_all_descriptions(hass)
2019-07-31 19:25:30 +00:00
return [{"domain": key, "services": value} for key, value in descriptions.items()]
2015-05-02 02:02:29 +00:00
2015-05-02 03:56:10 +00:00
@ha.callback
def async_events_json(hass):
2016-02-23 20:06:50 +00:00
"""Generate event data to JSONify."""
2019-07-31 19:25:30 +00:00
return [
{"event": key, "listener_count": value}
for key, value in hass.bus.async_listeners().items()
]