Validate API on starting remote instance
This commit is contained in:
parent
50b492c64a
commit
e9d1dfac84
4 changed files with 116 additions and 2 deletions
25
README.md
25
README.md
|
@ -120,6 +120,22 @@ Because each slave maintains it's own ServiceRegistry it is possible to have mul
|
|||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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. """
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue