mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
API updated to be RESTful
This commit is contained in:
parent
102690a770
commit
f9d712d175
75
README.md
75
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.
|
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.
@ -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 |
@ -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()
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user