Validate API on starting remote instance

This commit is contained in:
Paulus Schoutsen 2014-05-01 23:03:14 -07:00
parent 50b492c64a
commit e9d1dfac84
4 changed files with 116 additions and 2 deletions

View file

@ -120,6 +120,22 @@ Because each slave maintains it's own ServiceRegistry it is possible to have mul
![home assistant master-slave architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture-remote.png)
A slave instance can be started with the following code.
```python
import homeassistant.remote as remote
import homeassistant.components.http as http
remote_api = remote.API("remote_host_or_ip", "remote_api_password")
hass = remote.HomeAssistant(remote_api)
http.setup(hass, "my_local_api_password")
hass.start()
hass.block_till_stopped()
```
Web interface and API
---------------------
Home Assistent runs a webserver accessible on port 8123.
@ -143,6 +159,15 @@ Other status codes that can occur are:
The api supports the following actions:
**/api - GET**<br>
Returns message if API is up and running.
```json
{
"message": "API running."
}
```
**/api/events - GET**<br>
Returns a dict with as keys the events and as value the number of listeners.

View file

@ -17,6 +17,13 @@ Other status codes that can occur are:
The api supports the following actions:
/api - GET
Returns message if API is up and running.
Example result:
{
"message": "API running."
}
/api/states - GET
Returns a list of entities for which a state is available
Example result:
@ -112,6 +119,10 @@ def setup(hass, api_password, server_port=None, server_host=None):
lambda event:
threading.Thread(target=server.start, daemon=True).start())
# If no local api set, set one with known information
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
hass.local_api = rem.API(util.get_local_ip(), api_password, server_port)
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
""" Handle HTTP requests in a threaded fashion. """
@ -149,6 +160,9 @@ class RequestHandler(BaseHTTPRequestHandler):
('POST', re.compile(URL_FIRE_EVENT), '_handle_fire_event'),
('POST', re.compile(URL_CALL_SERVICE), '_handle_call_service'),
# /api - for validation purposes
('GET', rem.URL_API, '_handle_get_api'),
# /states
('GET', rem.URL_API_STATES, '_handle_get_api_states'),
('GET',
@ -619,6 +633,11 @@ class RequestHandler(BaseHTTPRequestHandler):
self._message(
"Invalid JSON for service_data", HTTP_UNPROCESSABLE_ENTITY)
# pylint: disable=unused-argument
def _handle_get_api(self, path_match, data):
""" Renders the debug interface. """
self._message("API running.")
# pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data):
""" Returns a dict containing all entity ids and their state. """

View file

@ -12,6 +12,7 @@ HomeAssistantError will be raised.
import threading
import logging
import json
import enum
import urllib.parse
import requests
@ -20,6 +21,7 @@ import homeassistant as ha
SERVER_PORT = 8123
URL_API = "/api/"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
@ -32,6 +34,18 @@ METHOD_GET = "get"
METHOD_POST = "post"
class APIStatus(enum.Enum):
""" Represents API status. """
OK = "ok"
INVALID_PASSWORD = "invalid_password"
CANNOT_CONNECT = "cannot_connect"
UNKNOWN = "unknown"
def __str__(self):
return self.value
class API(object):
""" Object to pass around Home Assistant API location and credentials. """
# pylint: disable=too-few-public-methods
@ -41,6 +55,13 @@ class API(object):
self.port = port or SERVER_PORT
self.api_password = api_password
self.base_url = "http://{}:{}".format(host, self.port)
self.status = None
def validate_api(self, force_validate=False):
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):
""" Makes a call to the Home Assistant api. """
@ -64,9 +85,13 @@ class HomeAssistant(ha.HomeAssistant):
""" Home Assistant that forwards work. """
# pylint: disable=super-init-not-called
def __init__(self, local_api, remote_api):
self.local_api = local_api
def __init__(self, remote_api, local_api=None):
if not remote_api.validate_api():
raise ha.HomeAssistantError(
"Remote API not valid: {}".format(remote_api.status))
self.remote_api = remote_api
self.local_api = local_api
self._pool = pool = ha.create_worker_pool()
@ -75,6 +100,14 @@ class HomeAssistant(ha.HomeAssistant):
self.states = StateMachine(self.bus, self.remote_api)
def start(self):
# If there is no local API setup but we do want to connect with remote
# We create a random password and set up a local api
if self.local_api is None:
import homeassistant.components.http as http
import random
http.setup(self, '%030x'.format(random.randrange(16**30)))
ha.Timer(self)
# Setup that events from remote_api get forwarded to local_api
@ -201,6 +234,24 @@ class JSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj)
def validate_api(api):
""" Makes 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 ha.HomeAssistantError:
return APIStatus.CANNOT_CONNECT
def connect_remote_events(from_api, to_api):
""" Sets up from_api to forward all events to to_api. """

View file

@ -9,6 +9,8 @@ import queue
import datetime
import re
import enum
import socket
import os
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SLUGIFY = re.compile(r'[^A-Za-z0-9_]+')
@ -124,6 +126,23 @@ def ensure_unique_string(preferred_string, current_strings):
return string
# Taken from: http://stackoverflow.com/a/11735897
def get_local_ip():
""" Tries to determine the local IP address of the machine. """
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(('8.8.8.8', 80))
ip_addr = sock.getsockname()[0]
sock.close()
return ip_addr
except socket.error:
return socket.gethostbyname(socket.gethostname())
class OrderedEnum(enum.Enum):
""" Taken from Python 3.4.0 docs. """
# pylint: disable=no-init