Tons of fixes - WIP

This commit is contained in:
Paulus Schoutsen 2016-05-14 00:58:36 -07:00
parent 768c98d359
commit 15e329a588
22 changed files with 938 additions and 1604 deletions

View File

@ -7,14 +7,14 @@ https://home-assistant.io/components/alexa/
import enum import enum
import logging import logging
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script from homeassistant.helpers import template, script
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'alexa' DOMAIN = 'alexa'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_CONFIG = {}
API_ENDPOINT = '/api/alexa' API_ENDPOINT = '/api/alexa'
@ -26,80 +26,88 @@ CONF_ACTION = 'action'
def setup(hass, config): def setup(hass, config):
"""Activate Alexa component.""" """Activate Alexa component."""
intents = config[DOMAIN].get(CONF_INTENTS, {}) hass.wsgi.register_view(AlexaView(hass,
config[DOMAIN].get(CONF_INTENTS, {})))
for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
"Alexa intent {}".format(name))
_CONFIG.update(intents)
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
return True return True
def _handle_alexa(handler, path_match, data): class AlexaView(HomeAssistantView):
"""Handle Alexa.""" """Handle Alexa requests."""
_LOGGER.debug('Received Alexa request: %s', data)
req = data.get('request') url = API_ENDPOINT
name = 'api:alexa'
if req is None: def __init__(self, hass, intents):
_LOGGER.error('Received invalid data from Alexa: %s', data) """Initialize Alexa view."""
handler.write_json_message( super().__init__(hass)
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
req_type = req['type'] for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
if req_type == 'SessionEndedRequest': self.intents = intents
handler.send_response(HTTP_OK)
handler.end_headers()
return
intent = req.get('intent') def post(self, request):
response = AlexaResponse(handler.server.hass, intent) """Handle Alexa."""
data = request.json
if req_type == 'LaunchRequest': _LOGGER.debug('Received Alexa request: %s', data)
response.add_speech(
SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
handler.write_json(response.as_dict())
return
if req_type != 'IntentRequest': req = data.get('request')
_LOGGER.warning('Received unsupported request: %s', req_type)
return
intent_name = intent['name'] if req is None:
config = _CONFIG.get(intent_name) _LOGGER.error('Received invalid data from Alexa: %s', data)
return self.json_message('Expected request value not received',
HTTP_BAD_REQUEST)
if config is None: req_type = req['type']
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
handler.write_json(response.as_dict())
return
speech = config.get(CONF_SPEECH) if req_type == 'SessionEndedRequest':
card = config.get(CONF_CARD) return None
action = config.get(CONF_ACTION)
# pylint: disable=unsubscriptable-object intent = req.get('intent')
if speech is not None: response = AlexaResponse(self.hass, intent)
response.add_speech(SpeechType[speech['type']], speech['text'])
if card is not None: if req_type == 'LaunchRequest':
response.add_card(CardType[card['type']], card['title'], response.add_speech(
card['content']) SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
return self.json(response)
if action is not None: if req_type != 'IntentRequest':
action.run(response.variables) _LOGGER.warning('Received unsupported request: %s', req_type)
return self.json_message(
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)
handler.write_json(response.as_dict()) intent_name = intent['name']
config = self.intents.get(intent_name)
if config is None:
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(response)
speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD)
action = config.get(CONF_ACTION)
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(SpeechType[speech['type']], speech['text'])
if card is not None:
response.add_card(CardType[card['type']], card['title'],
card['content'])
if action is not None:
action.run(response.variables)
return self.json(response)
class SpeechType(enum.Enum): class SpeechType(enum.Enum):

View File

@ -6,16 +6,14 @@ https://home-assistant.io/developers/api/
""" """
import json import json
import logging import logging
import re
import threading
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import ( from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES, URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
@ -23,10 +21,11 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates from homeassistant.helpers.state import TrackStates
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.components.wsgi import HomeAssistantView from homeassistant.helpers.event import track_utc_time_change
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'api' DOMAIN = 'api'
DEPENDENCIES = ['http', 'wsgi'] DEPENDENCIES = ['http']
STREAM_PING_PAYLOAD = "ping" STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds STREAM_PING_INTERVAL = 50 # seconds
@ -36,70 +35,6 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /api/discovery_info
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
_handle_get_api_discovery_info,
require_auth=False)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_get_api_states_entity)
hass.http.register_path(
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
hass.http.register_path(
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
hass.http.register_path(
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_delete_state_entity)
# /api/events
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
hass.http.register_path(
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
_handle_api_post_events_event)
# /api/services
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
hass.http.register_path(
'POST',
re.compile((r'/api/services/'
r'(?P<domain>[a-zA-Z\._0-9]+)/'
r'(?P<service>[a-zA-Z\._0-9]+)')),
_handle_post_api_services_domain_service)
# /api/event_forwarding
hass.http.register_path(
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
hass.http.register_path(
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
# /api/components
hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components)
# /api/error_log
hass.http.register_path('GET', URL_API_ERROR_LOG,
_handle_get_api_error_log)
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
# /api/template
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
hass.wsgi.register_view(APIStatusView) hass.wsgi.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream) hass.wsgi.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView) hass.wsgi.register_view(APIConfigView)
@ -120,159 +55,143 @@ def setup(hass, config):
class APIStatusView(HomeAssistantView): class APIStatusView(HomeAssistantView):
"""View to handle Status requests."""
url = URL_API url = URL_API
name = "api:status" name = "api:status"
def get(self, request): def get(self, request):
return {'message': 'API running.'} """Retrieve if API is running."""
return self.json_message('API running.')
def _handle_get_api(handler, path_match, data):
"""Render the debug interface."""
handler.write_json_message("API running.")
class APIEventStream(HomeAssistantView): class APIEventStream(HomeAssistantView):
url = "" """View to handle EventSt requests."""
name = ""
# TODO Implement this... url = URL_API_STREAM
name = "api:stream"
def get(self, request):
"""Provide a streaming interface for the event bus."""
from eventlet import Queue
def _handle_get_api_stream(handler, path_match, data): queue = Queue()
"""Provide a streaming interface for the event bus.""" stop_obj = object()
gracefully_closed = False hass = self.hass
hass = handler.server.hass
wfile = handler.wfile
write_lock = threading.Lock()
block = threading.Event()
session_id = None
restrict = data.get('restrict') restrict = request.args.get('restrict')
if restrict: if restrict:
restrict = restrict.split(',') restrict = restrict.split(',')
def write_message(payload): def ping(now):
"""Write a message to the output.""" """Add a ping message to queue."""
with write_lock: queue.put(STREAM_PING_PAYLOAD)
msg = "data: {}\n\n".format(payload)
def forward_events(event):
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
pass
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
queue.put(stop_obj)
else:
queue.put(json.dumps(event, cls=rem.JSONEncoder))
def stream():
"""Stream events to response."""
if restrict:
for event in restrict:
hass.bus.listen(event, forward_events)
else:
hass.bus.listen(MATCH_ALL, forward_events)
attached_ping = track_utc_time_change(hass, ping, second=(0, 30))
try: try:
wfile.write(msg.encode("UTF-8")) while True:
wfile.flush() payload = queue.get()
except (IOError, ValueError):
# IOError: socket errors
# ValueError: raised when 'I/O operation on closed file'
block.set()
def forward_events(event): if payload is stop_obj:
"""Forward events to the open request.""" break
nonlocal gracefully_closed
if block.is_set() or event.event_type == EVENT_TIME_CHANGED: msg = "data: {}\n\n".format(payload)
return
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
gracefully_closed = True
block.set()
return
handler.server.sessions.extend_validation(session_id) yield msg.encode("UTF-8")
write_message(json.dumps(event, cls=rem.JSONEncoder)) except GeneratorExit:
pass
handler.send_response(HTTP_OK) hass.bus.remove_listener(EVENT_TIME_CHANGED, attached_ping)
handler.send_header('Content-type', 'text/event-stream')
session_id = handler.set_session_cookie_header()
handler.end_headers()
if restrict: if restrict:
for event in restrict: for event in restrict:
hass.bus.listen(event, forward_events) hass.bus.remove_listener(event, forward_events)
else: else:
hass.bus.listen(MATCH_ALL, forward_events) hass.bus.remove_listener(MATCH_ALL, forward_events)
while True: return self.Response(stream(), mimetype='text/event-stream')
write_message(STREAM_PING_PAYLOAD)
block.wait(STREAM_PING_INTERVAL)
if block.is_set():
break
if not gracefully_closed:
_LOGGER.info("Found broken event stream to %s, cleaning up",
handler.client_address[0])
if restrict:
for event in restrict:
hass.bus.remove_listener(event, forward_events)
else:
hass.bus.remove_listener(MATCH_ALL, forward_events)
class APIConfigView(HomeAssistantView): class APIConfigView(HomeAssistantView):
"""View to handle Config requests."""
url = URL_API_CONFIG url = URL_API_CONFIG
name = "api:config" name = "api:config"
def get(self, request): def get(self, request):
return self.hass.config.as_dict() """Get current configuration."""
return self.json(self.hass.config.as_dict())
def _handle_get_api_config(handler, path_match, data):
"""Return the Home Assistant configuration."""
handler.write_json(handler.server.hass.config.as_dict())
class APIDiscoveryView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView):
"""View to provide discovery info."""
requires_auth = False
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = "api:discovery" name = "api:discovery"
def get(self, request): def get(self, request):
# TODO """Get discovery info."""
return {} needs_auth = self.hass.config.api.api_password is not None
return self.json({
'base_url': self.hass.config.api.base_url,
def _handle_get_api_discovery_info(handler, path_match, data): 'location_name': self.hass.config.location_name,
needs_auth = (handler.server.hass.config.api.api_password is not None) 'requires_api_password': needs_auth,
params = { 'version': __version__
'base_url': handler.server.hass.config.api.base_url, })
'location_name': handler.server.hass.config.location_name,
'requires_api_password': needs_auth,
'version': __version__
}
handler.write_json(params)
class APIStatesView(HomeAssistantView): class APIStatesView(HomeAssistantView):
"""View to handle States requests."""
url = URL_API_STATES url = URL_API_STATES
name = "api:states" name = "api:states"
def get(self, request): def get(self, request):
return self.hass.states.all() """Get current states."""
return self.json(self.hass.states.all())
def _handle_get_api_states(handler, path_match, data):
"""Return a dict containing all entity ids and their state."""
handler.write_json(handler.server.hass.states.all())
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""
url = "/api/states/<entity_id>" url = "/api/states/<entity_id>"
name = "api:entity-state" name = "api:entity-state"
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity."""
state = self.hass.states.get(entity_id) state = self.hass.states.get(entity_id)
if state: if state:
return state return self.json(state)
else: else:
raise self.NotFound("State does not exist.") return self.json_message('Entity not found', HTTP_NOT_FOUND)
def post(self, request, entity_id): def post(self, request, entity_id):
"""Update state of entity."""
try: try:
new_state = request.values['state'] new_state = request.json['state']
except KeyError: except KeyError:
raise self.BadRequest("state not specified") return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.values.get('attributes') attributes = request.json.get('attributes')
is_new_state = self.hass.states.get(entity_id) is None is_new_state = self.hass.states.get(entity_id) is None
@ -280,13 +199,7 @@ class APIEntityStateView(HomeAssistantView):
self.hass.states.set(entity_id, new_state, attributes) self.hass.states.set(entity_id, new_state, attributes)
# Read the state back for our response # Read the state back for our response
msg = json.dumps( resp = self.json(self.hass.states.get(entity_id))
self.hass.states.get(entity_id).as_dict(),
sort_keys=True,
cls=rem.JSONEncoder
).encode('UTF-8')
resp = Response(msg, mimetype="application/json")
if is_new_state: if is_new_state:
resp.status_code = HTTP_CREATED resp.status_code = HTTP_CREATED
@ -296,93 +209,37 @@ class APIEntityStateView(HomeAssistantView):
return resp return resp
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity."""
if self.hass.states.remove(entity_id): if self.hass.states.remove(entity_id):
return {"message:" "Entity removed"} return self.json_message('Entity removed')
else: else:
return { return self.json_message('Entity not found', HTTP_NOT_FOUND)
"message": "Entity not found",
"status_code": HTTP_NOT_FOUND,
}
def _handle_get_api_states_entity(handler, path_match, data):
"""Return the state of a specific entity."""
entity_id = path_match.group('entity_id')
state = handler.server.hass.states.get(entity_id)
if state:
handler.write_json(state)
else:
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
def _handle_post_state_entity(handler, path_match, data):
"""Handle 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:
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
return
attributes = data['attributes'] if 'attributes' in data else None
is_new_state = handler.server.hass.states.get(entity_id) is None
# Write state
handler.server.hass.states.set(entity_id, new_state, attributes)
state = handler.server.hass.states.get(entity_id)
status_code = HTTP_CREATED if is_new_state else HTTP_OK
handler.write_json(
state.as_dict(),
status_code=status_code,
location=URL_API_STATES_ENTITY.format(entity_id))
def _handle_delete_state_entity(handler, path_match, data):
"""Handle request to delete an entity from state machine.
This handles the following paths:
/api/states/<entity_id>
"""
entity_id = path_match.group('entity_id')
if handler.server.hass.states.remove(entity_id):
handler.write_json_message(
"Entity not found", HTTP_NOT_FOUND)
else:
handler.write_json_message(
"Entity removed", HTTP_OK)
class APIEventListenersView(HomeAssistantView): class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests."""
url = URL_API_EVENTS url = URL_API_EVENTS
name = "api:event-listeners" name = "api:event-listeners"
def get(self, request): def get(self, request):
return events_json(self.hass) """Get event listeners."""
return self.json(events_json(self.hass))
def _handle_get_api_events(handler, path_match, data):
"""Handle getting overview of event listeners."""
handler.write_json(events_json(handler.server.hass))
class APIEventView(HomeAssistantView): class APIEventView(HomeAssistantView):
"""View to handle Event requests."""
url = '/api/events/<event_type>' url = '/api/events/<event_type>'
name = "api:event" name = "api:event"
def post(self, request, event_type): def post(self, request, event_type):
event_data = request.values """Fire events."""
event_data = request.json
if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object',
HTTP_BAD_REQUEST)
# Special case handling for event STATE_CHANGED # Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects # We will try to convert state dicts back to State objects
@ -393,266 +250,150 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = state event_data[key] = state
self.hass.bus.fire(event_type, request.values, ha.EventOrigin.remote) self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
return {"message": "Event {} fired.".format(event_type)} return self.json_message("Event {} fired.".format(event_type))
def _handle_api_post_events_event(handler, path_match, event_data):
"""Handle 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')
if event_data is not None and not isinstance(event_data, dict):
handler.write_json_message(
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
return
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
handler.server.hass.bus.fire(event_type, event_data, event_origin)
handler.write_json_message("Event {} fired.".format(event_type))
class APIServicesView(HomeAssistantView): class APIServicesView(HomeAssistantView):
"""View to handle Services requests."""
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = "api:services"
def get(self, request): def get(self, request):
return services_json(self.hass) """Get registered services."""
return self.json(services_json(self.hass))
def _handle_get_api_services(handler, path_match, data):
"""Handle getting overview of services."""
handler.write_json(services_json(handler.server.hass))
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>" url = "/api/services/<domain>/<service>"
name = "api:domain-services" name = "api:domain-services"
def post(self, request): def post(self, request, domain, service):
"""Call a service.
Returns a list of changed states.
"""
with TrackStates(self.hass) as changed_states: with TrackStates(self.hass) as changed_states:
self.hass.services.call(domain, service, request.values, True) self.hass.services.call(domain, service, request.json, True)
return changed_states return self.json(changed_states)
# pylint: disable=invalid-name
def _handle_post_api_services_domain_service(handler, path_match, data):
"""Handle calling a service.
This handles the following paths: /api/services/<domain>/<service>
"""
domain = path_match.group('domain')
service = path_match.group('service')
with TrackStates(handler.server.hass) as changed_states:
handler.server.hass.services.call(domain, service, data, True)
handler.write_json(changed_states)
class APIEventForwardingView(HomeAssistantView): class APIEventForwardingView(HomeAssistantView):
"""View to handle EventForwarding requests."""
url = URL_API_EVENT_FORWARD url = URL_API_EVENT_FORWARD
name = "api:event-forward" name = "api:event-forward"
event_forwarder = None
def post(self, request): def post(self, request):
"""Setup an event forwarder."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
host = request.values['host'] host = data['host']
api_password = request.values['api_password'] api_password = data['api_password']
except KeyError: except KeyError:
return { return self.json_message("No host or api_password received.",
"message": "No host or api_password received.", HTTP_BAD_REQUEST)
"status_code": HTTP_BAD_REQUEST,
}
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
return { return self.json_message("Invalid value received for port.",
"message": "Invalid value received for port.", HTTP_UNPROCESSABLE_ENTITY)
"status_code": HTTP_UNPROCESSABLE_ENTITY,
}
api = rem.API(host, api_password, port) api = rem.API(host, api_password, port)
if not api.validate_api(): if not api.validate_api():
return { return self.json_message("Unable to validate API.",
"message": "Unable to validate API.", HTTP_UNPROCESSABLE_ENTITY)
"status_code": HTTP_UNPROCESSABLE_ENTITY,
}
if self.hass.event_forwarder is None: if self.event_forwarder is None:
self.hass.event_forwarder = rem.EventForwarder(self.hass) self.event_forwarder = rem.EventForwarder(self.hass)
self.hass.event_forwarder.connect(api) self.event_forwarder.connect(api)
return {"message": "Event forwarding setup."} return self.json_message("Event forwarding setup.")
def delete(self, request): def delete(self, request):
"""Remove event forwarer."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
host = request.values['host'] host = data['host']
except KeyError: except KeyError:
return { return self.json_message("No host received.", HTTP_BAD_REQUEST)
"message": "No host received.",
"status_code": HTTP_BAD_REQUEST,
}
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
return { return self.json_message("Invalid value received for port.",
"message": "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
"status_code": HTTP_UNPROCESSABLE_ENTITY,
}
if self.hass.event_forwarder is not None: if self.event_forwarder is not None:
api = rem.API(host, None, port) api = rem.API(host, None, port)
self.hass.event_forwarder.disconnect(api) self.event_forwarder.disconnect(api)
return {"message": "Event forwarding cancelled."} return self.json_message("Event forwarding cancelled.")
# pylint: disable=invalid-name
def _handle_post_api_event_forward(handler, path_match, data):
"""Handle adding an event forwarding target."""
try:
host = data['host']
api_password = data['api_password']
except KeyError:
handler.write_json_message(
"No host or api_password received.", HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
api = rem.API(host, api_password, port)
if not api.validate_api():
handler.write_json_message(
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is None:
handler.server.event_forwarder = \
rem.EventForwarder(handler.server.hass)
handler.server.event_forwarder.connect(api)
handler.write_json_message("Event forwarding setup.")
def _handle_delete_api_event_forward(handler, path_match, data):
"""Handle deleting an event forwarding target."""
try:
host = data['host']
except KeyError:
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is not None:
api = rem.API(host, None, port)
handler.server.event_forwarder.disconnect(api)
handler.write_json_message("Event forwarding cancelled.")
class APIComponentsView(HomeAssistantView): class APIComponentsView(HomeAssistantView):
"""View to handle Components requests."""
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = "api:components" name = "api:components"
def get(self, request): def get(self, request):
return self.hass.config.components """Get current loaded components."""
return self.json(self.hass.config.components)
def _handle_get_api_components(handler, path_match, data):
"""Return all the loaded components."""
handler.write_json(handler.server.hass.config.components)
class APIErrorLogView(HomeAssistantView): class APIErrorLogView(HomeAssistantView):
"""View to handle ErrorLog requests."""
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = "api:error-log" name = "api:error-log"
def get(self, request): def get(self, request):
# TODO """Serve error log."""
return {} return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
def _handle_get_api_error_log(handler, path_match, data):
"""Return the logged errors for this session."""
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
False)
class APILogOutView(HomeAssistantView): class APILogOutView(HomeAssistantView):
"""View to handle Log Out requests."""
url = URL_API_LOG_OUT url = URL_API_LOG_OUT
name = "api:log-out" name = "api:log-out"
def post(self, request): def post(self, request):
# TODO """Handle log out."""
# TODO kill session
return {} return {}
def _handle_post_api_log_out(handler, path_match, data):
"""Log user out."""
handler.send_response(HTTP_OK)
handler.destroy_session()
handler.end_headers()
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle requests."""
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = "api:template"
def post(self, request): def post(self, request):
# TODO """Render a template."""
return {} try:
return template.render(self.hass, request.json['template'],
request.json.get('variables'))
def _handle_post_api_template(handler, path_match, data): except TemplateError as ex:
"""Log user out.""" return self.json_message('Error rendering template: {}'.format(ex),
template_string = data.get('template', '') HTTP_BAD_REQUEST)
try:
rendered = template.render(handler.server.hass, template_string)
handler.send_response(HTTP_OK)
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
handler.end_headers()
handler.wfile.write(rendered.encode('utf-8'))
except TemplateError as e:
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
return
def services_json(hass): def services_json(hass):

