From 98de7c9287bc1620f9ce190ac3d0c6b99e2da80d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 May 2016 20:14:46 -0700 Subject: [PATCH 01/11] Upgrade eventlet to 0.19 --- homeassistant/components/api.py | 5 ++--- homeassistant/components/http.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index aa94914ae14..ad8f21f069b 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -113,14 +113,13 @@ class APIEventStream(HomeAssistantView): while True: try: - # Somehow our queue.get takes too long to - # be notified of arrival of object. Probably + # Somehow our queue.get sometimes takes too long to + # be notified of arrival of data. Probably # because of our spawning on hub in other thread # hack. Because current goal is to get this out, # We just timeout every second because it will # return right away if qsize() > 0. # So yes, we're basically polling :( - # socket.io anyone? payload = to_write.get(timeout=1) if payload is stop_obj: diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 864e517699b..8647f3c97c6 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import split_entity_id import homeassistant.util.dt as dt_util DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.18.4", "static3==0.7.0", "Werkzeug==0.11.5",) +REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",) CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" diff --git a/requirements_all.txt b/requirements_all.txt index 92c9b735b2c..cedf121eb43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -57,7 +57,7 @@ dweepy==0.2.0 eliqonline==1.0.12 # homeassistant.components.http -eventlet==0.18.4 +eventlet==0.19.0 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 From b3afb386b70dbe1c98e9a0d5c34dcfe488c8f2f5 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 23 May 2016 13:48:47 -0700 Subject: [PATCH 02/11] If no departure time is set, use now as the default. If departure time is set but does not have a :, assume its a preformed Unix timestamp and send along as raw input. Assume same for arrival_time. --- homeassistant/components/sensor/google_travel_time.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index b8513fa9bb6..a2f5e317aec 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -175,9 +175,15 @@ class GoogleTravelTimeSensor(Entity): atime = options_copy.get('arrival_time') if dtime is not None and ':' in dtime: options_copy['departure_time'] = convert_time_to_utc(dtime) + elif dtime is not None: + options_copy['departure_time'] = dtime + else: + options_copy['departure_time'] = 'now' if atime is not None and ':' in atime: options_copy['arrival_time'] = convert_time_to_utc(atime) + elif atime is not None: + options_copy['arrival_time'] = atime self._matrix = self._client.distance_matrix(self._origin, self._destination, From c96f73d1be946b5e0311c396a92aeeb3fde1fa7b Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 23 May 2016 14:05:12 -0700 Subject: [PATCH 03/11] If we have duration_in_traffic use that as the state, otherwise use duration --- homeassistant/components/sensor/google_travel_time.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index a2f5e317aec..c4415cc2cef 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -136,11 +136,12 @@ class GoogleTravelTimeSensor(Entity): @property def state(self): """Return the state of the sensor.""" - try: - res = self._matrix['rows'][0]['elements'][0]['duration']['value'] - return round(res/60) - except KeyError: - return None + _data = self._matrix['rows'][0]['elements'][0] + if 'duration_in_traffic' in _data: + return round(_data['duration_in_traffic']['value']/60) + if 'duration' in _data: + return round(_data['duration']['value']/60) + return None @property def name(self): From 712c51e28354a8b94fc8863f0fdc6b0e684d379c Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Mon, 23 May 2016 20:39:55 -0400 Subject: [PATCH 04/11] Fix TLS with eventlet (#2151) * Fix TLS with eventlet This fixes a simple error on my part when implementing the WSGI stuff. eventlet.wrap_ssl() returns a wrapped socket, it does not modify the object passed to it. We need to grab the returned value and use that. * Fix style issue --- homeassistant/components/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 8647f3c97c6..06fa09bb138 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -243,8 +243,8 @@ class HomeAssistantWSGI(object): 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) + sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, + keyfile=self.ssl_key, server_side=True) wsgi.server(sock, self) def dispatch_request(self, request): From dc8e55fb8be06dd393e9051757df78dfa736127a Mon Sep 17 00:00:00 2001 From: Jan Harkes Date: Mon, 23 May 2016 23:00:46 -0400 Subject: [PATCH 05/11] Don't even bother trying to kill stray child processes. When we change our process group id we don't get keyboard interrupt signals passed if our parent is a bash script. --- homeassistant/__main__.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index ab83d2aa09a..5dd43e0508a 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -4,7 +4,6 @@ from __future__ import print_function import argparse import os import platform -import signal import subprocess import sys import threading @@ -334,29 +333,6 @@ def try_to_restart(): except AssertionError: sys.stderr.write("Failed to count non-daemonic threads.\n") - # Send terminate signal to all processes in our process group which - # should be any children that have not themselves changed the process - # group id. Don't bother if couldn't even call setpgid. - if hasattr(os, 'setpgid'): - sys.stderr.write("Signalling child processes to terminate...\n") - os.kill(0, signal.SIGTERM) - - # wait for child processes to terminate - try: - while True: - time.sleep(1) - if os.waitpid(0, os.WNOHANG) == (0, 0): - break - except OSError: - pass - - elif os.name == 'nt': - # Maybe one of the following will work, but how do we indicate which - # processes are our children if there is no process group? - # os.kill(0, signal.CTRL_C_EVENT) - # os.kill(0, signal.CTRL_BREAK_EVENT) - pass - # Try to not leave behind open filedescriptors with the emphasis on try. try: max_fd = os.sysconf("SC_OPEN_MAX") @@ -408,13 +384,6 @@ def main(): if args.pid_file: write_pid(args.pid_file) - # Create new process group if we can - if hasattr(os, 'setpgid'): - try: - os.setpgid(0, 0) - except PermissionError: - pass - exit_code = setup_and_run_hass(config_dir, args) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() From 4cecc626f447c6d44434f7683a11b65d9d8e4c7f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 May 2016 22:45:35 -0700 Subject: [PATCH 06/11] manifest.json: remove trailing commas --- homeassistant/components/frontend/www_static/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/www_static/manifest.json b/homeassistant/components/frontend/www_static/manifest.json index 3767a4b1c5b..957c5812cd2 100644 --- a/homeassistant/components/frontend/www_static/manifest.json +++ b/homeassistant/components/frontend/www_static/manifest.json @@ -8,12 +8,12 @@ { "src": "/static/favicon-192x192.png", "sizes": "192x192", - "type": "image/png", + "type": "image/png" }, { "src": "/static/favicon-384x384.png", "sizes": "384x384", - "type": "image/png", + "type": "image/png" } ] } From 88bb13681356f0585027321aaab9e1de1621fda0 Mon Sep 17 00:00:00 2001 From: wokar Date: Tue, 24 May 2016 17:36:40 +0200 Subject: [PATCH 07/11] lg_netcast: fix exception on missing access_token (#2150) * specified default value for acccess_token to prevent exception on init --- homeassistant/components/media_player/lg_netcast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index fa215731d0d..7f15962723b 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): "lg_netcast", vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)), + vol.Optional(CONF_ACCESS_TOKEN, default=None): + vol.All(cv.string, vol.Length(max=6)), }) From 415cfc25376790a2de64da9ef747b2b29cb00aab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 May 2016 23:19:37 -0700 Subject: [PATCH 08/11] WSGI: Hide password in logs (#2164) * WSGI: Hide password in logs * Add auth + pw in logs tests --- homeassistant/components/http.py | 41 +++++++----- requirements_test.txt | 1 + tests/components/test_api.py | 24 +------ tests/components/test_http.py | 110 +++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 tests/components/test_http.py diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 06fa09bb138..6b2dc53a59f 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -31,8 +31,29 @@ _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _LOGGER = logging.getLogger(__name__) +class HideSensitiveFilter(logging.Filter): + """Filter API password calls.""" + + # pylint: disable=too-few-public-methods + def __init__(self, hass): + """Initialize sensitive data filter.""" + super().__init__() + self.hass = hass + + def filter(self, record): + """Hide sensitive data in messages.""" + if self.hass.wsgi.api_password is None: + return True + + record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******') + + return True + + def setup(hass, config): """Set up the HTTP API and debug interface.""" + _LOGGER.addFilter(HideSensitiveFilter(hass)) + conf = config.get(DOMAIN, {}) api_password = util.convert(conf.get(CONF_API_PASSWORD), str) @@ -202,7 +223,7 @@ class HomeAssistantWSGI(object): """Register a redirect with the server. If given this must be either a string or callable. In case of a - callable it’s called with the url adapter that triggered the match and + callable it's 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. @@ -245,7 +266,7 @@ class HomeAssistantWSGI(object): if self.ssl_certificate: sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, keyfile=self.ssl_key, server_side=True) - wsgi.server(sock, self) + wsgi.server(sock, self, log=_LOGGER) def dispatch_request(self, request): """Handle incoming request.""" @@ -318,9 +339,7 @@ class HomeAssistantView(object): def handle_request(self, request, **values): """Handle request to url.""" - from werkzeug.exceptions import ( - MethodNotAllowed, Unauthorized, BadRequest, - ) + from werkzeug.exceptions import MethodNotAllowed, Unauthorized try: handler = getattr(self, request.method.lower()) @@ -342,18 +361,6 @@ class HomeAssistantView(object): self.hass.wsgi.api_password): authenticated = True - else: - # 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 - if self.requires_auth and not authenticated: raise Unauthorized() diff --git a/requirements_test.txt b/requirements_test.txt index 52fc23680b9..5aba9dc540f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,5 +4,6 @@ coveralls>=1.1 pytest>=2.9.1 pytest-cov>=2.2.0 pytest-timeout>=1.0.0 +pytest-capturelog>=0.7 betamax==0.5.1 pydocstyle>=1.0.0 diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 532e0d66d3d..66fb97dfd33 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,4 +1,4 @@ -"""The tests for the Home Assistant HTTP component.""" +"""The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods # from contextlib import closing import json @@ -66,28 +66,6 @@ class TestAPI(unittest.TestCase): """Stop everything that was started.""" hass.pool.block_till_done() - # TODO move back to http component and test with use_auth. - def test_access_denied_without_password(self): - """Test access without password.""" - req = requests.get(_url(const.URL_API)) - - self.assertEqual(401, req.status_code) - - def test_access_denied_with_wrong_password(self): - """Test ascces with wrong password.""" - req = requests.get( - _url(const.URL_API), - headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) - - self.assertEqual(401, req.status_code) - - def test_access_with_password_in_url(self): - """Test access with password in URL.""" - req = requests.get( - "{}?api_password={}".format(_url(const.URL_API), API_PASSWORD)) - - self.assertEqual(200, req.status_code) - def test_api_list_state_entities(self): """Test if the debug interface allows us to list state entities.""" req = requests.get(_url(const.URL_API_STATES), diff --git a/tests/components/test_http.py b/tests/components/test_http.py new file mode 100644 index 00000000000..f665a9530c8 --- /dev/null +++ b/tests/components/test_http.py @@ -0,0 +1,110 @@ +"""The tests for the Home Assistant HTTP component.""" +# pylint: disable=protected-access,too-many-public-methods +import logging + +import eventlet +import requests + +from homeassistant import bootstrap, const +import homeassistant.components.http as http + +from tests.common import get_test_instance_port, get_test_home_assistant + +API_PASSWORD = "test1234" +SERVER_PORT = get_test_instance_port() +HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) +HA_HEADERS = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} + +hass = None + + +def _url(path=""): + """Helper method to generate URLs.""" + return HTTP_BASE_URL + path + + +def setUpModule(): # pylint: disable=invalid-name + """Initialize a Home Assistant server.""" + global hass + + hass = get_test_home_assistant() + + hass.bus.listen('test_event', lambda _: _) + hass.states.set('test.test', 'a_state') + + bootstrap.setup_component( + hass, http.DOMAIN, + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT}}) + + bootstrap.setup_component(hass, 'api') + + hass.start() + + eventlet.sleep(0.05) + + +def tearDownModule(): # pylint: disable=invalid-name + """Stop the Home Assistant server.""" + hass.stop() + + +class TestHttp: + """Test HTTP component.""" + + def test_access_denied_without_password(self): + """Test access without password.""" + req = requests.get(_url(const.URL_API)) + + assert req.status_code == 401 + + def test_access_denied_with_wrong_password_in_header(self): + """Test ascces with wrong password.""" + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) + + assert req.status_code == 401 + + def test_access_with_password_in_header(self, caplog): + """Test access with password in URL.""" + # Hide logging from requests package that we use to test logging + caplog.setLevel(logging.WARNING, + logger='requests.packages.urllib3.connectionpool') + + req = requests.get( + _url(const.URL_API), + headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD}) + + assert req.status_code == 200 + + logs = caplog.text() + + assert const.URL_API in logs + assert API_PASSWORD not in logs + + def test_access_denied_with_wrong_password_in_url(self): + """Test ascces with wrong password.""" + req = requests.get(_url(const.URL_API), + params={'api_password': 'wrongpassword'}) + + assert req.status_code == 401 + + def test_access_with_password_in_url(self, caplog): + """Test access with password in URL.""" + # Hide logging from requests package that we use to test logging + caplog.setLevel(logging.WARNING, + logger='requests.packages.urllib3.connectionpool') + + req = requests.get(_url(const.URL_API), + params={'api_password': API_PASSWORD}) + + assert req.status_code == 200 + + logs = caplog.text() + + assert const.URL_API in logs + assert API_PASSWORD not in logs From 3db31cb951b7c307940469b0c99442c2e40c7264 Mon Sep 17 00:00:00 2001 From: Scott Bartuska Date: Wed, 25 May 2016 09:09:40 -0700 Subject: [PATCH 09/11] Update PyISY to 1.0.6 (#2133) * Update PyISY to 1.0.6 1.0.6 is the newest version of PyISY * PyISY to 1.0.6 --- homeassistant/components/isy994.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 697aa4e8ea6..09bf62ce849 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.loader import get_component DOMAIN = "isy994" -REQUIREMENTS = ['PyISY==1.0.5'] +REQUIREMENTS = ['PyISY==1.0.6'] DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" diff --git a/requirements_all.txt b/requirements_all.txt index cedf121eb43..2f46d51fe3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ voluptuous==0.8.9 webcolors==1.5 # homeassistant.components.isy994 -PyISY==1.0.5 +PyISY==1.0.6 # homeassistant.components.arduino PyMata==2.12 From 49882255c4d1c151886652320fe473d4d19151b6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 25 May 2016 18:10:08 +0200 Subject: [PATCH 10/11] Upgrade astral to 1.1 (#2131) --- homeassistant/components/sun.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 9e678ae0ebe..5982af6131f 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -15,7 +15,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util import location as location_util from homeassistant.const import CONF_ELEVATION -REQUIREMENTS = ['astral==1.0'] +REQUIREMENTS = ['astral==1.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" diff --git a/requirements_all.txt b/requirements_all.txt index 2f46d51fe3d..614e4d85d3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -30,7 +30,7 @@ Werkzeug==0.11.5 apcaccess==0.0.4 # homeassistant.components.sun -astral==1.0 +astral==1.1 # homeassistant.components.light.blinksticklight blinkstick==1.1.7 From ca3da0e53e74b9cf0a67c0d51d070d863f757424 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 25 May 2016 12:10:59 -0400 Subject: [PATCH 11/11] Round temp and percentage for octoprint sensors (#2128) --- homeassistant/components/sensor/octoprint.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index bb4e6973df8..4bf543f3831 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -93,7 +93,11 @@ class OctoPrintSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + sensor_unit = self.unit_of_measurement + if sensor_unit == TEMP_CELSIUS or sensor_unit == "%": + return round(self._state, 2) + else: + return self._state @property def unit_of_measurement(self):