API updated to be RESTful
This commit is contained in:
parent
102690a770
commit
f9d712d175
7 changed files with 495 additions and 437 deletions
79
README.md
79
README.md
|
@ -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.
|
@ -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 |
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue