Remote instances are now 100% operational
parent
8e65afa994
commit
50b492c64a
303
README.md
303
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:
|
||||
![screenshot-debug-interface](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-debug-interface.png)
|
||||
|
||||
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**<br>
|
||||
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**<br>
|
||||
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**<br>
|
||||
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<br>
|
||||
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<br>
|
||||
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.<br>
|
||||
parameter: new_state - string<br>
|
||||
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<br>
|
||||
Fires an event with event_type<br>
|
||||
optional parameter: event_data - JSON encoded object
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Event download_file fired."
|
||||
}
|
||||
```
|
||||
|
||||
**/api/services/<domain>/<service>** - POST<br>
|
||||
Calls a service within a specific domain.<br>
|
||||
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)
|
||||
|
||||
![screenshot-android-tasker.jpg](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-android-tasker.png)
|
||||
|
||||
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.
|
||||
|
||||
![screenshot-android-tasker.jpg](https://raw.github.com/balloob/home-assistant/master/docs/architecture.png)
|
||||
![home assistant architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture.png)
|
||||
|
||||
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.
|
||||
|
||||
![home assistant master-slave architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture-remote.png)
|
||||
|
||||
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:
|
||||
![screenshot-debug-interface](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-debug-interface.png)
|
||||
|
||||
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**<br>
|
||||
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**<br>
|
||||
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**<br>
|
||||
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<br>
|
||||
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<br>
|
||||
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.<br>
|
||||
parameter: new_state - string<br>
|
||||
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<br>
|
||||
Fires an event with event_type<br>
|
||||
optional parameter: event_data - JSON encoded object
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Event download_file fired."
|
||||
}
|
||||
```
|
||||
|
||||
**/api/services/<domain>/<service>** - POST<br>
|
||||
Calls a service within a specific domain.<br>
|
||||
optional parameter: service_data - JSON encoded object
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Service keyboard/volume_up called."
|
||||
}
|
||||
```
|
||||
|
||||
**/api/event_forwarding** - POST<br>
|
||||
Setup event forwarding to another Home Assistant instance.<br>
|
||||
parameter: host - string<br>
|
||||
parameter: api_password - string<br>
|
||||
optional parameter: port - int<br>
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Event forwarding setup."
|
||||
}
|
||||
```
|
||||
|
||||
**/api/event_forwarding** - DELETE<br>
|
||||
Cancel event forwarding to another Home Assistant instance.<br>
|
||||
parameter: host - string<br>
|
||||
optional parameter: port - int<br>
|
||||
|
||||
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)
|
||||
|
||||
![screenshot-android-tasker.jpg](https://raw.github.com/balloob/home-assistant/master/docs/screenshot-android-tasker.png)
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 31 KiB |
|
@ -2,7 +2,7 @@
|
|||
latitude=32.87336
|
||||
longitude=-117.22743
|
||||
|
||||
[httpinterface]
|
||||
[http]
|
||||
api_password=mypass
|
||||
|
||||
[light.hue]
|
||||
|
|
|
@ -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 "<Event {}: {}>".format(
|
||||
self.event_type, util.repr_helper(self.data))
|
||||
return "<Event {}[{}]: {}>".format(
|
||||
self.event_type, self.origin.value[0],
|
||||
util.repr_helper(self.data))
|
||||
else:
|
||||
return "<Event {}>".format(self.event_type)
|
||||
return "<Event {}[{}]>".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
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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<entity_id>[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<event_type>[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<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[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<file>[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):
|
|||
"<th>Attributes</th><th>Last Changed</th>"
|
||||
"</tr>").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 = "<br>".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/<event_type>
|
||||
|
||||
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"))
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue