1
mirror of https://github.com/home-assistant/core synced 2024-09-28 03:04:04 +02:00
ha-core/homeassistant/remote.py

328 lines
9.3 KiB
Python
Raw Normal View History

"""
2016-03-08 00:06:04 +01:00
Support for an interface to work with a remote instance of Home Assistant.
If a connection error occurs while communicating with the API a
HomeAssistantError will be raised.
For more details about the Python API, please refer to the documentation at
https://home-assistant.io/developers/python_api/
"""
2016-04-16 09:55:35 +02:00
from datetime import datetime
import enum
2016-02-19 06:27:50 +01:00
import json
import logging
import urllib.parse
from typing import Optional
import requests
from homeassistant import core as ha
from homeassistant.const import (
HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API,
2016-07-26 17:50:38 +02:00
URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, URL_API_CONFIG,
2016-05-14 09:58:36 +02:00
URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY,
HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
2016-02-19 06:27:50 +01:00
from homeassistant.exceptions import HomeAssistantError
2013-10-29 08:22:38 +01:00
METHOD_GET = "get"
METHOD_POST = "post"
METHOD_DELETE = "delete"
2013-10-29 08:22:38 +01:00
2014-11-08 22:57:08 +01:00
_LOGGER = logging.getLogger(__name__)
2013-11-11 01:46:48 +01:00
class APIStatus(enum.Enum):
2016-03-08 00:06:04 +01:00
"""Represent API status."""
# pylint: disable=no-init, invalid-name
OK = "ok"
INVALID_PASSWORD = "invalid_password"
CANNOT_CONNECT = "cannot_connect"
UNKNOWN = "unknown"
def __str__(self) -> str:
2016-03-08 00:06:04 +01:00
"""Return the state."""
return self.value
class API(object):
2016-03-08 00:06:04 +01:00
"""Object to pass around Home Assistant API location and credentials."""
def __init__(self, host: str, api_password: Optional[str]=None,
port: Optional[int]=SERVER_PORT, use_ssl: bool=False) -> None:
2016-03-08 00:06:04 +01:00
"""Initalize the API."""
self.host = host
self.port = port
self.api_password = api_password
if host.startswith(("http://", "https://")):
self.base_url = host
elif use_ssl:
self.base_url = "https://{}".format(host)
2015-12-06 23:13:35 +01:00
else:
self.base_url = "http://{}".format(host)
if port is not None:
self.base_url += ':{}'.format(port)
self.status = None
2016-05-14 09:58:36 +02:00
self._headers = {
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
}
if api_password is not None:
self._headers[HTTP_HEADER_HA_AUTH] = api_password
def validate_api(self, force_validate: bool=False) -> bool:
2016-03-08 00:06:04 +01:00
"""Test if we can communicate with the API."""
if self.status is None or force_validate:
self.status = validate_api(self)
return self.status == APIStatus.OK
def __call__(self, method, path, data=None, timeout=5):
2016-03-08 00:06:04 +01:00
"""Make a call to the Home Assistant API."""
2014-10-17 09:17:02 +02:00
if data is not None:
data = json.dumps(data, cls=JSONEncoder)
url = urllib.parse.urljoin(self.base_url, path)
2013-10-29 08:22:38 +01:00
try:
if method == METHOD_GET:
2014-10-17 09:17:02 +02:00
return requests.get(
url, params=data, timeout=timeout, headers=self._headers)
else:
2014-10-17 09:17:02 +02:00
return requests.request(
method, url, data=data, timeout=timeout,
headers=self._headers)
except requests.exceptions.ConnectionError:
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Error connecting to server")
raise HomeAssistantError("Error connecting to server")
2014-06-13 08:09:56 +02:00
except requests.exceptions.Timeout:
error = "Timeout when talking to {}".format(self.host)
2014-11-08 22:57:08 +01:00
_LOGGER.exception(error)
raise HomeAssistantError(error)
2014-06-13 08:09:56 +02:00
def __repr__(self) -> str:
2016-03-08 00:06:04 +01:00
"""Return the representation of the API."""
return "<API({}, password: {})>".format(
self.base_url, 'yes' if self.api_password is not None else 'no')
2014-11-23 21:57:29 +01:00
class JSONEncoder(json.JSONEncoder):
2016-03-08 00:06:04 +01:00
"""JSONEncoder that supports Home Assistant objects."""
2014-11-23 21:57:29 +01:00
# pylint: disable=method-hidden
2014-11-23 21:57:29 +01:00
def default(self, obj):
2016-03-08 00:06:04 +01:00
"""Convert Home Assistant objects.
Hand other objects to the original method.
"""
2016-04-16 09:55:35 +02:00
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, set):
return list(obj)
2016-04-16 09:55:35 +02:00
elif hasattr(obj, 'as_dict'):
2014-11-23 21:57:29 +01:00
return obj.as_dict()
2015-02-02 03:00:30 +01:00
try:
return json.JSONEncoder.default(self, obj)
except TypeError:
# If the JSON serializer couldn't serialize it
# it might be a generator, convert it to a list
try:
2015-02-07 22:22:23 +01:00
return [self.default(child_obj)
2015-02-02 03:00:30 +01:00
for child_obj in obj]
except TypeError:
# Ok, we're lost, cause the original error
return json.JSONEncoder.default(self, obj)
2014-11-23 21:57:29 +01:00
def validate_api(api):
2016-03-08 00:06:04 +01:00
"""Make a call to validate API."""
try:
req = api(METHOD_GET, URL_API)
if req.status_code == 200:
return APIStatus.OK
elif req.status_code == 401:
return APIStatus.INVALID_PASSWORD
else:
return APIStatus.UNKNOWN
except HomeAssistantError:
return APIStatus.CANNOT_CONNECT
2014-11-08 22:57:08 +01:00
def get_event_listeners(api):
2016-03-08 00:06:04 +01:00
"""List of events that is being listened for."""
try:
req = api(METHOD_GET, URL_API_EVENTS)
2014-10-17 09:17:02 +02:00
return req.json() if req.status_code == 200 else {}
except (HomeAssistantError, ValueError):
# ValueError if req.json() can't parse the json
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Unexpected result retrieving event listeners")
return {}
2014-11-08 22:57:08 +01:00
def fire_event(api, event_type, data=None):
2016-03-08 00:06:04 +01:00
"""Fire an event at remote API."""
try:
req = api(METHOD_POST, URL_API_EVENTS_EVENT.format(event_type), data)
2014-11-08 22:57:08 +01:00
if req.status_code != 200:
2016-05-14 09:58:36 +02:00
_LOGGER.error("Error firing event: %d - %s",
2014-11-08 22:57:08 +01:00
req.status_code, req.text)
except HomeAssistantError:
2014-11-29 06:01:44 +01:00
_LOGGER.exception("Error firing event")
2014-11-08 22:57:08 +01:00
def get_state(api, entity_id):
2016-03-08 00:06:04 +01:00
"""Query given API for state of entity_id."""
try:
req = api(METHOD_GET, URL_API_STATES_ENTITY.format(entity_id))
# req.status_code == 422 if entity does not exist
return ha.State.from_dict(req.json()) \
if req.status_code == 200 else None
except (HomeAssistantError, ValueError):
# ValueError if req.json() can't parse the json
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Error fetching state")
return None
2014-11-08 22:57:08 +01:00
def get_states(api):
2016-03-08 00:06:04 +01:00
"""Query given API for all states."""
try:
req = api(METHOD_GET,
URL_API_STATES)
2014-10-17 09:17:02 +02:00
return [ha.State.from_dict(item) for
item in req.json()]
2013-11-01 19:34:43 +01:00
except (HomeAssistantError, ValueError, AttributeError):
# ValueError if req.json() can't parse the json
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Error fetching states")
2014-11-29 06:01:44 +01:00
return []
def remove_state(api, entity_id):
"""Call API to remove state for entity_id.
2016-03-08 00:06:04 +01:00
Return True if entity is gone (removed/never existed).
"""
try:
req = api(METHOD_DELETE, URL_API_STATES_ENTITY.format(entity_id))
if req.status_code in (200, 404):
return True
_LOGGER.error("Error removing state: %d - %s",
req.status_code, req.text)
return False
except HomeAssistantError:
_LOGGER.exception("Error removing state")
return False
def set_state(api, entity_id, new_state, attributes=None, force_update=False):
2016-03-08 00:06:04 +01:00
"""Tell API to update state for entity_id.
2016-03-08 00:06:04 +01:00
Return True if success.
"""
attributes = attributes or {}
2014-10-17 09:17:02 +02:00
data = {'state': new_state,
'attributes': attributes,
'force_update': force_update}
try:
req = api(METHOD_POST,
URL_API_STATES_ENTITY.format(entity_id),
data)
if req.status_code not in (200, 201):
2014-11-08 22:57:08 +01:00
_LOGGER.error("Error changing state: %d - %s",
req.status_code, req.text)
return False
else:
return True
except HomeAssistantError:
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Error setting state")
return False
2014-11-08 22:57:08 +01:00
def is_state(api, entity_id, state):
2016-03-08 00:06:04 +01:00
"""Query API to see if entity_id is specified state."""
2014-11-08 22:57:08 +01:00
cur_state = get_state(api, entity_id)
return cur_state and cur_state.state == state
2014-11-08 22:57:08 +01:00
def get_services(api):
2016-03-08 00:06:04 +01:00
"""Return a list of dicts.
Each dict has a string "domain" and a list of strings "services".
2014-10-20 08:37:43 +02:00
"""
try:
req = api(METHOD_GET, URL_API_SERVICES)
2014-10-17 09:17:02 +02:00
return req.json() if req.status_code == 200 else {}
except (HomeAssistantError, ValueError):
# ValueError if req.json() can't parse the json
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Got unexpected services result")
return {}
def call_service(api, domain, service, service_data=None, timeout=5):
2016-03-08 00:06:04 +01:00
"""Call a service at the remote API."""
2014-10-20 03:41:06 +02:00
try:
req = api(METHOD_POST,
URL_API_SERVICES_SERVICE.format(domain, service),
service_data, timeout=timeout)
2014-11-08 22:57:08 +01:00
if req.status_code != 200:
_LOGGER.error("Error calling service: %d - %s",
req.status_code, req.text)
2014-10-20 03:41:06 +02:00
except HomeAssistantError:
2014-11-08 22:57:08 +01:00
_LOGGER.exception("Error calling service")
2016-07-26 17:50:38 +02:00
def get_config(api):
"""Return configuration."""
try:
req = api(METHOD_GET, URL_API_CONFIG)
if req.status_code != 200:
return {}
result = req.json()
if 'components' in result:
result['components'] = set(result['components'])
return result
2016-07-26 17:50:38 +02:00
except (HomeAssistantError, ValueError):
# ValueError if req.json() can't parse the JSON
_LOGGER.exception("Got unexpected configuration results")
return {}