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() 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/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" } ] } diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 864e517699b..6b2dc53a59f 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" @@ -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. @@ -243,9 +264,9 @@ 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) - wsgi.server(sock, self) + sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, + keyfile=self.ssl_key, server_side=True) + 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/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/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)), }) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index b8513fa9bb6..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): @@ -175,9 +176,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, 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): 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 ea490d08194..5fb7916189e 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 @@ -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 @@ -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 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