View File

@ -6,17 +6,12 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import logging import logging
import re
import time
import requests
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import bloomsky from homeassistant.components import bloomsky
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -45,57 +40,11 @@ def setup(hass, config):
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS) DISCOVERY_PLATFORMS)
hass.wsgi.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config) component.setup(config)
def _proxy_camera_image(handler, path_match, data):
"""Serve the camera image via the HA server."""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
if camera is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
response = camera.camera_image()
if response is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
handler.send_response(HTTP_OK)
handler.write_content(response)
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image)
def _proxy_camera_mjpeg_stream(handler, path_match, data):
"""Proxy the camera image as an mjpeg stream via the HA server."""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
if camera is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
try:
camera.is_streaming = True
camera.update_ha_state()
camera.mjpeg_stream(handler)
except (requests.RequestException, IOError):
camera.is_streaming = False
camera.update_ha_state()
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_mjpeg_stream)
return True return True
@ -135,32 +84,39 @@ class Camera(Entity):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
def mjpeg_stream(self, handler): def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images.""" """Generate an HTTP MJPEG stream from camera images."""
def write_string(text): import eventlet
"""Helper method to write a string to the stream.""" response.mimetype = ('multipart/x-mixed-replace; '
handler.request.sendall(bytes(text + '\r\n', 'utf-8')) 'boundary={}'.format(MULTIPART_BOUNDARY))
write_string('HTTP/1.1 200 OK') boundary = bytes('\r\n{}\r\n'.format(MULTIPART_BOUNDARY), 'utf-8')
write_string('Content-type: multipart/x-mixed-replace; '
'boundary={}'.format(MULTIPART_BOUNDARY))
write_string('')
write_string(MULTIPART_BOUNDARY)
while True: def stream():
img_bytes = self.camera_image() """Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
if img_bytes is None: if img_bytes is None:
continue continue
elif img_bytes == last_image:
eventlet.sleep(0.5)
write_string('Content-length: {}'.format(len(img_bytes))) yield bytes('Content-length: {}'.format(len(img_bytes)) +
write_string('Content-type: image/jpeg') '\r\nContent-type: image/jpeg\r\n\r\n',
write_string('') 'utf-8')
handler.request.sendall(img_bytes) yield img_bytes
write_string('') yield boundary
write_string(MULTIPART_BOUNDARY)
time.sleep(0.5) eventlet.sleep(0.5)
except GeneratorExit:
pass
response.response = stream()
return response
@property @property
def state(self): def state(self):
@ -184,3 +140,49 @@ class Camera(Entity):
attr['brand'] = self.brand attr['brand'] = self.brand
return attr return attr
class CameraView(HomeAssistantView):
"""Base CameraView."""
def __init__(self, hass, entities):
"""Initialize a basic camera view."""
super().__init__(hass)
self.entities = entities
class CameraImageView(CameraView):
"""Camera view to serve an image."""
url = "/api/camera_proxy/<entity_id>"
name = "api:camera:image"
def get(self, request, entity_id):
"""Serve camera image."""
camera = self.entities.get(entity_id)
if camera is None:
return self.Response(status=404)
response = camera.camera_image()
if response is None:
return self.Response(status=500)
return self.Response(response)
class CameraMjpegStream(CameraView):
"""Camera View to serve an MJPEG stream."""
url = "/api/camera_proxy_stream/<entity_id>"
name = "api:camera:stream"
def get(self, request, entity_id):
"""Serve camera image."""
camera = self.entities.get(entity_id)
if camera is None:
return self.Response(status=404)
return camera.mjpeg_stream(self.Response())

