diff --git a/README.md b/README.md
index 5ae5de1cf84..f52bc381f22 100644
--- a/README.md
+++ b/README.md
@@ -40,138 +40,11 @@ Installation instructions
Done. Start it now by running `python start.py`
-Web interface and API
----------------------
-Home Assistent runs a webserver accessible on port 8123.
-
- * At http://localhost:8123/ it will provide a debug interface showing the current state of the system and an overview of registered services.
- * At http://localhost:8123/api/ it provides a password protected API.
-
-A screenshot of the debug interface:
-
-
-All API calls have to be accompanied by an 'api_password' parameter (as specified in `home-assistant.conf`) and will
-return JSON encoded objects. If successful calls will return status code 200 or 201.
-
-Other status codes that can occur are:
- - 400 (Bad Request)
- - 401 (Unauthorized)
- - 404 (Not Found)
- - 405 (Method not allowed)
-
-The api supports the following actions:
-
-**/api/states - GET**
-Returns a list of entity ids for which a state is available
-
-```json
-{
- "entity_ids": [
- "Paulus_Nexus_4",
- "weather.sun",
- "all_devices"
- ]
-}
-```
-
-**/api/events - GET**
-Returns a dict with as keys the events and as value the number of listeners.
-
-```json
-{
- "event_listeners": {
- "state_changed": 5,
- "time_changed": 2
- }
-}
-```
-
-**/api/services - GET**
-Returns a dict with as keys the domain and as value a list of published services.
-
-```json
-{
- "services": {
- "browser": [
- "browse_url"
- ],
- "keyboard": [
- "volume_up",
- "volume_down"
- ]
- }
-}
-```
-
-**/api/states/<entity_id>** - GET
-Returns the current state from an entity
-
-```json
-{
- "attributes": {
- "next_rising": "07:04:15 29-10-2013",
- "next_setting": "18:00:31 29-10-2013"
- },
- "entity_id": "weather.sun",
- "last_changed": "23:24:33 28-10-2013",
- "state": "below_horizon"
-}
-```
-
-**/api/states/<entity_id>** - POST
-Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.
-parameter: new_state - string
-optional parameter: attributes - JSON encoded object
-
-```json
-{
- "attributes": {
- "next_rising": "07:04:15 29-10-2013",
- "next_setting": "18:00:31 29-10-2013"
- },
- "entity_id": "weather.sun",
- "last_changed": "23:24:33 28-10-2013",
- "state": "below_horizon"
-}
-```
-
-**/api/events/<event_type>** - POST
-Fires an event with event_type
-optional parameter: event_data - JSON encoded object
-
-```json
-{
- "message": "Event download_file fired."
-}
-```
-
-**/api/services/<domain>/<service>** - POST
-Calls a service within a specific domain.
-optional parameter: service_data - JSON encoded object
-
-```json
-{
- "message": "Service keyboard/volume_up called."
-}
-```
-
-Android remote control
-----------------------
-
-An app has been built using [Tasker for Android](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) that:
-
- * Provides buttons to control the lights and the chromecast
- * Reports the charging state and battery level of the phone
-
-The [APK](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.apk) and [Tasker project XML](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.prj.xml) can be found in [/android-tasker/](https://github.com/balloob/home-assistant/tree/master/android-tasker)
-
-
-
Architecture
------------
-The core of Home Assistant exists of two parts; a Bus for calling services and firing events and a State Machine that keeps track of the state of things.
+The core of Home Assistant exists of three parts; an EventBus for firing events, a StateMachine that keeps track of the state of things and a ServiceRegistry to manage services.
-
+
For example to control the lights there are two components. One is the device_tracker that polls the wireless router for connected devices and updates the state of the tracked devices in the State Machine to be either 'Home' or 'Not Home'.
@@ -238,3 +111,175 @@ Registers service `downloader/download_file` that will download files. File to d
**browser**
Registers service `browser/browse_url` that opens `url` as specified in event_data in the system default browser.
+
+### Multiple connected instances
+
+Home Assistant supports running multiple synchronzied instances using a master-slave model. Slaves forward all local events fired and states set to the master instance which will then replicate it to each slave.
+
+Because each slave maintains it's own ServiceRegistry it is possible to have multiple slaves respond to one service call.
+
+
+
+Web interface and API
+---------------------
+Home Assistent runs a webserver accessible on port 8123.
+
+ * At http://localhost:8123/ it will provide a debug interface showing the current state of the system and an overview of registered services.
+ * At http://localhost:8123/api/ it provides a password protected API.
+
+A screenshot of the debug interface:
+
+
+In the package `homeassistant.remote` a Python API on top of the HTTP API can be found.
+
+All API calls have to be accompanied by an 'api_password' parameter (as specified in `home-assistant.conf`) and will
+return JSON encoded objects. If successful calls will return status code 200 or 201.
+
+Other status codes that can occur are:
+ - 400 (Bad Request)
+ - 401 (Unauthorized)
+ - 404 (Not Found)
+ - 405 (Method not allowed)
+
+The api supports the following actions:
+
+**/api/events - GET**
+Returns a dict with as keys the events and as value the number of listeners.
+
+```json
+{
+ "event_listeners": {
+ "state_changed": 5,
+ "time_changed": 2
+ }
+}
+```
+
+**/api/services - GET**
+Returns a dict with as keys the domain and as value a list of published services.
+
+```json
+{
+ "services": {
+ "browser": [
+ "browse_url"
+ ],
+ "keyboard": [
+ "volume_up",
+ "volume_down"
+ ]
+ }
+}
+```
+
+**/api/states - GET**
+Returns a dict with as keys the entity_ids and as value the state.
+
+```json
+{
+ "sun.sun": {
+ "attributes": {
+ "next_rising": "07:04:15 29-10-2013",
+ "next_setting": "18:00:31 29-10-2013"
+ },
+ "entity_id": "sun.sun",
+ "last_changed": "23:24:33 28-10-2013",
+ "state": "below_horizon"
+ },
+ "process.Dropbox": {
+ "attributes": {},
+ "entity_id": "process.Dropbox",
+ "last_changed": "23:24:33 28-10-2013",
+ "state": "on"
+ }
+}
+```
+
+**/api/states/<entity_id>** - GET
+Returns the current state from an entity
+
+```json
+{
+ "attributes": {
+ "next_rising": "07:04:15 29-10-2013",
+ "next_setting": "18:00:31 29-10-2013"
+ },
+ "entity_id": "sun.sun",
+ "last_changed": "23:24:33 28-10-2013",
+ "state": "below_horizon"
+}
+```
+
+**/api/states/<entity_id>** - POST
+Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.
+parameter: new_state - string
+optional parameter: attributes - JSON encoded object
+
+```json
+{
+ "attributes": {
+ "next_rising": "07:04:15 29-10-2013",
+ "next_setting": "18:00:31 29-10-2013"
+ },
+ "entity_id": "weather.sun",
+ "last_changed": "23:24:33 28-10-2013",
+ "state": "below_horizon"
+}
+```
+
+**/api/events/<event_type>** - POST
+Fires an event with event_type
+optional parameter: event_data - JSON encoded object
+
+```json
+{
+ "message": "Event download_file fired."
+}
+```
+
+**/api/services/<domain>/<service>** - POST
+Calls a service within a specific domain.
+optional parameter: service_data - JSON encoded object
+
+```json
+{
+ "message": "Service keyboard/volume_up called."
+}
+```
+
+**/api/event_forwarding** - POST
+Setup event forwarding to another Home Assistant instance.
+parameter: host - string
+parameter: api_password - string
+optional parameter: port - int
+
+```json
+{
+ "message": "Event forwarding setup."
+}
+```
+
+**/api/event_forwarding** - DELETE
+Cancel event forwarding to another Home Assistant instance.
+parameter: host - string
+optional parameter: port - int
+
+If your client does not support DELETE HTTP requests you can add an optional attribute _METHOD and set its value to DELETE.
+
+```json
+{
+ "message": "Event forwarding cancelled."
+}
+```
+
+Android remote control
+----------------------
+
+An app has been built using [Tasker for Android](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) that:
+
+ * Provides buttons to control the lights and the chromecast
+ * Reports the charging state and battery level of the phone
+
+The [APK](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.apk) and [Tasker project XML](https://raw.github.com/balloob/home-assistant/master/android-tasker/Home_Assistant.prj.xml) can be found in [/android-tasker/](https://github.com/balloob/home-assistant/tree/master/android-tasker)
+
+
diff --git a/docs/architecture-remote.png b/docs/architecture-remote.png
new file mode 100644
index 00000000000..3109c921846
Binary files /dev/null and b/docs/architecture-remote.png differ
diff --git a/docs/architecture.png b/docs/architecture.png
index 927410ee875..8a53f65c7ca 100644
Binary files a/docs/architecture.png and b/docs/architecture.png differ
diff --git a/home-assistant.conf.default b/home-assistant.conf.default
index d0ae2c4738c..5227d34ef99 100644
--- a/home-assistant.conf.default
+++ b/home-assistant.conf.default
@@ -2,7 +2,7 @@
latitude=32.87336
longitude=-117.22743
-[httpinterface]
+[http]
api_password=mypass
[light.hue]
diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py
index 75204036e17..b7962442501 100644
--- a/homeassistant/__init__.py
+++ b/homeassistant/__init__.py
@@ -9,6 +9,7 @@ of entities and react to changes.
import time
import logging
import threading
+import enum
import datetime as dt
import functools as ft
@@ -40,24 +41,21 @@ class HomeAssistant(object):
""" Core class to route all communication to right components. """
def __init__(self):
- self._pool = pool = _create_worker_pool()
+ self._pool = pool = create_worker_pool()
self.bus = EventBus(pool)
- self.states = StateMachine(self.bus)
self.services = ServiceRegistry(self.bus, pool)
+ self.states = StateMachine(self.bus)
- def start(self, non_blocking=False):
- """ Start home assistant.
- Set non_blocking to True if you don't want this method to block
- as long as Home Assistant is running. """
-
+ def start(self):
+ """ Start home assistant. """
Timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START)
- if non_blocking:
- return
-
+ def block_till_stopped(self):
+ """ Will register service homeassistant/stop and
+ will block until called. """
request_shutdown = threading.Event()
self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP,
@@ -96,6 +94,7 @@ class HomeAssistant(object):
def state_listener(event):
""" The listener that listens for specific state changes. """
if entity_id == event.data['entity_id'] and \
+ 'old_state' in event.data and \
_matcher(event.data['old_state'].state, from_state) and \
_matcher(event.data['new_state'].state, to_state):
@@ -235,7 +234,7 @@ class JobPriority(util.OrderedEnum):
return JobPriority.EVENT_DEFAULT
-def _create_worker_pool(thread_count=POOL_NUM_THREAD):
+def create_worker_pool(thread_count=POOL_NUM_THREAD):
""" Creates a worker pool to be used. """
logger = logging.getLogger(__name__)
@@ -264,22 +263,37 @@ def _create_worker_pool(thread_count=POOL_NUM_THREAD):
return util.ThreadPool(thread_count, job_handler, busy_callback)
+class EventOrigin(enum.Enum):
+ """ Distinguish between origin of event. """
+ # pylint: disable=no-init
+
+ local = "LOCAL"
+ remote = "REMOTE"
+
+ def __str__(self):
+ return self.value
+
+
# pylint: disable=too-few-public-methods
class Event(object):
""" Represents an event within the Bus. """
- __slots__ = ['event_type', 'data']
+ __slots__ = ['event_type', 'data', 'origin']
- def __init__(self, event_type, data=None):
+ def __init__(self, event_type, data=None, origin=EventOrigin.local):
self.event_type = event_type
self.data = data or {}
+ self.origin = origin
def __repr__(self):
+ # pylint: disable=maybe-no-member
if self.data:
- return "".format(
- self.event_type, util.repr_helper(self.data))
+ return "".format(
+ self.event_type, self.origin.value[0],
+ util.repr_helper(self.data))
else:
- return "".format(self.event_type)
+ return "".format(self.event_type,
+ self.origin.value[0])
class EventBus(object):
@@ -291,7 +305,7 @@ class EventBus(object):
self._listeners = {}
self._logger = logging.getLogger(__name__)
self._lock = threading.Lock()
- self._pool = pool or _create_worker_pool()
+ self._pool = pool or create_worker_pool()
@property
def listeners(self):
@@ -302,7 +316,7 @@ class EventBus(object):
return {key: len(self._listeners[key])
for key in self._listeners}
- def fire(self, event_type, event_data=None):
+ def fire(self, event_type, event_data=None, origin=EventOrigin.local):
""" Fire an event. """
with self._lock:
# Copy the list of the current listeners because some listeners
@@ -311,7 +325,7 @@ class EventBus(object):
get = self._listeners.get
listeners = get(MATCH_ALL, []) + get(event_type, [])
- event = Event(event_type, event_data)
+ event = Event(event_type, event_data, origin)
self._logger.info("Bus:Handling {}".format(event))
@@ -390,7 +404,9 @@ class State(object):
""" Static method to create a state from a dict.
Ensures: state == State.from_json_dict(state.to_json_dict()) """
- if 'entity_id' not in json_dict and 'state' not in json_dict:
+ if not (json_dict and
+ 'entity_id' in json_dict and
+ 'state' in json_dict):
return None
last_changed = json_dict.get('last_changed')
@@ -429,6 +445,11 @@ class StateMachine(object):
""" List of entity ids that are being tracked. """
return list(self._states.keys())
+ def all(self):
+ """ Returns a dict mapping all entity_ids to their state. """
+ return {entity_id: state.copy() for entity_id, state
+ in self._states.items()}
+
def get(self, entity_id):
""" Returns the state of the specified entity. """
state = self._states.get(entity_id)
@@ -456,24 +477,22 @@ class StateMachine(object):
attributes = attributes or {}
with self._lock:
- if entity_id in self._states:
- old_state = self._states[entity_id]
+ old_state = self._states.get(entity_id)
- if old_state.state != new_state or \
- old_state.attributes != attributes:
+ # If state did not exist or is different, set it
+ if not old_state or \
+ old_state.state != new_state or \
+ old_state.attributes != attributes:
- state = self._states[entity_id] = \
- State(entity_id, new_state, attributes)
+ state = self._states[entity_id] = \
+ State(entity_id, new_state, attributes)
- self._bus.fire(EVENT_STATE_CHANGED,
- {'entity_id': entity_id,
- 'old_state': old_state,
- 'new_state': state})
+ event_data = {'entity_id': entity_id, 'new_state': state}
- else:
- # If state did not exist yet
- self._states[entity_id] = State(entity_id, new_state,
- attributes)
+ if old_state:
+ event_data['old_state'] = old_state
+
+ self._bus.fire(EVENT_STATE_CHANGED, event_data)
# pylint: disable=too-few-public-methods
@@ -501,7 +520,7 @@ class ServiceRegistry(object):
def __init__(self, bus, pool=None):
self._services = {}
self._lock = threading.Lock()
- self._pool = pool or _create_worker_pool()
+ self._pool = pool or create_worker_pool()
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
@property
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 7f63d725964..5d089aa31c6 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -194,13 +194,12 @@ def from_config_file(config_path, enable_logging=True):
add_status("Keyboard", load_module('keyboard').setup(hass))
# Init HTTP interface
- if has_opt("httpinterface", "api_password"):
- httpinterface = load_module('httpinterface')
+ if has_opt("http", "api_password"):
+ http = load_module('http')
- httpinterface.HTTPInterface(
- hass, get_opt("httpinterface", "api_password"))
+ http.setup(hass, get_opt("http", "api_password"))
- add_status("HTTPInterface", True)
+ add_status("HTTP", True)
# Init groups
if has_section("group"):
diff --git a/homeassistant/components/httpinterface/__init__.py b/homeassistant/components/http/__init__.py
similarity index 80%
rename from homeassistant/components/httpinterface/__init__.py
rename to homeassistant/components/http/__init__.py
index be20e5be228..2cd5a67f3f4 100644
--- a/homeassistant/components/httpinterface/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -73,13 +73,13 @@ import logging
import re
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
+from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
import homeassistant as ha
+import homeassistant.remote as rem
import homeassistant.util as util
-SERVER_PORT = 8123
-
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_MOVED_PERMANENTLY = 301
@@ -92,46 +92,49 @@ HTTP_UNPROCESSABLE_ENTITY = 422
URL_ROOT = "/"
URL_CHANGE_STATE = "/change_state"
URL_FIRE_EVENT = "/fire_event"
-
-URL_API_STATES = "/api/states"
-URL_API_STATES_ENTITY = "/api/states/{}"
-URL_API_EVENTS = "/api/events"
-URL_API_EVENTS_EVENT = "/api/events/{}"
-URL_API_SERVICES = "/api/services"
-URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
+URL_CALL_SERVICE = "/call_service"
URL_STATIC = "/static/{}"
-class HTTPInterface(threading.Thread):
- """ Provides an HTTP interface for Home Assistant. """
+def setup(hass, api_password, server_port=None, server_host=None):
+ """ Sets up the HTTP API and debug interface. """
+ server_port = server_port or rem.SERVER_PORT
- # pylint: disable=too-many-arguments
- def __init__(self, hass, api_password, server_port=None, server_host=None):
- threading.Thread.__init__(self)
+ # If no server host is given, accept all incoming requests
+ server_host = server_host or '0.0.0.0'
- self.daemon = True
+ server = HomeAssistantHTTPServer((server_host, server_port),
+ RequestHandler, hass, api_password)
- server_port = server_port or SERVER_PORT
+ hass.listen_once_event(
+ ha.EVENT_HOMEASSISTANT_START,
+ lambda event:
+ threading.Thread(target=server.start, daemon=True).start())
- # If no server host is given, accept all incoming requests
- server_host = server_host or '0.0.0.0'
- self.server = HTTPServer((server_host, server_port), RequestHandler)
+class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
+ """ Handle HTTP requests in a threaded fashion. """
- self.server.flash_message = None
- self.server.logger = logging.getLogger(__name__)
- self.server.hass = hass
- self.server.api_password = api_password
+ def __init__(self, server_address, RequestHandlerClass,
+ hass, api_password):
+ super().__init__(server_address, RequestHandlerClass)
- hass.listen_once_event(ha.EVENT_HOMEASSISTANT_START,
- lambda event: self.start())
+ self.hass = hass
+ self.api_password = api_password
+ self.logger = logging.getLogger(__name__)
- def run(self):
- """ Start the HTTP interface. """
- self.server.logger.info("Starting")
+ # To store flash messages between sessions
+ self.flash_message = None
- self.server.serve_forever()
+ # We will lazy init this one if needed
+ self.event_forwarder = None
+
+ def start(self):
+ """ Starts the server. """
+ self.logger.info("Starting")
+
+ self.serve_forever()
# pylint: disable=too-many-public-methods
@@ -139,13 +142,15 @@ class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """
PATHS = [ # debug interface
- ('GET', '/', '_handle_get_root'),
- ('POST', re.compile(r'/change_state'), '_handle_change_state'),
- ('POST', re.compile(r'/fire_event'), '_handle_fire_event'),
- ('POST', re.compile(r'/call_service'), '_handle_call_service'),
+ ('GET', URL_ROOT, '_handle_get_root'),
+ # These get compiled as RE because these methods are reused
+ # by other urls that use url parameters
+ ('POST', re.compile(URL_CHANGE_STATE), '_handle_change_state'),
+ ('POST', re.compile(URL_FIRE_EVENT), '_handle_fire_event'),
+ ('POST', re.compile(URL_CALL_SERVICE), '_handle_call_service'),
# /states
- ('GET', '/api/states', '_handle_get_api_states'),
+ ('GET', rem.URL_API_STATES, '_handle_get_api_states'),
('GET',
re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'),
'_handle_get_api_states_entity'),
@@ -154,19 +159,24 @@ class RequestHandler(BaseHTTPRequestHandler):
'_handle_change_state'),
# /events
- ('GET', '/api/events', '_handle_get_api_events'),
+ ('GET', rem.URL_API_EVENTS, '_handle_get_api_events'),
('POST',
re.compile(r'/api/events/(?P[a-zA-Z\._0-9]+)'),
'_handle_fire_event'),
# /services
- ('GET', '/api/services', '_handle_get_api_services'),
+ ('GET', rem.URL_API_SERVICES, '_handle_get_api_services'),
('POST',
re.compile((r'/api/services/'
r'(?P[a-zA-Z\._0-9]+)/'
r'(?P[a-zA-Z\._0-9]+)')),
'_handle_call_service'),
+ # /event_forwarding
+ ('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
+ ('DELETE', rem.URL_API_EVENT_FORWARD,
+ '_handle_delete_api_event_forward'),
+
# Statis files
('GET', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'),
'_handle_get_static')
@@ -193,6 +203,9 @@ class RequestHandler(BaseHTTPRequestHandler):
except KeyError:
api_password = ''
+ if '_METHOD' in data:
+ method = data['_METHOD'][0]
+
if url.path.startswith('/api/'):
self.use_json = True
@@ -327,11 +340,9 @@ class RequestHandler(BaseHTTPRequestHandler):
"Attributes | Last Changed | "
"").format(self.server.api_password))
- for entity_id in \
- sorted(self.server.hass.states.entity_ids,
- key=lambda key: key.lower()):
-
- state = self.server.hass.states.get(entity_id)
+ for entity_id, state in \
+ sorted(self.server.hass.states.all().items(),
+ key=lambda item: item[0].lower()):
attributes = "
".join(
["{}: {}".format(attr, state.attributes[attr])
@@ -512,7 +523,7 @@ class RequestHandler(BaseHTTPRequestHandler):
self._write_json(state.as_dict(),
status_code=HTTP_CREATED,
location=
- URL_API_STATES_ENTITY.format(entity_id))
+ rem.URL_API_STATES_ENTITY.format(entity_id))
else:
self._message(
"State of {} changed to {}".format(entity_id, new_state))
@@ -534,21 +545,33 @@ class RequestHandler(BaseHTTPRequestHandler):
This handles the following paths:
/fire_event
/api/events/
+
+ Events from /api are threated as remote events.
"""
try:
try:
event_type = path_match.group('event_type')
+ event_origin = ha.EventOrigin.remote
except IndexError:
# If group event_type does not exist in path_match
event_type = data['event_type'][0]
+ event_origin = ha.EventOrigin.local
- try:
+ if 'event_data' in data:
event_data = json.loads(data['event_data'][0])
- except KeyError:
- # Happens if key 'event_data' does not exist
+ else:
event_data = None
- self.server.hass.bus.fire(event_type, event_data)
+ # Special case handling for event STATE_CHANGED
+ # We will try to convert state dicts back to State objects
+ if event_type == ha.EVENT_STATE_CHANGED and event_data:
+ for key in ('old_state', 'new_state'):
+ state = ha.State.from_dict(event_data.get(key))
+
+ if state:
+ event_data[key] = state
+
+ self.server.hass.bus.fire(event_type, event_data, event_origin)
self._message("Event {} fired.".format(event_type))
@@ -598,9 +621,8 @@ class RequestHandler(BaseHTTPRequestHandler):
# pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data):
- """ Returns the entitie ids which state are being tracked. """
- self._write_json(
- {'entity_ids': list(self.server.hass.states.entity_ids)})
+ """ Returns a dict containing all entity ids and their state. """
+ self._write_json(self.server.hass.states.all())
# pylint: disable=unused-argument
def _handle_get_api_states_entity(self, path_match, data):
@@ -609,10 +631,9 @@ class RequestHandler(BaseHTTPRequestHandler):
state = self.server.hass.states.get(entity_id)
- try:
- self._write_json(state.as_dict())
- except AttributeError:
- # If state for entity_id does not exist
+ if state:
+ self._write_json(state)
+ else:
self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY)
def _handle_get_api_events(self, path_match, data):
@@ -623,6 +644,60 @@ class RequestHandler(BaseHTTPRequestHandler):
""" Handles getting overview of services. """
self._write_json({'services': self.server.hass.services.services})
+ def _handle_post_api_event_forward(self, path_match, data):
+ """ Handles adding an event forwarding target. """
+
+ try:
+ host = data['host'][0]
+ api_password = data['api_password'][0]
+
+ port = int(data['port'][0]) if 'port' in data else None
+
+ if self.server.event_forwarder is None:
+ self.server.event_forwarder = \
+ rem.EventForwarder(self.server.hass)
+
+ api = rem.API(host, api_password, port)
+
+ self.server.event_forwarder.connect(api)
+
+ self._message("Event forwarding setup.")
+
+ except KeyError:
+ # Occurs if domain or service does not exist in data
+ self._message("No host or api_password received.",
+ HTTP_BAD_REQUEST)
+
+ except ValueError:
+ # Occurs during error parsing port
+ self._message(
+ "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
+
+ def _handle_delete_api_event_forward(self, path_match, data):
+ """ Handles deleting an event forwarding target. """
+
+ try:
+ host = data['host'][0]
+
+ port = int(data['port'][0]) if 'port' in data else None
+
+ if self.server.event_forwarder is not None:
+ api = rem.API(host, None, port)
+
+ self.server.event_forwarder.disconnect(api)
+
+ self._message("Event forwarding cancelled.")
+
+ except KeyError:
+ # Occurs if domain or service does not exist in data
+ self._message("No host or api_password received.",
+ HTTP_BAD_REQUEST)
+
+ except ValueError:
+ # Occurs during error parsing port
+ self._message(
+ "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
+
def _handle_get_static(self, path_match, data):
""" Returns a static file. """
req_file = util.sanitize_filename(path_match.group('file'))
@@ -680,4 +755,5 @@ class RequestHandler(BaseHTTPRequestHandler):
if data:
self.wfile.write(
- json.dumps(data, indent=4, sort_keys=True).encode("UTF-8"))
+ json.dumps(data, indent=4, sort_keys=True,
+ cls=rem.JSONEncoder).encode("UTF-8"))
diff --git a/homeassistant/components/httpinterface/www_static/favicon.ico b/homeassistant/components/http/www_static/favicon.ico
similarity index 100%
rename from homeassistant/components/httpinterface/www_static/favicon.ico
rename to homeassistant/components/http/www_static/favicon.ico
diff --git a/homeassistant/components/httpinterface/www_static/style.css b/homeassistant/components/http/www_static/style.css
similarity index 100%
rename from homeassistant/components/httpinterface/www_static/style.css
rename to homeassistant/components/http/www_static/style.css
diff --git a/homeassistant/remote.py b/homeassistant/remote.py
index 26641b113c9..b5f24620508 100644
--- a/homeassistant/remote.py
+++ b/homeassistant/remote.py
@@ -17,24 +17,37 @@ import urllib.parse
import requests
import homeassistant as ha
-import homeassistant.components.httpinterface as hah
+
+SERVER_PORT = 8123
+
+URL_API_STATES = "/api/states"
+URL_API_STATES_ENTITY = "/api/states/{}"
+URL_API_EVENTS = "/api/events"
+URL_API_EVENTS_EVENT = "/api/events/{}"
+URL_API_SERVICES = "/api/services"
+URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
+URL_API_EVENT_FORWARD = "/api/event_forwarding"
METHOD_GET = "get"
METHOD_POST = "post"
-def _setup_call_api(host, port, api_password):
- """ Helper method to setup a call api method. """
- port = port or hah.SERVER_PORT
+class API(object):
+ """ Object to pass around Home Assistant API location and credentials. """
+ # pylint: disable=too-few-public-methods
- base_url = "http://{}:{}".format(host, port)
+ def __init__(self, host, api_password, port=None):
+ self.host = host
+ self.port = port or SERVER_PORT
+ self.api_password = api_password
+ self.base_url = "http://{}:{}".format(host, self.port)
- def _call_api(method, path, data=None):
+ def __call__(self, method, path, data=None):
""" Makes a call to the Home Assistant api. """
data = data or {}
- data['api_password'] = api_password
+ data['api_password'] = self.api_password
- url = urllib.parse.urljoin(base_url, path)
+ url = urllib.parse.urljoin(self.base_url, path)
try:
if method == METHOD_GET:
@@ -46,7 +59,134 @@ def _setup_call_api(host, port, api_password):
logging.getLogger(__name__).exception("Error connecting to server")
raise ha.HomeAssistantError("Error connecting to server")
- return _call_api
+
+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
+ self.remote_api = remote_api
+
+ self._pool = pool = ha.create_worker_pool()
+
+ self.bus = EventBus(remote_api, pool)
+ self.services = ha.ServiceRegistry(self.bus, pool)
+ self.states = StateMachine(self.bus, self.remote_api)
+
+ def start(self):
+ ha.Timer(self)
+
+ # Setup that events from remote_api get forwarded to local_api
+ connect_remote_events(self.remote_api, self.local_api)
+
+ self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
+ origin=ha.EventOrigin.remote)
+
+
+class EventBus(ha.EventBus):
+ """ EventBus implementation that forwards fire_event to remote API. """
+
+ def __init__(self, api, pool=None):
+ super().__init__(pool)
+ self._api = api
+
+ def fire(self, event_type, event_data=None, origin=ha.EventOrigin.local):
+ """ Forward local events to remote target,
+ handles remote event as usual. """
+ # All local events that are not TIME_CHANGED are forwarded to API
+ if origin == ha.EventOrigin.local and \
+ event_type != ha.EVENT_TIME_CHANGED:
+
+ fire_event(self._api, event_type, event_data)
+
+ else:
+ super().fire(event_type, event_data, origin)
+
+
+class EventForwarder(object):
+ """ Listens for events and forwards to specified APIs. """
+
+ def __init__(self, hass, restrict_origin=None):
+ self.hass = hass
+ self.restrict_origin = restrict_origin
+ self.logger = logging.getLogger(__name__)
+
+ # We use a tuple (host, port) as key to ensure
+ # that we do not forward to the same host twice
+ self._targets = {}
+
+ self._lock = threading.Lock()
+
+ def connect(self, api):
+ """
+ Attach to a HA instance and forward events.
+
+ Will overwrite old target if one exists with same host/port.
+ """
+ with self._lock:
+ if len(self._targets) == 0:
+ # First target we get, setup listener for events
+ self.hass.bus.listen(ha.MATCH_ALL, self._event_listener)
+
+ key = (api.host, api.port)
+
+ self._targets[key] = api
+
+ def disconnect(self, api):
+ """ Removes target from being forwarded to. """
+ with self._lock:
+ key = (api.host, api.port)
+
+ did_remove = self._targets.pop(key, None) is None
+
+ if len(self._targets) == 0:
+ # Remove event listener if no forwarding targets present
+ self.hass.bus.remove_listener(ha.MATCH_ALL,
+ self._event_listener)
+
+ return did_remove
+
+ def _event_listener(self, event):
+ """ Listen and forwards all events. """
+ with self._lock:
+ # We don't forward time events or, if enabled, non-local events
+ if event.event_type == ha.EVENT_TIME_CHANGED or \
+ (self.restrict_origin and event.origin != self.restrict_origin):
+ return
+
+ for api in self._targets.values():
+ fire_event(api, event.event_type, event.data, self.logger)
+
+
+class StateMachine(ha.StateMachine):
+ """
+ Fires set events to an API.
+ Uses state_change events to track states.
+ """
+
+ def __init__(self, bus, api):
+ super().__init__(None)
+
+ self.logger = logging.getLogger(__name__)
+
+ self._api = api
+
+ self.mirror()
+
+ bus.listen(ha.EVENT_STATE_CHANGED, self._state_changed_listener)
+
+ def set(self, entity_id, new_state, attributes=None):
+ """ Calls set_state on remote API . """
+ set_state(self._api, entity_id, new_state, attributes)
+
+ def mirror(self):
+ """ Discards current data and mirrors the remote state machine. """
+ self._states = get_states(self._api, self.logger)
+
+ def _state_changed_listener(self, event):
+ """ Listens for state changed events and applies them. """
+ self._states[event.data['entity_id']] = event.data['new_state']
class JSONEncoder(json.JSONEncoder):
@@ -61,212 +201,168 @@ class JSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj)
-class EventBus(object):
- """ Allows to interface with a Home Assistant EventBus via the API. """
+def connect_remote_events(from_api, to_api):
+ """ Sets up from_api to forward all events to to_api. """
- def __init__(self, host, api_password, port=None):
- self.logger = logging.getLogger(__name__)
+ data = {'host': to_api.host, 'api_password': to_api.api_password}
- self._call_api = _setup_call_api(host, port, api_password)
+ if to_api.port is not None:
+ data['port'] = to_api.port
- @property
- def listeners(self):
- """ List of events that is being listened for. """
- try:
- req = self._call_api(METHOD_GET, hah.URL_API_EVENTS)
+ try:
+ from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
- if req.status_code == 200:
- data = req.json()
-
- return data['event_listeners']
-
- else:
- raise ha.HomeAssistantError(
- "Got unexpected result (3): {}.".format(req.text))
-
- except ValueError: # If req.json() can't parse the json
- self.logger.exception("Bus:Got unexpected result")
- raise ha.HomeAssistantError(
- "Got unexpected result: {}".format(req.text))
-
- except KeyError: # If not all expected keys are in the returned JSON
- self.logger.exception("Bus:Got unexpected result (2)")
- raise ha.HomeAssistantError(
- "Got unexpected result (2): {}".format(req.text))
-
- def fire(self, event_type, event_data=None):
- """ Fire an event. """
-
- if event_data:
- data = {'event_data': json.dumps(event_data, cls=JSONEncoder)}
- else:
- data = None
-
- req = self._call_api(METHOD_POST,
- hah.URL_API_EVENTS_EVENT.format(event_type),
- data)
-
- if req.status_code != 200:
- error = "Error firing event: {} - {}".format(
- req.status_code, req.text)
-
- self.logger.error("Bus:{}".format(error))
- raise ha.HomeAssistantError(error)
+ except ha.HomeAssistantError:
+ pass
-class StateMachine(object):
- """ Allows to interface with a Home Assistant StateMachine via the API. """
+def disconnect_remote_events(from_api, to_api):
+ """ Disconnects forwarding events from from_api to to_api. """
+ data = {'host': to_api.host, '_METHOD': 'DELETE'}
- def __init__(self, host, api_password, port=None):
- self._call_api = _setup_call_api(host, port, api_password)
+ if to_api.port is not None:
+ data['port'] = to_api.port
- self.lock = threading.Lock()
- self.logger = logging.getLogger(__name__)
+ try:
+ from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
- @property
- def entity_ids(self):
- """ List of entity ids which states are being tracked. """
-
- try:
- req = self._call_api(METHOD_GET, hah.URL_API_STATES)
-
- return req.json()['entity_ids']
-
- except requests.exceptions.ConnectionError:
- self.logger.exception("StateMachine:Error connecting to server")
- return []
-
- except ValueError: # If req.json() can't parse the json
- self.logger.exception("StateMachine:Got unexpected result")
- return []
-
- except KeyError: # If 'entity_ids' key not in parsed json
- self.logger.exception("StateMachine:Got unexpected result (2)")
- return []
-
- def set(self, entity_id, new_state, attributes=None):
- """ Set the state of a entity, add entity if it does not exist.
-
- Attributes is an optional dict to specify attributes of this state. """
-
- attributes = attributes or {}
-
- self.lock.acquire()
-
- data = {'new_state': new_state,
- 'attributes': json.dumps(attributes)}
-
- try:
- req = self._call_api(METHOD_POST,
- hah.URL_API_STATES_ENTITY.format(entity_id),
- data)
-
- if req.status_code != 201:
- error = "Error changing state: {} - {}".format(
- req.status_code, req.text)
-
- self.logger.error("StateMachine:{}".format(error))
- raise ha.HomeAssistantError(error)
-
- except requests.exceptions.ConnectionError:
- self.logger.exception("StateMachine:Error connecting to server")
- raise ha.HomeAssistantError("Error connecting to server")
-
- finally:
- self.lock.release()
-
- def get(self, entity_id):
- """ Returns the state of the specified entity. """
-
- try:
- req = self._call_api(METHOD_GET,
- hah.URL_API_STATES_ENTITY.format(entity_id))
-
- if req.status_code == 200:
- data = req.json()
-
- return ha.State.from_dict(data)
-
- elif req.status_code == 422:
- # Entity does not exist
- return None
-
- else:
- raise ha.HomeAssistantError(
- "Got unexpected result (3): {}.".format(req.text))
-
- except requests.exceptions.ConnectionError:
- self.logger.exception("StateMachine:Error connecting to server")
- raise ha.HomeAssistantError("Error connecting to server")
-
- except ValueError: # If req.json() can't parse the json
- self.logger.exception("StateMachine:Got unexpected result")
- raise ha.HomeAssistantError(
- "Got unexpected result: {}".format(req.text))
-
- except KeyError: # If not all expected keys are in the returned JSON
- self.logger.exception("StateMachine:Got unexpected result (2)")
- raise ha.HomeAssistantError(
- "Got unexpected result (2): {}".format(req.text))
-
- def is_state(self, entity_id, state):
- """ Returns True if entity exists and is specified state. """
- try:
- return self.get(entity_id).state == state
- except AttributeError:
- # get returned None
- return False
+ except ha.HomeAssistantError:
+ pass
-class ServiceRegistry(object):
- """ Allows to interface with a Home Assistant ServiceRegistry
- via the API. """
+def get_event_listeners(api, logger=None):
+ """ List of events that is being listened for. """
+ try:
+ req = api(METHOD_GET, URL_API_EVENTS)
- def __init__(self, host, api_password, port=None):
- self.logger = logging.getLogger(__name__)
+ return req.json()['event_listeners'] if req.status_code == 200 else {}
- self._call_api = _setup_call_api(host, port, api_password)
+ except (ha.HomeAssistantError, ValueError, KeyError):
+ # ValueError if req.json() can't parse the json
+ # KeyError if 'event_listeners' not found in parsed json
+ if logger:
+ logger.exception("Bus:Got unexpected result")
- @property
- def services(self):
- """ List the available services. """
- try:
- req = self._call_api(METHOD_GET, hah.URL_API_SERVICES)
+ return {}
- if req.status_code == 200:
- data = req.json()
- return data['services']
+def fire_event(api, event_type, event_data=None, logger=None):
+ """ Fire an event at remote API. """
- else:
- raise ha.HomeAssistantError(
- "Got unexpected result (3): {}.".format(req.text))
+ if event_data:
+ data = {'event_data': json.dumps(event_data, cls=JSONEncoder)}
+ else:
+ data = None
- except ValueError: # If req.json() can't parse the json
- self.logger.exception("ServiceRegistry:Got unexpected result")
- raise ha.HomeAssistantError(
- "Got unexpected result: {}".format(req.text))
+ try:
+ req = api(METHOD_POST, URL_API_EVENTS_EVENT.format(event_type), data)
- except KeyError: # If not all expected keys are in the returned JSON
- self.logger.exception("ServiceRegistry:Got unexpected result (2)")
- raise ha.HomeAssistantError(
- "Got unexpected result (2): {}".format(req.text))
+ if req.status_code != 200 and logger:
+ logger.error(
+ "Error firing event: {} - {}".format(
+ req.status_code, req.text))
- def call_service(self, domain, service, service_data=None):
- """ Calls a service. """
+ except ha.HomeAssistantError:
+ pass
- if service_data:
- data = {'service_data': json.dumps(service_data)}
- else:
- data = None
- req = self._call_api(METHOD_POST,
- hah.URL_API_SERVICES_SERVICE.format(
- domain, service),
- data)
+def get_state(api, entity_id, logger=None):
+ """ Queries given API for state of entity_id. """
- if req.status_code != 200:
- error = "Error calling service: {} - {}".format(
- req.status_code, req.text)
+ try:
+ req = api(METHOD_GET,
+ URL_API_STATES_ENTITY.format(entity_id))
- self.logger.error("ServiceRegistry:{}".format(error))
+ # req.status_code == 422 if entity does not exist
+
+ return ha.State.from_dict(req.json()) \
+ if req.status_code == 200 else None
+
+ except (ha.HomeAssistantError, ValueError):
+ # ValueError if req.json() can't parse the json
+ if logger:
+ logger.exception("Error getting state")
+
+ return None
+
+
+def get_states(api, logger=None):
+ """ Queries given API for all states. """
+
+ try:
+ req = api(METHOD_GET,
+ URL_API_STATES)
+
+ json_result = req.json()
+ states = {}
+
+ for entity_id, state_dict in json_result.items():
+ state = ha.State.from_dict(state_dict)
+
+ if state:
+ states[entity_id] = state
+
+ return states
+
+ except (ha.HomeAssistantError, ValueError, AttributeError):
+ # ValueError if req.json() can't parse the json
+ # AttributeError if parsed JSON was not a dict
+ if logger:
+ logger.exception("Error getting state")
+
+ return {}
+
+
+def set_state(api, entity_id, new_state, attributes=None, logger=None):
+ """ Tells API to update state for entity_id. """
+
+ attributes = attributes or {}
+
+ data = {'new_state': new_state,
+ 'attributes': json.dumps(attributes)}
+
+ try:
+ req = api(METHOD_POST,
+ URL_API_STATES_ENTITY.format(entity_id),
+ data)
+
+ if req.status_code != 201 and logger:
+ logger.error(
+ "Error changing state: {} - {}".format(
+ req.status_code, req.text))
+
+ except ha.HomeAssistantError:
+ if logger:
+ logger.exception("Error setting state to server")
+
+
+def is_state(api, entity_id, state, logger=None):
+ """ Queries API to see if entity_id is specified state. """
+ cur_state = get_state(api, entity_id, logger)
+
+ return cur_state and cur_state.state == state
+
+
+def get_services(api, logger=None):
+ """ Returns a dict with per domain the available services at API. """
+ try:
+ req = api(METHOD_GET, URL_API_SERVICES)
+
+ return req.json()['services'] if req.status_code == 200 else {}
+
+ except (ha.HomeAssistantError, ValueError, KeyError):
+ # ValueError if req.json() can't parse the json
+ # KeyError if not all expected keys are in the returned JSON
+ if logger:
+ logger.exception("ServiceRegistry:Got unexpected result")
+
+ return {}
+
+
+def call_service(api, domain, service, service_data=None, logger=None):
+ """ Calls a service at the remote API. """
+ event_data = service_data or {}
+ event_data[ha.ATTR_DOMAIN] = domain
+ event_data[ha.ATTR_SERVICE] = service
+
+ fire_event(api, ha.EVENT_CALL_SERVICE, event_data, logger)
diff --git a/homeassistant/test.py b/homeassistant/test.py
index c193e2df7b2..b61b17f32d2 100644
--- a/homeassistant/test.py
+++ b/homeassistant/test.py
@@ -13,11 +13,11 @@ import requests
import homeassistant as ha
import homeassistant.remote as remote
-import homeassistant.components.httpinterface as hah
+import homeassistant.components.http as http
API_PASSWORD = "test1234"
-HTTP_BASE_URL = "http://127.0.0.1:{}".format(hah.SERVER_PORT)
+HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT)
def _url(path=""):
@@ -28,6 +28,7 @@ def _url(path=""):
class HAHelper(object): # pylint: disable=too-few-public-methods
""" Helper class to keep track of current running HA instance. """
hass = None
+ slave = None
def ensure_homeassistant_started():
@@ -39,9 +40,9 @@ def ensure_homeassistant_started():
hass.bus.listen('test_event', len)
hass.states.set('test', 'a_state')
- hah.HTTPInterface(hass, API_PASSWORD)
+ http.setup(hass, API_PASSWORD)
- hass.bus.fire(ha.EVENT_HOMEASSISTANT_START)
+ hass.start()
# Give objects time to startup
time.sleep(1)
@@ -51,6 +52,26 @@ def ensure_homeassistant_started():
return HAHelper.hass
+def ensure_slave_started():
+ """ Ensure a home assistant slave is started. """
+
+ if not HAHelper.slave:
+ local_api = remote.API("127.0.0.1", API_PASSWORD, 8124)
+ remote_api = remote.API("127.0.0.1", API_PASSWORD)
+ slave = remote.HomeAssistant(local_api, remote_api)
+
+ http.setup(slave, API_PASSWORD, 8124)
+
+ slave.start()
+
+ # Give objects time to startup
+ time.sleep(1)
+
+ HAHelper.slave = slave
+
+ return HAHelper.slave
+
+
# pylint: disable=too-many-public-methods
class TestHTTPInterface(unittest.TestCase):
""" Test the HTTP debug interface and API. """
@@ -75,12 +96,12 @@ class TestHTTPInterface(unittest.TestCase):
""" Test if we get access denied if we omit or provide
a wrong api password. """
req = requests.get(
- _url(hah.URL_API_STATES_ENTITY.format("test")))
+ _url(remote.URL_API_STATES_ENTITY.format("test")))
self.assertEqual(req.status_code, 401)
req = requests.get(
- _url(hah.URL_API_STATES_ENTITY.format("test")),
+ _url(remote.URL_API_STATES_ENTITY.format("test")),
params={"api_password": "not the password"})
self.assertEqual(req.status_code, 401)
@@ -89,7 +110,7 @@ class TestHTTPInterface(unittest.TestCase):
""" Test if we can change a state from the debug interface. """
self.hass.states.set("test.test", "not_to_be_set")
- requests.post(_url(hah.URL_CHANGE_STATE),
+ requests.post(_url(http.URL_CHANGE_STATE),
data={"entity_id": "test.test",
"new_state": "debug_state_change2",
"api_password": API_PASSWORD})
@@ -110,7 +131,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.listen_once_event("test_event_with_data", listener)
requests.post(
- _url(hah.URL_FIRE_EVENT),
+ _url(http.URL_FIRE_EVENT),
data={"event_type": "test_event_with_data",
"event_data": '{"test": 1}',
"api_password": API_PASSWORD})
@@ -122,18 +143,20 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_list_state_entities(self):
""" Test if the debug interface allows us to list state entities. """
- req = requests.get(_url(hah.URL_API_STATES),
+ req = requests.get(_url(remote.URL_API_STATES),
data={"api_password": API_PASSWORD})
- data = req.json()
+ remote_data = req.json()
- self.assertEqual(list(self.hass.states.entity_ids),
- data['entity_ids'])
+ local_data = {entity_id: state.as_dict() for entity_id, state
+ in self.hass.states.all().items()}
+
+ self.assertEqual(local_data, remote_data)
def test_api_get(self):
""" Test if the debug interface allows us to get a state. """
req = requests.get(
- _url(hah.URL_API_STATES_ENTITY.format("test")),
+ _url(remote.URL_API_STATES_ENTITY.format("test")),
data={"api_password": API_PASSWORD})
data = ha.State.from_dict(req.json())
@@ -147,7 +170,7 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_non_existing_state(self):
""" Test if the debug interface allows us to get a state. """
req = requests.get(
- _url(hah.URL_API_STATES_ENTITY.format("does_not_exist")),
+ _url(remote.URL_API_STATES_ENTITY.format("does_not_exist")),
params={"api_password": API_PASSWORD})
self.assertEqual(req.status_code, 422)
@@ -157,7 +180,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.states.set("test.test", "not_to_be_set")
- requests.post(_url(hah.URL_API_STATES_ENTITY.format("test.test")),
+ requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
data={"new_state": "debug_state_change2",
"api_password": API_PASSWORD})
@@ -172,7 +195,7 @@ class TestHTTPInterface(unittest.TestCase):
new_state = "debug_state_change"
req = requests.post(
- _url(hah.URL_API_STATES_ENTITY.format(
+ _url(remote.URL_API_STATES_ENTITY.format(
"test_entity_that_does_not_exist")),
data={"new_state": new_state,
"api_password": API_PASSWORD})
@@ -195,7 +218,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.listen_once_event("test.event_no_data", listener)
requests.post(
- _url(hah.URL_API_EVENTS_EVENT.format("test.event_no_data")),
+ _url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
data={"api_password": API_PASSWORD})
# Allow the event to take place
@@ -217,7 +240,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.listen_once_event("test_event_with_data", listener)
requests.post(
- _url(hah.URL_API_EVENTS_EVENT.format("test_event_with_data")),
+ _url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
data={"event_data": '{"test": 1}',
"api_password": API_PASSWORD})
@@ -238,7 +261,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.listen_once_event("test_event_with_bad_data", listener)
req = requests.post(
- _url(hah.URL_API_EVENTS_EVENT.format("test_event")),
+ _url(remote.URL_API_EVENTS_EVENT.format("test_event")),
data={"event_data": 'not json',
"api_password": API_PASSWORD})
@@ -250,7 +273,7 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_event_listeners(self):
""" Test if we can get the list of events being listened for. """
- req = requests.get(_url(hah.URL_API_EVENTS),
+ req = requests.get(_url(remote.URL_API_EVENTS),
params={"api_password": API_PASSWORD})
data = req.json()
@@ -259,7 +282,7 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_services(self):
""" Test if we can get a dict describing current services. """
- req = requests.get(_url(hah.URL_API_SERVICES),
+ req = requests.get(_url(remote.URL_API_SERVICES),
params={"api_password": API_PASSWORD})
data = req.json()
@@ -277,7 +300,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.services.register("test_domain", "test_service", listener)
requests.post(
- _url(hah.URL_API_SERVICES_SERVICE.format(
+ _url(remote.URL_API_SERVICES_SERVICE.format(
"test_domain", "test_service")),
data={"api_password": API_PASSWORD})
@@ -299,7 +322,7 @@ class TestHTTPInterface(unittest.TestCase):
self.hass.services.register("test_domain", "test_service", listener)
requests.post(
- _url(hah.URL_API_SERVICES_SERVICE.format(
+ _url(remote.URL_API_SERVICES_SERVICE.format(
"test_domain", "test_service")),
data={"service_data": '{"test": 1}',
"api_password": API_PASSWORD})
@@ -310,7 +333,7 @@ class TestHTTPInterface(unittest.TestCase):
self.assertEqual(len(test_value), 1)
-class TestRemote(unittest.TestCase):
+class TestRemoteMethods(unittest.TestCase):
""" Test the homeassistant.remote module. """
@classmethod
@@ -318,134 +341,115 @@ class TestRemote(unittest.TestCase):
""" things to be run when tests are started. """
cls.hass = ensure_homeassistant_started()
- cls.remote_sm = remote.StateMachine("127.0.0.1", API_PASSWORD)
- cls.remote_eb = remote.EventBus("127.0.0.1", API_PASSWORD)
- cls.remote_sr = remote.ServiceRegistry("127.0.0.1", API_PASSWORD)
- cls.sm_with_remote_eb = ha.StateMachine(cls.remote_eb)
- cls.sm_with_remote_eb.set("test", "a_state")
+ cls.api = remote.API("127.0.0.1", API_PASSWORD)
- # pylint: disable=invalid-name
- def test_remote_sm_list_state_entities(self):
- """ Test if the debug interface allows us to list state entity ids. """
+ def test_get_event_listeners(self):
+ """ Test Python API get_event_listeners. """
- self.assertEqual(list(self.hass.states.entity_ids),
- self.remote_sm.entity_ids)
+ self.assertEqual(
+ remote.get_event_listeners(self.api), self.hass.bus.listeners)
- def test_remote_sm_get(self):
- """ Test if debug interface allows us to get state of an entity. """
- remote_state = self.remote_sm.get("test")
-
- state = self.hass.states.get("test")
-
- self.assertEqual(remote_state.state, state.state)
- self.assertEqual(remote_state.last_changed, state.last_changed)
- self.assertEqual(remote_state.attributes, state.attributes)
-
- def test_remote_sm_get_non_existing_state(self):
- """ Test remote state machine to get state of non existing entity. """
- self.assertEqual(self.remote_sm.get("test_does_not_exist"), None)
-
- def test_remote_sm_state_change(self):
- """ Test if we can change the state of an existing entity. """
-
- self.remote_sm.set("test", "set_remotely", {"test": 1})
-
- state = self.hass.states.get("test")
-
- self.assertEqual(state.state, "set_remotely")
- self.assertEqual(state.attributes['test'], 1)
-
- def test_remote_eb_listening_for_same(self):
- """ Test if remote EB correctly reports listener overview. """
- self.assertEqual(self.hass.bus.listeners,
- self.remote_eb.listeners)
-
- # pylint: disable=invalid-name
- def test_remote_eb_fire_event_with_no_data(self):
- """ Test if the remote bus allows us to fire an event. """
+ def test_fire_event(self):
+ """ Test Python API fire_event. """
test_value = []
def listener(event): # pylint: disable=unused-argument
""" Helper method that will verify our event got called. """
test_value.append(1)
- self.hass.listen_once_event("test_event_no_data", listener)
+ self.hass.listen_once_event("test.event_no_data", listener)
- self.remote_eb.fire("test_event_no_data")
+ remote.fire_event(self.api, "test.event_no_data")
# Allow the event to take place
time.sleep(1)
self.assertEqual(len(test_value), 1)
- # pylint: disable=invalid-name
- def test_remote_eb_fire_event_with_data(self):
- """ Test if the remote bus allows us to fire an event. """
- test_value = []
+ def test_get_state(self):
+ """ Test Python API get_state. """
- def listener(event): # pylint: disable=unused-argument
- """ Helper method that will verify our event got called. """
- if event.data["test"] == 1:
- test_value.append(1)
+ self.assertEqual(
+ remote.get_state(self.api, 'test'), self.hass.states.get('test'))
- self.hass.listen_once_event("test_event_with_data", listener)
+ def test_get_states(self):
+ """ Test Python API get_state_entity_ids. """
- self.remote_eb.fire("test_event_with_data", {"test": 1})
+ self.assertEqual(
+ remote.get_states(self.api), self.hass.states.all())
- # Allow the event to take place
- time.sleep(1)
+ def test_set_state(self):
+ """ Test Python API set_state. """
+ remote.set_state(self.api, 'test', 'set_test')
- self.assertEqual(len(test_value), 1)
+ self.assertEqual(self.hass.states.get('test').state, 'set_test')
- # pylint: disable=invalid-name
- def test_remote_sr_call_service_with_no_data(self):
- """ Test if the remote bus allows us to fire a service. """
+ def test_is_state(self):
+ """ Test Python API is_state. """
+
+ self.assertEqual(
+ remote.is_state(self.api, 'test',
+ self.hass.states.get('test').state),
+ True)
+
+ def test_get_services(self):
+ """ Test Python API get_services. """
+
+ self.assertEqual(
+ remote.get_services(self.api), self.hass.services.services)
+
+ def test_call_service(self):
+ """ Test Python API call_service. """
test_value = []
def listener(service_call): # pylint: disable=unused-argument
- """ Helper method that will verify our service got called. """
+ """ Helper method that will verify that our service got called. """
test_value.append(1)
self.hass.services.register("test_domain", "test_service", listener)
- self.remote_sr.call_service("test_domain", "test_service")
-
- # Allow the service call to take place
- time.sleep(1)
-
- self.assertEqual(len(test_value), 1)
-
- # pylint: disable=invalid-name
- def test_remote_sr_call_service_with_data(self):
- """ Test if the remote bus allows us to fire an event. """
- test_value = []
-
- def listener(service_call): # pylint: disable=unused-argument
- """ Helper method that will verify our service got called. """
- if service_call.data["test"] == 1:
- test_value.append(1)
-
- self.hass.services.register("test_domain", "test_service", listener)
-
- self.remote_sr.call_service("test_domain", "test_service", {"test": 1})
+ remote.call_service(self.api, "test_domain", "test_service")
# Allow the event to take place
time.sleep(1)
self.assertEqual(len(test_value), 1)
- def test_local_sm_with_remote_eb(self):
- """ Test if we get the event if we change a state on a
- StateMachine connected to a remote bus. """
+
+class TestRemoteClasses(unittest.TestCase):
+ """ Test the homeassistant.remote module. """
+
+ @classmethod
+ def setUpClass(cls): # pylint: disable=invalid-name
+ """ things to be run when tests are started. """
+ cls.hass = ensure_homeassistant_started()
+ cls.slave = ensure_slave_started()
+
+ def test_statemachine_init(self):
+ """ Tests if remote.StateMachine copies all states on init. """
+ self.assertEqual(self.hass.states.all(), self.slave.states.all())
+
+ def test_statemachine_set(self):
+ """ Tests if setting the state on a slave is recorded. """
+ self.slave.states.set("test", "remote.statemachine test")
+
+ # Allow interaction between 2 instances
+ time.sleep(1)
+
+ self.assertEqual(self.slave.states.get("test").state,
+ "remote.statemachine test")
+
+ def test_eventbus_fire(self):
+ """ Test if events fired from the eventbus get fired. """
test_value = []
def listener(event): # pylint: disable=unused-argument
""" Helper method that will verify our event got called. """
test_value.append(1)
- self.hass.listen_once_event(ha.EVENT_STATE_CHANGED, listener)
+ self.slave.listen_once_event("test.event_no_data", listener)
- self.sm_with_remote_eb.set("test", "local sm with remote eb")
+ self.slave.bus.fire("test.event_no_data")
# Allow the event to take place
time.sleep(1)
diff --git a/start.py b/start.py
index eeb29a27a12..527110856df 100644
--- a/start.py
+++ b/start.py
@@ -3,4 +3,6 @@
import homeassistant
import homeassistant.bootstrap
-homeassistant.bootstrap.from_config_file("home-assistant.conf").start()
+hass = homeassistant.bootstrap.from_config_file("home-assistant.conf")
+hass.start()
+hass.block_till_stopped()