From 8f26311b03d1a6e48d3dbcd4dfaa92126c21ee8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Oct 2016 01:26:20 -0700 Subject: [PATCH] Fix more things --- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/emulated_hue.py | 99 ++++++++++++--------- homeassistant/components/history.py | 29 ++++-- homeassistant/components/http.py | 96 +++----------------- homeassistant/components/logbook.py | 32 +++++-- homeassistant/components/sensor/fitbit.py | 7 +- homeassistant/components/sensor/torque.py | 12 +-- homeassistant/components/switch/netio.py | 8 +- 8 files changed, 133 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2f23118a1c3006..6cc3c000de292f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -152,7 +152,7 @@ def get(self, request, entity_id): return self.Response(status=404) authenticated = (request.authenticated or - request.args.get('token') == camera.access_token) + request.GET.get('token') == camera.access_token) if not authenticated: return self.Response(status=401) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index d97974cd523c21..17cebe94789f1f 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -4,6 +4,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/emulated_hue/ """ +import asyncio import threading import socket import logging @@ -11,13 +12,14 @@ import os import select +from aiohttp import web import voluptuous as vol from homeassistant import util, core from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, HTTP_BAD_REQUEST + STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS @@ -85,17 +87,28 @@ def setup(hass, yaml_config): upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port) + # @core.callback def start_emulated_hue_bridge(event): """Start the emulated hue bridge.""" - server.start() + # hass.loop.create_task(server.start()) + # Temp, while fixing listen_once + from homeassistant.util.async import run_coroutine_threadsafe + + run_coroutine_threadsafe(server.start(), hass.loop).result() + upnp_listener.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + # @core.callback def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" upnp_listener.stop() - server.stop() + # hass.loop.create_task(server.stop()) + # Temp, while fixing listen_once + from homeassistant.util.async import run_coroutine_threadsafe + + run_coroutine_threadsafe(server.stop(), hass.loop).result() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) @@ -156,6 +169,7 @@ def __init__(self, hass, config): super().__init__(hass) self.config = config + @core.callback def get(self, request): """Handle a GET request.""" xml_template = """ @@ -183,7 +197,7 @@ def get(self, request): resp_text = xml_template.format( self.config.host_ip_addr, self.config.listen_port) - return self.Response(resp_text, mimetype='text/xml') + return web.Response(text=resp_text, content_type='text/xml') class HueUsernameView(HomeAssistantView): @@ -198,9 +212,13 @@ def __init__(self, hass): """Initialize the instance of the view.""" super().__init__(hass) + @asyncio.coroutine def post(self, request): """Handle a POST request.""" - data = request.json + try: + data = yield from request.json() + except json.decoder.JSONDecodeError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) if 'devicetype' not in data: return self.json_message('devicetype not specified', @@ -212,10 +230,10 @@ def post(self, request): class HueLightsView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" - url = '/api//lights' + url = '/api/{username}/lights' name = 'api:username:lights' - extra_urls = ['/api//lights/', - '/api//lights//state'] + extra_urls = ['/api/{username}/lights/{entity_id}', + '/api/{username}/lights/{entity_id}/state'] requires_auth = False def __init__(self, hass, config): @@ -224,58 +242,49 @@ def __init__(self, hass, config): self.config = config self.cached_states = {} + @core.callback def get(self, request, username, entity_id=None): """Handle a GET request.""" if entity_id is None: - return self.get_lights_list() + return self.async_get_lights_list() - if not request.base_url.endswith('state'): - return self.get_light_state(entity_id) + if not request.path.endswith('state'): + return self.async_get_light_state(entity_id) - return self.Response("Method not allowed", status=405) + return web.Response(text="Method not allowed", status=405) + @asyncio.coroutine def put(self, request, username, entity_id=None): """Handle a PUT request.""" - if not request.base_url.endswith('state'): - return self.Response("Method not allowed", status=405) - - content_type = request.environ.get('CONTENT_TYPE', '') - if content_type == 'application/x-www-form-urlencoded': - # Alexa sends JSON data with a form data content type, for - # whatever reason, and Werkzeug parses form data automatically, - # so we need to do some gymnastics to get the data we need - json_data = None - - for key in request.form: - try: - json_data = json.loads(key) - break - except ValueError: - # Try the next key? - pass - - if json_data is None: - return self.Response("Bad request", status=400) - else: - json_data = request.json + if not request.path.endswith('state'): + return web.Response(text="Method not allowed", status=405) + + if entity_id and self.hass.states.get(entity_id) is None: + return self.json_message('Entity not found', HTTP_NOT_FOUND) + + try: + json_data = yield from request.json() + except json.decoder.JSONDecodeError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - return self.put_light_state(json_data, entity_id) + result = yield from self.async_put_light_state(json_data, entity_id) + return result - def get_lights_list(self): + def async_get_lights_list(self): """Process a request to get the list of available lights.""" json_response = {} - for entity in self.hass.states.all(): + for entity in self.hass.states.async_all(): if self.is_entity_exposed(entity): json_response[entity.entity_id] = entity_to_json(entity) return self.json(json_response) - def get_light_state(self, entity_id): + def async_get_light_state(self, entity_id): """Process a request to get the state of an individual light.""" entity = self.hass.states.get(entity_id) if entity is None or not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) cached_state = self.cached_states.get(entity_id, None) @@ -290,23 +299,24 @@ def get_light_state(self, entity_id): return self.json(json_response) - def put_light_state(self, request_json, entity_id): + @asyncio.coroutine + def async_put_light_state(self, request_json, entity_id): """Process a request to set the state of an individual light.""" config = self.config # Retrieve the entity from the state machine entity = self.hass.states.get(entity_id) if entity is None: - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) if not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) # Parse the request into requested "on" status and brightness parsed = parse_hue_api_put_light_body(request_json, entity) if parsed is None: - return self.Response("Bad request", status=400) + return web.Response(text="Bad request", status=400) result, brightness = parsed @@ -331,7 +341,8 @@ def put_light_state(self, request_json, entity_id): self.cached_states[entity_id] = (result, brightness) # Perform the requested action - self.hass.services.call(core.DOMAIN, service, data, blocking=True) + yield from self.hass.services.async_call(core.DOMAIN, service, data, + blocking=True) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 4cebf637c16da7..e7f0d1572b728a 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,11 +4,15 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/history/ """ +import asyncio from collections import defaultdict from datetime import timedelta from itertools import groupby import voluptuous as vol +from aiohttp import web + +from homeassistant.const import HTTP_BAD_REQUEST import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components import recorder, script @@ -192,16 +196,19 @@ def setup(hass, config): class Last5StatesView(HomeAssistantView): """Handle last 5 state view requests.""" - url = '/api/history/entity//recent_states' + url = '/api/history/entity/{entity_id}/recent_states' name = 'api:history:entity-recent-states' def __init__(self, hass): """Initilalize the history last 5 states view.""" super().__init__(hass) + @asyncio.coroutine def get(self, request, entity_id): """Retrieve last 5 states of entity.""" - return self.json(last_5_states(entity_id)) + result = yield from self.hass.loop.run_in_executor( + last_5_states, entity_id) + return self.json(result) class HistoryPeriodView(HomeAssistantView): @@ -209,15 +216,22 @@ class HistoryPeriodView(HomeAssistantView): url = '/api/history/period' name = 'api:history:view-period' - extra_urls = ['/api/history/period/'] + extra_urls = ['/api/history/period/{datetime}'] def __init__(self, hass, filters): """Initilalize the history period view.""" super().__init__(hass) self.filters = filters + @asyncio.coroutine def get(self, request, datetime=None): """Return history over a period of time.""" + if datetime: + datetime = dt_util.parse_datetime(datetime) + + if datetime is None: + return web.Response('Invalid datetime', HTTP_BAD_REQUEST) + one_day = timedelta(days=1) if datetime: @@ -226,10 +240,13 @@ def get(self, request, datetime=None): start_time = dt_util.utcnow() - one_day end_time = start_time + one_day - entity_id = request.args.get('filter_entity_id') + entity_id = request.GET.get('filter_entity_id') + + result = yield from self.hass.loop.run_in_executor( + get_significant_states, start_time, end_time, entity_id, + self.filters) - return self.json(get_significant_states( - start_time, end_time, entity_id, self.filters).values()) + return self.json(result.values()) # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 9d471916ff264c..100ca0a2f5e727 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -135,21 +135,25 @@ def setup(hass, config): trusted_networks=trusted_networks ) - @callback + # @callback def start_server(event): - hass.loop.create_task(server.start()) + # hass.loop.create_task(server.start()) - # Temp, while fixing listen_once - from homeassistant.util.async import run_coroutine_threadsafe + # Temp, while fixing listen_once + from homeassistant.util.async import run_coroutine_threadsafe - def start_server(event): run_coroutine_threadsafe(server.start(), hass.loop).result() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server) - @callback + # @callback def stop_server(event): - hass.loop.create_task(server.stop) + # hass.loop.create_task(server.stop) + + # Temp, while fixing listen_once + from homeassistant.util.async import run_coroutine_threadsafe + + run_coroutine_threadsafe(server.stop(), hass.loop).result() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) @@ -162,79 +166,6 @@ def stop_server(event): return True -# def routing_map(hass): -# """Generate empty routing map with HA validators.""" -# from werkzeug.routing import Map, BaseConverter, ValidationError - -# class EntityValidator(BaseConverter): -# """Validate entity_id in urls.""" - -# regex = r"(\w+)\.(\w+)" - -# def __init__(self, url_map, exist=True, domain=None): -# """Initilalize entity validator.""" -# super().__init__(url_map) -# self._exist = exist -# self._domain = domain - -# def to_python(self, value): -# """Validate entity id.""" -# if self._exist and hass.states.get(value) is None: -# raise ValidationError() -# if self._domain is not None and \ -# split_entity_id(value)[0] != self._domain: -# raise ValidationError() - -# return value - -# def to_url(self, value): -# """Convert entity_id for a url.""" -# return value - -# class DateValidator(BaseConverter): -# """Validate dates in urls.""" - -# regex = r'\d{4}-\d{1,2}-\d{1,2}' - -# def to_python(self, value): -# """Validate and convert date.""" -# parsed = dt_util.parse_date(value) - -# if parsed is None: -# raise ValidationError() - -# return parsed - -# def to_url(self, value): -# """Convert date to url value.""" -# return value.isoformat() - -# class DateTimeValidator(BaseConverter): -# """Validate datetimes in urls formatted per ISO 8601.""" - -# regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \ -# r'\.\d+([+-][0-2]\d:[0-5]\d|Z)' - -# def to_python(self, value): -# """Validate and convert date.""" -# parsed = dt_util.parse_datetime(value) - -# if parsed is None: -# raise ValidationError() - -# return parsed - -# def to_url(self, value): -# """Convert date to url value.""" -# return value.isoformat() - -# return Map(converters={ -# 'entity': EntityValidator, -# 'date': DateValidator, -# 'datetime': DateTimeValidator, -# }) - - class HomeAssistantWSGI(object): """WSGI server for Home Assistant.""" @@ -463,7 +394,7 @@ def __call__(self, request): request.path, remote_addr, authenticated) assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ - "Handler should be a coroutine or a callback" + "Handler should be a coroutine or a callback." result = handler(request, **request.match_info) @@ -484,6 +415,7 @@ def __call__(self, request): elif result is None: result = b'' elif not isinstance(result, bytes): - assert False, 'Result should be None, string, bytes or Response.' + assert False, ('Result should be None, string, bytes or Response. ' + 'Got: {}').format(result) return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index e732d5c350d774..3a38196d303cd7 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -9,8 +9,10 @@ from datetime import timedelta from itertools import groupby +from aiohttp import web import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components import recorder, sun @@ -19,7 +21,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_OFF, STATE_ON, - ATTR_HIDDEN) + ATTR_HIDDEN, HTTP_BAD_REQUEST) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN from homeassistant.util.async import run_callback_threadsafe @@ -88,7 +90,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): def setup(hass, config): """Listen for download events to download files.""" - @asyncio.coroutine + @callback def log_message(service): """Handle sending notification message service calls.""" message = service.data[ATTR_MESSAGE] @@ -125,15 +127,27 @@ def __init__(self, hass, config): @asyncio.coroutine def get(self, request, datetime=None): """Retrieve logbook entries.""" - start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day()) + if datetime: + datetime = dt_util.parse_datetime(datetime) + + if datetime is None: + return web.Response('Invalid datetime', HTTP_BAD_REQUEST) + else: + datetime = dt_util.start_of_local_day() + + start_day = dt_util.as_utc(datetime) end_day = start_day + timedelta(days=1) - events = recorder.get_model('Events') - query = recorder.query('Events').filter( - (events.time_fired > start_day) & - (events.time_fired < end_day)) - events = recorder.execute(query) - events = _exclude_events(events, self.config) + def get_results(): + """Query DB for results.""" + events = recorder.get_model('Events') + query = recorder.query('Events').filter( + (events.time_fired > start_day) & + (events.time_fired < end_day)) + events = recorder.execute(query) + return _exclude_events(events, self.config) + + events = yield from self.hass.loop.run_in_executor(get_results) return self.json(humanify(events)) diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 11288bae63a49c..2ad7f60cce8334 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -12,6 +12,7 @@ import voluptuous as vol +from homeassistant.const import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity @@ -294,12 +295,13 @@ def __init__(self, hass, config, add_devices, oauth): self.add_devices = add_devices self.oauth = oauth + @callback 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 + data = request.GET response_message = """Fitbit has been successfully authorized! You can close this window now!""" @@ -340,7 +342,8 @@ def get(self, request): config_contents): _LOGGER.error("Failed to save config file") - setup_platform(self.hass, self.config, self.add_devices) + self.hass.async_add_job(setup_platform, self.hass, self.config, + self.add_devices) return html_response diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index c05217692acead..dcb53b605035dc 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -9,6 +9,7 @@ import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_EMAIL, CONF_NAME) @@ -77,9 +78,10 @@ def __init__(self, hass, email, vehicle, sensors, add_devices): self.sensors = sensors self.add_devices = add_devices + @callback def get(self, request): """Handle Torque data request.""" - data = request.args + data = request.GET if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: return @@ -100,14 +102,14 @@ def get(self, request): elif is_value: pid = convert_pid(is_value.group(1)) if pid in self.sensors: - self.sensors[pid].on_update(data[key]) + self.sensors[pid].async_on_update(data[key]) for pid in names: if pid not in self.sensors: self.sensors[pid] = TorqueSensor( ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]), units.get(pid, None)) - self.add_devices([self.sensors[pid]]) + self.hass.async_add_job(self.add_devices, [self.sensors[pid]]) return None @@ -141,7 +143,7 @@ def icon(self): """Return the default icon of the sensor.""" return 'mdi:car' - def on_update(self, value): + def async_on_update(self, value): """Receive an update.""" self._state = value - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 03a3d311f3c73b..28e71af87b31ef 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -10,6 +10,7 @@ import voluptuous as vol +from homeassistant.core import callback from homeassistant import util from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -40,7 +41,7 @@ REQ_CONF = [CONF_HOST, CONF_OUTLETS] -URL_API_NETIO_EP = '/api/netio/' +URL_API_NETIO_EP = '/api/netio/{host}' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -93,9 +94,10 @@ class NetioApiView(HomeAssistantView): url = URL_API_NETIO_EP name = 'api:netio' + @callback def get(self, request, host): """Request handler.""" - data = request.args + data = request.GET states, consumptions, cumulated_consumptions, start_dates = \ [], [], [], [] @@ -117,7 +119,7 @@ def get(self, request, host): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - dev.update_ha_state() + self.hass.loop.create_task(dev.async_update_ha_state()) return self.json(True)