View File

@ -11,7 +11,6 @@ import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.const import HTTP_OK
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
CONTENT_TYPE_HEADER = 'Content-Type' CONTENT_TYPE_HEADER = 'Content-Type'
@ -68,19 +67,12 @@ class MjpegCamera(Camera):
with closing(self.camera_stream()) as response: with closing(self.camera_stream()) as response:
return process_response(response) return process_response(response)
def mjpeg_stream(self, handler): def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
response = self.camera_stream() stream = self.camera_stream()
content_type = response.headers[CONTENT_TYPE_HEADER] response.mimetype = stream.headers[CONTENT_TYPE_HEADER]
response.response = stream.iter_content(chunk_size=1024)
handler.send_response(HTTP_OK) return response
handler.send_header(CONTENT_TYPE_HEADER, content_type)
handler.end_headers()
for chunk in response.iter_content(chunk_size=1024):
if not chunk:
break
handler.wfile.write(chunk)
@property @property
def name(self): def name(self):

View File

@ -5,95 +5,92 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/ https://home-assistant.io/components/device_tracker.locative/
""" """
import logging import logging
from functools import partial
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup an endpoint for the Locative application.""" """Setup an endpoint for the Locative application."""
# POST would be semantically better, but that currently does not work hass.wsgi.register_view(LocativeView(hass, see))
# since Locative sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_LOCATIVE_ENDPOINT,
partial(_handle_get_api_locative, hass, see))
return True return True
def _handle_get_api_locative(hass, see, handler, path_match, data): class LocativeView(HomeAssistantView):
"""Locative message received.""" """View to handle locative requests."""
if not _check_data(handler, data):
return
device = data['device'].replace('-', '') url = "/api/locative"
location_name = data['id'].lower() name = "api:bootstrap"
direction = data['trigger']
if direction == 'enter': def __init__(self, hass, see):
see(dev_id=device, location_name=location_name) """Initialize Locative url endpoints."""
handler.write_text("Setting location to {}".format(location_name)) super().__init__(hass)
self.see = see
elif direction == 'exit': def get(self, request):
current_state = hass.states.get("{}.{}".format(DOMAIN, device)) """Locative message received as GET."""
return self.post(request)
def post(self, request):
"""Locative message received."""
# pylint: disable=too-many-return-statements
data = request.values
if 'latitude' not in data or 'longitude' not in data:
return ("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error("Device id not specified.")
return ("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'id' not in data:
_LOGGER.error("Location id not specified.")
return ("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
if 'trigger' not in data:
_LOGGER.error("Trigger is not specified.")
return ("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
if direction == 'enter':
self.see(dev_id=device, location_name=location_name)
return "Setting location to {}".format(location_name)
elif direction == 'exit':
current_state = self.hass.states.get(
"{}.{}".format(DOMAIN, device))
if current_state is None or current_state.state == location_name:
self.see(dev_id=device, location_name=STATE_NOT_HOME)
return "Setting location to not home"
else:
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered
# before the previous zone was exited. The enter message will
# be sent first, then the exit message will be sent second.
return 'Ignoring exit from {} (already in {})'.format(
location_name, current_state)
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
return "Received test message."
if current_state is None or current_state.state == location_name:
see(dev_id=device, location_name=STATE_NOT_HOME)
handler.write_text("Setting location to not home")
else: else:
# Ignore the message if it is telling us to exit a zone that we _LOGGER.error("Received unidentified message from Locative: %s",
# aren't currently in. This occurs when a zone is entered before direction)
# the previous zone was exited. The enter message will be sent return ("Received unidentified message: {}".format(direction),
# first, then the exit message will be sent second. HTTP_UNPROCESSABLE_ENTITY)
handler.write_text(
'Ignoring exit from {} (already in {})'.format(
location_name, current_state))
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
handler.write_text("Received test message.")
else:
handler.write_text(
"Received unidentified message: {}".format(direction),
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Received unidentified message from Locative: %s",
direction)
def _check_data(handler, data):
"""Check the data."""
if 'latitude' not in data or 'longitude' not in data:
handler.write_text("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Latitude and longitude not specified.")
return False
if 'device' not in data:
handler.write_text("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Device id not specified.")
return False
if 'id' not in data:
handler.write_text("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Location id not specified.")
return False
if 'trigger' not in data:
handler.write_text("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Trigger is not specified.")
return False
return True

View File

@ -4,10 +4,9 @@ import os
import logging import logging
from . import version, mdi_version from . import version, mdi_version
import homeassistant.util as util from homeassistant.const import URL_ROOT
from homeassistant.const import URL_ROOT, HTTP_OK
from homeassistant.components import api from homeassistant.components import api
from homeassistant.components.wsgi import HomeAssistantView from homeassistant.components.http import HomeAssistantView
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api'] DEPENDENCIES = ['api']
@ -29,27 +28,6 @@ _FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
def setup(hass, config): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', '/service_worker.js',
_handle_get_service_worker, False)
# Bootstrap API
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# Static files
hass.http.register_path(
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_static, False)
hass.http.register_path(
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_static, False)
hass.http.register_path(
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
_handle_get_local, False)
hass.wsgi.register_view(IndexView) hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView) hass.wsgi.register_view(BootstrapView)
@ -70,32 +48,37 @@ def setup(hass, config):
class BootstrapView(HomeAssistantView): class BootstrapView(HomeAssistantView):
"""View to bootstrap frontend with all needed data."""
url = URL_API_BOOTSTRAP url = URL_API_BOOTSTRAP
name = "api:bootstrap" name = "api:bootstrap"
def get(self, request): def get(self, request):
"""Return all data needed to bootstrap Home Assistant.""" """Return all data needed to bootstrap Home Assistant."""
return self.json({
return {
'config': self.hass.config.as_dict(), 'config': self.hass.config.as_dict(),
'states': self.hass.states.all(), 'states': self.hass.states.all(),
'events': api.events_json(self.hass), 'events': api.events_json(self.hass),
'services': api.services_json(self.hass), 'services': api.services_json(self.hass),
} })
class IndexView(HomeAssistantView): class IndexView(HomeAssistantView):
"""Serve the frontend."""
url = URL_ROOT url = URL_ROOT
name = "frontend:index" name = "frontend:index"
requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState', extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate', '/states/<entity>'] '/devEvent', '/devInfo', '/devTemplate', '/states/<entity>']
def __init__(self, hass): def __init__(self, hass):
"""Initialize the frontend view."""
super().__init__(hass) super().__init__(hass)
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
self.TEMPLATES = Environment( self.templates = Environment(
loader=FileSystemLoader( loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/') os.path.join(os.path.dirname(__file__), 'templates/')
) )
@ -106,81 +89,12 @@ class IndexView(HomeAssistantView):
app_url = "frontend-{}.html".format(version.VERSION) app_url = "frontend-{}.html".format(version.VERSION)
# auto login if no password was set, else check api_password param # auto login if no password was set, else check api_password param
auth = ('no_password_set' if request.api_password is None auth = ('no_password_set' if self.hass.config.api.api_password is None
else request.values.get('api_password', '')) else request.values.get('api_password', ''))
template = self.TEMPLATES.get_template('index.html') template = self.templates.get_template('index.html')
resp = template.render(app_url=app_url, auth=auth, resp = template.render(app_url=app_url, auth=auth,
icons=mdi_version.VERSION) icons=mdi_version.VERSION)
return self.Response(resp, mimetype="text/html") return self.Response(resp, mimetype="text/html")
def _handle_get_api_bootstrap(handler, path_match, data):
"""Return all data needed to bootstrap Home Assistant."""
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': api.events_json(hass),
'services': api.services_json(hass),
})
def _handle_get_root(handler, path_match, data):
"""Render the frontend."""
if handler.server.development:
app_url = "home-assistant-polymer/src/home-assistant.html"
else:
app_url = "frontend-{}.html".format(version.VERSION)
# auto login if no password was set, else check api_password param
auth = ('no_password_set' if handler.server.api_password is None
else data.get('api_password', ''))
with open(INDEX_PATH) as template_file:
template_html = template_file.read()
template_html = template_html.replace('{{ app_url }}', app_url)
template_html = template_html.replace('{{ auth }}', auth)
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
handler.send_response(HTTP_OK)
handler.write_content(template_html.encode("UTF-8"),
'text/html; charset=utf-8')
def _handle_get_service_worker(handler, path_match, data):
"""Return service worker for the frontend."""
if handler.server.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
sw_path))
def _handle_get_static(handler, path_match, data):
"""Return a static file for the frontend."""
req_file = util.sanitize_path(path_match.group('file'))
# Strip md5 hash out
fingerprinted = _FINGERPRINT.match(req_file)
if fingerprinted:
req_file = "{}.{}".format(*fingerprinted.groups())
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
handler.write_file(path)
def _handle_get_local(handler, path_match, data):
"""Return a static file from the hass.config.path/www for the frontend."""
req_file = util.sanitize_path(path_match.group('file'))
path = handler.server.hass.config.path('www', req_file)
handler.write_file(path)

View File

@ -12,6 +12,7 @@ from itertools import groupby
from homeassistant.components import recorder, script from homeassistant.components import recorder, script
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history' DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
@ -155,49 +156,51 @@ def get_state(utc_point_in_time, entity_id, run=None):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup(hass, config): def setup(hass, config):
"""Setup the history hooks.""" """Setup the history hooks."""
hass.http.register_path( hass.wsgi.register_view(Last5StatesView)
'GET', hass.wsgi.register_view(HistoryPeriodView)
re.compile(
r'/api/history/entity/(?P<entity_id>[a-zA-Z\._0-9]+)/'
r'recent_states'),
_api_last_5_states)
hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
return True return True
# pylint: disable=unused-argument class Last5StatesView(HomeAssistantView):
# pylint: disable=invalid-name """Handle last 5 state view requests."""
def _api_last_5_states(handler, path_match, data):
"""Return the last 5 states for an entity id as JSON."""
entity_id = path_match.group('entity_id')
handler.write_json(last_5_states(entity_id)) url = '/api/history/entity/<entity_id>/recent_states'
name = 'api:history:entity-recent-states'
def get(self, request, entity_id):
"""Retrieve last 5 states of entity."""
return self.json(last_5_states(entity_id))
def _api_history_period(handler, path_match, data): class HistoryPeriodView(HomeAssistantView):
"""Return history over a period of time.""" """Handle history period requests."""
date_str = path_match.group('date')
one_day = timedelta(seconds=86400)
if date_str: url = '/api/history/period'
start_date = dt_util.parse_date(date_str) name = 'api:history:entity-recent-states'
extra_urls = ['/api/history/period/<date>']
if start_date is None: def get(self, request, date=None):
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST) """Return history over a period of time."""
return one_day = timedelta(seconds=86400)
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date)) if date:
else: start_date = dt_util.parse_date(date)
start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day if start_date is None:
return self.json_message('Error parsing JSON',
HTTP_BAD_REQUEST)
entity_id = data.get('filter_entity_id') start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
else:
start_time = dt_util.utcnow() - one_day
handler.write_json( end_time = start_time + one_day
get_significant_states(start_time, end_time, entity_id).values())
entity_id = request.args.get('filter_entity_id')
return self.json(
get_significant_states(start_time, end_time, entity_id).values())
def _is_significant(state): def _is_significant(state):

View File

@ -1,41 +1,17 @@
""" """This module provides WSGI application to serve the Home Assistant API."""
This module provides an API and a HTTP interface for debug purposes.
For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/
"""
import gzip
import hmac import hmac
import json import json
import logging import logging
import ssl
import threading import threading
import time import re
from datetime import timedelta
from http import cookies
from http.server import HTTPServer, SimpleHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import parse_qs, urlparse
import voluptuous as vol
import homeassistant.bootstrap as bootstrap
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.remote as rem import homeassistant.remote as rem
import homeassistant.util as util from homeassistant import util
import homeassistant.util.dt as date_util from homeassistant.const import SERVER_PORT, HTTP_HEADER_HA_AUTH
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
ALLOWED_CORS_HEADERS,
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
DOMAIN = "http" DOMAIN = "http"
REQUIREMENTS = ("eventlet==0.18.4", "static3==0.6.1", "Werkzeug==0.11.5",)
CONF_API_PASSWORD = "api_password" CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
@ -43,61 +19,42 @@ CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development" CONF_DEVELOPMENT = "development"
CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key' CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins'
DATA_API_PASSWORD = 'api_password' DATA_API_PASSWORD = 'api_password'
# Throttling time in seconds for expired sessions check _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
SESSION_TIMEOUT_SECONDS = 1800
SESSION_KEY = 'sessionId'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_PASSWORD): cv.string,
vol.Optional(CONF_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_DEVELOPMENT): cv.string,
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): cv.ensure_list
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Set up the HTTP API and debug interface.""" """Set up the HTTP API and debug interface."""
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
api_password = util.convert(conf.get(CONF_API_PASSWORD), str) api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
# If no server host is given, accept all incoming requests
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1" development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY) ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
try: server = HomeAssistantWSGI(
server = HomeAssistantHTTPServer( hass,
(server_host, server_port), RequestHandler, hass, api_password, development=development,
development, ssl_certificate, ssl_key, cors_origins) server_host=server_host,
except OSError: server_port=server_port,
# If address already in use api_password=api_password,
_LOGGER.exception("Error setting up HTTP server") ssl_certificate=ssl_certificate,
return False ssl_key=ssl_key,
)
hass.bus.listen_once( hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START, ha.EVENT_HOMEASSISTANT_START,
lambda event: lambda event:
threading.Thread(target=server.start, daemon=True, threading.Thread(target=server.start, daemon=True,
name='HTTP-server').start()) name='WSGI-server').start())
hass.http = server hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0' hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
else util.get_local_ip(), else util.get_local_ip(),
api_password, server_port, api_password, server_port,
@ -106,413 +63,277 @@ def setup(hass, config):
return True return True
# pylint: disable=too-many-instance-attributes # class StaticFileServer(object):
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): # """Static file serving middleware."""
"""Handle HTTP requests in a threaded fashion."""
# pylint: disable=too-few-public-methods # def __call__(self, environ, start_response):
allow_reuse_address = True # from werkzeug.wsgi import DispatcherMiddleware
daemon_threads = True # app = DispatcherMiddleware(self.base_app, self.extra_apps)
# # Strip out any cachebusting MD% fingerprints
# fingerprinted = _FINGERPRINT.match(environ['PATH_INFO'])
# if fingerprinted:
# environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
# return app(environ, start_response)
class HomeAssistantWSGI(object):
"""WSGI server for Home Assistant."""
# pylint: disable=too-many-instance-attributes, too-many-locals
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class,
hass, api_password, development, ssl_certificate, ssl_key,
cors_origins):
"""Initialize the server."""
super().__init__(server_address, request_handler_class)
self.server_address = server_address def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port):
"""Initilalize the WSGI Home Assistant server."""
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import BaseRequest, AcceptMixin
from werkzeug.contrib.wrappers import JSONRequestMixin
from werkzeug.routing import Map
from werkzeug.utils import cached_property
from werkzeug.wrappers import Response
class Request(BaseRequest, AcceptMixin, JSONRequestMixin):
"""Base class for incoming requests."""
@cached_property
def json(self):
"""Get the result of json.loads if possible."""
if not self.data:
return None
elif 'json' not in self.environ.get('CONTENT_TYPE', ''):
raise BadRequest('Not a JSON request')
try:
return json.loads(self.data.decode(
self.charset, self.encoding_errors))
except (TypeError, ValueError):
raise BadRequest('Unable to read JSON request')
Response.mimetype = 'text/html'
# pylint: disable=invalid-name
self.Request = Request
self.url_map = Map()
self.views = {}
self.hass = hass self.hass = hass
self.api_password = api_password self.extra_apps = {}
self.development = development self.development = development
self.paths = [] self.api_password = api_password
self.sessions = SessionStore() self.ssl_certificate = ssl_certificate
self.use_ssl = ssl_certificate is not None self.ssl_key = ssl_key
self.cors_origins = cors_origins self.server_host = server_host
self.server_port = server_port
# We will lazy init this one if needed
self.event_forwarder = None self.event_forwarder = None
if development: def register_view(self, view):
_LOGGER.info("running http in development mode") """Register a view with the WSGI server.
if ssl_certificate is not None: The view argument must inherit from the HomeAssistantView class, and
context = ssl.create_default_context( it must have (globally unique) 'url' and 'name' attributes.
purpose=ssl.Purpose.CLIENT_AUTH) """
context.load_cert_chain(ssl_certificate, keyfile=ssl_key) from werkzeug.routing import Rule
self.socket = context.wrap_socket(self.socket, server_side=True)
if view.name in self.views:
_LOGGER.warning("View '%s' is being overwritten", view.name)
if isinstance(view, type):
view = view(self.hass)
self.views[view.name] = view
rule = Rule(view.url, endpoint=view.name)
self.url_map.add(rule)
for url in view.extra_urls:
rule = Rule(url, endpoint=view.name)
self.url_map.add(rule)
def register_redirect(self, url, redirect_to):
"""Register a redirect with the server.
If given this must be either a string or callable. In case of a
callable its called with the url adapter that triggered the match and
the values of the URL as keyword arguments and has to return the target
for the redirect, otherwise it has to be a string with placeholders in
rule syntax.
"""
from werkzeug.routing import Rule
self.url_map.add(Rule(url, redirect_to=redirect_to))
def register_static_path(self, url_root, path):
"""Register a folder to serve as a static path."""
from static import Cling
if url_root in self.extra_apps:
_LOGGER.warning("Static path '%s' is being overwritten", path)
self.extra_apps[url_root] = Cling(path)
def start(self): def start(self):
"""Start the HTTP server.""" """Start the wsgi server."""
def stop_http(event): from eventlet import wsgi
"""Stop the HTTP server.""" import eventlet
self.shutdown()
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) sock = eventlet.listen((self.server_host, self.server_port))
if self.ssl_certificate:
eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
keyfile=self.ssl_key, server_side=True)
wsgi.server(sock, self)
protocol = 'https' if self.use_ssl else 'http' def dispatch_request(self, request):
"""Handle incoming request."""
from werkzeug.exceptions import (
MethodNotAllowed, NotFound, BadRequest, Unauthorized,
)
from werkzeug.routing import RequestRedirect
_LOGGER.info( adapter = self.url_map.bind_to_environ(request.environ)
"Starting web interface at %s://%s:%d",
protocol, self.server_address[0], self.server_address[1])
# 31-1-2015: Refactored frontend/api components out of this component
# To prevent stuff from breaking, load the two extracted components
bootstrap.setup_component(self.hass, 'api')
bootstrap.setup_component(self.hass, 'frontend')
self.serve_forever()
def register_path(self, method, url, callback, require_auth=True):
"""Register a path with the server."""
self.paths.append((method, url, callback, require_auth))
def log_message(self, fmt, *args):
"""Redirect built-in log to HA logging."""
# pylint: disable=no-self-use
_LOGGER.info(fmt, *args)
# pylint: disable=too-many-public-methods,too-many-locals
class RequestHandler(SimpleHTTPRequestHandler):
"""Handle incoming HTTP requests.
We extend from SimpleHTTPRequestHandler instead of Base so we
can use the guess content type methods.
"""
server_version = "HomeAssistant/1.0"
def __init__(self, req, client_addr, server):
"""Constructor, call the base constructor and set up session."""
# Track if this was an authenticated request
self.authenticated = False
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
self.protocol_version = 'HTTP/1.1'
def log_message(self, fmt, *arguments):
"""Redirect built-in log to HA logging."""
if self.server.api_password is None:
_LOGGER.info(fmt, *arguments)
else:
_LOGGER.info(
fmt, *(arg.replace(self.server.api_password, '*******')
if isinstance(arg, str) else arg for arg in arguments))
def _handle_request(self, method): # pylint: disable=too-many-branches
"""Perform some common checks and call appropriate method."""
url = urlparse(self.path)
# Read query input. parse_qs gives a list for each value, we want last
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
# Did we get post input ?
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
if content_length:
body_content = self.rfile.read(content_length).decode("UTF-8")
try:
data.update(json.loads(body_content))
except (TypeError, ValueError):
# TypeError if JSON object is not a dict
# ValueError if we could not parse JSON
_LOGGER.exception(
"Exception parsing JSON: %s", body_content)
self.write_json_message(
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return
if self.verify_session():
# The user has a valid session already
self.authenticated = True
elif self.server.api_password is None:
# No password is set, so everyone is authenticated
self.authenticated = True
elif hmac.compare_digest(self.headers.get(HTTP_HEADER_HA_AUTH, ''),
self.server.api_password):
# A valid auth header has been set
self.authenticated = True
elif hmac.compare_digest(data.get(DATA_API_PASSWORD, ''),
self.server.api_password):
# A valid password has been specified
self.authenticated = True
else:
self.authenticated = False
# we really shouldn't need to forward the password from here
if url.path not in [URL_ROOT, URL_API_EVENT_FORWARD]:
data.pop(DATA_API_PASSWORD, None)
if '_METHOD' in data:
method = data.pop('_METHOD')
# Var to keep track if we found a path that matched a handler but
# the method was different
path_matched_but_not_method = False
# Var to hold the handler for this path and method if found
handle_request_method = False
require_auth = True
# Check every handler to find matching result
for t_method, t_path, t_handler, t_auth in self.server.paths:
# we either do string-comparison or regular expression matching
# pylint: disable=maybe-no-member
if isinstance(t_path, str):
path_match = url.path == t_path
else:
path_match = t_path.match(url.path)
if path_match and method == t_method:
# Call the method
handle_request_method = t_handler
require_auth = t_auth
break
elif path_match:
path_matched_but_not_method = True
# Did we find a handler for the incoming request?
if handle_request_method:
# For some calls we need a valid password
msg = "API password missing or incorrect."
if require_auth and not self.authenticated:
self.write_json_message(msg, HTTP_UNAUTHORIZED)
_LOGGER.warning('%s Source IP: %s',
msg,
self.client_address[0])
return
handle_request_method(self, path_match, data)
elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED)
self.end_headers()
else:
self.send_response(HTTP_NOT_FOUND)
self.end_headers()
def do_HEAD(self): # pylint: disable=invalid-name
"""HEAD request handler."""
self._handle_request('HEAD')
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 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 write_json_message(self, message, status_code=HTTP_OK):
"""Helper method to return a message to the caller."""
self.write_json({'message': message}, status_code=status_code)
def write_json(self, data=None, status_code=HTTP_OK, location=None):
"""Helper method to return JSON to the caller."""
json_data = json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode('UTF-8')
self.send_response(status_code)
if location:
self.send_header('Location', location)
self.set_session_cookie_header()
self.write_content(json_data, CONTENT_TYPE_JSON)
def write_text(self, message, status_code=HTTP_OK):
"""Helper method to return a text message to the caller."""
msg_data = message.encode('UTF-8')
self.send_response(status_code)
self.set_session_cookie_header()
self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
def write_file(self, path, cache_headers=True):
"""Return a file to the user."""
try: try:
with open(path, 'rb') as inp: endpoint, values = adapter.match()
self.write_file_pointer(self.guess_type(path), inp, return self.views[endpoint].handle_request(request, **values)
cache_headers) except RequestRedirect as ex:
return ex
except BadRequest as ex:
return self._handle_error(request, str(ex), 400)
except NotFound as ex:
return self._handle_error(request, str(ex), 404)
except MethodNotAllowed as ex:
return self._handle_error(request, str(ex), 405)
except Unauthorized as ex:
return self._handle_error(request, str(ex), 401)
# TODO This long chain of except blocks is silly. _handle_error should
# just take the exception as an argument and parse the status code
# itself
except IOError: def base_app(self, environ, start_response):
self.send_response(HTTP_NOT_FOUND) """WSGI Handler of requests to base app."""
self.end_headers() request = self.Request(environ)
_LOGGER.exception("Unable to serve %s", path) response = self.dispatch_request(request)
return response(environ, start_response)
def write_file_pointer(self, content_type, inp, cache_headers=True): def __call__(self, environ, start_response):
"""Helper function to write a file pointer to the user.""" """Handle a request for base app + extra apps."""
self.send_response(HTTP_OK) from werkzeug.wsgi import DispatcherMiddleware
if cache_headers: app = DispatcherMiddleware(self.base_app, self.extra_apps)
self.set_cache_header() # Strip out any cachebusting MD5 fingerprints
self.set_session_cookie_header() fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
if fingerprinted:
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
return app(environ, start_response)
self.write_content(inp.read(), content_type) def _handle_error(self, request, message, status):
"""Handle a WSGI request error."""
from werkzeug.wrappers import Response
if request.accept_mimetypes.accept_json:
message = json.dumps({
"result": "error",
"message": message,
})
mimetype = "application/json"
else:
mimetype = "text/plain"
return Response(message, status=status, mimetype=mimetype)
def write_content(self, content, content_type=None):
"""Helper method to write content bytes to output stream."""
if content_type is not None:
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''): class HomeAssistantView(object):
content = gzip.compress(content) """Base view for all views."""
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip") extra_urls = []
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING) requires_auth = True # Views inheriting from this class can override this
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content))) def __init__(self, hass):
"""Initilalize the base view."""
from werkzeug.wrappers import Response
cors_check = (self.headers.get("Origin") in self.server.cors_origins) self.hass = hass
# pylint: disable=invalid-name
self.Response = Response
cors_headers = ", ".join(ALLOWED_CORS_HEADERS) def handle_request(self, request, **values):
"""Handle request to url."""
if self.server.cors_origins and cors_check: from werkzeug.exceptions import (
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, MethodNotAllowed, Unauthorized, BadRequest,
self.headers.get("Origin"))
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
cors_headers)
self.end_headers()
if self.command == 'HEAD':
return
self.wfile.write(content)
def set_cache_header(self):
"""Add cache headers if not in development."""
if self.server.development:
return
# 1 year in seconds
cache_time = 365 * 86400
self.send_header(
HTTP_HEADER_CACHE_CONTROL,
"public, max-age={}".format(cache_time))
self.send_header(
HTTP_HEADER_EXPIRES,
self.date_time_string(time.time()+cache_time))
def set_session_cookie_header(self):
"""Add the header for the session cookie and return session ID."""
if not self.authenticated:
return None
session_id = self.get_cookie_session_id()
if session_id is not None:
self.server.sessions.extend_validation(session_id)
return session_id
self.send_header(
'Set-Cookie',
'{}={}'.format(SESSION_KEY, self.server.sessions.create())
) )
return session_id
def verify_session(self):
"""Verify that we are in a valid session."""
return self.get_cookie_session_id() is not None
def get_cookie_session_id(self):
"""Extract the current session ID from the cookie.
Return None if not set or invalid.
"""
if 'Cookie' not in self.headers:
return None
cookie = cookies.SimpleCookie()
try: try:
cookie.load(self.headers["Cookie"]) handler = getattr(self, request.method.lower())
except cookies.CookieError: except AttributeError:
return None raise MethodNotAllowed
morsel = cookie.get(SESSION_KEY) # TODO: session support + uncomment session test
if morsel is None: # Auth code verbose on purpose
return None authenticated = False
session_id = cookie[SESSION_KEY].value if not self.requires_auth:
authenticated = True
if self.server.sessions.is_valid(session_id): elif self.hass.wsgi.api_password is None:
return session_id authenticated = True
return None elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
self.hass.wsgi.api_password):
# A valid auth header has been set
authenticated = True
def destroy_session(self): elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''),
"""Destroy the session.""" self.hass.wsgi.api_password):
session_id = self.get_cookie_session_id() authenticated = True
if session_id is None: else:
return # Do we still want to support passing it in as post data?
try:
json_data = request.json
if (json_data is not None and
hmac.compare_digest(
json_data.get(DATA_API_PASSWORD, ''),
self.hass.wsgi.api_password)):
authenticated = True
except BadRequest:
pass
self.send_header('Set-Cookie', '') if not authenticated:
self.server.sessions.destroy(session_id) raise Unauthorized()
result = handler(request, **values)
def session_valid_time(): if isinstance(result, self.Response):
"""Time till when a session will be valid.""" # The method handler returned a ready-made Response, how nice of it
return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS) return result
status_code = 200
class SessionStore(object): if isinstance(result, tuple):
"""Responsible for storing and retrieving HTTP sessions.""" result, status_code = result
def __init__(self): return self.Response(result, status=status_code)
"""Setup the session store."""
self._sessions = {}
self._lock = threading.RLock()
@util.Throttle(SESSION_CLEAR_INTERVAL) def json(self, result, status_code=200):
def _remove_expired(self): """Return a JSON response."""
"""Remove any expired sessions.""" msg = json.dumps(
now = date_util.utcnow() result,
for key in [key for key, valid_time in self._sessions.items() sort_keys=True,
if valid_time < now]: cls=rem.JSONEncoder
self._sessions.pop(key) ).encode('UTF-8')
return self.Response(msg, mimetype="application/json",
status=status_code)
def is_valid(self, key): def json_message(self, error, status_code=200):
"""Return True if a valid session is given.""" """Return a JSON message response."""
with self._lock: return self.json({'message': error}, status_code)
self._remove_expired()
return (key in self._sessions and def file(self, request, fil, content_type=None):
self._sessions[key] > date_util.utcnow()) """Return a file."""
from werkzeug.wsgi import wrap_file
from werkzeug.exceptions import NotFound
def extend_validation(self, key): if isinstance(fil, str):
"""Extend a session validation time.""" try:
with self._lock: fil = open(fil)
if key not in self._sessions: except IOError:
return raise NotFound()
self._sessions[key] = session_valid_time()
def destroy(self, key): # TODO mimetypes, etc
"""Destroy a session by key."""
with self._lock:
self._sessions.pop(key, None)
def create(self): resp = self.Response(wrap_file(request.environ, fil))
"""Create a new session.""" if content_type is not None:
with self._lock: resp.mimetype = content_type
session_id = util.get_random_string(20) return resp
while session_id in self._sessions:
session_id = util.get_random_string(20)
self._sessions[session_id] = session_valid_time()
return session_id

