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,238 +123,110 @@ 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)
self.send_header('Content-type','text/html')
self.end_headers()
try:
api_password = data['api_password'][0]
except KeyError:
api_password = ''
write(("<html>" # We respond to API requests with JSON
"<head><title>Home Assistant</title></head>" # For other requests we respond with html
"<body>")) if url.path.startswith('/api/'):
path = url.path[4:]
# Flash message support # pylint: disable=attribute-defined-outside-init
if self.server.flash_message: self.use_json = True
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>")
else: 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']) path_matched_but_not_method = False
post_data = parse_qs(self.rfile.read(length)) handle_request_method = False
if self.path.startswith('/api/'): # Check every url to find matching result
action = self.path[5:] for t_method, t_path, t_handler in RequestHandler.PATHS:
use_json = True
# 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: else:
action = self.path[1:] self.send_response(HTTP_NOT_FOUND)
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)
except KeyError: def do_GET(self): # pylint: disable=invalid-name
# If category or new_state don't exist in post data """ GET request handler. """
self._response(use_json, "Invalid state received.", self._handle_request('GET')
MESSAGE_STATUS_ERROR)
elif action == "state/change": def do_POST(self): # pylint: disable=invalid-name
if self._verify_api_password(given_api_password, use_json): """ POST request handler. """
try: self._handle_request('POST')
changed = []
for idx, category, new_state in zip(itertools.count(), def _verify_api_password(self, api_password):
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):
""" Helper method to verify the API password """ Helper method to verify the API password
and take action if incorrect. """ and take action if incorrect. """
if api_password == self.server.api_password: if api_password == self.server.api_password:
return True return True
elif use_json: elif self.use_json:
self._response(True, "API password missing or incorrect.", self._message("API password missing or incorrect.",
MESSAGE_STATUS_UNAUTHORIZED) HTTP_UNAUTHORIZED)
else: else:
self.send_response(200) 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()
write = lambda txt: self.wfile.write(txt+"\n") self.wfile.write((
"<html>"
write(("<html>"
"<head><title>Home Assistant</title></head>" "<head><title>Home Assistant</title></head>"
"<body>" "<body>"
"<form action='/' method='GET'>" "<form action='/' method='GET'>"
@ -318,35 +237,164 @@ class RequestHandler(BaseHTTPRequestHandler):
return False return False
def _response(self, use_json, message, # pylint: disable=unused-argument
status=MESSAGE_STATUS_OK, json_data=None): def _handle_get_root(self, path_match, data):
""" Helper method to show a message to the user. """ """ Renders the debug interface. """
log_message = "{}: {}".format(status, message)
if status == MESSAGE_STATUS_OK: write = lambda txt: self.wfile.write(txt+"\n")
self.server.logger.info(log_message)
response_code = 200
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: else:
self.server.logger.error(log_message) self._redirect('/', message)
response_code = (401 if status == MESSAGE_STATUS_UNAUTHORIZED
else 400)
if use_json: def _redirect(self, location, message=None,
self.send_response(response_code) status_code=HTTP_MOVED_PERMANENTLY):
self.send_header('Content-type','application/json') """ Helper method to redirect caller. """
self.end_headers() # Only save as flash message if we will go to debug interface next
if not self.use_json and message:
json_data = json_data or {}
json_data['status'] = status
json_data['message'] = message
self.wfile.write(json.dumps(json_data))
else:
self.server.flash_message = message self.server.flash_message = message
self.send_response(301) self.send_response(status_code)
self.send_header("Location", "/?api_password={}". self.send_header("Location", "{}?api_password={}".
format(self.server.api_password)) format(location, self.server.api_password))
self.end_headers() 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 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,17 +58,21 @@ 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'],
"debug_state_change") "debug_state_change")
@ -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")),
"api_password":API_PASSWORD}) data={"new_state": new_state,
"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})