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