mirror of
https://github.com/home-assistant/core
synced 2024-09-09 12:51:22 +02:00
HTTP API is now more RESTful
This commit is contained in:
parent
951c3683b2
commit
001f27cdb4
@ -475,9 +475,8 @@ class StateMachine(object):
|
||||
return list(self._states.keys())
|
||||
|
||||
def all(self):
|
||||
""" Returns a dict mapping all entity_ids to their state. """
|
||||
return {entity_id: state.copy() for entity_id, state
|
||||
in self._states.items()}
|
||||
""" Returns a list of all states. """
|
||||
return [state.copy() for state in self._states.values()]
|
||||
|
||||
def get(self, entity_id):
|
||||
""" Returns the state of the specified entity. """
|
||||
|
@ -27,13 +27,10 @@ Example result:
|
||||
/api/states - GET
|
||||
Returns a list of entities for which a state is available
|
||||
Example result:
|
||||
{
|
||||
"entity_ids": [
|
||||
"Paulus_Nexus_4",
|
||||
"weather.sun",
|
||||
"all_devices"
|
||||
]
|
||||
}
|
||||
[
|
||||
{ .. state object .. },
|
||||
{ .. state object .. }
|
||||
]
|
||||
|
||||
/api/states/<entity_id> - GET
|
||||
Returns the current state from an entity
|
||||
@ -102,9 +99,6 @@ HTTP_METHOD_NOT_ALLOWED = 405
|
||||
HTTP_UNPROCESSABLE_ENTITY = 422
|
||||
|
||||
URL_ROOT = "/"
|
||||
URL_CHANGE_STATE = "/change_state"
|
||||
URL_FIRE_EVENT = "/fire_event"
|
||||
URL_CALL_SERVICE = "/call_service"
|
||||
|
||||
URL_STATIC = "/static/{}"
|
||||
|
||||
@ -196,11 +190,6 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
PATHS = [ # debug interface
|
||||
('GET', URL_ROOT, '_handle_get_root'),
|
||||
# These get compiled as RE because these methods are reused
|
||||
# by other urls that use url parameters
|
||||
('POST', re.compile(URL_CHANGE_STATE), '_handle_change_state'),
|
||||
('POST', re.compile(URL_FIRE_EVENT), '_handle_fire_event'),
|
||||
('POST', re.compile(URL_CALL_SERVICE), '_handle_call_service'),
|
||||
|
||||
# /api - for validation purposes
|
||||
('GET', rem.URL_API, '_handle_get_api'),
|
||||
@ -212,13 +201,16 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
'_handle_get_api_states_entity'),
|
||||
('POST',
|
||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_change_state'),
|
||||
'_handle_post_state_entity'),
|
||||
('PUT',
|
||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_post_state_entity'),
|
||||
|
||||
# /events
|
||||
('GET', rem.URL_API_EVENTS, '_handle_get_api_events'),
|
||||
('POST',
|
||||
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
'_handle_fire_event'),
|
||||
'_handle_api_post_events_event'),
|
||||
|
||||
# /services
|
||||
('GET', rem.URL_API_SERVICES, '_handle_get_api_services'),
|
||||
@ -226,7 +218,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
re.compile((r'/api/services/'
|
||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
'_handle_call_service'),
|
||||
'_handle_post_api_services_domain_service'),
|
||||
|
||||
# /event_forwarding
|
||||
('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
|
||||
@ -247,20 +239,31 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
# Read query input
|
||||
data = parse_qs(url.query)
|
||||
|
||||
# parse_qs gives a list for each value, take the latest element
|
||||
for key in data:
|
||||
data[key] = data[key][-1]
|
||||
|
||||
# Did we get post input ?
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
|
||||
if content_length:
|
||||
data.update(parse_qs(self.rfile.read(
|
||||
content_length).decode("UTF-8")))
|
||||
body_content = self.rfile.read(content_length).decode("UTF-8")
|
||||
try:
|
||||
data.update(json.loads(body_content))
|
||||
except ValueError:
|
||||
self.server.logger.exception(
|
||||
"Exception parsing JSON: {}".format(body_content))
|
||||
|
||||
try:
|
||||
api_password = data['api_password'][0]
|
||||
except KeyError:
|
||||
api_password = ''
|
||||
self.send_response(HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
api_password = self.headers.get(rem.AUTH_HEADER)
|
||||
|
||||
if not api_password and 'api_password' in data:
|
||||
api_password = data['api_password']
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data['_METHOD'][0]
|
||||
method = data['_METHOD']
|
||||
|
||||
if url.path.startswith('/api/'):
|
||||
self.use_json = True
|
||||
@ -313,6 +316,14 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
""" POST request handler. """
|
||||
self._handle_request('POST')
|
||||
|
||||
def do_PUT(self): # pylint: disable=invalid-name
|
||||
""" PUT request handler. """
|
||||
self._handle_request('PUT')
|
||||
|
||||
def do_DELETE(self): # pylint: disable=invalid-name
|
||||
""" DELETE request handler. """
|
||||
self._handle_request('DELETE')
|
||||
|
||||
def _verify_api_password(self, api_password):
|
||||
""" Helper method to verify the API password
|
||||
and take action if incorrect. """
|
||||
@ -402,18 +413,18 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
"<th>Attributes</th><th>Last Changed</th>"
|
||||
"</tr>").format(self.server.api_password))
|
||||
|
||||
for entity_id, state in \
|
||||
sorted(self.server.hass.states.all().items(),
|
||||
key=lambda item: item[0].lower()):
|
||||
for state in \
|
||||
sorted(self.server.hass.states.all(),
|
||||
key=lambda item: item.entity_id.lower()):
|
||||
|
||||
domain = util.split_entity_id(entity_id)[0]
|
||||
domain = util.split_entity_id(state.entity_id)[0]
|
||||
|
||||
attributes = "<br>".join(
|
||||
"{}: {}".format(attr, val)
|
||||
for attr, val in state.attributes.items())
|
||||
|
||||
write("<tr><td>{}</td><td>{}</td><td>{}".format(
|
||||
_get_domain_icon(domain), entity_id, state.state))
|
||||
_get_domain_icon(domain), state.entity_id, state.state))
|
||||
|
||||
if state.state == STATE_ON or state.state == STATE_OFF:
|
||||
if state.state == STATE_ON:
|
||||
@ -569,135 +580,6 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
write("</div></body></html>")
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_change_state(self, path_match, data):
|
||||
""" Handles updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/change_state
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
entity_id = path_match.group('entity_id')
|
||||
except IndexError:
|
||||
# If group 'entity_id' does not exist in path_match
|
||||
entity_id = data['entity_id'][0]
|
||||
|
||||
new_state = data['new_state'][0]
|
||||
|
||||
try:
|
||||
attributes = json.loads(data['attributes'][0])
|
||||
except KeyError:
|
||||
# Happens if key 'attributes' does not exist
|
||||
attributes = None
|
||||
|
||||
# Write state
|
||||
self.server.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
# Return state if json, else redirect to main page
|
||||
if self.use_json:
|
||||
state = self.server.hass.states.get(entity_id)
|
||||
|
||||
self._write_json(state.as_dict(),
|
||||
status_code=HTTP_CREATED,
|
||||
location=
|
||||
rem.URL_API_STATES_ENTITY.format(entity_id))
|
||||
else:
|
||||
self._message(
|
||||
"State of {} changed to {}".format(entity_id, new_state))
|
||||
|
||||
except KeyError:
|
||||
# If new_state don't exist in post data
|
||||
self._message(
|
||||
"No new_state submitted.", HTTP_BAD_REQUEST)
|
||||
|
||||
except ValueError:
|
||||
# Occurs during error parsing json
|
||||
self._message(
|
||||
"Invalid JSON for attributes", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_fire_event(self, path_match, data):
|
||||
""" Handles firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/fire_event
|
||||
/api/events/<event_type>
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
event_type = path_match.group('event_type')
|
||||
event_origin = ha.EventOrigin.remote
|
||||
except IndexError:
|
||||
# If group event_type does not exist in path_match
|
||||
event_type = data['event_type'][0]
|
||||
event_origin = ha.EventOrigin.local
|
||||
|
||||
if 'event_data' in data:
|
||||
event_data = json.loads(data['event_data'][0])
|
||||
else:
|
||||
event_data = None
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
|
||||
self._message("Event {} fired.".format(event_type))
|
||||
|
||||
except KeyError:
|
||||
# Occurs if event_type does not exist in data
|
||||
self._message("No event_type received.", HTTP_BAD_REQUEST)
|
||||
|
||||
except ValueError:
|
||||
# Occurs during error parsing json
|
||||
self._message(
|
||||
"Invalid JSON for event_data", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
def _handle_call_service(self, path_match, data):
|
||||
""" Handles calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/call_service
|
||||
/api/services/<domain>/<service>
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
except IndexError:
|
||||
# If group domain or service does not exist in path_match
|
||||
domain = data['domain'][0]
|
||||
service = data['service'][0]
|
||||
|
||||
try:
|
||||
service_data = json.loads(data['service_data'][0])
|
||||
except KeyError:
|
||||
# Happens if key 'service_data' does not exist
|
||||
service_data = None
|
||||
|
||||
self.server.hass.call_service(domain, service, service_data)
|
||||
|
||||
self._message("Service {}/{} called.".format(domain, service))
|
||||
|
||||
except KeyError:
|
||||
# Occurs if domain or service does not exist in data
|
||||
self._message("No domain or service received.", HTTP_BAD_REQUEST)
|
||||
|
||||
except ValueError:
|
||||
# Occurs during error parsing json
|
||||
self._message(
|
||||
"Invalid JSON for service_data", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _handle_get_api(self, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
@ -718,69 +600,152 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
if state:
|
||||
self._write_json(state)
|
||||
else:
|
||||
self._message("State does not exist.", HTTP_UNPROCESSABLE_ENTITY)
|
||||
self._message("State does not exist.", HTTP_NOT_FOUND)
|
||||
|
||||
def _handle_post_state_entity(self, path_match, data):
|
||||
""" Handles updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
try:
|
||||
new_state = data['state']
|
||||
except KeyError:
|
||||
self._message("state not specified", HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
attributes = data['attributes'] if 'attributes' in data else None
|
||||
|
||||
is_new_state = self.server.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.server.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
# Return state if json, else redirect to main page
|
||||
if self.use_json:
|
||||
state = self.server.hass.states.get(entity_id)
|
||||
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
|
||||
self._write_json(state.as_dict(),
|
||||
status_code=status_code,
|
||||
location=
|
||||
rem.URL_API_STATES_ENTITY.format(entity_id))
|
||||
else:
|
||||
self._message(
|
||||
"State of {} changed to {}".format(entity_id, new_state))
|
||||
|
||||
def _handle_get_api_events(self, path_match, data):
|
||||
""" Handles getting overview of event listeners. """
|
||||
self._write_json({'event_listeners': self.server.hass.bus.listeners})
|
||||
self._write_json(self.server.hass.bus.listeners)
|
||||
|
||||
def _handle_api_post_events_event(self, path_match, data):
|
||||
""" Handles firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/api/events/<event_type>
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
event_type = path_match.group('event_type')
|
||||
event_data = data.get('event_data')
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
self._message("event_data should be an object",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
|
||||
self._message("Event {} fired.".format(event_type))
|
||||
|
||||
def _handle_get_api_services(self, path_match, data):
|
||||
""" Handles getting overview of services. """
|
||||
self._write_json({'services': self.server.hass.services.services})
|
||||
self._write_json(self.server.hass.services.services)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(self, path_match, data):
|
||||
""" Handles calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
service_data = data.get('service_data')
|
||||
|
||||
if service_data is not None and not isinstance(service_data, dict):
|
||||
self._message("service_data should be an object",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
self.server.hass.call_service(domain, service, service_data)
|
||||
|
||||
self._message("Service {}/{} called.".format(domain, service))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(self, path_match, data):
|
||||
""" Handles adding an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host'][0]
|
||||
api_password = data['api_password'][0]
|
||||
|
||||
port = int(data['port'][0]) if 'port' in data else None
|
||||
|
||||
if self.server.event_forwarder is None:
|
||||
self.server.event_forwarder = \
|
||||
rem.EventForwarder(self.server.hass)
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
self.server.event_forwarder.connect(api)
|
||||
|
||||
self._message("Event forwarding setup.")
|
||||
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
# Occurs if domain or service does not exist in data
|
||||
self._message("No host or api_password received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
# Occurs during error parsing port
|
||||
self._message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if self.server.event_forwarder is None:
|
||||
self.server.event_forwarder = \
|
||||
rem.EventForwarder(self.server.hass)
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
self.server.event_forwarder.connect(api)
|
||||
|
||||
self._message("Event forwarding setup.")
|
||||
|
||||
def _handle_delete_api_event_forward(self, path_match, data):
|
||||
""" Handles deleting an event forwarding target. """
|
||||
|
||||
try:
|
||||
host = data['host'][0]
|
||||
|
||||
port = int(data['port'][0]) if 'port' in data else None
|
||||
|
||||
if self.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.server.event_forwarder.disconnect(api)
|
||||
|
||||
self._message("Event forwarding cancelled.")
|
||||
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
# Occurs if domain or service does not exist in data
|
||||
self._message("No host or api_password received.",
|
||||
self._message("No host received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
# Occurs during error parsing port
|
||||
self._message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
if self.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.server.event_forwarder.disconnect(api)
|
||||
|
||||
self._message("Event forwarding cancelled.")
|
||||
|
||||
def _handle_get_static(self, path_match, data):
|
||||
""" Returns a static file. """
|
||||
@ -838,7 +803,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
self.end_headers()
|
||||
|
||||
if data:
|
||||
if data is not None:
|
||||
self.wfile.write(
|
||||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
@ -21,6 +21,8 @@ import homeassistant as ha
|
||||
|
||||
SERVER_PORT = 8123
|
||||
|
||||
AUTH_HEADER = "HA-access"
|
||||
|
||||
URL_API = "/api/"
|
||||
URL_API_STATES = "/api/states"
|
||||
URL_API_STATES_ENTITY = "/api/states/{}"
|
||||
@ -57,6 +59,7 @@ class API(object):
|
||||
self.api_password = api_password
|
||||
self.base_url = "http://{}:{}".format(host, self.port)
|
||||
self.status = None
|
||||
self._headers = {AUTH_HEADER: api_password}
|
||||
|
||||
def validate_api(self, force_validate=False):
|
||||
""" Tests if we can communicate with the API. """
|
||||
@ -67,16 +70,18 @@ class API(object):
|
||||
|
||||
def __call__(self, method, path, data=None):
|
||||
""" Makes a call to the Home Assistant api. """
|
||||
data = data or {}
|
||||
data['api_password'] = self.api_password
|
||||
if data is not None:
|
||||
data = json.dumps(data, cls=JSONEncoder)
|
||||
|
||||
url = urllib.parse.urljoin(self.base_url, path)
|
||||
|
||||
try:
|
||||
if method == METHOD_GET:
|
||||
return requests.get(url, params=data, timeout=5)
|
||||
return requests.get(
|
||||
url, params=data, timeout=5, headers=self._headers)
|
||||
else:
|
||||
return requests.request(method, url, data=data, timeout=5)
|
||||
return requests.request(
|
||||
method, url, data=data, timeout=5, headers=self._headers)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.getLogger(__name__).exception("Error connecting to server")
|
||||
@ -226,7 +231,8 @@ class StateMachine(ha.StateMachine):
|
||||
|
||||
def mirror(self):
|
||||
""" Discards current data and mirrors the remote state machine. """
|
||||
self._states = get_states(self._api, self.logger)
|
||||
self._states = {state.entity_id: state for state
|
||||
in get_states(self._api, self.logger)}
|
||||
|
||||
def _state_changed_listener(self, event):
|
||||
""" Listens for state changed events and applies them. """
|
||||
@ -297,11 +303,10 @@ def get_event_listeners(api, logger=None):
|
||||
try:
|
||||
req = api(METHOD_GET, URL_API_EVENTS)
|
||||
|
||||
return req.json()['event_listeners'] if req.status_code == 200 else {}
|
||||
return req.json() if req.status_code == 200 else {}
|
||||
|
||||
except (ha.HomeAssistantError, ValueError, KeyError):
|
||||
except (ha.HomeAssistantError, ValueError):
|
||||
# ValueError if req.json() can't parse the json
|
||||
# KeyError if 'event_listeners' not found in parsed json
|
||||
if logger:
|
||||
logger.exception("Bus:Got unexpected result")
|
||||
|
||||
@ -312,7 +317,7 @@ def fire_event(api, event_type, event_data=None, logger=None):
|
||||
""" Fire an event at remote API. """
|
||||
|
||||
if event_data:
|
||||
data = {'event_data': json.dumps(event_data, cls=JSONEncoder)}
|
||||
data = {'event_data': event_data}
|
||||
else:
|
||||
data = None
|
||||
|
||||
@ -355,20 +360,11 @@ def get_states(api, logger=None):
|
||||
req = api(METHOD_GET,
|
||||
URL_API_STATES)
|
||||
|
||||
json_result = req.json()
|
||||
states = {}
|
||||
|
||||
for entity_id, state_dict in json_result.items():
|
||||
state = ha.State.from_dict(state_dict)
|
||||
|
||||
if state:
|
||||
states[entity_id] = state
|
||||
|
||||
return states
|
||||
return [ha.State.from_dict(item) for
|
||||
item in req.json()]
|
||||
|
||||
except (ha.HomeAssistantError, ValueError, AttributeError):
|
||||
# ValueError if req.json() can't parse the json
|
||||
# AttributeError if parsed JSON was not a dict
|
||||
if logger:
|
||||
logger.exception("Error getting state")
|
||||
|
||||
@ -380,8 +376,8 @@ def set_state(api, entity_id, new_state, attributes=None, logger=None):
|
||||
|
||||
attributes = attributes or {}
|
||||
|
||||
data = {'new_state': new_state,
|
||||
'attributes': json.dumps(attributes)}
|
||||
data = {'state': new_state,
|
||||
'attributes': attributes}
|
||||
|
||||
try:
|
||||
req = api(METHOD_POST,
|
||||
@ -410,11 +406,10 @@ def get_services(api, logger=None):
|
||||
try:
|
||||
req = api(METHOD_GET, URL_API_SERVICES)
|
||||
|
||||
return req.json()['services'] if req.status_code == 200 else {}
|
||||
return req.json() if req.status_code == 200 else {}
|
||||
|
||||
except (ha.HomeAssistantError, ValueError, KeyError):
|
||||
except (ha.HomeAssistantError, ValueError):
|
||||
# ValueError if req.json() can't parse the json
|
||||
# KeyError if not all expected keys are in the returned JSON
|
||||
if logger:
|
||||
logger.exception("ServiceRegistry:Got unexpected result")
|
||||
|
||||
|
@ -8,6 +8,7 @@ Provides tests to verify that Home Assistant modules do what they should do.
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
@ -19,6 +20,8 @@ API_PASSWORD = "test1234"
|
||||
|
||||
HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT)
|
||||
|
||||
HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD}
|
||||
|
||||
|
||||
def _url(path=""):
|
||||
""" Helper method to generate urls. """
|
||||
@ -37,7 +40,7 @@ def ensure_homeassistant_started():
|
||||
if not HAHelper.hass:
|
||||
hass = ha.HomeAssistant()
|
||||
|
||||
hass.bus.listen('test_event', len)
|
||||
hass.bus.listen('test_event', lambda _: _)
|
||||
hass.states.set('test', 'a_state')
|
||||
|
||||
http.setup(hass,
|
||||
@ -90,8 +93,7 @@ class TestHTTP(unittest.TestCase):
|
||||
""" Test if we can login by comparing not logged in screen to
|
||||
logged in screen. """
|
||||
|
||||
with_pw = requests.get(
|
||||
_url("/?api_password={}".format(API_PASSWORD)))
|
||||
with_pw = requests.get(_url(), headers=HA_HEADERS)
|
||||
|
||||
without_pw = requests.get(_url())
|
||||
|
||||
@ -107,62 +109,24 @@ class TestHTTP(unittest.TestCase):
|
||||
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")),
|
||||
params={"api_password": "not the password"})
|
||||
headers={remote.AUTH_HEADER: 'wrongpassword'})
|
||||
|
||||
self.assertEqual(req.status_code, 401)
|
||||
|
||||
def test_debug_change_state(self):
|
||||
""" Test if we can change a state from the debug interface. """
|
||||
self.hass.states.set("test.test", "not_to_be_set")
|
||||
|
||||
requests.post(_url(http.URL_CHANGE_STATE),
|
||||
data={"entity_id": "test.test",
|
||||
"new_state": "debug_state_change2",
|
||||
"api_password": API_PASSWORD})
|
||||
|
||||
self.assertEqual(self.hass.states.get("test.test").state,
|
||||
"debug_state_change2")
|
||||
|
||||
def test_debug_fire_event(self):
|
||||
""" Test if we can fire an event from the debug interface. """
|
||||
test_value = []
|
||||
|
||||
def listener(event): # pylint: disable=unused-argument
|
||||
""" 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.hass.listen_once_event("test_event_with_data", listener)
|
||||
|
||||
requests.post(
|
||||
_url(http.URL_FIRE_EVENT),
|
||||
data={"event_type": "test_event_with_data",
|
||||
"event_data": '{"test": 1}',
|
||||
"api_password": API_PASSWORD})
|
||||
|
||||
# Allow the event to take place
|
||||
time.sleep(1)
|
||||
|
||||
self.assertEqual(len(test_value), 1)
|
||||
|
||||
def test_api_list_state_entities(self):
|
||||
""" Test if the debug interface allows us to list state entities. """
|
||||
req = requests.get(_url(remote.URL_API_STATES),
|
||||
data={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
remote_data = req.json()
|
||||
remote_data = [ha.State.from_dict(item) for item in req.json()]
|
||||
|
||||
local_data = {entity_id: state.as_dict() for entity_id, state
|
||||
in self.hass.states.all().items()}
|
||||
|
||||
self.assertEqual(local_data, remote_data)
|
||||
self.assertEqual(self.hass.states.all(), remote_data)
|
||||
|
||||
def test_api_get(self):
|
||||
""" Test if the debug interface allows us to get a state. """
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("test")),
|
||||
data={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
data = ha.State.from_dict(req.json())
|
||||
|
||||
@ -176,9 +140,9 @@ class TestHTTP(unittest.TestCase):
|
||||
""" Test if the debug interface allows us to get a state. """
|
||||
req = requests.get(
|
||||
_url(remote.URL_API_STATES_ENTITY.format("does_not_exist")),
|
||||
params={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
self.assertEqual(req.status_code, 422)
|
||||
self.assertEqual(req.status_code, 404)
|
||||
|
||||
def test_api_state_change(self):
|
||||
""" Test if we can change the state of an entity that exists. """
|
||||
@ -186,8 +150,8 @@ class TestHTTP(unittest.TestCase):
|
||||
self.hass.states.set("test.test", "not_to_be_set")
|
||||
|
||||
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
|
||||
data={"new_state": "debug_state_change2",
|
||||
"api_password": API_PASSWORD})
|
||||
data=json.dumps({"state": "debug_state_change2",
|
||||
"api_password": API_PASSWORD}))
|
||||
|
||||
self.assertEqual(self.hass.states.get("test.test").state,
|
||||
"debug_state_change2")
|
||||
@ -202,8 +166,8 @@ class TestHTTP(unittest.TestCase):
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_STATES_ENTITY.format(
|
||||
"test_entity_that_does_not_exist")),
|
||||
data={"new_state": new_state,
|
||||
"api_password": API_PASSWORD})
|
||||
data=json.dumps({"state": new_state,
|
||||
"api_password": API_PASSWORD}))
|
||||
|
||||
cur_state = (self.hass.states.
|
||||
get("test_entity_that_does_not_exist").state)
|
||||
@ -224,7 +188,7 @@ class TestHTTP(unittest.TestCase):
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
|
||||
data={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
# Allow the event to take place
|
||||
time.sleep(1)
|
||||
@ -246,8 +210,8 @@ class TestHTTP(unittest.TestCase):
|
||||
|
||||
requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
|
||||
data={"event_data": '{"test": 1}',
|
||||
"api_password": API_PASSWORD})
|
||||
data=json.dumps({"event_data": {"test": 1}}),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
# Allow the event to take place
|
||||
time.sleep(1)
|
||||
@ -267,8 +231,8 @@ class TestHTTP(unittest.TestCase):
|
||||
|
||||
req = requests.post(
|
||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event")),
|
||||
data={"event_data": 'not json',
|
||||
"api_password": API_PASSWORD})
|
||||
data=json.dumps({"event_data": 'not an object'}),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
# It shouldn't but if it fires, allow the event to take place
|
||||
time.sleep(1)
|
||||
@ -279,20 +243,16 @@ class TestHTTP(unittest.TestCase):
|
||||
def test_api_get_event_listeners(self):
|
||||
""" Test if we can get the list of events being listened for. """
|
||||
req = requests.get(_url(remote.URL_API_EVENTS),
|
||||
params={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
data = req.json()
|
||||
|
||||
self.assertEqual(data['event_listeners'], self.hass.bus.listeners)
|
||||
self.assertEqual(req.json(), self.hass.bus.listeners)
|
||||
|
||||
def test_api_get_services(self):
|
||||
""" Test if we can get a dict describing current services. """
|
||||
req = requests.get(_url(remote.URL_API_SERVICES),
|
||||
params={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
data = req.json()
|
||||
|
||||
self.assertEqual(data['services'], self.hass.services.services)
|
||||
self.assertEqual(req.json(), self.hass.services.services)
|
||||
|
||||
def test_api_call_service_no_data(self):
|
||||
""" Test if the API allows us to call a service. """
|
||||
@ -307,7 +267,7 @@ class TestHTTP(unittest.TestCase):
|
||||
requests.post(
|
||||
_url(remote.URL_API_SERVICES_SERVICE.format(
|
||||
"test_domain", "test_service")),
|
||||
data={"api_password": API_PASSWORD})
|
||||
headers=HA_HEADERS)
|
||||
|
||||
# Allow the event to take place
|
||||
time.sleep(1)
|
||||
@ -329,8 +289,8 @@ class TestHTTP(unittest.TestCase):
|
||||
requests.post(
|
||||
_url(remote.URL_API_SERVICES_SERVICE.format(
|
||||
"test_domain", "test_service")),
|
||||
data={"service_data": '{"test": 1}',
|
||||
"api_password": API_PASSWORD})
|
||||
data=json.dumps({"service_data": {"test": 1}}),
|
||||
headers=HA_HEADERS)
|
||||
|
||||
# Allow the event to take place
|
||||
time.sleep(1)
|
||||
|
Loading…
Reference in New Issue
Block a user