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

+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