API updated to be RESTful

This commit is contained in:
Paulus Schoutsen 2013-10-29 00:22:38 -07:00
parent 102690a770
commit f9d712d175
7 changed files with 495 additions and 437 deletions

View file

@ -34,47 +34,54 @@ A screenshot of the debug interface (battery and charging states are controlled
To interface with the API requests should include the parameter api_password which matches the api_password in home-assistant.conf.
The following API commands are currently supported:
All API calls have to be accompanied by an 'api_password' parameter and will
return JSON. If successful calls will return status code 200 or 201.
/api/state/categories - POST
parameter: api_password - string
Will list all the categories for which a state is currently tracked. Returns a json object like this:
Other status codes that can occur are:
- 400 (Bad Request)
- 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
```json
{"status": "OK",
"message":"State categories",
"categories": ["all_devices", "Paulus_Nexus_4"]}
```
The api supports the following actions:
/api/state/get - POST
parameter: api_password - string
parameter: category - string
Will get the current state of a category. Returns a json object like this:
`/api/states` - GET
Returns a list of categories for which a state is available
Example result:
```json{
"categories": [
"Paulus_Nexus_4",
"weather.sun",
"all_devices"
]
}```
```json
{"status": "OK",
"message": "State of all_devices",
"category": "all_devices",
"state": "device_home",
"last_changed": "19:10:39 25-10-2013",
"attributes": {}}
```
`/api/states/<category>` - GET
Returns the current state from a category
Example result:
```json{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"category": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}```
/api/state/change - POST
parameter: api_password - string
parameter: category - string
parameter: new_state - string
parameter: attributes - object encoded as JSON string (optional)
Changes category 'category' to 'new_state'
It is possible to sent multiple values for category and new_state.
If the number of values for category and new_state do not match only
combinations where both values are supplied will be set.
/api/event/fire - POST
parameter: api_password - string
parameter: event_name - string
parameter: event_data - object encoded as JSON string (optional)
Fires an 'event_name' event containing data from 'event_data'
`/api/states/<category>` - POST
Updates the current state of a category. Returns status code 201 if successful
with location header of updated resource.
parameter: new_state - string
optional parameter: attributes - JSON encoded object
`/api/events/<event_type>` - POST
Fires an event with event_type
optional parameter: event_data - JSON encoded object
Example result:
```json{
"message": "Event download_file fired."
}```
Android remote control
----------------------

Binary file not shown.

View file

@ -1,7 +1,8 @@
<TaskerData sr="" dvi="1" tv="4.1u3m">
<Profile sr="prof24" ve="2">
<cdate>1381116787665</cdate>
<edate>1381116787665</edate>
<clp>true</clp>
<edate>1382062270688</edate>
<id>24</id>
<mid0>20</mid0>
<Event sr="con0" ve="2">
@ -11,8 +12,7 @@
</Profile>
<Profile sr="prof25" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1381001553706</edate>
<edate>1382769497429</edate>
<id>25</id>
<mid0>23</mid0>
<mid1>20</mid1>
@ -26,7 +26,7 @@
<Profile sr="prof26" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1381110280839</edate>
<edate>1383003483161</edate>
<id>26</id>
<mid0>22</mid0>
<mid1>20</mid1>
@ -37,13 +37,27 @@
<Int sr="arg0" val="3"/>
</State>
</Profile>
<Profile sr="prof3" ve="2">
<cdate>1380613730755</cdate>
<clp>true</clp>
<edate>1383003498566</edate>
<id>3</id>
<mid0>10</mid0>
<mid1>20</mid1>
<nme>HA Power AC</nme>
<pri>10</pri>
<State sr="con0">
<code>10</code>
<Int sr="arg0" val="1"/>
</State>
</Profile>
<Profile sr="prof5" ve="2">
<cdate>1380496514959</cdate>
<cldm>1500</cldm>
<clp>true</clp>
<edate>1381110261999</edate>
<edate>1382769618501</edate>
<id>5</id>
<mid0>7</mid0>
<mid0>19</mid0>
<nme>HA Battery Changed</nme>
<Event sr="con0" ve="2">
<code>203</code>
@ -53,14 +67,14 @@
<Project sr="proj0">
<cdate>1381110247781</cdate>
<name>Home Assistant</name>
<pids>24,26,5,25</pids>
<pids>5,3,25,26,24</pids>
<scenes>Variable Query,Home Assistant Start</scenes>
<tids>14,16,4,15,7,20,6,8,22,23,9,11,12,13</tids>
<tids>19,8,10,6,16,9,20,14,11,4,23,15,12,13,22</tids>
<Kid sr="Kid">
<launchID>12</launchID>
<pkg>nl.paulus.homeassistant</pkg>
<vnme>1.0</vnme>
<vnum>10</vnum>
<vnme>1.1</vnme>
<vnum>14</vnum>
</Kid>
<Img sr="icon" ve="2">
<nme>cust_animal_penguin</nme>
@ -69,7 +83,7 @@
<Scene sr="sceneHome Assistant Start">
<backColour>-637534208</backColour>
<cdate>1381113309678</cdate>
<edate>1381118413367</edate>
<edate>1381162068611</edate>
<heightLand>-1</heightLand>
<heightPort>688</heightPort>
<nme>Home Assistant Start</nme>
@ -308,9 +322,24 @@
<Int sr="arg2" val="255"/>
</ImageElement>
</Scene>
<Task sr="task10">
<cdate>1380613530339</cdate>
<edate>1383030846230</edate>
<id>10</id>
<nme>Charging AC</nme>
<Action sr="act0" ve="3">
<code>130</code>
<Str sr="arg0" ve="3">Update Charging</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3">ac</Str>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
</Action>
</Task>
<Task sr="task11">
<cdate>1381110672417</cdate>
<edate>1381116046765</edate>
<edate>1383030844501</edate>
<id>11</id>
<nme>Open Debug Interface</nme>
<pri>10</pri>
@ -321,7 +350,7 @@
</Task>
<Task sr="task12">
<cdate>1381113015963</cdate>
<edate>1381116866174</edate>
<edate>1383030888271</edate>
<id>12</id>
<nme>Start Screen</nme>
<pri>10</pri>
@ -338,6 +367,9 @@
<code>49</code>
<Str sr="arg0" ve="3">Home Assistant Start</Str>
</Action>
<Img sr="icn" ve="2">
<nme>hd_aaa_ext_tiles_small</nme>
</Img>
</Task>
<Task sr="task13">
<cdate>1381114398467</cdate>
@ -354,16 +386,15 @@
</Task>
<Task sr="task14">
<cdate>1381114829583</cdate>
<edate>1381115098684</edate>
<edate>1383030731979</edate>
<id>14</id>
<nme>API Fire Event</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/event/fire</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
event_name=%par1</Str>
<Str sr="arg1" ve="3">/api/events/%par1</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
@ -372,7 +403,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task15">
<cdate>1380262442154</cdate>
<edate>1381115642332</edate>
<edate>1383030894445</edate>
<id>15</id>
<nme>Light On</nme>
<pri>10</pri>
@ -391,7 +422,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task16">
<cdate>1380262442154</cdate>
<edate>1381115613658</edate>
<edate>1383030896170</edate>
<id>16</id>
<nme>Start Epic Sax</nme>
<pri>10</pri>
@ -408,9 +439,29 @@ event_name=%par1</Str>
<nme>hd_aaa_ext_guitar</nme>
</Img>
</Task>
<Task sr="task19">
<cdate>1380262442154</cdate>
<edate>1383030903842</edate>
<id>19</id>
<nme>Update Battery</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/state/change</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
category=%HA_DEVICE_NAME.charging
new_state=%HA_CHARGING
attributes={"battery":%BATT}</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
</Task>
<Task sr="task20">
<cdate>1380613530339</cdate>
<edate>1381116102459</edate>
<edate>1383030848142</edate>
<id>20</id>
<nme>Charging None</nme>
<Action sr="act0" ve="3">
@ -425,7 +476,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task22">
<cdate>1380613530339</cdate>
<edate>1381116000403</edate>
<edate>1383030909347</edate>
<id>22</id>
<nme>Charging Wireless</nme>
<Action sr="act0" ve="3">
@ -440,7 +491,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task23">
<cdate>1380613530339</cdate>
<edate>1381115997137</edate>
<edate>1383030849758</edate>
<id>23</id>
<nme>Charging USB</nme>
<Action sr="act0" ve="3">
@ -455,7 +506,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task4">
<cdate>1380262442154</cdate>
<edate>1381115633261</edate>
<edate>1383030892718</edate>
<id>4</id>
<nme>Light Off</nme>
<pri>10</pri>
@ -474,7 +525,7 @@ event_name=%par1</Str>
</Task>
<Task sr="task6">
<cdate>1380522560890</cdate>
<edate>1381117976853</edate>
<edate>1383030900554</edate>
<id>6</id>
<nme>Setup</nme>
<pri>10</pri>
@ -580,28 +631,9 @@ event_name=%par1</Str>
<nme>hd_ab_action_settings</nme>
</Img>
</Task>
<Task sr="task7">
<cdate>1380262442154</cdate>
<edate>1381111978825</edate>
<id>7</id>
<nme>Update Battery</nme>
<pri>10</pri>
<Action sr="act0" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/state/change</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
category=%HA_DEVICE_NAME.battery
new_state=%BATT</Str>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
</Task>
<Task sr="task8">
<cdate>1380262442154</cdate>
<edate>1381115955507</edate>
<edate>1383030906782</edate>
<id>8</id>
<nme>Update Charging</nme>
<pri>10</pri>
@ -613,23 +645,18 @@ new_state=%BATT</Str>
<Int sr="arg3" val="0"/>
</Action>
<Action sr="act1" ve="3">
<code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/state/change</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD
category=%HA_DEVICE_NAME.charging
new_state=%HA_CHARGING
category=%HA_DEVICE_NAME.battery
new_state=%BATT</Str>
<code>130</code>
<Str sr="arg0" ve="3">Update Battery</Str>
<Int sr="arg1" val="0"/>
<Int sr="arg2" val="5"/>
<Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/>
<Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action>
</Task>
<Task sr="task9">
<cdate>1380262442154</cdate>
<edate>1381115659673</edate>
<edate>1383030890674</edate>
<id>9</id>
<nme>Start Fireplace</nme>
<pri>10</pri>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -4,31 +4,63 @@ homeassistant.httpinterface
This module provides an API and a HTTP interface for debug purposes.
By default it will run on port 8080.
By default it will run on port 8123.
All API calls have to be accompanied by an 'api_password' parameter.
All API calls have to be accompanied by an 'api_password' parameter and will
return JSON. 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/state/change - POST
parameter: category - string
parameter: new_state - string
Changes category 'category' to 'new_state'
It is possible to sent multiple values for category and new_state.
If the number of values for category and new_state do not match only
combinations where both values are supplied will be set.
/api/states - GET
Returns a list of categories for which a state is available
Example result:
{
"categories": [
"Paulus_Nexus_4",
"weather.sun",
"all_devices"
]
}
/api/event/fire - POST
parameter: event_name - string
parameter: event_data - JSON-string (optional)
Fires an 'event_name' event containing data from 'event_data'
/api/states/<category> - GET
Returns the current state from a category
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"category": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/states/<category> - POST
Updates the current state of a category. Returns status code 201 if successful
with location header of updated resource.
parameter: new_state - string
optional parameter: attributes - JSON encoded object
/api/events/<event_type> - POST
Fires an event with event_type
optional parameter: event_data - JSON encoded object
Example result:
{
"message": "Event download_file fired."
}
"""
import json
import threading
import itertools
import logging
import re
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs
@ -36,9 +68,24 @@ import homeassistant as ha
SERVER_PORT = 8123
MESSAGE_STATUS_OK = "OK"
MESSAGE_STATUS_ERROR = "ERROR"
MESSAGE_STATUS_UNAUTHORIZED = "UNAUTHORIZED"
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_MOVED_PERMANENTLY = 301
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_NOT_FOUND = 404
HTTP_METHOD_NOT_ALLOWED = 405
URL_ROOT = "/"
URL_STATES_CATEGORY = "/states/{}"
URL_API_STATES = "/api/states"
URL_API_STATES_CATEGORY = "/api/states/{}"
URL_EVENTS_EVENT = "/events/{}"
URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}"
class HTTPInterface(threading.Thread):
""" Provides an HTTP interface for Home Assistant. """
@ -76,238 +123,110 @@ class HTTPInterface(threading.Thread):
class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """
#Handler for the GET requests
def do_GET(self): # pylint: disable=invalid-name
""" Handle incoming GET requests. """
write = lambda txt: self.wfile.write(txt+"\n")
PATHS = [ ('GET', '/', '_handle_get_root'),
# /states
('GET', '/states', '_handle_get_states'),
('GET', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'),
'_handle_get_states_category'),
('POST', re.compile(r'/states/(?P<category>[a-zA-Z\.\_0-9]+)'),
'_handle_post_states_category'),
# /events
('POST', re.compile(r'/events/(?P<event_type>\w+)'),
'_handle_post_events_event_type')
]
def _handle_request(self, method): # pylint: disable=too-many-branches
""" Does some common checks and calls appropriate method. """
url = urlparse(self.path)
get_data = parse_qs(url.query)
# Read query input
data = parse_qs(url.query)
api_password = get_data.get('api_password', [''])[0]
# Did we get post input ?
content_length = int(self.headers.get('Content-Length', 0))
if url.path == "/":
if self._verify_api_password(api_password, False):
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
if content_length:
data.update(parse_qs(self.rfile.read(content_length)))
try:
api_password = data['api_password'][0]
except KeyError:
api_password = ''
write(("<html>"
"<head><title>Home Assistant</title></head>"
"<body>"))
# Flash message support
if self.server.flash_message:
write("<h3>{}</h3>".format(self.server.flash_message))
self.server.flash_message = None
# Describe state machine:
categories = []
write(("<table><tr>"
"<th>Name</th><th>State</th>"
"<th>Last Changed</th><th>Attributes</th></tr>"))
for category in \
sorted(self.server.statemachine.categories,
key=lambda key: key.lower()):
categories.append(category)
state = self.server.statemachine.get_state(category)
attributes = "<br>".join(
["{}: {}".format(attr, state['attributes'][attr])
for attr in state['attributes']])
write(("<tr>"
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
"</tr>").
format(category,
state['state'],
state['last_changed'],
attributes))
write("</table>")
# Small form to change the state
write(("<br />Change state:<br />"
"<form action='state/change' method='POST'>"))
write("<input type='hidden' name='api_password' value='{}' />".
format(self.server.api_password))
write("<select name='category'>")
for category in categories:
write("<option>{}</option>".format(category))
write("</select>")
write(("<input name='new_state' />"
"<input type='submit' value='set state' />"
"</form>"))
# Describe event bus:
write(("<table><tr><th>Event</th><th>Listeners</th></tr>"))
for category in sorted(self.server.eventbus.listeners,
key=lambda key: key.lower()):
write("<tr><td>{}</td><td>{}</td></tr>".
format(category,
len(self.server.eventbus.listeners[category])))
# Form to allow firing events
write(("</table><br />"
"<form action='event/fire' method='POST'>"))
write("<input type='hidden' name='api_password' value='{}' />".
format(self.server.api_password))
write(("Event name: <input name='event_name' /><br />"
"Event data (json): <input name='event_data' /><br />"
"<input type='submit' value='fire event' />"
"</form>"))
write("</body></html>")
# We respond to API requests with JSON
# For other requests we respond with html
if url.path.startswith('/api/'):
path = url.path[4:]
# pylint: disable=attribute-defined-outside-init
self.use_json = True
else:
self.send_response(404)
path = url.path
# pylint: disable=attribute-defined-outside-init
self.use_json = False
# pylint: disable=invalid-name, too-many-branches, too-many-statements
def do_POST(self):
""" Handle incoming POST requests. """
length = int(self.headers['Content-Length'])
post_data = parse_qs(self.rfile.read(length))
path_matched_but_not_method = False
handle_request_method = False
if self.path.startswith('/api/'):
action = self.path[5:]
use_json = True
# Check every url to find matching result
for t_method, t_path, t_handler in RequestHandler.PATHS:
# we either do string-comparison or regular expression matching
if isinstance(t_path, str):
path_match = path == t_path
else:
path_match = t_path.match(path) #pylint:disable=maybe-no-member
if path_match and method == t_method:
# Call the method
handle_request_method = getattr(self, t_handler)
break
elif path_match:
path_matched_but_not_method = True
if handle_request_method:
if self._verify_api_password(api_password):
handle_request_method(path_match, data)
elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED)
else:
action = self.path[1:]
use_json = False
given_api_password = post_data.get("api_password", [''])[0]
# Action to change the state
if action == "state/categories":
if self._verify_api_password(given_api_password, use_json):
self._response(use_json, "State categories",
json_data=
{'categories': self.server.statemachine.categories})
elif action == "state/get":
if self._verify_api_password(given_api_password, use_json):
try:
category = post_data['category'][0]
state = self.server.statemachine.get_state(category)
state['category'] = category
self._response(use_json, "State of {}".format(category),
json_data=state)
self.send_response(HTTP_NOT_FOUND)
except KeyError:
# If category or new_state don't exist in post data
self._response(use_json, "Invalid state received.",
MESSAGE_STATUS_ERROR)
def do_GET(self): # pylint: disable=invalid-name
""" GET request handler. """
self._handle_request('GET')
elif action == "state/change":
if self._verify_api_password(given_api_password, use_json):
try:
changed = []
def do_POST(self): # pylint: disable=invalid-name
""" POST request handler. """
self._handle_request('POST')
for idx, category, new_state in zip(itertools.count(),
post_data['category'],
post_data['new_state']
):
# See if we also received attributes for this state
try:
attributes = json.loads(
post_data['attributes'][idx])
except KeyError:
# Happens if key 'attributes' or idx does not exist
attributes = None
self.server.statemachine.set_state(category,
new_state,
attributes)
changed.append("{}={}".format(category, new_state))
self._response(use_json, "States changed: {}".
format( ", ".join(changed) ) )
except KeyError:
# If category or new_state don't exist in post data
self._response(use_json, "Invalid parameters received.",
MESSAGE_STATUS_ERROR)
except ValueError:
# If json.loads doesn't understand the attributes
self._response(use_json, "Invalid state data received.",
MESSAGE_STATUS_ERROR)
# Action to fire an event
elif action == "event/fire":
if self._verify_api_password(given_api_password, use_json):
try:
event_name = post_data['event_name'][0]
if (not 'event_data' in post_data or
post_data['event_data'][0] == ""):
event_data = None
else:
event_data = json.loads(post_data['event_data'][0])
self.server.eventbus.fire(event_name, event_data)
self._response(use_json, "Event {} fired.".
format(event_name))
except ValueError:
# If JSON decode error
self._response(use_json, "Invalid event received (1).",
MESSAGE_STATUS_ERROR)
except KeyError:
# If "event_name" not in post_data
self._response(use_json, "Invalid event received (2).",
MESSAGE_STATUS_ERROR)
else:
self.send_response(404)
def _verify_api_password(self, api_password, use_json):
def _verify_api_password(self, api_password):
""" Helper method to verify the API password
and take action if incorrect. """
if api_password == self.server.api_password:
return True
elif use_json:
self._response(True, "API password missing or incorrect.",
MESSAGE_STATUS_UNAUTHORIZED)
elif self.use_json:
self._message("API password missing or incorrect.",
HTTP_UNAUTHORIZED)
else:
self.send_response(200)
self.send_response(HTTP_OK)
self.send_header('Content-type','text/html')
self.end_headers()
write = lambda txt: self.wfile.write(txt+"\n")
write(("<html>"
self.wfile.write((
"<html>"
"<head><title>Home Assistant</title></head>"
"<body>"
"<form action='/' method='GET'>"
@ -318,35 +237,164 @@ class RequestHandler(BaseHTTPRequestHandler):
return False
def _response(self, use_json, message,
status=MESSAGE_STATUS_OK, json_data=None):
""" Helper method to show a message to the user. """
log_message = "{}: {}".format(status, message)
# pylint: disable=unused-argument
def _handle_get_root(self, path_match, data):
""" Renders the debug interface. """
if status == MESSAGE_STATUS_OK:
self.server.logger.info(log_message)
response_code = 200
write = lambda txt: self.wfile.write(txt+"\n")
self.send_response(HTTP_OK)
self.send_header('Content-type','text/html')
self.end_headers()
write(("<html>"
"<head><title>Home Assistant</title></head>"
"<body>"))
# Flash message support
if self.server.flash_message:
write("<h3>{}</h3>".format(self.server.flash_message))
self.server.flash_message = None
# Describe state machine:
categories = []
write(("<table><tr>"
"<th>Name</th><th>State</th>"
"<th>Last Changed</th><th>Attributes</th></tr>"))
for category in \
sorted(self.server.statemachine.categories,
key=lambda key: key.lower()):
categories.append(category)
state = self.server.statemachine.get_state(category)
attributes = "<br>".join(
["{}: {}".format(attr, state['attributes'][attr])
for attr in state['attributes']])
write(("<tr>"
"<td>{}</td><td>{}</td><td>{}</td><td>{}</td>"
"</tr>").
format(category,
state['state'],
state['last_changed'],
attributes))
write("</table>")
# Describe event bus:
write(("<table><tr><th>Event</th><th>Listeners</th></tr>"))
for category in sorted(self.server.eventbus.listeners,
key=lambda key: key.lower()):
write("<tr><td>{}</td><td>{}</td></tr>".
format(category,
len(self.server.eventbus.listeners[category])))
# Form to allow firing events
write("</table>")
write("</body></html>")
# pylint: disable=unused-argument
def _handle_get_states(self, path_match, data):
""" Returns the categories which state is being tracked. """
self._write_json({'categories': self.server.statemachine.categories})
# pylint: disable=unused-argument
def _handle_get_states_category(self, path_match, data):
""" Returns the state of a specific category. """
try:
category = path_match.group('category')
state = self.server.statemachine.get_state(category)
state['category'] = category
self._write_json(state)
except KeyError:
# If category or new_state don't exist in post data
self._message("Invalid state received.", HTTP_BAD_REQUEST)
def _handle_post_states_category(self, path_match, data):
""" Handles updating the state of a category. """
try:
category = path_match.group('category')
new_state = data['new_state'][0]
try:
attributes = json.loads(data['attributes'][0])
except KeyError:
# Happens if key 'attributes' does not exist
attributes = None
self.server.statemachine.set_state(category,
new_state,
attributes)
self._redirect("/states/{}".format(category),
"State changed: {}={}".format(category, new_state),
HTTP_CREATED)
except KeyError:
# If category or new_state don't exist in post data
self._message("Invalid parameters received.",
HTTP_BAD_REQUEST)
except ValueError:
# Occurs during error parsing json
self._message("Invalid JSON for attributes", HTTP_BAD_REQUEST)
def _handle_post_events_event_type(self, path_match, data):
""" Handles firing of an event. """
event_type = path_match.group('event_type')
try:
try:
event_data = json.loads(data['event_data'][0])
except KeyError:
# Happens if key 'event_data' does not exist
event_data = None
self.server.eventbus.fire(event_type, event_data)
self._message("Event {} fired.".format(event_type))
except ValueError:
# Occurs during error parsing json
self._message("Invalid JSON for event_data", HTTP_BAD_REQUEST)
def _message(self, message, status_code=HTTP_OK):
""" Helper method to return a message to the caller. """
if self.use_json:
self._write_json({'message': message}, status_code=status_code)
else:
self.server.logger.error(log_message)
response_code = (401 if status == MESSAGE_STATUS_UNAUTHORIZED
else 400)
self._redirect('/', message)
if use_json:
self.send_response(response_code)
self.send_header('Content-type','application/json')
self.end_headers()
json_data = json_data or {}
json_data['status'] = status
json_data['message'] = message
self.wfile.write(json.dumps(json_data))
else:
def _redirect(self, location, message=None,
status_code=HTTP_MOVED_PERMANENTLY):
""" Helper method to redirect caller. """
# Only save as flash message if we will go to debug interface next
if not self.use_json and message:
self.server.flash_message = message
self.send_response(301)
self.send_header("Location", "/?api_password={}".
format(self.server.api_password))
self.end_headers()
self.send_response(status_code)
self.send_header("Location", "{}?api_password={}".
format(location, self.server.api_password))
self.end_headers()
def _write_json(self, data=None, status_code=HTTP_OK):
""" Helper method to return JSON to the caller. """
self.send_response(status_code)
self.send_header('Content-type','application/json')
self.end_headers()
if data:
self.wfile.write(json.dumps(data, indent=4, sort_keys=True))

View file

@ -12,25 +12,33 @@ HomeAssistantException will be raised.
import threading
import logging
import json
import urlparse
import requests
import homeassistant as ha
import homeassistant.httpinterface as httpinterface
import homeassistant.httpinterface as hah
def _setup_call_api(host, port, base_path, api_password):
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 httpinterface.SERVER_PORT
port = port or hah.SERVER_PORT
base_url = "http://{}:{}/api/{}".format(host, port, base_path)
base_url = "http://{}:{}".format(host, port)
def _call_api(action, data=None):
def _call_api(method, path, data=None):
""" Makes a call to the Home Assistant api. """
data = data or {}
data['api_password'] = api_password
return requests.post(base_url + action, data=data)
url = urlparse.urljoin(base_url, path)
if method == METHOD_GET:
return requests.get(url, params=data)
else:
return requests.request(method, url, data=data)
return _call_api
@ -43,21 +51,19 @@ class EventBus(ha.EventBus):
def __init__(self, host, api_password, port=None):
ha.EventBus.__init__(self)
self._call_api = _setup_call_api(host, port, "event/", api_password)
self._call_api = _setup_call_api(host, port, api_password)
self.logger = logging.getLogger(__name__)
def fire(self, event_type, event_data=None):
""" Fire an event. """
if not event_data:
event_data = {}
data = {'event_name': event_type,
'event_data': json.dumps(event_data)}
data = {'event_data': json.dumps(event_data)} if event_data else None
try:
req = self._call_api("fire", data)
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(
@ -66,7 +72,6 @@ class EventBus(ha.EventBus):
self.logger.error("EventBus:{}".format(error))
raise ha.HomeAssistantException(error)
except requests.exceptions.ConnectionError:
self.logger.exception("EventBus:Error connecting to server")
@ -91,7 +96,7 @@ class StateMachine(ha.StateMachine):
def __init__(self, host, api_password, port=None):
ha.StateMachine.__init__(self, None)
self._call_api = _setup_call_api(host, port, "state/", api_password)
self._call_api = _setup_call_api(host, port, api_password)
self.lock = threading.Lock()
self.logger = logging.getLogger(__name__)
@ -101,7 +106,7 @@ class StateMachine(ha.StateMachine):
""" List of categories which states are being tracked. """
try:
req = self._call_api("categories")
req = self._call_api(METHOD_GET, hah.URL_API_STATES)
return req.json()['categories']
@ -126,14 +131,15 @@ class StateMachine(ha.StateMachine):
self.lock.acquire()
data = {'category': category,
'new_state': new_state,
data = {'new_state': new_state,
'attributes': json.dumps(attributes)}
try:
req = self._call_api('change', data)
req = self._call_api(METHOD_POST,
hah.URL_API_STATES_CATEGORY.format(category),
data)
if req.status_code != 200:
if req.status_code != 201:
error = "Error changing state: {} - {}".format(
req.status_code, req.text)
@ -152,7 +158,8 @@ class StateMachine(ha.StateMachine):
the state of the specified category. """
try:
req = self._call_api("get", {'category': category})
req = self._call_api(METHOD_GET,
hah.URL_API_STATES_CATEGORY.format(category))
data = req.json()

View file

@ -13,13 +13,13 @@ import requests
import homeassistant as ha
import homeassistant.remote as remote
import homeassistant.httpinterface as httpinterface
import homeassistant.httpinterface as hah
API_PASSWORD = "test1234"
HTTP_BASE_URL = "http://127.0.0.1:{}".format(httpinterface.SERVER_PORT)
HTTP_BASE_URL = "http://127.0.0.1:{}".format(hah.SERVER_PORT)
# pylint: disable=too-many-public-methods
class TestHTTPInterface(unittest.TestCase):
@ -27,13 +27,16 @@ class TestHTTPInterface(unittest.TestCase):
HTTP_init = False
def _url(self, path=""):
""" Helper method to generate urls. """
return HTTP_BASE_URL + path
def setUp(self): # pylint: disable=invalid-name
""" Initialize the HTTP interface if not started yet. """
if not TestHTTPInterface.HTTP_init:
TestHTTPInterface.HTTP_init = True
httpinterface.HTTPInterface(self.eventbus, self.statemachine,
API_PASSWORD)
hah.HTTPInterface(self.eventbus, self.statemachine, API_PASSWORD)
self.statemachine.set_state("test", "INIT_STATE")
self.sm_with_remote_eb.set_state("test", "INIT_STATE")
@ -55,17 +58,21 @@ class TestHTTPInterface(unittest.TestCase):
def test_debug_interface(self):
""" Test if we can login by comparing not logged in screen to
logged in screen. """
self.assertNotEqual(requests.get(HTTP_BASE_URL).text,
requests.get("{}/?api_password={}".format(
HTTP_BASE_URL, API_PASSWORD)).text)
with_pw = requests.get(
self._url("/?api_password={}".format(API_PASSWORD)))
without_pw = requests.get(self._url())
self.assertNotEqual(without_pw.text, with_pw.text)
def test_debug_state_change(self):
""" Test if the debug interface allows us to change a state. """
requests.post("{}/state/change".format(HTTP_BASE_URL),
data={"category":"test",
"new_state":"debug_state_change",
"api_password":API_PASSWORD})
requests.post(
self._url(hah.URL_STATES_CATEGORY.format("test")),
data={"new_state":"debug_state_change",
"api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test")['state'],
"debug_state_change")
@ -74,19 +81,21 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_password(self):
""" Test if we get access denied if we omit or provide
a wrong api password. """
req = requests.post("{}/api/state/change".format(HTTP_BASE_URL))
req = requests.post(
self._url(hah.URL_API_STATES_CATEGORY.format("test")))
self.assertEqual(req.status_code, 401)
req = requests.post("{}/api/state/change".format(HTTP_BASE_URL,
data={"api_password":"not the password"}))
req = requests.post(
self._url(hah.URL_API_STATES_CATEGORY.format("test")),
data={"api_password":"not the password"})
self.assertEqual(req.status_code, 401)
def test_api_list_state_categories(self):
""" Test if the debug interface allows us to list state categories. """
req = requests.post("{}/api/state/categories".format(HTTP_BASE_URL),
req = requests.get(self._url(hah.URL_API_STATES),
data={"api_password":API_PASSWORD})
data = req.json()
@ -96,16 +105,15 @@ class TestHTTPInterface(unittest.TestCase):
def test_api_get_state(self):
""" Test if the debug interface allows us to list state categories. """
req = requests.post("{}/api/state/get".format(HTTP_BASE_URL),
data={"api_password":API_PASSWORD,
"category": "test"})
""" Test if the debug interface allows us to get a state. """
req = requests.get(
self._url(hah.URL_API_STATES_CATEGORY.format("test")),
data={"api_password":API_PASSWORD})
data = req.json()
state = self.statemachine.get_state("test")
self.assertEqual(data['category'], "test")
self.assertEqual(data['state'], state['state'])
self.assertEqual(data['last_changed'], state['last_changed'])
@ -117,9 +125,8 @@ class TestHTTPInterface(unittest.TestCase):
self.statemachine.set_state("test", "not_to_be_set_state")
requests.post("{}/api/state/change".format(HTTP_BASE_URL),
data={"category":"test",
"new_state":"debug_state_change2",
requests.post(self._url(hah.URL_API_STATES_CATEGORY.format("test")),
data={"new_state":"debug_state_change2",
"api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test")['state'],
@ -156,22 +163,6 @@ class TestHTTPInterface(unittest.TestCase):
self.assertEqual(state['attributes']['test'], 1)
def test_api_multiple_state_change(self):
""" Test if we can change multiple states in 1 request. """
self.statemachine.set_state("test", "not_to_be_set_state")
self.statemachine.set_state("test2", "not_to_be_set_state")
requests.post("{}/api/state/change".format(HTTP_BASE_URL),
data={"category": ["test", "test2"],
"new_state": ["test_state_1", "test_state_2"],
"api_password":API_PASSWORD})
self.assertEqual(self.statemachine.get_state("test")['state'],
"test_state_1")
self.assertEqual(self.statemachine.get_state("test2")['state'],
"test_state_2")
# pylint: disable=invalid-name
def test_api_state_change_of_non_existing_category(self):
""" Test if the API allows us to change a state of
@ -179,15 +170,16 @@ class TestHTTPInterface(unittest.TestCase):
new_state = "debug_state_change"
req = requests.post("{}/api/state/change".format(HTTP_BASE_URL),
data={"category":"test_category_that_does_not_exist",
"new_state":new_state,
"api_password":API_PASSWORD})
req = requests.post(
self._url(hah.URL_API_STATES_CATEGORY.format(
"test_category_that_does_not_exist")),
data={"new_state": new_state,
"api_password": API_PASSWORD})
cur_state = (self.statemachine.
get_state("test_category_that_does_not_exist")['state'])
get_state("test_category_that_does_not_exist")['state'])
self.assertEqual(req.status_code, 200)
self.assertEqual(req.status_code, 201)
self.assertEqual(cur_state, new_state)
# pylint: disable=invalid-name
@ -201,10 +193,9 @@ class TestHTTPInterface(unittest.TestCase):
self.eventbus.listen_once("test_event_no_data", listener)
requests.post("{}/api/event/fire".format(HTTP_BASE_URL),
data={"event_name":"test_event_no_data",
"event_data":"",
"api_password":API_PASSWORD})
requests.post(
self._url(hah.URL_EVENTS_EVENT.format("test_event_no_data")),
data={"api_password":API_PASSWORD})
# Allow the event to take place
time.sleep(1)
@ -224,9 +215,9 @@ class TestHTTPInterface(unittest.TestCase):
self.eventbus.listen_once("test_event_with_data", listener)
requests.post("{}/api/event/fire".format(HTTP_BASE_URL),
data={"event_name":"test_event_with_data",
"event_data":'{"test": 1}',
requests.post(
self._url(hah.URL_EVENTS_EVENT.format("test_event_with_data")),
data={"event_data":'{"test": 1}',
"api_password":API_PASSWORD})
# Allow the event to take place
@ -235,28 +226,6 @@ class TestHTTPInterface(unittest.TestCase):
self.assertEqual(len(test_value), 1)
# pylint: disable=invalid-name
def test_api_fire_event_with_no_params(self):
""" Test how the API respsonds when we specify no event attributes. """
test_value = []
def listener(event):
""" Helper method that will verify that our event got called and
that test if our data came through. """
if "test" in event.data:
test_value.append(1)
self.eventbus.listen_once("test_event_with_data", listener)
requests.post("{}/api/event/fire".format(HTTP_BASE_URL),
data={"api_password":API_PASSWORD})
# Allow the event to take place
time.sleep(1)
self.assertEqual(len(test_value), 0)
# pylint: disable=invalid-name
def test_api_fire_event_with_invalid_json(self):
""" Test if the API allows us to fire an event. """
@ -268,9 +237,9 @@ class TestHTTPInterface(unittest.TestCase):
self.eventbus.listen_once("test_event_with_bad_data", listener)
req = requests.post("{}/api/event/fire".format(HTTP_BASE_URL),
data={"event_name":"test_event_with_bad_data",
"event_data":'not json',
req = requests.post(
self._url(hah.URL_API_EVENTS_EVENT.format("test_event")),
data={"event_data":'not json',
"api_password":API_PASSWORD})