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. 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 Other status codes that can occur are:
parameter: api_password - string - 400 (Bad Request)
Will list all the categories for which a state is currently tracked. Returns a json object like this: - 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
```json The api supports the following actions:
{"status": "OK",
"message":"State categories",
"categories": ["all_devices", "Paulus_Nexus_4"]}
```
/api/state/get - POST `/api/states` - GET
parameter: api_password - string Returns a list of categories for which a state is available
parameter: category - string Example result:
Will get the current state of a category. Returns a json object like this: ```json{
"categories": [
"Paulus_Nexus_4",
"weather.sun",
"all_devices"
]
}```
```json `/api/states/<category>` - GET
{"status": "OK", Returns the current state from a category
"message": "State of all_devices", Example result:
"category": "all_devices", ```json{
"state": "device_home", "attributes": {
"last_changed": "19:10:39 25-10-2013", "next_rising": "07:04:15 29-10-2013",
"attributes": {}} "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 `/api/states/<category>` - POST
parameter: api_password - string Updates the current state of a category. Returns status code 201 if successful
parameter: category - string with location header of updated resource.
parameter: new_state - string parameter: new_state - string
parameter: attributes - object encoded as JSON string (optional) optional parameter: attributes - JSON encoded object
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 `/api/events/<event_type>` - POST
parameter: api_password - string Fires an event with event_type
parameter: event_name - string optional parameter: event_data - JSON encoded object
parameter: event_data - object encoded as JSON string (optional) Example result:
Fires an 'event_name' event containing data from 'event_data' ```json{
"message": "Event download_file fired."
}```
Android remote control Android remote control
---------------------- ----------------------

Binary file not shown.

View File

@ -1,7 +1,8 @@
<TaskerData sr="" dvi="1" tv="4.1u3m"> <TaskerData sr="" dvi="1" tv="4.1u3m">
<Profile sr="prof24" ve="2"> <Profile sr="prof24" ve="2">
<cdate>1381116787665</cdate> <cdate>1381116787665</cdate>
<edate>1381116787665</edate> <clp>true</clp>
<edate>1382062270688</edate>
<id>24</id> <id>24</id>
<mid0>20</mid0> <mid0>20</mid0>
<Event sr="con0" ve="2"> <Event sr="con0" ve="2">
@ -11,8 +12,7 @@
</Profile> </Profile>
<Profile sr="prof25" ve="2"> <Profile sr="prof25" ve="2">
<cdate>1380613730755</cdate> <cdate>1380613730755</cdate>
<clp>true</clp> <edate>1382769497429</edate>
<edate>1381001553706</edate>
<id>25</id> <id>25</id>
<mid0>23</mid0> <mid0>23</mid0>
<mid1>20</mid1> <mid1>20</mid1>
@ -26,7 +26,7 @@
<Profile sr="prof26" ve="2"> <Profile sr="prof26" ve="2">
<cdate>1380613730755</cdate> <cdate>1380613730755</cdate>
<clp>true</clp> <clp>true</clp>
<edate>1381110280839</edate> <edate>1383003483161</edate>
<id>26</id> <id>26</id>
<mid0>22</mid0> <mid0>22</mid0>
<mid1>20</mid1> <mid1>20</mid1>
@ -37,13 +37,27 @@
<Int sr="arg0" val="3"/> <Int sr="arg0" val="3"/>
</State> </State>
</Profile> </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"> <Profile sr="prof5" ve="2">
<cdate>1380496514959</cdate> <cdate>1380496514959</cdate>
<cldm>1500</cldm> <cldm>1500</cldm>
<clp>true</clp> <clp>true</clp>
<edate>1381110261999</edate> <edate>1382769618501</edate>
<id>5</id> <id>5</id>
<mid0>7</mid0> <mid0>19</mid0>
<nme>HA Battery Changed</nme> <nme>HA Battery Changed</nme>
<Event sr="con0" ve="2"> <Event sr="con0" ve="2">
<code>203</code> <code>203</code>
@ -53,14 +67,14 @@
<Project sr="proj0"> <Project sr="proj0">
<cdate>1381110247781</cdate> <cdate>1381110247781</cdate>
<name>Home Assistant</name> <name>Home Assistant</name>
<pids>24,26,5,25</pids> <pids>5,3,25,26,24</pids>
<scenes>Variable Query,Home Assistant Start</scenes> <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"> <Kid sr="Kid">
<launchID>12</launchID> <launchID>12</launchID>
<pkg>nl.paulus.homeassistant</pkg> <pkg>nl.paulus.homeassistant</pkg>
<vnme>1.0</vnme> <vnme>1.1</vnme>
<vnum>10</vnum> <vnum>14</vnum>
</Kid> </Kid>
<Img sr="icon" ve="2"> <Img sr="icon" ve="2">
<nme>cust_animal_penguin</nme> <nme>cust_animal_penguin</nme>
@ -69,7 +83,7 @@
<Scene sr="sceneHome Assistant Start"> <Scene sr="sceneHome Assistant Start">
<backColour>-637534208</backColour> <backColour>-637534208</backColour>
<cdate>1381113309678</cdate> <cdate>1381113309678</cdate>
<edate>1381118413367</edate> <edate>1381162068611</edate>
<heightLand>-1</heightLand> <heightLand>-1</heightLand>
<heightPort>688</heightPort> <heightPort>688</heightPort>
<nme>Home Assistant Start</nme> <nme>Home Assistant Start</nme>
@ -308,9 +322,24 @@
<Int sr="arg2" val="255"/> <Int sr="arg2" val="255"/>
</ImageElement> </ImageElement>
</Scene> </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"> <Task sr="task11">
<cdate>1381110672417</cdate> <cdate>1381110672417</cdate>
<edate>1381116046765</edate> <edate>1383030844501</edate>
<id>11</id> <id>11</id>
<nme>Open Debug Interface</nme> <nme>Open Debug Interface</nme>
<pri>10</pri> <pri>10</pri>
@ -321,7 +350,7 @@
</Task> </Task>
<Task sr="task12"> <Task sr="task12">
<cdate>1381113015963</cdate> <cdate>1381113015963</cdate>
<edate>1381116866174</edate> <edate>1383030888271</edate>
<id>12</id> <id>12</id>
<nme>Start Screen</nme> <nme>Start Screen</nme>
<pri>10</pri> <pri>10</pri>
@ -338,6 +367,9 @@
<code>49</code> <code>49</code>
<Str sr="arg0" ve="3">Home Assistant Start</Str> <Str sr="arg0" ve="3">Home Assistant Start</Str>
</Action> </Action>
<Img sr="icn" ve="2">
<nme>hd_aaa_ext_tiles_small</nme>
</Img>
</Task> </Task>
<Task sr="task13"> <Task sr="task13">
<cdate>1381114398467</cdate> <cdate>1381114398467</cdate>
@ -354,16 +386,15 @@
</Task> </Task>
<Task sr="task14"> <Task sr="task14">
<cdate>1381114829583</cdate> <cdate>1381114829583</cdate>
<edate>1381115098684</edate> <edate>1383030731979</edate>
<id>14</id> <id>14</id>
<nme>API Fire Event</nme> <nme>API Fire Event</nme>
<pri>10</pri> <pri>10</pri>
<Action sr="act0" ve="3"> <Action sr="act0" ve="3">
<code>116</code> <code>116</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str> <Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str>
<Str sr="arg1" ve="3">/api/event/fire</Str> <Str sr="arg1" ve="3">/api/events/%par1</Str>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD <Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD</Str>
event_name=%par1</Str>
<Str sr="arg3" ve="3"/> <Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/> <Int sr="arg4" val="10"/>
<Str sr="arg5" ve="3"/> <Str sr="arg5" ve="3"/>
@ -372,7 +403,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task15"> <Task sr="task15">
<cdate>1380262442154</cdate> <cdate>1380262442154</cdate>
<edate>1381115642332</edate> <edate>1383030894445</edate>
<id>15</id> <id>15</id>
<nme>Light On</nme> <nme>Light On</nme>
<pri>10</pri> <pri>10</pri>
@ -391,7 +422,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task16"> <Task sr="task16">
<cdate>1380262442154</cdate> <cdate>1380262442154</cdate>
<edate>1381115613658</edate> <edate>1383030896170</edate>
<id>16</id> <id>16</id>
<nme>Start Epic Sax</nme> <nme>Start Epic Sax</nme>
<pri>10</pri> <pri>10</pri>
@ -408,9 +439,29 @@ event_name=%par1</Str>
<nme>hd_aaa_ext_guitar</nme> <nme>hd_aaa_ext_guitar</nme>
</Img> </Img>
</Task> </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"> <Task sr="task20">
<cdate>1380613530339</cdate> <cdate>1380613530339</cdate>
<edate>1381116102459</edate> <edate>1383030848142</edate>
<id>20</id> <id>20</id>
<nme>Charging None</nme> <nme>Charging None</nme>
<Action sr="act0" ve="3"> <Action sr="act0" ve="3">
@ -425,7 +476,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task22"> <Task sr="task22">
<cdate>1380613530339</cdate> <cdate>1380613530339</cdate>
<edate>1381116000403</edate> <edate>1383030909347</edate>
<id>22</id> <id>22</id>
<nme>Charging Wireless</nme> <nme>Charging Wireless</nme>
<Action sr="act0" ve="3"> <Action sr="act0" ve="3">
@ -440,7 +491,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task23"> <Task sr="task23">
<cdate>1380613530339</cdate> <cdate>1380613530339</cdate>
<edate>1381115997137</edate> <edate>1383030849758</edate>
<id>23</id> <id>23</id>
<nme>Charging USB</nme> <nme>Charging USB</nme>
<Action sr="act0" ve="3"> <Action sr="act0" ve="3">
@ -455,7 +506,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task4"> <Task sr="task4">
<cdate>1380262442154</cdate> <cdate>1380262442154</cdate>
<edate>1381115633261</edate> <edate>1383030892718</edate>
<id>4</id> <id>4</id>
<nme>Light Off</nme> <nme>Light Off</nme>
<pri>10</pri> <pri>10</pri>
@ -474,7 +525,7 @@ event_name=%par1</Str>
</Task> </Task>
<Task sr="task6"> <Task sr="task6">
<cdate>1380522560890</cdate> <cdate>1380522560890</cdate>
<edate>1381117976853</edate> <edate>1383030900554</edate>
<id>6</id> <id>6</id>
<nme>Setup</nme> <nme>Setup</nme>
<pri>10</pri> <pri>10</pri>
@ -580,28 +631,9 @@ event_name=%par1</Str>
<nme>hd_ab_action_settings</nme> <nme>hd_ab_action_settings</nme>
</Img> </Img>
</Task> </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"> <Task sr="task8">
<cdate>1380262442154</cdate> <cdate>1380262442154</cdate>
<edate>1381115955507</edate> <edate>1383030906782</edate>
<id>8</id> <id>8</id>
<nme>Update Charging</nme> <nme>Update Charging</nme>
<pri>10</pri> <pri>10</pri>
@ -613,23 +645,18 @@ new_state=%BATT</Str>
<Int sr="arg3" val="0"/> <Int sr="arg3" val="0"/>
</Action> </Action>
<Action sr="act1" ve="3"> <Action sr="act1" ve="3">
<code>116</code> <code>130</code>
<Str sr="arg0" ve="3">%HA_HOST:%HA_PORT</Str> <Str sr="arg0" ve="3">Update Battery</Str>
<Str sr="arg1" ve="3">/api/state/change</Str> <Int sr="arg1" val="0"/>
<Str sr="arg2" ve="3">api_password=%HA_API_PASSWORD <Int sr="arg2" val="5"/>
category=%HA_DEVICE_NAME.charging
new_state=%HA_CHARGING
category=%HA_DEVICE_NAME.battery
new_state=%BATT</Str>
<Str sr="arg3" ve="3"/> <Str sr="arg3" ve="3"/>
<Int sr="arg4" val="10"/> <Str sr="arg4" ve="3"/>
<Str sr="arg5" ve="3"/> <Str sr="arg5" ve="3"/>
<Str sr="arg6" ve="3"/>
</Action> </Action>
</Task> </Task>
<Task sr="task9"> <Task sr="task9">
<cdate>1380262442154</cdate> <cdate>1380262442154</cdate>
<edate>1381115659673</edate> <edate>1383030890674</edate>
<id>9</id> <id>9</id>
<nme>Start Fireplace</nme> <nme>Start Fireplace</nme>
<pri>10</pri> <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. 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: The api supports the following actions:
/api/state/change - POST /api/states - GET
parameter: category - string Returns a list of categories for which a state is available
parameter: new_state - string Example result:
Changes category 'category' to 'new_state' {
It is possible to sent multiple values for category and new_state. "categories": [
If the number of values for category and new_state do not match only "Paulus_Nexus_4",
combinations where both values are supplied will be set. "weather.sun",
"all_devices"
]
}
/api/event/fire - POST /api/states/<category> - GET
parameter: event_name - string Returns the current state from a category
parameter: event_data - JSON-string (optional) Example result:
Fires an 'event_name' event containing data from 'event_data' {
"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 json
import threading import threading
import itertools
import logging import logging
import re
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs from urlparse import urlparse, parse_qs
@ -36,9 +68,24 @@ import homeassistant as ha
SERVER_PORT = 8123 SERVER_PORT = 8123
MESSAGE_STATUS_OK = "OK" HTTP_OK = 200
MESSAGE_STATUS_ERROR = "ERROR" HTTP_CREATED = 201
MESSAGE_STATUS_UNAUTHORIZED = "UNAUTHORIZED" 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): class HTTPInterface(threading.Thread):
""" Provides an HTTP interface for Home Assistant. """ """ Provides an HTTP interface for Home Assistant. """
@ -76,23 +123,129 @@ class HTTPInterface(threading.Thread):
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
""" Handles incoming HTTP requests """ """ Handles incoming HTTP requests """
#Handler for the GET requests PATHS = [ ('GET', '/', '_handle_get_root'),
def do_GET(self): # pylint: disable=invalid-name
""" Handle incoming GET requests. """
write = lambda txt: self.wfile.write(txt+"\n")
# /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) 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 content_length:
if self._verify_api_password(api_password, False): data.update(parse_qs(self.rfile.read(content_length)))
self.send_response(200)
try:
api_password = data['api_password'][0]
except KeyError:
api_password = ''
# 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:
path = url.path
# pylint: disable=attribute-defined-outside-init
self.use_json = False
path_matched_but_not_method = False
handle_request_method = False
# 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:
self.send_response(HTTP_NOT_FOUND)
def do_GET(self): # pylint: disable=invalid-name
""" GET request handler. """
self._handle_request('GET')
def do_POST(self): # pylint: disable=invalid-name
""" POST request handler. """
self._handle_request('POST')
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 self.use_json:
self._message("API password missing or incorrect.",
HTTP_UNAUTHORIZED)
else:
self.send_response(HTTP_OK)
self.send_header('Content-type','text/html') self.send_header('Content-type','text/html')
self.end_headers() self.end_headers()
self.wfile.write((
"<html>"
"<head><title>Home Assistant</title></head>"
"<body>"
"<form action='/' method='GET'>"
"API password: <input name='api_password' />"
"<input type='submit' value='submit' />"
"</form>"
"</body></html>"))
return False
# pylint: disable=unused-argument
def _handle_get_root(self, path_match, data):
""" Renders the debug interface. """
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>" write(("<html>"
"<head><title>Home Assistant</title></head>" "<head><title>Home Assistant</title></head>"
@ -133,24 +286,6 @@ class RequestHandler(BaseHTTPRequestHandler):
write("</table>") 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: # Describe event bus:
write(("<table><tr><th>Event</th><th>Listeners</th></tr>")) write(("<table><tr><th>Event</th><th>Listeners</th></tr>"))
@ -161,192 +296,105 @@ class RequestHandler(BaseHTTPRequestHandler):
len(self.server.eventbus.listeners[category]))) len(self.server.eventbus.listeners[category])))
# Form to allow firing events # Form to allow firing events
write(("</table><br />" write("</table>")
"<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>") 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})
else: # pylint: disable=unused-argument
self.send_response(404) def _handle_get_states_category(self, path_match, data):
""" Returns the state of a specific category. """
# 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))
if self.path.startswith('/api/'):
action = self.path[5:]
use_json = True
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: try:
category = post_data['category'][0] category = path_match.group('category')
state = self.server.statemachine.get_state(category) state = self.server.statemachine.get_state(category)
state['category'] = category state['category'] = category
self._response(use_json, "State of {}".format(category), self._write_json(state)
json_data=state)
except KeyError: except KeyError:
# If category or new_state don't exist in post data # If category or new_state don't exist in post data
self._response(use_json, "Invalid state received.", self._message("Invalid state received.", HTTP_BAD_REQUEST)
MESSAGE_STATUS_ERROR)
elif action == "state/change":
if self._verify_api_password(given_api_password, use_json): def _handle_post_states_category(self, path_match, data):
""" Handles updating the state of a category. """
try: try:
changed = [] category = path_match.group('category')
for idx, category, new_state in zip(itertools.count(), new_state = data['new_state'][0]
post_data['category'],
post_data['new_state']
):
# See if we also received attributes for this state
try: try:
attributes = json.loads( attributes = json.loads(data['attributes'][0])
post_data['attributes'][idx])
except KeyError: except KeyError:
# Happens if key 'attributes' or idx does not exist # Happens if key 'attributes' does not exist
attributes = None attributes = None
self.server.statemachine.set_state(category, self.server.statemachine.set_state(category,
new_state, new_state,
attributes) attributes)
changed.append("{}={}".format(category, new_state)) self._redirect("/states/{}".format(category),
"State changed: {}={}".format(category, new_state),
self._response(use_json, "States changed: {}". HTTP_CREATED)
format( ", ".join(changed) ) )
except KeyError: except KeyError:
# If category or new_state don't exist in post data # If category or new_state don't exist in post data
self._response(use_json, "Invalid parameters received.", self._message("Invalid parameters received.",
MESSAGE_STATUS_ERROR) HTTP_BAD_REQUEST)
except ValueError: except ValueError:
# If json.loads doesn't understand the attributes # Occurs during error parsing json
self._response(use_json, "Invalid state data received.", self._message("Invalid JSON for attributes", HTTP_BAD_REQUEST)
MESSAGE_STATUS_ERROR)
def _handle_post_events_event_type(self, path_match, data):
""" Handles firing of an event. """
event_type = path_match.group('event_type')
# Action to fire an event
elif action == "event/fire":
if self._verify_api_password(given_api_password, use_json):
try: try:
event_name = post_data['event_name'][0] try:
event_data = json.loads(data['event_data'][0])
if (not 'event_data' in post_data or except KeyError:
post_data['event_data'][0] == ""): # Happens if key 'event_data' does not exist
event_data = None event_data = None
else: self.server.eventbus.fire(event_type, event_data)
event_data = json.loads(post_data['event_data'][0])
self.server.eventbus.fire(event_name, event_data) self._message("Event {} fired.".format(event_type))
self._response(use_json, "Event {} fired.".
format(event_name))
except ValueError: except ValueError:
# If JSON decode error # Occurs during error parsing json
self._response(use_json, "Invalid event received (1).", self._message("Invalid JSON for event_data", HTTP_BAD_REQUEST)
MESSAGE_STATUS_ERROR)
except KeyError:
# If "event_name" not in post_data
self._response(use_json, "Invalid event received (2).",
MESSAGE_STATUS_ERROR)
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: else:
self.send_response(404) self._redirect('/', message)
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
def _verify_api_password(self, api_password, use_json): self.send_response(status_code)
""" Helper method to verify the API password self.send_header("Location", "{}?api_password={}".
and take action if incorrect. """ format(location, self.server.api_password))
if api_password == self.server.api_password:
return True
elif use_json:
self._response(True, "API password missing or incorrect.",
MESSAGE_STATUS_UNAUTHORIZED)
else:
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers() self.end_headers()
write = lambda txt: self.wfile.write(txt+"\n") def _write_json(self, data=None, status_code=HTTP_OK):
""" Helper method to return JSON to the caller. """
write(("<html>" self.send_response(status_code)
"<head><title>Home Assistant</title></head>"
"<body>"
"<form action='/' method='GET'>"
"API password: <input name='api_password' />"
"<input type='submit' value='submit' />"
"</form>"
"</body></html>"))
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)
if status == MESSAGE_STATUS_OK:
self.server.logger.info(log_message)
response_code = 200
else:
self.server.logger.error(log_message)
response_code = (401 if status == MESSAGE_STATUS_UNAUTHORIZED
else 400)
if use_json:
self.send_response(response_code)
self.send_header('Content-type','application/json') self.send_header('Content-type','application/json')
self.end_headers() self.end_headers()
json_data = json_data or {} if data:
json_data['status'] = status self.wfile.write(json.dumps(data, indent=4, sort_keys=True))
json_data['message'] = message
self.wfile.write(json.dumps(json_data))
else:
self.server.flash_message = message
self.send_response(301)
self.send_header("Location", "/?api_password={}".
format(self.server.api_password))
self.end_headers()

View File

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

View File

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