diff --git a/README.md b/README.md index f52bc381f22..5a81e2901e9 100644 --- a/README.md +++ b/README.md @@ -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**
+Returns message if API is up and running. + +```json +{ + "message": "API running." +} +``` + **/api/events - GET**
Returns a dict with as keys the events and as value the number of listeners. diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2cd5a67f3f4..b4ac12cff87 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -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. """ diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b5f24620508..e50ecd071a2 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -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. """ diff --git a/homeassistant/util.py b/homeassistant/util.py index ac551609e93..34a70e22e72 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -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