View File

@ -21,6 +21,7 @@ from homeassistant.core import State
from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
@ -76,34 +77,40 @@ def setup(hass, config):
message = template.render(hass, message) message = template.render(hass, message)
log_entry(hass, name, message, domain, entity_id) log_entry(hass, name, message, domain, entity_id)
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook) hass.wsgi.register_view(LogbookView)
hass.services.register(DOMAIN, 'log', log_message, hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA) schema=LOG_MESSAGE_SCHEMA)
return True return True
def _handle_get_logbook(handler, path_match, data): class LogbookView(HomeAssistantView):
"""Return logbook entries.""" """Handle logbook view requests."""
date_str = path_match.group('date')
if date_str: url = '/api/logbook'
start_date = dt_util.parse_date(date_str) name = 'api:logbook'
extra_urls = ['/api/logbook/<date>']
if start_date is None: def get(self, request, date=None):
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST) """Retrieve logbook entries."""
return if date:
start_date = dt_util.parse_date(date)
start_day = dt_util.start_of_local_day(start_date) if start_date is None:
else: return self.json_message('Error parsing JSON',
start_day = dt_util.start_of_local_day() HTTP_BAD_REQUEST)
end_day = start_day + timedelta(days=1) start_day = dt_util.start_of_local_day(start_date)
else:
start_day = dt_util.start_of_local_day()
events = recorder.query_events( end_day = start_day + timedelta(days=1)
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events)) events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
return self.json(humanify(events))
class Entry(object): class Entry(object):

