diff --git a/README.md b/README.md index 9243c29f4f9..addc0128044 100644 --- a/README.md +++ b/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/` - 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/` - 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/` - 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 ---------------------- diff --git a/android-tasker/Home_Assistant.apk b/android-tasker/Home_Assistant.apk index 9f75e86569e..9e3924aa8f2 100644 Binary files a/android-tasker/Home_Assistant.apk and b/android-tasker/Home_Assistant.apk differ diff --git a/android-tasker/Home_Assistant.prj.xml b/android-tasker/Home_Assistant.prj.xml index 4b789d07dd9..5a2ff8e8a57 100644 --- a/android-tasker/Home_Assistant.prj.xml +++ b/android-tasker/Home_Assistant.prj.xml @@ -1,7 +1,8 @@ 1381116787665 - 1381116787665 + true + 1382062270688 24 20 @@ -11,8 +12,7 @@ 1380613730755 - true - 1381001553706 + 1382769497429 25 23 20 @@ -26,7 +26,7 @@ 1380613730755 true - 1381110280839 + 1383003483161 26 22 20 @@ -37,13 +37,27 @@ + + 1380613730755 + true + 1383003498566 + 3 + 10 + 20 + HA Power AC + 10 + + 10 + + + 1380496514959 1500 true - 1381110261999 + 1382769618501 5 - 7 + 19 HA Battery Changed 203 @@ -53,14 +67,14 @@ 1381110247781 Home Assistant - 24,26,5,25 + 5,3,25,26,24 Variable Query,Home Assistant Start - 14,16,4,15,7,20,6,8,22,23,9,11,12,13 + 19,8,10,6,16,9,20,14,11,4,23,15,12,13,22 12 nl.paulus.homeassistant - 1.0 - 10 + 1.1 + 14 cust_animal_penguin @@ -69,7 +83,7 @@ -637534208 1381113309678 - 1381118413367 + 1381162068611 -1 688 Home Assistant Start @@ -308,9 +322,24 @@ + + 1380613530339 + 1383030846230 + 10 + Charging AC + + 130 + Update Charging + + + ac + + + + 1381110672417 - 1381116046765 + 1383030844501 11 Open Debug Interface 10 @@ -321,7 +350,7 @@ 1381113015963 - 1381116866174 + 1383030888271 12 Start Screen 10 @@ -338,6 +367,9 @@ 49 Home Assistant Start + + hd_aaa_ext_tiles_small + 1381114398467 @@ -354,16 +386,15 @@ 1381114829583 - 1381115098684 + 1383030731979 14 API Fire Event 10 116 %HA_HOST:%HA_PORT - /api/event/fire - api_password=%HA_API_PASSWORD -event_name=%par1 + /api/events/%par1 + api_password=%HA_API_PASSWORD @@ -372,7 +403,7 @@ event_name=%par1 1380262442154 - 1381115642332 + 1383030894445 15 Light On 10 @@ -391,7 +422,7 @@ event_name=%par1 1380262442154 - 1381115613658 + 1383030896170 16 Start Epic Sax 10 @@ -408,9 +439,29 @@ event_name=%par1 hd_aaa_ext_guitar + + 1380262442154 + 1383030903842 + 19 + Update Battery + 10 + + 116 + %HA_HOST:%HA_PORT + /api/state/change + api_password=%HA_API_PASSWORD +category=%HA_DEVICE_NAME.charging +new_state=%HA_CHARGING +attributes={"battery":%BATT} + + + + + + 1380613530339 - 1381116102459 + 1383030848142 20 Charging None @@ -425,7 +476,7 @@ event_name=%par1 1380613530339 - 1381116000403 + 1383030909347 22 Charging Wireless @@ -440,7 +491,7 @@ event_name=%par1 1380613530339 - 1381115997137 + 1383030849758 23 Charging USB @@ -455,7 +506,7 @@ event_name=%par1 1380262442154 - 1381115633261 + 1383030892718 4 Light Off 10 @@ -474,7 +525,7 @@ event_name=%par1 1380522560890 - 1381117976853 + 1383030900554 6 Setup 10 @@ -580,28 +631,9 @@ event_name=%par1 hd_ab_action_settings - - 1380262442154 - 1381111978825 - 7 - Update Battery - 10 - - 116 - %HA_HOST:%HA_PORT - /api/state/change - api_password=%HA_API_PASSWORD -category=%HA_DEVICE_NAME.battery -new_state=%BATT - - - - - - 1380262442154 - 1381115955507 + 1383030906782 8 Update Charging 10 @@ -613,23 +645,18 @@ new_state=%BATT - 116 - %HA_HOST:%HA_PORT - /api/state/change - api_password=%HA_API_PASSWORD -category=%HA_DEVICE_NAME.charging -new_state=%HA_CHARGING -category=%HA_DEVICE_NAME.battery -new_state=%BATT + 130 + Update Battery + + - + - 1380262442154 - 1381115659673 + 1383030890674 9 Start Fireplace 10 diff --git a/docs/screenshot-debug-interface.png b/docs/screenshot-debug-interface.png index 35b0dd86ff8..631e080c877 100644 Binary files a/docs/screenshot-debug-interface.png and b/docs/screenshot-debug-interface.png differ diff --git a/homeassistant/httpinterface.py b/homeassistant/httpinterface.py index 68a1e3086b0..b2563c00dd2 100644 --- a/homeassistant/httpinterface.py +++ b/homeassistant/httpinterface.py @@ -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/ - 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/ - 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/ - 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[a-zA-Z\.\_0-9]+)'), + '_handle_get_states_category'), + ('POST', re.compile(r'/states/(?P[a-zA-Z\.\_0-9]+)'), + '_handle_post_states_category'), + + # /events + ('POST', re.compile(r'/events/(?P\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(("" - "Home Assistant" - "")) - - # Flash message support - if self.server.flash_message: - write("

{}

".format(self.server.flash_message)) - - self.server.flash_message = None - - # Describe state machine: - categories = [] - - write(("" - "" - "")) - - for category in \ - sorted(self.server.statemachine.categories, - key=lambda key: key.lower()): - - categories.append(category) - - state = self.server.statemachine.get_state(category) - - attributes = "
".join( - ["{}: {}".format(attr, state['attributes'][attr]) - for attr in state['attributes']]) - - write(("" - "" - ""). - format(category, - state['state'], - state['last_changed'], - attributes)) - - write("
NameStateLast ChangedAttributes
{}{}{}{}
") - - # Small form to change the state - write(("
Change state:
" - "
")) - - write("". - format(self.server.api_password)) - - write("") - - write(("" - "" - "
")) - - # Describe event bus: - write(("")) - - for category in sorted(self.server.eventbus.listeners, - key=lambda key: key.lower()): - write("". - format(category, - len(self.server.eventbus.listeners[category]))) - - # Form to allow firing events - write(("
EventListeners
{}{}

" - "
")) - - write("". - format(self.server.api_password)) - - write(("Event name:
" - "Event data (json):
" - "" - "
")) - - write("") - + # 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(("" + self.wfile.write(( + "" "Home Assistant" "" "
" @@ -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(("" + "Home Assistant" + "")) + + # Flash message support + if self.server.flash_message: + write("

{}

".format(self.server.flash_message)) + + self.server.flash_message = None + + # Describe state machine: + categories = [] + + write(("" + "" + "")) + + for category in \ + sorted(self.server.statemachine.categories, + key=lambda key: key.lower()): + + categories.append(category) + + state = self.server.statemachine.get_state(category) + + attributes = "
".join( + ["{}: {}".format(attr, state['attributes'][attr]) + for attr in state['attributes']]) + + write(("" + "" + ""). + format(category, + state['state'], + state['last_changed'], + attributes)) + + write("
NameStateLast ChangedAttributes
{}{}{}{}
") + + # Describe event bus: + write(("")) + + for category in sorted(self.server.eventbus.listeners, + key=lambda key: key.lower()): + write("". + format(category, + len(self.server.eventbus.listeners[category]))) + + # Form to allow firing events + write("
EventListeners
{}{}
") + + write("") + + # 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)) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 93ab356d131..8e59f31c69a 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -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() diff --git a/homeassistant/test.py b/homeassistant/test.py index 1c45e1114fd..0ca1650b533 100644 --- a/homeassistant/test.py +++ b/homeassistant/test.py @@ -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})