View File

@ -14,6 +14,7 @@ from homeassistant.const import HTTP_OK, TEMP_CELSIUS
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["fitbit==0.2.2"] REQUIREMENTS = ["fitbit==0.2.2"]
@ -248,70 +249,83 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
redirect_uri = "{}{}".format(hass.config.api.base_url, redirect_uri = "{}{}".format(hass.config.api.base_url,
FITBIT_AUTH_CALLBACK_PATH) FITBIT_AUTH_CALLBACK_PATH)
def _start_fitbit_auth(handler, path_match, data): fitbit_auth_start_url, _ = oauth.authorize_token_url(
"""Start Fitbit OAuth2 flow.""" redirect_uri=redirect_uri,
url, _ = oauth.authorize_token_url(redirect_uri=redirect_uri, scope=["activity", "heartrate", "nutrition", "profile",
scope=["activity", "heartrate", "settings", "sleep", "weight"])
"nutrition", "profile",
"settings", "sleep",
"weight"])
handler.send_response(301)
handler.send_header("Location", url)
handler.end_headers()
def _finish_fitbit_auth(handler, path_match, data): hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
"""Finish Fitbit OAuth2 flow.""" hass.wsgi.register_view(FitbitAuthCallbackView(hass, config,
response_message = """Fitbit has been successfully authorized! add_devices, oauth))
You can close this window now!"""
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
if data.get("code") is not None:
try:
oauth.fetch_access_token(data.get("code"), redirect_uri)
except MissingTokenError as error:
_LOGGER.error("Missing token: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
except MismatchingStateError as error:
_LOGGER.error("Mismatched state, CSRF error: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
else:
_LOGGER.error("Unknown error when authing")
response_message = """Something went wrong when
attempting authenticating with Fitbit.
An unknown error occurred. Please try again!
"""
html_response = """<html><head><title>Fitbit Auth</title></head>
<body><h1>{}</h1></body></html>""".format(response_message)
html_response = html_response.encode("utf-8")
handler.send_response(HTTP_OK)
handler.write_content(html_response, content_type="text/html")
config_contents = {
"access_token": oauth.token["access_token"],
"refresh_token": oauth.token["refresh_token"],
"client_id": oauth.client_id,
"client_secret": oauth.client_secret
}
if not config_from_file(config_path, config_contents):
_LOGGER.error("failed to save config file")
setup_platform(hass, config, add_devices, discovery_info=None)
hass.http.register_path("GET", FITBIT_AUTH_START, _start_fitbit_auth,
require_auth=False)
hass.http.register_path("GET", FITBIT_AUTH_CALLBACK_PATH,
_finish_fitbit_auth, require_auth=False)
request_oauth_completion(hass) request_oauth_completion(hass)
class FitbitAuthCallbackView(HomeAssistantView):
"""Handle OAuth finish callback requests."""
requires_auth = False
url = '/auth/fitbit/callback'
name = 'auth:fitbit:callback'
def __init__(self, hass, config, add_devices, oauth):
"""Initialize the OAuth callback view."""
super().__init__(hass)
self.config = config
self.add_devices = add_devices
self.oauth = oauth
def get(self, request):
"""Finish OAuth callback request."""
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
data = request.args
response_message = """Fitbit has been successfully authorized!
You can close this window now!"""
if data.get("code") is not None:
redirect_uri = "{}{}".format(self.hass.config.api.base_url,
FITBIT_AUTH_CALLBACK_PATH)
try:
self.oauth.fetch_access_token(data.get("code"), redirect_uri)
except MissingTokenError as error:
_LOGGER.error("Missing token: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
except MismatchingStateError as error:
_LOGGER.error("Mismatched state, CSRF error: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
else:
_LOGGER.error("Unknown error when authing")
response_message = """Something went wrong when
attempting authenticating with Fitbit.
An unknown error occurred. Please try again!
"""
html_response = """<html><head><title>Fitbit Auth</title></head>
<body><h1>{}</h1></body></html>""".format(response_message)
config_contents = {
"access_token": self.oauth.token["access_token"],
"refresh_token": self.oauth.token["refresh_token"],
"client_id": self.oauth.client_id,
"client_secret": self.oauth.client_secret
}
if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE),
config_contents):
_LOGGER.error("failed to save config file")
setup_platform(self.hass, self.config, self.add_devices)
return html_response
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class FitbitSensor(Entity): class FitbitSensor(Entity):
"""Implementation of a Fitbit sensor.""" """Implementation of a Fitbit sensor."""

View File

@ -7,8 +7,8 @@ https://home-assistant.io/components/sensor.torque/
import re import re
from homeassistant.const import HTTP_OK
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'torque' DOMAIN = 'torque'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -43,12 +43,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
email = config.get('email', None) email = config.get('email', None)
sensors = {} sensors = {}
def _receive_data(handler, path_match, data): hass.wsgi.register_view(TorqueReceiveDataView(hass, email, vehicle,
"""Received data from Torque.""" sensors, add_devices))
handler.send_response(HTTP_OK) return True
handler.end_headers()
if email is not None and email != data[SENSOR_EMAIL_FIELD]:
class TorqueReceiveDataView(HomeAssistantView):
"""Handle data from Torque requests."""
url = API_PATH
name = 'api:torque'
# pylint: disable=too-many-arguments
def __init__(self, hass, email, vehicle, sensors, add_devices):
"""Initialize a Torque view."""
super().__init__(hass)
self.email = email
self.vehicle = vehicle
self.sensors = sensors
self.add_devices = add_devices
def get(self, request):
"""Handle Torque data request."""
data = request.args
if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]:
return return
names = {} names = {}
@ -66,18 +85,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
units[pid] = decode(data[key]) units[pid] = decode(data[key])
elif is_value: elif is_value:
pid = convert_pid(is_value.group(1)) pid = convert_pid(is_value.group(1))
if pid in sensors: if pid in self.sensors:
sensors[pid].on_update(data[key]) self.sensors[pid].on_update(data[key])
for pid in names: for pid in names:
if pid not in sensors: if pid not in self.sensors:
sensors[pid] = TorqueSensor( self.sensors[pid] = TorqueSensor(
ENTITY_NAME_FORMAT.format(vehicle, names[pid]), ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]),
units.get(pid, None)) units.get(pid, None))
add_devices([sensors[pid]]) self.add_devices([self.sensors[pid]])
hass.http.register_path('GET', API_PATH, _receive_data) return None
return True
class TorqueSensor(Entity): class TorqueSensor(Entity):

View File

@ -1,218 +0,0 @@
"""
This module provides WSGI application to serve the Home Assistant API.
"""
import json
import logging
import threading
import re
import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_OK, HTTP_NOT_FOUND, HTTP_BAD_REQUEST
)
DOMAIN = "wsgi"
REQUIREMENTS = ("eventlet==0.18.4", "static3==0.6.1", "Werkzeug==0.11.5",)
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development"
CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
DATA_API_PASSWORD = 'api_password'
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Set up the HTTP API and debug interface."""
conf = config.get(DOMAIN, {})
server = HomeAssistantWSGI(
hass,
development=str(conf.get(CONF_DEVELOPMENT, "")) == "1",
server_host=conf.get(CONF_SERVER_HOST, '0.0.0.0'),
server_port=conf.get(CONF_SERVER_PORT, SERVER_PORT),
api_password=util.convert(conf.get(CONF_API_PASSWORD), str),
ssl_certificate=conf.get(CONF_SSL_CERTIFICATE),
ssl_key=conf.get(CONF_SSL_KEY),
)
hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True,
name='WSGI-server').start())
hass.wsgi = server
return True
class StaticFileServer(object):
def __call__(self, environ, start_response):
from werkzeug.wsgi import DispatcherMiddleware
app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD% fingerprints
fingerprinted = _FINGERPRINT.match(environ['PATH_INFO'])
if fingerprinted:
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
return app(environ, start_response)
class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port):
from werkzeug.wrappers import BaseRequest, AcceptMixin
from werkzeug.routing import Map
class Request(BaseRequest, AcceptMixin):
pass
self.Request = Request
self.url_map = Map()
self.views = {}
self.hass = hass
self.extra_apps = {}
self.development = development
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_key = ssl_key
def register_view(self, view):
""" Register a view with the WSGI server.
The view argument must inherit from the HomeAssistantView class, and
it must have (globally unique) 'url' and 'name' attributes.
"""
from werkzeug.routing import Rule
if view.name in self.views:
_LOGGER.warning("View '{}' is being overwritten".format(view.name))
self.views[view.name] = view(self.hass)
# TODO Warn if we're overriding an existing view
rule = Rule(view.url, endpoint=view.name)
self.url_map.add(rule)
for url in view.extra_urls:
rule = Rule(url, endpoint=view.name)
self.url_map.add(rule)
def register_static_path(self, url_root, path):
"""Register a folder to serve as a static path."""
from static import Cling
# TODO Warn if we're overwriting an existing path
self.extra_apps[url_root] = Cling(path)
def start(self):
"""Start the wsgi server."""
from eventlet import wsgi
import eventlet
sock = eventlet.listen(('', 8090))
if self.ssl_certificate:
eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
keyfile=self.ssl_key, server_side=True)
wsgi.server(sock, self)
def dispatch_request(self, request):
"""Handle incoming request."""
from werkzeug.exceptions import (
MethodNotAllowed, NotFound, BadRequest, Unauthorized
)
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return self.views[endpoint].handle_request(request, **values)
except BadRequest as e:
return self.handle_error(request, str(e), HTTP_BAD_REQUEST)
except NotFound as e:
return self.handle_error(request, str(e), HTTP_NOT_FOUND)
except MethodNotAllowed as e:
return self.handle_error(request, str(e), 405)
except Unauthorized as e:
return self.handle_error(request, str(e), 401)
# TODO This long chain of except blocks is silly. _handle_error should
# just take the exception as an argument and parse the status code
# itself
def base_app(self, environ, start_response):
request = self.Request(environ)
request.api_password = self.api_password
request.development = self.development
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
from werkzeug.wsgi import DispatcherMiddleware
app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
if fingerprinted:
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
return app(environ, start_response)
def _handle_error(self, request, message, status):
from werkzeug.wrappers import Response
if request.accept_mimetypes.accept_json:
message = json.dumps({
"result": "error",
"message": message,
})
mimetype = "application/json"
else:
mimetype = "text/plain"
return Response(message, status=status, mimetype=mimetype)
class HomeAssistantView(object):
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
def __init__(self, hass):
from werkzeug.wrappers import Response
from werkzeug.exceptions import NotFound, BadRequest
self.hass = hass
self.Response = Response
self.NotFound = NotFound
self.BadRequest = BadRequest
def handle_request(self, request, **values):
"""Handle request to url."""
from werkzeug.exceptions import MethodNotAllowed
try:
handler = getattr(self, request.method.lower())
except AttributeError:
raise MethodNotAllowed
# TODO This would be a good place to check the auth if
# self.requires_auth is true, and raise Unauthorized on a failure
result = handler(request, **values)
if isinstance(result, self.Response):
# The method handler returned a ready-made Response, how nice of it
return result
elif (isinstance(result, dict) or
isinstance(result, list) or
isinstance(result, ha.State)):
# There are a few result types we know we always want to jsonify
if isinstance(result, dict) and 'status_code' in result:
status_code = result['status_code']
del result['status_code']
else:
status_code = HTTP_OK
msg = json.dumps(
result,
sort_keys=True,
cls=rem.JSONEncoder
).encode('UTF-8')
return self.Response(msg, mimetype="application/json",
status_code=status_code)

View File

@ -186,8 +186,8 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
def track_time_change(hass, action, year=None, month=None, day=None, def track_time_change(hass, action, year=None, month=None, day=None,
hour=None, minute=None, second=None): hour=None, minute=None, second=None):
"""Add a listener that will fire if UTC time matches a pattern.""" """Add a listener that will fire if UTC time matches a pattern."""
track_utc_time_change(hass, action, year, month, day, hour, minute, second, return track_utc_time_change(hass, action, year, month, day, hour, minute,
local=True) second, local=True)
def _process_match_param(parameter): def _process_match_param(parameter):

View File

@ -21,7 +21,8 @@ import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API, URL_API_EVENT_FORWARD, HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API, URL_API_EVENT_FORWARD,
URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES,
URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY) URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY,
HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
METHOD_GET = "get" METHOD_GET = "get"
@ -59,7 +60,9 @@ class API(object):
else: else:
self.base_url = "http://{}:{}".format(host, self.port) self.base_url = "http://{}:{}".format(host, self.port)
self.status = None self.status = None
self._headers = {} self._headers = {
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
}
if api_password is not None: if api_password is not None:
self._headers[HTTP_HEADER_HA_AUTH] = api_password self._headers[HTTP_HEADER_HA_AUTH] = api_password
@ -126,7 +129,7 @@ class HomeAssistant(ha.HomeAssistant):
def start(self): def start(self):
"""Start the instance.""" """Start the instance."""
# Ensure a local API exists to connect with remote # Ensure a local API exists to connect with remote
if self.config.api is None: if 'api' not in self.config.components:
if not bootstrap.setup_component(self, 'api'): if not bootstrap.setup_component(self, 'api'):
raise HomeAssistantError( raise HomeAssistantError(
'Unable to setup local API to receive events') 'Unable to setup local API to receive events')
@ -136,6 +139,10 @@ class HomeAssistant(ha.HomeAssistant):
self.bus.fire(ha.EVENT_HOMEASSISTANT_START, self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
origin=ha.EventOrigin.remote) origin=ha.EventOrigin.remote)
# Give eventlet time to startup
import eventlet
eventlet.sleep(0.1)
# Setup that events from remote_api get forwarded to local_api # Setup that events from remote_api get forwarded to local_api
# Do this after we fire START, otherwise HTTP is not started # Do this after we fire START, otherwise HTTP is not started
if not connect_remote_events(self.remote_api, self.config.api): if not connect_remote_events(self.remote_api, self.config.api):
@ -383,7 +390,7 @@ def fire_event(api, event_type, data=None):
req = api(METHOD_POST, URL_API_EVENTS_EVENT.format(event_type), data) req = api(METHOD_POST, URL_API_EVENTS_EVENT.format(event_type), data)
if req.status_code != 200: if req.status_code != 200:
_LOGGER.error("Error firing event: %d - %d", _LOGGER.error("Error firing event: %d - %s",
req.status_code, req.text) req.status_code, req.text)
except HomeAssistantError: except HomeAssistantError:

View File

@ -23,7 +23,7 @@ SoCo==0.11.1
# homeassistant.components.notify.twitter # homeassistant.components.notify.twitter
TwitterAPI==2.4.1 TwitterAPI==2.4.1
# homeassistant.components.wsgi # homeassistant.components.http
Werkzeug==0.11.5 Werkzeug==0.11.5
# homeassistant.components.apcupsd # homeassistant.components.apcupsd
@ -56,7 +56,7 @@ dweepy==0.2.0
# homeassistant.components.sensor.eliqonline # homeassistant.components.sensor.eliqonline
eliqonline==1.0.12 eliqonline==1.0.12
# homeassistant.components.wsgi # homeassistant.components.http
eventlet==0.18.4 eventlet==0.18.4
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
@ -337,7 +337,7 @@ somecomfort==0.2.1
# homeassistant.components.sensor.speedtest # homeassistant.components.sensor.speedtest
speedtest-cli==0.3.4 speedtest-cli==0.3.4
# homeassistant.components.wsgi # homeassistant.components.http
static3==0.6.1 static3==0.6.1
# homeassistant.components.sensor.steam_online # homeassistant.components.sensor.steam_online

View File

@ -120,7 +120,7 @@ def mock_state_change_event(hass, new_state, old_state=None):
def mock_http_component(hass): def mock_http_component(hass):
"""Mock the HTTP component.""" """Mock the HTTP component."""
hass.http = MockHTTP() hass.wsgi = mock.MagicMock()
hass.config.components.append('http') hass.config.components.append('http')
@ -135,14 +135,6 @@ def mock_mqtt_component(hass, mock_mqtt):
return mock_mqtt return mock_mqtt
class MockHTTP(object):
"""Mock the HTTP module."""
def register_path(self, method, url, callback, require_auth=True):
"""Register a path."""
pass
class MockModule(object): class MockModule(object):
"""Representation of a fake module.""" """Representation of a fake module."""

View File

@ -2,6 +2,7 @@
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -45,6 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name
}) })
hass.start() hass.start()
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name

View File

@ -3,6 +3,7 @@
import unittest import unittest
import json import json
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -13,7 +14,10 @@ from tests.common import get_test_instance_port, get_test_home_assistant
API_PASSWORD = "test1234" API_PASSWORD = "test1234"
SERVER_PORT = get_test_instance_port() SERVER_PORT = get_test_instance_port()
API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT) API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT)
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
SESSION_ID = 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000' SESSION_ID = 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000'
APPLICATION_ID = 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' APPLICATION_ID = 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
@ -83,6 +87,8 @@ def setUpModule(): # pylint: disable=invalid-name
hass.start() hass.start()
eventlet.sleep(0.1)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
"""Stop the Home Assistant server.""" """Stop the Home Assistant server."""

View File

@ -1,11 +1,12 @@
"""The tests for the Home Assistant HTTP component.""" """The tests for the Home Assistant HTTP component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
from contextlib import closing # from contextlib import closing
import json import json
import tempfile import tempfile
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -17,7 +18,10 @@ from tests.common import get_test_instance_port, get_test_home_assistant
API_PASSWORD = "test1234" API_PASSWORD = "test1234"
SERVER_PORT = get_test_instance_port() SERVER_PORT = get_test_instance_port()
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
hass = None hass = None
@ -45,6 +49,10 @@ def setUpModule(): # pylint: disable=invalid-name
hass.start() hass.start()
# To start HTTP
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
"""Stop the Home Assistant server.""" """Stop the Home Assistant server."""
@ -80,14 +88,14 @@ class TestAPI(unittest.TestCase):
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)
def test_access_via_session(self): # def test_access_via_session(self):
"""Test access wia session.""" # """Test access wia session."""
session = requests.Session() # session = requests.Session()
req = session.get(_url(const.URL_API), headers=HA_HEADERS) # req = session.get(_url(const.URL_API), headers=HA_HEADERS)
self.assertEqual(200, req.status_code) # self.assertEqual(200, req.status_code)
req = session.get(_url(const.URL_API)) # req = session.get(_url(const.URL_API))
self.assertEqual(200, req.status_code) # self.assertEqual(200, req.status_code)
def test_api_list_state_entities(self): def test_api_list_state_entities(self):
"""Test if the debug interface allows us to list state entities.""" """Test if the debug interface allows us to list state entities."""
@ -220,7 +228,7 @@ class TestAPI(unittest.TestCase):
hass.pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(422, req.status_code) self.assertEqual(400, req.status_code)
self.assertEqual(0, len(test_value)) self.assertEqual(0, len(test_value))
# Try now with valid but unusable JSON # Try now with valid but unusable JSON
@ -231,7 +239,7 @@ class TestAPI(unittest.TestCase):
hass.pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(422, req.status_code) self.assertEqual(400, req.status_code)
self.assertEqual(0, len(test_value)) self.assertEqual(0, len(test_value))
def test_api_get_config(self): def test_api_get_config(self):
@ -333,8 +341,7 @@ class TestAPI(unittest.TestCase):
req = requests.post( req = requests.post(
_url(const.URL_API_TEMPLATE), _url(const.URL_API_TEMPLATE),
data=json.dumps({"template": json={"template": '{{ states.sensor.temperature.state }}'},
'{{ states.sensor.temperature.state }}'}),
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual('10', req.text) self.assertEqual('10', req.text)
@ -349,7 +356,7 @@ class TestAPI(unittest.TestCase):
'{{ states.sensor.temperature.state'}), '{{ states.sensor.temperature.state'}),
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(422, req.status_code) self.assertEqual(400, req.status_code)
def test_api_event_forward(self): def test_api_event_forward(self):
"""Test setting up event forwarding.""" """Test setting up event forwarding."""
@ -390,23 +397,25 @@ class TestAPI(unittest.TestCase):
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(422, req.status_code) self.assertEqual(422, req.status_code)
# Setup a real one # TODO disabled because eventlet cannot validate
req = requests.post( # a connection to itself, need a second instance
_url(const.URL_API_EVENT_FORWARD), # # Setup a real one
data=json.dumps({ # req = requests.post(
'api_password': API_PASSWORD, # _url(const.URL_API_EVENT_FORWARD),
'host': '127.0.0.1', # data=json.dumps({
'port': SERVER_PORT # 'api_password': API_PASSWORD,
}), # 'host': '127.0.0.1',
headers=HA_HEADERS) # 'port': SERVER_PORT
self.assertEqual(200, req.status_code) # }),
# headers=HA_HEADERS)
# self.assertEqual(200, req.status_code)
# Delete it again.. # # Delete it again..
req = requests.delete( # req = requests.delete(
_url(const.URL_API_EVENT_FORWARD), # _url(const.URL_API_EVENT_FORWARD),
data=json.dumps({}), # data=json.dumps({}),
headers=HA_HEADERS) # headers=HA_HEADERS)
self.assertEqual(400, req.status_code) # self.assertEqual(400, req.status_code)
req = requests.delete( req = requests.delete(
_url(const.URL_API_EVENT_FORWARD), _url(const.URL_API_EVENT_FORWARD),
@ -426,63 +435,61 @@ class TestAPI(unittest.TestCase):
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)
def test_stream(self): # def test_stream(self):
"""Test the stream.""" # """Test the stream."""
listen_count = self._listen_count() # listen_count = self._listen_count()
with closing(requests.get(_url(const.URL_API_STREAM), # with closing(requests.get(_url(const.URL_API_STREAM),
stream=True, headers=HA_HEADERS)) as req: # stream=True, headers=HA_HEADERS)) as req:
data = self._stream_next_event(req) # self.assertEqual(listen_count + 1, self._listen_count())
self.assertEqual('ping', data)
self.assertEqual(listen_count + 1, self._listen_count()) # hass.bus.fire('test_event')
# hass.pool.block_till_done()
hass.bus.fire('test_event') # data = self._stream_next_event(req)
hass.pool.block_till_done()
data = self._stream_next_event(req) # self.assertEqual('test_event', data['event_type'])
self.assertEqual('test_event', data['event_type']) # def test_stream_with_restricted(self):
# """Test the stream with restrictions."""
# listen_count = self._listen_count()
# with closing(requests.get(_url(const.URL_API_STREAM),
# data=json.dumps({
# 'restrict':
# 'test_event1,test_event3'}),
# stream=True, headers=HA_HEADERS)) as req:
def test_stream_with_restricted(self): # data = self._stream_next_event(req)
"""Test the stream with restrictions.""" # self.assertEqual('ping', data)
listen_count = self._listen_count()
with closing(requests.get(_url(const.URL_API_STREAM),
data=json.dumps({
'restrict': 'test_event1,test_event3'}),
stream=True, headers=HA_HEADERS)) as req:
data = self._stream_next_event(req) # self.assertEqual(listen_count + 2, self._listen_count())
self.assertEqual('ping', data)
self.assertEqual(listen_count + 2, self._listen_count()) # hass.bus.fire('test_event1')
# hass.pool.block_till_done()
# hass.bus.fire('test_event2')
# hass.pool.block_till_done()
# hass.bus.fire('test_event3')
# hass.pool.block_till_done()
hass.bus.fire('test_event1') # data = self._stream_next_event(req)
hass.pool.block_till_done() # self.assertEqual('test_event1', data['event_type'])
hass.bus.fire('test_event2') # data = self._stream_next_event(req)
hass.pool.block_till_done() # self.assertEqual('test_event3', data['event_type'])
hass.bus.fire('test_event3')
hass.pool.block_till_done()
data = self._stream_next_event(req) # def _stream_next_event(self, stream):
self.assertEqual('test_event1', data['event_type']) # """Test the stream for next event."""
data = self._stream_next_event(req) # data = b''
self.assertEqual('test_event3', data['event_type']) # last_new_line = False
# for dat in stream.iter_content(1):
# if dat == b'\n' and last_new_line:
# break
# data += dat
# last_new_line = dat == b'\n'
def _stream_next_event(self, stream): # conv = data.decode('utf-8').strip()[6:]
"""Test the stream for next event."""
data = b''
last_new_line = False
for dat in stream.iter_content(1):
if dat == b'\n' and last_new_line:
break
data += dat
last_new_line = dat == b'\n'
conv = data.decode('utf-8').strip()[6:] # return conv if conv == 'ping' else json.loads(conv)
return conv if conv == 'ping' else json.loads(conv) # def _listen_count(self):
# """Return number of event listeners."""
def _listen_count(self): # return sum(hass.bus.listeners.values())
"""Return number of event listeners."""
return sum(hass.bus.listeners.values())

View File

@ -3,6 +3,7 @@
import re import re
import unittest import unittest
import eventlet
import requests import requests
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
@ -42,6 +43,10 @@ def setUpModule(): # pylint: disable=invalid-name
hass.start() hass.start()
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
"""Stop everything that was started.""" """Stop everything that was started."""

View File

@ -12,7 +12,7 @@ from homeassistant.components import logbook
from tests.common import mock_http_component, get_test_home_assistant from tests.common import mock_http_component, get_test_home_assistant
class TestComponentHistory(unittest.TestCase): class TestComponentLogbook(unittest.TestCase):
"""Test the History component.""" """Test the History component."""
def setUp(self): def setUp(self):

View File

@ -2,6 +2,8 @@
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest import unittest
import eventlet
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
import homeassistant.remote as remote import homeassistant.remote as remote
@ -46,6 +48,10 @@ def setUpModule(): # pylint: disable=invalid-name
hass.start() hass.start()
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT)
# Start slave # Start slave
@ -57,6 +63,10 @@ def setUpModule(): # pylint: disable=invalid-name
slave.start() slave.start()
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
"""Stop the Home Assistant server and slave.""" """Stop the Home Assistant server and slave."""
@ -232,6 +242,7 @@ class TestRemoteClasses(unittest.TestCase):
slave.pool.block_till_done() slave.pool.block_till_done()
# Wait till master gives updated state # Wait till master gives updated state
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertEqual("remote.statemachine test", self.assertEqual("remote.statemachine test",
slave.states.get("remote.test").state) slave.states.get("remote.test").state)
@ -240,11 +251,13 @@ class TestRemoteClasses(unittest.TestCase):
"""Remove statemachine from master.""" """Remove statemachine from master."""
hass.states.set("remote.master_remove", "remove me!") hass.states.set("remote.master_remove", "remove me!")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertIn('remote.master_remove', slave.states.entity_ids()) self.assertIn('remote.master_remove', slave.states.entity_ids())
hass.states.remove("remote.master_remove") hass.states.remove("remote.master_remove")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertNotIn('remote.master_remove', slave.states.entity_ids()) self.assertNotIn('remote.master_remove', slave.states.entity_ids())
@ -252,12 +265,14 @@ class TestRemoteClasses(unittest.TestCase):
"""Remove statemachine from slave.""" """Remove statemachine from slave."""
hass.states.set("remote.slave_remove", "remove me!") hass.states.set("remote.slave_remove", "remove me!")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertIn('remote.slave_remove', slave.states.entity_ids())
self.assertTrue(slave.states.remove("remote.slave_remove")) self.assertTrue(slave.states.remove("remote.slave_remove"))
slave.pool.block_till_done() slave.pool.block_till_done()
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) self.assertNotIn('remote.slave_remove', slave.states.entity_ids())
@ -276,5 +291,6 @@ class TestRemoteClasses(unittest.TestCase):
slave.pool.block_till_done() slave.pool.block_till_done()
# Wait till master gives updated event # Wait till master gives updated event
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))