From 2bc84af87e219da35a8deaedbd41017d679214e7 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 22 Oct 2016 15:23:01 -0700 Subject: [PATCH 001/149] Version bump to 0.32.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index efb11cdffbf..5c9c8c88b75 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 31 -PATCH_VERSION = '0' +MINOR_VERSION = 32 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 13ab2be5f6e81c697e437b425841368e32ce1948 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Sun, 23 Oct 2016 15:47:06 +0100 Subject: [PATCH 002/149] Exclude dirs/files prefixed with . (#3986) --- homeassistant/util/yaml.py | 12 +++++-- tests/util/test_yaml.py | 71 ++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index cf773bb999f..c831a9e9784 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -61,11 +61,17 @@ def _include_yaml(loader: SafeLineLoader, return load_yaml(fname) -def _find_files(directory, pattern): +def _is_file_valid(name: str) -> bool: + """Decide if a file is valid.""" + return not name.startswith('.') + + +def _find_files(directory: str, pattern: str): """Recursively load files in a directory.""" - for root, _dirs, files in os.walk(directory): + for root, dirs, files in os.walk(directory, topdown=True): + dirs[:] = [d for d in dirs if _is_file_valid(d)] for basename in files: - if fnmatch.fnmatch(basename, pattern): + if _is_file_valid(basename) and fnmatch.fnmatch(basename, pattern): filename = os.path.join(root, basename) yield filename diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 7c7bb0b9255..5b68042a1ff 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -76,10 +76,13 @@ class TestYaml(unittest.TestCase): @patch('homeassistant.util.yaml.os.walk') def test_include_dir_list(self, mock_walk): """Test include dir list yaml.""" - mock_walk.return_value = [['/tmp', [], ['one.yaml', 'two.yaml']]] + mock_walk.return_value = [ + ['/tmp', [], ['one.yaml', 'two.yaml']], + ] with patch_yaml_files({ - '/tmp/one.yaml': 'one', '/tmp/two.yaml': 'two' + '/tmp/one.yaml': 'one', + '/tmp/two.yaml': 'two', }): conf = "key: !include_dir_list /tmp" with io.StringIO(conf) as file: @@ -90,26 +93,36 @@ class TestYaml(unittest.TestCase): def test_include_dir_list_recursive(self, mock_walk): """Test include dir recursive list yaml.""" mock_walk.return_value = [ - ['/tmp', ['tmp2'], ['zero.yaml']], + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['zero.yaml']], ['/tmp/tmp2', [], ['one.yaml', 'two.yaml']], + ['/tmp/.ignore', [], []], + ['/tmp/ignore', [], ['.ignore.yaml']] ] with patch_yaml_files({ - '/tmp/zero.yaml': 'zero', '/tmp/tmp2/one.yaml': 'one', - '/tmp/tmp2/two.yaml': 'two' + '/tmp/zero.yaml': 'zero', + '/tmp/tmp2/one.yaml': 'one', + '/tmp/tmp2/two.yaml': 'two' }): conf = "key: !include_dir_list /tmp" with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] assert sorted(doc["key"]) == sorted(["zero", "one", "two"]) @patch('homeassistant.util.yaml.os.walk') def test_include_dir_named(self, mock_walk): """Test include dir named yaml.""" - mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] + mock_walk.return_value = [ + ['/tmp', [], ['first.yaml', 'second.yaml']] + ] with patch_yaml_files({ - '/tmp/first.yaml': 'one', '/tmp/second.yaml': 'two' + '/tmp/first.yaml': 'one', + '/tmp/second.yaml': 'two' }): conf = "key: !include_dir_named /tmp" correct = {'first': 'one', 'second': 'two'} @@ -121,18 +134,25 @@ class TestYaml(unittest.TestCase): def test_include_dir_named_recursive(self, mock_walk): """Test include dir named yaml.""" mock_walk.return_value = [ - ['/tmp', ['tmp2'], ['first.yaml']], + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/.ignore', [], []], + ['/tmp/ignore', [], ['.ignore.yaml']] ] with patch_yaml_files({ - '/tmp/first.yaml': 'one', '/tmp/tmp2/second.yaml': 'two', - '/tmp/tmp2/third.yaml': 'three' + '/tmp/first.yaml': 'one', + '/tmp/tmp2/second.yaml': 'two', + '/tmp/tmp2/third.yaml': 'three' }): conf = "key: !include_dir_named /tmp" correct = {'first': 'one', 'second': 'two', 'third': 'three'} with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] assert doc["key"] == correct @patch('homeassistant.util.yaml.os.walk') @@ -141,8 +161,8 @@ class TestYaml(unittest.TestCase): mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] with patch_yaml_files({ - '/tmp/first.yaml': '- one', - '/tmp/second.yaml': '- two\n- three' + '/tmp/first.yaml': '- one', + '/tmp/second.yaml': '- two\n- three' }): conf = "key: !include_dir_merge_list /tmp" with io.StringIO(conf) as file: @@ -153,17 +173,24 @@ class TestYaml(unittest.TestCase): def test_include_dir_merge_list_recursive(self, mock_walk): """Test include dir merge list yaml.""" mock_walk.return_value = [ - ['/tmp', ['tmp2'], ['first.yaml']], + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/.ignore', [], []], + ['/tmp/ignore', [], ['.ignore.yaml']] ] with patch_yaml_files({ - '/tmp/first.yaml': '- one', '/tmp/tmp2/second.yaml': '- two', - '/tmp/tmp2/third.yaml': '- three\n- four' + '/tmp/first.yaml': '- one', + '/tmp/tmp2/second.yaml': '- two', + '/tmp/tmp2/third.yaml': '- three\n- four' }): conf = "key: !include_dir_merge_list /tmp" with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] assert sorted(doc["key"]) == sorted(["one", "two", "three", "four"]) @@ -189,18 +216,24 @@ class TestYaml(unittest.TestCase): def test_include_dir_merge_named_recursive(self, mock_walk): """Test include dir merge named yaml.""" mock_walk.return_value = [ - ['/tmp', ['tmp2'], ['first.yaml']], + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/.ignore', [], []], + ['/tmp/ignore', [], ['.ignore.yaml']] ] with patch_yaml_files({ - '/tmp/first.yaml': 'key1: one', - '/tmp/tmp2/second.yaml': 'key2: two', - '/tmp/tmp2/third.yaml': 'key3: three\nkey4: four' + '/tmp/first.yaml': 'key1: one', + '/tmp/tmp2/second.yaml': 'key2: two', + '/tmp/tmp2/third.yaml': 'key3: three\nkey4: four' }): conf = "key: !include_dir_merge_named /tmp" with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] assert doc["key"] == { "key1": "one", "key2": "two", From 1f89e6ddba9f9ee5142f41e79c61fda465b24fc8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Oct 2016 20:51:41 +0200 Subject: [PATCH 003/149] Upgrade pytz to 2016.7 (#4002) --- requirements_all.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 01a0acdeebd..4ac6c45c01d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,7 +1,7 @@ # Home Assistant core requests>=2,<3 pyyaml>=3.11,<4 -pytz>=2016.6.1 +pytz>=2016.7 pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.2 diff --git a/setup.py b/setup.py index 67366bd7d83..d20aabb0f71 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'requests>=2,<3', 'pyyaml>=3.11,<4', - 'pytz>=2016.6.1', + 'pytz>=2016.7', 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.9.2', From 5df84775365bdcc671fb510dba7875dc6b611ccf Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 24 Oct 2016 03:55:06 +0200 Subject: [PATCH 004/149] Catch UnicodeDecodeError Error (#4007) * Catch UnicodeDecodeError Error Error for #3933 * Forgot (exc) * catch... * Tests by @lwis * Docstring * Create open --- homeassistant/util/yaml.py | 3 +++ tests/util/test_yaml.py | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index c831a9e9784..3ee47e76cf2 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -43,6 +43,9 @@ def load_yaml(fname: str) -> Union[List, Dict]: except yaml.YAMLError as exc: _LOGGER.error(exc) raise HomeAssistantError(exc) + except UnicodeDecodeError as exc: + _LOGGER.error('Unable to read file %s: %s', fname, exc) + raise HomeAssistantError(exc) def clear_secret_cache() -> None: diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 5b68042a1ff..9ead3c858a5 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -95,7 +95,6 @@ class TestYaml(unittest.TestCase): mock_walk.return_value = [ ['/tmp', ['tmp2', '.ignore', 'ignore'], ['zero.yaml']], ['/tmp/tmp2', [], ['one.yaml', 'two.yaml']], - ['/tmp/.ignore', [], []], ['/tmp/ignore', [], ['.ignore.yaml']] ] @@ -136,7 +135,6 @@ class TestYaml(unittest.TestCase): mock_walk.return_value = [ ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/.ignore', [], []], ['/tmp/ignore', [], ['.ignore.yaml']] ] @@ -175,7 +173,6 @@ class TestYaml(unittest.TestCase): mock_walk.return_value = [ ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/.ignore', [], []], ['/tmp/ignore', [], ['.ignore.yaml']] ] @@ -218,7 +215,6 @@ class TestYaml(unittest.TestCase): mock_walk.return_value = [ ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/.ignore', [], []], ['/tmp/ignore', [], ['.ignore.yaml']] ] @@ -241,6 +237,13 @@ class TestYaml(unittest.TestCase): "key4": "four" } + @patch('homeassistant.util.yaml.open', create=True) + def test_load_yaml_encoding_error(self, mock_open): + """Test raising a UnicodeDecodeError.""" + mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '') + self.assertRaises(HomeAssistantError, yaml.load_yaml, 'test') + + FILES = {} From 626763a7c344235073d6831e54502f74fcd4365c Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 23 Oct 2016 19:17:34 -0700 Subject: [PATCH 005/149] iOS component hotfixes (#4015) * iOS component hot fixes around component/platform loading, logging, and more * Load device_tracker and zeroconf in deps instead of bootstraping * Change conditional check on status code --- homeassistant/components/ios.py | 35 ++++---------------- homeassistant/components/notify/ios.py | 45 ++++++++++++++++---------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 0793417fab3..e8545210182 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -13,8 +13,6 @@ from voluptuous.humanize import humanize_error from homeassistant.helpers import config_validation as cv -import homeassistant.loader as loader - from homeassistant.helpers import discovery from homeassistant.components.http import HomeAssistantView @@ -22,13 +20,11 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, HTTP_BAD_REQUEST) -from homeassistant.components.notify import DOMAIN as NotifyDomain - _LOGGER = logging.getLogger(__name__) DOMAIN = "ios" -DEPENDENCIES = ["http"] +DEPENDENCIES = ["device_tracker", "http", "zeroconf"] CONF_PUSH = "push" CONF_PUSH_CATEGORIES = "categories" @@ -245,34 +241,17 @@ def setup(hass, config): if CONFIG_FILE == {}: CONFIG_FILE[ATTR_DEVICES] = {} - device_tracker = loader.get_component("device_tracker") - if device_tracker.DOMAIN not in hass.config.components: - device_tracker.setup(hass, {}) - # Need this to enable requirements checking in the app. - hass.config.components.append(device_tracker.DOMAIN) - - if "notify.ios" not in hass.config.components: - notify = loader.get_component("notify.ios") - notify.get_service(hass, {}) - # Need this to enable requirements checking in the app. - if NotifyDomain not in hass.config.components: - hass.config.components.append(NotifyDomain) - - zeroconf = loader.get_component("zeroconf") - if zeroconf.DOMAIN not in hass.config.components: - zeroconf.setup(hass, config) - # Need this to enable requirements checking in the app. - hass.config.components.append(zeroconf.DOMAIN) + # Notify needs to have discovery + # notify_config = {"notify": {CONF_PLATFORM: "ios"}} + # bootstrap.setup_component(hass, "notify", notify_config) discovery.load_platform(hass, "sensor", DOMAIN, {}, config) hass.wsgi.register_view(iOSIdentifyDeviceView(hass)) - if config.get(DOMAIN) is not None: - app_config = config[DOMAIN] - if app_config.get(CONF_PUSH) is not None: - push_config = app_config[CONF_PUSH] - hass.wsgi.register_view(iOSPushConfigView(hass, push_config)) + app_config = config.get(DOMAIN, {}) + hass.wsgi.register_view(iOSPushConfigView(hass, + app_config.get(CONF_PUSH, {}))) return True diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index cb85ab8f753..2517020434e 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -23,6 +23,22 @@ PUSH_URL = "https://ios-push.home-assistant.io/push" DEPENDENCIES = ["ios"] +# pylint: disable=invalid-name +def log_rate_limits(target, resp, level=20): + """Output rate limit log line at given level.""" + rate_limits = resp["rateLimits"] + resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) + resetsAtTime = resetsAt - datetime.now(timezone.utc) + rate_limit_msg = ("iOS push notification rate limits for %s: " + "%d sent, %d allowed, %d errors, " + "resets in %s") + _LOGGER.log(level, rate_limit_msg, + ios.device_name_for_push_id(target), + rate_limits["successful"], + rate_limits["maximum"], rate_limits["errors"], + str(resetsAtTime).split(".")[0]) + + def get_service(hass, config): """Get the iOS notification service.""" if "notify.ios" not in hass.config.components: @@ -66,22 +82,17 @@ class iOSNotificationService(BaseNotificationService): req = requests.post(PUSH_URL, json=data, timeout=10) - if req.status_code is not 201: - message = req.json()["message"] - if req.status_code is 429: + if req.status_code != 201: + fallback_error = req.json().get("errorMessage", + "Unknown error") + fallback_message = ("Internal server error, " + "please try again later: " + "{}").format(fallback_error) + message = req.json().get("message", fallback_message) + if req.status_code == 429: _LOGGER.warning(message) - elif req.status_code is 400 or 500: + log_rate_limits(target, req.json(), 30) + else: _LOGGER.error(message) - - if req.status_code in (201, 429): - rate_limits = req.json()["rateLimits"] - resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) - resetsAtTime = resetsAt - datetime.now(timezone.utc) - rate_limit_msg = ("iOS push notification rate limits for %s: " - "%d sent, %d allowed, %d errors, " - "resets in %s") - _LOGGER.info(rate_limit_msg, - ios.device_name_for_push_id(target), - rate_limits["successful"], - rate_limits["maximum"], rate_limits["errors"], - str(resetsAtTime).split(".")[0]) + else: + log_rate_limits(target, req.json()) From 9aa88819a5c7d2560ebab58c693dbf0d3af12d84 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sun, 23 Oct 2016 20:33:49 -0700 Subject: [PATCH 006/149] Fix a spelling problem on user-facing error --- homeassistant/helpers/condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index bfc637cf946..a6db4f9150d 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -283,7 +283,7 @@ def async_template(hass, value_template, variables=None): try: value = value_template.async_render(variables) except TemplateError as ex: - _LOGGER.error('Error duriong template condition: %s', ex) + _LOGGER.error('Error during template condition: %s', ex) return False return value.lower() == 'true' From 519d9f2fd01751492909e574b8ba3812487d7d6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 23 Oct 2016 23:48:01 -0700 Subject: [PATCH 007/149] async HTTP component (#3914) * Migrate WSGI to asyncio * Rename wsgi -> http * Python 3.4 compat * Move linting to Python 3.4 * lint * Lint * Fix Python 3.4 mock_open + binary data * Surpress logging aiohttp.access * Spelling * Sending files is a coroutine * More callback annotations and naming fixes * Fix ios --- .travis.yml | 6 +- homeassistant/bootstrap.py | 1 + homeassistant/components/alexa.py | 32 +- homeassistant/components/api.py | 195 ++++--- .../components/binary_sensor/ffmpeg.py | 2 +- homeassistant/components/camera/__init__.py | 113 ++-- homeassistant/components/camera/ffmpeg.py | 61 +- homeassistant/components/camera/generic.py | 55 +- homeassistant/components/camera/mjpeg.py | 71 ++- .../components/device_tracker/locative.py | 26 +- homeassistant/components/emulated_hue.py | 110 ++-- homeassistant/components/ffmpeg.py | 28 +- homeassistant/components/foursquare.py | 31 +- homeassistant/components/frontend/__init__.py | 54 +- homeassistant/components/history.py | 31 +- homeassistant/components/http.py | 521 ++++++++---------- homeassistant/components/ios.py | 2 +- homeassistant/components/logbook.py | 36 +- .../components/media_player/__init__.py | 34 +- homeassistant/components/notify/html5.py | 41 +- homeassistant/components/openalpr.py | 2 +- .../components/persistent_notification.py | 12 +- homeassistant/components/sensor/fitbit.py | 11 +- homeassistant/components/sensor/torque.py | 15 +- homeassistant/components/switch/netio.py | 10 +- homeassistant/helpers/state.py | 7 +- homeassistant/remote.py | 34 +- requirements_all.txt | 15 +- requirements_test.txt | 2 + setup.py | 2 + tests/__init__.py | 30 +- tests/common.py | 56 +- tests/components/camera/test_generic.py | 142 +++-- tests/components/camera/test_local_file.py | 84 ++- tests/components/camera/test_uvc.py | 2 +- tests/components/media_player/test_demo.py | 87 +-- tests/components/notify/test_html5.py | 171 +++--- tests/components/sensor/test_yr.py | 23 +- tests/components/test_api.py | 22 +- tests/components/test_frontend.py | 6 +- tests/components/test_http.py | 12 +- tests/components/test_influxdb.py | 3 + tests/conftest.py | 59 ++ tests/helpers/test_state.py | 62 ++- tests/test_util/aiohttp.py | 112 ++++ 45 files changed, 1422 insertions(+), 1009 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_util/aiohttp.py diff --git a/.travis.yml b/.travis.yml index 3d575c1d778..9cf13f2c831 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,11 @@ sudo: false matrix: fast_finish: true include: - - python: "3.4" + - python: "3.4.2" env: TOXENV=py34 - - python: "3.4" + - python: "3.4.2" env: TOXENV=requirements - - python: "3.5" + - python: "3.4.2" env: TOXENV=lint - python: "3.5" env: TOXENV=typing diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8ad4e16c8cd..0eca105952f 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -359,6 +359,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, # suppress overly verbose logs from libraries that aren't helpful logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("aiohttp.access").setLevel(logging.WARNING) try: from colorlog import ColoredFormatter diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 64ff50af323..3093b0eb12f 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -4,6 +4,7 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ +import asyncio import copy import enum import logging @@ -12,6 +13,7 @@ from datetime import datetime import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) INTENTS_API_ENDPOINT = '/api/alexa' -FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/' +FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}' CONF_ACTION = 'action' CONF_CARD = 'card' @@ -102,8 +104,8 @@ def setup(hass, config): intents = config[DOMAIN].get(CONF_INTENTS, {}) flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {}) - hass.wsgi.register_view(AlexaIntentsView(hass, intents)) - hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings)) + hass.http.register_view(AlexaIntentsView(hass, intents)) + hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings)) return True @@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView): self.intents = intents + @asyncio.coroutine def post(self, request): """Handle Alexa.""" - data = request.json + data = yield from request.json() _LOGGER.debug('Received Alexa request: %s', data) @@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView): action = config.get(CONF_ACTION) if action is not None: - action.run(response.variables) + yield from action.async_run(response.variables) # pylint: disable=unsubscriptable-object if speech is not None: @@ -218,8 +221,8 @@ class AlexaResponse(object): self.card = card return - card["title"] = title.render(self.variables) - card["content"] = content.render(self.variables) + card["title"] = title.async_render(self.variables) + card["content"] = content.async_render(self.variables) self.card = card def add_speech(self, speech_type, text): @@ -229,7 +232,7 @@ class AlexaResponse(object): key = 'ssml' if speech_type == SpeechType.ssml else 'text' if isinstance(text, template.Template): - text = text.render(self.variables) + text = text.async_render(self.variables) self.speech = { 'type': speech_type.value, @@ -244,7 +247,7 @@ class AlexaResponse(object): self.reprompt = { 'type': speech_type.value, - key: text.render(self.variables) + key: text.async_render(self.variables) } def as_dict(self): @@ -284,6 +287,7 @@ class AlexaFlashBriefingView(HomeAssistantView): template.attach(hass, self.flash_briefings) # pylint: disable=too-many-branches + @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" _LOGGER.debug('Received Alexa flash briefing request for: %s', @@ -292,7 +296,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if self.flash_briefings.get(briefing_id) is None: err = 'No configured Alexa flash briefing was found for: %s' _LOGGER.error(err, briefing_id) - return self.Response(status=404) + return b'', 404 briefing = [] @@ -300,13 +304,13 @@ class AlexaFlashBriefingView(HomeAssistantView): output = {} if item.get(CONF_TITLE) is not None: if isinstance(item.get(CONF_TITLE), template.Template): - output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render() + output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render() else: output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) if item.get(CONF_TEXT) is not None: if isinstance(item.get(CONF_TEXT), template.Template): - output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render() + output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render() else: output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) @@ -315,7 +319,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if item.get(CONF_AUDIO) is not None: if isinstance(item.get(CONF_AUDIO), template.Template): - output[ATTR_STREAM_URL] = item[CONF_AUDIO].render() + output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render() else: output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) @@ -323,7 +327,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if isinstance(item.get(CONF_DISPLAY_URL), template.Template): output[ATTR_REDIRECTION_URL] = \ - item[CONF_DISPLAY_URL].render() + item[CONF_DISPLAY_URL].async_render() else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 5eb28c53a34..ae5e1de7c1b 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/ import asyncio import json import logging -import queue + +from aiohttp import web +import async_timeout import homeassistant.core as ha import homeassistant.remote as rem @@ -21,7 +23,7 @@ from homeassistant.const import ( URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, __version__) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import TrackStates +from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView @@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """Register the API with the HTTP interface.""" - hass.wsgi.register_view(APIStatusView) - hass.wsgi.register_view(APIEventStream) - hass.wsgi.register_view(APIConfigView) - hass.wsgi.register_view(APIDiscoveryView) - hass.wsgi.register_view(APIStatesView) - hass.wsgi.register_view(APIEntityStateView) - hass.wsgi.register_view(APIEventListenersView) - hass.wsgi.register_view(APIEventView) - hass.wsgi.register_view(APIServicesView) - hass.wsgi.register_view(APIDomainServicesView) - hass.wsgi.register_view(APIEventForwardingView) - hass.wsgi.register_view(APIComponentsView) - hass.wsgi.register_view(APIErrorLogView) - hass.wsgi.register_view(APITemplateView) + hass.http.register_view(APIStatusView) + hass.http.register_view(APIEventStream) + hass.http.register_view(APIConfigView) + hass.http.register_view(APIDiscoveryView) + hass.http.register_view(APIStatesView) + hass.http.register_view(APIEntityStateView) + hass.http.register_view(APIEventListenersView) + hass.http.register_view(APIEventView) + hass.http.register_view(APIServicesView) + hass.http.register_view(APIDomainServicesView) + hass.http.register_view(APIEventForwardingView) + hass.http.register_view(APIComponentsView) + hass.http.register_view(APIErrorLogView) + hass.http.register_view(APITemplateView) return True @@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView): url = URL_API name = "api:status" + @ha.callback def get(self, request): """Retrieve if API is running.""" return self.json_message('API running.') @@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" + @asyncio.coroutine def get(self, request): """Provide a streaming interface for the event bus.""" stop_obj = object() - to_write = queue.Queue() + to_write = asyncio.Queue(loop=self.hass.loop) - restrict = request.args.get('restrict') + restrict = request.GET.get('restrict') if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] @@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - to_write.put(data) + yield from to_write.put(data) - def stream(): - """Stream events to response.""" - unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events) + response = web.StreamResponse() + response.content_type = 'text/event-stream' + yield from response.prepare(request) - try: - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events) - # Fire off one message so browsers fire open event right away - to_write.put(STREAM_PING_PAYLOAD) + try: + _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - while True: - try: - payload = to_write.get(timeout=STREAM_PING_INTERVAL) + # Fire off one message so browsers fire open event right away + yield from to_write.put(STREAM_PING_PAYLOAD) - if payload is stop_obj: - break + while True: + try: + with async_timeout.timeout(STREAM_PING_INTERVAL, + loop=self.hass.loop): + payload = yield from to_write.get() - msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - yield msg.encode("UTF-8") - except queue.Empty: - to_write.put(STREAM_PING_PAYLOAD) - except GeneratorExit: + if payload is stop_obj: break - finally: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) - unsub_stream() - return self.Response(stream(), mimetype='text/event-stream') + msg = "data: {}\n\n".format(payload) + _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), + msg.strip()) + response.write(msg.encode("UTF-8")) + yield from response.drain() + except asyncio.TimeoutError: + yield from to_write.put(STREAM_PING_PAYLOAD) + + finally: + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + unsub_stream() class APIConfigView(HomeAssistantView): @@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView): url = URL_API_CONFIG name = "api:config" + @ha.callback def get(self, request): """Get current configuration.""" return self.json(self.hass.config.as_dict()) @@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView): url = URL_API_DISCOVERY_INFO name = "api:discovery" + @ha.callback def get(self, request): """Get discovery info.""" needs_auth = self.hass.config.api.api_password is not None @@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView): url = URL_API_STATES name = "api:states" + @ha.callback def get(self, request): """Get current states.""" - return self.json(self.hass.states.all()) + return self.json(self.hass.states.async_all()) class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = "/api/states/" + url = "/api/states/{entity_id}" name = "api:entity-state" + @ha.callback def get(self, request, entity_id): """Retrieve state of entity.""" state = self.hass.states.get(entity_id) @@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView): else: return self.json_message('Entity not found', HTTP_NOT_FOUND) + @asyncio.coroutine def post(self, request, entity_id): """Update state of entity.""" try: - new_state = request.json['state'] - except KeyError: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON specified', + HTTP_BAD_REQUEST) + + new_state = data.get('state') + + if not new_state: return self.json_message('No state specified', HTTP_BAD_REQUEST) - attributes = request.json.get('attributes') - force_update = request.json.get('force_update', False) + attributes = data.get('attributes') + force_update = data.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # Write state - self.hass.states.set(entity_id, new_state, attributes, force_update) + self.hass.states.async_set(entity_id, new_state, attributes, + force_update) # Read the state back for our response - resp = self.json(self.hass.states.get(entity_id)) - - if is_new_state: - resp.status_code = HTTP_CREATED + status_code = HTTP_CREATED if is_new_state else 200 + resp = self.json(self.hass.states.get(entity_id), status_code) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) return resp + @ha.callback def delete(self, request, entity_id): """Remove entity.""" - if self.hass.states.remove(entity_id): + if self.hass.states.async_remove(entity_id): return self.json_message('Entity removed') else: return self.json_message('Entity not found', HTTP_NOT_FOUND) @@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView): url = URL_API_EVENTS name = "api:event-listeners" + @ha.callback def get(self, request): """Get event listeners.""" - return self.json(events_json(self.hass)) + return self.json(async_events_json(self.hass)) class APIEventView(HomeAssistantView): """View to handle Event requests.""" - url = '/api/events/' + url = '/api/events/{event_type}' name = "api:event" + @asyncio.coroutine def post(self, request, event_type): """Fire events.""" - event_data = request.json + body = yield from request.text() + event_data = json.loads(body) if body else None if event_data is not None and not isinstance(event_data, dict): return self.json_message('Event data should be a JSON object', @@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote) + self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" + @ha.callback def get(self, request): """Get registered services.""" - return self.json(services_json(self.hass)) + return self.json(async_services_json(self.hass)) class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = "/api/services//" + url = "/api/services/{domain}/{service}" name = "api:domain-services" + @asyncio.coroutine def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ - with TrackStates(self.hass) as changed_states: - self.hass.services.call(domain, service, request.json, True) + body = yield from request.text() + data = json.loads(body) if body else None + + with AsyncTrackStates(self.hass) as changed_states: + yield from self.hass.services.async_call(domain, service, data, + True) return self.json(changed_states) @@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView): name = "api:event-forward" event_forwarder = None + @asyncio.coroutine def post(self, request): """Setup an event forwarder.""" - data = request.json - if data is None: + try: + data = yield from request.json() + except ValueError: return self.json_message("No data received.", HTTP_BAD_REQUEST) + try: host = data['host'] api_password = data['api_password'] @@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView): api = rem.API(host, api_password, port) - if not api.validate_api(): + valid = yield from self.hass.loop.run_in_executor( + None, api.validate_api) + if not valid: return self.json_message("Unable to validate API.", HTTP_UNPROCESSABLE_ENTITY) if self.event_forwarder is None: self.event_forwarder = rem.EventForwarder(self.hass) - self.event_forwarder.connect(api) + self.event_forwarder.async_connect(api) return self.json_message("Event forwarding setup.") + @asyncio.coroutine def delete(self, request): - """Remove event forwarer.""" - data = request.json - if data is None: + """Remove event forwarder.""" + try: + data = yield from request.json() + except ValueError: return self.json_message("No data received.", HTTP_BAD_REQUEST) try: @@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView): if self.event_forwarder is not None: api = rem.API(host, None, port) - self.event_forwarder.disconnect(api) + self.event_forwarder.async_disconnect(api) return self.json_message("Event forwarding cancelled.") @@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView): url = URL_API_COMPONENTS name = "api:components" + @ha.callback def get(self, request): """Get current loaded components.""" return self.json(self.hass.config.components) @@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView): url = URL_API_ERROR_LOG name = "api:error-log" + @asyncio.coroutine def get(self, request): """Serve error log.""" - return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME)) + resp = yield from self.file( + request, self.hass.config.path(ERROR_LOG_FILENAME)) + return resp class APITemplateView(HomeAssistantView): @@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" + @asyncio.coroutine def post(self, request): """Render a template.""" try: - tpl = template.Template(request.json['template'], self.hass) - return tpl.render(request.json.get('variables')) - except TemplateError as ex: + data = yield from request.json() + tpl = template.Template(data['template'], self.hass) + return tpl.async_render(data.get('variables')) + except (ValueError, TemplateError) as ex: return self.json_message('Error rendering template: {}'.format(ex), HTTP_BAD_REQUEST) -def services_json(hass): +def async_services_json(hass): """Generate services data to JSONify.""" return [{"domain": key, "services": value} - for key, value in hass.services.services.items()] + for key, value in hass.services.async_services().items()] -def events_json(hass): +def async_events_json(hass): """Generate event data to JSONify.""" return [{"event": key, "listener_count": value} - for key, value in hass.bus.listeners.items()] + for key, value in hass.bus.async_listeners().items()] diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index ce89ae2e4db..72140936e18 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from haffmpeg import SensorNoise, SensorMotion # check source - if not run_test(config.get(CONF_INPUT)): + if not run_test(hass, config.get(CONF_INPUT)): return # generate sensor object diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2f23118a1c3..ce811780856 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -5,8 +5,10 @@ Component to interface with cameras. For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ +import asyncio import logging -import time + +from aiohttp import web from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -31,8 +33,8 @@ def setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.wsgi.register_view(CameraImageView(hass, component.entities)) - hass.wsgi.register_view(CameraMjpegStream(hass, component.entities)) + hass.http.register_view(CameraImageView(hass, component.entities)) + hass.http.register_view(CameraMjpegStream(hass, component.entities)) component.setup(config) @@ -80,33 +82,59 @@ class Camera(Entity): """Return bytes of camera image.""" raise NotImplementedError() - def mjpeg_stream(self, response): - """Generate an HTTP MJPEG stream from camera images.""" - def stream(): - """Stream images as mjpeg stream.""" - try: - last_image = None - while True: - img_bytes = self.camera_image() + @asyncio.coroutine + def async_camera_image(self): + """Return bytes of camera image. - if img_bytes is not None and img_bytes != last_image: - yield bytes( - '--jpegboundary\r\n' - 'Content-Type: image/jpeg\r\n' - 'Content-Length: {}\r\n\r\n'.format( - len(img_bytes)), 'utf-8') + img_bytes + b'\r\n' + This method must be run in the event loop. + """ + image = yield from self.hass.loop.run_in_executor( + None, self.camera_image) + return image - last_image = img_bytes + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from camera images. - time.sleep(0.5) - except GeneratorExit: - pass + This method must be run in the event loop. + """ + response = web.StreamResponse() - return response( - stream(), - content_type=('multipart/x-mixed-replace; ' - 'boundary=--jpegboundary') - ) + response.content_type = ('multipart/x-mixed-replace; ' + 'boundary=--jpegboundary') + response.enable_chunked_encoding() + yield from response.prepare(request) + + def write(img_bytes): + """Write image to stream.""" + response.write(bytes( + '--jpegboundary\r\n' + 'Content-Type: image/jpeg\r\n' + 'Content-Length: {}\r\n\r\n'.format( + len(img_bytes)), 'utf-8') + img_bytes + b'\r\n') + + last_image = None + + try: + while True: + img_bytes = yield from self.async_camera_image() + if not img_bytes: + break + + if img_bytes is not None and img_bytes != last_image: + write(img_bytes) + + # Chrome seems to always ignore first picture, + # print it twice. + if last_image is None: + write(img_bytes) + + last_image = img_bytes + yield from response.drain() + + yield from asyncio.sleep(.5) + finally: + self.hass.loop.create_task(response.write_eof()) @property def state(self): @@ -144,22 +172,25 @@ class CameraView(HomeAssistantView): super().__init__(hass) self.entities = entities + @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" camera = self.entities.get(entity_id) if camera is None: - return self.Response(status=404) + return web.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) + return web.Response(status=401) - return self.handle(camera) + response = yield from self.handle(request, camera) + return response - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Hanlde the camera request.""" raise NotImplementedError() @@ -167,25 +198,27 @@ class CameraView(HomeAssistantView): class CameraImageView(CameraView): """Camera view to serve an image.""" - url = "/api/camera_proxy/" + url = "/api/camera_proxy/{entity_id}" name = "api:camera:image" - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Serve camera image.""" - response = camera.camera_image() + image = yield from camera.async_camera_image() - if response is None: - return self.Response(status=500) + if image is None: + return web.Response(status=500) - return self.Response(response) + return web.Response(body=image) class CameraMjpegStream(CameraView): """Camera View to serve an MJPEG stream.""" - url = "/api/camera_proxy_stream/" + url = "/api/camera_proxy_stream/{entity_id}" name = "api:camera:stream" - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Serve camera image.""" - return camera.mjpeg_stream(self.Response) + yield from camera.handle_async_mjpeg_stream(request) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 1115bc2d2c1..85567eca18e 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.ffmpeg/ """ +import asyncio import logging import voluptuous as vol +from aiohttp import web from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.ffmpeg import ( - run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) + async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME +from homeassistant.util.async import run_coroutine_threadsafe DEPENDENCIES = ['ffmpeg'] @@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" - if not run_test(config.get(CONF_INPUT)): + if not async_run_test(hass, config.get(CONF_INPUT)): return - add_devices([FFmpegCamera(config)]) + hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)])) class FFmpegCamera(Camera): """An implementation of an FFmpeg camera.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize a FFmpeg camera.""" super().__init__() self._name = config.get(CONF_NAME) @@ -45,24 +49,45 @@ class FFmpegCamera(Camera): self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg import ImageSingle, IMAGE_JPEG - ffmpeg = ImageSingle(get_binary()) + from haffmpeg import ImageSingleAsync, IMAGE_JPEG + ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop) - return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments) + image = yield from ffmpeg.get_image( + self._input, output_format=IMAGE_JPEG, + extra_cmd=self._extra_arguments) + return image - def mjpeg_stream(self, response): + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg import CameraMjpeg + from haffmpeg import CameraMjpegAsync - stream = CameraMjpeg(get_binary()) - stream.open_camera(self._input, extra_cmd=self._extra_arguments) - return response( - stream, - mimetype='multipart/x-mixed-replace;boundary=ffserver', - direct_passthrough=True - ) + stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop) + yield from stream.open_camera( + self._input, extra_cmd=self._extra_arguments) + + response = web.StreamResponse() + response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.close()) + self.hass.loop.create_task(response.write_eof()) @property def name(self): diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 5d7488b8e68..e6dc8968030 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -4,10 +4,13 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.generic/ """ +import asyncio import logging +import aiohttp +import async_timeout import requests -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from requests.auth import HTTPDigestAuth import voluptuous as vol from homeassistant.const import ( @@ -16,6 +19,7 @@ from homeassistant.const import ( from homeassistant.exceptions import TemplateError from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers import config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -35,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +@asyncio.coroutine # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a generic IP Camera.""" - add_devices([GenericCamera(hass, config)]) + hass.loop.create_task(async_add_devices([GenericCamera(hass, config)])) # pylint: disable=too-many-instance-attributes @@ -49,6 +54,7 @@ class GenericCamera(Camera): """Initialize a generic camera.""" super().__init__() self.hass = hass + self._authentication = device_info.get(CONF_AUTHENTICATION) self._name = device_info.get(CONF_NAME) self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url.hass = hass @@ -58,20 +64,27 @@ class GenericCamera(Camera): password = device_info.get(CONF_PASSWORD) if username and password: - if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION: + if self._authentication == HTTP_DIGEST_AUTHENTICATION: self._auth = HTTPDigestAuth(username, password) else: - self._auth = HTTPBasicAuth(username, password) + self._auth = aiohttp.BasicAuth(username, password=password) else: self._auth = None self._last_url = None self._last_image = None + self._session = aiohttp.ClientSession(loop=hass.loop, auth=self._auth) def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" try: - url = self._still_image_url.render() + url = self._still_image_url.async_render() except TemplateError as err: _LOGGER.error('Error parsing template %s: %s', self._still_image_url, err) @@ -80,16 +93,32 @@ class GenericCamera(Camera): if url == self._last_url and self._limit_refetch: return self._last_image - kwargs = {'timeout': 10, 'auth': self._auth} + # aiohttp don't support DigestAuth jet + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + def fetch(): + """Read image from a URL.""" + try: + kwargs = {'timeout': 10, 'auth': self._auth} + response = requests.get(url, **kwargs) + return response.content + except requests.exceptions.RequestException as error: + _LOGGER.error('Error getting camera image: %s', error) + return self._last_image - try: - response = requests.get(url, **kwargs) - except requests.exceptions.RequestException as error: - _LOGGER.error('Error getting camera image: %s', error) - return None + self._last_image = yield from self.hass.loop.run_in_executor( + None, fetch) + # async + else: + try: + with async_timeout.timeout(10, loop=self.hass.loop): + respone = yield from self._session.get(url) + self._last_image = yield from respone.read() + self.hass.loop.create_task(respone.release()) + except asyncio.TimeoutError: + _LOGGER.error('Timeout getting camera image') + return self._last_image self._last_url = url - self._last_image = response.content return self._last_image @property diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 04f099d8b1e..e1c39a62572 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -4,9 +4,14 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.mjpeg/ """ +import asyncio import logging from contextlib import closing +import aiohttp +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol @@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +@asyncio.coroutine # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" - add_devices([MjpegCamera(config)]) + hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)])) def extract_image_from_mjpeg(stream): @@ -56,7 +62,7 @@ def extract_image_from_mjpeg(stream): class MjpegCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" - def __init__(self, device_info): + def __init__(self, hass, device_info): """Initialize a MJPEG camera.""" super().__init__() self._name = device_info.get(CONF_NAME) @@ -65,32 +71,57 @@ class MjpegCamera(Camera): self._password = device_info.get(CONF_PASSWORD) self._mjpeg_url = device_info[CONF_MJPEG_URL] - def camera_stream(self): - """Return a MJPEG stream image response directly from the camera.""" + auth = None + if self._authentication == HTTP_BASIC_AUTHENTICATION: + auth = aiohttp.BasicAuth(self._username, password=self._password) + + self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth) + + def camera_image(self): + """Return a still image response from the camera.""" if self._username and self._password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: auth = HTTPDigestAuth(self._username, self._password) else: auth = HTTPBasicAuth(self._username, self._password) - return requests.get(self._mjpeg_url, - auth=auth, - stream=True, timeout=10) + req = requests.get( + self._mjpeg_url, auth=auth, stream=True, timeout=10) else: - return requests.get(self._mjpeg_url, stream=True, timeout=10) + req = requests.get(self._mjpeg_url, stream=True, timeout=10) - def camera_image(self): - """Return a still image response from the camera.""" - with closing(self.camera_stream()) as response: - return extract_image_from_mjpeg(response.iter_content(1024)) + with closing(req) as response: + return extract_image_from_mjpeg(response.iter_content(102400)) - def mjpeg_stream(self, response): + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - stream = self.camera_stream() - return response( - stream.iter_content(chunk_size=1024), - mimetype=stream.headers[CONTENT_TYPE_HEADER], - direct_passthrough=True - ) + # aiohttp don't support DigestAuth -> Fallback + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + yield from super().handle_async_mjpeg_stream(request) + return + + # connect to stream + try: + with async_timeout.timeout(10, loop=self.hass.loop): + stream = yield from self._session.get(self._mjpeg_url) + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.content.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.release()) + self.hass.loop.create_task(response.write_eof()) @property def name(self): diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f3f2c3c94f5..adbd1dd13d4 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,6 +4,8 @@ Support for the Locative platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ +import asyncio +from functools import partial import logging from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME @@ -19,7 +21,7 @@ DEPENDENCIES = ['http'] def setup_scanner(hass, config, see): """Setup an endpoint for the Locative application.""" - hass.wsgi.register_view(LocativeView(hass, see)) + hass.http.register_view(LocativeView(hass, see)) return True @@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView): super().__init__(hass) self.see = see + @asyncio.coroutine def get(self, request): """Locative message received as GET.""" - return self.post(request) + res = yield from self._handle(request.GET) + return res + @asyncio.coroutine def post(self, request): """Locative message received.""" - # pylint: disable=too-many-return-statements - data = request.values + data = yield from request.post() + res = yield from self._handle(data) + return res + @asyncio.coroutine + def _handle(self, data): + """Handle locative request.""" + # pylint: disable=too-many-return-statements if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) @@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView): direction = data['trigger'] if direction == 'enter': - self.see(dev_id=device, location_name=location_name) + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + location_name=location_name)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView): '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: - self.see(dev_id=device, location_name=STATE_NOT_HOME) + yield from self.hass.loop.run_in_executor( + None, partial(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 diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index a63117fc31b..62680d84d36 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -4,20 +4,21 @@ Support for local control of entities by emulating the Phillips Hue bridge. 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 -import json 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 @@ -25,8 +26,6 @@ from homeassistant.components.light import ( from homeassistant.components.http import ( HomeAssistantView, HomeAssistantWSGI ) -# pylint: disable=unused-import -from homeassistant.components.http import REQUIREMENTS # noqa import homeassistant.helpers.config_validation as cv DOMAIN = 'emulated_hue' @@ -87,19 +86,21 @@ def setup(hass, yaml_config): upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port) - def start_emulated_hue_bridge(event): - """Start the emulated hue bridge.""" - server.start() - 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()) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + @core.callback + def start_emulated_hue_bridge(event): + """Start the emulated hue bridge.""" + hass.loop.create_task(server.start()) + upnp_listener.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + stop_emulated_hue_bridge) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) return True @@ -158,6 +159,7 @@ class DescriptionXmlView(HomeAssistantView): super().__init__(hass) self.config = config + @core.callback def get(self, request): """Handle a GET request.""" xml_template = """ @@ -185,7 +187,7 @@ class DescriptionXmlView(HomeAssistantView): 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): @@ -200,9 +202,13 @@ class HueUsernameView(HomeAssistantView): """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 ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) if 'devicetype' not in data: return self.json_message('devicetype not specified', @@ -214,10 +220,10 @@ class HueUsernameView(HomeAssistantView): 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): @@ -226,58 +232,51 @@ class HueLightsView(HomeAssistantView): 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) + if not request.path.endswith('state'): + return web.Response(text="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 + if entity_id and self.hass.states.get(entity_id) is None: + return self.json_message('Entity not found', HTTP_NOT_FOUND) - for key in request.form: - try: - json_data = json.loads(key) - break - except ValueError: - # Try the next key? - pass + try: + json_data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - if json_data is None: - return self.Response("Bad request", status=400) - else: - json_data = request.json + result = yield from self.async_put_light_state(json_data, entity_id) + return result - return self.put_light_state(json_data, entity_id) - - def get_lights_list(self): + @core.callback + 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): + @core.callback + 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) @@ -292,23 +291,24 @@ class HueLightsView(HomeAssistantView): 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 @@ -333,7 +333,8 @@ class HueLightsView(HomeAssistantView): 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)] @@ -345,7 +346,10 @@ class HueLightsView(HomeAssistantView): return self.json(json_response) def is_entity_exposed(self, entity): - """Determine if an entity should be exposed on the emulated bridge.""" + """Determine if an entity should be exposed on the emulated bridge. + + Async friendly. + """ config = self.config if entity.attributes.get('view') is not None: diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 0ba015a4660..dea9e2f1bcf 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -4,14 +4,16 @@ Component that will help set the ffmpeg component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ffmpeg/ """ +import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==0.13"] +REQUIREMENTS = ["ha-ffmpeg==0.14"] _LOGGER = logging.getLogger(__name__) @@ -47,13 +49,26 @@ def setup(hass, config): def get_binary(): - """Return ffmpeg binary from config.""" + """Return ffmpeg binary from config. + + Async friendly. + """ return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) -def run_test(input_source): +def run_test(hass, input_source): """Run test on this input. TRUE is deactivate or run correct.""" - from haffmpeg import Test + return run_coroutine_threadsafe( + async_run_test(hass, input_source), hass.loop).result() + + +@asyncio.coroutine +def async_run_test(hass, input_source): + """Run test on this input. TRUE is deactivate or run correct. + + This method must be run in the event loop. + """ + from haffmpeg import TestAsync if FFMPEG_CONFIG.get(CONF_RUN_TEST): # if in cache @@ -61,8 +76,9 @@ def run_test(input_source): return FFMPEG_TEST_CACHE[input_source] # run test - test = Test(get_binary()) - if not test.run_test(input_source): + ffmpeg_test = TestAsync(get_binary(), loop=hass.loop) + success = yield from ffmpeg_test.run_test(input_source) + if not success: _LOGGER.error("FFmpeg '%s' test fails!", input_source) FFMPEG_TEST_CACHE[input_source] = False return False diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index b08ba89ca77..bb4c66ad1f9 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -4,14 +4,14 @@ Allows utilizing the Foursquare (Swarm) API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/foursquare/ """ +import asyncio import logging import os -import json import requests import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -75,7 +75,7 @@ def setup(hass, config): descriptions[DOMAIN][SERVICE_CHECKIN], schema=CHECKIN_SERVICE_SCHEMA) - hass.wsgi.register_view(FoursquarePushReceiver( + hass.http.register_view(FoursquarePushReceiver( hass, config[CONF_PUSH_SECRET])) return True @@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView): super().__init__(hass) self.push_secret = push_secret + @asyncio.coroutine def post(self, request): """Accept the POST from Foursquare.""" - raw_data = request.form - _LOGGER.debug("Received Foursquare push: %s", raw_data) - if self.push_secret != raw_data["secret"]: + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + secret = data.pop('secret', None) + + _LOGGER.debug("Received Foursquare push: %s", data) + + if self.push_secret != secret: _LOGGER.error("Received Foursquare push with invalid" - "push secret! Data: %s", raw_data) - return - parsed_payload = { - key: json.loads(val) for key, val in raw_data.items() - if key != "secret" - } - self.hass.bus.fire(EVENT_PUSH, parsed_payload) + "push secret: %s", secret) + return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) + + self.hass.bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2d9abe8fe33..494e3aee401 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,8 +1,13 @@ """Handle the frontend for Home Assistant.""" +import asyncio import hashlib +import json import logging import os +from aiohttp import web + +from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components import api from homeassistant.components.http import HomeAssistantView @@ -39,7 +44,7 @@ def register_built_in_panel(hass, component_name, sidebar_title=None, # pylint: disable=too-many-arguments path = 'panels/ha-panel-{}.html'.format(component_name) - if hass.wsgi.development: + if hass.http.development: url = ('/static/home-assistant-polymer/panels/' '{0}/ha-panel-{0}.html'.format(component_name)) else: @@ -98,7 +103,7 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, url = URL_PANEL_COMPONENT.format(component_name) if url not in _REGISTERED_COMPONENTS: - hass.wsgi.register_static_path(url, path) + hass.http.register_static_path(url, path) _REGISTERED_COMPONENTS.add(url) fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) @@ -114,20 +119,23 @@ def add_manifest_json_key(key, val): def setup(hass, config): """Setup serving the frontend.""" - hass.wsgi.register_view(BootstrapView) - hass.wsgi.register_view(ManifestJSONView) + hass.http.register_view(BootstrapView) + hass.http.register_view(ManifestJSONView) - if hass.wsgi.development: + if hass.http.development: sw_path = "home-assistant-polymer/build/service_worker.js" else: sw_path = "service_worker.js" - hass.wsgi.register_static_path("/service_worker.js", + hass.http.register_static_path("/service_worker.js", os.path.join(STATIC_PATH, sw_path), 0) - hass.wsgi.register_static_path("/robots.txt", + hass.http.register_static_path("/robots.txt", os.path.join(STATIC_PATH, "robots.txt")) - hass.wsgi.register_static_path("/static", STATIC_PATH) - hass.wsgi.register_static_path("/local", hass.config.path('www')) + hass.http.register_static_path("/static", STATIC_PATH) + + local = hass.config.path('www') + if os.path.isdir(local): + hass.http.register_static_path("/local", local) register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') @@ -140,7 +148,7 @@ def setup(hass, config): Done when Home Assistant is started so that all panels are known. """ - hass.wsgi.register_view(IndexView( + hass.http.register_view(IndexView( hass, ['/{}'.format(name) for name in PANELS])) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) @@ -161,13 +169,14 @@ class BootstrapView(HomeAssistantView): url = "/api/bootstrap" name = "api:bootstrap" + @callback def get(self, request): """Return all data needed to bootstrap Home Assistant.""" return self.json({ 'config': self.hass.config.as_dict(), - 'states': self.hass.states.all(), - 'events': api.events_json(self.hass), - 'services': api.services_json(self.hass), + 'states': self.hass.states.async_all(), + 'events': api.async_events_json(self.hass), + 'services': api.async_services_json(self.hass), 'panels': PANELS, }) @@ -193,9 +202,10 @@ class IndexView(HomeAssistantView): ) ) + @asyncio.coroutine def get(self, request, entity_id=None): """Serve the index view.""" - if self.hass.wsgi.development: + if self.hass.http.development: core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' else: @@ -215,22 +225,24 @@ class IndexView(HomeAssistantView): if self.hass.config.api.api_password: # require password if set no_auth = 'false' - if self.hass.wsgi.is_trusted_ip( - self.hass.wsgi.get_real_ip(request)): + if self.hass.http.is_trusted_ip( + self.hass.http.get_real_ip(request)): # bypass for trusted networks no_auth = 'true' icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) - template = self.templates.get_template('index.html') + template = yield from self.hass.loop.run_in_executor( + None, self.templates.get_template, 'index.html') # pylint is wrong # pylint: disable=no-member + # This is a jinja2 template, not a HA template so we call 'render'. resp = template.render( core_url=core_url, ui_url=ui_url, no_auth=no_auth, icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], panel_url=panel_url, panels=PANELS) - return self.Response(resp, mimetype='text/html') + return web.Response(text=resp, content_type='text/html') class ManifestJSONView(HomeAssistantView): @@ -240,8 +252,8 @@ class ManifestJSONView(HomeAssistantView): url = "/manifest.json" name = "manifestjson" - def get(self, request): + @asyncio.coroutine + def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" - import json msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') - return self.Response(msg, mimetype="application/manifest+json") + return web.Response(body=msg, content_type="application/manifest+json") diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 199a6b47b99..c8230386aa0 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,11 +4,13 @@ Provide pre-made queries on top of the recorder component. 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 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 @@ -182,8 +184,8 @@ def setup(hass, config): filters.included_entities = include[CONF_ENTITIES] filters.included_domains = include[CONF_DOMAINS] - hass.wsgi.register_view(Last5StatesView(hass)) - hass.wsgi.register_view(HistoryPeriodView(hass, filters)) + hass.http.register_view(Last5StatesView(hass)) + hass.http.register_view(HistoryPeriodView(hass, filters)) register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') return True @@ -192,16 +194,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( + None, last_5_states, entity_id) + return self.json(result) class HistoryPeriodView(HomeAssistantView): @@ -209,15 +214,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 self.json_message('Invalid datetime', HTTP_BAD_REQUEST) + one_day = timedelta(days=1) if datetime: @@ -226,10 +238,13 @@ class HistoryPeriodView(HomeAssistantView): 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') - return self.json(get_significant_states( - start_time, end_time, entity_id, self.filters).values()) + result = yield from self.hass.loop.run_in_executor( + None, get_significant_states, start_time, end_time, entity_id, + self.filters) + + return self.json(result.values()) # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 97009b69d1c..25515c61046 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -4,31 +4,36 @@ This module provides WSGI application to serve the Home Assistant API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/http/ """ +import asyncio import hmac import json import logging import mimetypes -import threading +import os +from pathlib import Path import re import ssl from ipaddress import ip_address, ip_network import voluptuous as vol +from aiohttp import web, hdrs +from aiohttp.file_sender import FileSender +from aiohttp.web_exceptions import ( + HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified) +from aiohttp.web_urldispatcher import StaticRoute +from homeassistant.core import callback, is_callback import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( - SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, - HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE_JSON, - HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -from homeassistant.core import split_entity_id -import homeassistant.util.dt as dt_util + SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, + CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) import homeassistant.helpers.config_validation as cv from homeassistant.components import persistent_notification DOMAIN = 'http' -REQUIREMENTS = ('cherrypy==8.1.2', 'static3==0.7.0', 'Werkzeug==0.11.11') +REQUIREMENTS = ('aiohttp_cors==0.4.0',) CONF_API_PASSWORD = 'api_password' CONF_SERVER_HOST = 'server_host' @@ -83,6 +88,12 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +# TEMP TO GET TESTS TO RUN +def request_class(): + """.""" + raise Exception('not implemented') + + class HideSensitiveFilter(logging.Filter): """Filter API password calls.""" @@ -94,17 +105,17 @@ class HideSensitiveFilter(logging.Filter): def filter(self, record): """Hide sensitive data in messages.""" - if self.hass.wsgi.api_password is None: + if self.hass.http.api_password is None: return True - record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******') + record.msg = record.msg.replace(self.hass.http.api_password, '*******') return True def setup(hass, config): """Set up the HTTP API and debug interface.""" - _LOGGER.addFilter(HideSensitiveFilter(hass)) + logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass)) conf = config.get(DOMAIN, {}) @@ -131,19 +142,20 @@ def setup(hass, config): trusted_networks=trusted_networks ) - def start_wsgi_server(event): - """Start the WSGI server.""" - server.start() + @callback + def stop_server(event): + """Callback to stop the server.""" + hass.loop.create_task(server.stop()) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server) + @callback + def start_server(event): + """Callback to start the server.""" + hass.loop.create_task(server.start()) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) - def stop_wsgi_server(event): - """Stop the WSGI server.""" - server.stop() + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server) - - hass.wsgi = server + hass.http = server hass.config.api = rem.API(server_host if server_host != '0.0.0.0' else util.get_local_ip(), api_password, server_port, @@ -152,105 +164,84 @@ def setup(hass, config): return True -def request_class(): - """Generate request class. +class GzipFileSender(FileSender): + """FileSender class capable of sending gzip version if available.""" - Done in method because of imports. - """ - from werkzeug.exceptions import BadRequest - from werkzeug.wrappers import BaseRequest, AcceptMixin - from werkzeug.utils import cached_property + # pylint: disable=invalid-name, too-few-public-methods - class Request(BaseRequest, AcceptMixin): - """Base class for incoming requests.""" + development = False - @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') + @asyncio.coroutine + def send(self, request, filepath): + """Send filepath to client using request.""" + gzip = False + if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]: + gzip_path = filepath.with_name(filepath.name + '.gz') - return Request + if gzip_path.is_file(): + filepath = gzip_path + gzip = True + + st = filepath.stat() + + modsince = request.if_modified_since + if modsince is not None and st.st_mtime <= modsince.timestamp(): + raise HTTPNotModified() + + ct, encoding = mimetypes.guess_type(str(filepath)) + if not ct: + ct = 'application/octet-stream' + + resp = self._response_factory() + resp.content_type = ct + if encoding: + resp.headers[hdrs.CONTENT_ENCODING] = encoding + if gzip: + resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING + resp.last_modified = st.st_mtime + + # CACHE HACK + if not self.development: + cache_time = 31 * 86400 # = 1 month + resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( + cache_time) + + file_size = st.st_size + + resp.content_length = file_size + resp.set_tcp_cork(True) + try: + with filepath.open('rb') as f: + yield from self._sendfile(request, resp, f, file_size) + + finally: + resp.set_tcp_nodelay(True) + + return resp + +_GZIP_FILE_SENDER = GzipFileSender() -def routing_map(hass): - """Generate empty routing map with HA validators.""" - from werkzeug.routing import Map, BaseConverter, ValidationError +class HAStaticRoute(StaticRoute): + """StaticRoute with support for fingerprinting.""" - class EntityValidator(BaseConverter): - """Validate entity_id in urls.""" + def __init__(self, prefix, path): + """Initialize a static route with gzip and cache busting support.""" + super().__init__(None, prefix, path) + self._file_sender = _GZIP_FILE_SENDER - regex = r"(\w+)\.(\w+)" + def match(self, path): + """Match path to filename.""" + if not path.startswith(self._prefix): + return None - def __init__(self, url_map, exist=True, domain=None): - """Initilalize entity validator.""" - super().__init__(url_map) - self._exist = exist - self._domain = domain + # Extra sauce to remove fingerprinted resource names + filename = path[self._prefix_len:] + fingerprinted = _FINGERPRINT.match(filename) + if fingerprinted: + filename = '{}.{}'.format(*fingerprinted.groups()) - 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, - }) + return {'filename': filename} class HomeAssistantWSGI(object): @@ -262,28 +253,35 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, trusted_networks): - """Initilalize the WSGI Home Assistant server.""" - from werkzeug.wrappers import Response + """Initialize the WSGI Home Assistant server.""" + import aiohttp_cors - Response.mimetype = 'text/html' - - # pylint: disable=invalid-name - self.Request = request_class() - self.url_map = routing_map(hass) - self.views = {} + self.app = web.Application(loop=hass.loop) self.hass = hass - self.extra_apps = {} self.development = development self.api_password = api_password self.ssl_certificate = ssl_certificate self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port - self.cors_origins = cors_origins self.trusted_networks = trusted_networks self.event_forwarder = None + self._handler = None self.server = None + if cors_origins: + self.cors = aiohttp_cors.setup(self.app, defaults={ + host: aiohttp_cors.ResourceOptions( + allow_headers=ALLOWED_CORS_HEADERS, + allow_methods='*', + ) for host in cors_origins + }) + else: + self.cors = None + + # CACHE HACK + _GZIP_FILE_SENDER.development = development + def register_view(self, view): """Register a view with the WSGI server. @@ -291,21 +289,11 @@ class HomeAssistantWSGI(object): It is optional to instantiate it before registering; this method will handle it either way. """ - from werkzeug.routing import Rule - - if view.name in self.views: - _LOGGER.warning("View '%s' is being overwritten", view.name) if isinstance(view, type): # Instantiate the view, if needed 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) + view.register(self.app.router) def register_redirect(self, url, redirect_to): """Register a redirect with the server. @@ -316,149 +304,92 @@ class HomeAssistantWSGI(object): for the redirect, otherwise it has to be a string with placeholders in rule syntax. """ - from werkzeug.routing import Rule + def redirect(request): + """Redirect to location.""" + raise HTTPMovedPermanently(redirect_to) - self.url_map.add(Rule(url, redirect_to=redirect_to)) + self.app.router.add_route('GET', url, redirect) def register_static_path(self, url_root, path, cache_length=31): """Register a folder to serve as a static path. Specify optional cache length of asset in days. """ - from static import Cling + if os.path.isdir(path): + assert url_root.startswith('/') + if not url_root.endswith('/'): + url_root += '/' + route = HAStaticRoute(url_root, path) + self.app.router.register_route(route) + return - headers = [] + filepath = Path(path) - if cache_length and not self.development: - # 1 year in seconds - cache_time = cache_length * 86400 + @asyncio.coroutine + def serve_file(request): + """Redirect to location.""" + return _GZIP_FILE_SENDER.send(request, filepath) - headers.append({ - 'prefix': '', - HTTP_HEADER_CACHE_CONTROL: - "public, max-age={}".format(cache_time) - }) + # aiohttp supports regex matching for variables. Using that as temp + # to work around cache busting MD5. + # Turns something like /static/dev-panel.html into + # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} + base, ext = url_root.rsplit('.', 1) + base, file = base.rsplit('/', 1) + regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext) + url_pattern = "{}/{{filename:{}}}".format(base, regex) - self.register_wsgi_app(url_root, Cling(path, headers=headers)) - - def register_wsgi_app(self, url_root, app): - """Register a path to serve a WSGI app.""" - if url_root in self.extra_apps: - _LOGGER.warning("Url root '%s' is being overwritten", url_root) - - self.extra_apps[url_root] = app + self.app.router.add_route('GET', url_pattern, serve_file) + @asyncio.coroutine def start(self): """Start the wsgi server.""" - from cherrypy import wsgiserver - from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter - - # pylint: disable=too-few-public-methods,super-init-not-called - class ContextSSLAdapter(BuiltinSSLAdapter): - """SSL Adapter that takes in an SSL context.""" - - def __init__(self, context): - self.context = context - - # pylint: disable=no-member - self.server = wsgiserver.CherryPyWSGIServer( - (self.server_host, self.server_port), self, - server_name='Home Assistant') + if self.cors is not None: + for route in list(self.app.router.routes()): + self.cors.add(route) if self.ssl_certificate: context = ssl.SSLContext(SSL_VERSION) context.options |= SSL_OPTS context.set_ciphers(CIPHERS) context.load_cert_chain(self.ssl_certificate, self.ssl_key) - self.server.ssl_adapter = ContextSSLAdapter(context) + else: + context = None - threading.Thread( - target=self.server.start, daemon=True, name='WSGI-server').start() + self._handler = self.app.make_handler() + self.server = yield from self.hass.loop.create_server( + self._handler, self.server_host, self.server_port, ssl=context) + @asyncio.coroutine def stop(self): """Stop the wsgi server.""" - self.server.stop() - - def dispatch_request(self, request): - """Handle incoming request.""" - from werkzeug.exceptions import ( - MethodNotAllowed, NotFound, BadRequest, Unauthorized, - ) - from werkzeug.routing import RequestRedirect - - with request: - adapter = self.url_map.bind_to_environ(request.environ) - try: - endpoint, values = adapter.match() - return self.views[endpoint].handle_request(request, **values) - except RequestRedirect as ex: - return ex - except (BadRequest, NotFound, MethodNotAllowed, - Unauthorized) as ex: - resp = ex.get_response(request.environ) - if request.accept_mimetypes.accept_json: - resp.data = json.dumps({ - 'result': 'error', - 'message': str(ex), - }) - resp.mimetype = CONTENT_TYPE_JSON - return resp - - def base_app(self, environ, start_response): - """WSGI Handler of requests to base app.""" - request = self.Request(environ) - response = self.dispatch_request(request) - - if self.cors_origins: - cors_check = (environ.get('HTTP_ORIGIN') in self.cors_origins) - cors_headers = ", ".join(ALLOWED_CORS_HEADERS) - if cors_check: - response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \ - environ.get('HTTP_ORIGIN') - response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \ - cors_headers - - return response(environ, start_response) - - def __call__(self, environ, start_response): - """Handle a request for base app + extra apps.""" - from werkzeug.wsgi import DispatcherMiddleware - - if not self.hass.is_running: - from werkzeug.exceptions import BadRequest - return BadRequest()(environ, start_response) - - 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) + self.server.close() + yield from self.server.wait_closed() + yield from self.app.shutdown() + yield from self._handler.finish_connections(60.0) + yield from self.app.cleanup() @staticmethod def get_real_ip(request): """Return the clients correct ip address, even in proxied setups.""" - if request.access_route: - return request.access_route[-1] - else: - return request.remote_addr + peername = request.transport.get_extra_info('peername') + return peername[0] if peername is not None else None def is_trusted_ip(self, remote_addr): """Match an ip address against trusted CIDR networks.""" return any(ip_address(remote_addr) in trusted_network - for trusted_network in self.hass.wsgi.trusted_networks) + for trusted_network in self.hass.http.trusted_networks) class HomeAssistantView(object): """Base view for all views.""" + url = None extra_urls = [] requires_auth = True # Views inheriting from this class can override this def __init__(self, hass): """Initilalize the base view.""" - from werkzeug.wrappers import Response - if not hasattr(self, 'url'): class_name = self.__class__.__name__ raise AttributeError( @@ -472,59 +403,99 @@ class HomeAssistantView(object): ) self.hass = hass - # pylint: disable=invalid-name - self.Response = Response - def handle_request(self, request, **values): - """Handle request to url.""" - from werkzeug.exceptions import MethodNotAllowed, Unauthorized + def json(self, result, status_code=200): # pylint: disable=no-self-use + """Return a JSON response.""" + msg = json.dumps( + result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') + return web.Response( + body=msg, content_type=CONTENT_TYPE_JSON, status=status_code) - if request.method == "OPTIONS": - # For CORS preflight requests. - return self.options(request) + def json_message(self, error, status_code=200): + """Return a JSON message response.""" + return self.json({'message': error}, status_code) - try: - handler = getattr(self, request.method.lower()) - except AttributeError: - raise MethodNotAllowed + @asyncio.coroutine + def file(self, request, fil): # pylint: disable=no-self-use + """Return a file.""" + assert isinstance(fil, str), 'only string paths allowed' + response = yield from _GZIP_FILE_SENDER.send(request, Path(fil)) + return response + def register(self, router): + """Register the view with a router.""" + assert self.url is not None, 'No url set for view' + urls = [self.url] + self.extra_urls + + for method in ('get', 'post', 'delete', 'put'): + handler = getattr(self, method, None) + + if not handler: + continue + + handler = request_handler_factory(self, handler) + + for url in urls: + router.add_route(method, url, handler) + + # aiohttp_cors does not work with class based views + # self.app.router.add_route('*', self.url, self, name=self.name) + + # for url in self.extra_urls: + # self.app.router.add_route('*', url, self) + + +def request_handler_factory(view, handler): + """Factory to wrap our handler classes. + + Eventually authentication should be managed by middleware. + """ + @asyncio.coroutine + def handle(request): + """Handle incoming request.""" remote_addr = HomeAssistantWSGI.get_real_ip(request) # Auth code verbose on purpose authenticated = False - if self.hass.wsgi.api_password is None: + if view.hass.http.api_password is None: authenticated = True - elif self.hass.wsgi.is_trusted_ip(remote_addr): + elif view.hass.http.is_trusted_ip(remote_addr): authenticated = True elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''), - self.hass.wsgi.api_password): + view.hass.http.api_password): # A valid auth header has been set authenticated = True - elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''), - self.hass.wsgi.api_password): + elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''), + view.hass.http.api_password): authenticated = True - if self.requires_auth and not authenticated: + if view.requires_auth and not authenticated: _LOGGER.warning('Login attempt or request with an invalid ' 'password from %s', remote_addr) - persistent_notification.create( - self.hass, + persistent_notification.async_create( + view.hass, 'Invalid password used from {}'.format(remote_addr), 'Login attempt failed', NOTIFICATION_ID_LOGIN) - raise Unauthorized() + raise HTTPUnauthorized() request.authenticated = authenticated _LOGGER.info('Serving %s to %s (auth: %s)', request.path, remote_addr, authenticated) - result = handler(request, **values) + assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \ + "Handler should be a coroutine or a callback." - if isinstance(result, self.Response): + result = handler(request, **request.match_info) + + if asyncio.iscoroutine(result): + result = yield from result + + if isinstance(result, web.StreamResponse): # The method handler returned a ready-made Response, how nice of it return result @@ -533,36 +504,14 @@ class HomeAssistantView(object): if isinstance(result, tuple): result, status_code = result - return self.Response(result, status=status_code) + if isinstance(result, str): + result = result.encode('utf-8') + elif result is None: + result = b'' + elif not isinstance(result, bytes): + assert False, ('Result should be None, string, bytes or Response. ' + 'Got: {}').format(result) - def json(self, result, status_code=200): - """Return a JSON response.""" - msg = json.dumps( - result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') - return self.Response( - msg, mimetype=CONTENT_TYPE_JSON, status=status_code) + return web.Response(body=result, status=status_code) - def json_message(self, error, status_code=200): - """Return a JSON message response.""" - return self.json({'message': error}, status_code) - - def file(self, request, fil, mimetype=None): - """Return a file.""" - from werkzeug.wsgi import wrap_file - from werkzeug.exceptions import NotFound - - if isinstance(fil, str): - if mimetype is None: - mimetype = mimetypes.guess_type(fil)[0] - - try: - fil = open(fil, mode='br') - except IOError: - raise NotFound() - - return self.Response(wrap_file(request.environ, fil), - mimetype=mimetype, direct_passthrough=True) - - def options(self, request): - """Default handler for OPTIONS (necessary for CORS preflight).""" - return self.Response('', status=200) + return handle diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index e8545210182..dac03f1a07b 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -247,7 +247,7 @@ def setup(hass, config): discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - hass.wsgi.register_view(iOSIdentifyDeviceView(hass)) + hass.http.register_view(iOSIdentifyDeviceView(hass)) app_config = config.get(DOMAIN, {}) hass.wsgi.register_view(iOSPushConfigView(hass, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 266496fff78..9d9936bd474 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -11,6 +11,7 @@ from itertools import groupby 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 +20,7 @@ from homeassistant.components.http import HomeAssistantView 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 +89,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] @@ -100,7 +101,7 @@ def setup(hass, config): message = message.async_render() async_log_entry(hass, name, message, domain, entity_id) - hass.wsgi.register_view(LogbookView(hass, config)) + hass.http.register_view(LogbookView(hass, config)) register_built_in_panel(hass, 'logbook', 'Logbook', 'mdi:format-list-bulleted-type') @@ -115,24 +116,37 @@ class LogbookView(HomeAssistantView): url = '/api/logbook' name = 'api:logbook' - extra_urls = ['/api/logbook/'] + extra_urls = ['/api/logbook/{datetime}'] def __init__(self, hass, config): """Initilalize the logbook view.""" super().__init__(hass) self.config = 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 self.json_message('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(None, get_results) return self.json(humanify(events)) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a3a6274a89e..838202fdcab 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -4,11 +4,13 @@ Component to interface with various media players. For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player/ """ +import asyncio import hashlib import logging import os import requests +from aiohttp import web import voluptuous as vol from homeassistant.config import load_yaml_config_file @@ -291,7 +293,7 @@ def setup(hass, config): component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.wsgi.register_view(MediaPlayerImageView(hass, component.entities)) + hass.http.register_view(MediaPlayerImageView(hass, component.entities)) component.setup(config) @@ -677,7 +679,7 @@ class MediaPlayerImageView(HomeAssistantView): """Media player view to serve an image.""" requires_auth = False - url = "/api/media_player_proxy/" + url = "/api/media_player_proxy/{entity_id}" name = "api:media_player:image" def __init__(self, hass, entities): @@ -685,26 +687,34 @@ class MediaPlayerImageView(HomeAssistantView): super().__init__(hass) self.entities = entities + @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" player = self.entities.get(entity_id) - if player is None: - return self.Response(status=404) + return web.Response(status=404) authenticated = (request.authenticated or - request.args.get('token') == player.access_token) + request.GET.get('token') == player.access_token) if not authenticated: - return self.Response(status=401) + return web.Response(status=401) image_url = player.media_image_url - if image_url: - response = requests.get(image_url) - else: - response = None + + if image_url is None: + return web.Response(status=404) + + def fetch_image(): + """Helper method to fetch image.""" + try: + return requests.get(image_url).content + except requests.RequestException: + return None + + response = yield from self.hass.loop.run_in_executor(None, fetch_image) if response is None: - return self.Response(status=500) + return web.Response(status=500) - return self.Response(response) + return web.Response(body=response) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 7173538032c..4ded65ba3ed 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -4,6 +4,7 @@ HTML5 Push Messaging notification service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.html5/ """ +import asyncio import os import logging import json @@ -107,9 +108,9 @@ def get_service(hass, config): if registrations is None: return None - hass.wsgi.register_view( + hass.http.register_view( HTML5PushRegistrationView(hass, registrations, json_path)) - hass.wsgi.register_view(HTML5PushCallbackView(hass, registrations)) + hass.http.register_view(HTML5PushCallbackView(hass, registrations)) gcm_api_key = config.get(ATTR_GCM_API_KEY) gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) @@ -163,12 +164,18 @@ class HTML5PushRegistrationView(HomeAssistantView): self.registrations = registrations self.json_path = json_path + @asyncio.coroutine def post(self, request): """Accept the POST request for push registrations from a browser.""" try: - data = REGISTER_SCHEMA(request.json) + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + try: + data = REGISTER_SCHEMA(data) except vol.Invalid as ex: - return self.json_message(humanize_error(request.json, ex), + return self.json_message(humanize_error(data, ex), HTTP_BAD_REQUEST) name = ensure_unique_string('unnamed device', @@ -182,9 +189,15 @@ class HTML5PushRegistrationView(HomeAssistantView): return self.json_message('Push notification subscriber registered.') + @asyncio.coroutine def delete(self, request): """Delete a registration.""" - subscription = request.json.get(ATTR_SUBSCRIPTION) + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + subscription = data.get(ATTR_SUBSCRIPTION) found = None @@ -270,23 +283,29 @@ class HTML5PushCallbackView(HomeAssistantView): status_code=HTTP_UNAUTHORIZED) return payload + @asyncio.coroutine def post(self, request): """Accept the POST request for push registrations event callback.""" auth_check = self.check_authorization_header(request) if not isinstance(auth_check, dict): return auth_check + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + event_payload = { - ATTR_TAG: request.json.get(ATTR_TAG), - ATTR_TYPE: request.json[ATTR_TYPE], + ATTR_TAG: data.get(ATTR_TAG), + ATTR_TYPE: data[ATTR_TYPE], ATTR_TARGET: auth_check[ATTR_TARGET], } - if request.json.get(ATTR_ACTION) is not None: - event_payload[ATTR_ACTION] = request.json.get(ATTR_ACTION) + if data.get(ATTR_ACTION) is not None: + event_payload[ATTR_ACTION] = data.get(ATTR_ACTION) - if request.json.get(ATTR_DATA) is not None: - event_payload[ATTR_DATA] = request.json.get(ATTR_DATA) + if data.get(ATTR_DATA) is not None: + event_payload[ATTR_DATA] = data.get(ATTR_DATA) try: event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload) diff --git a/homeassistant/components/openalpr.py b/homeassistant/components/openalpr.py index d6bda321141..35793c89144 100644 --- a/homeassistant/components/openalpr.py +++ b/homeassistant/components/openalpr.py @@ -153,7 +153,7 @@ def setup(hass, config): # Create Alpr device / render engine if render == RENDER_FFMPEG: use_render_fffmpeg = True - if not run_test(input_source): + if not run_test(hass, input_source): _LOGGER.error("'%s' is not valid ffmpeg input", input_source) continue diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 54c93b3270f..d27389b51f9 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -4,6 +4,7 @@ A component which is collecting configuration errors. For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ +import asyncio import os import logging @@ -14,6 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import slugify from homeassistant.config import load_yaml_config_file +from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'persistent_notification' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -35,6 +37,14 @@ _LOGGER = logging.getLogger(__name__) def create(hass, message, title=None, notification_id=None): + """Generate a notification.""" + run_coroutine_threadsafe( + async_create(hass, message, title, notification_id), hass.loop + ).result() + + +@asyncio.coroutine +def async_create(hass, message, title=None, notification_id=None): """Generate a notification.""" data = { key: value for key, value in [ @@ -44,7 +54,7 @@ def create(hass, message, title=None, notification_id=None): ] if value is not None } - hass.services.call(DOMAIN, SERVICE_CREATE, data) + yield from hass.services.async_call(DOMAIN, SERVICE_CREATE, data) def setup(hass, config): diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 11288bae63a..2c73bb764fb 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -12,6 +12,7 @@ import time import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity @@ -273,8 +274,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): scope=['activity', 'heartrate', 'nutrition', 'profile', 'settings', 'sleep', 'weight']) - hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) - hass.wsgi.register_view(FitbitAuthCallbackView( + hass.http.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) + hass.http.register_view(FitbitAuthCallbackView( hass, config, add_devices, oauth)) request_oauth_completion(hass) @@ -294,12 +295,13 @@ class FitbitAuthCallbackView(HomeAssistantView): 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 @@ class FitbitAuthCallbackView(HomeAssistantView): 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 c05217692ac..c1cb0cd98ca 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -9,6 +9,7 @@ import re 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) @@ -57,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): email = config.get(CONF_EMAIL) sensors = {} - hass.wsgi.register_view(TorqueReceiveDataView( + hass.http.register_view(TorqueReceiveDataView( hass, email, vehicle, sensors, add_devices)) return True @@ -77,9 +78,10 @@ class TorqueReceiveDataView(HomeAssistantView): 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 @@ class TorqueReceiveDataView(HomeAssistantView): 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,8 @@ class TorqueSensor(Entity): """Return the default icon of the sensor.""" return 'mdi:car' - def on_update(self, value): + @callback + 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 03a3d311f3c..dde7b791d90 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -10,6 +10,7 @@ from datetime import timedelta 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 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) 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, @@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) if len(DEVICES) == 0: - hass.wsgi.register_view(NetioApiView) + hass.http.register_view(NetioApiView) dev = Netio(host, port, username, password) @@ -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 @@ class NetioApiView(HomeAssistantView): 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) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 4935251db7d..c9addefec2b 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -83,12 +83,14 @@ SERVICE_TO_STATE = { # pylint: disable=too-few-public-methods, attribute-defined-outside-init -class TrackStates(object): +class AsyncTrackStates(object): """ Record the time when the with-block is entered. Add all states that have changed since the start time to the return list when with-block is exited. + + Must be run within the event loop. """ def __init__(self, hass): @@ -103,7 +105,8 @@ class TrackStates(object): def __exit__(self, exc_type, exc_value, traceback): """Add changes states to changes list.""" - self.states.extend(get_changed_since(self.hass.states.all(), self.now)) + self.states.extend(get_changed_since(self.hass.states.async_all(), + self.now)) def get_changed_since(states, utc_point_in_time): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 15a84e08ffe..ce20eb4ce0d 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -213,35 +213,35 @@ class EventForwarder(object): self._targets = {} self._lock = threading.Lock() - self._unsub_listener = None + self._async_unsub_listener = None - def connect(self, api): + @ha.callback + def async_connect(self, api): """Attach to a Home Assistant instance and forward events. Will overwrite old target if one exists with same host/port. """ - with self._lock: - if self._unsub_listener is None: - self._unsub_listener = self.hass.bus.listen( - ha.MATCH_ALL, self._event_listener) + if self._async_unsub_listener is None: + self._async_unsub_listener = self.hass.bus.async_listen( + ha.MATCH_ALL, self._event_listener) - key = (api.host, api.port) + key = (api.host, api.port) - self._targets[key] = api + self._targets[key] = api - def disconnect(self, api): + @ha.callback + def async_disconnect(self, api): """Remove target from being forwarded to.""" - with self._lock: - key = (api.host, api.port) + key = (api.host, api.port) - did_remove = self._targets.pop(key, None) is None + did_remove = self._targets.pop(key, None) is None - if len(self._targets) == 0: - # Remove event listener if no forwarding targets present - self._unsub_listener() - self._unsub_listener = None + if len(self._targets) == 0: + # Remove event listener if no forwarding targets present + self._async_unsub_listener() + self._async_unsub_listener = None - return did_remove + return did_remove def _event_listener(self, event): """Listen and forward all events.""" diff --git a/requirements_all.txt b/requirements_all.txt index 4ac6c45c01d..e79f9c9ab2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,6 +6,8 @@ pip>=7.0.0 jinja2>=2.8 voluptuous==0.9.2 typing>=3,<4 +aiohttp==1.0.5 +async_timeout==1.0.0 # homeassistant.components.nuimo_controller --only-binary=all git+https://github.com/getSenic/nuimo-linux-python#nuimo==1.0.0 @@ -28,9 +30,8 @@ SoCo==0.12 # homeassistant.components.notify.twitter TwitterAPI==2.4.2 -# homeassistant.components.emulated_hue # homeassistant.components.http -Werkzeug==0.11.11 +aiohttp_cors==0.4.0 # homeassistant.components.apcupsd apcaccess==0.0.4 @@ -62,10 +63,6 @@ blockchain==1.3.3 # homeassistant.components.notify.aws_sqs boto3==1.3.1 -# homeassistant.components.emulated_hue -# homeassistant.components.http -cherrypy==8.1.2 - # homeassistant.components.sensor.coinmarketcap coinmarketcap==2.0.1 @@ -136,7 +133,7 @@ gps3==0.33.3 ha-alpr==0.3 # homeassistant.components.ffmpeg -ha-ffmpeg==0.13 +ha-ffmpeg==0.14 # homeassistant.components.mqtt.server hbmqtt==0.7.1 @@ -483,10 +480,6 @@ speedtest-cli==0.3.4 # homeassistant.scripts.db_migrator sqlalchemy==1.1.1 -# homeassistant.components.emulated_hue -# homeassistant.components.http -static3==0.7.0 - # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test.txt b/requirements_test.txt index fd782a66933..933bb8a7c7b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,6 +2,7 @@ flake8>=3.0.4 pylint>=1.5.6 coveralls>=1.1 pytest>=2.9.2 +pytest-aiohttp>=0.1.3 pytest-asyncio>=0.5.0 pytest-cov>=2.3.1 pytest-timeout>=1.0.0 @@ -9,3 +10,4 @@ pytest-catchlog>=1.2.2 pydocstyle>=1.0.0 requests_mock>=1.0 mypy-lang>=0.4 +mock-open>=1.3.1 diff --git a/setup.py b/setup.py index d20aabb0f71..145b027e975 100755 --- a/setup.py +++ b/setup.py @@ -21,6 +21,8 @@ REQUIRES = [ 'jinja2>=2.8', 'voluptuous==0.9.2', 'typing>=3,<4', + 'aiohttp==1.0.5', + 'async_timeout==1.0.0', ] setup( diff --git a/tests/__init__.py b/tests/__init__.py index 2c44763f234..35d25f27356 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,29 +1 @@ -"""Setup some common test helper things.""" -import functools -import logging - -from homeassistant import util -from homeassistant.util import location - -logging.basicConfig() -logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) - - -def test_real(func): - """Force a function to require a keyword _test_real to be passed in.""" - @functools.wraps(func) - def guard_func(*args, **kwargs): - real = kwargs.pop('_test_real', None) - - if not real: - raise Exception('Forgot to mock or pass "_test_real=True" to %s', - func.__name__) - - return func(*args, **kwargs) - - return guard_func - -# Guard a few functions that would make network connections -location.detect_location_info = test_real(location.detect_location_info) -location.elevation = test_real(location.elevation) -util.get_local_ip = lambda: '127.0.0.1' +"""Tests for Home Assistant.""" diff --git a/tests/common.py b/tests/common.py index b185a47e66c..b73af5fc4c5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -38,23 +38,11 @@ def get_test_home_assistant(num_threads=None): orig_num_threads = ha.MIN_WORKER_THREAD ha.MIN_WORKER_THREAD = num_threads - hass = ha.HomeAssistant(loop) + hass = loop.run_until_complete(async_test_home_assistant(loop)) if num_threads: ha.MIN_WORKER_THREAD = orig_num_threads - hass.config.location_name = 'test home' - hass.config.config_dir = get_test_config_dir() - hass.config.latitude = 32.87336 - hass.config.longitude = -117.22743 - hass.config.elevation = 0 - hass.config.time_zone = date_util.get_time_zone('US/Pacific') - hass.config.units = METRIC_SYSTEM - hass.config.skip_pip = True - - if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: - loader.prepare(hass) - # FIXME should not be a daemon. Means hass.stop() not called in teardown stop_event = threading.Event() @@ -98,6 +86,35 @@ def get_test_home_assistant(num_threads=None): return hass +@asyncio.coroutine +def async_test_home_assistant(loop): + """Return a Home Assistant object pointing at test config dir.""" + loop._thread_ident = threading.get_ident() + + def get_hass(): + """Temp while we migrate core HASS over to be async constructors.""" + hass = ha.HomeAssistant(loop) + + hass.config.location_name = 'test home' + hass.config.config_dir = get_test_config_dir() + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 + hass.config.elevation = 0 + hass.config.time_zone = date_util.get_time_zone('US/Pacific') + hass.config.units = METRIC_SYSTEM + hass.config.skip_pip = True + + if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: + loader.prepare(hass) + + hass.state = ha.CoreState.running + return hass + + hass = yield from loop.run_in_executor(None, get_hass) + + return hass + + def get_test_instance_port(): """Return unused port for running test instance. @@ -181,8 +198,19 @@ def mock_state_change_event(hass, new_state, old_state=None): def mock_http_component(hass): """Mock the HTTP component.""" - hass.wsgi = mock.MagicMock() + hass.http = mock.MagicMock() hass.config.components.append('http') + hass.http.views = {} + + def mock_register_view(view): + """Store registered view.""" + if isinstance(view, type): + # Instantiate the view, if needed + view = view(hass) + + hass.http.views[view.name] = view + + hass.http.register_view = mock_register_view def mock_mqtt_component(hass): diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index df80b48e36b..e2ce9c15936 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -1,36 +1,18 @@ """The tests for generic camera component.""" -import unittest +import asyncio from unittest import mock -import requests_mock -from werkzeug.test import EnvironBuilder - from homeassistant.bootstrap import setup_component -from homeassistant.components.http import request_class - -from tests.common import get_test_home_assistant -class TestGenericCamera(unittest.TestCase): - """Test the generic camera platform.""" +@asyncio.coroutine +def test_fetching_url(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + aioclient_mock.get('http://example.com', text='hello world') - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.wsgi = mock.MagicMock() - self.hass.config.components.append('http') - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @requests_mock.Mocker() - def test_fetching_url(self, m): - """Test that it fetches the given url.""" - self.hass.wsgi = mock.MagicMock() - m.get('http://example.com', text='hello world') - - assert setup_component(self.hass, 'camera', { + def setup_platform(): + """Setup the platform.""" + assert setup_component(hass, 'camera', { 'camera': { 'name': 'config_test', 'platform': 'generic', @@ -39,32 +21,32 @@ class TestGenericCamera(unittest.TestCase): 'password': 'pass' }}) - image_view = self.hass.wsgi.mock_calls[0][1][0] + yield from hass.loop.run_in_executor(None, setup_platform) - builder = EnvironBuilder(method='GET') - Request = request_class() - request = Request(builder.get_environ()) - request.authenticated = True - resp = image_view.get(request, 'camera.config_test') + client = yield from test_client(hass.http.app) - assert m.call_count == 1 - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == 'hello world' + resp = yield from client.get('/api/camera_proxy/camera.config_test') - image_view.get(request, 'camera.config_test') - assert m.call_count == 2 + assert aioclient_mock.call_count == 1 + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello world' - @requests_mock.Mocker() - def test_limit_refetch(self, m): - """Test that it fetches the given url.""" - self.hass.wsgi = mock.MagicMock() - from requests.exceptions import Timeout - m.get('http://example.com/5a', text='hello world') - m.get('http://example.com/10a', text='hello world') - m.get('http://example.com/15a', text='hello planet') - m.get('http://example.com/20a', status_code=404) + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 2 - assert setup_component(self.hass, 'camera', { + +@asyncio.coroutine +def test_limit_refetch(aioclient_mock, hass, test_client): + """Test that it fetches the given url.""" + aioclient_mock.get('http://example.com/5a', text='hello world') + aioclient_mock.get('http://example.com/10a', text='hello world') + aioclient_mock.get('http://example.com/15a', text='hello planet') + aioclient_mock.get('http://example.com/20a', status=404) + + def setup_platform(): + """Setup the platform.""" + assert setup_component(hass, 'camera', { 'camera': { 'name': 'config_test', 'platform': 'generic', @@ -73,43 +55,47 @@ class TestGenericCamera(unittest.TestCase): 'limit_refetch_to_url_change': True, }}) - image_view = self.hass.wsgi.mock_calls[0][1][0] + yield from hass.loop.run_in_executor(None, setup_platform) - builder = EnvironBuilder(method='GET') - Request = request_class() - request = Request(builder.get_environ()) - request.authenticated = True + client = yield from test_client(hass.http.app) - self.hass.states.set('sensor.temp', '5') + resp = yield from client.get('/api/camera_proxy/camera.config_test') - with mock.patch('requests.get', side_effect=Timeout()): - resp = image_view.get(request, 'camera.config_test') - assert m.call_count == 0 - assert resp.status_code == 500, resp.response + hass.states.async_set('sensor.temp', '5') - self.hass.states.set('sensor.temp', '10') + with mock.patch('async_timeout.timeout', + side_effect=asyncio.TimeoutError()): + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 0 + assert resp.status == 500 - resp = image_view.get(request, 'camera.config_test') - assert m.call_count == 1 - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == 'hello world' + hass.states.async_set('sensor.temp', '10') - resp = image_view.get(request, 'camera.config_test') - assert m.call_count == 1 - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == 'hello world' + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 1 + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello world' - self.hass.states.set('sensor.temp', '15') + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 1 + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello world' - # Url change = fetch new image - resp = image_view.get(request, 'camera.config_test') - assert m.call_count == 2 - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == 'hello planet' + hass.states.async_set('sensor.temp', '15') - # Cause a template render error - self.hass.states.remove('sensor.temp') - resp = image_view.get(request, 'camera.config_test') - assert m.call_count == 2 - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == 'hello planet' + # Url change = fetch new image + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 2 + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello planet' + + # Cause a template render error + hass.states.async_remove('sensor.temp') + resp = yield from client.get('/api/camera_proxy/camera.config_test') + assert aioclient_mock.call_count == 2 + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello planet' diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 0c131b441b5..d43c138c570 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -1,70 +1,60 @@ """The tests for local file camera component.""" -import unittest +import asyncio from unittest import mock -from werkzeug.test import EnvironBuilder +# Using third party package because of a bug reading binary data in Python 3.4 +# https://bugs.python.org/issue23004 +from mock_open import MockOpen from homeassistant.bootstrap import setup_component -from homeassistant.components.http import request_class -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, mock_http_component -class TestLocalCamera(unittest.TestCase): - """Test the local file camera component.""" +@asyncio.coroutine +def test_loading_file(hass, test_client): + """Test that it loads image from disk.""" + @mock.patch('os.path.isfile', mock.Mock(return_value=True)) + @mock.patch('os.access', mock.Mock(return_value=True)) + def setup_platform(): + """Setup platform inside callback.""" + assert setup_component(hass, 'camera', { + 'camera': { + 'name': 'config_test', + 'platform': 'local_file', + 'file_path': 'mock.file', + }}) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.wsgi = mock.MagicMock() - self.hass.config.components.append('http') + yield from hass.loop.run_in_executor(None, setup_platform) - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + client = yield from test_client(hass.http.app) - def test_loading_file(self): - """Test that it loads image from disk.""" - test_string = 'hello' - self.hass.wsgi = mock.MagicMock() + m_open = MockOpen(read_data=b'hello') + with mock.patch( + 'homeassistant.components.camera.local_file.open', + m_open, create=True + ): + resp = yield from client.get('/api/camera_proxy/camera.config_test') - with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ - mock.patch('os.access', mock.Mock(return_value=True)): - assert setup_component(self.hass, 'camera', { - 'camera': { - 'name': 'config_test', - 'platform': 'local_file', - 'file_path': 'mock.file', - }}) + assert resp.status == 200 + body = yield from resp.text() + assert body == 'hello' - image_view = self.hass.wsgi.mock_calls[0][1][0] - m_open = mock.mock_open(read_data=test_string) - with mock.patch( - 'homeassistant.components.camera.local_file.open', - m_open, create=True - ): - builder = EnvironBuilder(method='GET') - Request = request_class() # pylint: disable=invalid-name - request = Request(builder.get_environ()) - request.authenticated = True - resp = image_view.get(request, 'camera.config_test') - - assert resp.status_code == 200, resp.response - assert resp.response[0].decode('utf-8') == test_string - - def test_file_not_readable(self): - """Test local file will not setup when file is not readable.""" - self.hass.wsgi = mock.MagicMock() +@asyncio.coroutine +def test_file_not_readable(hass): + """Test local file will not setup when file is not readable.""" + mock_http_component(hass) + def run_test(): with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ mock.patch('os.access', return_value=False), \ - assert_setup_component(0): - assert setup_component(self.hass, 'camera', { + assert_setup_component(0, 'camera'): + assert setup_component(hass, 'camera', { 'camera': { 'name': 'config_test', 'platform': 'local_file', 'file_path': 'mock.file', }}) - assert [] == self.hass.states.all() + yield from hass.loop.run_in_executor(None, run_test) diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 5addb3266c3..41b272c15eb 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -18,7 +18,7 @@ class TestUVCSetup(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.wsgi = mock.MagicMock() + self.hass.http = mock.MagicMock() self.hass.config.components = ['http'] def tearDown(self): diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index b49502054f1..2bbfaa77b8d 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -18,42 +18,19 @@ HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) API_PASSWORD = "test1234" HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} -hass = None - entity_id = 'media_player.walkman' -def setUpModule(): # pylint: disable=invalid-name - """Initalize a Home Assistant server.""" - global hass - - hass = get_test_home_assistant() - setup_component(hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_API_PASSWORD: API_PASSWORD, - }, - }) - - hass.start() - time.sleep(0.05) - - -def tearDownModule(): # pylint: disable=invalid-name - """Stop the Home Assistant server.""" - hass.stop() - - class TestDemoMediaPlayer(unittest.TestCase): """Test the media_player module.""" def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = hass - try: - self.hass.config.components.remove(mp.DOMAIN) - except ValueError: - pass + self.hass = get_test_home_assistant() + + def tearDown(self): + """Shut down test instance.""" + self.hass.stop() def test_source_select(self): """Test the input source service.""" @@ -226,21 +203,6 @@ class TestDemoMediaPlayer(unittest.TestCase): assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & state.attributes.get('supported_media_commands')) - @requests_mock.Mocker(real_http=True) - def test_media_image_proxy(self, m): - """Test the media server image proxy server .""" - fake_picture_data = 'test.test' - m.get('https://graph.facebook.com/v2.5/107771475912710/' - 'picture?type=large', text=fake_picture_data) - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) - assert self.hass.states.is_state(entity_id, 'playing') - state = self.hass.states.get(entity_id) - req = requests.get(HTTP_BASE_URL + - state.attributes.get('entity_picture')) - assert req.text == fake_picture_data - @patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.' 'media_seek') def test_play_media(self, mock_seek): @@ -275,3 +237,42 @@ class TestDemoMediaPlayer(unittest.TestCase): mp.media_seek(self.hass, 100, ent_id) self.hass.block_till_done() assert mock_seek.called + + +class TestMediaPlayerWeb(unittest.TestCase): + """Test the media player web views sensor.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + setup_component(self.hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT, + http.CONF_API_PASSWORD: API_PASSWORD, + }, + }) + + self.hass.start() + time.sleep(0.05) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker(real_http=True) + def test_media_image_proxy(self, m): + """Test the media server image proxy server .""" + fake_picture_data = 'test.test' + m.get('https://graph.facebook.com/v2.5/107771475912710/' + 'picture?type=large', text=fake_picture_data) + self.hass.block_till_done() + assert setup_component( + self.hass, mp.DOMAIN, + {'media_player': {'platform': 'demo'}}) + assert self.hass.states.is_state(entity_id, 'playing') + state = self.hass.states.get(entity_id) + req = requests.get(HTTP_BASE_URL + + state.attributes.get('entity_picture')) + assert req.status_code == 200 + assert req.text == fake_picture_data diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index e3439e4cb2f..1247d8a0548 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -1,10 +1,10 @@ """Test HTML5 notify platform.""" +import asyncio import json from unittest.mock import patch, MagicMock, mock_open -from werkzeug.test import EnvironBuilder +from aiohttp import web -from homeassistant.components.http import request_class from homeassistant.components.notify import html5 SUBSCRIPTION_1 = { @@ -35,6 +35,9 @@ SUBSCRIPTION_3 = { }, } +REGISTER_URL = '/api/notify.html5' +PUBLISH_URL = '/api/notify.html5/callback' + class TestHtml5Notify(object): """Tests for HTML5 notify platform.""" @@ -94,9 +97,13 @@ class TestHtml5Notify(object): assert payload['body'] == 'Hello' assert payload['icon'] == 'beer.png' - def test_registering_new_device_view(self): + @asyncio.coroutine + def test_registering_new_device_view(self, loop, test_client): """Test that the HTML view works.""" hass = MagicMock() + expected = { + 'unnamed device': SUBSCRIPTION_1, + } m = mock_open() with patch( @@ -114,21 +121,20 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == {} - builder = EnvironBuilder(method='POST', - data=json.dumps(SUBSCRIPTION_1)) - Request = request_class() - resp = view.post(Request(builder.get_environ())) + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) + resp = yield from client.post(REGISTER_URL, + data=json.dumps(SUBSCRIPTION_1)) - expected = { - 'unnamed device': SUBSCRIPTION_1, - } - - assert resp.status_code == 200, resp.response + content = yield from resp.text() + assert resp.status == 200, content assert view.registrations == expected handle = m() assert json.loads(handle.write.call_args[0][0]) == expected - def test_registering_new_device_validation(self): + @asyncio.coroutine + def test_registering_new_device_validation(self, loop, test_client): """Test various errors when registering a new device.""" hass = MagicMock() @@ -146,34 +152,34 @@ class TestHtml5Notify(object): view = hass.mock_calls[1][1][0] - Request = request_class() + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) - builder = EnvironBuilder(method='POST', data=json.dumps({ + resp = yield from client.post(REGISTER_URL, data=json.dumps({ 'browser': 'invalid browser', 'subscription': 'sub info', })) - resp = view.post(Request(builder.get_environ())) - assert resp.status_code == 400, resp.response + assert resp.status == 400 - builder = EnvironBuilder(method='POST', data=json.dumps({ + resp = yield from client.post(REGISTER_URL, data=json.dumps({ 'browser': 'chrome', })) - resp = view.post(Request(builder.get_environ())) - assert resp.status_code == 400, resp.response + assert resp.status == 400 - builder = EnvironBuilder(method='POST', data=json.dumps({ - 'browser': 'chrome', - 'subscription': 'sub info', - })) with patch('homeassistant.components.notify.html5._save_config', return_value=False): - resp = view.post(Request(builder.get_environ())) - assert resp.status_code == 400, resp.response + # resp = view.post(Request(builder.get_environ())) + resp = yield from client.post(REGISTER_URL, data=json.dumps({ + 'browser': 'chrome', + 'subscription': 'sub info', + })) - @patch('homeassistant.components.notify.html5.os') - def test_unregistering_device_view(self, mock_os): + assert resp.status == 400 + + @asyncio.coroutine + def test_unregistering_device_view(self, loop, test_client): """Test that the HTML unregister view works.""" - mock_os.path.isfile.return_value = True hass = MagicMock() config = { @@ -182,11 +188,14 @@ class TestHtml5Notify(object): } m = mock_open(read_data=json.dumps(config)) - with patch( - 'homeassistant.components.notify.html5.open', m, create=True - ): + + with patch('homeassistant.components.notify.html5.open', m, + create=True): hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {}) + + with patch('homeassistant.components.notify.html5.os.path.isfile', + return_value=True): + service = html5.get_service(hass, {}) assert service is not None @@ -197,23 +206,25 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - builder = EnvironBuilder(method='DELETE', data=json.dumps({ + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) + + resp = yield from client.delete(REGISTER_URL, data=json.dumps({ 'subscription': SUBSCRIPTION_1['subscription'], })) - Request = request_class() - resp = view.delete(Request(builder.get_environ())) config.pop('some device') - assert resp.status_code == 200, resp.response + assert resp.status == 200, resp.response assert view.registrations == config handle = m() assert json.loads(handle.write.call_args[0][0]) == config - @patch('homeassistant.components.notify.html5.os') - def test_unregister_device_view_handle_unknown_subscription(self, mock_os): + @asyncio.coroutine + def test_unregister_device_view_handle_unknown_subscription(self, loop, + test_client): """Test that the HTML unregister view handles unknown subscriptions.""" - mock_os.path.isfile.return_value = True hass = MagicMock() config = { @@ -226,7 +237,9 @@ class TestHtml5Notify(object): 'homeassistant.components.notify.html5.open', m, create=True ): hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {}) + with patch('homeassistant.components.notify.html5.os.path.isfile', + return_value=True): + service = html5.get_service(hass, {}) assert service is not None @@ -237,21 +250,23 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - builder = EnvironBuilder(method='DELETE', data=json.dumps({ + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) + + resp = yield from client.delete(REGISTER_URL, data=json.dumps({ 'subscription': SUBSCRIPTION_3['subscription'] })) - Request = request_class() - resp = view.delete(Request(builder.get_environ())) - assert resp.status_code == 200, resp.response + assert resp.status == 200, resp.response assert view.registrations == config handle = m() assert handle.write.call_count == 0 - @patch('homeassistant.components.notify.html5.os') - def test_unregistering_device_view_handles_json_safe_error(self, mock_os): + @asyncio.coroutine + def test_unregistering_device_view_handles_json_safe_error(self, loop, + test_client): """Test that the HTML unregister view handles JSON write errors.""" - mock_os.path.isfile.return_value = True hass = MagicMock() config = { @@ -264,7 +279,9 @@ class TestHtml5Notify(object): 'homeassistant.components.notify.html5.open', m, create=True ): hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {}) + with patch('homeassistant.components.notify.html5.os.path.isfile', + return_value=True): + service = html5.get_service(hass, {}) assert service is not None @@ -275,21 +292,23 @@ class TestHtml5Notify(object): assert view.json_path == hass.config.path.return_value assert view.registrations == config - builder = EnvironBuilder(method='DELETE', data=json.dumps({ - 'subscription': SUBSCRIPTION_1['subscription'], - })) - Request = request_class() + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) with patch('homeassistant.components.notify.html5._save_config', return_value=False): - resp = view.delete(Request(builder.get_environ())) + resp = yield from client.delete(REGISTER_URL, data=json.dumps({ + 'subscription': SUBSCRIPTION_1['subscription'], + })) - assert resp.status_code == 500, resp.response + assert resp.status == 500, resp.response assert view.registrations == config handle = m() assert handle.write.call_count == 0 - def test_callback_view_no_jwt(self): + @asyncio.coroutine + def test_callback_view_no_jwt(self, loop, test_client): """Test that the notification callback view works without JWT.""" hass = MagicMock() @@ -307,20 +326,20 @@ class TestHtml5Notify(object): view = hass.mock_calls[2][1][0] - builder = EnvironBuilder(method='POST', data=json.dumps({ + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) + + resp = yield from client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72' })) - Request = request_class() - resp = view.post(Request(builder.get_environ())) - assert resp.status_code == 401, resp.response + assert resp.status == 401, resp.response - @patch('homeassistant.components.notify.html5.os') - @patch('pywebpush.WebPusher') - def test_callback_view_with_jwt(self, mock_wp, mock_os): + @asyncio.coroutine + def test_callback_view_with_jwt(self, loop, test_client): """Test that the notification callback view works with JWT.""" - mock_os.path.isfile.return_value = True hass = MagicMock() data = { @@ -332,15 +351,18 @@ class TestHtml5Notify(object): 'homeassistant.components.notify.html5.open', m, create=True ): hass.config.path.return_value = 'file.conf' - service = html5.get_service(hass, {'gcm_sender_id': '100'}) + with patch('homeassistant.components.notify.html5.os.path.isfile', + return_value=True): + service = html5.get_service(hass, {'gcm_sender_id': '100'}) assert service is not None # assert hass.called assert len(hass.mock_calls) == 3 - service.send_message('Hello', target=['device'], - data={'icon': 'beer.png'}) + with patch('pywebpush.WebPusher') as mock_wp: + service.send_message('Hello', target=['device'], + data={'icon': 'beer.png'}) assert len(mock_wp.mock_calls) == 2 @@ -359,13 +381,14 @@ class TestHtml5Notify(object): bearer_token = "Bearer {}".format(push_payload['data']['jwt']) - builder = EnvironBuilder(method='POST', data=json.dumps({ + app = web.Application(loop=loop) + view.register(app.router) + client = yield from test_client(app) + + resp = yield from client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', }), headers={'Authorization': bearer_token}) - Request = request_class() - resp = view.post(Request(builder.get_environ())) - assert resp.status_code == 200, resp.response - returned = resp.response[0].decode('utf-8') - expected = '{"event": "push", "status": "ok"}' - assert json.loads(returned) == json.loads(expected) + assert resp.status == 200 + body = yield from resp.json() + assert body == {"event": "push", "status": "ok"} diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 3ea94938f0d..0f7162c079e 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -1,33 +1,29 @@ """The tests for the Yr sensor platform.""" from datetime import datetime -from unittest import TestCase from unittest.mock import patch -import requests_mock - from homeassistant.bootstrap import _setup_component import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant, load_fixture -class TestSensorYr(TestCase): +class TestSensorYr: """Test the Yr sensor.""" - def setUp(self): + def setup_method(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.latitude = 32.87336 self.hass.config.longitude = 117.22743 - def tearDown(self): + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() - @requests_mock.Mocker() - def test_default_setup(self, m): + def test_default_setup(self, requests_mock): """Test the default setup.""" - m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', - text=load_fixture('yr.no.json')) + requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) with patch('homeassistant.components.sensor.yr.dt_util.utcnow', @@ -42,11 +38,10 @@ class TestSensorYr(TestCase): assert state.state.isnumeric() assert state.attributes.get('unit_of_measurement') is None - @requests_mock.Mocker() - def test_custom_setup(self, m): + def test_custom_setup(self, requests_mock): """Test a custom setup.""" - m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', - text=load_fixture('yr.no.json')) + requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) with patch('homeassistant.components.sensor.yr.dt_util.utcnow', diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 78affc70648..ee00c42b8cc 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,11 +1,13 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods +import asyncio from contextlib import closing import json import time import unittest from unittest.mock import Mock, patch +from aiohttp import web import requests from homeassistant import bootstrap, const @@ -243,20 +245,18 @@ class TestAPI(unittest.TestCase): def test_api_get_error_log(self): """Test the return of the error log.""" - test_string = 'Test String°'.encode('UTF-8') + test_string = 'Test String°' - # Can't use read_data with wsgiserver in Python 3.4.2. Due to a - # bug in read_data, it can't handle byte types ('Type str doesn't - # support the buffer API'), but wsgiserver requires byte types - # ('WSGI Applications must yield bytes'). So just mock our own - # read method. - m_open = Mock(return_value=Mock( - read=Mock(side_effect=[test_string])) - ) - with patch('homeassistant.components.http.open', m_open, create=True): + @asyncio.coroutine + def mock_send(): + """Mock file send.""" + return web.Response(text=test_string) + + with patch('homeassistant.components.http.HomeAssistantView.file', + Mock(return_value=mock_send())): req = requests.get(_url(const.URL_API_ERROR_LOG), headers=HA_HEADERS) - self.assertEqual(test_string, req.text.encode('UTF-8')) + self.assertEqual(test_string, req.text) self.assertIsNone(req.headers.get('expires')) def test_api_get_event_listeners(self): diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 2023ea24a35..765b3e3f35c 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -34,12 +34,12 @@ def setUpModule(): # pylint: disable=invalid-name hass.bus.listen('test_event', lambda _: _) hass.states.set('test.test', 'a_state') - bootstrap.setup_component( + assert bootstrap.setup_component( hass, http.DOMAIN, {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, http.CONF_SERVER_PORT: SERVER_PORT}}) - bootstrap.setup_component(hass, 'frontend') + assert bootstrap.setup_component(hass, 'frontend') hass.start() time.sleep(0.05) @@ -71,7 +71,7 @@ class TestFrontend(unittest.TestCase): self.assertIsNotNone(frontendjs) - req = requests.head(_url(frontendjs.groups(0)[0])) + req = requests.get(_url(frontendjs.groups(0)[0])) self.assertEqual(200, req.status_code) diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 57f21fd76d2..5ef26d5d5ab 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -56,7 +56,7 @@ def setUpModule(): bootstrap.setup_component(hass, 'api') - hass.wsgi.trusted_networks = [ + hass.http.trusted_networks = [ ip_network(trusted_network) for trusted_network in TRUSTED_NETWORKS] @@ -159,12 +159,9 @@ class TestHttp: headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL}) allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS - all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS) assert req.status_code == 200 assert req.headers.get(allow_origin) == HTTP_BASE_URL - assert req.headers.get(allow_headers) == all_allow_headers def test_cors_allowed_with_password_in_header(self): """Test cross origin resource sharing with password in header.""" @@ -175,12 +172,9 @@ class TestHttp: req = requests.get(_url(const.URL_API), headers=headers) allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN - allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS - all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS) assert req.status_code == 200 assert req.headers.get(allow_origin) == HTTP_BASE_URL - assert req.headers.get(allow_headers) == all_allow_headers def test_cors_denied_without_origin_header(self): """Test cross origin resource sharing with password in header.""" @@ -207,8 +201,8 @@ class TestHttp: allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS - all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS) assert req.status_code == 200 assert req.headers.get(allow_origin) == HTTP_BASE_URL - assert req.headers.get(allow_headers) == all_allow_headers + assert req.headers.get(allow_headers) == \ + const.HTTP_HEADER_HA_AUTH.upper() diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index 1f934e64a19..060fdf01dca 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -1,6 +1,7 @@ """The tests for the InfluxDB component.""" import unittest from unittest import mock +from unittest.mock import patch import influxdb as influx_client @@ -60,6 +61,8 @@ class TestInfluxDB(unittest.TestCase): assert setup_component(self.hass, influxdb.DOMAIN, config) + @patch('homeassistant.components.persistent_notification.create', + mock.MagicMock()) def test_setup_missing_password(self, mock_client): """Test the setup with existing username and missing password.""" config = { diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000000..815765a8ed2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,59 @@ +"""Setup some common test helper things.""" +import functools +import logging + +import pytest +import requests_mock as _requests_mock + +from homeassistant import util +from homeassistant.util import location + +from .common import async_test_home_assistant +from .test_util.aiohttp import mock_aiohttp_client + +logging.basicConfig() +logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + + +def test_real(func): + """Force a function to require a keyword _test_real to be passed in.""" + @functools.wraps(func) + def guard_func(*args, **kwargs): + real = kwargs.pop('_test_real', None) + + if not real: + raise Exception('Forgot to mock or pass "_test_real=True" to %s', + func.__name__) + + return func(*args, **kwargs) + + return guard_func + +# Guard a few functions that would make network connections +location.detect_location_info = test_real(location.detect_location_info) +location.elevation = test_real(location.elevation) +util.get_local_ip = lambda: '127.0.0.1' + + +@pytest.fixture +def hass(loop): + """Fixture to provide a test instance of HASS.""" + hass = loop.run_until_complete(async_test_home_assistant(loop)) + + yield hass + + loop.run_until_complete(hass.async_stop()) + + +@pytest.fixture +def requests_mock(): + """Fixture to provide a requests mocker.""" + with _requests_mock.mock() as m: + yield m + + +@pytest.fixture +def aioclient_mock(): + """Fixture to mock aioclient calls.""" + with mock_aiohttp_client() as mock_session: + yield mock_session diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 1dbf86edae9..0d7b8c46d86 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,4 +1,5 @@ """Test state helpers.""" +import asyncio from datetime import timedelta import unittest from unittest.mock import patch @@ -20,6 +21,42 @@ from homeassistant.components.sun import (STATE_ABOVE_HORIZON, from tests.common import get_test_home_assistant, mock_service +def test_async_track_states(event_loop): + """Test AsyncTrackStates context manager.""" + hass = get_test_home_assistant() + + try: + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + @asyncio.coroutine + @patch('homeassistant.core.dt_util.utcnow') + def run_test(mock_utcnow): + """Run the test.""" + mock_utcnow.return_value = point2 + + with state.AsyncTrackStates(hass) as states: + mock_utcnow.return_value = point1 + hass.states.set('light.test', 'on') + + mock_utcnow.return_value = point2 + hass.states.set('light.test2', 'on') + state2 = hass.states.get('light.test2') + + mock_utcnow.return_value = point3 + hass.states.set('light.test3', 'on') + state3 = hass.states.get('light.test3') + + assert [state2, state3] == \ + sorted(states, key=lambda state: state.entity_id) + + event_loop.run_until_complete(run_test()) + + finally: + hass.stop() + + class TestStateHelpers(unittest.TestCase): """Test the Home Assistant event helpers.""" @@ -54,31 +91,6 @@ class TestStateHelpers(unittest.TestCase): [state2, state3], state.get_changed_since([state1, state2, state3], point2)) - def test_track_states(self): - """Test tracking of states.""" - point1 = dt_util.utcnow() - point2 = point1 + timedelta(seconds=5) - point3 = point2 + timedelta(seconds=5) - - with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: - mock_utcnow.return_value = point2 - - with state.TrackStates(self.hass) as states: - mock_utcnow.return_value = point1 - self.hass.states.set('light.test', 'on') - - mock_utcnow.return_value = point2 - self.hass.states.set('light.test2', 'on') - state2 = self.hass.states.get('light.test2') - - mock_utcnow.return_value = point3 - self.hass.states.set('light.test3', 'on') - state3 = self.hass.states.get('light.test3') - - self.assertEqual( - sorted([state2, state3], key=lambda state: state.entity_id), - sorted(states, key=lambda state: state.entity_id)) - def test_reproduce_with_no_entity(self): """Test reproduce_state with no entity.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py new file mode 100644 index 00000000000..f2a33a3ac3c --- /dev/null +++ b/tests/test_util/aiohttp.py @@ -0,0 +1,112 @@ +"""Aiohttp test utils.""" +import asyncio +from contextlib import contextmanager +import functools +import json as _json +from unittest import mock + + +class AiohttpClientMocker: + """Mock Aiohttp client requests.""" + + def __init__(self): + """Initialize the request mocker.""" + self._mocks = [] + self.mock_calls = [] + + def request(self, method, url, *, + status=200, + text=None, + content=None, + json=None): + """Mock a request.""" + if json: + text = _json.dumps(json) + if text: + content = text.encode('utf-8') + if content is None: + content = b'' + + self._mocks.append(AiohttpClientMockResponse( + method, url, status, content)) + + def get(self, *args, **kwargs): + """Register a mock get request.""" + self.request('get', *args, **kwargs) + + def put(self, *args, **kwargs): + """Register a mock put request.""" + self.request('put', *args, **kwargs) + + def post(self, *args, **kwargs): + """Register a mock post request.""" + self.request('post', *args, **kwargs) + + def delete(self, *args, **kwargs): + """Register a mock delete request.""" + self.request('delete', *args, **kwargs) + + def options(self, *args, **kwargs): + """Register a mock options request.""" + self.request('options', *args, **kwargs) + + @property + def call_count(self): + """Number of requests made.""" + return len(self.mock_calls) + + @asyncio.coroutine + def match_request(self, method, url): + """Match a request against pre-registered requests.""" + for response in self._mocks: + if response.match_request(method, url): + self.mock_calls.append((method, url)) + return response + + assert False, "No mock registered for {} {}".format(method.upper(), + url) + + +class AiohttpClientMockResponse: + """Mock Aiohttp client response.""" + + def __init__(self, method, url, status, response): + """Initialize a fake response.""" + self.method = method + self.url = url + self.status = status + self.response = response + + def match_request(self, method, url): + """Test if response answers request.""" + return method == self.method and url == self.url + + @asyncio.coroutine + def read(self): + """Return mock response.""" + return self.response + + @asyncio.coroutine + def text(self, encoding='utf-8'): + """Return mock response as a string.""" + return self.response.decode(encoding) + + @asyncio.coroutine + def release(self): + """Mock release.""" + pass + + +@contextmanager +def mock_aiohttp_client(): + """Context manager to mock aiohttp client.""" + mocker = AiohttpClientMocker() + + with mock.patch('aiohttp.ClientSession') as mock_session: + instance = mock_session() + + for method in ('get', 'post', 'put', 'options', 'delete'): + setattr(instance, method, + functools.partial(mocker.match_request, method)) + + yield mocker From 0c563f7b148f539118d9ae87f5290221ba0673b8 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 00:01:56 -0700 Subject: [PATCH 008/149] Minor updater... updates (#4020) * Enable updater in dev versions * Code clarity * Add log line about being on the current version already * Remove dev check test --- homeassistant/components/updater.py | 30 +++++++++++++++++------------ tests/components/test_updater.py | 9 --------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 40899af9803..515e5f431a5 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -61,8 +61,9 @@ def setup(hass, config): """Setup the updater component.""" if 'dev' in CURRENT_VERSION: # This component only makes sense in release versions - _LOGGER.warning('Updater not supported in development version') - return False + _LOGGER.warning(('Updater component enabled in dev. ' + 'You will not receive notifications of new ' + 'versions but analytics will be submitted.')) config = config.get(DOMAIN, {}) huuid = _load_uuid(hass) if config.get(CONF_REPORTING) else None @@ -80,12 +81,18 @@ def check_newest_version(hass, huuid): """Check if a new version is available and report if one is.""" newest, releasenotes = get_newest_version(huuid) - if newest is not None: - if StrictVersion(newest) > StrictVersion(CURRENT_VERSION): - hass.states.set( - ENTITY_ID, newest, {ATTR_FRIENDLY_NAME: 'Update Available', - ATTR_RELEASE_NOTES: releasenotes} - ) + if newest is None or 'dev' in CURRENT_VERSION: + return + + if StrictVersion(newest) > StrictVersion(CURRENT_VERSION): + _LOGGER.info('The latest available version is %s.', newest) + hass.states.set( + ENTITY_ID, newest, {ATTR_FRIENDLY_NAME: 'Update Available', + ATTR_RELEASE_NOTES: releasenotes} + ) + elif StrictVersion(newest) == StrictVersion(CURRENT_VERSION): + _LOGGER.info('You are on the latest version (%s) of Home Assistant.', + newest) def get_newest_version(huuid): @@ -95,7 +102,7 @@ def get_newest_version(huuid): 'os_name': platform.system(), "arch": platform.machine(), 'python_version': platform.python_version(), 'virtualenv': (os.environ.get('VIRTUAL_ENV') is not None), - 'docker': False} + 'docker': False, 'dev': ('dev' in CURRENT_VERSION)} if platform.system() == 'Windows': info_object['os_version'] = platform.win32_ver()[0] @@ -114,9 +121,8 @@ def get_newest_version(huuid): try: req = requests.post(UPDATER_URL, json=info_object) res = req.json() - _LOGGER.info(('The latest version is %s. ' - 'Information submitted includes %s'), - res['version'], info_object) + _LOGGER.info(('Submitted analytics to Home Assistant servers. ' + 'Information submitted includes %s'), info_object) return (res['version'], res['release-notes']) except requests.RequestException: _LOGGER.exception('Could not contact HASS Update to check for updates') diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index 7cc2ba8d962..94a43c1f281 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -89,15 +89,6 @@ class TestUpdater(unittest.TestCase): mock_get.side_effect = KeyError self.assertIsNone(updater.get_newest_version(uuid)) - def test_updater_disabled_on_dev(self): - """Test if the updater component is disabled on dev.""" - updater.CURRENT_VERSION = MOCK_CURRENT_VERSION + 'dev' - - with assert_setup_component(1) as config: - assert not setup_component( - self.hass, updater.DOMAIN, {updater.DOMAIN: {}}) - assert config['updater'] == {'reporting': True} - def test_uuid_function(self): """Test if the uuid function works.""" path = self.hass.config.path(updater.UPDATER_UUID_FILE) From f26a7fc6bbb1c08527e1bc60aa803bb2c3637ee9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Oct 2016 00:09:20 -0700 Subject: [PATCH 009/149] Update http --- homeassistant/components/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 25515c61046..b13e786f50d 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -328,7 +328,8 @@ class HomeAssistantWSGI(object): @asyncio.coroutine def serve_file(request): """Redirect to location.""" - return _GZIP_FILE_SENDER.send(request, filepath) + yield from _GZIP_FILE_SENDER.send(request, filepath) + return # aiohttp supports regex matching for variables. Using that as temp # to work around cache busting MD5. From fc3b7907ed656b20204f8c31487729bf8ae7782d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 24 Oct 2016 21:59:48 +0200 Subject: [PATCH 010/149] Upgrade python-telegram-bot to 5.1.1 (#4001) --- homeassistant/components/notify/telegram.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 04ae74e4655..9b0b735b128 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -19,7 +19,7 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==5.1.0'] +REQUIREMENTS = ['python-telegram-bot==5.1.1'] ATTR_PHOTO = 'photo' ATTR_DOCUMENT = 'document' diff --git a/requirements_all.txt b/requirements_all.txt index e79f9c9ab2f..8c6f2747683 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -418,7 +418,7 @@ python-nmap==0.6.1 python-pushover==0.2 # homeassistant.components.notify.telegram -python-telegram-bot==5.1.0 +python-telegram-bot==5.1.1 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 72751b95b54bee15c295b673208f9f28738ace35 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 24 Oct 2016 22:00:02 +0200 Subject: [PATCH 011/149] Upgrade slacker to 0.9.29 (#4000) --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 48f80138073..8dedee2a127 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_USERNAME, CONF_ICON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['slacker==0.9.28'] +REQUIREMENTS = ['slacker==0.9.29'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8c6f2747683..6a37ff695cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ scsgate==0.1.0 sendgrid==3.6.0 # homeassistant.components.notify.slack -slacker==0.9.28 +slacker==0.9.29 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 From 4ecfc7d066c91cea0c687c8ab9292f72608d0b64 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 24 Oct 2016 22:00:22 +0200 Subject: [PATCH 012/149] Upgrade sqlalchemy to 1.1.2 (#4003) --- homeassistant/components/recorder/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 6feee95be45..3ce05e8f72b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util DOMAIN = 'recorder' -REQUIREMENTS = ['sqlalchemy==1.1.1'] +REQUIREMENTS = ['sqlalchemy==1.1.2'] DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_DB_FILE = 'home-assistant_v2.db' diff --git a/requirements_all.txt b/requirements_all.txt index 6a37ff695cb..5c79e7ebff3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,7 +478,7 @@ speedtest-cli==0.3.4 # homeassistant.components.recorder # homeassistant.scripts.db_migrator -sqlalchemy==1.1.1 +sqlalchemy==1.1.2 # homeassistant.components.statsd statsd==3.2.1 From 627517cbbc2e2170547ccb15787503ad6a4a4306 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 24 Oct 2016 22:24:33 +0200 Subject: [PATCH 013/149] Upgrade psutil to 4.4.0 (#4032) --- homeassistant/components/sensor/systemmonitor.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 125a2871f28..e7a1db077aa 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==4.3.1'] +REQUIREMENTS = ['psutil==4.4.0'] _LOGGER = logging.getLogger(__name__) @@ -51,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the System sensors.""" + """Set up the system monitor sensors.""" dev = [] for resource in config[CONF_RESOURCES]: if 'arg' not in resource: diff --git a/requirements_all.txt b/requirements_all.txt index 5c79e7ebff3..5db96a87977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -312,7 +312,7 @@ pmsensor==0.3 proliphix==0.4.0 # homeassistant.components.sensor.systemmonitor -psutil==4.3.1 +psutil==4.4.0 # homeassistant.components.wink pubnub==3.8.2 From 71589193467186051ea98cbd3bf2d82a98b08c98 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 15:03:51 -0700 Subject: [PATCH 014/149] Missed a wsgi->http on iOS component --- homeassistant/components/ios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index dac03f1a07b..ce110c017d6 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -250,7 +250,7 @@ def setup(hass, config): hass.http.register_view(iOSIdentifyDeviceView(hass)) app_config = config.get(DOMAIN, {}) - hass.wsgi.register_view(iOSPushConfigView(hass, + hass.http.register_view(iOSPushConfigView(hass, app_config.get(CONF_PUSH, {}))) return True From f25ddef4d7178dc3371a6e4a7d9f2fd7ace75b9f Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 17:31:45 -0700 Subject: [PATCH 015/149] More iOS HTTP Async updates --- homeassistant/components/ios.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index ce110c017d6..f67ad966ead 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -4,6 +4,7 @@ Native Home Assistant iOS app component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/ios/ """ +import asyncio import os import json import logging @@ -15,6 +16,8 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery +from homeassistant.core import callback + from homeassistant.components.http import HomeAssistantView from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, @@ -268,6 +271,7 @@ class iOSPushConfigView(HomeAssistantView): super().__init__(hass) self.push_config = push_config + @callback def get(self, request): """Handle the GET request for the push configuration.""" return self.json(self.push_config) @@ -283,10 +287,16 @@ class iOSIdentifyDeviceView(HomeAssistantView): """Init the view.""" super().__init__(hass) + @asyncio.coroutine def post(self, request): """Handle the POST request for device identification.""" try: - data = IDENTIFY_SCHEMA(request.json) + req_data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + try: + data = IDENTIFY_SCHEMA(req_data) except vol.Invalid as ex: return self.json_message(humanize_error(request.json, ex), HTTP_BAD_REQUEST) From 23f54b07c70814efdac33dfe062ff070bd7f6e6a Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 18:46:47 -0700 Subject: [PATCH 016/149] Dont load notify.ios if no devices exist. Thanks @arsaboo for catching this. --- homeassistant/components/notify/ios.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 2517020434e..940804ab49c 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -45,6 +45,13 @@ def get_service(hass, config): # Need this to enable requirements checking in the app. hass.config.components.append("notify.ios") + if not ios.devices_with_push(): + _LOGGER.error(("The notify.ios platform was loaded but no " + "devices exist! Please check the documentation at " + "https://home-assistant.io/components/notify.ios/ " + "for more information")) + return None + return iOSNotificationService() From 0ff500ca25e50488e2b133465059cec71586546e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Oct 2016 00:49:49 -0400 Subject: [PATCH 017/149] Add mochad component (#3970) This commit adds a new component for communicating with mochad[1] a socket interface for the CM15A and CM19A USB X10 controllers. This commit leverages the pymochad library to interface with a mochad socket either on a local or remote machine. Mochad is added as as a generic platform because it supports multiple different classes of device, however in this patch only the switch device implemented as a starting point. Future patches will include other devices types. (although that's dependent on someone gaining access to those) [1] https://sourceforge.net/projects/mochad/ --- .coveragerc | 3 + homeassistant/components/mochad.py | 85 ++++++++++++++++++++++ homeassistant/components/switch/mochad.py | 81 +++++++++++++++++++++ homeassistant/helpers/config_validation.py | 9 +++ requirements_all.txt | 3 + tests/components/switch/test_mochad.py | 79 ++++++++++++++++++++ tests/helpers/test_config_validation.py | 12 +++ 7 files changed, 272 insertions(+) create mode 100644 homeassistant/components/mochad.py create mode 100644 homeassistant/components/switch/mochad.py create mode 100644 tests/components/switch/test_mochad.py diff --git a/.coveragerc b/.coveragerc index 15fa27dd1c0..d3f142d6efe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,6 +109,9 @@ omit = homeassistant/components/zoneminder.py homeassistant/components/*/zoneminder.py + homeassistant/components/mochad.py + homeassistant/components/*/mochad.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py new file mode 100644 index 00000000000..83665a3c6d1 --- /dev/null +++ b/homeassistant/components/mochad.py @@ -0,0 +1,85 @@ +""" +Support for CM15A/CM19A X10 Controller using mochad daemon. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mochad/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_HOST, CONF_PORT) +from homeassistant.helpers import config_validation as cv + +REQUIREMENTS = ['pymochad==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +CONTROLLER = None + +DOMAIN = 'mochad' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_HOST, default='localhost'): cv.string, + vol.Optional(CONF_PORT, default=1099): cv.port, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the mochad platform.""" + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + + from pymochad import exceptions + + global CONTROLLER + try: + CONTROLLER = MochadCtrl(host, port) + except exceptions.ConfigurationError: + _LOGGER.exception() + return False + + def stop_mochad(event): + """Stop the Mochad service.""" + CONTROLLER.disconnect() + + def start_mochad(event): + """Start the Mochad service.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mochad) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mochad) + + return True + + +class MochadCtrl(object): + """Mochad controller.""" + + def __init__(self, host, port): + """Initialize a PyMochad controller.""" + super(MochadCtrl, self).__init__() + self._host = host + self._port = port + + from pymochad import controller + + self.ctrl = controller.PyMochad(server=self._host, port=self._port) + + @property + def host(self): + """The server where mochad is running.""" + return self._host + + @property + def port(self): + """The port mochad is running on.""" + return self._port + + def disconnect(self): + """Close the connection to the mochad socket.""" + self.ctrl.socket.close() diff --git a/homeassistant/components/switch/mochad.py b/homeassistant/components/switch/mochad.py new file mode 100644 index 00000000000..b7ebcabeb86 --- /dev/null +++ b/homeassistant/components/switch/mochad.py @@ -0,0 +1,81 @@ +""" +Contains functionality to use a X10 switch over Mochad. + +For more details about this platform, please refer to the documentation at +https://home.assistant.io/components/switch.mochad +""" + +import logging + +import voluptuous as vol + +from homeassistant.components import mochad +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import (CONF_NAME, CONF_PLATFORM) +from homeassistant.helpers import config_validation as cv + +DEPENDENCIES = ['mochad'] +_LOGGER = logging.getLogger(__name__) + +CONF_ADDRESS = 'address' +CONF_DEVICES = 'devices' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): mochad.DOMAIN, + CONF_DEVICES: [{ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.x10_address, + vol.Optional('comm_type'): cv.string, + }] +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup X10 switches over a mochad controller.""" + devs = config.get(CONF_DEVICES) + add_devices([MochadSwitch( + hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + return True + + +class MochadSwitch(SwitchDevice): + """Representation of a X10 switch over Mochad.""" + + def __init__(self, hass, ctrl, dev): + """Initialize a Mochad Switch Device.""" + from pymochad import device + + self._controller = ctrl + self._address = dev[CONF_ADDRESS] + self._name = dev.get(CONF_NAME, 'x10_switch_dev_%s' % self._address) + self._comm_type = dev.get('comm_type', 'pl') + self.device = device.Device(ctrl, self._address, + comm_type=self._comm_type) + self._state = self._get_device_status() + + @property + def name(self): + """Get the name of the switch.""" + return self._name + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._state = True + self.device.send_cmd('on') + self._controller.read_data() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._state = False + self.device.send_cmd('off') + self._controller.read_data() + + def _get_device_status(self): + """Get the status of the switch from mochad.""" + status = self.device.get_status().rstrip() + return status == 'on' + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4c6efe11001..9598c57a7b2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -2,6 +2,7 @@ from collections import OrderedDict from datetime import timedelta import os +import re from urllib.parse import urlparse from socket import _GLOBAL_DEFAULT_TIMEOUT @@ -336,6 +337,14 @@ def url(value: Any) -> str: raise vol.Invalid('invalid url') +def x10_address(value): + """Validate an x10 address.""" + regex = re.compile(r'([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$') + if not regex.match(value): + raise vol.Invalid('Invalid X10 Address') + return str(value).lower() + + def ordered_dict(value_validator, key_validator=match_all): """Validate an ordered dict validator that maintains ordering. diff --git a/requirements_all.txt b/requirements_all.txt index 5db96a87977..fd454ba809e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -370,6 +370,9 @@ pylast==1.6.0 # homeassistant.components.sensor.loopenergy pyloopenergy==0.0.15 +# homeassistant.components.mochad +pymochad==0.1.1 + # homeassistant.components.device_tracker.netgear pynetgear==0.3.3 diff --git a/tests/components/switch/test_mochad.py b/tests/components/switch/test_mochad.py new file mode 100644 index 00000000000..c6c570449cb --- /dev/null +++ b/tests/components/switch/test_mochad.py @@ -0,0 +1,79 @@ +"""The tests for the mochad switch platform.""" +import unittest +import unittest.mock as mock + +from homeassistant.bootstrap import setup_component +from homeassistant.components import switch +from homeassistant.components.switch import mochad + +from tests.common import get_test_home_assistant + + +class TestMochadSwitchSetup(unittest.TestCase): + """Test the mochad switch.""" + + PLATFORM = mochad + COMPONENT = switch + THING = 'switch' + + def setUp(self): + """Setup things to be run when tests are started.""" + super(TestMochadSwitchSetup, self).setUp() + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everyhing that was started.""" + self.hass.stop() + super(TestMochadSwitchSetup, self).tearDown() + + @mock.patch('pymochad.controller.PyMochad') + @mock.patch('homeassistant.components.switch.mochad.MochadSwitch') + def test_setup_adds_proper_devices(self, mock_switch, mock_client): + """Test if setup adds devices.""" + good_config = { + 'mochad': {}, + 'switch': { + 'platform': 'mochad', + 'devices': [ + { + 'name': 'Switch1', + 'address': 'a1', + }, + ], + } + } + self.assertTrue(setup_component(self.hass, switch.DOMAIN, good_config)) + + +class TestMochadSwitch(unittest.TestCase): + """Test for mochad switch platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + super(TestMochadSwitch, self).setUp() + self.hass = get_test_home_assistant() + controller_mock = mock.MagicMock() + device_patch = mock.patch('pymochad.device.Device') + device_patch.start() + self.addCleanup(device_patch.stop) + dev_dict = {'address': 'a1', 'name': 'fake_switch'} + self.switch = mochad.MochadSwitch(self.hass, controller_mock, + dev_dict) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_name(self): + """Test the name.""" + self.assertEqual('fake_switch', self.switch.name) + + def test_turn_on(self): + """Test turn_on.""" + self.switch.turn_on() + self.switch.device.send_cmd.assert_called_once_with('on') + + def test_turn_off(self): + """Test turn_off.""" + self.switch.turn_off() + self.switch.device.send_cmd.assert_called_once_with('off') diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 3ff9755bba2..7d9030bdc96 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -300,6 +300,18 @@ def test_temperature_unit(): schema('F') +def test_x10_address(): + """Test x10 addr validator.""" + schema = vol.Schema(cv.x10_address) + with pytest.raises(vol.Invalid): + schema('Q1') + schema('q55') + schema('garbage_addr') + + schema('a1') + schema('C11') + + def test_template(): """Test template validator.""" schema = vol.Schema(cv.template) From 4c86721e7099cb6290250355681abe48bde16056 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 25 Oct 2016 06:53:03 +0200 Subject: [PATCH 018/149] Update tests, rename variable, and change conversion (#3546) --- .coveragerc | 1 + homeassistant/components/weather/__init__.py | 129 ++++++++++++++ homeassistant/components/weather/demo.py | 95 +++++++++++ .../components/weather/openweathermap.py | 159 ++++++++++++++++++ requirements_all.txt | 1 + tests/components/weather/__init__.py | 1 + tests/components/weather/test_weather.py | 57 +++++++ 7 files changed, 443 insertions(+) create mode 100644 homeassistant/components/weather/__init__.py create mode 100644 homeassistant/components/weather/demo.py create mode 100644 homeassistant/components/weather/openweathermap.py create mode 100644 tests/components/weather/__init__.py create mode 100644 tests/components/weather/test_weather.py diff --git a/.coveragerc b/.coveragerc index d3f142d6efe..d0a3f34223a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -314,6 +314,7 @@ omit = homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py homeassistant/components/upnp.py + homeassistant/components/weather/openweathermap.py homeassistant/components/zeroconf.py diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py new file mode 100644 index 00000000000..dcb5dc49233 --- /dev/null +++ b/homeassistant/components/weather/__init__.py @@ -0,0 +1,129 @@ +""" +Weather component that handles meteorological data for your location. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/weather/ +""" +import logging + +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = [] +DOMAIN = 'weather' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +ATTR_CONDITION_CLASS = 'condition_class' +ATTR_WEATHER_ATTRIBUTION = 'attribution' +ATTR_WEATHER_HUMIDITY = 'humidity' +ATTR_WEATHER_OZONE = 'ozone' +ATTR_WEATHER_PRESSURE = 'pressure' +ATTR_WEATHER_TEMPERATURE = 'temperature' +ATTR_WEATHER_WIND_BEARING = 'wind_bearing' +ATTR_WEATHER_WIND_SPEED = 'wind_speed' + + +def setup(hass, config): + """Setup the weather component.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + component.setup(config) + + return True + + +# pylint: disable=no-member, no-self-use, too-many-return-statements +class WeatherEntity(Entity): + """ABC for a weather data.""" + + @property + def temperature(self): + """Return the platform temperature.""" + raise NotImplementedError() + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + raise NotImplementedError() + + @property + def pressure(self): + """Return the pressure.""" + return None + + @property + def humidity(self): + """Return the humidity.""" + raise NotImplementedError() + + @property + def wind_speed(self): + """Return the wind speed.""" + return None + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return None + + @property + def ozone(self): + """Return the ozone level.""" + return None + + @property + def attribution(self): + """Return the attribution.""" + return None + + @property + def state_attributes(self): + """Return the state attributes.""" + data = { + ATTR_WEATHER_TEMPERATURE: + convert_temperature( + self.temperature, self.temperature_unit, + self.hass.config.units.temperature_unit), + ATTR_WEATHER_HUMIDITY: self.humidity, + } + + ozone = self.ozone + if ozone is not None: + data[ATTR_WEATHER_OZONE] = ozone + + pressure = self.pressure + if pressure is not None: + data[ATTR_WEATHER_PRESSURE] = pressure + + wind_bearing = self.wind_bearing + if wind_bearing is not None: + data[ATTR_WEATHER_WIND_BEARING] = wind_bearing + + wind_speed = self.wind_speed + if wind_speed is not None: + data[ATTR_WEATHER_WIND_SPEED] = wind_speed + + attribution = self.attribution + if attribution is not None: + data[ATTR_WEATHER_ATTRIBUTION] = attribution + + return data + + @property + def state(self): + """Return the current state.""" + return self.condition + + @property + def condition(self): + """Return the current condition.""" + raise NotImplementedError() + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return None diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py new file mode 100644 index 00000000000..dec4dcf2450 --- /dev/null +++ b/homeassistant/components/weather/demo.py @@ -0,0 +1,95 @@ +""" +Demo platform that offers fake meteorological data. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.weather import WeatherEntity +from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) + +CONDITION_CLASSES = { + 'cloudy': [], + 'fog': [], + 'hail': [], + 'lightning': [], + 'lightning-rainy': [], + 'partlycloudy': [], + 'pouring': [], + 'rainy': ['shower rain'], + 'snowy': [], + 'snowy-rainy': [], + 'sunny': ['sunshine'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo weather.""" + add_devices([ + DemoWeather('South', 'Sunshine', 21, 92, 1099, 0.5, TEMP_CELSIUS), + DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT) + ]) + + +# pylint: disable=too-many-arguments +class DemoWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, name, condition, temperature, humidity, pressure, + wind_speed, temperature_unit): + """Initialize the Demo weather.""" + self._name = name + self._condition = condition + self._temperature = temperature + self._temperature_unit = temperature_unit + self._humidity = humidity + self._pressure = pressure + self._wind_speed = wind_speed + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format('Demo Weather', self._name) + + @property + def should_poll(self): + """No polling needed for a demo weather condition.""" + return False + + @property + def temperature(self): + """Return the temperature.""" + return self._temperature + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def humidity(self): + """Return the humidity.""" + return self._humidity + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._wind_speed + + @property + def pressure(self): + """Return the wind speed.""" + return self._pressure + + @property + def condition(self): + """Return the weather condition.""" + return [k for k, v in CONDITION_CLASSES.items() if + self._condition.lower() in v][0] + + @property + def attribution(self): + """Return the attribution.""" + return 'Powered by Home Assistant' diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py new file mode 100644 index 00000000000..4133509de89 --- /dev/null +++ b/homeassistant/components/weather/openweathermap.py @@ -0,0 +1,159 @@ +""" +Support for the OpenWeatherMap (OWM) service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.openweathermap/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyowm==2.5.0'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'OpenWeatherMap' +ATTRIBUTION = 'Data provided by OpenWeatherMap' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) + +CONDITION_CLASSES = { + 'cloudy': [804], + 'fog': [701, 741], + 'hail': [906], + 'lightning': [210, 211, 212, 221], + 'lightning-rainy': [200, 201, 202, 230, 231, 232], + 'partlycloudy': [801, 802, 803], + 'pouring': [504, 314, 502, 503, 522], + 'rainy': [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], + 'snowy': [600, 601, 602, 611, 612, 620, 621, 622], + 'snowy-rainy': [511, 615, 616], + 'sunny': [800], + 'windy': [905, 951, 952, 953, 954, 955, 956, 957], + 'windy-variant': [958, 959, 960, 961], + 'exceptional': [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, + 904], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the OpenWeatherMap weather platform.""" + import pyowm + + longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) + latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) + name = config.get(CONF_NAME) + + try: + owm = pyowm.OWM(config.get(CONF_API_KEY)) + except pyowm.exceptions.api_call_error.APICallError: + _LOGGER.error("Error while connecting to OpenWeatherMap") + return False + + data = WeatherData(owm, latitude, longitude) + + add_devices([OpenWeatherMapWeather( + name, data, hass.config.units.temperature_unit)]) + + +# pylint: disable=too-few-public-methods +class OpenWeatherMapWeather(WeatherEntity): + """Implementation of an OpenWeatherMap sensor.""" + + def __init__(self, name, owm, temperature_unit): + """Initialize the sensor.""" + self._name = name + self._owm = owm + self._temperature_unit = temperature_unit + self.date = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + try: + return [k for k, v in CONDITION_CLASSES.items() if + self.data.get_weather_code() in v][0] + except IndexError: + return STATE_UNKNOWN + + @property + def temperature(self): + """Return the temperature.""" + return self.data.get_temperature('celsius')['temp'] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def pressure(self): + """Return the pressure.""" + return self.data.get_pressure()['press'] + + @property + def humidity(self): + """Return the humidity.""" + return self.data.get_humidity() + + @property + def wind_speed(self): + """Return the wind speed.""" + return self.data.get_wind()['speed'] + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self.data.get_wind()['deg'] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + # pylint: disable=too-many-branches + def update(self): + """Get the latest data from OWM and updates the states.""" + self._owm.update() + self.data = self._owm.data + + +class WeatherData(object): + """Get the latest data from OpenWeatherMap.""" + + def __init__(self, owm, latitude, longitude): + """Initialize the data object.""" + self.owm = owm + self.latitude = latitude + self.longitude = longitude + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from OpenWeatherMap.""" + obs = self.owm.weather_at_coords(self.latitude, self.longitude) + if obs is None: + _LOGGER.warning("Failed to fetch data from OWM") + return + + self.data = obs.get_weather() diff --git a/requirements_all.txt b/requirements_all.txt index fd454ba809e..5ec6876efaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,6 +384,7 @@ pynetio==0.1.6 pynx584==0.2 # homeassistant.components.sensor.openweathermap +# homeassistant.components.weather.openweathermap pyowm==2.5.0 # homeassistant.components.switch.acer_projector diff --git a/tests/components/weather/__init__.py b/tests/components/weather/__init__.py new file mode 100644 index 00000000000..24df7abb1f3 --- /dev/null +++ b/tests/components/weather/__init__.py @@ -0,0 +1 @@ +"""The tests for Weather platforms.""" diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py new file mode 100644 index 00000000000..97aaf0f6486 --- /dev/null +++ b/tests/components/weather/test_weather.py @@ -0,0 +1,57 @@ +"""The tests for the Weather component.""" +import unittest + +from homeassistant.components import weather +from homeassistant.components.weather import ( + ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED) +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.bootstrap import setup_component + +from tests.common import get_test_home_assistant + + +class TestWeather(unittest.TestCase): + """Test the Weather component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + self.assertTrue(setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'platform': 'demo', + } + })) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_attributes(self): + """Test weather attributes.""" + state = self.hass.states.get('weather.demo_weather_south') + assert state is not None + + assert state.state == 'sunny' + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == 21 + assert data.get(ATTR_WEATHER_HUMIDITY) == 92 + assert data.get(ATTR_WEATHER_PRESSURE) == 1099 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 + assert data.get(ATTR_WEATHER_WIND_BEARING) is None + assert data.get(ATTR_WEATHER_OZONE) is None + assert data.get(ATTR_WEATHER_ATTRIBUTION) == \ + 'Powered by Home Assistant' + + def test_temperature_convert(self): + """Test temperature conversion.""" + state = self.hass.states.get('weather.demo_weather_north') + assert state is not None + + assert state.state == 'rainy' + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 From 1707cdf9f36b19e8294eafee57e93355b9d8c137 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 25 Oct 2016 06:59:09 +0200 Subject: [PATCH 019/149] Added support for Notifications for Android TV / FireTV (#3978) * Added support for Notifications for Android TV / FireTV * Silly me forgot to commit coverage * Fixed pylint * Fixed flake8 * Fixed another flake8 -.- * Changed option 'ip' to 'host' like most other platforms do --- .coveragerc | 1 + .../components/notify/nfandroidtv.py | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 homeassistant/components/notify/nfandroidtv.py diff --git a/.coveragerc b/.coveragerc index d0a3f34223a..651ea7cbad7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -212,6 +212,7 @@ omit = homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py + homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py new file mode 100644 index 00000000000..598493d8fd0 --- /dev/null +++ b/homeassistant/components/notify/nfandroidtv.py @@ -0,0 +1,192 @@ +""" +Notifications for Android TV notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.nfandroidtv/ +""" +import os +import logging +import requests +import voluptuous as vol + +from homeassistant.components.notify import (ATTR_TITLE, + ATTR_TITLE_DEFAULT, + ATTR_DATA, + BaseNotificationService, + PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_IP = 'host' +CONF_DURATION = 'duration' +CONF_POSITION = 'position' +CONF_TRANSPARENCY = 'transparency' +CONF_COLOR = 'color' +CONF_INTERRUPT = 'interrupt' +CONF_TIMEOUT = 'timeout' + +DEFAULT_DURATION = 5 +DEFAULT_POSITION = 'bottom-right' +DEFAULT_TRANSPARENCY = 'default' +DEFAULT_COLOR = 'grey' +DEFAULT_INTERRUPT = False +DEFAULT_TIMEOUT = 5 + +ATTR_DURATION = 'duration' +ATTR_POSITION = 'position' +ATTR_TRANSPARENCY = 'transparency' +ATTR_COLOR = 'color' +ATTR_BKGCOLOR = 'bkgcolor' +ATTR_INTERRUPT = 'interrupt' + +POSITIONS = { + "bottom-right": 0, + "bottom-left": 1, + "top-right": 2, + "top-left": 3, + "center": 4, +} + +TRANSPARENCIES = { + "default": 0, + "0%": 1, + "25%": 2, + "50%": 3, + "75%": 4, + "100%": 5, +} + +COLORS = { + "grey": "#607d8b", + "black": "#000000", + "indigo": "#303F9F", + "green": "#4CAF50", + "red": "#F44336", + "cyan": "#00BCD4", + "teal": "#009688", + "amber": "#FFC107", + "pink": "#E91E63", +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP): cv.string, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Coerce(int), + vol.Optional(CONF_POSITION, default=DEFAULT_POSITION): + vol.In(POSITIONS.keys()), + vol.Optional(CONF_TRANSPARENCY, default=DEFAULT_TRANSPARENCY): + vol.In(TRANSPARENCIES.keys()), + vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): + vol.In(COLORS.keys()), + vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), + vol.Optional(CONF_INTERRUPT, default=DEFAULT_INTERRUPT): cv.boolean, +}) + + +# pylint: disable=unused-argument +def get_service(hass, config): + """Get the Notifications for Android TV notification service.""" + remoteip = config.get(CONF_IP) + duration = config.get(CONF_DURATION) + position = config.get(CONF_POSITION) + transparency = config.get(CONF_TRANSPARENCY) + color = config.get(CONF_COLOR) + interrupt = config.get(CONF_INTERRUPT) + timeout = config.get(CONF_TIMEOUT) + + return NFAndroidTVNotificationService(remoteip, + duration, + position, + transparency, + color, + interrupt, + timeout) + + +# pylint: disable=too-many-instance-attributes +class NFAndroidTVNotificationService(BaseNotificationService): + """Notification service for Notifications for Android TV.""" + + # pylint: disable=too-many-arguments,too-few-public-methods + def __init__(self, remoteip, duration, position, transparency, + color, interrupt, timeout): + """Initialize the service.""" + self._target = "http://%s:7676" % remoteip + self._default_duration = duration + self._default_position = position + self._default_transparency = transparency + self._default_color = color + self._default_interrupt = interrupt + self._timeout = timeout + self._icon_file = os.path.join(os.path.dirname(__file__), "..", + "frontend", + "www_static", "icons", + "favicon-192x192.png") + + # pylint: disable=too-many-branches + def send_message(self, message="", **kwargs): + """Send a message to a Android TV device.""" + _LOGGER.debug("Sending notification to: %s", self._target) + + payload = dict(filename=('icon.png', + open(self._icon_file, 'rb'), + 'application/octet-stream', + {'Expires': '0'}), type="0", + title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + msg=message, duration="%i" % self._default_duration, + position="%i" % POSITIONS.get(self._default_position), + bkgcolor="%s" % COLORS.get(self._default_color), + transparency="%i" % TRANSPARENCIES.get( + self._default_transparency), + offset="0", app=ATTR_TITLE_DEFAULT, force="true", + interrupt="%i" % self._default_interrupt) + + data = kwargs.get(ATTR_DATA) + if data: + if ATTR_DURATION in data: + duration = data.get(ATTR_DURATION) + try: + payload[ATTR_DURATION] = "%i" % int(duration) + except ValueError: + _LOGGER.warning("Invalid duration-value: %s", + str(duration)) + if ATTR_POSITION in data: + position = data.get(ATTR_POSITION) + if position in POSITIONS: + payload[ATTR_POSITION] = "%i" % POSITIONS.get(position) + else: + _LOGGER.warning("Invalid position-value: %s", + str(position)) + if ATTR_TRANSPARENCY in data: + transparency = data.get(ATTR_TRANSPARENCY) + if transparency in TRANSPARENCIES: + payload[ATTR_TRANSPARENCY] = "%i" % TRANSPARENCIES.get( + transparency) + else: + _LOGGER.warning("Invalid transparency-value: %s", + str(transparency)) + if ATTR_COLOR in data: + color = data.get(ATTR_COLOR) + if color in COLORS: + payload[ATTR_BKGCOLOR] = "%s" % COLORS.get(color) + else: + _LOGGER.warning("Invalid color-value: %s", str(color)) + if ATTR_INTERRUPT in data: + interrupt = data.get(ATTR_INTERRUPT) + try: + payload[ATTR_INTERRUPT] = "%i" % cv.boolean(interrupt) + except vol.Invalid: + _LOGGER.warning("Invalid interrupt-value: %s", + str(interrupt)) + + try: + _LOGGER.debug("Payload: %s", str(payload)) + response = requests.post(self._target, + files=payload, + timeout=self._timeout) + if response.status_code != 200: + _LOGGER.error("Error sending message: %s", str(response)) + except requests.exceptions.ConnectionError as err: + _LOGGER.error("Error communicating with %s: %s", + self._target, str(err)) From 044b9caa7611f8693855fb92050db0a6e8dcf65d Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 22:00:43 -0700 Subject: [PATCH 020/149] Remove garage_door, hvac, rollershutter and thermostat components/platforms --- .coveragerc | 7 - .../components/garage_door/__init__.py | 111 ---- homeassistant/components/garage_door/demo.py | 51 -- homeassistant/components/garage_door/mqtt.py | 141 ----- .../components/garage_door/rpi_gpio.py | 110 ---- .../components/garage_door/services.yaml | 15 - homeassistant/components/garage_door/wink.py | 40 -- homeassistant/components/garage_door/zwave.py | 70 --- homeassistant/components/hvac/__init__.py | 500 ------------------ homeassistant/components/hvac/demo.py | 164 ------ homeassistant/components/hvac/services.yaml | 84 --- homeassistant/components/hvac/zwave.py | 241 --------- .../components/rollershutter/__init__.py | 174 ------ .../components/rollershutter/command_line.py | 125 ----- .../components/rollershutter/demo.py | 92 ---- .../components/rollershutter/mqtt.py | 123 ----- .../components/rollershutter/rfxtrx.py | 67 --- .../components/rollershutter/scsgate.py | 98 ---- .../components/rollershutter/services.yaml | 31 -- .../components/rollershutter/wink.py | 61 --- .../components/rollershutter/zwave.py | 139 ----- .../components/thermostat/__init__.py | 330 ------------ homeassistant/components/thermostat/demo.py | 86 --- homeassistant/components/thermostat/ecobee.py | 246 --------- .../components/thermostat/eq3btsmart.py | 90 ---- .../components/thermostat/heat_control.py | 215 -------- .../components/thermostat/heatmiser.py | 114 ---- .../components/thermostat/honeywell.py | 266 ---------- homeassistant/components/thermostat/knx.py | 83 --- homeassistant/components/thermostat/nest.py | 189 ------- .../components/thermostat/proliphix.py | 90 ---- .../components/thermostat/radiotherm.py | 136 ----- .../components/thermostat/services.yaml | 48 -- homeassistant/components/thermostat/zwave.py | 168 ------ tests/components/garage_door/__init__.py | 1 - tests/components/garage_door/test_demo.py | 50 -- tests/components/garage_door/test_mqtt.py | 138 ----- tests/components/hvac/__init__.py | 1 - tests/components/hvac/test_demo.py | 167 ------ tests/components/rollershutter/__init__.py | 1 - .../rollershutter/test_command_line.py | 88 --- tests/components/rollershutter/test_demo.py | 55 -- tests/components/rollershutter/test_mqtt.py | 174 ------ tests/components/rollershutter/test_rfxtrx.py | 219 -------- tests/components/thermostat/__init__.py | 1 - tests/components/thermostat/test_demo.py | 101 ---- .../thermostat/test_heat_control.py | 494 ----------------- tests/components/thermostat/test_honeywell.py | 391 -------------- 48 files changed, 6386 deletions(-) delete mode 100644 homeassistant/components/garage_door/__init__.py delete mode 100644 homeassistant/components/garage_door/demo.py delete mode 100644 homeassistant/components/garage_door/mqtt.py delete mode 100644 homeassistant/components/garage_door/rpi_gpio.py delete mode 100644 homeassistant/components/garage_door/services.yaml delete mode 100644 homeassistant/components/garage_door/wink.py delete mode 100644 homeassistant/components/garage_door/zwave.py delete mode 100644 homeassistant/components/hvac/__init__.py delete mode 100644 homeassistant/components/hvac/demo.py delete mode 100644 homeassistant/components/hvac/services.yaml delete mode 100755 homeassistant/components/hvac/zwave.py delete mode 100644 homeassistant/components/rollershutter/__init__.py delete mode 100644 homeassistant/components/rollershutter/command_line.py delete mode 100644 homeassistant/components/rollershutter/demo.py delete mode 100644 homeassistant/components/rollershutter/mqtt.py delete mode 100644 homeassistant/components/rollershutter/rfxtrx.py delete mode 100644 homeassistant/components/rollershutter/scsgate.py delete mode 100644 homeassistant/components/rollershutter/services.yaml delete mode 100644 homeassistant/components/rollershutter/wink.py delete mode 100644 homeassistant/components/rollershutter/zwave.py delete mode 100644 homeassistant/components/thermostat/__init__.py delete mode 100644 homeassistant/components/thermostat/demo.py delete mode 100644 homeassistant/components/thermostat/ecobee.py delete mode 100644 homeassistant/components/thermostat/eq3btsmart.py delete mode 100644 homeassistant/components/thermostat/heat_control.py delete mode 100644 homeassistant/components/thermostat/heatmiser.py delete mode 100644 homeassistant/components/thermostat/honeywell.py delete mode 100644 homeassistant/components/thermostat/knx.py delete mode 100644 homeassistant/components/thermostat/nest.py delete mode 100644 homeassistant/components/thermostat/proliphix.py delete mode 100644 homeassistant/components/thermostat/radiotherm.py delete mode 100644 homeassistant/components/thermostat/services.yaml delete mode 100644 homeassistant/components/thermostat/zwave.py delete mode 100644 tests/components/garage_door/__init__.py delete mode 100644 tests/components/garage_door/test_demo.py delete mode 100644 tests/components/garage_door/test_mqtt.py delete mode 100644 tests/components/hvac/__init__.py delete mode 100644 tests/components/hvac/test_demo.py delete mode 100644 tests/components/rollershutter/__init__.py delete mode 100644 tests/components/rollershutter/test_command_line.py delete mode 100644 tests/components/rollershutter/test_demo.py delete mode 100644 tests/components/rollershutter/test_mqtt.py delete mode 100644 tests/components/rollershutter/test_rfxtrx.py delete mode 100644 tests/components/thermostat/__init__.py delete mode 100644 tests/components/thermostat/test_demo.py delete mode 100644 tests/components/thermostat/test_heat_control.py delete mode 100644 tests/components/thermostat/test_honeywell.py diff --git a/.coveragerc b/.coveragerc index d0a3f34223a..f39ae8ef052 100644 --- a/.coveragerc +++ b/.coveragerc @@ -160,8 +160,6 @@ omit = homeassistant/components/fan/mqtt.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py - homeassistant/components/garage_door/rpi_gpio.py - homeassistant/components/garage_door/wink.py homeassistant/components/hdmi_cec.py homeassistant/components/ifttt.py homeassistant/components/joaoapps_join.py @@ -308,11 +306,6 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py - homeassistant/components/thermostat/eq3btsmart.py - homeassistant/components/thermostat/heatmiser.py - homeassistant/components/thermostat/homematic.py - homeassistant/components/thermostat/proliphix.py - homeassistant/components/thermostat/radiotherm.py homeassistant/components/upnp.py homeassistant/components/weather/openweathermap.py homeassistant/components/zeroconf.py diff --git a/homeassistant/components/garage_door/__init__.py b/homeassistant/components/garage_door/__init__.py deleted file mode 100644 index c5576b1da84..00000000000 --- a/homeassistant/components/garage_door/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Component to interface with garage doors that can be controlled remotely. - -For more details about this component, please refer to the documentation -at https://home-assistant.io/components/garage_door/ -""" -import logging -import os - -import voluptuous as vol - -from homeassistant.config import load_yaml_config_file -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, - ATTR_ENTITY_ID) -from homeassistant.components import group - -DOMAIN = 'garage_door' -SCAN_INTERVAL = 30 - -GROUP_NAME_ALL_GARAGE_DOORS = 'all garage doors' -ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors') - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -_LOGGER = logging.getLogger(__name__) - - -def is_closed(hass, entity_id=None): - """Return if the garage door is closed based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_GARAGE_DOORS - return hass.states.is_state(entity_id, STATE_CLOSED) - - -def close_door(hass, entity_id=None): - """Close all or a specified garage door.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE, data) - - -def open_door(hass, entity_id=None): - """Open all or specified garage door.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN, data) - - -def setup(hass, config): - """Track states and offer events for garage door.""" - _LOGGER.warning('This component has been deprecated in favour of the ' - '"cover" component and will be removed in the future.' - ' Please upgrade.') - component = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS) - component.setup(config) - - def handle_garage_door_service(service): - """Handle calls to the garage door services.""" - target_locks = component.extract_from_service(service) - - for item in target_locks: - if service.service == SERVICE_CLOSE: - item.close_door() - else: - item.open_door() - - if item.should_poll: - item.update_ha_state(True) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service, - descriptions.get(SERVICE_OPEN), - schema=GARAGE_DOOR_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service, - descriptions.get(SERVICE_CLOSE), - schema=GARAGE_DOOR_SERVICE_SCHEMA) - return True - - -class GarageDoorDevice(Entity): - """Representation of a garage door.""" - - # pylint: disable=no-self-use - @property - def is_closed(self): - """Return true if door is closed.""" - return None - - def close_door(self): - """Close the garage door.""" - raise NotImplementedError() - - def open_door(self): - """Open the garage door.""" - raise NotImplementedError() - - @property - def state(self): - """Return the state of the garage door.""" - closed = self.is_closed - if closed is None: - return STATE_UNKNOWN - return STATE_CLOSED if closed else STATE_OPEN diff --git a/homeassistant/components/garage_door/demo.py b/homeassistant/components/garage_door/demo.py deleted file mode 100644 index dad8df7782c..00000000000 --- a/homeassistant/components/garage_door/demo.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Demo garage door platform that has two fake doors. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" -from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.const import STATE_CLOSED, STATE_OPEN - - -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup demo garage door platform.""" - add_devices_callback([ - DemoGarageDoor('Left Garage Door', STATE_CLOSED), - DemoGarageDoor('Right Garage Door', STATE_OPEN) - ]) - - -class DemoGarageDoor(GarageDoorDevice): - """Provides a demo garage door.""" - - def __init__(self, name, state): - """Initialize the garage door.""" - self._name = name - self._state = state - - @property - def should_poll(self): - """No polling needed for a demo garage door.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_closed(self): - """Return true if garage door is closed.""" - return self._state == STATE_CLOSED - - def close_door(self, **kwargs): - """Close the garage door.""" - self._state = STATE_CLOSED - self.update_ha_state() - - def open_door(self, **kwargs): - """Open the garage door.""" - self._state = STATE_OPEN - self.update_ha_state() diff --git a/homeassistant/components/garage_door/mqtt.py b/homeassistant/components/garage_door/mqtt.py deleted file mode 100644 index 8fa6a110be8..00000000000 --- a/homeassistant/components/garage_door/mqtt.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Support for MQTT garage doors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/garage_door.mqtt/ -""" -import logging - -import voluptuous as vol -from homeassistant.const import (STATE_OPEN, STATE_CLOSED, SERVICE_OPEN, - SERVICE_CLOSE) -import homeassistant.components.mqtt as mqtt -from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.const import ( - CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) -from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_STATE_OPEN = 'state_open' -CONF_STATE_CLOSED = 'state_closed' -CONF_SERVICE_OPEN = 'service_open' -CONF_SERVICE_CLOSE = 'service_close' - -DEFAULT_NAME = 'MQTT Garage Door' -DEFAULT_OPTIMISTIC = False -DEFAULT_RETAIN = False - -DEPENDENCIES = ['mqtt'] - -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, - vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, - vol.Optional(CONF_SERVICE_OPEN, default=SERVICE_OPEN): cv.string, - vol.Optional(CONF_SERVICE_CLOSE, default=SERVICE_CLOSE): cv.string -}) - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Add MQTT Garage Door.""" - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - add_devices_callback([MqttGarageDoor( - hass, - config[CONF_NAME], - config.get(CONF_STATE_TOPIC), - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_RETAIN], - config[CONF_STATE_OPEN], - config[CONF_STATE_CLOSED], - config[CONF_SERVICE_OPEN], - config[CONF_SERVICE_CLOSE], - config[CONF_OPTIMISTIC], - value_template)]) - - -# pylint: disable=too-many-arguments, too-many-instance-attributes -class MqttGarageDoor(GarageDoorDevice): - """Representation of a MQTT garage door.""" - - def __init__(self, hass, name, state_topic, command_topic, qos, retain, - state_open, state_closed, service_open, service_close, - optimistic, value_template): - """Initialize the garage door.""" - self._hass = hass - self._name = name - self._state_topic = state_topic - self._command_topic = command_topic - self._qos = qos - self._retain = retain - self._state_open = state_open - self._state_closed = state_closed - self._service_open = service_open - self._service_close = service_close - self._optimistic = optimistic or state_topic is None - self._state = False - - def message_received(topic, payload, qos): - """A new MQTT message has been received.""" - if value_template is not None: - payload = value_template.render_with_possible_json_value( - payload) - if payload == self._state_open: - self._state = True - self.update_ha_state() - elif payload == self._state_closed: - self._state = False - self.update_ha_state() - - if self._state_topic is None: - # Force into optimistic mode. - self._optimistic = True - else: - mqtt.subscribe(hass, self._state_topic, message_received, - self._qos) - - @property - def name(self): - """Return the name of the garage door if any.""" - return self._name - - @property - def is_opened(self): - """Return true if door is closed.""" - return self._state - - @property - def is_closed(self): - """Return true if door is closed.""" - return self._state is False - - @property - def assumed_state(self): - """Return true if we do optimistic updates.""" - return self._optimistic - - def close_door(self): - """Close the door.""" - mqtt.publish(self.hass, self._command_topic, self._service_close, - self._qos, self._retain) - if self._optimistic: - # Optimistically assume that door has changed state. - self._state = False - self.update_ha_state() - - def open_door(self): - """Open the door.""" - mqtt.publish(self.hass, self._command_topic, self._service_open, - self._qos, self._retain) - if self._optimistic: - # Optimistically assume that door has changed state. - self._state = True - self.update_ha_state() diff --git a/homeassistant/components/garage_door/rpi_gpio.py b/homeassistant/components/garage_door/rpi_gpio.py deleted file mode 100644 index 3969e12371c..00000000000 --- a/homeassistant/components/garage_door/rpi_gpio.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Support for building a Raspberry Pi garage controller in HA. - -Instructions for building the controller can be found here -https://github.com/andrewshilliday/garage-door-controller - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/garage_door.rpi_gpio/ -""" - -import logging -from time import sleep -import voluptuous as vol -from homeassistant.components.garage_door import GarageDoorDevice -import homeassistant.components.rpi_gpio as rpi_gpio -import homeassistant.helpers.config_validation as cv - -RELAY_TIME = 'relay_time' -STATE_PULL_MODE = 'state_pull_mode' -DEFAULT_PULL_MODE = 'UP' -DEFAULT_RELAY_TIME = .2 -DEPENDENCIES = ['rpi_gpio'] - -_LOGGER = logging.getLogger(__name__) - -_DOORS_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema({ - 'name': str, - 'relay_pin': int, - 'state_pin': int, - }) - ] -) -PLATFORM_SCHEMA = vol.Schema({ - 'platform': str, - vol.Required('doors'): _DOORS_SCHEMA, - vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, - vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int), -}) - - -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the garage door platform.""" - relay_time = config.get(RELAY_TIME) - state_pull_mode = config.get(STATE_PULL_MODE) - doors = [] - doors_conf = config.get('doors') - - for door in doors_conf: - doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'], - door['state_pin'], - state_pull_mode, - relay_time)) - add_devices(doors) - - -class RPiGPIOGarageDoor(GarageDoorDevice): - """Representation of a Raspberry garage door.""" - - # pylint: disable=too-many-arguments - def __init__(self, name, relay_pin, state_pin, - state_pull_mode, relay_time): - """Initialize the garage door.""" - self._name = name - self._state = False - self._relay_pin = relay_pin - self._state_pin = state_pin - self._state_pull_mode = state_pull_mode - self._relay_time = relay_time - rpi_gpio.setup_output(self._relay_pin) - rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) - rpi_gpio.write_output(self._relay_pin, True) - - @property - def unique_id(self): - """Return the ID of this garage door.""" - return "{}.{}".format(self.__class__, self._name) - - @property - def name(self): - """Return the name of the garage door if any.""" - return self._name - - def update(self): - """Update the state of the garage door.""" - self._state = rpi_gpio.read_input(self._state_pin) - - @property - def is_closed(self): - """Return true if door is closed.""" - return self._state - - def _trigger(self): - """Trigger the door.""" - rpi_gpio.write_output(self._relay_pin, False) - sleep(self._relay_time) - rpi_gpio.write_output(self._relay_pin, True) - - def close_door(self): - """Close the door.""" - if not self.is_closed: - self._trigger() - - def open_door(self): - """Open the door.""" - if self.is_closed: - self._trigger() diff --git a/homeassistant/components/garage_door/services.yaml b/homeassistant/components/garage_door/services.yaml deleted file mode 100644 index a73c05ce24e..00000000000 --- a/homeassistant/components/garage_door/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -open: - description: Open all or specified garage door - - fields: - entity_id: - description: Name(s) of garage door(s) to open - example: 'garage.main' - -close: - description: Close all or a specified garage door - - fields: - entity_id: - description: Name(s) of garage door(s) to close - example: 'garage.main' diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py deleted file mode 100644 index c1436d7556a..00000000000 --- a/homeassistant/components/garage_door/wink.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Support for Wink garage doors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/garage_door.wink/ -""" - -from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.components.wink import WinkDevice - -DEPENDENCIES = ['wink'] - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Wink garage door platform.""" - import pywink - - add_devices(WinkGarageDoorDevice(door) for door in - pywink.get_garage_doors()) - - -class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice): - """Representation of a Wink garage door.""" - - def __init__(self, wink): - """Initialize the garage door.""" - WinkDevice.__init__(self, wink) - - @property - def is_closed(self): - """Return true if door is closed.""" - return self.wink.state() == 0 - - def close_door(self): - """Close the door.""" - self.wink.set_state(0) - - def open_door(self): - """Open the door.""" - self.wink.set_state(1) diff --git a/homeassistant/components/garage_door/zwave.py b/homeassistant/components/garage_door/zwave.py deleted file mode 100644 index b180dd76e46..00000000000 --- a/homeassistant/components/garage_door/zwave.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Support for Zwave garage door components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/garagedoor.zwave/ -""" -# Because we do not compile openzwave on CI -# pylint: disable=import-error -import logging -from homeassistant.components.garage_door import DOMAIN -from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.components import zwave -from homeassistant.components.garage_door import GarageDoorDevice - -COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 -COMMAND_CLASS_BARRIER_OPERATOR = 0x66 # 102 -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and return Z-Wave garage door device.""" - if discovery_info is None or zwave.NETWORK is None: - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - - if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_BINARY and \ - value.command_class != zwave.const.COMMAND_CLASS_BARRIER_OPERATOR: - return - if value.type != zwave.const.TYPE_BOOL: - return - if value.genre != zwave.const.GENRE_USER: - return - - value.set_change_verified(False) - add_devices([ZwaveGarageDoor(value)]) - - -class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): - """Representation of an Zwave garage door device.""" - - def __init__(self, value): - """Initialize the zwave garage door.""" - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._state = value.data - dispatcher.connect( - self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - - def value_changed(self, value): - """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: - self._state = value.data - self.update_ha_state() - _LOGGER.debug("Value changed on network %s", value) - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return not self._state - - def close_door(self): - """Close the garage door.""" - self._value.data = False - - def open_door(self): - """Open the garage door.""" - self._value.data = True diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py deleted file mode 100644 index ab27af480a7..00000000000 --- a/homeassistant/components/hvac/__init__.py +++ /dev/null @@ -1,500 +0,0 @@ -""" -Provides functionality to interact with hvacs. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hvac/ -""" -import logging -import os -from numbers import Number - -from homeassistant.helpers.entity_component import EntityComponent - -from homeassistant.config import load_yaml_config_file -import homeassistant.util as util -from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, - TEMP_CELSIUS) - -DOMAIN = "hvac" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = 60 - -SERVICE_SET_AWAY_MODE = "set_away_mode" -SERVICE_SET_AUX_HEAT = "set_aux_heat" -SERVICE_SET_TEMPERATURE = "set_temperature" -SERVICE_SET_FAN_MODE = "set_fan_mode" -SERVICE_SET_OPERATION_MODE = "set_operation_mode" -SERVICE_SET_SWING_MODE = "set_swing_mode" -SERVICE_SET_HUMIDITY = "set_humidity" - -STATE_HEAT = "heat" -STATE_COOL = "cool" -STATE_IDLE = "idle" -STATE_AUTO = "auto" -STATE_DRY = "dry" -STATE_FAN_ONLY = "fan_only" - -ATTR_CURRENT_TEMPERATURE = "current_temperature" -ATTR_MAX_TEMP = "max_temp" -ATTR_MIN_TEMP = "min_temp" -ATTR_AWAY_MODE = "away_mode" -ATTR_AUX_HEAT = "aux_heat" -ATTR_FAN_MODE = "fan_mode" -ATTR_FAN_LIST = "fan_list" -ATTR_CURRENT_HUMIDITY = "current_humidity" -ATTR_HUMIDITY = "humidity" -ATTR_MAX_HUMIDITY = "max_humidity" -ATTR_MIN_HUMIDITY = "min_humidity" -ATTR_OPERATION_MODE = "operation_mode" -ATTR_OPERATION_LIST = "operation_list" -ATTR_SWING_MODE = "swing_mode" -ATTR_SWING_LIST = "swing_list" - -_LOGGER = logging.getLogger(__name__) - - -def set_away_mode(hass, away_mode, entity_id=None): - """Turn all or specified hvac away mode on.""" - data = { - ATTR_AWAY_MODE: away_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) - - -def set_aux_heat(hass, aux_heat, entity_id=None): - """Turn all or specified hvac auxillary heater on.""" - data = { - ATTR_AUX_HEAT: aux_heat - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) - - -def set_temperature(hass, temperature, entity_id=None): - """Set new target temperature.""" - data = {ATTR_TEMPERATURE: temperature} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) - - -def set_humidity(hass, humidity, entity_id=None): - """Set new target humidity.""" - data = {ATTR_HUMIDITY: humidity} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) - - -def set_fan_mode(hass, fan, entity_id=None): - """Turn all or specified hvac fan mode on.""" - data = {ATTR_FAN_MODE: fan} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) - - -def set_operation_mode(hass, operation_mode, entity_id=None): - """Set new target operation mode.""" - data = {ATTR_OPERATION_MODE: operation_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) - - -def set_swing_mode(hass, swing_mode, entity_id=None): - """Set new target swing mode.""" - data = {ATTR_SWING_MODE: swing_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) - - -# pylint: disable=too-many-branches -def setup(hass, config): - """Setup hvacs.""" - _LOGGER.warning('This component has been deprecated in favour of' - ' the "climate" component and will be removed ' - 'in the future. Please upgrade.') - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - component.setup(config) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - def away_mode_set_service(service): - """Set away mode on target hvacs.""" - target_hvacs = component.extract_from_service(service) - - away_mode = service.data.get(ATTR_AWAY_MODE) - - if away_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) - return - - for hvac in target_hvacs: - if away_mode: - hvac.turn_away_mode_on() - else: - hvac.turn_away_mode_off() - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, - descriptions.get(SERVICE_SET_AWAY_MODE)) - - def aux_heat_set_service(service): - """Set auxillary heater on target hvacs.""" - target_hvacs = component.extract_from_service(service) - - aux_heat = service.data.get(ATTR_AUX_HEAT) - - if aux_heat is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT) - return - - for hvac in target_hvacs: - if aux_heat: - hvac.turn_aux_heat_on() - else: - hvac.turn_aux_heat_off() - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service, - descriptions.get(SERVICE_SET_AUX_HEAT)) - - def temperature_set_service(service): - """Set temperature on the target hvacs.""" - target_hvacs = component.extract_from_service(service) - - temperature = util.convert( - service.data.get(ATTR_TEMPERATURE), float) - - if temperature is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE) - return - - for hvac in target_hvacs: - hvac.set_temperature(convert_temperature( - temperature, hass.config.units.temperature_unit, - hvac.unit_of_measurement)) - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, - descriptions.get(SERVICE_SET_TEMPERATURE)) - - def humidity_set_service(service): - """Set humidity on the target hvacs.""" - target_hvacs = component.extract_from_service(service) - - humidity = service.data.get(ATTR_HUMIDITY) - - if humidity is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_HUMIDITY, ATTR_HUMIDITY) - return - - for hvac in target_hvacs: - hvac.set_humidity(humidity) - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service, - descriptions.get(SERVICE_SET_HUMIDITY)) - - def fan_mode_set_service(service): - """Set fan mode on target hvacs.""" - target_hvacs = component.extract_from_service(service) - - fan = service.data.get(ATTR_FAN_MODE) - - if fan is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_FAN_MODE, ATTR_FAN_MODE) - return - - for hvac in target_hvacs: - hvac.set_fan_mode(fan) - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, - descriptions.get(SERVICE_SET_FAN_MODE)) - - def operation_set_service(service): - """Set operating mode on the target hvacs.""" - target_hvacs = component.extract_from_service(service) - - operation_mode = service.data.get(ATTR_OPERATION_MODE) - - if operation_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE) - return - - for hvac in target_hvacs: - hvac.set_operation_mode(operation_mode) - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service, - descriptions.get(SERVICE_SET_OPERATION_MODE)) - - def swing_set_service(service): - """Set swing mode on the target hvacs.""" - target_hvacs = component.extract_from_service(service) - - swing_mode = service.data.get(ATTR_SWING_MODE) - - if swing_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_SWING_MODE, ATTR_SWING_MODE) - return - - for hvac in target_hvacs: - hvac.set_swing_mode(swing_mode) - - if hvac.should_poll: - hvac.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service, - descriptions.get(SERVICE_SET_SWING_MODE)) - return True - - -class HvacDevice(Entity): - """Representation of a hvac.""" - - # pylint: disable=too-many-public-methods,no-self-use - @property - def state(self): - """Return the current state.""" - return self.current_operation or STATE_UNKNOWN - - @property - def state_attributes(self): - """Return the optional state attributes.""" - data = { - ATTR_CURRENT_TEMPERATURE: - self._convert_for_display(self.current_temperature), - ATTR_MIN_TEMP: self._convert_for_display(self.min_temp), - ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), - ATTR_TEMPERATURE: - self._convert_for_display(self.target_temperature), - } - - humidity = self.target_humidity - if humidity is not None: - data[ATTR_HUMIDITY] = humidity - data[ATTR_CURRENT_HUMIDITY] = self.current_humidity - data[ATTR_MIN_HUMIDITY] = self.min_humidity - data[ATTR_MAX_HUMIDITY] = self.max_humidity - - fan_mode = self.current_fan_mode - if fan_mode is not None: - data[ATTR_FAN_MODE] = fan_mode - data[ATTR_FAN_LIST] = self.fan_list - - operation_mode = self.current_operation - if operation_mode is not None: - data[ATTR_OPERATION_MODE] = operation_mode - data[ATTR_OPERATION_LIST] = self.operation_list - - swing_mode = self.current_swing_mode - if swing_mode is not None: - data[ATTR_SWING_MODE] = swing_mode - data[ATTR_SWING_LIST] = self.swing_list - - is_away = self.is_away_mode_on - if is_away is not None: - data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF - - is_aux_heat = self.is_aux_heat_on - if is_aux_heat is not None: - data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF - - return data - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - raise NotImplementedError - - @property - def current_humidity(self): - """Return the current humidity.""" - return None - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - return None - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return None - - @property - def operation_list(self): - """List of available operation modes.""" - return None - - @property - def current_temperature(self): - """Return the current temperature.""" - return None - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - raise NotImplementedError - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return None - - @property - def is_aux_heat_on(self): - """Return true if away mode is on.""" - return None - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return None - - @property - def fan_list(self): - """List of available fan modes.""" - return None - - @property - def current_swing_mode(self): - """Return the fan setting.""" - return None - - @property - def swing_list(self): - """List of available swing modes.""" - return None - - def set_temperature(self, temperature): - """Set new target temperature.""" - raise NotImplementedError() - - def set_humidity(self, humidity): - """Set new target humidity.""" - raise NotImplementedError() - - def set_fan_mode(self, fan): - """Set new target fan mode.""" - raise NotImplementedError() - - def set_operation_mode(self, operation_mode): - """Set new target operation mode.""" - raise NotImplementedError() - - def set_swing_mode(self, swing_mode): - """Set new target swing operation.""" - raise NotImplementedError() - - def turn_away_mode_on(self): - """Turn away mode on.""" - raise NotImplementedError() - - def turn_away_mode_off(self): - """Turn away mode off.""" - raise NotImplementedError() - - def turn_aux_heat_on(self): - """Turn auxillary heater on.""" - raise NotImplementedError() - - def turn_aux_heat_off(self): - """Turn auxillary heater off.""" - raise NotImplementedError() - - @property - def min_temp(self): - """Return the minimum temperature.""" - return convert_temperature(19, TEMP_CELSIUS, self.unit_of_measurement) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return convert_temperature(30, TEMP_CELSIUS, self.unit_of_measurement) - - @property - def min_humidity(self): - """Return the minimum humidity.""" - return 30 - - @property - def max_humidity(self): - """Return the maximum humidity.""" - return 99 - - def _convert_for_display(self, temp): - """Convert temperature into preferred units for display purposes.""" - if temp is None or not isinstance(temp, Number): - return temp - - value = convert_temperature(temp, self.unit_of_measurement, - self.hass.config.units.temperature_unit) - - if self.hass.config.units.temperature_unit is TEMP_CELSIUS: - decimal_count = 1 - else: - # Users of fahrenheit generally expect integer units. - decimal_count = 0 - - return round(value, decimal_count) diff --git a/homeassistant/components/hvac/demo.py b/homeassistant/components/hvac/demo.py deleted file mode 100644 index 9e4f2c15d29..00000000000 --- a/homeassistant/components/hvac/demo.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Demo platform that offers a fake hvac. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" -from homeassistant.components.hvac import HvacDevice -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Demo hvacs.""" - add_devices([ - DemoHvac("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", - None, None, "Auto", "Heat", None), - DemoHvac("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", - 67, 54, "Off", "Cool", False), - ]) - - -# pylint: disable=too-many-arguments, too-many-public-methods -class DemoHvac(HvacDevice): - """Representation of a demo hvac.""" - - # pylint: disable=too-many-instance-attributes - def __init__(self, name, target_temperature, unit_of_measurement, - away, current_temperature, current_fan_mode, - target_humidity, current_humidity, current_swing_mode, - current_operation, aux): - """Initialize the hvac.""" - self._name = name - self._target_temperature = target_temperature - self._target_humidity = target_humidity - self._unit_of_measurement = unit_of_measurement - self._away = away - self._current_temperature = current_temperature - self._current_humidity = current_humidity - self._current_fan_mode = current_fan_mode - self._current_operation = current_operation - self._aux = aux - self._current_swing_mode = current_swing_mode - self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"] - self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"] - self._swing_list = ["Auto", 1, 2, 3, "Off"] - - @property - def should_poll(self): - """Polling not needed for a demo hvac.""" - return False - - @property - def name(self): - """Return the name of the hvac.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._current_humidity - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - return self._target_humidity - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation - - @property - def operation_list(self): - """List of available operation modes.""" - return self._operation_list - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self._away - - @property - def is_aux_heat_on(self): - """Return true if away mode is on.""" - return self._aux - - @property - def current_fan_mode(self): - """Return the fan setting.""" - return self._current_fan_mode - - @property - def fan_list(self): - """List of available fan modes.""" - return self._fan_list - - def set_temperature(self, temperature): - """Set new target temperature.""" - self._target_temperature = temperature - self.update_ha_state() - - def set_humidity(self, humidity): - """Set new target temperature.""" - self._target_humidity = humidity - self.update_ha_state() - - def set_swing_mode(self, swing_mode): - """Set new target temperature.""" - self._current_swing_mode = swing_mode - self.update_ha_state() - - def set_fan_mode(self, fan): - """Set new target temperature.""" - self._current_fan_mode = fan - self.update_ha_state() - - def set_operation_mode(self, operation_mode): - """Set new target temperature.""" - self._current_operation = operation_mode - self.update_ha_state() - - @property - def current_swing_mode(self): - """Return the swing setting.""" - return self._current_swing_mode - - @property - def swing_list(self): - """List of available swing modes.""" - return self._swing_list - - def turn_away_mode_on(self): - """Turn away mode on.""" - self._away = True - self.update_ha_state() - - def turn_away_mode_off(self): - """Turn away mode off.""" - self._away = False - self.update_ha_state() - - def turn_aux_heat_on(self): - """Turn away auxillary heater on.""" - self._aux = True - self.update_ha_state() - - def turn_aux_heat_off(self): - """Turn auxillary heater off.""" - self._aux = False - self.update_ha_state() diff --git a/homeassistant/components/hvac/services.yaml b/homeassistant/components/hvac/services.yaml deleted file mode 100644 index 5d9f7463399..00000000000 --- a/homeassistant/components/hvac/services.yaml +++ /dev/null @@ -1,84 +0,0 @@ -set_aux_heat: - description: Turn auxillary heater on/off for hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.kitchen' - - aux_heat: - description: New value of axillary heater - example: true - -set_away_mode: - description: Turn away mode on/off for hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.kitchen' - - away_mode: - description: New value of away mode - example: true - -set_temperature: - description: Set target temperature of hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.kitchen' - - temperature: - description: New target temperature for hvac - example: 25 - -set_humidity: - description: Set target humidity of hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.kitchen' - - humidity: - description: New target humidity for hvac - example: 60 - -set_fan_mode: - description: Set fan operation for hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.nest' - - fan: - description: New value of fan mode - example: On Low - -set_operation_mode: - description: Set operation mode for hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.nest' - - operation_mode: - description: New value of operation mode - example: Heat - - -set_swing_mode: - description: Set swing operation for hvac - - fields: - entity_id: - description: Name(s) of entities to change - example: 'hvac.nest' - - swing_mode: - description: New value of swing mode - example: 1 diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py deleted file mode 100755 index 5415fe0b41c..00000000000 --- a/homeassistant/components/hvac/zwave.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -Support for ZWave HVAC devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hvac.zwave/ -""" -# Because we do not compile openzwave on CI -# pylint: disable=import-error -import logging -from homeassistant.components.hvac import DOMAIN -from homeassistant.components.hvac import HvacDevice -from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.components import zwave -from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS) - -_LOGGER = logging.getLogger(__name__) - -CONF_NAME = 'name' -DEFAULT_NAME = 'ZWave Hvac' - -REMOTEC = 0x5254 -REMOTEC_ZXT_120 = 0x8377 -REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) - -WORKAROUND_ZXT_120 = 'zxt_120' - -DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 -} - -ZXT_120_SET_TEMP = { - 'Heat': 1, - 'Cool': 2, - 'Dry Air': 8, - 'Auto Changeover': 10 -} - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the ZWave Hvac devices.""" - if discovery_info is None or zwave.NETWORK is None: - _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", - discovery_info, zwave.NETWORK) - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - value.set_change_verified(False) - add_devices([ZWaveHvac(value)]) - _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", - discovery_info, zwave.NETWORK) - - -# pylint: disable=too-many-arguments, abstract-method -class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): - """Represents a HeatControl hvac.""" - - # pylint: disable=too-many-public-methods, too-many-instance-attributes - def __init__(self, value): - """Initialize the zwave hvac.""" - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._node = value.node - self._target_temperature = None - self._current_temperature = None - self._current_operation = None - self._operation_list = None - self._current_operation_state = None - self._current_fan_mode = None - self._fan_list = None - self._current_swing_mode = None - self._swing_list = None - self._unit = None - self._zxt_120 = None - self.update_properties() - # register listener - dispatcher.connect( - self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - # Make sure that we have values for the key before converting to int - if (value.node.manufacturer_id.strip() and - value.node.product_id.strip()): - specific_sensor_key = (int(value.node.manufacturer_id, 16), - int(value.node.product_id, 16), - value.index) - - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120: - _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat as HVAC") - self._zxt_120 = 1 - - def value_changed(self, value): - """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id or \ - self._value.node == value.node: - self.update_properties() - self.update_ha_state() - _LOGGER.debug("Value changed on network %s", value) - - def update_properties(self): - """Callback on data change for the registered node/value pair.""" - # Set point - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) - .values()): - if int(value.data) != 0: - self._target_temperature = int(value.data) - # Operation Mode - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE) - .values()): - self._current_operation = value.data - self._operation_list = list(value.data_items) - _LOGGER.debug("self._operation_list=%s", self._operation_list) - # Current Temp - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) - .values()): - if value.label == 'Temperature': - self._current_temperature = int(value.data) - self._unit = value.units - # Fan Mode - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE) - .values()): - self._current_operation_state = value.data - self._fan_list = list(value.data_items) - _LOGGER.debug("self._fan_list=%s", self._fan_list) - _LOGGER.debug("self._current_operation_state=%s", - self._current_operation_state) - # Swing mode - if self._zxt_120 == 1: - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) - .values()): - if value.command_class == 112 and value.index == 33: - self._current_swing_mode = value.data - self._swing_list = list(value.data_items) - _LOGGER.debug("self._swing_list=%s", self._swing_list) - - @property - def should_poll(self): - """No polling on ZWave.""" - return False - - @property - def current_fan_mode(self): - """Return the fan speed set.""" - return self._current_operation_state - - @property - def fan_list(self): - """List of available fan modes.""" - return self._fan_list - - @property - def current_swing_mode(self): - """Return the swing mode set.""" - return self._current_swing_mode - - @property - def swing_list(self): - """List of available swing modes.""" - return self._swing_list - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - unit = self._unit - if unit == 'C': - return TEMP_CELSIUS - elif unit == 'F': - return TEMP_FAHRENHEIT - else: - _LOGGER.exception("unit_of_measurement=%s is not valid", - unit) - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def current_operation(self): - """Return the current operation mode.""" - return self._current_operation - - @property - def operation_list(self): - """List of available operation modes.""" - return self._operation_list - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def set_temperature(self, temperature): - """Set new target temperature.""" - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) - .values()): - if value.command_class != 67: - continue - if self._zxt_120: - # ZXT-120 does not support get setpoint - self._target_temperature = temperature - if ZXT_120_SET_TEMP.get(self._current_operation) \ - != value.index: - continue - # ZXT-120 responds only to whole int - value.data = int(round(temperature, 0)) - else: - value.data = int(temperature) - break - - def set_fan_mode(self, fan): - """Set new target fan mode.""" - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE) - .values()): - if value.command_class == 68 and value.index == 0: - value.data = bytes(fan, 'utf-8') - break - - def set_operation_mode(self, operation_mode): - """Set new target operation mode.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): - if value.command_class == 64 and value.index == 0: - value.data = bytes(operation_mode, 'utf-8') - break - - def set_swing_mode(self, swing_mode): - """Set new target swing mode.""" - if self._zxt_120 == 1: - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values(): - if value.command_class == 112 and value.index == 33: - value.data = bytes(swing_mode, 'utf-8') - break diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py deleted file mode 100644 index 3928eb384d8..00000000000 --- a/homeassistant/components/rollershutter/__init__.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Support for Roller shutters. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter/ -""" -import os -import logging - -import voluptuous as vol - -from homeassistant.config import load_yaml_config_file -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -import homeassistant.helpers.config_validation as cv -from homeassistant.components import group -from homeassistant.const import ( - SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_MOVE_POSITION, SERVICE_STOP, - STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID) - - -DOMAIN = 'rollershutter' -SCAN_INTERVAL = 15 - -GROUP_NAME_ALL_ROLLERSHUTTERS = 'all rollershutters' -ENTITY_ID_ALL_ROLLERSHUTTERS = group.ENTITY_ID_FORMAT.format( - 'all_rollershutters') - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -_LOGGER = logging.getLogger(__name__) - -ATTR_CURRENT_POSITION = 'current_position' -ATTR_POSITION = 'position' - -ROLLERSHUTTER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -ROLLERSHUTTER_MOVE_POSITION_SCHEMA = ROLLERSHUTTER_SERVICE_SCHEMA.extend({ - vol.Required(ATTR_POSITION): - vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), -}) - - -def is_open(hass, entity_id=None): - """Return if the roller shutter is open based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_ROLLERSHUTTERS - return hass.states.is_state(entity_id, STATE_OPEN) - - -def move_up(hass, entity_id=None): - """Move up all or specified roller shutter.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_MOVE_UP, data) - - -def move_down(hass, entity_id=None): - """Move down all or specified roller shutter.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_MOVE_DOWN, data) - - -def move_position(hass, position, entity_id=None): - """Move to specific position all or specified roller shutter.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_POSITION] = position - hass.services.call(DOMAIN, SERVICE_MOVE_POSITION, data) - - -def stop(hass, entity_id=None): - """Stop all or specified roller shutter.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP, data) - - -def setup(hass, config): - """Track states and offer events for roller shutters.""" - _LOGGER.warning('This component has been deprecated in favour of the ' - '"cover" component and will be removed in the future.' - ' Please upgrade.') - component = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_ROLLERSHUTTERS) - component.setup(config) - - def handle_rollershutter_service(service): - """Handle calls to the roller shutter services.""" - target_rollershutters = component.extract_from_service(service) - - for rollershutter in target_rollershutters: - if service.service == SERVICE_MOVE_UP: - rollershutter.move_up() - elif service.service == SERVICE_MOVE_DOWN: - rollershutter.move_down() - elif service.service == SERVICE_MOVE_POSITION: - rollershutter.move_position(service.data[ATTR_POSITION]) - elif service.service == SERVICE_STOP: - rollershutter.stop() - - if rollershutter.should_poll: - rollershutter.update_ha_state(True) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - hass.services.register(DOMAIN, SERVICE_MOVE_UP, - handle_rollershutter_service, - descriptions.get(SERVICE_MOVE_UP), - schema=ROLLERSHUTTER_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_MOVE_DOWN, - handle_rollershutter_service, - descriptions.get(SERVICE_MOVE_DOWN), - schema=ROLLERSHUTTER_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_MOVE_POSITION, - handle_rollershutter_service, - descriptions.get(SERVICE_MOVE_POSITION), - schema=ROLLERSHUTTER_MOVE_POSITION_SCHEMA) - hass.services.register(DOMAIN, SERVICE_STOP, - handle_rollershutter_service, - descriptions.get(SERVICE_STOP), - schema=ROLLERSHUTTER_SERVICE_SCHEMA) - return True - - -class RollershutterDevice(Entity): - """Representation a rollers hutter.""" - - # pylint: disable=no-self-use - @property - def current_position(self): - """Return current position of roller shutter. - - None is unknown, 0 is closed, 100 is fully open. - """ - raise NotImplementedError() - - @property - def state(self): - """Return the state of the roller shutter.""" - current = self.current_position - - if current is None: - return STATE_UNKNOWN - - return STATE_CLOSED if current == 0 else STATE_OPEN - - @property - def state_attributes(self): - """Return the state attributes.""" - current = self.current_position - - if current is None: - return None - - return { - ATTR_CURRENT_POSITION: current - } - - def move_up(self, **kwargs): - """Move the roller shutter down.""" - raise NotImplementedError() - - def move_down(self, **kwargs): - """Move the roller shutter up.""" - raise NotImplementedError() - - def move_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - raise NotImplementedError() - - def stop(self, **kwargs): - """Stop the roller shutter.""" - raise NotImplementedError() diff --git a/homeassistant/components/rollershutter/command_line.py b/homeassistant/components/rollershutter/command_line.py deleted file mode 100644 index 8ee88ae9ce5..00000000000 --- a/homeassistant/components/rollershutter/command_line.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Support for command roller shutters. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.command_line/ -""" -import logging -import subprocess - -from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.helpers.template import Template - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup roller shutter controlled by shell commands.""" - rollershutters = config.get('rollershutters', {}) - devices = [] - - for dev_name, properties in rollershutters.items(): - value_template = properties.get(CONF_VALUE_TEMPLATE) - - if value_template is not None: - value_template = Template(value_template, hass) - - devices.append( - CommandRollershutter( - hass, - properties.get('name', dev_name), - properties.get('upcmd', 'true'), - properties.get('downcmd', 'true'), - properties.get('stopcmd', 'true'), - properties.get('statecmd', False), - value_template)) - add_devices_callback(devices) - - -# pylint: disable=abstract-method -# pylint: disable=too-many-arguments, too-many-instance-attributes -class CommandRollershutter(RollershutterDevice): - """Representation a command line roller shutter.""" - - # pylint: disable=too-many-arguments - def __init__(self, hass, name, command_up, command_down, command_stop, - command_state, value_template): - """Initialize the roller shutter.""" - self._hass = hass - self._name = name - self._state = None - self._command_up = command_up - self._command_down = command_down - self._command_stop = command_stop - self._command_state = command_state - self._value_template = value_template - - @staticmethod - def _move_rollershutter(command): - """Execute the actual commands.""" - _LOGGER.info('Running command: %s', command) - - success = (subprocess.call(command, shell=True) == 0) - - if not success: - _LOGGER.error('Command failed: %s', command) - - return success - - @staticmethod - def _query_state_value(command): - """Execute state command for return value.""" - _LOGGER.info('Running state command: %s', command) - - try: - return_value = subprocess.check_output(command, shell=True) - return return_value.strip().decode('utf-8') - except subprocess.CalledProcessError: - _LOGGER.error('Command failed: %s', command) - - @property - def should_poll(self): - """Only poll if we have state command.""" - return self._command_state is not None - - @property - def name(self): - """Return the name of the roller shutter.""" - return self._name - - @property - def current_position(self): - """Return current position of roller shutter. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._state - - def _query_state(self): - """Query for the state.""" - if not self._command_state: - _LOGGER.error('No state command specified') - return - return self._query_state_value(self._command_state) - - def update(self): - """Update device state.""" - if self._command_state: - payload = str(self._query_state()) - if self._value_template: - payload = self._value_template.render_with_possible_json_value( - payload) - self._state = int(payload) - - def move_up(self, **kwargs): - """Move the roller shutter up.""" - self._move_rollershutter(self._command_up) - - def move_down(self, **kwargs): - """Move the roller shutter down.""" - self._move_rollershutter(self._command_down) - - def stop(self, **kwargs): - """Stop the device.""" - self._move_rollershutter(self._command_stop) diff --git a/homeassistant/components/rollershutter/demo.py b/homeassistant/components/rollershutter/demo.py deleted file mode 100644 index 6799d062e43..00000000000 --- a/homeassistant/components/rollershutter/demo.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Demo platform for the rollor shutter component. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" -from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.helpers.event import track_utc_time_change - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Demo roller shutters.""" - add_devices([ - DemoRollershutter(hass, 'Kitchen Window', 0), - DemoRollershutter(hass, 'Living Room Window', 100), - ]) - - -class DemoRollershutter(RollershutterDevice): - """Representation of a demo roller shutter.""" - - # pylint: disable=no-self-use - def __init__(self, hass, name, position): - """Initialize the roller shutter.""" - self.hass = hass - self._name = name - self._position = position - self._moving_up = True - self._unsub_listener = None - - @property - def name(self): - """Return the name of the roller shutter.""" - return self._name - - @property - def should_poll(self): - """No polling needed for a demo roller shutter.""" - return False - - @property - def current_position(self): - """Return the current position of the roller shutter.""" - return self._position - - def move_up(self, **kwargs): - """Move the roller shutter down.""" - if self._position == 0: - return - - self._listen() - self._moving_up = True - - def move_down(self, **kwargs): - """Move the roller shutter up.""" - if self._position == 100: - return - - self._listen() - self._moving_up = False - - def move_position(self, position, **kwargs): - """Move the roller shutter to a specific position.""" - if self._position == position: - return - - self._listen() - self._moving_up = position < self._position - - def stop(self, **kwargs): - """Stop the roller shutter.""" - if self._unsub_listener is not None: - self._unsub_listener() - self._unsub_listener = None - - def _listen(self): - """Listen for changes.""" - if self._unsub_listener is None: - self._unsub_listener = track_utc_time_change(self.hass, - self._time_changed) - - def _time_changed(self, now): - """Track time changes.""" - if self._moving_up: - self._position -= 10 - else: - self._position += 10 - - if self._position % 100 == 0: - self.stop() - - self.update_ha_state() diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py deleted file mode 100644 index aa0839ff094..00000000000 --- a/homeassistant/components/rollershutter/mqtt.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Support for MQTT roller shutters. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.mqtt/ -""" -import logging - -import voluptuous as vol - -import homeassistant.components.mqtt as mqtt -from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE -from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['mqtt'] - -CONF_PAYLOAD_UP = 'payload_up' -CONF_PAYLOAD_DOWN = 'payload_down' -CONF_PAYLOAD_STOP = 'payload_stop' - -DEFAULT_NAME = "MQTT Rollershutter" -DEFAULT_PAYLOAD_UP = "UP" -DEFAULT_PAYLOAD_DOWN = "DOWN" -DEFAULT_PAYLOAD_STOP = "STOP" - -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PAYLOAD_UP, default=DEFAULT_PAYLOAD_UP): cv.string, - vol.Optional(CONF_PAYLOAD_DOWN, default=DEFAULT_PAYLOAD_DOWN): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, -}) - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Add MQTT Rollershutter.""" - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - add_devices_callback([MqttRollershutter( - hass, - config[CONF_NAME], - config.get(CONF_STATE_TOPIC), - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_PAYLOAD_UP], - config[CONF_PAYLOAD_DOWN], - config[CONF_PAYLOAD_STOP], - value_template, - )]) - - -# pylint: disable=abstract-method -# pylint: disable=too-many-arguments, too-many-instance-attributes -class MqttRollershutter(RollershutterDevice): - """Representation of a roller shutter that can be controlled using MQTT.""" - - def __init__(self, hass, name, state_topic, command_topic, qos, - payload_up, payload_down, payload_stop, value_template): - """Initialize the roller shutter.""" - self._state = None - self._hass = hass - self._name = name - self._state_topic = state_topic - self._command_topic = command_topic - self._qos = qos - self._payload_up = payload_up - self._payload_down = payload_down - self._payload_stop = payload_stop - - if self._state_topic is None: - return - - def message_received(topic, payload, qos): - """A new MQTT message has been received.""" - if value_template is not None: - payload = value_template.render_with_possible_json_value( - payload) - if payload.isnumeric() and 0 <= int(payload) <= 100: - self._state = int(payload) - self.update_ha_state() - else: - _LOGGER.warning( - "Payload is expected to be an integer between 0 and 100") - - mqtt.subscribe(hass, self._state_topic, message_received, self._qos) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the roller shutter.""" - return self._name - - @property - def current_position(self): - """Return current position of roller shutter. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._state - - def move_up(self, **kwargs): - """Move the roller shutter up.""" - mqtt.publish(self.hass, self._command_topic, self._payload_up, - self._qos) - - def move_down(self, **kwargs): - """Move the roller shutter down.""" - mqtt.publish(self.hass, self._command_topic, self._payload_down, - self._qos) - - def stop(self, **kwargs): - """Stop the device.""" - mqtt.publish(self.hass, self._command_topic, self._payload_stop, - self._qos) diff --git a/homeassistant/components/rollershutter/rfxtrx.py b/homeassistant/components/rollershutter/rfxtrx.py deleted file mode 100644 index 19bcea4e892..00000000000 --- a/homeassistant/components/rollershutter/rfxtrx.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Support for RFXtrx roller shutter components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/rollershutter.rfxtrx/ -""" - -import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.rollershutter import RollershutterDevice - -DEPENDENCIES = ['rfxtrx'] - -PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup the Demo roller shutters.""" - import RFXtrx as rfxtrxmod - - # Add rollershutter from config file - rollershutters = rfxtrx.get_devices_from_config(config, - RfxtrxRollershutter) - add_devices_callback(rollershutters) - - def rollershutter_update(event): - """Callback for roller shutter updates from the RFXtrx gateway.""" - if not isinstance(event.device, rfxtrxmod.LightingDevice) or \ - event.device.known_to_be_dimmable or \ - not event.device.known_to_be_rollershutter: - return - - new_device = rfxtrx.get_new_device(event, config, RfxtrxRollershutter) - if new_device: - add_devices_callback([new_device]) - - rfxtrx.apply_received_command(event) - - # Subscribe to main rfxtrx events - if rollershutter_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(rollershutter_update) - - -# pylint: disable=abstract-method -class RfxtrxRollershutter(rfxtrx.RfxtrxDevice, RollershutterDevice): - """Representation of an rfxtrx roller shutter.""" - - @property - def should_poll(self): - """No polling available in rfxtrx roller shutter.""" - return False - - @property - def current_position(self): - """No position available in rfxtrx roller shutter.""" - return None - - def move_up(self, **kwargs): - """Move the roller shutter up.""" - self._send_command("roll_up") - - def move_down(self, **kwargs): - """Move the roller shutter down.""" - self._send_command("roll_down") - - def stop(self, **kwargs): - """Stop the roller shutter.""" - self._send_command("stop_roll") diff --git a/homeassistant/components/rollershutter/scsgate.py b/homeassistant/components/rollershutter/scsgate.py deleted file mode 100644 index e67395d054c..00000000000 --- a/homeassistant/components/rollershutter/scsgate.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Allow to configure a SCSGate roller shutter. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.scsgate/ -""" -import logging - -import homeassistant.components.scsgate as scsgate -from homeassistant.components.rollershutter import RollershutterDevice - -DEPENDENCIES = ['scsgate'] - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup the SCSGate roller shutter.""" - devices = config.get('devices') - rollershutters = [] - logger = logging.getLogger(__name__) - - if devices: - for _, entity_info in devices.items(): - if entity_info['scs_id'] in scsgate.SCSGATE.devices: - continue - - logger.info("Adding %s scsgate.rollershutter", entity_info['name']) - - name = entity_info['name'] - scs_id = entity_info['scs_id'] - rollershutter = SCSGateRollerShutter( - name=name, - scs_id=scs_id, - logger=logger) - scsgate.SCSGATE.add_device(rollershutter) - rollershutters.append(rollershutter) - - add_devices_callback(rollershutters) - - -# pylint: disable=abstract-method -# pylint: disable=too-many-arguments, too-many-instance-attributes -class SCSGateRollerShutter(RollershutterDevice): - """Representation of SCSGate rollershutter.""" - - def __init__(self, scs_id, name, logger): - """Initialize the roller shutter.""" - self._scs_id = scs_id - self._name = name - self._logger = logger - - @property - def scs_id(self): - """Return the SCSGate ID.""" - return self._scs_id - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the roller shutter.""" - return self._name - - @property - def current_position(self): - """Return current position of roller shutter. - - None is unknown, 0 is closed, 100 is fully open. - """ - return None - - def move_up(self, **kwargs): - """Move the roller shutter up.""" - from scsgate.tasks import RaiseRollerShutterTask - - scsgate.SCSGATE.append_task( - RaiseRollerShutterTask(target=self._scs_id)) - - def move_down(self, **kwargs): - """Move the rollers hutter down.""" - from scsgate.tasks import LowerRollerShutterTask - - scsgate.SCSGATE.append_task( - LowerRollerShutterTask(target=self._scs_id)) - - def stop(self, **kwargs): - """Stop the device.""" - from scsgate.tasks import HaltRollerShutterTask - - scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) - - def process_event(self, message): - """Handle a SCSGate message related with this roller shutter.""" - self._logger.debug( - "Rollershutter %s, got message %s", - self._scs_id, message.toggled) diff --git a/homeassistant/components/rollershutter/services.yaml b/homeassistant/components/rollershutter/services.yaml deleted file mode 100644 index 2991693961b..00000000000 --- a/homeassistant/components/rollershutter/services.yaml +++ /dev/null @@ -1,31 +0,0 @@ -move_up: - description: Move up all or specified roller shutter - - fields: - entity_id: - description: Name(s) of roller shutter(s) to move up - example: 'rollershutter.living_room' - -move_down: - description: Move down all or specified roller shutter - - fields: - entity_id: - description: Name(s) of roller shutter(s) to move down - example: 'rollershutter.living_room' - -move_position: - description: Move to specific position all or specified roller shutter - - fields: - position: - description: Position of the rollershutter (0 to 100) - example: 30 - -stop: - description: Stop all or specified roller shutter - - fields: - entity_id: - description: Name(s) of roller shutter(s) to stop - example: 'rollershutter.living_room' diff --git a/homeassistant/components/rollershutter/wink.py b/homeassistant/components/rollershutter/wink.py deleted file mode 100644 index 3ba1c578ef8..00000000000 --- a/homeassistant/components/rollershutter/wink.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Support for Wink Shades. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.wink/ -""" - -from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.components.wink import WinkDevice - -DEPENDENCIES = ['wink'] - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Wink rollershutter platform.""" - import pywink - - add_devices(WinkRollershutterDevice(shade) for shade in - pywink.get_shades()) - - -# pylint: disable=abstract-method -class WinkRollershutterDevice(WinkDevice, RollershutterDevice): - """Representation of a Wink rollershutter (shades).""" - - def __init__(self, wink): - """Initialize the rollershutter.""" - WinkDevice.__init__(self, wink) - - @property - def should_poll(self): - """Wink Shades don't track their position.""" - return False - - def move_down(self): - """Close the shade.""" - self.wink.set_state(0) - - def move_up(self): - """Open the shade.""" - self.wink.set_state(1) - - @property - def current_position(self): - """Return current position of roller shutter. - - Wink reports blind shade positions as 0 or 1. - home-assistant expects: - None is unknown, 0 is closed, 100 is fully open. - """ - state = self.wink.state() - if state == 0: - return 0 - elif state == 1: - return 100 - else: - return None - - def stop(self): - """Can't stop Wink rollershutter due to API.""" - pass diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py deleted file mode 100644 index e0f1d2b6e4b..00000000000 --- a/homeassistant/components/rollershutter/zwave.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Support for Zwave roller shutter components. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/rollershutter.zwave/ -""" -# Because we do not compile openzwave on CI -# pylint: disable=import-error -import logging -from homeassistant.components.rollershutter import DOMAIN -from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.components import zwave -from homeassistant.components.rollershutter import RollershutterDevice - -SOMFY = 0x47 -SOMFY_ZRTSI = 0x5a52 -SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI) -WORKAROUND = 'workaround' - -DEVICE_MAPPINGS = { - SOMFY_ZRTSI_CONTROLLER: WORKAROUND -} - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and return Z-Wave roller shutters.""" - if discovery_info is None or zwave.NETWORK is None: - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - - if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL: - return - if value.index != 0: - return - - value.set_change_verified(False) - add_devices([ZwaveRollershutter(value)]) - - -class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): - """Representation of an Zwave roller shutter.""" - - def __init__(self, value): - """Initialize the zwave rollershutter.""" - import libopenzwave - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._lozwmgr = libopenzwave.PyManager() - self._lozwmgr.create() - self._node = value.node - self._current_position = None - self._workaround = None - dispatcher.connect( - self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - if (value.node.manufacturer_id.strip() and - value.node.product_id.strip()): - specific_sensor_key = (int(value.node.manufacturer_id, 16), - int(value.node.product_type, 16)) - - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND: - _LOGGER.debug("Controller without positioning feedback") - self._workaround = 1 - - def value_changed(self, value): - """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id or \ - self._value.node == value.node: - self.update_properties() - self.update_ha_state() - _LOGGER.debug("Value changed on network %s", value) - - def update_properties(self): - """Callback on data change for the registered node/value pair.""" - # Position value - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \ - value.label == 'Level': - self._current_position = value.data - - @property - def current_position(self): - """Return the current position of Zwave roller shutter.""" - if not self._workaround: - if self._current_position is not None: - if self._current_position <= 5: - return 100 - elif self._current_position >= 95: - return 0 - else: - return 100 - self._current_position - - def move_up(self, **kwargs): - """Move the roller shutter up.""" - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) - .values()): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Open' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down': - self._lozwmgr.pressButton(value.value_id) - break - - def move_down(self, **kwargs): - """Move the roller shutter down.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Up' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Close': - self._lozwmgr.pressButton(value.value_id) - break - - def move_position(self, position, **kwargs): - """Move the roller shutter to a specific position.""" - self._node.set_dimmer(self._value.value_id, 100 - position) - - def stop(self, **kwargs): - """Stop the roller shutter.""" - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Open' or value.command_class == \ - zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down': - self._lozwmgr.releaseButton(value.value_id) - break diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py deleted file mode 100644 index 52452ef0e59..00000000000 --- a/homeassistant/components/thermostat/__init__.py +++ /dev/null @@ -1,330 +0,0 @@ -""" -Provides functionality to interact with thermostats. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/thermostat/ -""" -import logging -import os -from numbers import Number - -import voluptuous as vol - -from homeassistant.helpers.entity_component import EntityComponent - -from homeassistant.config import load_yaml_config_file -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa -import homeassistant.helpers.config_validation as cv -from homeassistant.util.temperature import convert -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, - TEMP_CELSIUS) - -DOMAIN = "thermostat" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = 60 - -SERVICE_SET_AWAY_MODE = "set_away_mode" -SERVICE_SET_TEMPERATURE = "set_temperature" -SERVICE_SET_FAN_MODE = "set_fan_mode" -SERVICE_SET_HVAC_MODE = "set_hvac_mode" - -STATE_HEAT = "heat" -STATE_COOL = "cool" -STATE_IDLE = "idle" -STATE_AUTO = "auto" - -ATTR_CURRENT_TEMPERATURE = "current_temperature" -ATTR_AWAY_MODE = "away_mode" -ATTR_FAN = "fan" -ATTR_HVAC_MODE = "hvac_mode" -ATTR_MAX_TEMP = "max_temp" -ATTR_MIN_TEMP = "min_temp" -ATTR_TEMPERATURE_LOW = "target_temp_low" -ATTR_TEMPERATURE_HIGH = "target_temp_high" -ATTR_OPERATION = "current_operation" - -_LOGGER = logging.getLogger(__name__) - -SET_AWAY_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AWAY_MODE): cv.boolean, -}) -SET_TEMPERATURE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), -}) -SET_FAN_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN): cv.boolean, -}) -SET_HVAC_MODE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_HVAC_MODE): cv.string, -}) - - -def set_away_mode(hass, away_mode, entity_id=None): - """Turn all or specified thermostat away mode on.""" - data = { - ATTR_AWAY_MODE: away_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) - - -def set_temperature(hass, temperature, entity_id=None): - """Set new target temperature.""" - data = {ATTR_TEMPERATURE: temperature} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) - - -def set_fan_mode(hass, fan_mode, entity_id=None): - """Turn all or specified thermostat fan mode on.""" - data = { - ATTR_FAN: fan_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) - - -def set_hvac_mode(hass, hvac_mode, entity_id=None): - """Set specified thermostat hvac mode.""" - data = { - ATTR_HVAC_MODE: hvac_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data) - - -# pylint: disable=too-many-branches -def setup(hass, config): - """Setup thermostats.""" - _LOGGER.warning('This component has been deprecated in favour of' - ' the "climate" component and will be removed ' - 'in the future. Please upgrade.') - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - component.setup(config) - - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - - def away_mode_set_service(service): - """Set away mode on target thermostats.""" - target_thermostats = component.extract_from_service(service) - - away_mode = service.data[ATTR_AWAY_MODE] - - for thermostat in target_thermostats: - if away_mode: - thermostat.turn_away_mode_on() - else: - thermostat.turn_away_mode_off() - - thermostat.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, - descriptions.get(SERVICE_SET_AWAY_MODE), - schema=SET_AWAY_MODE_SCHEMA) - - def temperature_set_service(service): - """Set temperature on the target thermostats.""" - target_thermostats = component.extract_from_service(service) - - temperature = service.data[ATTR_TEMPERATURE] - - for thermostat in target_thermostats: - converted_temperature = convert( - temperature, hass.config.units.temperature_unit, - thermostat.unit_of_measurement) - - thermostat.set_temperature(converted_temperature) - thermostat.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, - descriptions.get(SERVICE_SET_TEMPERATURE), - schema=SET_TEMPERATURE_SCHEMA) - - def fan_mode_set_service(service): - """Set fan mode on target thermostats.""" - target_thermostats = component.extract_from_service(service) - - fan_mode = service.data[ATTR_FAN] - - for thermostat in target_thermostats: - if fan_mode: - thermostat.turn_fan_on() - else: - thermostat.turn_fan_off() - - thermostat.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, - descriptions.get(SERVICE_SET_FAN_MODE), - schema=SET_FAN_MODE_SCHEMA) - - def hvac_mode_set_service(service): - """Set hvac mode on target thermostats.""" - target_thermostats = component.extract_from_service(service) - - hvac_mode = service.data[ATTR_HVAC_MODE] - - for thermostat in target_thermostats: - thermostat.set_hvac_mode(hvac_mode) - - thermostat.update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_SET_HVAC_MODE, hvac_mode_set_service, - descriptions.get(SERVICE_SET_HVAC_MODE), - schema=SET_HVAC_MODE_SCHEMA) - - return True - - -class ThermostatDevice(Entity): - """Representation of a thermostat.""" - - # pylint: disable=no-self-use - @property - def state(self): - """Return the current state.""" - return self.target_temperature or STATE_UNKNOWN - - @property - def state_attributes(self): - """Return the optional state attributes.""" - data = { - ATTR_CURRENT_TEMPERATURE: - self._convert_for_display(self.current_temperature), - ATTR_MIN_TEMP: self._convert_for_display(self.min_temp), - ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), - ATTR_TEMPERATURE: - self._convert_for_display(self.target_temperature), - ATTR_TEMPERATURE_LOW: - self._convert_for_display(self.target_temperature_low), - ATTR_TEMPERATURE_HIGH: - self._convert_for_display(self.target_temperature_high), - } - - operation = self.operation - if operation is not None: - data[ATTR_OPERATION] = operation - - is_away = self.is_away_mode_on - if is_away is not None: - data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF - - is_fan_on = self.is_fan_on - if is_fan_on is not None: - data[ATTR_FAN] = STATE_ON if is_fan_on else STATE_OFF - - return data - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - raise NotImplementedError - - @property - def current_temperature(self): - """Return the current temperature.""" - raise NotImplementedError - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - return None - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - raise NotImplementedError - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return self.target_temperature - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return self.target_temperature - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return None - - @property - def is_fan_on(self): - """Return true if the fan is on.""" - return None - - def set_temperature(self, temperature): - """Set new target temperature.""" - raise NotImplementedError() - - def set_hvac_mode(self, hvac_mode): - """Set hvac mode.""" - raise NotImplementedError() - - def turn_away_mode_on(self): - """Turn away mode on.""" - raise NotImplementedError() - - def turn_away_mode_off(self): - """Turn away mode off.""" - raise NotImplementedError() - - def turn_fan_on(self): - """Turn fan on.""" - raise NotImplementedError() - - def turn_fan_off(self): - """Turn fan off.""" - raise NotImplementedError() - - @property - def min_temp(self): - """Return the minimum temperature.""" - return convert(7, TEMP_CELSIUS, self.unit_of_measurement) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return convert(35, TEMP_CELSIUS, self.unit_of_measurement) - - def _convert_for_display(self, temp): - """Convert temperature into preferred units for display purposes.""" - if temp is None or not isinstance(temp, Number): - return temp - - value = self.hass.config.units.temperature(temp, - self.unit_of_measurement) - - if self.hass.config.units.is_metric: - decimal_count = 1 - else: - # Users of fahrenheit generally expect integer units. - decimal_count = 0 - - return round(value, decimal_count) diff --git a/homeassistant/components/thermostat/demo.py b/homeassistant/components/thermostat/demo.py deleted file mode 100644 index 7718299ef6a..00000000000 --- a/homeassistant/components/thermostat/demo.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Demo platform that offers a fake thermostat. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Demo thermostats.""" - add_devices([ - DemoThermostat("Nest", 21, TEMP_CELSIUS, False, 19, False), - DemoThermostat("Thermostat", 68, TEMP_FAHRENHEIT, True, 77, True), - ]) - - -# pylint: disable=too-many-arguments, abstract-method -class DemoThermostat(ThermostatDevice): - """Representation of a demo thermostat.""" - - def __init__(self, name, target_temperature, unit_of_measurement, - away, current_temperature, is_fan_on): - """Initialize the thermostat.""" - self._name = name - self._target_temperature = target_temperature - self._unit_of_measurement = unit_of_measurement - self._away = away - self._current_temperature = current_temperature - self._is_fan_on = is_fan_on - - @property - def should_poll(self): - """No polling needed for a demo thermostat.""" - return False - - @property - def name(self): - """Return the name of the thermostat.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self._away - - @property - def is_fan_on(self): - """Return true if the fan is on.""" - return self._is_fan_on - - def set_temperature(self, temperature): - """Set new target temperature.""" - self._target_temperature = temperature - - def turn_away_mode_on(self): - """Turn away mode on.""" - self._away = True - - def turn_away_mode_off(self): - """Turn away mode off.""" - self._away = False - - def turn_fan_on(self): - """Turn fan on.""" - self._is_fan_on = True - - def turn_fan_off(self): - """Turn fan off.""" - self._is_fan_on = False diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py deleted file mode 100644 index 577a33c87f4..00000000000 --- a/homeassistant/components/thermostat/ecobee.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -Platform for Ecobee Thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.ecobee/ -""" -import logging -from os import path -import voluptuous as vol - -from homeassistant.components import ecobee -from homeassistant.components.thermostat import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice) -from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT) -from homeassistant.config import load_yaml_config_file -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = ['ecobee'] -_LOGGER = logging.getLogger(__name__) -ECOBEE_CONFIG_FILE = 'ecobee.conf' -_CONFIGURING = {} - -ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" -SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Ecobee Thermostat Platform.""" - if discovery_info is None: - return - data = ecobee.NETWORK - hold_temp = discovery_info['hold_temp'] - _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", - hold_temp) - devices = [Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats))] - add_devices(devices) - - def fan_min_on_time_set_service(service): - """Set the minimum fan on time on the target thermostats.""" - entity_id = service.data.get('entity_id') - - if entity_id: - target_thermostats = [device for device in devices - if device.entity_id == entity_id] - else: - target_thermostats = devices - - fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME] - - for thermostat in target_thermostats: - thermostat.set_fan_min_on_time(str(fan_min_on_time)) - - thermostat.update_ha_state(True) - - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) - - hass.services.register( - DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, - descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME), - schema=SET_FAN_MIN_ON_TIME_SCHEMA) - - -# pylint: disable=too-many-public-methods, abstract-method -class Thermostat(ThermostatDevice): - """A thermostat class for Ecobee.""" - - def __init__(self, data, thermostat_index, hold_temp): - """Initialize the thermostat.""" - self.data = data - self.thermostat_index = thermostat_index - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) - self._name = self.thermostat['name'] - self.hold_temp = hold_temp - - def update(self): - """Get the latest state from the thermostat.""" - self.data.update() - self.thermostat = self.data.ecobee.get_thermostat( - self.thermostat_index) - - @property - def name(self): - """Return the name of the Ecobee Thermostat.""" - return self.thermostat['name'] - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.thermostat['runtime']['actualTemperature'] / 10 - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.hvac_mode == 'heat' or self.hvac_mode == 'auxHeatOnly': - return self.target_temperature_low - elif self.hvac_mode == 'cool': - return self.target_temperature_high - else: - return (self.target_temperature_low + - self.target_temperature_high) / 2 - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return int(self.thermostat['runtime']['desiredHeat'] / 10) - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return int(self.thermostat['runtime']['desiredCool'] / 10) - - @property - def humidity(self): - """Return the current humidity.""" - return self.thermostat['runtime']['actualHumidity'] - - @property - def desired_fan_mode(self): - """Return the desired fan mode of operation.""" - return self.thermostat['runtime']['desiredFanMode'] - - @property - def fan(self): - """Return the current fan state.""" - if 'fan' in self.thermostat['equipmentStatus']: - return STATE_ON - else: - return STATE_OFF - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - status = self.thermostat['equipmentStatus'] - if status == '': - return STATE_IDLE - elif 'Cool' in status: - return STATE_COOL - elif 'auxHeat' in status: - return STATE_HEAT - elif 'heatPump' in status: - return STATE_HEAT - else: - return status - - @property - def mode(self): - """Return current mode ie. home, away, sleep.""" - return self.thermostat['program']['currentClimateRef'] - - @property - def hvac_mode(self): - """Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off.""" - return self.thermostat['settings']['hvacMode'] - - @property - def fan_min_on_time(self): - """Return current fan minimum on time.""" - return self.thermostat['settings']['fanMinOnTime'] - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - # Move these to Thermostat Device and make them global - return { - "humidity": self.humidity, - "fan": self.fan, - "mode": self.mode, - "hvac_mode": self.hvac_mode, - "fan_min_on_time": self.fan_min_on_time - } - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - mode = self.mode - events = self.thermostat['events'] - for event in events: - if event['running']: - mode = event['holdClimateRef'] - break - return 'away' in mode - - def turn_away_mode_on(self): - """Turn away on.""" - if self.hold_temp: - self.data.ecobee.set_climate_hold(self.thermostat_index, - "away", "indefinite") - else: - self.data.ecobee.set_climate_hold(self.thermostat_index, "away") - - def turn_away_mode_off(self): - """Turn away off.""" - self.data.ecobee.resume_program(self.thermostat_index) - - def set_temperature(self, temperature): - """Set new target temperature.""" - temperature = int(temperature) - low_temp = temperature - 1 - high_temp = temperature + 1 - if self.hold_temp: - self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, - high_temp, "indefinite") - else: - self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, - high_temp) - - def set_hvac_mode(self, mode): - """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" - self.data.ecobee.set_hvac_mode(self.thermostat_index, mode) - - def set_fan_min_on_time(self, fan_min_on_time): - """Set the minimum fan on time.""" - self.data.ecobee.set_fan_min_on_time(self.thermostat_index, - fan_min_on_time) - - # Home and Sleep mode aren't used in UI yet: - - # def turn_home_mode_on(self): - # """ Turns home mode on. """ - # self.data.ecobee.set_climate_hold(self.thermostat_index, "home") - - # def turn_home_mode_off(self): - # """ Turns home mode off. """ - # self.data.ecobee.resume_program(self.thermostat_index) - - # def turn_sleep_mode_on(self): - # """ Turns sleep mode on. """ - # self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep") - - # def turn_sleep_mode_off(self): - # """ Turns sleep mode off. """ - # self.data.ecobee.resume_program(self.thermostat_index) diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py deleted file mode 100644 index a2aec1b8f60..00000000000 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Support for eq3 Bluetooth Smart thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.eq3btsmart/ -""" -import logging - -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS -from homeassistant.util.temperature import convert - -REQUIREMENTS = ['bluepy_devices==0.2.0'] - -CONF_MAC = 'mac' -CONF_DEVICES = 'devices' -CONF_ID = 'id' - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the eq3 BLE thermostats.""" - devices = [] - - for name, device_cfg in config[CONF_DEVICES].items(): - mac = device_cfg[CONF_MAC] - devices.append(EQ3BTSmartThermostat(mac, name)) - - add_devices(devices) - return True - - -# pylint: disable=too-many-instance-attributes, import-error, abstract-method -class EQ3BTSmartThermostat(ThermostatDevice): - """Representation of a EQ3 Bluetooth Smart thermostat.""" - - def __init__(self, _mac, _name): - """Initialize the thermostat.""" - from bluepy_devices.devices import eq3btsmart - - self._name = _name - - self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Can not report temperature, so return target_temperature.""" - return self.target_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._thermostat.target_temperature - - def set_temperature(self, temperature): - """Set new target temperature.""" - self._thermostat.target_temperature = temperature - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return {"mode": self._thermostat.mode, - "mode_readable": self._thermostat.mode_readable} - - @property - def min_temp(self): - """Return the minimum temperature.""" - return convert(self._thermostat.min_temp, TEMP_CELSIUS, - self.unit_of_measurement) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return convert(self._thermostat.max_temp, TEMP_CELSIUS, - self.unit_of_measurement) - - def update(self): - """Update the data from the thermostat.""" - self._thermostat.update() diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py deleted file mode 100644 index faf4059f891..00000000000 --- a/homeassistant/components/thermostat/heat_control.py +++ /dev/null @@ -1,215 +0,0 @@ -""" -Adds support for heat control units. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.heat_control/ -""" -import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components import switch -from homeassistant.components.thermostat import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, ThermostatDevice) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF -from homeassistant.helpers import condition -from homeassistant.helpers.event import track_state_change - -DEPENDENCIES = ['switch', 'sensor'] - -TOL_TEMP = 0.3 - -CONF_NAME = 'name' -DEFAULT_NAME = 'Heat Control' -CONF_HEATER = 'heater' -CONF_SENSOR = 'target_sensor' -CONF_MIN_TEMP = 'min_temp' -CONF_MAX_TEMP = 'max_temp' -CONF_TARGET_TEMP = 'target_temp' -CONF_AC_MODE = 'ac_mode' -CONF_MIN_DUR = 'min_cycle_duration' - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required("platform"): "heat_control", - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HEATER): cv.entity_id, - vol.Required(CONF_SENSOR): cv.entity_id, - vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), - vol.Optional(CONF_AC_MODE): vol.Coerce(bool), - vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the heat control thermostat.""" - name = config.get(CONF_NAME) - heater_entity_id = config.get(CONF_HEATER) - sensor_entity_id = config.get(CONF_SENSOR) - min_temp = config.get(CONF_MIN_TEMP) - max_temp = config.get(CONF_MAX_TEMP) - target_temp = config.get(CONF_TARGET_TEMP) - ac_mode = config.get(CONF_AC_MODE) - min_cycle_duration = config.get(CONF_MIN_DUR) - - add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode, - min_cycle_duration)]) - - -# pylint: disable=too-many-instance-attributes, abstract-method -class HeatControl(ThermostatDevice): - """Representation of a HeatControl device.""" - - # pylint: disable=too-many-arguments - def __init__(self, hass, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): - """Initialize the thermostat.""" - self.hass = hass - self._name = name - self.heater_entity_id = heater_entity_id - self.ac_mode = ac_mode - self.min_cycle_duration = min_cycle_duration - - self._active = False - self._cur_temp = None - self._min_temp = min_temp - self._max_temp = max_temp - self._target_temp = target_temp - self._unit = hass.config.units.temperature_unit - - track_state_change(hass, sensor_entity_id, self._sensor_changed) - - sensor_state = hass.states.get(sensor_entity_id) - if sensor_state: - self._update_temp(sensor_state) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the thermostat.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def current_temperature(self): - """Return the sensor temperature.""" - return self._cur_temp - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - if self.ac_mode: - cooling = self._active and self._is_device_active - return STATE_COOL if cooling else STATE_IDLE - else: - heating = self._active and self._is_device_active - return STATE_HEAT if heating else STATE_IDLE - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temp - - def set_temperature(self, temperature): - """Set new target temperature.""" - self._target_temp = temperature - self._control_heating() - self.update_ha_state() - - @property - def min_temp(self): - """Return the minimum temperature.""" - # pylint: disable=no-member - if self._min_temp: - return self._min_temp - else: - # get default temp from super class - return ThermostatDevice.min_temp.fget(self) - - @property - def max_temp(self): - """Return the maximum temperature.""" - # pylint: disable=no-member - if self._min_temp: - return self._max_temp - else: - # Get default temp from super class - return ThermostatDevice.max_temp.fget(self) - - def _sensor_changed(self, entity_id, old_state, new_state): - """Called when temperature changes.""" - if new_state is None: - return - - self._update_temp(new_state) - self._control_heating() - self.update_ha_state() - - def _update_temp(self, state): - """Update thermostat with latest state from sensor.""" - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - try: - self._cur_temp = self.hass.config.units.temperature( - float(state.state), unit) - except ValueError as ex: - _LOGGER.error('Unable to update from sensor: %s', ex) - - def _control_heating(self): - """Check if we need to turn heating on or off.""" - if not self._active and None not in (self._cur_temp, - self._target_temp): - self._active = True - _LOGGER.info('Obtained current and target temperature. ' - 'Heat control active.') - - if not self._active: - return - - if self.min_cycle_duration: - if self._is_device_active: - current_state = STATE_ON - else: - current_state = STATE_OFF - long_enough = condition.state(self.hass, self.heater_entity_id, - current_state, - self.min_cycle_duration) - if not long_enough: - return - - if self.ac_mode: - too_hot = self._cur_temp - self._target_temp > TOL_TEMP - is_cooling = self._is_device_active - if too_hot and not is_cooling: - _LOGGER.info('Turning on AC %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) - elif not too_hot and is_cooling: - _LOGGER.info('Turning off AC %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) - else: - too_cold = self._target_temp - self._cur_temp > TOL_TEMP - is_heating = self._is_device_active - - if too_cold and not is_heating: - _LOGGER.info('Turning on heater %s', self.heater_entity_id) - switch.turn_on(self.hass, self.heater_entity_id) - elif not too_cold and is_heating: - _LOGGER.info('Turning off heater %s', self.heater_entity_id) - switch.turn_off(self.hass, self.heater_entity_id) - - @property - def _is_device_active(self): - """If the toggleable device is currently active.""" - return switch.is_on(self.hass, self.heater_entity_id) diff --git a/homeassistant/components/thermostat/heatmiser.py b/homeassistant/components/thermostat/heatmiser.py deleted file mode 100644 index e7bbfd72f9b..00000000000 --- a/homeassistant/components/thermostat/heatmiser.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Support for the PRT Heatmiser themostats using the V3 protocol. - -See https://github.com/andylockran/heatmiserV3 for more info on the -heatmiserV3 module dependency. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.heatmiser/ -""" -import logging - -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS - -CONF_IPADDRESS = 'ipaddress' -CONF_PORT = 'port' -CONF_TSTATS = 'tstats' - -REQUIREMENTS = ["heatmiserV3==0.9.1"] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the heatmiser thermostat.""" - from heatmiserV3 import heatmiser, connection - - ipaddress = str(config[CONF_IPADDRESS]) - port = str(config[CONF_PORT]) - - if ipaddress is None or port is None: - _LOGGER.error("Missing required configuration items %s or %s", - CONF_IPADDRESS, CONF_PORT) - return False - - serport = connection.connection(ipaddress, port) - serport.open() - - tstats = [] - if CONF_TSTATS in config: - tstats = config[CONF_TSTATS] - - if tstats is None: - _LOGGER.error("No thermostats configured.") - return False - - for tstat in tstats: - add_devices([ - HeatmiserV3Thermostat( - heatmiser, - tstat.get("id"), - tstat.get("name"), - serport) - ]) - return - - -class HeatmiserV3Thermostat(ThermostatDevice): - """Representation of a HeatmiserV3 thermostat.""" - - # pylint: disable=too-many-instance-attributes, abstract-method - def __init__(self, heatmiser, device, name, serport): - """Initialize the thermostat.""" - self.heatmiser = heatmiser - self.device = device - self.serport = serport - self._current_temperature = None - self._name = name - self._id = device - self.dcb = None - self.update() - self._target_temperature = int(self.dcb.get("roomset")) - - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if self.dcb is not None: - low = self.dcb.get("floortemplow ") - high = self.dcb.get("floortemphigh") - temp = (high*256 + low)/10.0 - self._current_temperature = temp - else: - self._current_temperature = None - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def set_temperature(self, temperature): - """Set new target temperature.""" - temperature = int(temperature) - self.heatmiser.hmSendAddress( - self._id, - 18, - temperature, - 1, - self.serport) - self._target_temperature = int(temperature) - - def update(self): - """Get the latest data.""" - self.dcb = self.heatmiser.hmReadAddress(self._id, 'prt', self.serport) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py deleted file mode 100644 index 31b2e1945b2..00000000000 --- a/homeassistant/components/thermostat/honeywell.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -Support for Honeywell Round Connected and Honeywell Evohome thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.honeywell/ -""" -import logging -import socket - -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) - -REQUIREMENTS = ['evohomeclient==0.2.5', - 'somecomfort==0.3.2'] - -_LOGGER = logging.getLogger(__name__) - -CONF_AWAY_TEMP = "away_temperature" -DEFAULT_AWAY_TEMP = 16 - - -def _setup_round(username, password, config, add_devices): - """Setup rounding function.""" - from evohomeclient import EvohomeClient - - try: - away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP)) - except ValueError: - _LOGGER.error("value entered for item %s should convert to a number", - CONF_AWAY_TEMP) - return False - - evo_api = EvohomeClient(username, password) - - try: - zones = evo_api.temperatures(force_refresh=True) - for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, - zone['id'], - i == 0, - away_temp)]) - except socket.error: - _LOGGER.error( - "Connection error logging into the honeywell evohome web service" - ) - return False - return True - - -# config will be used later -def _setup_us(username, password, config, add_devices): - """Setup user.""" - import somecomfort - - try: - client = somecomfort.SomeComfort(username, password) - except somecomfort.AuthError: - _LOGGER.error('Failed to login to honeywell account %s', username) - return False - except somecomfort.SomeComfortError as ex: - _LOGGER.error('Failed to initialize honeywell client: %s', str(ex)) - return False - - dev_id = config.get('thermostat') - loc_id = config.get('location') - - add_devices([HoneywellUSThermostat(client, device) - for location in client.locations_by_id.values() - for device in location.devices_by_id.values() - if ((not loc_id or location.locationid == loc_id) and - (not dev_id or device.deviceid == dev_id))]) - return True - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the honeywel thermostat.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - region = config.get('region', 'eu').lower() - - if username is None or password is None: - _LOGGER.error("Missing required configuration items %s or %s", - CONF_USERNAME, CONF_PASSWORD) - return False - if region not in ('us', 'eu'): - _LOGGER.error('Region `%s` is invalid (use either us or eu)', region) - return False - - if region == 'us': - return _setup_us(username, password, config, add_devices) - else: - return _setup_round(username, password, config, add_devices) - - -class RoundThermostat(ThermostatDevice): - """Representation of a Honeywell Round Connected thermostat.""" - - # pylint: disable=too-many-instance-attributes, abstract-method - def __init__(self, device, zone_id, master, away_temp): - """Initialize the thermostat.""" - self.device = device - self._current_temperature = None - self._target_temperature = None - self._name = "round connected" - self._id = zone_id - self._master = master - self._is_dhw = False - self._away_temp = away_temp - self._away = False - self.update() - - @property - def name(self): - """Return the name of the honeywell, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._is_dhw: - return None - return self._target_temperature - - def set_temperature(self, temperature): - """Set new target temperature.""" - self.device.set_temperature(self._name, temperature) - - @property - def operation(self: ThermostatDevice) -> str: - """Get the current operation of the system.""" - return getattr(self.device, 'system_mode', None) - - @property - def is_away_mode_on(self): - """Return true if away mode is on.""" - return self._away - - def set_hvac_mode(self: ThermostatDevice, hvac_mode: str) -> None: - """Set the HVAC mode for the thermostat.""" - if hasattr(self.device, 'system_mode'): - self.device.system_mode = hvac_mode - - def turn_away_mode_on(self): - """Turn away on. - - Evohome does have a proprietary away mode, but it doesn't really work - the way it should. For example: If you set a temperature manually - it doesn't get overwritten when away mode is switched on. - """ - self._away = True - self.device.set_temperature(self._name, self._away_temp) - - def turn_away_mode_off(self): - """Turn away off.""" - self._away = False - self.device.cancel_temp_override(self._name) - - def update(self): - """Get the latest date.""" - try: - # Only refresh if this is the "master" device, - # others will pick up the cache - for val in self.device.temperatures(force_refresh=self._master): - if val['id'] == self._id: - data = val - - except StopIteration: - _LOGGER.error("Did not receive any temperature data from the " - "evohomeclient API.") - return - - self._current_temperature = data['temp'] - self._target_temperature = data['setpoint'] - if data['thermostat'] == "DOMESTIC_HOT_WATER": - self._name = "Hot Water" - self._is_dhw = True - else: - self._name = data['name'] - self._is_dhw = False - - -# pylint: disable=abstract-method -class HoneywellUSThermostat(ThermostatDevice): - """Representation of a Honeywell US Thermostat.""" - - def __init__(self, client, device): - """Initialize the thermostat.""" - self._client = client - self._device = device - - @property - def is_fan_on(self): - """Return true if fan is on.""" - return self._device.fan_running - - @property - def name(self): - """Return the name of the honeywell, if any.""" - return self._device.name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return (TEMP_CELSIUS if self._device.temperature_unit == 'C' - else TEMP_FAHRENHEIT) - - @property - def current_temperature(self): - """Return the current temperature.""" - self._device.refresh() - return self._device.current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._device.system_mode == 'cool': - return self._device.setpoint_cool - else: - return self._device.setpoint_heat - - @property - def operation(self: ThermostatDevice) -> str: - """Return current operation ie. heat, cool, idle.""" - return getattr(self._device, 'system_mode', None) - - def set_temperature(self, temperature): - """Set target temperature.""" - import somecomfort - try: - if self._device.system_mode == 'cool': - self._device.setpoint_cool = temperature - else: - self._device.setpoint_heat = temperature - except somecomfort.SomeComfortError: - _LOGGER.error('Temperature %.1f out of range', temperature) - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return {'fan': (self.is_fan_on and 'running' or 'idle'), - 'fanmode': self._device.fan_mode, - 'system_mode': self._device.system_mode} - - def turn_away_mode_on(self): - """Turn away on.""" - pass - - def turn_away_mode_off(self): - """Turn away off.""" - pass - - def set_hvac_mode(self: ThermostatDevice, hvac_mode: str) -> None: - """Set the system mode (Cool, Heat, etc).""" - if hasattr(self._device, 'system_mode'): - self._device.system_mode = hvac_mode diff --git a/homeassistant/components/thermostat/knx.py b/homeassistant/components/thermostat/knx.py deleted file mode 100644 index af8c2af156b..00000000000 --- a/homeassistant/components/thermostat/knx.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Support for KNX thermostats. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/knx/ -""" -import logging - -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS - -from homeassistant.components.knx import ( - KNXConfig, KNXMultiAddressDevice) - -DEPENDENCIES = ["knx"] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create and add an entity based on the configuration.""" - add_entities([ - KNXThermostat(hass, KNXConfig(config)) - ]) - - -class KNXThermostat(KNXMultiAddressDevice, ThermostatDevice): - """Representation of a KNX thermostat. - - A KNX thermostat will has the following parameters: - - temperature (current temperature) - - setpoint (target temperature in HASS terms) - - hvac mode selection (comfort/night/frost protection) - - This version supports only polling. Messages from the KNX bus do not - automatically update the state of the thermostat (to be implemented - in future releases) - """ - - def __init__(self, hass, config): - """Initialize the thermostat based on the given configuration.""" - KNXMultiAddressDevice.__init__(self, hass, config, - ["temperature", "setpoint"], - ["mode"]) - - self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius - self._away = False # not yet supported - self._is_fan_on = False # not yet supported - - @property - def should_poll(self): - """Polling is needed for the KNX thermostat.""" - return True - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def current_temperature(self): - """Return the current temperature.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value("temperature")) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value("setpoint")) - - def set_temperature(self, temperature): - """Set new target temperature.""" - from knxip.conversion import float_to_knx2 - - self.set_value("setpoint", float_to_knx2(temperature)) - _LOGGER.debug("Set target temperature to %s", temperature) - - def set_hvac_mode(self, hvac_mode): - """Set hvac mode.""" - raise NotImplementedError() diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py deleted file mode 100644 index 2e0a8c05888..00000000000 --- a/homeassistant/components/thermostat/nest.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Support for Nest thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.nest/ -""" -import voluptuous as vol - -import homeassistant.components.nest as nest -from homeassistant.components.thermostat import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice) -from homeassistant.const import TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL - -DEPENDENCIES = ['nest'] - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): nest.DOMAIN, - vol.Optional(CONF_SCAN_INTERVAL): - vol.All(vol.Coerce(int), vol.Range(min=1)), -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Nest thermostat.""" - add_devices([NestThermostat(structure, device) - for structure, device in nest.devices()]) - - -# pylint: disable=abstract-method -class NestThermostat(ThermostatDevice): - """Representation of a Nest thermostat.""" - - def __init__(self, structure, device): - """Initialize the thermostat.""" - self.structure = structure - self.device = device - - @property - def name(self): - """Return the name of the nest, if any.""" - location = self.device.where - name = self.device.name - if location is None: - return name - else: - if name == '': - return location.capitalize() - else: - return location.capitalize() + '(' + name + ')' - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - # Move these to Thermostat Device and make them global - return { - "humidity": self.device.humidity, - "target_humidity": self.device.target_humidity, - "mode": self.device.mode - } - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.device.temperature - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - if self.device.hvac_ac_state is True: - return STATE_COOL - elif self.device.hvac_heater_state is True: - return STATE_HEAT - else: - return STATE_IDLE - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.device.mode == 'range': - low, high = self.target_temperature_low, \ - self.target_temperature_high - if self.operation == STATE_COOL: - temp = high - elif self.operation == STATE_HEAT: - temp = low - else: - # If the outside temp is lower than the current temp, consider - # the 'low' temp to the target, otherwise use the high temp - if (self.device.structure.weather.current.temperature < - self.current_temperature): - temp = low - else: - temp = high - else: - if self.is_away_mode_on: - # away_temperature is a low, high tuple. Only one should be set - # if not in range mode, the other will be None - temp = self.device.away_temperature[0] or \ - self.device.away_temperature[1] - else: - temp = self.device.target - - return temp - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[0]: - # away_temperature is always a low, high tuple - return self.device.away_temperature[0] - if self.device.mode == 'range': - return self.device.target[0] - return self.target_temperature - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[1]: - # away_temperature is always a low, high tuple - return self.device.away_temperature[1] - if self.device.mode == 'range': - return self.device.target[1] - return self.target_temperature - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self.structure.away - - def set_temperature(self, temperature): - """Set new target temperature.""" - if self.device.mode == 'range': - if self.target_temperature == self.target_temperature_low: - temperature = (temperature, self.target_temperature_high) - elif self.target_temperature == self.target_temperature_high: - temperature = (self.target_temperature_low, temperature) - self.device.target = temperature - - def set_hvac_mode(self, hvac_mode): - """Set hvac mode.""" - self.device.mode = hvac_mode - - def turn_away_mode_on(self): - """Turn away on.""" - self.structure.away = True - - def turn_away_mode_off(self): - """Turn away off.""" - self.structure.away = False - - @property - def is_fan_on(self): - """Return whether the fan is on.""" - return self.device.fan - - def turn_fan_on(self): - """Turn fan on.""" - self.device.fan = True - - def turn_fan_off(self): - """Turn fan off.""" - self.device.fan = False - - @property - def min_temp(self): - """Identify min_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.low - if temp is None: - return super().min_temp - else: - return temp - - @property - def max_temp(self): - """Identify max_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.high - if temp is None: - return super().max_temp - else: - return temp - - def update(self): - """Python-nest has its own mechanism for staying up to date.""" - pass diff --git a/homeassistant/components/thermostat/proliphix.py b/homeassistant/components/thermostat/proliphix.py deleted file mode 100644 index f92407b0d16..00000000000 --- a/homeassistant/components/thermostat/proliphix.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Support for Proliphix NT10e Thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.proliphix/ -""" -from homeassistant.components.thermostat import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice) -from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT) - -REQUIREMENTS = ['proliphix==0.4.0'] - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Proliphix thermostats.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - host = config.get(CONF_HOST) - - import proliphix - - pdp = proliphix.PDP(host, username, password) - - add_devices([ - ProliphixThermostat(pdp) - ]) - - -# pylint: disable=abstract-method -class ProliphixThermostat(ThermostatDevice): - """Representation a Proliphix thermostat.""" - - def __init__(self, pdp): - """Initialize the thermostat.""" - self._pdp = pdp - # initial data - self._pdp.update() - self._name = self._pdp.name - - @property - def should_poll(self): - """Polling needed for thermostat.""" - return True - - def update(self): - """Update the data from the thermostat.""" - self._pdp.update() - - @property - def name(self): - """Return the name of the thermostat.""" - return self._name - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return { - "fan": self._pdp.fan_state - } - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._pdp.cur_temp - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._pdp.setback - - @property - def operation(self): - """Return the current state of the thermostat.""" - state = self._pdp.hvac_state - if state in (1, 2): - return STATE_IDLE - elif state == 3: - return STATE_HEAT - elif state == 6: - return STATE_COOL - - def set_temperature(self, temperature): - """Set new target temperature.""" - self._pdp.setback = temperature diff --git a/homeassistant/components/thermostat/radiotherm.py b/homeassistant/components/thermostat/radiotherm.py deleted file mode 100644 index c4031fde940..00000000000 --- a/homeassistant/components/thermostat/radiotherm.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Support for Radio Thermostat wifi-enabled home thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.radiotherm/ -""" -import datetime -import logging -from urllib.error import URLError - -from homeassistant.components.thermostat import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF, - ThermostatDevice) -from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT - -REQUIREMENTS = ['radiotherm==1.2'] -HOLD_TEMP = 'hold_temp' -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Radio Thermostat.""" - import radiotherm - - hosts = [] - if CONF_HOST in config: - hosts = config[CONF_HOST] - else: - hosts.append(radiotherm.discover.discover_address()) - - if hosts is None: - _LOGGER.error("No radiotherm thermostats detected.") - return False - - hold_temp = config.get(HOLD_TEMP, False) - tstats = [] - - for host in hosts: - try: - tstat = radiotherm.get_thermostat(host) - tstats.append(RadioThermostat(tstat, hold_temp)) - except (URLError, OSError): - _LOGGER.exception("Unable to connect to Radio Thermostat: %s", - host) - - add_devices(tstats) - - -# pylint: disable=abstract-method -class RadioThermostat(ThermostatDevice): - """Representation of a Radio Thermostat.""" - - def __init__(self, device, hold_temp): - """Initialize the thermostat.""" - self.device = device - self.set_time() - self._target_temperature = None - self._current_temperature = None - self._operation = STATE_IDLE - self._name = None - self.hold_temp = hold_temp - self.update() - - @property - def name(self): - """Return the name of the Radio Thermostat.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return { - "fan": self.device.fmode['human'], - "mode": self.device.tmode['human'] - } - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def operation(self): - """Return the current operation. head, cool idle.""" - return self._operation - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - def update(self): - """Update the data from the thermostat.""" - self._current_temperature = self.device.temp['raw'] - self._name = self.device.name['raw'] - if self.device.tmode['human'] == 'Cool': - self._target_temperature = self.device.t_cool['raw'] - self._operation = STATE_COOL - elif self.device.tmode['human'] == 'Heat': - self._target_temperature = self.device.t_heat['raw'] - self._operation = STATE_HEAT - else: - self._operation = STATE_IDLE - - def set_temperature(self, temperature): - """Set new target temperature.""" - if self._operation == STATE_COOL: - self.device.t_cool = temperature - elif self._operation == STATE_HEAT: - self.device.t_heat = temperature - if self.hold_temp: - self.device.hold = 1 - else: - self.device.hold = 0 - - def set_time(self): - """Set device time.""" - now = datetime.datetime.now() - self.device.time = {'day': now.weekday(), - 'hour': now.hour, 'minute': now.minute} - - def set_hvac_mode(self, mode): - """Set HVAC mode (auto, cool, heat, off).""" - if mode == STATE_OFF: - self.device.tmode = 0 - elif mode == STATE_AUTO: - self.device.tmode = 3 - elif mode == STATE_COOL: - self.device.t_cool = self._target_temperature - elif mode == STATE_HEAT: - self.device.t_heat = self._target_temperature diff --git a/homeassistant/components/thermostat/services.yaml b/homeassistant/components/thermostat/services.yaml deleted file mode 100644 index 9ce1ab704e6..00000000000 --- a/homeassistant/components/thermostat/services.yaml +++ /dev/null @@ -1,48 +0,0 @@ - -set_away_mode: - description: Turn away mode on/off for a thermostat - - fields: - entity_id: - description: Name(s) of entities to change - example: 'light.kitchen' - - away_mode: - description: New value of away mode - example: true - -set_temperature: - description: Set temperature of thermostat - - fields: - entity_id: - description: Name(s) of entities to change - example: 'light.kitchen' - - temperature: - description: New target temperature for thermostat - example: 25 - -set_fan_mode: - description: Turn fan on/off for a thermostat - - fields: - entity_id: - description: Name(s) of entities to change - example: 'thermostat.nest' - - fan: - description: New value of fan mode - example: true - -ecobee_set_fan_min_on_time: - description: Set the minimum time, in minutes, to run the fan each hour - - fields: - entity_id: - descriptions: Name(s) of entities to change - example: 'thermostat.ecobee' - - fan_min_on_time: - description: New value of fan minimum on time - example: 5 diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py deleted file mode 100644 index de1bd3bc03b..00000000000 --- a/homeassistant/components/thermostat/zwave.py +++ /dev/null @@ -1,168 +0,0 @@ -"""ZWave Thermostat.""" - -# Because we do not compile openzwave on CI -# pylint: disable=import-error -import logging -from homeassistant.components.thermostat import DOMAIN -from homeassistant.components.thermostat import ( - ThermostatDevice, - STATE_IDLE) -from homeassistant.components import zwave -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS - -_LOGGER = logging.getLogger(__name__) - -CONF_NAME = 'name' -DEFAULT_NAME = 'ZWave Thermostat' - -REMOTEC = 0x5254 -REMOTEC_ZXT_120 = 0x8377 -REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) - -WORKAROUND_IGNORE = 'ignore' - -DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE -} - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the ZWave thermostats.""" - if discovery_info is None or zwave.NETWORK is None: - _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", - discovery_info, zwave.NETWORK) - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - value.set_change_verified(False) - # Make sure that we have values for the key before converting to int - if (value.node.manufacturer_id.strip() and - value.node.product_id.strip()): - specific_sensor_key = (int(value.node.manufacturer_id, 16), - int(value.node.product_id, 16)) - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE: - _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat, ignoring") - return - if not (value.node.get_values_for_command_class( - zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) and - value.node.get_values_for_command_class( - zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)): - return - - if value.command_class != zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL and \ - value.command_class != zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT: - return - - add_devices([ZWaveThermostat(value)]) - _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", - discovery_info, zwave.NETWORK) - - -# pylint: disable=too-many-arguments, too-many-instance-attributes -# pylint: disable=abstract-method -class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): - """Represents a HeatControl thermostat.""" - - def __init__(self, value): - """Initialize the zwave thermostat.""" - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._node = value.node - self._index = value.index - self._current_temperature = None - self._unit = None - self._current_operation_state = STATE_IDLE - self._target_temperature = None - self._current_fan_state = STATE_IDLE - self.update_properties() - # register listener - dispatcher.connect( - self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - - def value_changed(self, value): - """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id or \ - self._value.node == value.node: - self.update_properties() - self.update_ha_state() - - def update_properties(self): - """Callback on data change for the registered node/value pair.""" - # current Temp - for _, value in self._node.get_values_for_command_class( - zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL).items(): - if value.label == 'Temperature': - self._current_temperature = int(value.data) - self._unit = value.units - - # operation state - for _, value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE) - .items()): - self._current_operation_state = value.data_as_string - - # target temperature - temps = [] - for _, value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) - .items()): - temps.append(int(value.data)) - if value.index == self._index: - self._target_temperature = value.data - self._target_temperature_high = max(temps) - self._target_temperature_low = min(temps) - - # fan state - for _, value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE) - .items()): - self._current_fan_state = value.data_as_string - - @property - def should_poll(self): - """No polling on ZWave.""" - return False - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - unit = self._unit - if unit == 'C': - return TEMP_CELSIUS - elif unit == 'F': - return TEMP_FAHRENHEIT - else: - return unit - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation_state - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def is_fan_on(self): - """Return true if the fan is on.""" - return not (self._current_fan_state == 'Idle' or - self._current_fan_state == STATE_IDLE) - - def set_temperature(self, temperature): - """Set new target temperature.""" - # set point - for _, value in self._node.get_values_for_command_class( - zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT).items(): - if int(value.data) != 0 and value.index == self._index: - value.data = temperature - break diff --git a/tests/components/garage_door/__init__.py b/tests/components/garage_door/__init__.py deleted file mode 100644 index 0523d661222..00000000000 --- a/tests/components/garage_door/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for Garage door platforms.""" diff --git a/tests/components/garage_door/test_demo.py b/tests/components/garage_door/test_demo.py deleted file mode 100644 index e282d697daf..00000000000 --- a/tests/components/garage_door/test_demo.py +++ /dev/null @@ -1,50 +0,0 @@ -"""The tests for the Demo Garage door platform.""" -import unittest - -from homeassistant.bootstrap import setup_component -import homeassistant.components.garage_door as gd - -from tests.common import get_test_home_assistant - - -LEFT = 'garage_door.left_garage_door' -RIGHT = 'garage_door.right_garage_door' - - -class TestGarageDoorDemo(unittest.TestCase): - """Test the demo garage door.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.assertTrue(setup_component(self.hass, gd.DOMAIN, { - 'garage_door': { - 'platform': 'demo' - } - })) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_is_closed(self): - """Test if door is closed.""" - self.assertTrue(gd.is_closed(self.hass, LEFT)) - self.hass.states.is_state(LEFT, 'close') - - self.assertFalse(gd.is_closed(self.hass, RIGHT)) - self.hass.states.is_state(RIGHT, 'open') - - def test_open_door(self): - """Test opeing of the door.""" - gd.open_door(self.hass, LEFT) - self.hass.block_till_done() - - self.assertFalse(gd.is_closed(self.hass, LEFT)) - - def test_close_door(self): - """Test closing ot the door.""" - gd.close_door(self.hass, RIGHT) - self.hass.block_till_done() - - self.assertTrue(gd.is_closed(self.hass, RIGHT)) diff --git a/tests/components/garage_door/test_mqtt.py b/tests/components/garage_door/test_mqtt.py deleted file mode 100644 index c46befe6f1b..00000000000 --- a/tests/components/garage_door/test_mqtt.py +++ /dev/null @@ -1,138 +0,0 @@ -"""The tests for the MQTT Garge door platform.""" -import unittest - -from homeassistant.bootstrap import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, ATTR_ASSUMED_STATE - -import homeassistant.components.garage_door as garage_door -from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, - assert_setup_component) - - -class TestGarageDoorMQTT(unittest.TestCase): - """Test the MQTT Garage door.""" - - # pylint: disable=invalid-name - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" - self.hass.stop() - - def test_fail_setup_if_no_command_topic(self): - """Test if command fails with command topic.""" - self.hass.config.components = ['mqtt'] - with assert_setup_component(0): - assert setup_component(self.hass, garage_door.DOMAIN, { - garage_door.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': '/home/garage_door/door' - } - }) - self.assertIsNone(self.hass.states.get('garage_door.test')) - - def test_controlling_state_via_topic(self): - """Test the controlling state via topic.""" - with assert_setup_component(1): - assert setup_component(self.hass, garage_door.DOMAIN, { - garage_door.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'state_open': 1, - 'state_closed': 0, - 'service_open': 1, - 'service_close': 0 - } - }) - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) - self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) - - fire_mqtt_message(self.hass, 'state-topic', '1') - self.hass.block_till_done() - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_OPEN, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '0') - self.hass.block_till_done() - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) - - def test_sending_mqtt_commands_and_optimistic(self): - """Test the sending MQTT commands in optimistic mode.""" - with assert_setup_component(1): - assert setup_component(self.hass, garage_door.DOMAIN, { - garage_door.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'command_topic': 'command-topic', - 'state_open': 'beer state open', - 'state_closed': 'beer state closed', - 'service_open': 'beer open', - 'service_close': 'beer close', - 'qos': '2' - } - }) - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) - self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - - garage_door.open_door(self.hass, 'garage_door.test') - self.hass.block_till_done() - - self.assertEqual(('command-topic', 'beer open', 2, False), - self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_OPEN, state.state) - - garage_door.close_door(self.hass, 'garage_door.test') - self.hass.block_till_done() - - self.assertEqual(('command-topic', 'beer close', 2, False), - self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) - - def test_controlling_state_via_topic_and_json_message(self): - """Test the controlling state via topic and JSON message.""" - with assert_setup_component(1): - assert setup_component(self.hass, garage_door.DOMAIN, { - garage_door.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'state_open': 'beer open', - 'state_closed': 'beer closed', - 'service_open': 'beer service open', - 'service_close': 'beer service close', - 'value_template': '{{ value_json.val }}' - } - }) - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '{"val":"beer open"}') - self.hass.block_till_done() - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_OPEN, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '{"val":"beer closed"}') - self.hass.block_till_done() - - state = self.hass.states.get('garage_door.test') - self.assertEqual(STATE_CLOSED, state.state) diff --git a/tests/components/hvac/__init__.py b/tests/components/hvac/__init__.py deleted file mode 100644 index 6a5696cfb62..00000000000 --- a/tests/components/hvac/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for hvac component.""" diff --git a/tests/components/hvac/test_demo.py b/tests/components/hvac/test_demo.py deleted file mode 100644 index b033b89f173..00000000000 --- a/tests/components/hvac/test_demo.py +++ /dev/null @@ -1,167 +0,0 @@ -"""The tests for the demo hvac.""" -import unittest - -from homeassistant.bootstrap import setup_component -from homeassistant.components import hvac -from homeassistant.util.unit_system import ( - METRIC_SYSTEM, -) - -from tests.common import get_test_home_assistant - - -ENTITY_HVAC = 'hvac.hvac' - - -class TestDemoHvac(unittest.TestCase): - """Test the demo hvac.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.units = METRIC_SYSTEM - self.assertTrue(setup_component(self.hass, hvac.DOMAIN, {'hvac': { - 'platform': 'demo', - }})) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_setup_params(self): - """Test the inititial parameters.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(21, state.attributes.get('temperature')) - self.assertEqual('on', state.attributes.get('away_mode')) - self.assertEqual(22, state.attributes.get('current_temperature')) - self.assertEqual("On High", state.attributes.get('fan_mode')) - self.assertEqual(67, state.attributes.get('humidity')) - self.assertEqual(54, state.attributes.get('current_humidity')) - self.assertEqual("Off", state.attributes.get('swing_mode')) - self.assertEqual("Cool", state.attributes.get('operation_mode')) - self.assertEqual('off', state.attributes.get('aux_heat')) - - def test_default_setup_params(self): - """Test the setup with default parameters.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(19, state.attributes.get('min_temp')) - self.assertEqual(30, state.attributes.get('max_temp')) - self.assertEqual(30, state.attributes.get('min_humidity')) - self.assertEqual(99, state.attributes.get('max_humidity')) - - def test_set_target_temp_bad_attr(self): - """Test setting the target temperature without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(21, state.attributes.get('temperature')) - hvac.set_temperature(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual(21, state.attributes.get('temperature')) - - def test_set_target_temp(self): - """Test the setting of the target temperature.""" - hvac.set_temperature(self.hass, 30, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(30.0, state.attributes.get('temperature')) - - def test_set_target_humidity_bad_attr(self): - """Test setting the target humidity without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(67, state.attributes.get('humidity')) - hvac.set_humidity(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual(67, state.attributes.get('humidity')) - - def test_set_target_humidity(self): - """Test the setting of the target humidity.""" - hvac.set_humidity(self.hass, 64, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual(64.0, state.attributes.get('humidity')) - - def test_set_fan_mode_bad_attr(self): - """Test setting fan mode without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual("On High", state.attributes.get('fan_mode')) - hvac.set_fan_mode(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual("On High", state.attributes.get('fan_mode')) - - def test_set_fan_mode(self): - """Test setting of new fan mode.""" - hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual("On Low", state.attributes.get('fan_mode')) - - def test_set_swing_mode_bad_attr(self): - """Test setting swing mode without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual("Off", state.attributes.get('swing_mode')) - hvac.set_swing_mode(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual("Off", state.attributes.get('swing_mode')) - - def test_set_swing(self): - """Test setting of new swing mode.""" - hvac.set_swing_mode(self.hass, "Auto", ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual("Auto", state.attributes.get('swing_mode')) - - def test_set_operation_bad_attr(self): - """Test setting operation mode without required attribute.""" - self.assertEqual("Cool", self.hass.states.get(ENTITY_HVAC).state) - hvac.set_operation_mode(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual("Cool", self.hass.states.get(ENTITY_HVAC).state) - - def test_set_operation(self): - """Test setting of new operation mode.""" - hvac.set_operation_mode(self.hass, "Heat", ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual("Heat", self.hass.states.get(ENTITY_HVAC).state) - - def test_set_away_mode_bad_attr(self): - """Test setting the away mode without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('on', state.attributes.get('away_mode')) - hvac.set_away_mode(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual('on', state.attributes.get('away_mode')) - - def test_set_away_mode_on(self): - """Test setting the away mode on/true.""" - hvac.set_away_mode(self.hass, True, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('on', state.attributes.get('away_mode')) - - def test_set_away_mode_off(self): - """Test setting the away mode off/false.""" - hvac.set_away_mode(self.hass, False, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('off', state.attributes.get('away_mode')) - - def test_set_aux_heat_bad_attr(self): - """Test setting the auxillary heater without required attribute.""" - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('off', state.attributes.get('aux_heat')) - hvac.set_aux_heat(self.hass, None, ENTITY_HVAC) - self.hass.block_till_done() - self.assertEqual('off', state.attributes.get('aux_heat')) - - def test_set_aux_heat_on(self): - """Test setting the axillary heater on/true.""" - hvac.set_aux_heat(self.hass, True, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('on', state.attributes.get('aux_heat')) - - def test_set_aux_heat_off(self): - """Test setting the auxillary heater off/false.""" - hvac.set_aux_heat(self.hass, False, ENTITY_HVAC) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_HVAC) - self.assertEqual('off', state.attributes.get('aux_heat')) diff --git a/tests/components/rollershutter/__init__.py b/tests/components/rollershutter/__init__.py deleted file mode 100644 index 4fc6ddee8a9..00000000000 --- a/tests/components/rollershutter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for Roller shutter platforms.""" diff --git a/tests/components/rollershutter/test_command_line.py b/tests/components/rollershutter/test_command_line.py deleted file mode 100644 index d8b5110578c..00000000000 --- a/tests/components/rollershutter/test_command_line.py +++ /dev/null @@ -1,88 +0,0 @@ -"""The tests the Roller shutter command line platform.""" - -import os -import tempfile -import unittest -from unittest import mock - -from homeassistant.bootstrap import setup_component -import homeassistant.components.rollershutter as rollershutter -from homeassistant.components.rollershutter import ( - command_line as cmd_rs) -from tests.common import get_test_home_assistant - - -class TestCommandRollerShutter(unittest.TestCase): - """Test the Roller shutter command line platform.""" - - def setup_method(self, method): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 - self.rs = cmd_rs.CommandRollershutter(self.hass, 'foo', - 'cmd_up', 'cmd_dn', - 'cmd_stop', 'cmd_state', - None) # FIXME - - def teardown_method(self, method): - """Stop down everything that was started.""" - self.hass.stop() - - def test_should_poll(self): - """Test the setting of polling.""" - self.assertTrue(self.rs.should_poll) - self.rs._command_state = None - self.assertFalse(self.rs.should_poll) - - def test_query_state_value(self): - """Test with state value.""" - with mock.patch('subprocess.check_output') as mock_run: - mock_run.return_value = b' foo bar ' - result = self.rs._query_state_value('runme') - self.assertEqual('foo bar', result) - self.assertEqual(mock_run.call_count, 1) - self.assertEqual( - mock_run.call_args, mock.call('runme', shell=True) - ) - - def test_state_value(self): - """Test with state value.""" - with tempfile.TemporaryDirectory() as tempdirname: - path = os.path.join(tempdirname, 'rollershutter_status') - test_rollershutter = { - 'statecmd': 'cat {}'.format(path), - 'upcmd': 'echo 1 > {}'.format(path), - 'downcmd': 'echo 1 > {}'.format(path), - 'stopcmd': 'echo 0 > {}'.format(path), - 'value_template': '{{ value }}' - } - self.assertTrue(setup_component(self.hass, rollershutter.DOMAIN, { - 'rollershutter': { - 'platform': 'command_line', - 'rollershutters': { - 'test': test_rollershutter - } - } - })) - - state = self.hass.states.get('rollershutter.test') - self.assertEqual('unknown', state.state) - - rollershutter.move_up(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual('open', state.state) - - rollershutter.move_down(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual('open', state.state) - - rollershutter.stop(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual('closed', state.state) diff --git a/tests/components/rollershutter/test_demo.py b/tests/components/rollershutter/test_demo.py deleted file mode 100644 index 4740845adcc..00000000000 --- a/tests/components/rollershutter/test_demo.py +++ /dev/null @@ -1,55 +0,0 @@ -"""The tests for the Demo roller shutter platform.""" -import unittest -import homeassistant.util.dt as dt_util - -from homeassistant.components.rollershutter import demo -from tests.common import fire_time_changed, get_test_home_assistant - - -class TestRollershutterDemo(unittest.TestCase): - """Test the Demo roller shutter.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_move_up(self): - """Test moving the rollershutter up.""" - entity = demo.DemoRollershutter(self.hass, 'test', 100) - entity.move_up() - - fire_time_changed(self.hass, dt_util.utcnow()) - self.hass.block_till_done() - self.assertEqual(90, entity.current_position) - - def test_move_down(self): - """Test moving the rollershutter down.""" - entity = demo.DemoRollershutter(self.hass, 'test', 0) - entity.move_down() - - fire_time_changed(self.hass, dt_util.utcnow()) - self.hass.block_till_done() - self.assertEqual(10, entity.current_position) - - def test_move_position(self): - """Test moving the rollershutter to a specific position.""" - entity = demo.DemoRollershutter(self.hass, 'test', 0) - entity.move_position(10) - - fire_time_changed(self.hass, dt_util.utcnow()) - self.hass.block_till_done() - self.assertEqual(10, entity.current_position) - - def test_stop(self): - """Test stopping the rollershutter.""" - entity = demo.DemoRollershutter(self.hass, 'test', 0) - entity.move_down() - entity.stop() - - fire_time_changed(self.hass, dt_util.utcnow()) - self.hass.block_till_done() - self.assertEqual(0, entity.current_position) diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/rollershutter/test_mqtt.py deleted file mode 100644 index eaff07d061b..00000000000 --- a/tests/components/rollershutter/test_mqtt.py +++ /dev/null @@ -1,174 +0,0 @@ -"""The tests for the MQTT roller shutter platform.""" -import unittest - -from homeassistant.bootstrap import _setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN -import homeassistant.components.rollershutter as rollershutter -from tests.common import mock_mqtt_component, fire_mqtt_message - -from tests.common import get_test_home_assistant - - -class TestRollershutterMQTT(unittest.TestCase): - """Test the MQTT roller shutter.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_controlling_state_via_topic(self): - """Test the controlling state via topic.""" - self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, rollershutter.DOMAIN, { - rollershutter.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_up': 'UP', - 'payload_down': 'DOWN', - 'payload_stop': 'STOP' - } - }) - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '0') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_CLOSED, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '50') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_OPEN, state.state) - - fire_mqtt_message(self.hass, 'state-topic', '100') - self.hass.block_till_done() - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_OPEN, state.state) - - def test_send_move_up_command(self): - """Test the sending of move_up.""" - self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, rollershutter.DOMAIN, { - rollershutter.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - rollershutter.move_up(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - self.assertEqual(('command-topic', 'UP', 2, False), - self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - def test_send_move_down_command(self): - """Test the sending of move_down.""" - self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, rollershutter.DOMAIN, { - rollershutter.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - rollershutter.move_down(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - self.assertEqual(('command-topic', 'DOWN', 2, False), - self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - def test_send_stop_command(self): - """Test the sending of stop.""" - self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, rollershutter.DOMAIN, { - rollershutter.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - rollershutter.stop(self.hass, 'rollershutter.test') - self.hass.block_till_done() - - self.assertEqual(('command-topic', 'STOP', 2, False), - self.mock_publish.mock_calls[-1][1]) - state = self.hass.states.get('rollershutter.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - def test_state_attributes_current_position(self): - """Test the current position.""" - self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, rollershutter.DOMAIN, { - rollershutter.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'payload_up': 'UP', - 'payload_down': 'DOWN', - 'payload_stop': 'STOP' - } - }) - - state_attributes_dict = self.hass.states.get( - 'rollershutter.test').attributes - self.assertFalse('current_position' in state_attributes_dict) - - fire_mqtt_message(self.hass, 'state-topic', '0') - self.hass.block_till_done() - current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] - self.assertEqual(0, current_position) - - fire_mqtt_message(self.hass, 'state-topic', '50') - self.hass.block_till_done() - current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] - self.assertEqual(50, current_position) - - fire_mqtt_message(self.hass, 'state-topic', '101') - self.hass.block_till_done() - current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] - self.assertEqual(50, current_position) - - fire_mqtt_message(self.hass, 'state-topic', 'non-numeric') - self.hass.block_till_done() - current_position = self.hass.states.get( - 'rollershutter.test').attributes['current_position'] - self.assertEqual(50, current_position) diff --git a/tests/components/rollershutter/test_rfxtrx.py b/tests/components/rollershutter/test_rfxtrx.py deleted file mode 100644 index e16f841c3fe..00000000000 --- a/tests/components/rollershutter/test_rfxtrx.py +++ /dev/null @@ -1,219 +0,0 @@ -"""The tests for the Rfxtrx roller shutter platform.""" -import unittest - -import pytest - -from homeassistant.bootstrap import _setup_component -from homeassistant.components import rfxtrx as rfxtrx_core - -from tests.common import get_test_home_assistant - - -@pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'") -class TestRollershutterRfxtrx(unittest.TestCase): - """Test the Rfxtrx roller shutter platform.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) - self.hass.config.components = ['rfxtrx'] - - def tearDown(self): - """Stop everything that was started.""" - rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS = [] - rfxtrx_core.RFX_DEVICES = {} - if rfxtrx_core.RFXOBJECT: - rfxtrx_core.RFXOBJECT.close_connection() - self.hass.stop() - - def test_valid_config(self): - """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'devices': - {'0b1100cd0213c7f210010f51': { - 'name': 'Test', - rfxtrx_core.ATTR_FIREEVENT: True} - }}})) - - def test_invalid_config1(self): - """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'devices': - {'2FF7f216': { - 'name': 'Test', - 'packetid': '0b1100cd0213c7f210010f51', - 'signal_repetitions': 3} - }}})) - - def test_invalid_config2(self): - """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'invalid_key': 'afda', - 'devices': - {'213c7f216': { - 'name': 'Test', - 'packetid': '0b1100cd0213c7f210010f51', - rfxtrx_core.ATTR_FIREEVENT: True} - }}})) - - def test_invalid_config3(self): - """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'devices': - {'213c7f216': { - 'name': 'Test', - 'packetid': 'AA1100cd0213c7f210010f51', - rfxtrx_core.ATTR_FIREEVENT: True} - }}})) - - def test_invalid_config4(self): - """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'devices': - {'213c7f216': { - 'name': 'Test', - rfxtrx_core.ATTR_FIREEVENT: True} - }}})) - - def test_default_config(self): - """Test with 0 roller shutter.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'devices': {}}})) - self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) - - def test_one_rollershutter(self): - """Test with 1 roller shutter.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'devices': - {'0b1400cd0213c7f210010f51': { - 'name': 'Test' - }}}})) - - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT =\ - rfxtrxmod.Core("", transport_protocol=rfxtrxmod.DummyTransport) - - self.assertEqual(1, len(rfxtrx_core.RFX_DEVICES)) - for id in rfxtrx_core.RFX_DEVICES: - entity = rfxtrx_core.RFX_DEVICES[id] - self.assertEqual(entity.signal_repetitions, 1) - self.assertFalse(entity.should_fire_event) - self.assertFalse(entity.should_poll) - entity.move_up() - entity.move_down() - entity.stop() - - def test_several_rollershutters(self): - """Test with 3 roller shutters.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'signal_repetitions': 3, - 'devices': - {'0b1100cd0213c7f230010f71': { - 'name': 'Test'}, - '0b1100100118cdea02010f70': { - 'name': 'Bath'}, - '0b1100101118cdea02010f70': { - 'name': 'Living'} - }}})) - - self.assertEqual(3, len(rfxtrx_core.RFX_DEVICES)) - device_num = 0 - for id in rfxtrx_core.RFX_DEVICES: - entity = rfxtrx_core.RFX_DEVICES[id] - self.assertEqual(entity.signal_repetitions, 3) - if entity.name == 'Living': - device_num = device_num + 1 - elif entity.name == 'Bath': - device_num = device_num + 1 - elif entity.name == 'Test': - device_num = device_num + 1 - - self.assertEqual(3, device_num) - - def test_discover_rollershutter(self): - """Test with discovery of roller shutters.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': True, - 'devices': {}}})) - - event = rfxtrx_core.get_rfx_object('0a140002f38cae010f0070') - event.data = bytearray([0x0A, 0x14, 0x00, 0x02, 0xF3, 0x8C, - 0xAE, 0x01, 0x0F, 0x00, 0x70]) - - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(1, len(rfxtrx_core.RFX_DEVICES)) - - event = rfxtrx_core.get_rfx_object('0a1400adf394ab020e0060') - event.data = bytearray([0x0A, 0x14, 0x00, 0xAD, 0xF3, 0x94, - 0xAB, 0x02, 0x0E, 0x00, 0x60]) - - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES)) - - # Trying to add a sensor - event = rfxtrx_core.get_rfx_object('0a52085e070100b31b0279') - event.data = bytearray(b'\nR\x08^\x07\x01\x00\xb3\x1b\x02y') - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES)) - - # Trying to add a light - event = rfxtrx_core.get_rfx_object('0b1100100118cdea02010f70') - event.data = bytearray([0x0b, 0x11, 0x11, 0x10, 0x01, 0x18, - 0xcd, 0xea, 0x01, 0x02, 0x0f, 0x70]) - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES)) - - def test_discover_rollershutter_noautoadd(self): - """Test with discovery of roller shutter when auto add is False.""" - self.assertTrue(_setup_component(self.hass, 'rollershutter', { - 'rollershutter': {'platform': 'rfxtrx', - 'automatic_add': False, - 'devices': {}}})) - - event = rfxtrx_core.get_rfx_object('0a1400adf394ab010d0060') - event.data = bytearray([0x0A, 0x14, 0x00, 0xAD, 0xF3, 0x94, - 0xAB, 0x01, 0x0D, 0x00, 0x60]) - - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) - - event = rfxtrx_core.get_rfx_object('0a1400adf394ab020e0060') - event.data = bytearray([0x0A, 0x14, 0x00, 0xAD, 0xF3, 0x94, - 0xAB, 0x02, 0x0E, 0x00, 0x60]) - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) - - # Trying to add a sensor - event = rfxtrx_core.get_rfx_object('0a52085e070100b31b0279') - event.data = bytearray(b'\nR\x08^\x07\x01\x00\xb3\x1b\x02y') - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) - - # Trying to add a light - event = rfxtrx_core.get_rfx_object('0b1100100118cdea02010f70') - event.data = bytearray([0x0b, 0x11, 0x11, 0x10, 0x01, - 0x18, 0xcd, 0xea, 0x01, 0x02, 0x0f, 0x70]) - for evt_sub in rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS: - evt_sub(event) - self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) diff --git a/tests/components/thermostat/__init__.py b/tests/components/thermostat/__init__.py deleted file mode 100644 index 9a15198ecb4..00000000000 --- a/tests/components/thermostat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for Thermostat platforms.""" diff --git a/tests/components/thermostat/test_demo.py b/tests/components/thermostat/test_demo.py deleted file mode 100644 index 2d564e97103..00000000000 --- a/tests/components/thermostat/test_demo.py +++ /dev/null @@ -1,101 +0,0 @@ -"""The tests for the demo thermostat.""" -import unittest - -from homeassistant.util.unit_system import ( - METRIC_SYSTEM, -) -from homeassistant.components import thermostat - -from tests.common import get_test_home_assistant - - -ENTITY_NEST = 'thermostat.nest' - - -class TestDemoThermostat(unittest.TestCase): - """Test the Heat Control thermostat.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.units = METRIC_SYSTEM - self.assertTrue(thermostat.setup(self.hass, {'thermostat': { - 'platform': 'demo', - }})) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_setup_params(self): - """Test the inititial parameters.""" - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual(21, state.attributes.get('temperature')) - self.assertEqual('off', state.attributes.get('away_mode')) - self.assertEqual(19, state.attributes.get('current_temperature')) - self.assertEqual('off', state.attributes.get('fan')) - - def test_default_setup_params(self): - """Test the setup with default parameters.""" - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual(7, state.attributes.get('min_temp')) - self.assertEqual(35, state.attributes.get('max_temp')) - - def test_set_target_temp_bad_attr(self): - """Test setting the target temperature without required attribute.""" - self.assertEqual('21.0', self.hass.states.get(ENTITY_NEST).state) - thermostat.set_temperature(self.hass, None, ENTITY_NEST) - self.hass.block_till_done() - self.assertEqual('21.0', self.hass.states.get(ENTITY_NEST).state) - - def test_set_target_temp(self): - """Test the setting of the target temperature.""" - thermostat.set_temperature(self.hass, 30, ENTITY_NEST) - self.hass.block_till_done() - self.assertEqual('30.0', self.hass.states.get(ENTITY_NEST).state) - - def test_set_away_mode_bad_attr(self): - """Test setting the away mode without required attribute.""" - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('away_mode')) - thermostat.set_away_mode(self.hass, None, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('away_mode')) - - def test_set_away_mode_on(self): - """Test setting the away mode on/true.""" - thermostat.set_away_mode(self.hass, True, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('on', state.attributes.get('away_mode')) - - def test_set_away_mode_off(self): - """Test setting the away mode off/false.""" - thermostat.set_away_mode(self.hass, False, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('away_mode')) - - def test_set_fan_mode_on_bad_attr(self): - """Test setting the fan mode on/true without required attribute.""" - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('fan')) - thermostat.set_fan_mode(self.hass, None, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('fan')) - - def test_set_fan_mode_on(self): - """Test setting the fan mode on/true.""" - thermostat.set_fan_mode(self.hass, True, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('on', state.attributes.get('fan')) - - def test_set_fan_mode_off(self): - """Test setting the fan mode off/false.""" - thermostat.set_fan_mode(self.hass, False, ENTITY_NEST) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_NEST) - self.assertEqual('off', state.attributes.get('fan')) diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py deleted file mode 100644 index 300bfd6cc4a..00000000000 --- a/tests/components/thermostat/test_heat_control.py +++ /dev/null @@ -1,494 +0,0 @@ -"""The tests for the heat control thermostat.""" -import datetime -import unittest -from unittest import mock - - -from homeassistant.bootstrap import setup_component -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_ON, - STATE_OFF, - TEMP_CELSIUS, -) -from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.components import thermostat - -from tests.common import assert_setup_component, get_test_home_assistant - - -ENTITY = 'thermostat.test' -ENT_SENSOR = 'sensor.test' -ENT_SWITCH = 'switch.test' -MIN_TEMP = 3.0 -MAX_TEMP = 65.0 -TARGET_TEMP = 42.0 - - -class TestSetupThermostatHeatControl(unittest.TestCase): - """Test the Heat Control thermostat with custom config.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_setup_missing_conf(self): - """Test set up heat_control with missing config values.""" - config = { - 'name': 'test', - 'target_sensor': ENT_SENSOR - } - with assert_setup_component(0): - setup_component(self.hass, 'thermostat', { - 'thermostat': config}) - - def test_valid_conf(self): - """Test set up heat_control with valid config values.""" - self.assertTrue(setup_component(self.hass, 'thermostat', - {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR}})) - - def test_setup_with_sensor(self): - """Test set up heat_control with sensor to trigger update at init.""" - self.hass.states.set(ENT_SENSOR, 22.0, { - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS - }) - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR - }}) - state = self.hass.states.get(ENTITY) - self.assertEqual( - TEMP_CELSIUS, state.attributes.get('unit_of_measurement')) - self.assertEqual(22.0, state.attributes.get('current_temperature')) - - -class TestThermostatHeatControl(unittest.TestCase): - """Test the Heat Control thermostat.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.units = METRIC_SYSTEM - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR - }}) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_setup_defaults_to_unknown(self): - """Test the setting of defaults to unknown.""" - self.assertEqual('unknown', self.hass.states.get(ENTITY).state) - - def test_default_setup_params(self): - """Test the setup with default parameters.""" - state = self.hass.states.get(ENTITY) - self.assertEqual(7, state.attributes.get('min_temp')) - self.assertEqual(35, state.attributes.get('max_temp')) - self.assertEqual(None, state.attributes.get('temperature')) - - def test_custom_setup_params(self): - """Test the setup with custom parameters.""" - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR, - 'min_temp': MIN_TEMP, - 'max_temp': MAX_TEMP, - 'target_temp': TARGET_TEMP - }}) - state = self.hass.states.get(ENTITY) - self.assertEqual(MIN_TEMP, state.attributes.get('min_temp')) - self.assertEqual(MAX_TEMP, state.attributes.get('max_temp')) - self.assertEqual(TARGET_TEMP, state.attributes.get('temperature')) - self.assertEqual(str(TARGET_TEMP), self.hass.states.get(ENTITY).state) - - def test_set_target_temp(self): - """Test the setting of the target temperature.""" - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self.assertEqual('30.0', self.hass.states.get(ENTITY).state) - - def test_sensor_bad_unit(self): - """Test sensor that have bad unit.""" - state = self.hass.states.get(ENTITY) - temp = state.attributes.get('current_temperature') - unit = state.attributes.get('unit_of_measurement') - - self._setup_sensor(22.0, unit='bad_unit') - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY) - self.assertEqual(unit, state.attributes.get('unit_of_measurement')) - self.assertEqual(temp, state.attributes.get('current_temperature')) - - def test_sensor_bad_value(self): - """Test sensor that have None as state.""" - state = self.hass.states.get(ENTITY) - temp = state.attributes.get('current_temperature') - unit = state.attributes.get('unit_of_measurement') - - self._setup_sensor(None) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY) - self.assertEqual(unit, state.attributes.get('unit_of_measurement')) - self.assertEqual(temp, state.attributes.get('current_temperature')) - - def test_set_target_temp_heater_on(self): - """Test if target temperature turn heater on.""" - self._setup_switch(False) - self._setup_sensor(25) - self.hass.block_till_done() - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_set_target_temp_heater_off(self): - """Test if target temperature turn heater off.""" - self._setup_switch(True) - self._setup_sensor(30) - self.hass.block_till_done() - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_set_temp_change_heater_on(self): - """Test if temperature change turn heater on.""" - self._setup_switch(False) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_temp_change_heater_off(self): - """Test if temperature change turn heater off.""" - self._setup_switch(True) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def _setup_sensor(self, temp, unit=TEMP_CELSIUS): - """Setup the test sensor.""" - self.hass.states.set(ENT_SENSOR, temp, { - ATTR_UNIT_OF_MEASUREMENT: unit - }) - - def _setup_switch(self, is_on): - """Setup the test switch.""" - self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) - self.calls = [] - - def log_call(call): - """Log service calls.""" - self.calls.append(call) - - self.hass.services.register('switch', SERVICE_TURN_ON, log_call) - self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) - - -class TestThermostatHeatControlACMode(unittest.TestCase): - """Test the Heat Control thermostat.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR, - 'ac_mode': True - }}) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_set_target_temp_ac_off(self): - """Test if target temperature turn ac off.""" - self._setup_switch(True) - self._setup_sensor(25) - self.hass.block_till_done() - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_set_target_temp_ac_on(self): - """Test if target temperature turn ac on.""" - self._setup_switch(False) - self._setup_sensor(30) - self.hass.block_till_done() - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_set_temp_change_ac_off(self): - """Test if temperature change turn ac off.""" - self._setup_switch(True) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_temp_change_ac_on(self): - """Test if temperature change turn ac on.""" - self._setup_switch(False) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def _setup_sensor(self, temp, unit=TEMP_CELSIUS): - """Setup the test sensor.""" - self.hass.states.set(ENT_SENSOR, temp, { - ATTR_UNIT_OF_MEASUREMENT: unit - }) - - def _setup_switch(self, is_on): - """Setup the test switch.""" - self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) - self.calls = [] - - def log_call(call): - """Log service calls.""" - self.calls.append(call) - - self.hass.services.register('switch', SERVICE_TURN_ON, log_call) - self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) - - -class TestThermostatHeatControlACModeMinCycle(unittest.TestCase): - """Test the Heat Control thermostat.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR, - 'ac_mode': True, - 'min_cycle_duration': datetime.timedelta(minutes=10) - }}) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_temp_change_ac_trigger_on_not_long_enough(self): - """Test if temperature change turn ac on.""" - self._setup_switch(False) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_temp_change_ac_trigger_on_long_enough(self): - """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, - tzinfo=datetime.timezone.utc) - with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=fake_changed): - self._setup_switch(False) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_temp_change_ac_trigger_off_not_long_enough(self): - """Test if temperature change turn ac on.""" - self._setup_switch(True) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_temp_change_ac_trigger_off_long_enough(self): - """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, - tzinfo=datetime.timezone.utc) - with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=fake_changed): - self._setup_switch(True) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def _setup_sensor(self, temp, unit=TEMP_CELSIUS): - """Setup the test sensor.""" - self.hass.states.set(ENT_SENSOR, temp, { - ATTR_UNIT_OF_MEASUREMENT: unit - }) - - def _setup_switch(self, is_on): - """Setup the test switch.""" - self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) - self.calls = [] - - def log_call(call): - """Log service calls.""" - self.calls.append(call) - - self.hass.services.register('switch', SERVICE_TURN_ON, log_call) - self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) - - -class TestThermostatHeatControlMinCycle(unittest.TestCase): - """Test the Heat Control thermostat.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.temperature_unit = TEMP_CELSIUS - thermostat.setup(self.hass, {'thermostat': { - 'platform': 'heat_control', - 'name': 'test', - 'heater': ENT_SWITCH, - 'target_sensor': ENT_SENSOR, - 'min_cycle_duration': datetime.timedelta(minutes=10) - }}) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_temp_change_heater_trigger_off_not_long_enough(self): - """Test if temp change doesn't turn heater off because of time.""" - self._setup_switch(True) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_temp_change_heater_trigger_on_not_long_enough(self): - """Test if temp change doesn't turn heater on because of time.""" - self._setup_switch(False) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_temp_change_heater_trigger_on_long_enough(self): - """Test if temperature change turn heater on after min cycle.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, - tzinfo=datetime.timezone.utc) - with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=fake_changed): - self._setup_switch(False) - thermostat.set_temperature(self.hass, 30) - self.hass.block_till_done() - self._setup_sensor(25) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_ON, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def test_temp_change_heater_trigger_off_long_enough(self): - """Test if temperature change turn heater off after min cycle.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, - tzinfo=datetime.timezone.utc) - with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=fake_changed): - self._setup_switch(True) - thermostat.set_temperature(self.hass, 25) - self.hass.block_till_done() - self._setup_sensor(30) - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - call = self.calls[0] - self.assertEqual('switch', call.domain) - self.assertEqual(SERVICE_TURN_OFF, call.service) - self.assertEqual(ENT_SWITCH, call.data['entity_id']) - - def _setup_sensor(self, temp, unit=TEMP_CELSIUS): - """Setup the test sensor.""" - self.hass.states.set(ENT_SENSOR, temp, { - ATTR_UNIT_OF_MEASUREMENT: unit - }) - - def _setup_switch(self, is_on): - """Setup the test switch.""" - self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) - self.calls = [] - - def log_call(call): - """Log service calls.""" - self.calls.append(call) - - self.hass.services.register('switch', SERVICE_TURN_ON, log_call) - self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) diff --git a/tests/components/thermostat/test_honeywell.py b/tests/components/thermostat/test_honeywell.py deleted file mode 100644 index b95cede77b3..00000000000 --- a/tests/components/thermostat/test_honeywell.py +++ /dev/null @@ -1,391 +0,0 @@ -"""The test the Honeywell thermostat module.""" -import socket -import unittest -from unittest import mock - -import somecomfort - -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, - TEMP_CELSIUS, TEMP_FAHRENHEIT) -import homeassistant.components.thermostat.honeywell as honeywell - - -class TestHoneywell(unittest.TestCase): - """A test class for Honeywell themostats.""" - - @mock.patch('somecomfort.SomeComfort') - @mock.patch('homeassistant.components.thermostat.' - 'honeywell.HoneywellUSThermostat') - def test_setup_us(self, mock_ht, mock_sc): - """Test for the US setup.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - 'region': 'us', - } - bad_pass_config = { - CONF_USERNAME: 'user', - 'region': 'us', - } - bad_region_config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - 'region': 'un', - } - hass = mock.MagicMock() - add_devices = mock.MagicMock() - - locations = [ - mock.MagicMock(), - mock.MagicMock(), - ] - devices_1 = [mock.MagicMock()] - devices_2 = [mock.MagicMock(), mock.MagicMock] - mock_sc.return_value.locations_by_id.values.return_value = \ - locations - locations[0].devices_by_id.values.return_value = devices_1 - locations[1].devices_by_id.values.return_value = devices_2 - - result = honeywell.setup_platform(hass, bad_pass_config, add_devices) - self.assertFalse(result) - result = honeywell.setup_platform(hass, bad_region_config, add_devices) - self.assertFalse(result) - result = honeywell.setup_platform(hass, config, add_devices) - self.assertTrue(result) - self.assertEqual(mock_sc.call_count, 1) - self.assertEqual(mock_sc.call_args, mock.call('user', 'pass')) - mock_ht.assert_has_calls([ - mock.call(mock_sc.return_value, devices_1[0]), - mock.call(mock_sc.return_value, devices_2[0]), - mock.call(mock_sc.return_value, devices_2[1]), - ]) - - @mock.patch('somecomfort.SomeComfort') - def test_setup_us_failures(self, mock_sc): - """Test the US setup.""" - hass = mock.MagicMock() - add_devices = mock.MagicMock() - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - 'region': 'us', - } - - mock_sc.side_effect = somecomfort.AuthError - result = honeywell.setup_platform(hass, config, add_devices) - self.assertFalse(result) - self.assertFalse(add_devices.called) - - mock_sc.side_effect = somecomfort.SomeComfortError - result = honeywell.setup_platform(hass, config, add_devices) - self.assertFalse(result) - self.assertFalse(add_devices.called) - - @mock.patch('somecomfort.SomeComfort') - @mock.patch('homeassistant.components.thermostat.' - 'honeywell.HoneywellUSThermostat') - def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None): - """Test for US filtered thermostats.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - 'region': 'us', - 'location': loc, - 'thermostat': dev, - } - locations = { - 1: mock.MagicMock(locationid=mock.sentinel.loc1, - devices_by_id={ - 11: mock.MagicMock( - deviceid=mock.sentinel.loc1dev1), - 12: mock.MagicMock( - deviceid=mock.sentinel.loc1dev2), - }), - 2: mock.MagicMock(locationid=mock.sentinel.loc2, - devices_by_id={ - 21: mock.MagicMock( - deviceid=mock.sentinel.loc2dev1), - }), - 3: mock.MagicMock(locationid=mock.sentinel.loc3, - devices_by_id={ - 31: mock.MagicMock( - deviceid=mock.sentinel.loc3dev1), - }), - } - mock_sc.return_value = mock.MagicMock(locations_by_id=locations) - hass = mock.MagicMock() - add_devices = mock.MagicMock() - self.assertEqual(True, - honeywell.setup_platform(hass, config, add_devices)) - - return mock_ht.call_args_list, mock_sc - - def test_us_filtered_thermostat_1(self): - """Test for US filtered thermostats.""" - result, client = self._test_us_filtered_devices( - dev=mock.sentinel.loc1dev1) - devices = [x[0][1].deviceid for x in result] - self.assertEqual([mock.sentinel.loc1dev1], devices) - - def test_us_filtered_thermostat_2(self): - """Test for US filtered location.""" - result, client = self._test_us_filtered_devices( - dev=mock.sentinel.loc2dev1) - devices = [x[0][1].deviceid for x in result] - self.assertEqual([mock.sentinel.loc2dev1], devices) - - def test_us_filtered_location_1(self): - """Test for US filtered locations.""" - result, client = self._test_us_filtered_devices( - loc=mock.sentinel.loc1) - devices = [x[0][1].deviceid for x in result] - self.assertEqual([mock.sentinel.loc1dev1, - mock.sentinel.loc1dev2], devices) - - def test_us_filtered_location_2(self): - """Test for US filtered locations.""" - result, client = self._test_us_filtered_devices( - loc=mock.sentinel.loc2) - devices = [x[0][1].deviceid for x in result] - self.assertEqual([mock.sentinel.loc2dev1], devices) - - @mock.patch('evohomeclient.EvohomeClient') - @mock.patch('homeassistant.components.thermostat.honeywell.' - 'RoundThermostat') - def test_eu_setup_full_config(self, mock_round, mock_evo): - """Test the EU setup wwith complete configuration.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - honeywell.CONF_AWAY_TEMP: 20, - 'region': 'eu', - } - mock_evo.return_value.temperatures.return_value = [ - {'id': 'foo'}, {'id': 'bar'}] - hass = mock.MagicMock() - add_devices = mock.MagicMock() - self.assertTrue(honeywell.setup_platform(hass, config, add_devices)) - self.assertEqual(mock_evo.call_count, 1) - self.assertEqual(mock_evo.call_args, mock.call('user', 'pass')) - self.assertEqual(mock_evo.return_value.temperatures.call_count, 1) - self.assertEqual( - mock_evo.return_value.temperatures.call_args, - mock.call(force_refresh=True) - ) - mock_round.assert_has_calls([ - mock.call(mock_evo.return_value, 'foo', True, 20), - mock.call(mock_evo.return_value, 'bar', False, 20), - ]) - self.assertEqual(2, add_devices.call_count) - - @mock.patch('evohomeclient.EvohomeClient') - @mock.patch('homeassistant.components.thermostat.honeywell.' - 'RoundThermostat') - def test_eu_setup_partial_config(self, mock_round, mock_evo): - """Test the EU setup with partial configuration.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - 'region': 'eu', - } - mock_evo.return_value.temperatures.return_value = [ - {'id': 'foo'}, {'id': 'bar'}] - hass = mock.MagicMock() - add_devices = mock.MagicMock() - self.assertTrue(honeywell.setup_platform(hass, config, add_devices)) - default = honeywell.DEFAULT_AWAY_TEMP - mock_round.assert_has_calls([ - mock.call(mock_evo.return_value, 'foo', True, default), - mock.call(mock_evo.return_value, 'bar', False, default), - ]) - - @mock.patch('evohomeclient.EvohomeClient') - @mock.patch('homeassistant.components.thermostat.honeywell.' - 'RoundThermostat') - def test_eu_setup_bad_temp(self, mock_round, mock_evo): - """Test the EU setup with invalid temperature.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - honeywell.CONF_AWAY_TEMP: 'ponies', - 'region': 'eu', - } - self.assertFalse(honeywell.setup_platform(None, config, None)) - - @mock.patch('evohomeclient.EvohomeClient') - @mock.patch('homeassistant.components.thermostat.honeywell.' - 'RoundThermostat') - def test_eu_setup_error(self, mock_round, mock_evo): - """Test the EU setup with errors.""" - config = { - CONF_USERNAME: 'user', - CONF_PASSWORD: 'pass', - honeywell.CONF_AWAY_TEMP: 20, - 'region': 'eu', - } - mock_evo.return_value.temperatures.side_effect = socket.error - add_devices = mock.MagicMock() - hass = mock.MagicMock() - self.assertFalse(honeywell.setup_platform(hass, config, add_devices)) - - -class TestHoneywellRound(unittest.TestCase): - """A test class for Honeywell Round thermostats.""" - - def setup_method(self, method): - """Test the setup method.""" - def fake_temperatures(force_refresh=None): - """Create fake temperatures.""" - temps = [ - {'id': '1', 'temp': 20, 'setpoint': 21, - 'thermostat': 'main', 'name': 'House'}, - {'id': '2', 'temp': 21, 'setpoint': 22, - 'thermostat': 'DOMESTIC_HOT_WATER'}, - ] - return temps - - self.device = mock.MagicMock() - self.device.temperatures.side_effect = fake_temperatures - self.round1 = honeywell.RoundThermostat(self.device, '1', - True, 16) - self.round2 = honeywell.RoundThermostat(self.device, '2', - False, 17) - - def test_attributes(self): - """Test the attributes.""" - self.assertEqual('House', self.round1.name) - self.assertEqual(TEMP_CELSIUS, self.round1.unit_of_measurement) - self.assertEqual(20, self.round1.current_temperature) - self.assertEqual(21, self.round1.target_temperature) - self.assertFalse(self.round1.is_away_mode_on) - - self.assertEqual('Hot Water', self.round2.name) - self.assertEqual(TEMP_CELSIUS, self.round2.unit_of_measurement) - self.assertEqual(21, self.round2.current_temperature) - self.assertEqual(None, self.round2.target_temperature) - self.assertFalse(self.round2.is_away_mode_on) - - def test_away_mode(self): - """Test setting the away mode.""" - self.assertFalse(self.round1.is_away_mode_on) - self.round1.turn_away_mode_on() - self.assertTrue(self.round1.is_away_mode_on) - self.assertEqual(self.device.set_temperature.call_count, 1) - self.assertEqual( - self.device.set_temperature.call_args, mock.call('House', 16) - ) - - self.device.set_temperature.reset_mock() - self.round1.turn_away_mode_off() - self.assertFalse(self.round1.is_away_mode_on) - self.assertEqual(self.device.cancel_temp_override.call_count, 1) - self.assertEqual( - self.device.cancel_temp_override.call_args, mock.call('House') - ) - - def test_set_temperature(self): - """Test setting the temperature.""" - self.round1.set_temperature(25) - self.assertEqual(self.device.set_temperature.call_count, 1) - self.assertEqual( - self.device.set_temperature.call_args, mock.call('House', 25) - ) - - def test_set_hvac_mode(self: unittest.TestCase) -> None: - """Test setting the system operation.""" - self.round1.set_hvac_mode('cool') - self.assertEqual('cool', self.round1.operation) - self.assertEqual('cool', self.device.system_mode) - - self.round1.set_hvac_mode('heat') - self.assertEqual('heat', self.round1.operation) - self.assertEqual('heat', self.device.system_mode) - - -class TestHoneywellUS(unittest.TestCase): - """A test class for Honeywell US thermostats.""" - - def setup_method(self, method): - """Test the setup method.""" - self.client = mock.MagicMock() - self.device = mock.MagicMock() - self.honeywell = honeywell.HoneywellUSThermostat( - self.client, self.device) - - self.device.fan_running = True - self.device.name = 'test' - self.device.temperature_unit = 'F' - self.device.current_temperature = 72 - self.device.setpoint_cool = 78 - self.device.setpoint_heat = 65 - self.device.system_mode = 'heat' - self.device.fan_mode = 'auto' - - def test_properties(self): - """Test the properties.""" - self.assertTrue(self.honeywell.is_fan_on) - self.assertEqual('test', self.honeywell.name) - self.assertEqual(72, self.honeywell.current_temperature) - - def test_unit_of_measurement(self): - """Test the unit of measurement.""" - self.assertEqual(TEMP_FAHRENHEIT, self.honeywell.unit_of_measurement) - self.device.temperature_unit = 'C' - self.assertEqual(TEMP_CELSIUS, self.honeywell.unit_of_measurement) - - def test_target_temp(self): - """Test the target temperature.""" - self.assertEqual(65, self.honeywell.target_temperature) - self.device.system_mode = 'cool' - self.assertEqual(78, self.honeywell.target_temperature) - - def test_set_temp(self): - """Test setting the temperature.""" - self.honeywell.set_temperature(70) - self.assertEqual(70, self.device.setpoint_heat) - self.assertEqual(70, self.honeywell.target_temperature) - - self.device.system_mode = 'cool' - self.assertEqual(78, self.honeywell.target_temperature) - self.honeywell.set_temperature(74) - self.assertEqual(74, self.device.setpoint_cool) - self.assertEqual(74, self.honeywell.target_temperature) - - def test_set_hvac_mode(self: unittest.TestCase) -> None: - """Test setting the HVAC mode.""" - self.honeywell.set_hvac_mode('cool') - self.assertEqual('cool', self.honeywell.operation) - self.assertEqual('cool', self.device.system_mode) - - self.honeywell.set_hvac_mode('heat') - self.assertEqual('heat', self.honeywell.operation) - self.assertEqual('heat', self.device.system_mode) - - def test_set_temp_fail(self): - """Test if setting the temperature fails.""" - self.device.setpoint_heat = mock.MagicMock( - side_effect=somecomfort.SomeComfortError) - self.honeywell.set_temperature(123) - - def test_attributes(self): - """Test the attributes.""" - expected = { - 'fan': 'running', - 'fanmode': 'auto', - 'system_mode': 'heat', - } - self.assertEqual(expected, self.honeywell.device_state_attributes) - expected['fan'] = 'idle' - self.device.fan_running = False - self.assertEqual(expected, self.honeywell.device_state_attributes) - - def test_with_no_fan(self): - """Test if there is on fan.""" - self.device.fan_running = False - self.device.fan_mode = None - expected = { - 'fan': 'idle', - 'fanmode': None, - 'system_mode': 'heat', - } - self.assertEqual(expected, self.honeywell.device_state_attributes) From 2604dd89a60373cf33cfaf666d6ed9f57945318a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 25 Oct 2016 07:01:38 +0200 Subject: [PATCH 021/149] Add test (#3999) --- .coveragerc | 1 - homeassistant/components/sensor/worldclock.py | 8 +++-- tests/components/sensor/test_worldclock.py | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/components/sensor/test_worldclock.py diff --git a/.coveragerc b/.coveragerc index 651ea7cbad7..039fa184e1f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,7 +290,6 @@ omit = homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/vasttrafik.py - homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/switch/acer_projector.py diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 43141b51b3d..7f4d91a78f9 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -17,8 +17,10 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Worldclock Sensor' + ICON = 'mdi:clock' -TIME_STR_FORMAT = "%H:%M" + +TIME_STR_FORMAT = '%H:%M' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TIME_ZONE): cv.time_zone, @@ -27,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Worldclock sensor.""" + """Setup the World clock sensor.""" name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) @@ -35,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WorldClockSensor(Entity): - """Representation of a Worldclock sensor.""" + """Representation of a World clock sensor.""" def __init__(self, time_zone, name): """Initialize the sensor.""" diff --git a/tests/components/sensor/test_worldclock.py b/tests/components/sensor/test_worldclock.py new file mode 100644 index 00000000000..40dd4ee0a5d --- /dev/null +++ b/tests/components/sensor/test_worldclock.py @@ -0,0 +1,36 @@ +"""The test for the World clock sensor platform.""" +import unittest + +from homeassistant.bootstrap import setup_component +from tests.common import get_test_home_assistant +import homeassistant.util.dt as dt_util + + +class TestWorldClockSensor(unittest.TestCase): + """Test the World clock sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.time_zone = dt_util.get_time_zone('America/New_York') + + config = { + 'sensor': { + 'platform': 'worldclock', + 'time_zone': 'America/New_York', + } + } + + self.assertTrue(setup_component(self.hass, 'sensor', config)) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_time(self): + """Test the time at a different location.""" + state = self.hass.states.get('sensor.worldclock_sensor') + assert state is not None + + assert state.state == dt_util.now( + time_zone=self.time_zone).strftime('%H:%M') From e2d23d902a0501d5ef49ed3ca5458e171a0a5d5e Mon Sep 17 00:00:00 2001 From: David-Leon Pohl Date: Tue, 25 Oct 2016 07:18:24 +0200 Subject: [PATCH 022/149] Unittests for ddwrt device tracker and bugfix (#3996) * BUG Message data cannot be changed thus use voluptuous to ensure format * Pilight daemon expects JSON serializable data Thus dict is needed and not a mapping proxy. * Add explanation why dict as message data is needed * Use more obvious voluptuous validation scheme * Pylint: Trailing whitespace * Pilight sensor component * Python 3.4 compatibility * D202 * Use pytest-caplog and no unittest.TestCase * Fix setup/teardown of unittests * Activate coverage testing * Bugfix whitelist filter and use bugfixed pilight library * Use newest pilight library that has a bugfix * Add unittest for pilight hub component * PEP257 for docstrings * Bugfix setting device name from host name and small cleanup - Init with connection error handling is more clear - Comments clean-up * PEP257 * New unittest with full coverage * Upload missing testfixtures * D209 * Handle double quotes in reply * Formatting --- .coveragerc | 1 - .../components/device_tracker/ddwrt.py | 34 ++- tests/components/device_tracker/test_ddwrt.py | 244 ++++++++++++++++++ tests/fixtures/Ddwrt_Status_Lan.txt | 18 ++ tests/fixtures/Ddwrt_Status_Wireless.txt | 13 + 5 files changed, 290 insertions(+), 20 deletions(-) create mode 100644 tests/components/device_tracker/test_ddwrt.py create mode 100644 tests/fixtures/Ddwrt_Status_Lan.txt create mode 100644 tests/fixtures/Ddwrt_Status_Wireless.txt diff --git a/.coveragerc b/.coveragerc index 039fa184e1f..cf5c0a36684 100644 --- a/.coveragerc +++ b/.coveragerc @@ -142,7 +142,6 @@ omit = homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bluetooth_le_tracker.py homeassistant/components/device_tracker/bt_home_hub_5.py - homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 4dc6229566c..9ccb15a1707 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -35,9 +35,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a DD-WRT scanner.""" - scanner = DdWrtDeviceScanner(config[DOMAIN]) - - return scanner if scanner.success_init else None + try: + return DdWrtDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None # pylint: disable=too-many-instance-attributes @@ -53,13 +54,13 @@ class DdWrtDeviceScanner(object): self.lock = threading.Lock() self.last_results = {} - self.mac2name = {} # Test the router is accessible url = 'http://{}/Status_Wireless.live.asp'.format(self.host) data = self.get_ddwrt_data(url) - self.success_init = data is not None + if not data: + raise ConnectionError('Cannot connect to DD-Wrt router') def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -83,14 +84,15 @@ class DdWrtDeviceScanner(object): if not dhcp_leases: return None - # Remove leading and trailing single quotes. - cleaned_str = dhcp_leases.strip().strip('"') - elements = cleaned_str.split('","') - num_clients = int(len(elements)/5) + # Remove leading and trailing quotes and spaces + cleaned_str = dhcp_leases.replace( + "\"", "").replace("\'", "").replace(" ", "") + elements = cleaned_str.split(',') + num_clients = int(len(elements) / 5) self.mac2name = {} for idx in range(0, num_clients): - # This is stupid but the data is a single array - # every 5 elements represents one hosts, the MAC + # The data is a single array + # every 5 elements represents one host, the MAC # is the third element and the name is the first. mac_index = (idx * 5) + 2 if mac_index < len(elements): @@ -105,9 +107,6 @@ class DdWrtDeviceScanner(object): Return boolean if scanning successful. """ - if not self.success_init: - return False - with self.lock: _LOGGER.info('Checking ARP') @@ -123,11 +122,8 @@ class DdWrtDeviceScanner(object): if not active_clients: return False - # This is really lame, instead of using JSON the DD-WRT UI - # uses its own data format for some reason and then - # regex's out values so I guess I have to do the same, - # LAME!!! - + # The DD-WRT UI uses its own data format and then + # regex's out values so this is done here too # Remove leading and trailing single quotes. clean_str = active_clients.strip().strip("'") elements = clean_str.split("','") diff --git a/tests/components/device_tracker/test_ddwrt.py b/tests/components/device_tracker/test_ddwrt.py new file mode 100644 index 00000000000..5e0a90d3bbe --- /dev/null +++ b/tests/components/device_tracker/test_ddwrt.py @@ -0,0 +1,244 @@ +"""The tests for the DD-WRT device tracker platform.""" +import os +import unittest +from unittest import mock +import logging +import requests +import requests_mock + +from homeassistant import config +from homeassistant.bootstrap import setup_component +from homeassistant.components import device_tracker +from homeassistant.const import ( + CONF_PLATFORM, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.util import slugify + +from tests.common import ( + get_test_home_assistant, assert_setup_component, load_fixture) + +TEST_HOST = '127.0.0.1' +_LOGGER = logging.getLogger(__name__) + + +class TestDdwrt(unittest.TestCase): + """Tests for the Ddwrt device tracker platform.""" + + hass = None + + def setup_method(self, _): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.components = ['zone'] + + def teardown_method(self, _): + """Stop everything that was started.""" + try: + os.remove(self.hass.config.path(device_tracker.YAML_DEVICES)) + except FileNotFoundError: + pass + + @mock.patch('homeassistant.components.device_tracker.ddwrt._LOGGER.error') + def test_login_failed(self, mock_error): + """Create a Ddwrt scanner with wrong credentials.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + status_code=401) + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + self.assertTrue( + 'Failed to authenticate' in + str(mock_error.call_args_list[-1])) + + @mock.patch('homeassistant.components.device_tracker.ddwrt._LOGGER.error') + def test_invalid_response(self, mock_error): + """Test error handling when response has an error status.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + status_code=444) + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + self.assertTrue( + 'Invalid response from ddwrt' in + str(mock_error.call_args_list[-1])) + + @mock.patch('homeassistant.components.device_tracker._LOGGER.error') + @mock.patch('homeassistant.components.device_tracker.' + 'ddwrt.DdWrtDeviceScanner.get_ddwrt_data', return_value=None) + def test_no_response(self, data_mock, error_mock): + """Create a Ddwrt scanner with no response in init, should fail.""" + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + self.assertTrue( + 'Error setting up platform' in + str(error_mock.call_args_list[-1])) + + @mock.patch('homeassistant.components.device_tracker.ddwrt.requests.get', + side_effect=requests.exceptions.Timeout) + @mock.patch('homeassistant.components.device_tracker.ddwrt._LOGGER.error') + def test_get_timeout(self, mock_error, mock_request): + """Test get Ddwrt data with request time out.""" + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + self.assertTrue( + 'Connection to the router timed out' in + str(mock_error.call_args_list[-1])) + + def test_scan_devices(self): + """Test creating device info (MAC, name) from response. + + The created known_devices.yaml device info is compared + to the DD-WRT Lan Status request response fixture. + This effectively checks the data parsing functions. + """ + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Wireless.txt')) + mock_request.register_uri( + 'GET', r'http://%s/Status_Lan.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Lan.txt')) + + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + path = self.hass.config.path(device_tracker.YAML_DEVICES) + devices = config.load_yaml_config_file(path) + for device in devices: + self.assertIn( + devices[device]['mac'], + load_fixture('Ddwrt_Status_Lan.txt')) + self.assertIn( + slugify(devices[device]['name']), + load_fixture('Ddwrt_Status_Lan.txt')) + + def test_device_name_no_data(self): + """Test creating device info (MAC only) when no response.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Wireless.txt')) + mock_request.register_uri( + 'GET', r'http://%s/Status_Lan.live.asp' % TEST_HOST, text=None) + + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + path = self.hass.config.path(device_tracker.YAML_DEVICES) + devices = config.load_yaml_config_file(path) + for device in devices: + _LOGGER.error(devices[device]) + self.assertIn( + devices[device]['mac'], + load_fixture('Ddwrt_Status_Lan.txt')) + + def test_device_name_no_dhcp(self): + """Test creating device info (MAC) when missing dhcp response.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Wireless.txt')) + mock_request.register_uri( + 'GET', r'http://%s/Status_Lan.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Lan.txt'). + replace('dhcp_leases', 'missing')) + + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + path = self.hass.config.path(device_tracker.YAML_DEVICES) + devices = config.load_yaml_config_file(path) + for device in devices: + _LOGGER.error(devices[device]) + self.assertIn( + devices[device]['mac'], + load_fixture('Ddwrt_Status_Lan.txt')) + + def test_update_no_data(self): + """Test error handling of no response when active devices checked.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + # First request has to work to setup connection + [{'text': load_fixture('Ddwrt_Status_Wireless.txt')}, + # Second request to get active devices fails + {'text': None}]) + mock_request.register_uri( + 'GET', r'http://%s/Status_Lan.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Lan.txt')) + + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) + + def test_update_wrong_data(self): + """Test error handling of bad response when active devices checked.""" + with requests_mock.Mocker() as mock_request: + mock_request.register_uri( + 'GET', r'http://%s/Status_Wireless.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Wireless.txt'). + replace('active_wireless', 'missing')) + mock_request.register_uri( + 'GET', r'http://%s/Status_Lan.live.asp' % TEST_HOST, + text=load_fixture('Ddwrt_Status_Lan.txt')) + + with assert_setup_component(1): + assert setup_component( + self.hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'ddwrt', + CONF_HOST: TEST_HOST, + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: '0' + }}) diff --git a/tests/fixtures/Ddwrt_Status_Lan.txt b/tests/fixtures/Ddwrt_Status_Lan.txt new file mode 100644 index 00000000000..b61d92c365e --- /dev/null +++ b/tests/fixtures/Ddwrt_Status_Lan.txt @@ -0,0 +1,18 @@ +{lan_mac::AA:BB:CC:DD:EE:F0} +{lan_ip::192.168.1.1} +{lan_ip_prefix::192.168.1.} +{lan_netmask::255.255.255.0} +{lan_gateway::0.0.0.0} +{lan_dns::8.8.8.8} +{lan_proto::dhcp} +{dhcp_daemon::DNSMasq} +{dhcp_start::100} +{dhcp_num::50} +{dhcp_lease_time::1440} +{dhcp_leases:: 'device_1','192.168.1.113','AA:BB:CC:DD:EE:00','1 day 00:00:00','113','device_2','192.168.1.201','AA:BB:CC:DD:EE:01','Static','201'} +{pptp_leases::} +{pppoe_leases::} +{arp_table:: 'device_1','192.168.1.113','AA:BB:CC:DD:EE:00','13','device_2','192.168.1.201','AA:BB:CC:DD:EE:01','1'} +{uptime:: 12:28:48 up 132 days, 18:02, load average: 0.15, 0.19, 0.21} +{ipinfo:: IP: 192.168.0.108} + diff --git a/tests/fixtures/Ddwrt_Status_Wireless.txt b/tests/fixtures/Ddwrt_Status_Wireless.txt new file mode 100644 index 00000000000..5343fea9904 --- /dev/null +++ b/tests/fixtures/Ddwrt_Status_Wireless.txt @@ -0,0 +1,13 @@ +{wl_mac::AA:BB:CC:DD:EE:FF} +{wl_ssid::WIFI_SSD} +{wl_channel::10} +{wl_radio::Radio is On} +{wl_xmit::Auto} +{wl_rate::72 Mbps} +{wl_ack::} +{active_wireless::'AA:BB:CC:DD:EE:00','eth1','3:13:14','72M','24M','HT20','-9','-92','83','1048','AA:BB:CC:DD:EE:01','eth1','10:48:22','72M','72M','HT20','-40','-92','52','664'} +{active_wds::} +{packet_info::SWRXgoodPacket=173673555;SWRXerrorPacket=27;SWTXgoodPacket=311344396;SWTXerrorPacket=3107;} +{uptime:: 12:29:23 up 132 days, 18:03, load average: 0.16, 0.19, 0.20} +{ipinfo:: IP: 192.168.0.108} + From 89e8fb4066e31e979e72d0e7596d32e505300115 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 25 Oct 2016 01:28:34 -0400 Subject: [PATCH 023/149] Configurator support for entity_picture (#4028) --- homeassistant/components/configurator.py | 11 +++++++---- .../www_static/images/logo_philips_hue.png | Bin 0 -> 17083 bytes ...ediaserver.png => logo_plex_mediaserver.png} | Bin homeassistant/components/light/hue.py | 1 + homeassistant/components/media_player/plex.py | 2 +- tests/components/test_configurator.py | 16 +++++++++++++--- 6 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/images/logo_philips_hue.png rename homeassistant/components/frontend/www_static/images/{config_plex_mediaserver.png => logo_plex_mediaserver.png} (100%) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 9f5cb397587..d205b45e446 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -8,7 +8,8 @@ the user has submitted configuration information. """ import logging -from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME +from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ + ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import generate_entity_id _INSTANCES = {} @@ -36,7 +37,8 @@ STATE_CONFIGURED = 'configured' # pylint: disable=too-many-arguments def request_config( hass, name, callback, description=None, description_image=None, - submit_caption=None, fields=None, link_name=None, link_url=None): + submit_caption=None, fields=None, link_name=None, link_url=None, + entity_picture=None): """Create a new request for configuration. Will return an ID to be used for sequent calls. @@ -46,7 +48,7 @@ def request_config( request_id = instance.request_config( name, callback, description, description_image, submit_caption, - fields, link_name, link_url) + fields, link_name, link_url, entity_picture) _REQUESTS[request_id] = instance @@ -104,7 +106,7 @@ class Configurator(object): def request_config( self, name, callback, description, description_image, submit_caption, - fields, link_name, link_url): + fields, link_name, link_url, entity_picture): """Setup a request for configuration.""" entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) @@ -119,6 +121,7 @@ class Configurator(object): ATTR_CONFIGURE_ID: request_id, ATTR_FIELDS: fields, ATTR_FRIENDLY_NAME: name, + ATTR_ENTITY_PICTURE: entity_picture, } data.update({ diff --git a/homeassistant/components/frontend/www_static/images/logo_philips_hue.png b/homeassistant/components/frontend/www_static/images/logo_philips_hue.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4df811fa8d14f6997f7b1daf0189847048655a GIT binary patch literal 17083 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 Date: Mon, 24 Oct 2016 22:33:54 -0700 Subject: [PATCH 024/149] Fix helpers.state tests --- homeassistant/helpers/state.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index c9addefec2b..c8b489a9532 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -14,15 +14,14 @@ from homeassistant.components.sun import ( STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON) from homeassistant.components.switch.mysensors import ( ATTR_IR_CODE, SERVICE_SEND_IR_CODE) -from homeassistant.components.thermostat import ( - ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, +from homeassistant.components.climate import ( + ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HUMIDITY, + ATTR_OPERATION_MODE, ATTR_SWING_MODE, + SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, + SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) -from homeassistant.components.thermostat.ecobee import ( +from homeassistant.components.climate.ecobee import ( ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME) -from homeassistant.components.hvac import ( - ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT, - SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE, - SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, @@ -49,7 +48,7 @@ SERVICE_ATTRIBUTES = { SERVICE_VOLUME_SET: [ATTR_MEDIA_VOLUME_LEVEL], SERVICE_NOTIFY: [ATTR_MESSAGE], SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE], - SERVICE_SET_FAN_MODE: [ATTR_FAN], + SERVICE_SET_FAN_MODE: [ATTR_FAN_MODE], SERVICE_SET_FAN_MIN_ON_TIME: [ATTR_FAN_MIN_ON_TIME], SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], From 2b1f4123db3626a103164d7b6bcc9a5c5e633dd2 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Mon, 24 Oct 2016 22:36:04 -0700 Subject: [PATCH 025/149] Update requirements_all.txt --- requirements_all.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 5ec6876efaa..c23bcb0d6e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,7 +55,6 @@ blinkstick==1.1.8 blockchain==1.3.3 # homeassistant.components.climate.eq3btsmart -# homeassistant.components.thermostat.eq3btsmart # bluepy_devices==0.2.0 # homeassistant.components.notify.aws_lambda @@ -96,7 +95,6 @@ enocean==0.31 # evdev==0.6.1 # homeassistant.components.climate.honeywell -# homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 # homeassistant.components.sensor.fastdotcom @@ -139,7 +137,6 @@ ha-ffmpeg==0.14 hbmqtt==0.7.1 # homeassistant.components.climate.heatmiser -# homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 # homeassistant.components.switch.hikvisioncam @@ -308,7 +305,6 @@ plexapi==2.0.2 pmsensor==0.3 # homeassistant.components.climate.proliphix -# homeassistant.components.thermostat.proliphix proliphix==0.4.0 # homeassistant.components.sensor.systemmonitor @@ -440,7 +436,6 @@ pyvera==0.2.20 pywemo==0.4.7 # homeassistant.components.climate.radiotherm -# homeassistant.components.thermostat.radiotherm radiotherm==1.2 # homeassistant.components.switch.rpi_rf @@ -474,7 +469,6 @@ sleepyq==0.6 snapcast==1.2.2 # homeassistant.components.climate.honeywell -# homeassistant.components.thermostat.honeywell somecomfort==0.3.2 # homeassistant.components.sensor.speedtest From 53ea926292664ea78e615b67b2dcb912470edb08 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Tue, 25 Oct 2016 10:59:20 +0200 Subject: [PATCH 026/149] Fix for see service attributes (#4023) --- homeassistant/components/device_tracker/__init__.py | 3 +-- tests/components/device_tracker/test_init.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 87b628b050b..12735940916 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -103,8 +103,7 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, (ATTR_GPS_ACCURACY, gps_accuracy), (ATTR_BATTERY, battery)) if value is not None} if attributes: - for key, value in attributes: - data[key] = value + data[ATTR_ATTRIBUTES] = attributes hass.services.call(DOMAIN, SERVICE_SEE, data) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 34f89d450eb..cb2da4c3f98 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -283,7 +283,10 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'dev_id': 'some_device', 'host_name': 'example.com', 'location_name': 'Work', - 'gps': [.3, .8] + 'gps': [.3, .8], + 'attributes': { + 'test': 'test' + } } device_tracker.see(self.hass, **params) self.hass.block_till_done() From 86b318e992f9a7b10a602a7555435fbef537d4ab Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Tue, 25 Oct 2016 07:20:40 -0400 Subject: [PATCH 027/149] fix typos in script module strings It looks like some copy / paste in docstrings, clean them up for posterity. --- homeassistant/components/script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index dfaa6b250b0..21511eea1b9 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -51,7 +51,7 @@ SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({ def is_on(hass, entity_id): - """Return if the switch is on based on the statemachine.""" + """Return if the script is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) @@ -154,7 +154,7 @@ class ScriptEntity(ToggleEntity): return self.script.is_running def turn_on(self, **kwargs): - """Turn the entity on.""" + """Turn the script on.""" self.script.run(kwargs.get(ATTR_VARIABLES)) def turn_off(self, **kwargs): From d308ea69cef91e6f46b36ba4d63df0ba6aae5d88 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 25 Oct 2016 17:36:20 +0200 Subject: [PATCH 028/149] Upgrade yahoo-finance to 1.3.2 (#4040) --- homeassistant/components/sensor/yahoo_finance.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index a389a13656d..1cf275c28e7 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['yahoo-finance==1.2.1'] +REQUIREMENTS = ['yahoo-finance==1.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5ec6876efaa..e8d7a671943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ xboxapi==0.1.1 xmltodict==0.10.2 # homeassistant.components.sensor.yahoo_finance -yahoo-finance==1.2.1 +yahoo-finance==1.3.2 # homeassistant.components.sensor.yweather yahooweather==0.8 From 0dfcf40d3730e0758c52fa0b079f326d75d785cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Tue, 25 Oct 2016 20:13:32 +0200 Subject: [PATCH 029/149] [WIP] Config validation error line numbers (#3976) Config validation error line numbers --- homeassistant/bootstrap.py | 7 ++++--- homeassistant/util/yaml.py | 28 +++++++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0eca105952f..ffef2fbc99d 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -431,9 +431,10 @@ def log_exception(ex, domain, config, hass=None): else: message += '{}.'.format(humanize_error(config, ex)) - if hasattr(config, '__line__'): - message += " (See {}:{})".format( - config.__config_file__, config.__line__ or '?') + domain_config = config.get(domain, config) + message += " (See {}:{})".format( + getattr(domain_config, '__config_file__', '?'), + getattr(domain_config, '__line__', '?')) if domain != 'homeassistant': message += (' Please check the docs at ' diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 3ee47e76cf2..6b1bc2227c8 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -134,11 +134,8 @@ def _ordered_dict(loader: SafeLineLoader, nodes = loader.construct_pairs(node) seen = {} # type: Dict - min_line = None - for (key, _), (node, _) in zip(nodes, node.value): - line = getattr(node, '__line__', 'unknown') - if line != 'unknown' and (min_line is None or line < min_line): - min_line = line + for (key, _), (child_node, _) in zip(nodes, node.value): + line = child_node.start_mark.line try: hash(key) @@ -146,7 +143,7 @@ def _ordered_dict(loader: SafeLineLoader, fname = getattr(loader.stream, 'name', '') raise yaml.MarkedYAMLError( context="invalid key: \"{}\"".format(key), - context_mark=yaml.Mark(fname, 0, min_line, -1, None, None) + context_mark=yaml.Mark(fname, 0, line, -1, None, None) ) if key in seen: @@ -161,7 +158,22 @@ def _ordered_dict(loader: SafeLineLoader, processed = OrderedDict(nodes) setattr(processed, '__config_file__', loader.name) - setattr(processed, '__line__', min_line) + setattr(processed, '__line__', node.start_mark.line) + return processed + + +def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node): + """Add line number and file name to Load YAML sequence.""" + obj, = loader.construct_yaml_seq(node) + + class NodeClass(list): + """Wrapper class to be able to add attributes on a list.""" + + pass + + processed = NodeClass(obj) + setattr(processed, '__config_file__', loader.name) + setattr(processed, '__line__', node.start_mark.line) return processed @@ -231,6 +243,8 @@ def _secret_yaml(loader: SafeLineLoader, yaml.SafeLoader.add_constructor('!include', _include_yaml) yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) +yaml.SafeLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq) yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml) yaml.SafeLoader.add_constructor('!secret', _secret_yaml) yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml) From 297a6f6f037e71efea3afc448029c8789a294bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sat, 22 Oct 2016 09:11:50 +0200 Subject: [PATCH 030/149] Improve support for Yamaha receiver * Playback (play, pause, stop, next, previous) * Media title, artist and album --- .../components/media_player/yamaha.py | 95 +++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 027fd607730..9679c9f186c 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -10,13 +10,15 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, + SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MEDIA_TYPE_MUSIC, MediaPlayerDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON) +from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, + STATE_PLAYING, STATE_IDLE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.2.0'] +REQUIREMENTS = ['rxv==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -24,6 +26,10 @@ SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \ SUPPORT_PLAY_MEDIA +# Only supported by some sources +SUPPORT_PLAYBACK = SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_STOP | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' @@ -76,16 +82,25 @@ class YamahaDevice(MediaPlayerDevice): self._source_ignore = source_ignore self._source_names = source_names self._reverse_mapping = None + self._is_playback_supported = False + self._play_status = None self.update() self._name = name self._zone = receiver.zone def update(self): """Get the latest details from the device.""" + self._play_status = self._receiver.play_status() if self._receiver.on: - self._pwstate = STATE_ON + if self._play_status is None: + self._pwstate = STATE_ON + elif self._play_status.playing: + self._pwstate = STATE_PLAYING + else: + self._pwstate = STATE_IDLE else: self._pwstate = STATE_OFF + self._muted = self._receiver.mute self._volume = (self._receiver.volume / 100) + 1 @@ -95,6 +110,8 @@ class YamahaDevice(MediaPlayerDevice): current_source = self._receiver.input self._current_source = self._source_names.get( current_source, current_source) + self._is_playback_supported = self._receiver.is_playback_supported( + self._current_source) def build_source_list(self): """Build the source list.""" @@ -143,7 +160,10 @@ class YamahaDevice(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" - return SUPPORT_YAMAHA + supported_commands = SUPPORT_YAMAHA + if self._is_playback_supported: + supported_commands |= SUPPORT_PLAYBACK + return supported_commands def turn_off(self): """Turn off media player.""" @@ -164,6 +184,34 @@ class YamahaDevice(MediaPlayerDevice): self._receiver.on = True self._volume = (self._receiver.volume / 100) + 1 + def media_play(self): + """Send play commmand.""" + self._call_playback_function(self._receiver.play, "play") + + def media_pause(self): + """Send pause command.""" + self._call_playback_function(self._receiver.pause, "pause") + + def media_stop(self): + """Send stop command.""" + self._call_playback_function(self._receiver.stop, "stop") + + def media_previous_track(self): + """Send previous track command.""" + self._call_playback_function(self._receiver.previous, "previous track") + + def media_next_track(self): + """Send next track command.""" + self._call_playback_function(self._receiver.next, "next track") + + def _call_playback_function(self, function, function_text): + import rxv + try: + function() + except rxv.exceptions.ResponseException: + _LOGGER.warning( + 'Failed to execute %s on %s', function_text, self._name) + def select_source(self, source): """Select input source.""" self._receiver.input = self._reverse_mapping.get(source, source) @@ -179,23 +227,36 @@ class YamahaDevice(MediaPlayerDevice): if media_type == "NET RADIO": self._receiver.net_radio(media_id) + @property + def media_artist(self): + """Artist of current playing media.""" + if self._play_status is not None: + return self._play_status.artist + + @property + def media_album_name(self): + """Album of current playing media.""" + if self._play_status is not None: + return self._play_status.album + @property def media_content_type(self): - """Return the media content type.""" - if self.source == "NET RADIO": + """Content type of current playing media.""" + # Loose assumption that if playback is supported, we are playing music + if self._is_playback_supported: return MEDIA_TYPE_MUSIC + return None @property def media_title(self): - """Return the media title. + """Artist of current playing media.""" + if self._play_status is not None: + song = self._play_status.song + station = self._play_status.station - This will vary by input source, as they provide different - information in metadata. - - """ - if self.source == "NET RADIO": - info = self._receiver.play_status() - if info.song: - return "%s: %s" % (info.station, info.song) + # If both song and station is available, print both, otherwise + # just the one we have. + if song and station: + return '{}: {}'.format(station, song) else: - return info.station + return song or station diff --git a/requirements_all.txt b/requirements_all.txt index e8d7a671943..03f8715ab22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -447,7 +447,7 @@ radiotherm==1.2 # rpi-rf==0.9.5 # homeassistant.components.media_player.yamaha -rxv==0.2.0 +rxv==0.3.0 # homeassistant.components.media_player.samsungtv samsungctl==0.5.1 From 1b2dfb8ed159f9abb460cb834de258e069a12aaf Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Oct 2016 16:38:22 -0400 Subject: [PATCH 031/149] Handle FreeBSD version in updater component (#4048) --- homeassistant/components/updater.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 515e5f431a5..f597c8d1c52 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -108,6 +108,8 @@ def get_newest_version(huuid): info_object['os_version'] = platform.win32_ver()[0] elif platform.system() == 'Darwin': info_object['os_version'] = platform.mac_ver()[0] + elif platform.system() == 'FreeBSD': + info_object['os_version'] = platform.release() elif platform.system() == 'Linux': import distro linux_dist = distro.linux_distribution(full_distribution_name=False) From fe174402d2f069c6956751c5d978efd7a427fc69 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Tue, 25 Oct 2016 14:16:08 -0700 Subject: [PATCH 032/149] Remove more deprecated things --- homeassistant/const.py | 5 ----- homeassistant/helpers/state.py | 18 +++++++----------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c9c8c88b75..56712b9a04d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -314,11 +314,6 @@ SERVICE_SET_COVER_TILT_POSITION = 'set_cover_tilt_position' SERVICE_STOP_COVER = 'stop_cover' SERVICE_STOP_COVER_TILT = 'stop_cover_tilt' -SERVICE_MOVE_UP = 'move_up' -SERVICE_MOVE_DOWN = 'move_down' -SERVICE_MOVE_POSITION = 'move_position' -SERVICE_STOP = 'stop' - # #### API / REMOTE #### SERVER_PORT = 8123 diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index c8b489a9532..21c35332797 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -25,13 +25,13 @@ from homeassistant.components.climate.ecobee import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - SERVICE_CLOSE, SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_SEEK, SERVICE_MOVE_DOWN, SERVICE_MOVE_UP, SERVICE_OPEN, - SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, - STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED) + SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, + STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, + STATE_UNKNOWN, STATE_UNLOCKED) from homeassistant.core import State _LOGGER = logging.getLogger(__name__) @@ -72,10 +72,6 @@ SERVICE_TO_STATE = { SERVICE_ALARM_TRIGGER: STATE_ALARM_TRIGGERED, SERVICE_LOCK: STATE_LOCKED, SERVICE_UNLOCK: STATE_UNLOCKED, - SERVICE_CLOSE: STATE_CLOSED, - SERVICE_OPEN: STATE_OPEN, - SERVICE_MOVE_UP: STATE_OPEN, - SERVICE_MOVE_DOWN: STATE_CLOSED, SERVICE_OPEN_COVER: STATE_OPEN, SERVICE_CLOSE_COVER: STATE_CLOSED } From 961c02f72a67e74f7e9a630fba1b9af5f1f52ecd Mon Sep 17 00:00:00 2001 From: Bjarni Ivarsson Date: Wed, 26 Oct 2016 00:37:47 +0200 Subject: [PATCH 033/149] Sonos improvements (#3997) * Sonos improvements: media_* properties delegate to coordinator if speaker is a slave, media_image_url and media_title now works for radio streams, source selection/list takes speaker model into account, commands on slaves delegate to coordinator. * Fixed failing unit tests. --- .../components/media_player/sonos.py | 229 +++++++++++++----- tests/components/media_player/test_sonos.py | 11 + 2 files changed, 182 insertions(+), 58 deletions(-) mode change 100644 => 100755 homeassistant/components/media_player/sonos.py mode change 100644 => 100755 tests/components/media_player/test_sonos.py diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py old mode 100644 new mode 100755 index 533b385f0fa..dd57a61f230 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -8,6 +8,7 @@ import datetime import logging from os import path import socket +import urllib import voluptuous as vol from homeassistant.components.media_player import ( @@ -44,7 +45,6 @@ SERVICE_RESTORE = 'sonos_restore' SUPPORT_SOURCE_LINEIN = 'Line-in' SUPPORT_SOURCE_TV = 'TV' -SUPPORT_SOURCE_RADIO = 'Radio' SONOS_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, @@ -204,7 +204,15 @@ class SonosDevice(MediaPlayerDevice): self.hass = hass self.volume_increment = 5 self._player = player + self._speaker_info = None self._name = None + self._coordinator = None + self._media_content_id = None + self._media_duration = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None self.update() self.soco_snapshot = Snapshot(self._player) @@ -236,6 +244,8 @@ class SonosDevice(MediaPlayerDevice): return STATE_PLAYING if self._status == 'STOPPED': return STATE_IDLE + if self._status == 'OFF': + return STATE_OFF return STATE_UNKNOWN @property @@ -245,16 +255,89 @@ class SonosDevice(MediaPlayerDevice): def update(self): """Retrieve latest state.""" - self._name = self._player.get_speaker_info()['zone_name'].replace( + self._speaker_info = self._player.get_speaker_info() + self._name = self._speaker_info['zone_name'].replace( ' (R)', '').replace(' (L)', '') if self.available: self._status = self._player.get_current_transport_info().get( 'current_transport_state') - self._trackinfo = self._player.get_current_track_info() + trackinfo = self._player.get_current_track_info() + + if trackinfo['uri'].startswith('x-rincon:'): + # this speaker is a slave, find the coordinator + # the uri of the track is 'x-rincon:{coordinator-id}' + coordinator_id = trackinfo['uri'][9:] + coordinators = [device for device in DEVICES + if device.unique_id == coordinator_id] + self._coordinator = coordinators[0] if coordinators else None + else: + self._coordinator = None + + if not self._coordinator: + mediainfo = self._player.avTransport.GetMediaInfo([ + ('InstanceID', 0) + ]) + + duration = trackinfo.get('duration', '0:00') + # if the speaker is playing from the "line-in" source, getting + # track metadata can return NOT_IMPLEMENTED, which breaks the + # volume logic below + if duration == 'NOT_IMPLEMENTED': + duration = None + else: + duration = sum(60 ** x[0] * int(x[1]) for x in enumerate( + reversed(duration.split(':')))) + + media_image_url = trackinfo.get('album_art', None) + media_artist = trackinfo.get('artist', None) + media_album_name = trackinfo.get('album', None) + media_title = trackinfo.get('title', None) + + if media_image_url in ('', 'NOT_IMPLEMENTED', None): + # fallback to asking the speaker directly + media_image_url = \ + 'http://{host}:{port}/getaa?s=1&u={uri}'.format( + host=self._player.ip_address, + port=1400, + uri=urllib.parse.quote(mediainfo['CurrentURI']) + ) + + if media_artist in ('', 'NOT_IMPLEMENTED', None): + # if listening to a radio stream the media_artist field + # will be empty and the title field will contain the + # filename that is being streamed + current_uri_metadata = mediainfo["CurrentURIMetaData"] + if current_uri_metadata not in \ + ('', 'NOT_IMPLEMENTED', None): + + # currently soco does not have an API for this + import soco + current_uri_metadata = soco.xml.XML.fromstring( + soco.utils.really_utf8(current_uri_metadata)) + + md_title = current_uri_metadata.findtext( + './/{http://purl.org/dc/elements/1.1/}title') + + if md_title not in ('', 'NOT_IMPLEMENTED', None): + media_artist = '' + media_title = md_title + + self._media_content_id = trackinfo.get('title', None) + self._media_duration = duration + self._media_image_url = media_image_url + self._media_artist = media_artist + self._media_album_name = media_album_name + self._media_title = media_title else: - self._status = STATE_OFF - self._trackinfo = {} + self._status = 'OFF' + self._coordinator = None + self._media_content_id = None + self._media_duration = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None @property def volume_level(self): @@ -269,7 +352,10 @@ class SonosDevice(MediaPlayerDevice): @property def media_content_id(self): """Content ID of current playing media.""" - return self._trackinfo.get('title', None) + if self._coordinator: + return self._coordinator.media_content_id + else: + return self._media_content_id @property def media_content_type(self): @@ -279,22 +365,34 @@ class SonosDevice(MediaPlayerDevice): @property def media_duration(self): """Duration of current playing media in seconds.""" - dur = self._trackinfo.get('duration', '0:00') - - # If the speaker is playing from the "line-in" source, getting - # track metadata can return NOT_IMPLEMENTED, which breaks the - # volume logic below - if dur == 'NOT_IMPLEMENTED': - return None - - return sum(60 ** x[0] * int(x[1]) for x in - enumerate(reversed(dur.split(':')))) + if self._coordinator: + return self._coordinator.media_duration + else: + return self._media_duration @property def media_image_url(self): """Image url of current playing media.""" - if 'album_art' in self._trackinfo: - return self._trackinfo['album_art'] + if self._coordinator: + return self._coordinator.media_image_url + else: + return self._media_image_url + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + if self._coordinator: + return self._coordinator.media_artist + else: + return self._media_artist + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + if self._coordinator: + return self._coordinator.media_album_name + else: + return self._media_album_name @property def media_title(self): @@ -303,17 +401,19 @@ class SonosDevice(MediaPlayerDevice): return SUPPORT_SOURCE_LINEIN if self._player.is_playing_tv: return SUPPORT_SOURCE_TV - if 'artist' in self._trackinfo and 'title' in self._trackinfo: - return '{artist} - {title}'.format( - artist=self._trackinfo['artist'], - title=self._trackinfo['title'] - ) - if 'title' in self._status: - return self._trackinfo['title'] + + if self._coordinator: + return self._coordinator.media_title + else: + return self._media_title @property def supported_media_commands(self): """Flag of media commands that are supported.""" + if not self.source_list: + # some devices do not allow source selection + return SUPPORT_SONOS ^ SUPPORT_SELECT_SOURCE + return SUPPORT_SONOS def volume_up(self): @@ -342,14 +442,12 @@ class SonosDevice(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - source = [] + model_name = self._speaker_info['model_name'] - # generate list of supported device - source.append(SUPPORT_SOURCE_LINEIN) - source.append(SUPPORT_SOURCE_TV) - source.append(SUPPORT_SOURCE_RADIO) - - return source + if 'PLAY:5' in model_name: + return [SUPPORT_SOURCE_LINEIN] + elif 'PLAYBAR' in model_name: + return [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV] @property def source(self): @@ -358,8 +456,7 @@ class SonosDevice(MediaPlayerDevice): return SUPPORT_SOURCE_LINEIN if self._player.is_playing_tv: return SUPPORT_SOURCE_TV - if self._player.is_playing_radio: - return SUPPORT_SOURCE_RADIO + return None @only_if_coordinator @@ -367,63 +464,79 @@ class SonosDevice(MediaPlayerDevice): """Turn off media player.""" self._player.pause() - @only_if_coordinator def media_play(self): """Send play command.""" - self._player.play() + if self._coordinator: + self._coordinator.media_play() + else: + self._player.play() - @only_if_coordinator def media_pause(self): """Send pause command.""" - self._player.pause() + if self._coordinator: + self._coordinator.media_pause() + else: + self._player.pause() - @only_if_coordinator def media_next_track(self): """Send next track command.""" - self._player.next() + if self._coordinator: + self._coordinator.media_next_track() + else: + self._player.next() - @only_if_coordinator def media_previous_track(self): """Send next track command.""" - self._player.previous() + if self._coordinator: + self._coordinator.media_previous_track() + else: + self._player.previous() - @only_if_coordinator def media_seek(self, position): """Send seek command.""" - self._player.seek(str(datetime.timedelta(seconds=int(position)))) + if self._coordinator: + self._coordinator.media_seek(position) + else: + self._player.seek(str(datetime.timedelta(seconds=int(position)))) - @only_if_coordinator def clear_playlist(self): """Clear players playlist.""" - self._player.clear_queue() + if self._coordinator: + self._coordinator.clear_playlist() + else: + self._player.clear_queue() @only_if_coordinator def turn_on(self): """Turn the media player on.""" self._player.play() - @only_if_coordinator def play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if kwargs.get(ATTR_MEDIA_ENQUEUE): - from soco.exceptions import SoCoUPnPException - try: - self._player.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error('Error parsing media uri "%s", ' - "please check it's a valid media resource " - 'supported by Sonos', media_id) + if self._coordinator: + self._coordinator.play_media(media_type, media_id, **kwargs) else: - self._player.play_uri(media_id) + if kwargs.get(ATTR_MEDIA_ENQUEUE): + from soco.exceptions import SoCoUPnPException + try: + self._player.add_uri_to_queue(media_id) + except SoCoUPnPException: + _LOGGER.error('Error parsing media uri "%s", ' + "please check it's a valid media resource " + 'supported by Sonos', media_id) + else: + self._player.play_uri(media_id) - @only_if_coordinator def group_players(self): """Group all players under this coordinator.""" - self._player.partymode() + if self._coordinator: + self._coordinator.group_players() + else: + self._player.partymode() @only_if_coordinator def unjoin(self): diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py old mode 100644 new mode 100755 index add1f0c3ce5..d1fb87ef44a --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -19,6 +19,16 @@ class socoDiscoverMock(): return {SoCoMock('192.0.2.1')} +class AvTransportMock(): + """Mock class for the avTransport property on soco.SoCo object.""" + def __init__(self): + pass + + def GetMediaInfo(self, _): + return {'CurrentURI': '', + 'CurrentURIMetaData': ''} + + class SoCoMock(): """Mock class for the soco.SoCo object.""" @@ -26,6 +36,7 @@ class SoCoMock(): """Initialize soco object.""" self.ip_address = ip self.is_visible = True + self.avTransport = AvTransportMock() def get_speaker_info(self): """Return a dict with various data points about the speaker.""" From 1f468fc94d41fd3c518d9ea52b51adabdb0913f2 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Wed, 26 Oct 2016 01:49:51 -0400 Subject: [PATCH 034/149] If no weather advisories were issued, state should return 0 instead Unknown (#4029) * If no weather advisories were issued, state should return 0 instead Unknown * Updated to keep on the same if statement * Revert "Updated to keep on the same if statement" This reverts commit 0e6a94aa0fa9b80dc60c7b222423fe71e1dda81b. --- homeassistant/components/sensor/wunderground.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 69b53b0c259..7abc2a0fc1e 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -117,8 +117,11 @@ class WUndergroundSensor(Entity): else: return self.rest.data[self._condition] - if self.rest.alerts and self._condition == 'alerts': - return len(self.rest.alerts) + if self._condition == 'alerts': + if self.rest.alerts: + return len(self.rest.alerts) + else: + return 0 return STATE_UNKNOWN @property From f58647849a1f682c2fb6abc519ce0240751994d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Oct 2016 23:17:34 -0700 Subject: [PATCH 035/149] Fix Z-Wave: Pin cython in Dockerfile (#4055) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b42d7edcc89..4b6560f3b44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN script/build_python_openzwave && \ COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install mysqlclient psycopg2 uvloop + pip3 install --upgrade cython==0.24.1 mysqlclient psycopg2 uvloop # Copy source COPY . . From 7f48c007937060349b437d8897f2d010b4455c16 Mon Sep 17 00:00:00 2001 From: Scott O'Neil Date: Wed, 26 Oct 2016 01:22:17 -0500 Subject: [PATCH 036/149] Adding timer setting functionality to sonos component (#3941) * Adding timer setting functionality to sonos component * Adding clear sleep timer for Sonos --- .../components/media_player/services.yaml | 19 ++++++++ .../components/media_player/sonos.py | 43 +++++++++++++++++++ tests/components/media_player/test_sonos.py | 26 +++++++++++ 3 files changed, 88 insertions(+) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 323fda37a02..ee0225d2a76 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -185,3 +185,22 @@ sonos_restore: entity_id: description: Name(s) of entites that will be restored. Platform dependent. example: 'media_player.living_room_sonos' + +sonos_set_sleep_timer: + description: Set a Sonos timer + + fields: + entity_id: + description: Name(s) of entites that will have a timer set. + example: 'media_player.living_room_sonos' + sleep_time: + description: Number of seconds to set the timer + example: '900' + +sonos_clear_sleep_timer: + description: Clear a Sonos timer + + fields: + entity_id: + description: Name(s) of entites that will have the timer cleared. + example: 'media_player.living_room_sonos' diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index dd57a61f230..3bc6778ce39 100755 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -42,14 +42,24 @@ SERVICE_GROUP_PLAYERS = 'sonos_group_players' SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_SNAPSHOT = 'sonos_snapshot' SERVICE_RESTORE = 'sonos_restore' +SERVICE_SET_TIMER = 'sonos_set_sleep_timer' +SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SUPPORT_SOURCE_LINEIN = 'Line-in' SUPPORT_SOURCE_TV = 'TV' +# Service call validation schemas +ATTR_SLEEP_TIME = 'sleep_time' + SONOS_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, }) +SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({ + vol.Required(ATTR_SLEEP_TIME): vol.All(vol.Coerce(int), + vol.Range(min=0, max=86399)) +}) + # List of devices that have been registered DEVICES = [] @@ -126,6 +136,16 @@ def register_services(hass): descriptions.get(SERVICE_RESTORE), schema=SONOS_SCHEMA) + hass.services.register(DOMAIN, SERVICE_SET_TIMER, + _set_sleep_timer_service, + descriptions.get(SERVICE_SET_TIMER), + schema=SONOS_SET_TIMER_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_CLEAR_TIMER, + _clear_sleep_timer_service, + descriptions.get(SERVICE_CLEAR_TIMER), + schema=SONOS_SCHEMA) + def _apply_service(service, service_func, *service_func_args): """Internal func for applying a service.""" @@ -162,6 +182,19 @@ def _restore_service(service): _apply_service(service, SonosDevice.restore) +def _set_sleep_timer_service(service): + """Set a timer.""" + _apply_service(service, + SonosDevice.set_sleep_timer, + service.data[ATTR_SLEEP_TIME]) + + +def _clear_sleep_timer_service(service): + """Set a timer.""" + _apply_service(service, + SonosDevice.clear_sleep_timer) + + def only_if_coordinator(func): """Decorator for coordinator. @@ -553,6 +586,16 @@ class SonosDevice(MediaPlayerDevice): """Restore snapshot for the player.""" self.soco_snapshot.restore(True) + @only_if_coordinator + def set_sleep_timer(self, sleep_time): + """Set the timer on the player.""" + self._player.set_sleep_timer(sleep_time) + + @only_if_coordinator + def clear_sleep_timer(self): + """Clear the timer on the player.""" + self._player.set_sleep_timer(None) + @property def available(self): """Return True if player is reachable, False otherwise.""" diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index d1fb87ef44a..42f39ca5572 100755 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -38,6 +38,10 @@ class SoCoMock(): self.is_visible = True self.avTransport = AvTransportMock() + def clear_sleep_timer(self): + """Clear the sleep timer.""" + return + def get_speaker_info(self): """Return a dict with various data points about the speaker.""" return {'serial_number': 'B8-E9-37-BO-OC-BA:2', @@ -74,6 +78,10 @@ class SoCoMock(): """Cause the speaker to join all other speakers in the network.""" return + def set_sleep_timer(self, sleep_time_seconds): + """Set the sleep timer.""" + return + def unjoin(self): """Cause the speaker to separate itself from other speakers.""" return @@ -154,6 +162,24 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(unjoinMock.call_count, 1) self.assertEqual(unjoinMock.call_args, mock.call()) + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(SoCoMock, 'set_sleep_timer') + def test_sonos_set_sleep_timer(self, set_sleep_timerMock): + """Ensuring soco methods called for sonos_set_sleep_timer service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + device.set_sleep_timer(30) + set_sleep_timerMock.assert_called_once_with(30) + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(SoCoMock, 'set_sleep_timer') + def test_sonos_clear_sleep_timer(self, set_sleep_timerMock): + """Ensuring soco methods called for sonos_clear_sleep_timer service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + device.set_sleep_timer(None) + set_sleep_timerMock.assert_called_once_with(None) + @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') def test_sonos_snapshot(self, snapshotMock): From 57402bcb43f7fe2d2d31356eea43ab5013a280df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Oct 2016 23:30:43 -0700 Subject: [PATCH 037/149] Update .coveragerc --- .coveragerc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index c75c06e8d84..1f5f3dcf5a4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -131,10 +131,6 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py - homeassistant/components/cover/homematic.py - homeassistant/components/cover/rpi_gpio.py - homeassistant/components/cover/scsgate.py - homeassistant/components/cover/wink.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py From fe3aed0f0c9a8138faadab188a74d7fb4e9b1d81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Oct 2016 23:32:58 -0700 Subject: [PATCH 038/149] Update .coveragerc --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1f5f3dcf5a4..c75c06e8d84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -131,6 +131,10 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py + homeassistant/components/cover/homematic.py + homeassistant/components/cover/rpi_gpio.py + homeassistant/components/cover/scsgate.py + homeassistant/components/cover/wink.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py From 4833e992fb931d236d98dd90e40480e83c50bd92 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Oct 2016 23:38:32 -0700 Subject: [PATCH 039/149] Pin cython==0.24.1 (#4057) --- Dockerfile | 2 +- script/build_python_openzwave | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4b6560f3b44..b42d7edcc89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN script/build_python_openzwave && \ COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --upgrade cython==0.24.1 mysqlclient psycopg2 uvloop + pip3 install mysqlclient psycopg2 uvloop # Copy source COPY . . diff --git a/script/build_python_openzwave b/script/build_python_openzwave index 8f88cace558..d4e3e07b769 100755 --- a/script/build_python_openzwave +++ b/script/build_python_openzwave @@ -20,5 +20,6 @@ else fi git checkout python3 +pip3 install --upgrade cython==0.24.1 PYTHON_EXEC=`which python3` make build PYTHON_EXEC=`which python3` make install From 4fb0b27310375f4809d8875b7d5d268013aadd66 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Thu, 27 Oct 2016 02:31:49 -0400 Subject: [PATCH 040/149] Wunderground sensor with alerts exceeds API limits (#4070) * Fixes issue #4067 - Wunderground sensor with alerts exceeds API limits To avoid hitting the max limit of 500 calls per day, this patch keeps weather conditions being updated each 5 minutes and weather advisories each 15 minutes. This formula will result the following: conditions -> 300 seconds -> 5 minutes -> 12 req/h -> 288 req/day alerts -> 900 seconds -> 15 minutes -> 4 req/h -> 96 req/day * Using timedelta in minutes instead seconds --- homeassistant/components/sensor/wunderground.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 7abc2a0fc1e..98a06c7545a 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -25,7 +25,8 @@ _LOGGER = logging.getLogger(__name__) CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" CONF_PWS_ID = 'pws_id' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) +MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -187,7 +188,7 @@ class WUndergroundData(object): return url + '.json' - @Throttle(MIN_TIME_BETWEEN_UPDATES) + @Throttle(MIN_TIME_BETWEEN_UPDATES_OBSERVATION) def update(self): """Get the latest data from WUnderground.""" try: @@ -202,7 +203,7 @@ class WUndergroundData(object): self.data = None raise - @Throttle(MIN_TIME_BETWEEN_UPDATES) + @Throttle(MIN_TIME_BETWEEN_UPDATES_ALERTS) def update_alerts(self): """Get the latest alerts data from WUnderground.""" try: From 5d3956ea980fd433c5214122a0b1daf1e561341a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 27 Oct 2016 02:33:43 -0400 Subject: [PATCH 041/149] Cleanup use of MQTT in emulated_hue tests (#4068) * Use unix newlines on test_emulated_hue This commit switches the test_emulated_hue module to use unix newlines instead of the DOS style that were there before. (using dos2unix on the file) This makes it consistent with the other files in the repo. * Cleanup emulated_hue tests Previously these tests relied on the mqtt light platform as test devices to control with the emulated hue. However, this was pretty heavyweight and required running an MQTT broker in the tests. Instead this commit switches it to use the demo light platform which is strictly in memory. Fixes #3549 --- tests/components/test_emulated_hue.py | 897 ++++++++++++-------------- 1 file changed, 425 insertions(+), 472 deletions(-) diff --git a/tests/components/test_emulated_hue.py b/tests/components/test_emulated_hue.py index 9433aacc20b..fb55551bdf3 100755 --- a/tests/components/test_emulated_hue.py +++ b/tests/components/test_emulated_hue.py @@ -1,472 +1,425 @@ -"""The tests for the emulated Hue component.""" -import time -import json -import threading -import asyncio - -import unittest -import requests - -from homeassistant import bootstrap, const, core -import homeassistant.components as core_components -from homeassistant.components import emulated_hue, http, light, mqtt -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.components.emulated_hue import ( - HUE_API_STATE_ON, HUE_API_STATE_BRI) - -from tests.common import get_test_instance_port, get_test_home_assistant - -HTTP_SERVER_PORT = get_test_instance_port() -BRIDGE_SERVER_PORT = get_test_instance_port() -MQTT_BROKER_PORT = get_test_instance_port() - -BRIDGE_URL_BASE = "http://127.0.0.1:{}".format(BRIDGE_SERVER_PORT) + "{}" -JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON} - -mqtt_broker = None - - -def setUpModule(): - """Setup things to be run when tests are started.""" - global mqtt_broker - - mqtt_broker = MQTTBroker('127.0.0.1', MQTT_BROKER_PORT) - mqtt_broker.start() - - -def tearDownModule(): - """Stop everything that was started.""" - global mqtt_broker - - mqtt_broker.stop() - - -def setup_hass_instance(emulated_hue_config): - """Setup the Home Assistant instance to test.""" - hass = get_test_home_assistant() - - # We need to do this to get access to homeassistant/turn_(on,off) - core_components.setup(hass, {core.DOMAIN: {}}) - - bootstrap.setup_component( - hass, http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}}) - - bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config) - - return hass - - -def start_hass_instance(hass): - """Start the Home Assistant instance to test.""" - hass.start() - time.sleep(0.05) - - -class TestEmulatedHue(unittest.TestCase): - """Test the emulated Hue component.""" - - hass = None - - @classmethod - def setUpClass(cls): - """Setup the class.""" - cls.hass = setup_hass_instance({ - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT - }}) - - start_hass_instance(cls.hass) - - @classmethod - def tearDownClass(cls): - """Stop the class.""" - cls.hass.stop() - - def test_description_xml(self): - """Test the description.""" - import xml.etree.ElementTree as ET - - result = requests.get( - BRIDGE_URL_BASE.format('/description.xml'), timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('text/xml' in result.headers['content-type']) - - # Make sure the XML is parsable - try: - ET.fromstring(result.text) - except: - self.fail('description.xml is not valid XML!') - - def test_create_username(self): - """Test the creation of an username.""" - request_json = {'devicetype': 'my_device'} - - result = requests.post( - BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), - timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('application/json' in result.headers['content-type']) - - resp_json = result.json() - success_json = resp_json[0] - - self.assertTrue('success' in success_json) - self.assertTrue('username' in success_json['success']) - - def test_valid_username_request(self): - """Test request with a valid username.""" - request_json = {'invalid_key': 'my_device'} - - result = requests.post( - BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), - timeout=5) - - self.assertEqual(result.status_code, 400) - - -class TestEmulatedHueExposedByDefault(unittest.TestCase): - """Test class for emulated hue component.""" - - @classmethod - def setUpClass(cls): - """Setup the class.""" - cls.hass = setup_hass_instance({ - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, - emulated_hue.CONF_EXPOSE_BY_DEFAULT: True - } - }) - - bootstrap.setup_component(cls.hass, mqtt.DOMAIN, { - 'mqtt': { - 'broker': '127.0.0.1', - 'port': MQTT_BROKER_PORT - } - }) - - bootstrap.setup_component(cls.hass, light.DOMAIN, { - 'light': [ - { - 'platform': 'mqtt', - 'name': 'Office light', - 'state_topic': 'office/rgb1/light/status', - 'command_topic': 'office/rgb1/light/switch', - 'brightness_state_topic': 'office/rgb1/brightness/status', - 'brightness_command_topic': 'office/rgb1/brightness/set', - 'optimistic': True - }, - { - 'platform': 'mqtt', - 'name': 'Bedroom light', - 'state_topic': 'bedroom/rgb1/light/status', - 'command_topic': 'bedroom/rgb1/light/switch', - 'brightness_state_topic': 'bedroom/rgb1/brightness/status', - 'brightness_command_topic': 'bedroom/rgb1/brightness/set', - 'optimistic': True - }, - { - 'platform': 'mqtt', - 'name': 'Kitchen light', - 'state_topic': 'kitchen/rgb1/light/status', - 'command_topic': 'kitchen/rgb1/light/switch', - 'brightness_state_topic': 'kitchen/rgb1/brightness/status', - 'brightness_command_topic': 'kitchen/rgb1/brightness/set', - 'optimistic': True - } - ] - }) - - start_hass_instance(cls.hass) - - # Kitchen light is explicitly excluded from being exposed - kitchen_light_entity = cls.hass.states.get('light.kitchen_light') - attrs = dict(kitchen_light_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE] = False - cls.hass.states.set( - kitchen_light_entity.entity_id, kitchen_light_entity.state, - attributes=attrs) - - @classmethod - def tearDownClass(cls): - """Stop the class.""" - cls.hass.stop() - - def test_discover_lights(self): - """Test the discovery of lights.""" - result = requests.get( - BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('application/json' in result.headers['content-type']) - - result_json = result.json() - - # Make sure the lights we added to the config are there - self.assertTrue('light.office_light' in result_json) - self.assertTrue('light.bedroom_light' in result_json) - self.assertTrue('light.kitchen_light' not in result_json) - - def test_get_light_state(self): - """Test the getting of light state.""" - # Turn office light on and set to 127 brightness - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_ON, - { - const.ATTR_ENTITY_ID: 'light.office_light', - light.ATTR_BRIGHTNESS: 127 - }, - blocking=True) - - office_json = self.perform_get_light_state('light.office_light', 200) - - self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) - self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) - - # Turn bedroom light off - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_OFF, - { - const.ATTR_ENTITY_ID: 'light.bedroom_light' - }, - blocking=True) - - bedroom_json = self.perform_get_light_state('light.bedroom_light', 200) - - self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False) - self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0) - - # Make sure kitchen light isn't accessible - kitchen_url = '/api/username/lights/{}'.format('light.kitchen_light') - kitchen_result = requests.get( - BRIDGE_URL_BASE.format(kitchen_url), timeout=5) - - self.assertEqual(kitchen_result.status_code, 404) - - def test_put_light_state(self): - """Test the seeting of light states.""" - self.perform_put_test_on_office_light() - - # Turn the bedroom light on first - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_ON, - {const.ATTR_ENTITY_ID: 'light.bedroom_light', - light.ATTR_BRIGHTNESS: 153}, - blocking=True) - - bedroom_light = self.hass.states.get('light.bedroom_light') - self.assertEqual(bedroom_light.state, STATE_ON) - self.assertEqual(bedroom_light.attributes[light.ATTR_BRIGHTNESS], 153) - - # Go through the API to turn it off - bedroom_result = self.perform_put_light_state( - 'light.bedroom_light', False) - - bedroom_result_json = bedroom_result.json() - - self.assertEqual(bedroom_result.status_code, 200) - self.assertTrue( - 'application/json' in bedroom_result.headers['content-type']) - - self.assertEqual(len(bedroom_result_json), 1) - - # Check to make sure the state changed - bedroom_light = self.hass.states.get('light.bedroom_light') - self.assertEqual(bedroom_light.state, STATE_OFF) - - # Make sure we can't change the kitchen light state - kitchen_result = self.perform_put_light_state( - 'light.kitchen_light', True) - self.assertEqual(kitchen_result.status_code, 404) - - def test_put_with_form_urlencoded_content_type(self): - """Test the form with urlencoded content.""" - # Needed for Alexa - self.perform_put_test_on_office_light( - 'application/x-www-form-urlencoded') - - # Make sure we fail gracefully when we can't parse the data - data = {'key1': 'value1', 'key2': 'value2'} - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("light.office_light")), - data=data) - - self.assertEqual(result.status_code, 400) - - def test_entity_not_found(self): - """Test for entity which are not found.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format("not.existant_entity")), - timeout=5) - - self.assertEqual(result.status_code, 404) - - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("non.existant_entity")), - timeout=5) - - self.assertEqual(result.status_code, 404) - - def test_allowed_methods(self): - """Test the allowed methods.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("light.office_light"))) - - self.assertEqual(result.status_code, 405) - - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format("light.office_light")), - data={'key1': 'value1'}) - - self.assertEqual(result.status_code, 405) - - result = requests.put( - BRIDGE_URL_BASE.format('/api/username/lights'), - data={'key1': 'value1'}) - - self.assertEqual(result.status_code, 405) - - def test_proper_put_state_request(self): - """Test the request to set the state.""" - # Test proper on value parsing - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("light.office_light")), - data=json.dumps({HUE_API_STATE_ON: 1234})) - - self.assertEqual(result.status_code, 400) - - # Test proper brightness value parsing - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("light.office_light")), - data=json.dumps({ - HUE_API_STATE_ON: True, - HUE_API_STATE_BRI: 'Hello world!' - })) - - self.assertEqual(result.status_code, 400) - - def perform_put_test_on_office_light(self, - content_type='application/json'): - """Test the setting of a light.""" - # Turn the office light off first - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_OFF, - {const.ATTR_ENTITY_ID: 'light.office_light'}, - blocking=True) - - office_light = self.hass.states.get('light.office_light') - self.assertEqual(office_light.state, STATE_OFF) - - # Go through the API to turn it on - office_result = self.perform_put_light_state( - 'light.office_light', True, 56, content_type) - - office_result_json = office_result.json() - - self.assertEqual(office_result.status_code, 200) - self.assertTrue( - 'application/json' in office_result.headers['content-type']) - - self.assertEqual(len(office_result_json), 2) - - # Check to make sure the state changed - office_light = self.hass.states.get('light.office_light') - self.assertEqual(office_light.state, STATE_ON) - self.assertEqual(office_light.attributes[light.ATTR_BRIGHTNESS], 56) - - def perform_get_light_state(self, entity_id, expected_status): - """Test the gettting of a light state.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format(entity_id)), timeout=5) - - self.assertEqual(result.status_code, expected_status) - - if expected_status == 200: - self.assertTrue( - 'application/json' in result.headers['content-type']) - - return result.json() - - return None - - def perform_put_light_state(self, entity_id, is_on, brightness=None, - content_type='application/json'): - """Test the setting of a light state.""" - url = BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format(entity_id)) - - req_headers = {'Content-Type': content_type} - - data = {HUE_API_STATE_ON: is_on} - - if brightness is not None: - data[HUE_API_STATE_BRI] = brightness - - result = requests.put( - url, data=json.dumps(data), timeout=5, headers=req_headers) - return result - - -class MQTTBroker(object): - """Encapsulates an embedded MQTT broker.""" - - def __init__(self, host, port): - """Initialize a new instance.""" - from hbmqtt.broker import Broker - - self._loop = asyncio.new_event_loop() - - hbmqtt_config = { - 'listeners': { - 'default': { - 'max-connections': 50000, - 'type': 'tcp', - 'bind': '{}:{}'.format(host, port) - } - }, - 'auth': { - 'plugins': ['auth.anonymous'], - 'allow-anonymous': True - } - } - - self._broker = Broker(config=hbmqtt_config, loop=self._loop) - - self._thread = threading.Thread(target=self._run_loop) - self._started_ev = threading.Event() - - def start(self): - """Start the broker.""" - self._thread.start() - self._started_ev.wait() - - def stop(self): - """Stop the broker.""" - self._loop.call_soon_threadsafe(asyncio.async, self._broker.shutdown()) - self._loop.call_soon_threadsafe(self._loop.stop) - self._thread.join() - - def _run_loop(self): - """Run the loop.""" - asyncio.set_event_loop(self._loop) - self._loop.run_until_complete(self._broker_coroutine()) - - self._started_ev.set() - - self._loop.run_forever() - self._loop.close() - - @asyncio.coroutine - def _broker_coroutine(self): - """The Broker coroutine.""" - yield from self._broker.start() +"""The tests for the emulated Hue component.""" +import time +import json +import threading +import asyncio + +import unittest +import requests + +from homeassistant import bootstrap, const, core +import homeassistant.components as core_components +from homeassistant.components import emulated_hue, http, light +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.emulated_hue import ( + HUE_API_STATE_ON, HUE_API_STATE_BRI) + +from tests.common import get_test_instance_port, get_test_home_assistant + +HTTP_SERVER_PORT = get_test_instance_port() +BRIDGE_SERVER_PORT = get_test_instance_port() + +BRIDGE_URL_BASE = "http://127.0.0.1:{}".format(BRIDGE_SERVER_PORT) + "{}" +JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON} + + +def setup_hass_instance(emulated_hue_config): + """Setup the Home Assistant instance to test.""" + hass = get_test_home_assistant() + + # We need to do this to get access to homeassistant/turn_(on,off) + core_components.setup(hass, {core.DOMAIN: {}}) + + bootstrap.setup_component( + hass, http.DOMAIN, + {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}}) + + bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config) + + return hass + + +def start_hass_instance(hass): + """Start the Home Assistant instance to test.""" + hass.start() + time.sleep(0.05) + + +class TestEmulatedHue(unittest.TestCase): + """Test the emulated Hue component.""" + + hass = None + + @classmethod + def setUpClass(cls): + """Setup the class.""" + cls.hass = setup_hass_instance({ + emulated_hue.DOMAIN: { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT + }}) + + start_hass_instance(cls.hass) + + @classmethod + def tearDownClass(cls): + """Stop the class.""" + cls.hass.stop() + + def test_description_xml(self): + """Test the description.""" + import xml.etree.ElementTree as ET + + result = requests.get( + BRIDGE_URL_BASE.format('/description.xml'), timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('text/xml' in result.headers['content-type']) + + # Make sure the XML is parsable + try: + ET.fromstring(result.text) + except: + self.fail('description.xml is not valid XML!') + + def test_create_username(self): + """Test the creation of an username.""" + request_json = {'devicetype': 'my_device'} + + result = requests.post( + BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), + timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('application/json' in result.headers['content-type']) + + resp_json = result.json() + success_json = resp_json[0] + + self.assertTrue('success' in success_json) + self.assertTrue('username' in success_json['success']) + + def test_valid_username_request(self): + """Test request with a valid username.""" + request_json = {'invalid_key': 'my_device'} + + result = requests.post( + BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), + timeout=5) + + self.assertEqual(result.status_code, 400) + + +class TestEmulatedHueExposedByDefault(unittest.TestCase): + """Test class for emulated hue component.""" + + @classmethod + def setUpClass(cls): + """Setup the class.""" + cls.hass = setup_hass_instance({ + emulated_hue.DOMAIN: { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, + emulated_hue.CONF_EXPOSE_BY_DEFAULT: True + } + }) + + bootstrap.setup_component(cls.hass, light.DOMAIN, { + 'light': [ + { + 'platform': 'demo', + } + ] + }) + + start_hass_instance(cls.hass) + + # Kitchen light is explicitly excluded from being exposed + kitchen_light_entity = cls.hass.states.get('light.kitchen_lights') + attrs = dict(kitchen_light_entity.attributes) + attrs[emulated_hue.ATTR_EMULATED_HUE] = False + cls.hass.states.set( + kitchen_light_entity.entity_id, kitchen_light_entity.state, + attributes=attrs) + + @classmethod + def tearDownClass(cls): + """Stop the class.""" + cls.hass.stop() + + def test_discover_lights(self): + """Test the discovery of lights.""" + result = requests.get( + BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('application/json' in result.headers['content-type']) + + result_json = result.json() + + # Make sure the lights we added to the config are there + self.assertTrue('light.ceiling_lights' in result_json) + self.assertTrue('light.bed_light' in result_json) + self.assertTrue('light.kitchen_lights' not in result_json) + + def test_get_light_state(self): + """Test the getting of light state.""" + # Turn office light on and set to 127 brightness + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_ON, + { + const.ATTR_ENTITY_ID: 'light.ceiling_lights', + light.ATTR_BRIGHTNESS: 127 + }, + blocking=True) + + office_json = self.perform_get_light_state('light.ceiling_lights', 200) + + self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) + self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) + + # Turn bedroom light off + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_OFF, + { + const.ATTR_ENTITY_ID: 'light.bed_light' + }, + blocking=True) + + bedroom_json = self.perform_get_light_state('light.bed_light', 200) + + self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False) + self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0) + + # Make sure kitchen light isn't accessible + kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights') + kitchen_result = requests.get( + BRIDGE_URL_BASE.format(kitchen_url), timeout=5) + + self.assertEqual(kitchen_result.status_code, 404) + + def test_put_light_state(self): + """Test the seeting of light states.""" + self.perform_put_test_on_ceiling_lights() + + # Turn the bedroom light on first + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_ON, + {const.ATTR_ENTITY_ID: 'light.bed_light', + light.ATTR_BRIGHTNESS: 153}, + blocking=True) + + bed_light = self.hass.states.get('light.bed_light') + self.assertEqual(bed_light.state, STATE_ON) + self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153) + + # Go through the API to turn it off + bedroom_result = self.perform_put_light_state( + 'light.bed_light', False) + + bedroom_result_json = bedroom_result.json() + + self.assertEqual(bedroom_result.status_code, 200) + self.assertTrue( + 'application/json' in bedroom_result.headers['content-type']) + + self.assertEqual(len(bedroom_result_json), 1) + + # Check to make sure the state changed + bed_light = self.hass.states.get('light.bed_light') + self.assertEqual(bed_light.state, STATE_OFF) + + # Make sure we can't change the kitchen light state + kitchen_result = self.perform_put_light_state( + 'light.kitchen_light', True) + self.assertEqual(kitchen_result.status_code, 404) + + def test_put_with_form_urlencoded_content_type(self): + """Test the form with urlencoded content.""" + # Needed for Alexa + self.perform_put_test_on_ceiling_lights( + 'application/x-www-form-urlencoded') + + # Make sure we fail gracefully when we can't parse the data + data = {'key1': 'value1', 'key2': 'value2'} + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), data=data) + + self.assertEqual(result.status_code, 400) + + def test_entity_not_found(self): + """Test for entity which are not found.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format("not.existant_entity")), + timeout=5) + + self.assertEqual(result.status_code, 404) + + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format("non.existant_entity")), + timeout=5) + + self.assertEqual(result.status_code, 404) + + def test_allowed_methods(self): + """Test the allowed methods.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights"))) + + self.assertEqual(result.status_code, 405) + + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format("light.ceiling_lights")), + data={'key1': 'value1'}) + + self.assertEqual(result.status_code, 405) + + result = requests.put( + BRIDGE_URL_BASE.format('/api/username/lights'), + data={'key1': 'value1'}) + + self.assertEqual(result.status_code, 405) + + def test_proper_put_state_request(self): + """Test the request to set the state.""" + # Test proper on value parsing + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), + data=json.dumps({HUE_API_STATE_ON: 1234})) + + self.assertEqual(result.status_code, 400) + + # Test proper brightness value parsing + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), data=json.dumps({ + HUE_API_STATE_ON: True, + HUE_API_STATE_BRI: 'Hello world!' + })) + + self.assertEqual(result.status_code, 400) + + def perform_put_test_on_ceiling_lights(self, + content_type='application/json'): + """Test the setting of a light.""" + # Turn the office light off first + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_OFF, + {const.ATTR_ENTITY_ID: 'light.ceiling_lights'}, + blocking=True) + + ceiling_lights = self.hass.states.get('light.ceiling_lights') + self.assertEqual(ceiling_lights.state, STATE_OFF) + + # Go through the API to turn it on + office_result = self.perform_put_light_state( + 'light.ceiling_lights', True, 56, content_type) + + office_result_json = office_result.json() + + self.assertEqual(office_result.status_code, 200) + self.assertTrue( + 'application/json' in office_result.headers['content-type']) + + self.assertEqual(len(office_result_json), 2) + + # Check to make sure the state changed + ceiling_lights = self.hass.states.get('light.ceiling_lights') + self.assertEqual(ceiling_lights.state, STATE_ON) + self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56) + + def perform_get_light_state(self, entity_id, expected_status): + """Test the gettting of a light state.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format(entity_id)), timeout=5) + + self.assertEqual(result.status_code, expected_status) + + if expected_status == 200: + self.assertTrue( + 'application/json' in result.headers['content-type']) + + return result.json() + + return None + + def perform_put_light_state(self, entity_id, is_on, brightness=None, + content_type='application/json'): + """Test the setting of a light state.""" + url = BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format(entity_id)) + + req_headers = {'Content-Type': content_type} + + data = {HUE_API_STATE_ON: is_on} + + if brightness is not None: + data[HUE_API_STATE_BRI] = brightness + + result = requests.put( + url, data=json.dumps(data), timeout=5, headers=req_headers) + return result + + +class MQTTBroker(object): + """Encapsulates an embedded MQTT broker.""" + + def __init__(self, host, port): + """Initialize a new instance.""" + from hbmqtt.broker import Broker + + self._loop = asyncio.new_event_loop() + + hbmqtt_config = { + 'listeners': { + 'default': { + 'max-connections': 50000, + 'type': 'tcp', + 'bind': '{}:{}'.format(host, port) + } + }, + 'auth': { + 'plugins': ['auth.anonymous'], + 'allow-anonymous': True + } + } + + self._broker = Broker(config=hbmqtt_config, loop=self._loop) + + self._thread = threading.Thread(target=self._run_loop) + self._started_ev = threading.Event() + + def start(self): + """Start the broker.""" + self._thread.start() + self._started_ev.wait() + + def stop(self): + """Stop the broker.""" + self._loop.call_soon_threadsafe(asyncio.async, self._broker.shutdown()) + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join() + + def _run_loop(self): + """Run the loop.""" + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._broker_coroutine()) + + self._started_ev.set() + + self._loop.run_forever() + self._loop.close() + + @asyncio.coroutine + def _broker_coroutine(self): + """The Broker coroutine.""" + yield from self._broker.start() From c6d5987109947fe279c887ee849a8136dad9f9a7 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 27 Oct 2016 02:46:13 -0400 Subject: [PATCH 042/149] Create Currencylayer exchange rate sensor (#4062) * Added Currencylayer exchange rate sensor * Updated .coveragerc to include currencylayer * Update currencylayer.py * Added Conf_name --- .coveragerc | 1 + .../components/sensor/currencylayer.py | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 homeassistant/components/sensor/currencylayer.py diff --git a/.coveragerc b/.coveragerc index c75c06e8d84..7eea7740297 100644 --- a/.coveragerc +++ b/.coveragerc @@ -235,6 +235,7 @@ omit = homeassistant/components/sensor/bom.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py + homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py new file mode 100644 index 00000000000..b1686e8302d --- /dev/null +++ b/homeassistant/components/sensor/currencylayer.py @@ -0,0 +1,118 @@ +"""Support for currencylayer.com exchange rates service.""" +from datetime import timedelta +import logging +import requests +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_PAYLOAD) + +_RESOURCE = 'http://apilayer.net/api/live' +_LOGGER = logging.getLogger(__name__) +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) +CONF_BASE = 'base' +CONF_QUOTE = 'quote' +DEFAULT_BASE = 'USD' +DEFAULT_NAME = 'CurrencyLayer Sensor' +ICON = 'mdi:currency' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Currencylayer sensor.""" + payload = config.get(CONF_PAYLOAD) + rest = CurrencylayerData( + _RESOURCE, + config.get(CONF_API_KEY), + config.get(CONF_BASE, 'USD'), + payload + ) + response = requests.get(_RESOURCE, params={'source': + config.get(CONF_BASE, 'USD'), + 'access_key': + config.get(CONF_API_KEY), + 'format': 1}, timeout=10) + sensors = [] + for variable in config['quote']: + sensors.append(CurrencylayerSensor(rest, config.get(CONF_BASE, 'USD'), + variable)) + if "error" in response.json(): + _LOGGER.error("Check your Currencylayer API") + return False + else: + add_devices(sensors) + rest.update() + + +class CurrencylayerSensor(Entity): + """Implementing the Currencylayer sensor.""" + + def __init__(self, rest, base, quote): + """Initialize the sensor.""" + self.rest = rest + self._quote = quote + self._base = base + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return str(self._base) + str(self._quote) + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update current conditions.""" + self.rest.update() + value = self.rest.data + if value is not None: + self._state = round(value[str(self._base) + str(self._quote)], 4) + + +# pylint: disable=too-few-public-methods +class CurrencylayerData(object): + """Get data from Currencylayer.org.""" + + # pylint: disable=too-many-arguments + def __init__(self, resource, api_key, base, data): + """Initialize the data object.""" + self._resource = resource + self._api_key = api_key + self._base = base + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Currencylayer.""" + try: + result = requests.get(self._resource, params={'source': self._base, + 'access_key': + self._api_key, + 'format': 1}, + timeout=10) + if "error" in result.json(): + raise ValueError(result.json()["error"]["info"]) + else: + self.data = result.json()['quotes'] + _LOGGER.debug("Currencylayer data updated: %s", + result.json()['timestamp']) + except ValueError as err: + _LOGGER.error("Check Currencylayer API %s", err.args) + self.data = None From 3d897e0e52aaad041b922aca777df4ed661fca28 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 27 Oct 2016 02:46:44 -0400 Subject: [PATCH 043/149] Add discovery for yamaha component (#4061) This uses the discovery code from netdisco/ha to discover yamaha receivers. The old discovery code remains if discovery is turned of in HA, at least for now. Though it probably is worth turning that off in the future. --- homeassistant/components/discovery.py | 3 ++- homeassistant/components/media_player/yamaha.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 32e1bbd5f6a..65a5af79bfb 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.2'] +REQUIREMENTS = ['netdisco==0.7.5'] DOMAIN = 'discovery' @@ -33,6 +33,7 @@ SERVICE_HANDLERS = { 'plex_mediaserver': ('media_player', 'plex'), 'roku': ('media_player', 'roku'), 'sonos': ('media_player', 'sonos'), + 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), } diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 027fd607730..40ca9151e50 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -47,7 +47,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): source_ignore = config.get(CONF_SOURCE_IGNORE) source_names = config.get(CONF_SOURCE_NAMES) - if host is None: + if discovery_info is not None: + name = discovery_info[0] + model = discovery_info[1] + ctrl_url = discovery_info[2] + desc_url = discovery_info[3] + receivers = rxv.RXV( + ctrl_url, + model_name=model, + friendly_name=name, + unit_desc_url=desc_url).zone_controllers() + _LOGGER.info("Receivers: %s", receivers) + elif host is None: receivers = [] for recv in rxv.find(): receivers.extend(recv.zone_controllers()) @@ -73,8 +84,8 @@ class YamahaDevice(MediaPlayerDevice): self._pwstate = STATE_OFF self._current_source = None self._source_list = None - self._source_ignore = source_ignore - self._source_names = source_names + self._source_ignore = source_ignore or [] + self._source_names = source_names or {} self._reverse_mapping = None self.update() self._name = name diff --git a/requirements_all.txt b/requirements_all.txt index 6528b07fb08..a746d7bbf34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -271,7 +271,7 @@ mficlient==0.3.0 miflora==0.1.9 # homeassistant.components.discovery -netdisco==0.7.2 +netdisco==0.7.5 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 From b3ad7989ae1bb5dd54c4cc30d9524701e1f2cab2 Mon Sep 17 00:00:00 2001 From: bestlibre Date: Thu, 27 Oct 2016 08:48:57 +0200 Subject: [PATCH 044/149] Influxdb sensor (#4060) * Influxdb sensor with voluptuous configuration validation * Adding sensor to coveragerc since there is no test for now --- .coveragerc | 1 + homeassistant/components/sensor/influxdb.py | 181 ++++++++++++++++++++ requirements_all.txt | 1 + 3 files changed, 183 insertions(+) create mode 100644 homeassistant/components/sensor/influxdb.py diff --git a/.coveragerc b/.coveragerc index 7eea7740297..973980ff7e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -253,6 +253,7 @@ omit = homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py + homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/lastfm.py diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py new file mode 100644 index 00000000000..59f13405808 --- /dev/null +++ b/homeassistant/components/sensor/influxdb.py @@ -0,0 +1,181 @@ +""" +InfluxDB component which allows you to get data from an Influx database. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.influxdb/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_USERNAME, + CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL, + CONF_NAME, CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE) +from homeassistant.const import STATE_UNKNOWN +from homeassistant.util import Throttle + +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8086 +DEFAULT_DATABASE = 'home_assistant' +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = False +DEFAULT_GROUP_FUNCTION = 'mean' +DEFAULT_FIELD = 'value' + +CONF_DB_NAME = 'database' +CONF_QUERIES = 'queries' +CONF_GROUP_FUNCTION = 'group_function' +CONF_FIELD = 'field' +CONF_MEASUREMENT_NAME = 'measurement' +CONF_WHERE = 'where' + +REQUIREMENTS = ['influxdb==3.0.0'] + +_QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Required(CONF_MEASUREMENT_NAME): cv.string, + vol.Required(CONF_WHERE): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_GROUP_FUNCTION, default=DEFAULT_GROUP_FUNCTION): + cv.string, + vol.Optional(CONF_FIELD, default=DEFAULT_FIELD): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_QUERIES): [_QUERY_SCHEME], + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean +}) + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the InfluxDB component.""" + influx_conf = {'host': config[CONF_HOST], + 'port': config.get(CONF_PORT), + 'username': config.get(CONF_USERNAME), + 'password': config.get(CONF_PASSWORD), + 'ssl': config.get(CONF_SSL), + 'verify_ssl': config.get(CONF_VERIFY_SSL)} + + dev = [] + + for query in config.get(CONF_QUERIES): + sensor = InfluxSensor(hass, influx_conf, query) + if sensor.connected: + dev.append(sensor) + + add_devices(dev) + + +class InfluxSensor(Entity): + """Implementation of a Influxdb sensor.""" + + def __init__(self, hass, influx_conf, query): + """Initialize the sensor.""" + from influxdb import InfluxDBClient, exceptions + self._name = query.get(CONF_NAME) + self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) + value_template = query.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + self._value_template = value_template + self._value_template.hass = hass + else: + self._value_template = None + database = query.get(CONF_DB_NAME) + self._state = None + self._hass = hass + formated_query = "select {}({}) as value from {} where {}"\ + .format(query.get(CONF_GROUP_FUNCTION), + query.get(CONF_FIELD), + query.get(CONF_MEASUREMENT_NAME), + query.get(CONF_WHERE)) + influx = InfluxDBClient(host=influx_conf['host'], + port=influx_conf['port'], + username=influx_conf['username'], + password=influx_conf['password'], + database=database, + ssl=influx_conf['ssl'], + verify_ssl=influx_conf['verify_ssl']) + try: + influx.query("select * from /.*/ LIMIT 1;") + self.connected = True + self.data = InfluxSensorData(influx, formated_query) + self.update() + except exceptions.InfluxDBClientError as exc: + _LOGGER.error("Database host is not accessible due to '%s', please" + " check your entries in the configuration file and" + " that the database exists and is READ/WRITE.", exc) + self.connected = False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Polling needed.""" + return True + + def update(self): + """Get the latest data from influxdb and updates the states.""" + self.data.update() + value = self.data.value + if value is None: + value = STATE_UNKNOWN + if self._value_template is not None: + value = self._value_template.render_with_possible_json_value( + str(value), STATE_UNKNOWN) + + self._state = value + + +# pylint: disable=too-few-public-methods +class InfluxSensorData(object): + """Class for handling the data retrieval.""" + + def __init__(self, influx, query): + """Initialize the data object.""" + self.influx = influx + self.query = query + self.value = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data with a shell command.""" + _LOGGER.info('Running query: %s', self.query) + + points = list(self.influx.query(self.query).get_points()) + if len(points) == 0: + _LOGGER.error('Query returned no points : %s', self.query) + return + if len(points) > 1: + _LOGGER.warning('Query returned multiple points, only first one' + ' shown : %s', self.query) + self.value = points[0].get('value') diff --git a/requirements_all.txt b/requirements_all.txt index a746d7bbf34..6233415c08d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -230,6 +230,7 @@ https://github.com/web-push-libs/pywebpush/archive/e743dc92558fc62178d255c001892 https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # homeassistant.components.influxdb +# homeassistant.components.sensor.influxdb influxdb==3.0.0 # homeassistant.components.insteon_hub From 541fec05344e82fa2b6f449aee136ad2c87fb730 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Oct 2016 23:50:11 -0700 Subject: [PATCH 045/149] Sort .coveragerc alphabetically. --- .coveragerc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 973980ff7e8..df71e507c28 100644 --- a/.coveragerc +++ b/.coveragerc @@ -139,8 +139,8 @@ omit = homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/bbox.py - homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bluetooth_le_tracker.py + homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/icloud.py @@ -253,9 +253,9 @@ omit = homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py - homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py + homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py From 235e1a0885e381a82cc7327af4896de9d93fbdc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 27 Oct 2016 08:51:13 +0200 Subject: [PATCH 046/149] Minor improvements to RPi camera platform (#4059) * Try to create output file instead of checking write permissions * Kill raspistill process during shutdown --- homeassistant/components/camera/rpi_camera.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index 22ab72ad8e7..c5603dac142 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -12,7 +12,8 @@ import shutil import voluptuous as vol from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_FILE_PATH) +from homeassistant.const import (CONF_NAME, CONF_FILE_PATH, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000 DEFAULT_VERTICAL_FLIP = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FILE_PATH): cv.isfile, + vol.Optional(CONF_FILE_PATH): cv.string, vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP): @@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +def kill_raspistill(*args): + """Kill any previously running raspistill process..""" + subprocess.Popen(['killall', 'raspistill'], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT) + + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Raspberry Camera.""" if shutil.which("raspistill") is None: @@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } ) - if not os.access(setup_config[CONF_FILE_PATH], os.W_OK): + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill) + + try: + # Try to create an empty file (or open existing) to ensure we have + # proper permissions. + open(setup_config[CONF_FILE_PATH], 'a').close() + + add_devices([RaspberryCamera(setup_config)]) + except PermissionError: _LOGGER.error("File path is not writable") return False - - add_devices([RaspberryCamera(setup_config)]) + except FileNotFoundError: + _LOGGER.error("Could not create output file (missing directory?)") + return False class RaspberryCamera(Camera): @@ -93,9 +110,7 @@ class RaspberryCamera(Camera): self._config = device_info # Kill if there's raspistill instance - subprocess.Popen(['killall', 'raspistill'], - stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT) + kill_raspistill() cmd_args = [ 'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH], From d9999f36e85bbe915e5a86de038ce2e378f41adb Mon Sep 17 00:00:00 2001 From: Simon Szustkowski Date: Thu, 27 Oct 2016 08:56:51 +0200 Subject: [PATCH 047/149] Added a ThingSpeak component (#4027) * Added a ThingSpeak component * Forgot a colon. Fixed it * Some config variables are better required * New requirements created by the script * Updated the .coveragerc * Fixed small linting errors * Removed unneccessary validation * Even more linting error fixes * Changed the way the component listens to state changes * Removed unneccessary declaration of 'state' variable, referring to new_state instead --- .coveragerc | 1 + homeassistant/components/thingspeak.py | 70 ++++++++++++++++++++++++++ requirements_all.txt | 3 ++ 3 files changed, 74 insertions(+) create mode 100644 homeassistant/components/thingspeak.py diff --git a/.coveragerc b/.coveragerc index df71e507c28..03e145a74de 100644 --- a/.coveragerc +++ b/.coveragerc @@ -307,6 +307,7 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py + homeassistant/components/thingspeak.py homeassistant/components/upnp.py homeassistant/components/weather/openweathermap.py homeassistant/components/zeroconf.py diff --git a/homeassistant/components/thingspeak.py b/homeassistant/components/thingspeak.py new file mode 100644 index 00000000000..f1689c1833e --- /dev/null +++ b/homeassistant/components/thingspeak.py @@ -0,0 +1,70 @@ +"""A component to submit data to thingspeak.""" +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, CONF_ID, CONF_WHITELIST, + STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.event as event + +REQUIREMENTS = ['thingspeak==0.4.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'thingspeak' +TIMEOUT = 5 + +# Validate the config +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ID): int, + vol.Required(CONF_WHITELIST): cv.string + }), + }, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the thingspeak environment.""" + import thingspeak + + # Read out config values + conf = config[DOMAIN] + api_key = conf.get(CONF_API_KEY) + channel_id = conf.get(CONF_ID) + entity = conf.get(CONF_WHITELIST) + + try: + channel = thingspeak.Channel( + channel_id, api_key=api_key, timeout=TIMEOUT) + channel.get() + except: + _LOGGER.error("Error while accessing the ThingSpeak channel. " + "Please check that the channel exists and your " + "API key is correct.") + return False + + def thingspeak_listener(entity_id, old_state, new_state): + """Listen for new events and send them to thingspeak.""" + if new_state is None or new_state.state in ( + STATE_UNKNOWN, '', STATE_UNAVAILABLE): + return + try: + if new_state.entity_id != entity: + return + _state = state_helper.state_as_number(new_state) + except ValueError: + return + try: + channel.update({'field1': _state}) + except: + _LOGGER.error( + 'Error while sending value "%s" to Thingspeak', + _state) + + event.track_state_change(hass, entity, thingspeak_listener) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index 6233415c08d..818ff621b0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -495,6 +495,9 @@ tellive-py==0.5.2 # homeassistant.components.sensor.temper temperusb==1.5.1 +# homeassistant.components.thingspeak +thingspeak==0.4.0 + # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission transmissionrpc==0.11 From d5368f6f78688229012bbc171ce45102080e60cb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 27 Oct 2016 09:16:23 +0200 Subject: [PATCH 048/149] Async bootstrap / component init (#3991) * Async bootstrap * Adress comments * Fix tests * More fixes * Tests fixes --- homeassistant/bootstrap.py | 338 ++++--- homeassistant/components/__init__.py | 32 +- .../components/automation/__init__.py | 60 +- .../components/device_tracker/__init__.py | 4 +- homeassistant/components/group.py | 18 +- .../components/persistent_notification.py | 40 +- homeassistant/config.py | 39 +- homeassistant/helpers/entity.py | 5 +- homeassistant/helpers/entity_component.py | 43 +- homeassistant/loader.py | 32 +- homeassistant/scripts/check_config.py | 10 +- homeassistant/util/dt.py | 10 +- homeassistant/util/yaml.py | 5 +- tests/common.py | 21 +- tests/components/automation/test_event.py | 8 +- tests/components/automation/test_init.py | 34 +- tests/components/automation/test_mqtt.py | 8 +- .../automation/test_numeric_state.py | 46 +- tests/components/automation/test_state.py | 179 ++-- tests/components/automation/test_sun.py | 22 +- tests/components/automation/test_template.py | 55 +- tests/components/automation/test_time.py | 71 +- tests/components/binary_sensor/test_mqtt.py | 8 +- tests/components/binary_sensor/test_nx584.py | 19 +- tests/components/cover/test_rfxtrx.py | 22 +- tests/components/device_tracker/test_mqtt.py | 6 +- tests/components/light/test_mqtt_json.py | 14 +- tests/components/light/test_rfxtrx.py | 20 +- tests/components/lock/test_mqtt.py | 8 +- tests/components/mqtt/test_init.py | 8 +- tests/components/mqtt/test_server.py | 9 +- tests/components/notify/test_file.py | 16 +- tests/components/recorder/test_init.py | 4 +- tests/components/sensor/test_mqtt.py | 6 +- tests/components/sensor/test_rfxtrx.py | 18 +- tests/components/sensor/test_yr.py | 6 +- tests/components/switch/test_mqtt.py | 8 +- tests/components/switch/test_rfxtrx.py | 26 +- tests/components/test_conversation.py | 13 +- tests/components/test_emulated_hue.py | 854 +++++++++--------- tests/components/test_influxdb.py | 13 +- tests/components/test_init.py | 11 +- tests/components/test_logentries.py | 31 +- tests/components/test_logger.py | 13 +- tests/components/test_panel_custom.py | 3 +- tests/components/test_rfxtrx.py | 22 +- tests/components/test_sleepiq.py | 4 +- tests/components/test_splunk.py | 31 +- tests/components/test_statsd.py | 44 +- tests/helpers/test_discovery.py | 2 +- tests/helpers/test_state.py | 4 +- tests/scripts/test_check_config.py | 21 +- tests/test_bootstrap.py | 84 +- tests/test_config.py | 134 +-- 54 files changed, 1431 insertions(+), 1131 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ffef2fbc99d..26dc2b977c6 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,11 +1,10 @@ """Provides methods to bootstrap a home assistant instance.""" - +import asyncio import logging import logging.handlers import os import sys from collections import defaultdict -from threading import RLock from types import ModuleType from typing import Any, Optional, Dict @@ -19,6 +18,8 @@ import homeassistant.config as conf_util import homeassistant.core as core import homeassistant.loader as loader import homeassistant.util.package as pkg_util +from homeassistant.util.async import ( + run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.util.yaml import clear_secret_cache from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError @@ -26,7 +27,6 @@ from homeassistant.helpers import ( event_decorators, service, config_per_platform, extract_domain_configs) _LOGGER = logging.getLogger(__name__) -_SETUP_LOCK = RLock() _CURRENT_SETUP = [] ATTR_COMPONENT = 'component' @@ -39,11 +39,23 @@ _PERSISTENT_VALIDATION = set() def setup_component(hass: core.HomeAssistant, domain: str, config: Optional[Dict]=None) -> bool: """Setup a component and all its dependencies.""" + return run_coroutine_threadsafe( + async_setup_component(hass, domain, config), loop=hass.loop).result() + + +@asyncio.coroutine +def async_setup_component(hass: core.HomeAssistant, domain: str, + config: Optional[Dict]=None) -> bool: + """Setup a component and all its dependencies. + + This method need to run in a executor. + """ if domain in hass.config.components: _LOGGER.debug('Component %s already set up.', domain) return True - _ensure_loader_prepared(hass) + if not loader.PREPARED: + yield from hass.loop.run_in_executor(None, loader.prepare, hass) if config is None: config = defaultdict(dict) @@ -55,7 +67,8 @@ def setup_component(hass: core.HomeAssistant, domain: str, return False for component in components: - if not _setup_component(hass, component, config): + res = yield from _async_setup_component(hass, component, config) + if not res: _LOGGER.error('Component %s failed to setup', component) return False @@ -64,7 +77,11 @@ def setup_component(hass: core.HomeAssistant, domain: str, def _handle_requirements(hass: core.HomeAssistant, component, name: str) -> bool: - """Install the requirements for a component.""" + """Install the requirements for a component. + + Asyncio don't support file operation jet. + This method need to run in a executor. + """ if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'): return True @@ -77,65 +94,82 @@ def _handle_requirements(hass: core.HomeAssistant, component, return True -def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: - """Setup a component for Home Assistant.""" +@asyncio.coroutine +def _async_setup_component(hass: core.HomeAssistant, + domain: str, config) -> bool: + """Setup a component for Home Assistant. + + This method is a coroutine. + """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements if domain in hass.config.components: return True - with _SETUP_LOCK: - # It might have been loaded while waiting for lock - if domain in hass.config.components: - return True + if domain in _CURRENT_SETUP: + _LOGGER.error('Attempt made to setup %s during setup of %s', + domain, domain) + return False - if domain in _CURRENT_SETUP: - _LOGGER.error('Attempt made to setup %s during setup of %s', - domain, domain) + config = yield from async_prepare_setup_component(hass, config, domain) + + if config is None: + return False + + component = loader.get_component(domain) + _CURRENT_SETUP.append(domain) + + try: + if hasattr(component, 'async_setup'): + result = yield from component.async_setup(hass, config) + else: + result = yield from hass.loop.run_in_executor( + None, component.setup, hass, config) + + if result is False: + _LOGGER.error('component %s failed to initialize', domain) return False - - config = prepare_setup_component(hass, config, domain) - - if config is None: + elif result is not True: + _LOGGER.error('component %s did not return boolean if setup ' + 'was successful. Disabling component.', domain) + loader.set_component(domain, None) return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error during setup of component %s', domain) + return False + finally: + _CURRENT_SETUP.remove(domain) - component = loader.get_component(domain) - _CURRENT_SETUP.append(domain) + hass.config.components.append(component.DOMAIN) - try: - result = component.setup(hass, config) - if result is False: - _LOGGER.error('component %s failed to initialize', domain) - return False - elif result is not True: - _LOGGER.error('component %s did not return boolean if setup ' - 'was successful. Disabling component.', domain) - loader.set_component(domain, None) - return False - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error during setup of component %s', domain) - return False - finally: - _CURRENT_SETUP.remove(domain) + # Assumption: if a component does not depend on groups + # it communicates with devices + if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ + hass.pool.worker_count <= 10: + hass.pool.add_worker() - hass.config.components.append(component.DOMAIN) + hass.bus.async_fire( + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + ) - # Assumption: if a component does not depend on groups - # it communicates with devices - if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ - hass.pool.worker_count <= 10: - hass.pool.add_worker() - - hass.bus.fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} - ) - - return True + return True def prepare_setup_component(hass: core.HomeAssistant, config: dict, domain: str): """Prepare setup of a component and return processed config.""" + return run_coroutine_threadsafe( + async_prepare_setup_component(hass, config, domain), loop=hass.loop + ).result() + + +@asyncio.coroutine +def async_prepare_setup_component(hass: core.HomeAssistant, config: dict, + domain: str): + """Prepare setup of a component and return processed config. + + This method is a coroutine. + """ # pylint: disable=too-many-return-statements component = loader.get_component(domain) missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) @@ -151,7 +185,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, try: config = component.CONFIG_SCHEMA(config) except vol.Invalid as ex: - log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass) return None elif hasattr(component, 'PLATFORM_SCHEMA'): @@ -161,7 +195,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, try: p_validated = component.PLATFORM_SCHEMA(p_config) except vol.Invalid as ex: - log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass) continue # Not all platform components follow same pattern for platforms @@ -171,8 +205,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, platforms.append(p_validated) continue - platform = prepare_setup_platform(hass, config, domain, - p_name) + platform = yield from async_prepare_setup_platform( + hass, config, domain, p_name) if platform is None: continue @@ -180,10 +214,11 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, # Validate platform specific schema if hasattr(platform, 'PLATFORM_SCHEMA'): try: + # pylint: disable=no-member p_validated = platform.PLATFORM_SCHEMA(p_validated) except vol.Invalid as ex: - log_exception(ex, '{}.{}'.format(domain, p_name), - p_validated, hass) + async_log_exception(ex, '{}.{}'.format(domain, p_name), + p_validated, hass) continue platforms.append(p_validated) @@ -195,7 +230,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if key not in filter_keys} config[domain] = platforms - if not _handle_requirements(hass, component, domain): + res = yield from hass.loop.run_in_executor( + None, _handle_requirements, hass, component, domain) + if not res: return None return config @@ -204,7 +241,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, platform_name: str) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup.""" - _ensure_loader_prepared(hass) + return run_coroutine_threadsafe( + async_prepare_setup_platform(hass, config, domain, platform_name), + loop=hass.loop + ).result() + + +@asyncio.coroutine +def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, + platform_name: str) \ + -> Optional[ModuleType]: + """Load a platform and makes sure dependencies are setup. + + This method is a coroutine. + """ + if not loader.PREPARED: + yield from hass.loop.run_in_executor(None, loader.prepare, hass) platform_path = PLATFORM_FORMAT.format(domain, platform_name) @@ -218,7 +270,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, message = ('Unable to find the following platforms: ' + ', '.join(list(_PERSISTENT_PLATFORMS)) + '(please check your configuration)') - persistent_notification.create( + persistent_notification.async_create( hass, message, 'Invalid platforms', 'platform_errors') return None @@ -228,14 +280,17 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, # Load dependencies for component in getattr(platform, 'DEPENDENCIES', []): - if not setup_component(hass, component, config): + res = yield from async_setup_component(hass, component, config) + if not res: _LOGGER.error( 'Unable to prepare setup for platform %s because ' 'dependency %s could not be initialized', platform_path, component) return None - if not _handle_requirements(hass, platform, platform_path): + res = yield from hass.loop.run_in_executor( + None, _handle_requirements, hass, platform, platform_path) + if not res: return None return platform @@ -261,15 +316,50 @@ def from_config_dict(config: Dict[str, Any], hass.config.config_dir = config_dir mount_local_lib_path(config_dir) + @asyncio.coroutine + def _async_init_from_config_dict(future): + try: + re_hass = yield from async_from_config_dict( + config, hass, config_dir, enable_log, verbose, skip_pip, + log_rotate_days) + future.set_result(re_hass) + # pylint: disable=broad-except + except Exception as exc: + future.set_exception(exc) + + # run task + future = asyncio.Future() + asyncio.Task(_async_init_from_config_dict(future), loop=hass.loop) + hass.loop.run_until_complete(future) + + return future.result() + + +@asyncio.coroutine +# pylint: disable=too-many-branches, too-many-statements, too-many-arguments +def async_from_config_dict(config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str]=None, + enable_log: bool=True, + verbose: bool=False, + skip_pip: bool=False, + log_rotate_days: Any=None) \ + -> Optional[core.HomeAssistant]: + """Try to configure Home Assistant from a config dict. + + Dynamically loads required components and its dependencies. + This method is a coroutine. + """ core_config = config.get(core.DOMAIN, {}) try: - conf_util.process_ha_core_config(hass, core_config) + yield from conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: - log_exception(ex, 'homeassistant', core_config, hass) + async_log_exception(ex, 'homeassistant', core_config, hass) return None - conf_util.process_ha_config_upgrade(hass) + yield from hass.loop.run_in_executor( + None, conf_util.process_ha_config_upgrade, hass) if enable_log: enable_logging(hass, verbose, log_rotate_days) @@ -279,7 +369,8 @@ def from_config_dict(config: Dict[str, Any], _LOGGER.warning('Skipping pip installation of required modules. ' 'This may cause issues.') - _ensure_loader_prepared(hass) + if not loader.PREPARED: + yield from hass.loop.run_in_executor(None, loader.prepare, hass) # Make a copy because we are mutating it. # Convert it to defaultdict so components can always have config dict @@ -291,29 +382,25 @@ def from_config_dict(config: Dict[str, Any], components = set(key.split(' ')[0] for key in config.keys() if key != core.DOMAIN) - # Setup in a thread to avoid blocking - def component_setup(): - """Set up a component.""" - if not core_components.setup(hass, config): - _LOGGER.error('Home Assistant core failed to initialize. ' - 'Further initialization aborted.') - return hass + # setup components + # pylint: disable=not-an-iterable + res = yield from core_components.async_setup(hass, config) + if not res: + _LOGGER.error('Home Assistant core failed to initialize. ' + 'Further initialization aborted.') + return hass - persistent_notification.setup(hass, config) + yield from persistent_notification.async_setup(hass, config) - _LOGGER.info('Home Assistant core initialized') + _LOGGER.info('Home Assistant core initialized') - # Give event decorators access to HASS - event_decorators.HASS = hass - service.HASS = hass + # Give event decorators access to HASS + event_decorators.HASS = hass + service.HASS = hass - # Setup the components - for domain in loader.load_order_components(components): - _setup_component(hass, domain, config) - - hass.loop.run_until_complete( - hass.loop.run_in_executor(None, component_setup) - ) + # Setup the components + for domain in loader.load_order_components(components): + yield from _async_setup_component(hass, domain, config) return hass @@ -331,27 +418,62 @@ def from_config_file(config_path: str, if hass is None: hass = core.HomeAssistant() + @asyncio.coroutine + def _async_init_from_config_file(future): + try: + re_hass = yield from async_from_config_file( + config_path, hass, verbose, skip_pip, log_rotate_days) + future.set_result(re_hass) + # pylint: disable=broad-except + except Exception as exc: + future.set_exception(exc) + + # run task + future = asyncio.Future() + asyncio.Task(_async_init_from_config_file(future), loop=hass.loop) + hass.loop.run_until_complete(future) + + return future.result() + + +@asyncio.coroutine +def async_from_config_file(config_path: str, + hass: core.HomeAssistant, + verbose: bool=False, + skip_pip: bool=True, + log_rotate_days: Any=None): + """Read the configuration file and try to start all the functionality. + + Will add functionality to 'hass' parameter. + This method is a coroutine. + """ # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + yield from hass.loop.run_in_executor( + None, mount_local_lib_path, config_dir) enable_logging(hass, verbose, log_rotate_days) try: - config_dict = conf_util.load_yaml_config_file(config_path) + config_dict = yield from hass.loop.run_in_executor( + None, conf_util.load_yaml_config_file, config_path) except HomeAssistantError: return None finally: clear_secret_cache() - return from_config_dict(config_dict, hass, enable_log=False, - skip_pip=skip_pip) + hass = yield from async_from_config_dict( + config_dict, hass, enable_log=False, skip_pip=skip_pip) + return hass def enable_logging(hass: core.HomeAssistant, verbose: bool=False, log_rotate_days=None) -> None: - """Setup the logging.""" + """Setup the logging. + + Async friendly. + """ logging.basicConfig(level=logging.INFO) fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s%(reset)s") @@ -407,44 +529,50 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, 'Unable to setup error log %s (access denied)', err_log_path) -def _ensure_loader_prepared(hass: core.HomeAssistant) -> None: - """Ensure Home Assistant loader is prepared.""" - if not loader.PREPARED: - loader.prepare(hass) - - -def log_exception(ex, domain, config, hass=None): +def log_exception(ex, domain, config, hass): """Generate log exception for config validation.""" + run_callback_threadsafe( + hass.loop, async_log_exception, ex, domain, config, hass).result() + + +@core.callback +def async_log_exception(ex, domain, config, hass): + """Generate log exception for config validation. + + Need to run in a async loop. + """ message = 'Invalid config for [{}]: '.format(domain) - if hass is not None: - _PERSISTENT_VALIDATION.add(domain) - message = ('The following platforms contain invalid configuration: ' + - ', '.join(list(_PERSISTENT_VALIDATION)) + - ' (please check your configuration)') - persistent_notification.create( - hass, message, 'Invalid config', 'invalid_config') + _PERSISTENT_VALIDATION.add(domain) + message = ('The following platforms contain invalid configuration: ' + + ', '.join(list(_PERSISTENT_VALIDATION)) + + ' (please check your configuration). ') + persistent_notification.async_create( + hass, message, 'Invalid config', 'invalid_config') if 'extra keys not allowed' in ex.error_message: message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ .format(ex.path[-1], domain, domain, - '->'.join('%s' % m for m in ex.path)) + '->'.join(str(m) for m in ex.path)) else: message += '{}.'.format(humanize_error(config, ex)) domain_config = config.get(domain, config) - message += " (See {}:{})".format( + message += " (See {}:{}). ".format( getattr(domain_config, '__config_file__', '?'), getattr(domain_config, '__line__', '?')) if domain != 'homeassistant': - message += (' Please check the docs at ' + message += ('Please check the docs at ' 'https://home-assistant.io/components/{}/'.format(domain)) _LOGGER.error(message) def mount_local_lib_path(config_dir: str) -> str: - """Add local library to Python Path.""" + """Add local library to Python Path. + + Async friendly. + """ deps_dir = os.path.join(config_dir, 'deps') if deps_dir not in sys.path: sys.path.insert(0, os.path.join(config_dir, 'deps')) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7d025bac765..81450c726f1 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -7,6 +7,7 @@ Component design guidelines: format ".". - Each component should publish services only under its own domain. """ +import asyncio import itertools as it import logging @@ -79,8 +80,10 @@ def reload_core_config(hass): hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup general services related to Home Assistant.""" + @asyncio.coroutine def handle_turn_service(service): """Method to handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -96,6 +99,8 @@ def setup(hass, config): by_domain = it.groupby(sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]) + tasks = [] + for domain, ent_ids in by_domain: # We want to block for all calls and only return when all calls # have been processed. If a service does not exist it causes a 10 @@ -111,27 +116,34 @@ def setup(hass, config): # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data, blocking) + tasks.append(hass.services.async_call( + domain, service.service, data, blocking)) - hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) - hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) - hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + yield from asyncio.gather(*tasks, loop=hass.loop) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + + @asyncio.coroutine def handle_reload_config(call): """Service handler for reloading core config.""" from homeassistant.exceptions import HomeAssistantError from homeassistant import config as conf_util try: - path = conf_util.find_config_file(hass.config.config_dir) - conf = conf_util.load_yaml_config_file(path) + conf = yield from conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error(err) return - conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + yield from conf_util.async_process_ha_core_config( + hass, conf.get(ha.DOMAIN) or {}) - hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, - handle_reload_config) + hass.services.async_register( + ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config) return True diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 244887ca10a..df0026a45ab 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,7 +11,6 @@ import os import voluptuous as vol -from homeassistant.core import callback from homeassistant.bootstrap import prepare_setup_platform from homeassistant import config as conf_util from homeassistant.const import ( @@ -25,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'automation' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -144,42 +142,50 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - success = run_coroutine_threadsafe( - _async_process_config(hass, config, component), hass.loop).result() + success = yield from _async_process_config(hass, config, component) if not success: return False - descriptions = conf_util.load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) + descriptions = yield from hass.loop.run_in_executor( + None, conf_util.load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml') + ) - @callback + @asyncio.coroutine def trigger_service_handler(service_call): """Handle automation triggers.""" + tasks = [] for entity in component.async_extract_from_service(service_call): - hass.loop.create_task(entity.async_trigger( + tasks.append(entity.async_trigger( service_call.data.get(ATTR_VARIABLES), True)) + yield from asyncio.gather(*tasks, loop=hass.loop) - @callback + @asyncio.coroutine def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" + tasks = [] method = 'async_{}'.format(service_call.service) for entity in component.async_extract_from_service(service_call): - hass.loop.create_task(getattr(entity, method)()) + tasks.append(getattr(entity, method)()) + yield from asyncio.gather(*tasks, loop=hass.loop) - @callback + @asyncio.coroutine def toggle_service_handler(service_call): """Handle automation toggle service calls.""" + tasks = [] for entity in component.async_extract_from_service(service_call): if entity.is_on: - hass.loop.create_task(entity.async_turn_off()) + tasks.append(entity.async_turn_off()) else: - hass.loop.create_task(entity.async_turn_on()) + tasks.append(entity.async_turn_on()) + yield from asyncio.gather(*tasks, loop=hass.loop) @asyncio.coroutine def reload_service_handler(service_call): @@ -187,24 +193,24 @@ def setup(hass, config): conf = yield from component.async_prepare_reload() if conf is None: return - hass.loop.create_task(_async_process_config(hass, conf, component)) + yield from _async_process_config(hass, conf, component) - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler, - descriptions.get(SERVICE_TRIGGER), - schema=TRIGGER_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TRIGGER, trigger_service_handler, + descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions.get(SERVICE_RELOAD), - schema=RELOAD_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, reload_service_handler, + descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler, - descriptions.get(SERVICE_TOGGLE), - schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, toggle_service_handler, + descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): - hass.services.register(DOMAIN, service, turn_onoff_service_handler, - descriptions.get(service), - schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, service, turn_onoff_service_handler, + descriptions.get(service), schema=SERVICE_SCHEMA) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 12735940916..aefc220c809 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -114,7 +114,7 @@ def setup(hass: HomeAssistantType, config: ConfigType): try: conf = config.get(DOMAIN, []) except vol.Invalid as ex: - log_exception(ex, DOMAIN, config) + log_exception(ex, DOMAIN, config, hass) return False else: conf = conf[0] if len(conf) > 0 else {} @@ -431,7 +431,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) except vol.Invalid as exp: - log_exception(exp, dev_id, devices) + log_exception(exp, dev_id, devices, hass) else: result.append(Device(hass, **device)) return result diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 915254bd618..59683247d5b 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -144,15 +144,17 @@ def get_entity_ids(hass, entity_id, domain_filter=None): if ent_id.startswith(domain_filter)] -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup all groups found definded in the configuration.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - run_coroutine_threadsafe( - _async_process_config(hass, config, component), hass.loop).result() + yield from _async_process_config(hass, config, component) - descriptions = conf_util.load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) + descriptions = yield from hass.loop.run_in_executor( + None, conf_util.load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml') + ) @asyncio.coroutine def reload_service_handler(service_call): @@ -162,9 +164,9 @@ def setup(hass, config): return hass.loop.create_task(_async_process_config(hass, conf, component)) - hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions[DOMAIN][SERVICE_RELOAD], - schema=RELOAD_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, reload_service_handler, + descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA) return True diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index d27389b51f9..5e91aef4d9f 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -10,12 +10,13 @@ import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify from homeassistant.config import load_yaml_config_file -from homeassistant.util.async import run_coroutine_threadsafe +from homeassistant.util.async import run_callback_threadsafe DOMAIN = 'persistent_notification' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -38,12 +39,12 @@ _LOGGER = logging.getLogger(__name__) def create(hass, message, title=None, notification_id=None): """Generate a notification.""" - run_coroutine_threadsafe( - async_create(hass, message, title, notification_id), hass.loop + run_callback_threadsafe( + hass.loop, async_create, hass, message, title, notification_id ).result() -@asyncio.coroutine +@callback def async_create(hass, message, title=None, notification_id=None): """Generate a notification.""" data = { @@ -54,11 +55,14 @@ def async_create(hass, message, title=None, notification_id=None): ] if value is not None } - yield from hass.services.async_call(DOMAIN, SERVICE_CREATE, data) + hass.loop.create_task( + hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the persistent notification component.""" + @callback def create_service(call): """Handle a create notification service call.""" title = call.data.get(ATTR_TITLE) @@ -68,13 +72,13 @@ def setup(hass, config): if notification_id is not None: entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) else: - entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, - hass=hass) + entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass) attr = {} if title is not None: try: title.hass = hass - title = title.render() + title = title.async_render() except TemplateError as ex: _LOGGER.error('Error rendering title %s: %s', title, ex) title = title.template @@ -83,17 +87,19 @@ def setup(hass, config): try: message.hass = hass - message = message.render() + message = message.async_render() except TemplateError as ex: _LOGGER.error('Error rendering message %s: %s', message, ex) message = message.template - hass.states.set(entity_id, message, attr) + hass.states.async_set(entity_id, message, attr) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_CREATE, create_service, - descriptions[DOMAIN][SERVICE_CREATE], - SCHEMA_SERVICE_CREATE) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml') + ) + hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service, + descriptions[DOMAIN][SERVICE_CREATE], + SCHEMA_SERVICE_CREATE) return True diff --git a/homeassistant/config.py b/homeassistant/config.py index 712740419d0..0e2192b3152 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,4 +1,5 @@ """Module to help with parsing and generating configuration files.""" +import asyncio import logging import os import shutil @@ -180,6 +181,24 @@ def create_default_config(config_dir, detect_location=True): return None +@asyncio.coroutine +def async_hass_config_yaml(hass): + """Load YAML from hass config File. + + This function allow component inside asyncio loop to reload his config by + self. + + This method is a coroutine. + """ + def _load_hass_yaml_config(): + path = find_config_file(hass.config.config_dir) + conf = load_yaml_config_file(path) + return conf + + conf = yield from hass.loop.run_in_executor(None, _load_hass_yaml_config) + return conf + + def find_config_file(config_dir): """Look in given directory for supported configuration files.""" config_path = os.path.join(config_dir, YAML_CONFIG_FILE) @@ -201,7 +220,11 @@ def load_yaml_config_file(config_path): def process_ha_config_upgrade(hass): - """Upgrade config if necessary.""" + """Upgrade config if necessary. + + Asyncio don't support file operation jet. + This method need to run in a executor. + """ version_path = hass.config.path(VERSION_FILE) try: @@ -225,8 +248,12 @@ def process_ha_config_upgrade(hass): outp.write(__version__) -def process_ha_core_config(hass, config): - """Process the [homeassistant] section from the config.""" +@asyncio.coroutine +def async_process_ha_core_config(hass, config): + """Process the [homeassistant] section from the config. + + This method is a coroutine. + """ # pylint: disable=too-many-branches config = CORE_CONFIG_SCHEMA(config) hac = hass.config @@ -282,7 +309,8 @@ def process_ha_core_config(hass, config): # If we miss some of the needed values, auto detect them if None in (hac.latitude, hac.longitude, hac.units, hac.time_zone): - info = loc_util.detect_location_info() + info = yield from hass.loop.run_in_executor( + None, loc_util.detect_location_info) if info is None: _LOGGER.error('Could not detect location information') @@ -307,7 +335,8 @@ def process_ha_core_config(hass, config): if hac.elevation is None and hac.latitude is not None and \ hac.longitude is not None: - elevation = loc_util.elevation(hac.latitude, hac.longitude) + elevation = yield from hass.loop.run_in_executor( + None, loc_util.elevation, hac.latitude, hac.longitude) hac.elevation = elevation discovered.append(('elevation', elevation)) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 08f93b3697b..27f180a72ca 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -56,7 +56,10 @@ def async_generate_entity_id(entity_id_format: str, name: Optional[str], def set_customize(customize: Dict[str, Any]) -> None: - """Overwrite all current customize settings.""" + """Overwrite all current customize settings. + + Async friendly. + """ global _OVERWRITE _OVERWRITE = {key.lower(): val for key, val in customize.items()} diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 2576970065f..ed450bb1a14 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -2,8 +2,8 @@ import asyncio from homeassistant import config as conf_util -from homeassistant.bootstrap import (prepare_setup_platform, - prepare_setup_component) +from homeassistant.bootstrap import ( + async_prepare_setup_platform, async_prepare_setup_component) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) @@ -118,10 +118,8 @@ class EntityComponent(object): This method must be run in the event loop. """ - platform = yield from self.hass.loop.run_in_executor( - None, prepare_setup_platform, self.hass, self.config, self.domain, - platform_type - ) + platform = yield from async_prepare_setup_platform( + self.hass, self.config, self.domain, platform_type) if platform is None: return @@ -238,20 +236,8 @@ class EntityComponent(object): def prepare_reload(self): """Prepare reloading this entity component.""" - try: - path = conf_util.find_config_file(self.hass.config.config_dir) - conf = conf_util.load_yaml_config_file(path) - except HomeAssistantError as err: - self.logger.error(err) - return None - - conf = prepare_setup_component(self.hass, conf, self.domain) - - if conf is None: - return None - - self.reset() - return conf + return run_coroutine_threadsafe( + self.async_prepare_reload(), loop=self.hass.loop).result() @asyncio.coroutine def async_prepare_reload(self): @@ -259,9 +245,20 @@ class EntityComponent(object): This method must be run in the event loop. """ - conf = yield from self.hass.loop.run_in_executor( - None, self.prepare_reload - ) + try: + conf = yield from \ + conf_util.async_hass_config_yaml(self.hass) + except HomeAssistantError as err: + self.logger.error(err) + return None + + conf = yield from async_prepare_setup_component( + self.hass, conf, self.domain) + + if conf is None: + return None + + yield from self.async_reset() return conf diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 591e7d229a3..a19d835543d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -40,7 +40,11 @@ _LOGGER = logging.getLogger(__name__) def prepare(hass: 'HomeAssistant'): - """Prepare the loading of components.""" + """Prepare the loading of components. + + Asyncio don't support file operation jet. + This method need to run in a executor. + """ global PREPARED # pylint: disable=global-statement # Load the built-in components @@ -81,14 +85,20 @@ def prepare(hass: 'HomeAssistant'): def set_component(comp_name: str, component: ModuleType) -> None: - """Set a component in the cache.""" + """Set a component in the cache. + + Async friendly. + """ _check_prepared() _COMPONENT_CACHE[comp_name] = component def get_platform(domain: str, platform: str) -> Optional[ModuleType]: - """Try to load specified platform.""" + """Try to load specified platform. + + Async friendly. + """ return get_component(PLATFORM_FORMAT.format(domain, platform)) @@ -97,6 +107,8 @@ def get_component(comp_name) -> Optional[ModuleType]: Looks in config dir first, then built-in components. Only returns it if also found to be valid. + + Async friendly. """ if comp_name in _COMPONENT_CACHE: return _COMPONENT_CACHE[comp_name] @@ -166,6 +178,8 @@ def load_order_components(components: Sequence[str]) -> OrderedSet: - Will ensure that all components that do not directly depend on the group component will be loaded before the group component. - returns an OrderedSet load order. + + Async friendly. """ _check_prepared() @@ -192,13 +206,18 @@ def load_order_component(comp_name: str) -> OrderedSet: Raises HomeAssistantError if a circular dependency is detected. Returns an empty list if component could not be loaded. + + Async friendly. """ return _load_order_component(comp_name, OrderedSet(), set()) def _load_order_component(comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: - """Recursive function to get load order of components.""" + """Recursive function to get load order of components. + + Async friendly. + """ component = get_component(comp_name) # If None it does not exist, error already thrown by get_component. @@ -235,7 +254,10 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, def _check_prepared() -> None: - """Issue a warning if loader.prepare() has never been called.""" + """Issue a warning if loader.prepare() has never been called. + + Async friendly. + """ if not PREPARED: _LOGGER.warning(( "You did not call loader.prepare() yet. " diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index f8b6fc6e69b..736498cf935 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -26,8 +26,8 @@ MOCKS = { 'load*': ("homeassistant.config.load_yaml", yaml.load_yaml), 'get': ("homeassistant.loader.get_component", loader.get_component), 'secrets': ("homeassistant.util.yaml._secret_yaml", yaml._secret_yaml), - 'except': ("homeassistant.bootstrap.log_exception", - bootstrap.log_exception) + 'except': ("homeassistant.bootstrap.async_log_exception", + bootstrap.async_log_exception) } SILENCE = ( 'homeassistant.bootstrap.clear_secret_cache', @@ -185,9 +185,15 @@ def check(config_path): # Test if platform/component and overwrite setup if '.' in comp_name: module.setup_platform = mock_setup + + if hasattr(module, 'async_setup_platform'): + del module.async_setup_platform else: module.setup = mock_setup + if hasattr(module, 'async_setup'): + del module.async_setup + return module def mock_secrets(ldr, node): # pylint: disable=unused-variable diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 282ddf9bb8c..828281aa897 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -23,7 +23,10 @@ DATETIME_RE = re.compile( def set_default_time_zone(time_zone: dt.tzinfo) -> None: - """Set a default time zone to be used when none is specified.""" + """Set a default time zone to be used when none is specified. + + Async friendly. + """ global DEFAULT_TIME_ZONE # pylint: disable=global-statement # NOTE: Remove in the future in favour of typing @@ -33,7 +36,10 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None: def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]: - """Get time zone from string. Return None if unable to determine.""" + """Get time zone from string. Return None if unable to determine. + + Async friendly. + """ try: return pytz.timezone(time_zone_str) except pytz.exceptions.UnknownTimeZoneError: diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 6b1bc2227c8..4a73ac7c8dd 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -49,7 +49,10 @@ def load_yaml(fname: str) -> Union[List, Dict]: def clear_secret_cache() -> None: - """Clear the secret cache.""" + """Clear the secret cache. + + Async friendly. + """ __SECRET_CACHE.clear() diff --git a/tests/common.py b/tests/common.py index b73af5fc4c5..8896a97881b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,8 @@ import threading from contextlib import contextmanager from homeassistant import core as ha, loader -from homeassistant.bootstrap import setup_component, prepare_setup_component +from homeassistant.bootstrap import ( + setup_component, async_prepare_setup_component) from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util @@ -234,6 +235,7 @@ class MockModule(object): self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] self.REQUIREMENTS = requirements or [] + self._setup = setup if config_schema is not None: self.CONFIG_SCHEMA = config_schema @@ -241,11 +243,11 @@ class MockModule(object): if platform_schema is not None: self.PLATFORM_SCHEMA = platform_schema - # Setup a mock setup if none given. - if setup is None: - self.setup = lambda hass, config: True - else: - self.setup = setup + def setup(self, hass, config): + """Setup the component.""" + if self._setup is not None: + return self._setup(hass, config) + return True class MockPlatform(object): @@ -366,16 +368,19 @@ def assert_setup_component(count, domain=None): """ config = {} + @asyncio.coroutine def mock_psc(hass, config_input, domain): """Mock the prepare_setup_component to capture config.""" - res = prepare_setup_component(hass, config_input, domain) + res = yield from async_prepare_setup_component( + hass, config_input, domain) config[domain] = None if res is None else res.get(domain) _LOGGER.debug('Configuration for %s, Validated: %s, Original %s', domain, config[domain], config_input.get(domain)) return res assert isinstance(config, dict) - with patch('homeassistant.bootstrap.prepare_setup_component', mock_psc): + with patch('homeassistant.bootstrap.async_prepare_setup_component', + mock_psc): yield config if domain is None: diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 33fc5bf117a..2ab62833eda 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,7 +1,7 @@ """The tests for the Event automation.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation from tests.common import get_test_home_assistant @@ -28,7 +28,7 @@ class TestAutomationEvent(unittest.TestCase): def test_if_fires_on_event(self): """Test the firing of events.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -53,7 +53,7 @@ class TestAutomationEvent(unittest.TestCase): def test_if_fires_on_event_with_data(self): """Test the firing of events with data.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -73,7 +73,7 @@ class TestAutomationEvent(unittest.TestCase): def test_if_not_fires_if_event_data_not_matches(self): """Test firing of event if no match.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index ec128f77756..2956be98b00 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -379,21 +379,22 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) - @patch('homeassistant.config.load_yaml_config_file', return_value={ - automation.DOMAIN: { - 'alias': 'bye', - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event2', - }, - 'action': { - 'service': 'test.automation', - 'data_template': { - 'event': '{{ trigger.event.event_type }}' + @patch('homeassistant.config.load_yaml_config_file', autospec=True, + return_value={ + automation.DOMAIN: { + 'alias': 'bye', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event2', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } } - } - } - }) + }) def test_reload_config_service(self, mock_load_yaml): """Test the reload config service.""" assert setup_component(self.hass, automation.DOMAIN, { @@ -443,9 +444,8 @@ class TestAutomation(unittest.TestCase): assert len(self.calls) == 2 assert self.calls[1].data.get('event') == 'test_event2' - @patch('homeassistant.config.load_yaml_config_file', return_value={ - automation.DOMAIN: 'not valid', - }) + @patch('homeassistant.config.load_yaml_config_file', autospec=True, + return_value={automation.DOMAIN: 'not valid'}) def test_reload_config_when_invalid_config(self, mock_load_yaml): """Test the reload config service handling invalid config.""" with assert_setup_component(1): diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index aade8b7dad8..b7da76fda20 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT automation.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) @@ -28,7 +28,7 @@ class TestAutomationMQTT(unittest.TestCase): def test_if_fires_on_topic_match(self): """Test if message is fired on topic match.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'mqtt', @@ -58,7 +58,7 @@ class TestAutomationMQTT(unittest.TestCase): def test_if_fires_on_topic_and_payload_match(self): """Test if message is fired on topic and payload match.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'mqtt', @@ -77,7 +77,7 @@ class TestAutomationMQTT(unittest.TestCase): def test_if_not_fires_on_topic_but_no_payload_match(self): """Test if message is not fired on topic but no payload.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'mqtt', diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index fecd8474763..fa2d237ee00 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,7 +1,7 @@ """The tests for numeric state automation.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation from tests.common import get_test_home_assistant @@ -28,7 +28,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below(self): """"Test the firing with changed entity.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -58,7 +58,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 11) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -81,7 +81,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 9) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -101,7 +101,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_above(self): """"Test the firing with changed entity.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -124,7 +124,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 9) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -148,7 +148,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 11) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -168,7 +168,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below_range(self): """"Test the firing with changed entity.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -188,7 +188,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below_above_range(self): """"Test the firing with changed entity.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -211,7 +211,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 11) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -235,7 +235,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity', 11) self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -256,7 +256,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_if_entity_not_match(self): """"Test if not fired with non matching entity.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -275,7 +275,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_entity_change_below_with_attribute(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -294,7 +294,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_on_entity_change_not_below_with_attribute(self): """"Test attributes.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -313,7 +313,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_fires_on_attribute_change_with_attribute_below(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -353,7 +353,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_on_entity_change_with_attribute_below(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -373,7 +373,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_if_not_fires_on_entity_change_with_not_attribute_below(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -393,7 +393,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(self): """"Test attributes change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -414,7 +414,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_template_list(self): """"Test template list.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -436,7 +436,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_template_string(self): """"Test template string.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -469,7 +469,7 @@ class TestAutomationNumericState(unittest.TestCase): def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): """"Test if not fired changed attributes.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'numeric_state', @@ -492,7 +492,7 @@ class TestAutomationNumericState(unittest.TestCase): """"Test if action.""" entity_id = 'domain.test_entity' test_state = 10 - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index d6fe56453ee..06c127ca6b7 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -3,11 +3,12 @@ import unittest from datetime import timedelta from unittest.mock import patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation -from tests.common import fire_time_changed, get_test_home_assistant +from tests.common import ( + fire_time_changed, get_test_home_assistant, assert_setup_component) class TestAutomationState(unittest.TestCase): @@ -34,7 +35,7 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity', 'hello') self.hass.block_till_done() - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -67,7 +68,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_from_filter(self): """Test for firing on entity change with filter.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -86,7 +87,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_to_filter(self): """Test for firing on entity change with no filter.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -105,7 +106,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_state_filter(self): """Test for firing on entity change with state filter.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -124,7 +125,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_both_filters(self): """Test for firing if both filters are a non match.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -144,7 +145,7 @@ class TestAutomationState(unittest.TestCase): def test_if_not_fires_if_to_filter_not_match(self): """Test for not firing if to filter is not a match.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -166,7 +167,7 @@ class TestAutomationState(unittest.TestCase): """Test for not firing if from filter is not a match.""" self.hass.states.set('test.entity', 'bye') - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -186,7 +187,7 @@ class TestAutomationState(unittest.TestCase): def test_if_not_fires_if_entity_not_match(self): """Test for not firing if entity is not matching.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -206,7 +207,7 @@ class TestAutomationState(unittest.TestCase): """Test for to action.""" entity_id = 'domain.test_entity' test_state = 'new_state' - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -237,68 +238,72 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_if_to_boolean_value(self): """Test for setup failure for boolean to.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'state', - 'entity_id': 'test.entity', - 'to': True, - }, - 'action': { - 'service': 'homeassistant.turn_on', - } - }}) + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'to': True, + }, + 'action': { + 'service': 'homeassistant.turn_on', + } + }}) def test_if_fails_setup_if_from_boolean_value(self): """Test for setup failure for boolean from.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'state', - 'entity_id': 'test.entity', - 'from': True, - }, - 'action': { - 'service': 'homeassistant.turn_on', - } - }}) + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'from': True, + }, + 'action': { + 'service': 'homeassistant.turn_on', + } + }}) def test_if_fails_setup_bad_for(self): """Test for setup failure for bad for.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'state', - 'entity_id': 'test.entity', - 'to': 'world', - 'for': { - 'invalid': 5 + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'to': 'world', + 'for': { + 'invalid': 5 + }, }, - }, - 'action': { - 'service': 'homeassistant.turn_on', - } - }}) + 'action': { + 'service': 'homeassistant.turn_on', + } + }}) def test_if_fails_setup_for_without_to(self): """Test for setup failures for missing to.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'state', - 'entity_id': 'test.entity', - 'for': { - 'seconds': 5 + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'for': { + 'seconds': 5 + }, }, - }, - 'action': { - 'service': 'homeassistant.turn_on', - } - }}) + 'action': { + 'service': 'homeassistant.turn_on', + } + }}) def test_if_not_fires_on_entity_change_with_for(self): """Test for not firing on entity change with for.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -324,7 +329,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change_with_for(self): """Test for firing on entity change with for.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -353,7 +358,7 @@ class TestAutomationState(unittest.TestCase): with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: mock_utcnow.return_value = point1 self.hass.states.set('test.entity', 'on') - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -384,32 +389,34 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_for_without_time(self): """Test for setup failure if no time is provided.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'bla' - }, - 'condition': { - 'platform': 'state', - 'entity_id': 'test.entity', - 'state': 'on', - 'for': {}, - }, - 'action': {'service': 'test.automation'}, - }}) + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'bla' + }, + 'condition': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'state': 'on', + 'for': {}, + }, + 'action': {'service': 'test.automation'}, + }}) def test_if_fails_setup_for_without_entity(self): """Test for setup failure if no entity is provided.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': {'event_type': 'bla'}, - 'condition': { - 'platform': 'state', - 'state': 'on', - 'for': { - 'seconds': 5 + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': {'event_type': 'bla'}, + 'condition': { + 'platform': 'state', + 'state': 'on', + 'for': { + 'seconds': 5 + }, }, - }, - 'action': {'service': 'test.automation'}, - }}) + 'action': {'service': 'test.automation'}, + }}) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 815c540eb3e..ca3d1618013 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -3,7 +3,7 @@ from datetime import datetime import unittest from unittest.mock import patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import sun import homeassistant.components.automation as automation import homeassistant.util.dt as dt_util @@ -42,7 +42,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'sun', @@ -81,7 +81,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'sun', @@ -108,7 +108,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'sun', @@ -142,7 +142,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'sun', @@ -165,7 +165,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -201,7 +201,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -237,7 +237,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -274,7 +274,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -312,7 +312,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T15:00:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -358,7 +358,7 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T17:30:00Z', }) - _setup_component(self.hass, automation.DOMAIN, { + setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 1334608ecdb..fcd1a48983a 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -1,10 +1,10 @@ """The tests for the Template automation.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.automation as automation -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, assert_setup_component class TestAutomationTemplate(unittest.TestCase): @@ -29,7 +29,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_bool(self): """Test for firing on boolean change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -54,7 +54,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_str(self): """Test for firing on change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -72,7 +72,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_str_crazy(self): """Test for firing on change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -90,7 +90,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_not_fires_on_change_bool(self): """Test for not firing on boolean change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -108,7 +108,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_not_fires_on_change_str(self): """Test for not firing on string change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -126,7 +126,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_not_fires_on_change_str_crazy(self): """Test for not firing on string change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -144,7 +144,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_no_change(self): """Test for firing on no change.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -165,7 +165,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_two_change(self): """Test for firing on two changes.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -189,7 +189,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_with_template(self): """Test for firing on change with template.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -207,7 +207,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_not_fires_on_change_with_template(self): """Test for not firing on change with template.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -228,7 +228,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_with_template_advanced(self): """Test for firing on change with template advanced.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -262,7 +262,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_no_change_with_template_advanced(self): """Test for firing on no change with template advanced.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -290,7 +290,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_with_template_2(self): """Test for firing on change with template.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', @@ -332,7 +332,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_action(self): """Test for firing if action.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -365,21 +365,22 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_with_bad_template(self): """Test for firing on change with bad template.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'template', - 'value_template': '{{ ', - }, - 'action': { - 'service': 'test.automation' + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'template', + 'value_template': '{{ ', + }, + 'action': { + 'service': 'test.automation' + } } - } - }) + }) def test_if_fires_on_change_with_bad_template_2(self): """Test for firing on change with bad template.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index afdab681460..dba100aa345 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -3,11 +3,12 @@ from datetime import timedelta import unittest from unittest.mock import patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation -from tests.common import fire_time_changed, get_test_home_assistant +from tests.common import ( + fire_time_changed, get_test_home_assistant, assert_setup_component) class TestAutomationTime(unittest.TestCase): @@ -30,7 +31,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_when_hour_matches(self): """Test for firing if hour is matching.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -55,7 +56,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_when_minute_matches(self): """Test for firing if minutes are matching.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -74,7 +75,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_when_second_matches(self): """Test for firing if seconds are matching.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -93,7 +94,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_when_all_matches(self): """Test for firing if everything matches.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -115,7 +116,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_periodic_seconds(self): """Test for firing periodically every second.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -135,7 +136,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_periodic_minutes(self): """Test for firing periodically every minute.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -155,7 +156,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_periodic_hours(self): """Test for firing periodically every hour.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -175,7 +176,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_fires_using_after(self): """Test for firing after.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -200,16 +201,17 @@ class TestAutomationTime(unittest.TestCase): def test_if_not_working_if_no_values_in_conf_provided(self): """Test for failure if no configuration.""" - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - }, - 'action': { - 'service': 'test.automation' + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + }, + 'action': { + 'service': 'test.automation' + } } - } - }) + }) fire_time_changed(self.hass, dt_util.utcnow().replace( hour=5, minute=0, second=0)) @@ -222,18 +224,19 @@ class TestAutomationTime(unittest.TestCase): This should break the before rule. """ - assert not _setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'time', - 'after': 3605, - # Total seconds. Hour = 3600 second - }, - 'action': { - 'service': 'test.automation' + with assert_setup_component(0): + assert not setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'after': 3605, + # Total seconds. Hour = 3600 second + }, + 'action': { + 'service': 'test.automation' + } } - } - }) + }) fire_time_changed(self.hass, dt_util.utcnow().replace( hour=1, minute=0, second=5)) @@ -243,7 +246,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_action_before(self): """Test for if action before.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -278,7 +281,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_action_after(self): """Test for if action after.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -313,7 +316,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_action_one_weekday(self): """Test for if action with one weekday.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -349,7 +352,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_action_list_weekday(self): """Test for action with a list of weekdays.""" - assert _setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index ada4a9b4224..3bff4420a66 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT binary sensor platform.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.binary_sensor as binary_sensor from tests.common import mock_mqtt_component, fire_mqtt_message from homeassistant.const import (STATE_OFF, STATE_ON) @@ -24,7 +24,7 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_message(self): """Test the setting of the value via MQTT.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, binary_sensor.DOMAIN, { + assert setup_component(self.hass, binary_sensor.DOMAIN, { binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -50,7 +50,7 @@ class TestSensorMQTT(unittest.TestCase): def test_valid_sensor_class(self): """Test the setting of a valid sensor class.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, binary_sensor.DOMAIN, { + assert setup_component(self.hass, binary_sensor.DOMAIN, { binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -65,7 +65,7 @@ class TestSensorMQTT(unittest.TestCase): def test_invalid_sensor_class(self): """Test the setting of an invalid sensor class.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, binary_sensor.DOMAIN, { + assert setup_component(self.hass, binary_sensor.DOMAIN, { binary_sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index 71efd1ff1b2..49147279711 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -8,6 +8,8 @@ from nx584 import client as nx584_client from homeassistant.components.binary_sensor import nx584 from homeassistant.bootstrap import setup_component +from tests.common import get_test_home_assistant + class StopMe(Exception): """Stop helper.""" @@ -20,6 +22,7 @@ class TestNX584SensorSetup(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() self._mock_client = mock.patch.object(nx584_client, 'Client') self._mock_client.start() @@ -35,6 +38,7 @@ class TestNX584SensorSetup(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" + self.hass.stop() self._mock_client.stop() @mock.patch('homeassistant.components.binary_sensor.nx584.NX584Watcher') @@ -42,14 +46,13 @@ class TestNX584SensorSetup(unittest.TestCase): def test_setup_defaults(self, mock_nx, mock_watcher): """Test the setup with no configuration.""" add_devices = mock.MagicMock() - hass = mock.MagicMock() config = { 'host': nx584.DEFAULT_HOST, 'port': nx584.DEFAULT_PORT, 'exclude_zones': [], 'zone_types': {}, } - self.assertTrue(nx584.setup_platform(hass, config, add_devices)) + self.assertTrue(nx584.setup_platform(self.hass, config, add_devices)) mock_nx.assert_has_calls( [mock.call(zone, 'opening') for zone in self.fake_zones]) self.assertTrue(add_devices.called) @@ -69,8 +72,7 @@ class TestNX584SensorSetup(unittest.TestCase): 'zone_types': {3: 'motion'}, } add_devices = mock.MagicMock() - hass = mock.MagicMock() - self.assertTrue(nx584.setup_platform(hass, config, add_devices)) + self.assertTrue(nx584.setup_platform(self.hass, config, add_devices)) mock_nx.assert_has_calls([ mock.call(self.fake_zones[0], 'opening'), mock.call(self.fake_zones[2], 'motion'), @@ -84,9 +86,8 @@ class TestNX584SensorSetup(unittest.TestCase): def _test_assert_graceful_fail(self, config): """Test the failing.""" - hass = add_devices = mock.MagicMock() - self.assertFalse(setup_component(hass, 'binary_sensor.nx584', config)) - self.assertFalse(add_devices.called) + self.assertFalse(setup_component( + self.hass, 'binary_sensor.nx584', config)) def test_setup_bad_config(self): """Test the setup with bad configuration.""" @@ -113,8 +114,8 @@ class TestNX584SensorSetup(unittest.TestCase): def test_setup_no_zones(self): """Test the setup with no zones.""" nx584_client.Client.return_value.list_zones.return_value = [] - hass = add_devices = mock.MagicMock() - self.assertTrue(nx584.setup_platform(hass, {}, add_devices)) + add_devices = mock.MagicMock() + self.assertTrue(nx584.setup_platform(self.hass, {}, add_devices)) self.assertFalse(add_devices.called) diff --git a/tests/components/cover/test_rfxtrx.py b/tests/components/cover/test_rfxtrx.py index 80d49dface4..85ff26145ed 100644 --- a/tests/components/cover/test_rfxtrx.py +++ b/tests/components/cover/test_rfxtrx.py @@ -3,7 +3,7 @@ import unittest import pytest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from tests.common import get_test_home_assistant @@ -28,7 +28,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_valid_config(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -39,7 +39,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_invalid_config_capital_letters(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'cover', { + self.assertFalse(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -51,7 +51,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_invalid_config_extra_key(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'cover', { + self.assertFalse(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'invalid_key': 'afda', @@ -64,7 +64,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_invalid_config_capital_packetid(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'cover', { + self.assertFalse(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -76,7 +76,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_invalid_config_missing_packetid(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'cover', { + self.assertFalse(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -87,14 +87,14 @@ class TestCoverRfxtrx(unittest.TestCase): def test_default_config(self): """Test with 0 cover.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'devices': {}}})) self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) def test_one_cover(self): """Test with 1 cover.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'devices': {'0b1400cd0213c7f210010f51': { @@ -117,7 +117,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_several_covers(self): """Test with 3 covers.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'signal_repetitions': 3, 'devices': @@ -145,7 +145,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_discover_covers(self): """Test with discovery of covers.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': {}}})) @@ -183,7 +183,7 @@ class TestCoverRfxtrx(unittest.TestCase): def test_discover_cover_noautoadd(self): """Test with discovery of cover when auto add is False.""" - self.assertTrue(_setup_component(self.hass, 'cover', { + self.assertTrue(setup_component(self.hass, 'cover', { 'cover': {'platform': 'rfxtrx', 'automatic_add': False, 'devices': {}}})) diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py index e2af75ed76b..4eebf46e632 100644 --- a/tests/components/device_tracker/test_mqtt.py +++ b/tests/components/device_tracker/test_mqtt.py @@ -4,7 +4,7 @@ from unittest.mock import patch import logging import os -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import device_tracker from homeassistant.const import CONF_PLATFORM @@ -42,7 +42,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): dev_id = 'paulus' topic = '/location/paulus' self.hass.config.components = ['mqtt', 'zone'] - assert _setup_component(self.hass, device_tracker.DOMAIN, { + assert setup_component(self.hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { CONF_PLATFORM: 'mqtt', 'devices': {dev_id: topic} @@ -58,7 +58,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase): location = 'work' self.hass.config.components = ['mqtt', 'zone'] - assert _setup_component(self.hass, device_tracker.DOMAIN, { + assert setup_component(self.hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { CONF_PLATFORM: 'mqtt', 'devices': {dev_id: topic} diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 6fc4a00097d..fc9ade7d6ac 100755 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -30,7 +30,7 @@ light: import json import unittest -from homeassistant.bootstrap import _setup_component, setup_component +from homeassistant.bootstrap import setup_component from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE import homeassistant.components.light as light from tests.common import ( @@ -67,7 +67,7 @@ class TestLightMQTTJSON(unittest.TestCase): # pylint: disable=invalid-name """Test if there is no color and brightness if they aren't defined.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', @@ -93,7 +93,7 @@ class TestLightMQTTJSON(unittest.TestCase): # pylint: disable=invalid-name """Test the controlling of the state via topic.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', @@ -153,7 +153,7 @@ class TestLightMQTTJSON(unittest.TestCase): # pylint: disable=invalid-name """Test the sending of command in optimistic mode.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', @@ -209,7 +209,7 @@ class TestLightMQTTJSON(unittest.TestCase): # pylint: disable=invalid-name """Test for flash length being sent when included.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', @@ -251,7 +251,7 @@ class TestLightMQTTJSON(unittest.TestCase): def test_transition(self): """Test for transition time being sent when included.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', @@ -293,7 +293,7 @@ class TestLightMQTTJSON(unittest.TestCase): # pylint: disable=invalid-name """Test that invalid color/brightness values are ignored.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, light.DOMAIN, { + assert setup_component(self.hass, light.DOMAIN, { light.DOMAIN: { 'platform': 'mqtt_json', 'name': 'test', diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index a9c2b2d8bcb..c87e562c4ff 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -3,7 +3,7 @@ import unittest import pytest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from tests.common import get_test_home_assistant @@ -28,7 +28,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_valid_config(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -36,7 +36,7 @@ class TestLightRfxtrx(unittest.TestCase): 'name': 'Test', rfxtrx_core.ATTR_FIREEVENT: True}}}})) - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -47,7 +47,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_invalid_config(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'light', { + self.assertFalse(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'automatic_add': True, 'invalid_key': 'afda', @@ -59,14 +59,14 @@ class TestLightRfxtrx(unittest.TestCase): def test_default_config(self): """Test with 0 switches.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'devices': {}}})) self.assertEqual(0, len(rfxtrx_core.RFX_DEVICES)) def test_old_config(self): """Test with 1 light.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'devices': {'123efab1': { @@ -110,7 +110,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_one_light(self): """Test with 1 light.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'devices': {'0b1100cd0213c7f210010f51': { @@ -179,7 +179,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_several_lights(self): """Test with 3 lights.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'signal_repetitions': 3, 'devices': @@ -212,7 +212,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_discover_light(self): """Test with discovery of lights.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': {}}})) @@ -265,7 +265,7 @@ class TestLightRfxtrx(unittest.TestCase): def test_discover_light_noautoadd(self): """Test with discover of light when auto add is False.""" - self.assertTrue(_setup_component(self.hass, 'light', { + self.assertTrue(setup_component(self.hass, 'light', { 'light': {'platform': 'rfxtrx', 'automatic_add': False, 'devices': {}}})) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index c51d2736b73..0c85360fc00 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT lock platform.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock @@ -24,7 +24,7 @@ class TestLockMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling state via topic.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, lock.DOMAIN, { + assert setup_component(self.hass, lock.DOMAIN, { lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -54,7 +54,7 @@ class TestLockMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, lock.DOMAIN, { + assert setup_component(self.hass, lock.DOMAIN, { lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -88,7 +88,7 @@ class TestLockMQTT(unittest.TestCase): def test_controlling_state_via_topic_and_json_message(self): """Test the controlling state via topic and JSON message.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, lock.DOMAIN, { + assert setup_component(self.hass, lock.DOMAIN, { lock.DOMAIN: { 'platform': 'mqtt', 'name': 'test', diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cfa0766c8ed..5b65df9e1da 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -6,7 +6,7 @@ import socket import voluptuous as vol -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.mqtt as mqtt from homeassistant.const import ( EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, @@ -52,7 +52,7 @@ class TestMQTT(unittest.TestCase): with mock.patch('homeassistant.components.mqtt.MQTT', side_effect=socket.error()): self.hass.config.components = [] - assert not _setup_component(self.hass, mqtt.DOMAIN, { + assert not setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'test-broker', } @@ -62,7 +62,7 @@ class TestMQTT(unittest.TestCase): """Test for setup failure if connection to broker is missing.""" with mock.patch('paho.mqtt.client.Client'): self.hass.config.components = [] - assert _setup_component(self.hass, mqtt.DOMAIN, { + assert setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'test-broker', mqtt.CONF_PROTOCOL: 3.1, @@ -222,7 +222,7 @@ class TestMQTTCallbacks(unittest.TestCase): with mock.patch('paho.mqtt.client.Client'): self.hass.config.components = [] - assert _setup_component(self.hass, mqtt.DOMAIN, { + assert setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: { mqtt.CONF_BROKER: 'mock-broker', } diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index eb7dabb28b3..7b0963da23c 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,7 +1,7 @@ """The tests for the MQTT component embedded server.""" from unittest.mock import Mock, MagicMock, patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.mqtt as mqtt from tests.common import get_test_home_assistant @@ -29,7 +29,7 @@ class TestMQTT: password = 'super_secret' self.hass.config.api = MagicMock(api_password=password) - assert _setup_component(self.hass, mqtt.DOMAIN, {}) + assert setup_component(self.hass, mqtt.DOMAIN, {}) assert mock_mqtt.called assert mock_mqtt.mock_calls[0][1][5] == 'homeassistant' assert mock_mqtt.mock_calls[0][1][6] == password @@ -38,7 +38,7 @@ class TestMQTT: self.hass.config.components = ['http'] self.hass.config.api = MagicMock(api_password=None) - assert _setup_component(self.hass, mqtt.DOMAIN, {}) + assert setup_component(self.hass, mqtt.DOMAIN, {}) assert mock_mqtt.called assert mock_mqtt.mock_calls[0][1][5] is None assert mock_mqtt.mock_calls[0][1][6] is None @@ -54,6 +54,7 @@ class TestMQTT: mock_gather.side_effect = BrokerException self.hass.config.api = MagicMock(api_password=None) - assert not _setup_component(self.hass, mqtt.DOMAIN, { + + assert not setup_component(self.hass, mqtt.DOMAIN, { mqtt.DOMAIN: {mqtt.CONF_EMBEDDED: {}} }) diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index f63d16a5711..08407b20a58 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -8,9 +8,8 @@ import homeassistant.components.notify as notify from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT) import homeassistant.util.dt as dt_util -from homeassistant.bootstrap import _setup_component -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, assert_setup_component class TestNotifyFile(unittest.TestCase): @@ -26,12 +25,13 @@ class TestNotifyFile(unittest.TestCase): def test_bad_config(self): """Test set up the platform with bad/missing config.""" - self.assertFalse(_setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - }, - })) + with assert_setup_component(0): + assert not setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + }, + }) @patch('homeassistant.components.notify.file.os.stat') @patch('homeassistant.util.dt.utcnow') diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index c261e5eedbd..2df88b7a6e4 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -6,7 +6,7 @@ import unittest from homeassistant.const import MATCH_ALL from homeassistant.components import recorder -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from tests.common import get_test_home_assistant @@ -17,7 +17,7 @@ class TestRecorder(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() db_uri = 'sqlite://' # In memory DB - _setup_component(self.hass, recorder.DOMAIN, { + setup_component(self.hass, recorder.DOMAIN, { recorder.DOMAIN: {recorder.CONF_DB_URL: db_uri}}) self.hass.start() recorder._verify_instance() diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 2ef341d9e21..cac02d6bcd2 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT sensor platform.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.components.sensor as sensor from tests.common import mock_mqtt_component, fire_mqtt_message @@ -23,7 +23,7 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_message(self): """Test the setting of the value via MQTT.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, sensor.DOMAIN, { + assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -43,7 +43,7 @@ class TestSensorMQTT(unittest.TestCase): def test_setting_sensor_value_via_mqtt_json_message(self): """Test the setting of the value via MQTT with JSON playload.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, sensor.DOMAIN, { + assert setup_component(self.hass, sensor.DOMAIN, { sensor.DOMAIN: { 'platform': 'mqtt', 'name': 'test', diff --git a/tests/components/sensor/test_rfxtrx.py b/tests/components/sensor/test_rfxtrx.py index cfe5a95605d..1de6cf19419 100644 --- a/tests/components/sensor/test_rfxtrx.py +++ b/tests/components/sensor/test_rfxtrx.py @@ -3,7 +3,7 @@ import unittest import pytest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.const import TEMP_CELSIUS @@ -29,7 +29,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_default_config(self): """Test with 0 sensor.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {}}})) @@ -37,7 +37,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_old_config_sensor(self): """Test with 1 sensor.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {'sensor_0502': { @@ -53,7 +53,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_one_sensor(self): """Test with 1 sensor.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {'0a52080705020095220269': { @@ -68,7 +68,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_one_sensor_no_datatype(self): """Test with 1 sensor.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {'0a52080705020095220269': { @@ -88,7 +88,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_several_sensors(self): """Test with 3 sensors.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {'0a52080705020095220269': { @@ -124,7 +124,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_discover_sensor(self): """Test with discovery of sensor.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': {}}})) @@ -182,7 +182,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_discover_sensor_noautoadd(self): """Test with discover of sensor when auto add is False.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'automatic_add': False, 'devices': {}}})) @@ -209,7 +209,7 @@ class TestSensorRfxtrx(unittest.TestCase): def test_update_of_sensors(self): """Test with 3 sensors.""" - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'devices': {'0a52080705020095220269': { diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 0f7162c079e..7df47a99688 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -2,7 +2,7 @@ from datetime import datetime from unittest.mock import patch -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant, load_fixture @@ -28,7 +28,7 @@ class TestSensorYr: with patch('homeassistant.components.sensor.yr.dt_util.utcnow', return_value=now): - assert _setup_component(self.hass, 'sensor', { + assert setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'yr', 'elevation': 0}}) @@ -46,7 +46,7 @@ class TestSensorYr: with patch('homeassistant.components.sensor.yr.dt_util.utcnow', return_value=now): - assert _setup_component(self.hass, 'sensor', { + assert setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'yr', 'elevation': 0, 'monitored_conditions': [ diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 6d7314a0895..f39f4d11ec5 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,7 +1,7 @@ """The tests for the MQTT switch platform.""" import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ASSUMED_STATE import homeassistant.components.switch as switch from tests.common import ( @@ -23,7 +23,7 @@ class TestSensorMQTT(unittest.TestCase): def test_controlling_state_via_topic(self): """Test the controlling state via topic.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, switch.DOMAIN, { + assert setup_component(self.hass, switch.DOMAIN, { switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -53,7 +53,7 @@ class TestSensorMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, switch.DOMAIN, { + assert setup_component(self.hass, switch.DOMAIN, { switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', @@ -87,7 +87,7 @@ class TestSensorMQTT(unittest.TestCase): def test_controlling_state_via_topic_and_json_message(self): """Test the controlling state via topic and JSON message.""" self.hass.config.components = ['mqtt'] - assert _setup_component(self.hass, switch.DOMAIN, { + assert setup_component(self.hass, switch.DOMAIN, { switch.DOMAIN: { 'platform': 'mqtt', 'name': 'test', diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/switch/test_rfxtrx.py index 4caf7b3405d..b45342336e3 100644 --- a/tests/components/switch/test_rfxtrx.py +++ b/tests/components/switch/test_rfxtrx.py @@ -3,7 +3,7 @@ import unittest import pytest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from tests.common import get_test_home_assistant @@ -28,7 +28,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_valid_config(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -39,7 +39,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_valid_config_int_device_id(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -49,7 +49,7 @@ class TestSwitchRfxtrx(unittest.TestCase): }}})) def test_invalid_config1(self): - self.assertFalse(_setup_component(self.hass, 'switch', { + self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -61,7 +61,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_invalid_config2(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'switch', { + self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'invalid_key': 'afda', @@ -73,7 +73,7 @@ class TestSwitchRfxtrx(unittest.TestCase): }}})) def test_invalid_config3(self): - self.assertFalse(_setup_component(self.hass, 'switch', { + self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -85,7 +85,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_invalid_config4(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'switch', { + self.assertFalse(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -96,7 +96,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_default_config(self): """Test with 0 switches.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'devices': {}}})) @@ -104,7 +104,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_old_config(self): """Test with 1 switch.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'devices': {'123efab1': { @@ -132,7 +132,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_one_switch(self): """Test with 1 switch.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'devices': {'0b1100cd0213c7f210010f51': { @@ -170,7 +170,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_several_switches(self): """Test with 3 switches.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'signal_repetitions': 3, 'devices': @@ -203,7 +203,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_discover_switch(self): """Test with discovery of switches.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': {}}})) @@ -253,7 +253,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def test_discover_switch_noautoadd(self): """Test with discovery of switch when auto add is False.""" - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': False, 'devices': {}}})) diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 26ace315a37..6235fafc495 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -7,8 +7,9 @@ from homeassistant.bootstrap import setup_component import homeassistant.components as core_components from homeassistant.components import conversation from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.util.async import run_coroutine_threadsafe -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, assert_setup_component class TestConversation(unittest.TestCase): @@ -19,9 +20,13 @@ class TestConversation(unittest.TestCase): self.ent_id = 'light.kitchen_lights' self.hass = get_test_home_assistant(3) self.hass.states.set(self.ent_id, 'on') - self.assertTrue(core_components.setup(self.hass, {})) - self.assertTrue(setup_component(self.hass, conversation.DOMAIN, { - conversation.DOMAIN: {}})) + self.assertTrue(run_coroutine_threadsafe( + core_components.async_setup(self.hass, {}), self.hass.loop + ).result()) + with assert_setup_component(0): + self.assertTrue(setup_component(self.hass, conversation.DOMAIN, { + conversation.DOMAIN: {} + })) def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" diff --git a/tests/components/test_emulated_hue.py b/tests/components/test_emulated_hue.py index fb55551bdf3..e280ba827ea 100755 --- a/tests/components/test_emulated_hue.py +++ b/tests/components/test_emulated_hue.py @@ -1,425 +1,429 @@ -"""The tests for the emulated Hue component.""" -import time -import json -import threading -import asyncio - -import unittest -import requests - -from homeassistant import bootstrap, const, core -import homeassistant.components as core_components -from homeassistant.components import emulated_hue, http, light -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.components.emulated_hue import ( - HUE_API_STATE_ON, HUE_API_STATE_BRI) - -from tests.common import get_test_instance_port, get_test_home_assistant - -HTTP_SERVER_PORT = get_test_instance_port() -BRIDGE_SERVER_PORT = get_test_instance_port() - -BRIDGE_URL_BASE = "http://127.0.0.1:{}".format(BRIDGE_SERVER_PORT) + "{}" -JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON} - - -def setup_hass_instance(emulated_hue_config): - """Setup the Home Assistant instance to test.""" - hass = get_test_home_assistant() - - # We need to do this to get access to homeassistant/turn_(on,off) - core_components.setup(hass, {core.DOMAIN: {}}) - - bootstrap.setup_component( - hass, http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}}) - - bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config) - - return hass - - -def start_hass_instance(hass): - """Start the Home Assistant instance to test.""" - hass.start() - time.sleep(0.05) - - -class TestEmulatedHue(unittest.TestCase): - """Test the emulated Hue component.""" - - hass = None - - @classmethod - def setUpClass(cls): - """Setup the class.""" - cls.hass = setup_hass_instance({ - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT - }}) - - start_hass_instance(cls.hass) - - @classmethod - def tearDownClass(cls): - """Stop the class.""" - cls.hass.stop() - - def test_description_xml(self): - """Test the description.""" - import xml.etree.ElementTree as ET - - result = requests.get( - BRIDGE_URL_BASE.format('/description.xml'), timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('text/xml' in result.headers['content-type']) - - # Make sure the XML is parsable - try: - ET.fromstring(result.text) - except: - self.fail('description.xml is not valid XML!') - - def test_create_username(self): - """Test the creation of an username.""" - request_json = {'devicetype': 'my_device'} - - result = requests.post( - BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), - timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('application/json' in result.headers['content-type']) - - resp_json = result.json() - success_json = resp_json[0] - - self.assertTrue('success' in success_json) - self.assertTrue('username' in success_json['success']) - - def test_valid_username_request(self): - """Test request with a valid username.""" - request_json = {'invalid_key': 'my_device'} - - result = requests.post( - BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), - timeout=5) - - self.assertEqual(result.status_code, 400) - - -class TestEmulatedHueExposedByDefault(unittest.TestCase): - """Test class for emulated hue component.""" - - @classmethod - def setUpClass(cls): - """Setup the class.""" - cls.hass = setup_hass_instance({ - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, - emulated_hue.CONF_EXPOSE_BY_DEFAULT: True - } - }) - - bootstrap.setup_component(cls.hass, light.DOMAIN, { - 'light': [ - { - 'platform': 'demo', - } - ] - }) - - start_hass_instance(cls.hass) - - # Kitchen light is explicitly excluded from being exposed - kitchen_light_entity = cls.hass.states.get('light.kitchen_lights') - attrs = dict(kitchen_light_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE] = False - cls.hass.states.set( - kitchen_light_entity.entity_id, kitchen_light_entity.state, - attributes=attrs) - - @classmethod - def tearDownClass(cls): - """Stop the class.""" - cls.hass.stop() - - def test_discover_lights(self): - """Test the discovery of lights.""" - result = requests.get( - BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5) - - self.assertEqual(result.status_code, 200) - self.assertTrue('application/json' in result.headers['content-type']) - - result_json = result.json() - - # Make sure the lights we added to the config are there - self.assertTrue('light.ceiling_lights' in result_json) - self.assertTrue('light.bed_light' in result_json) - self.assertTrue('light.kitchen_lights' not in result_json) - - def test_get_light_state(self): - """Test the getting of light state.""" - # Turn office light on and set to 127 brightness - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_ON, - { - const.ATTR_ENTITY_ID: 'light.ceiling_lights', - light.ATTR_BRIGHTNESS: 127 - }, - blocking=True) - - office_json = self.perform_get_light_state('light.ceiling_lights', 200) - - self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) - self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) - - # Turn bedroom light off - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_OFF, - { - const.ATTR_ENTITY_ID: 'light.bed_light' - }, - blocking=True) - - bedroom_json = self.perform_get_light_state('light.bed_light', 200) - - self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False) - self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0) - - # Make sure kitchen light isn't accessible - kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights') - kitchen_result = requests.get( - BRIDGE_URL_BASE.format(kitchen_url), timeout=5) - - self.assertEqual(kitchen_result.status_code, 404) - - def test_put_light_state(self): - """Test the seeting of light states.""" - self.perform_put_test_on_ceiling_lights() - - # Turn the bedroom light on first - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_ON, - {const.ATTR_ENTITY_ID: 'light.bed_light', - light.ATTR_BRIGHTNESS: 153}, - blocking=True) - - bed_light = self.hass.states.get('light.bed_light') - self.assertEqual(bed_light.state, STATE_ON) - self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153) - - # Go through the API to turn it off - bedroom_result = self.perform_put_light_state( - 'light.bed_light', False) - - bedroom_result_json = bedroom_result.json() - - self.assertEqual(bedroom_result.status_code, 200) - self.assertTrue( - 'application/json' in bedroom_result.headers['content-type']) - - self.assertEqual(len(bedroom_result_json), 1) - - # Check to make sure the state changed - bed_light = self.hass.states.get('light.bed_light') - self.assertEqual(bed_light.state, STATE_OFF) - - # Make sure we can't change the kitchen light state - kitchen_result = self.perform_put_light_state( - 'light.kitchen_light', True) - self.assertEqual(kitchen_result.status_code, 404) - - def test_put_with_form_urlencoded_content_type(self): - """Test the form with urlencoded content.""" - # Needed for Alexa - self.perform_put_test_on_ceiling_lights( - 'application/x-www-form-urlencoded') - - # Make sure we fail gracefully when we can't parse the data - data = {'key1': 'value1', 'key2': 'value2'} - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format( - "light.ceiling_lights")), data=data) - - self.assertEqual(result.status_code, 400) - - def test_entity_not_found(self): - """Test for entity which are not found.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format("not.existant_entity")), - timeout=5) - - self.assertEqual(result.status_code, 404) - - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format("non.existant_entity")), - timeout=5) - - self.assertEqual(result.status_code, 404) - - def test_allowed_methods(self): - """Test the allowed methods.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format( - "light.ceiling_lights"))) - - self.assertEqual(result.status_code, 405) - - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format("light.ceiling_lights")), - data={'key1': 'value1'}) - - self.assertEqual(result.status_code, 405) - - result = requests.put( - BRIDGE_URL_BASE.format('/api/username/lights'), - data={'key1': 'value1'}) - - self.assertEqual(result.status_code, 405) - - def test_proper_put_state_request(self): - """Test the request to set the state.""" - # Test proper on value parsing - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format( - "light.ceiling_lights")), - data=json.dumps({HUE_API_STATE_ON: 1234})) - - self.assertEqual(result.status_code, 400) - - # Test proper brightness value parsing - result = requests.put( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format( - "light.ceiling_lights")), data=json.dumps({ - HUE_API_STATE_ON: True, - HUE_API_STATE_BRI: 'Hello world!' - })) - - self.assertEqual(result.status_code, 400) - - def perform_put_test_on_ceiling_lights(self, - content_type='application/json'): - """Test the setting of a light.""" - # Turn the office light off first - self.hass.services.call( - light.DOMAIN, const.SERVICE_TURN_OFF, - {const.ATTR_ENTITY_ID: 'light.ceiling_lights'}, - blocking=True) - - ceiling_lights = self.hass.states.get('light.ceiling_lights') - self.assertEqual(ceiling_lights.state, STATE_OFF) - - # Go through the API to turn it on - office_result = self.perform_put_light_state( - 'light.ceiling_lights', True, 56, content_type) - - office_result_json = office_result.json() - - self.assertEqual(office_result.status_code, 200) - self.assertTrue( - 'application/json' in office_result.headers['content-type']) - - self.assertEqual(len(office_result_json), 2) - - # Check to make sure the state changed - ceiling_lights = self.hass.states.get('light.ceiling_lights') - self.assertEqual(ceiling_lights.state, STATE_ON) - self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56) - - def perform_get_light_state(self, entity_id, expected_status): - """Test the gettting of a light state.""" - result = requests.get( - BRIDGE_URL_BASE.format( - '/api/username/lights/{}'.format(entity_id)), timeout=5) - - self.assertEqual(result.status_code, expected_status) - - if expected_status == 200: - self.assertTrue( - 'application/json' in result.headers['content-type']) - - return result.json() - - return None - - def perform_put_light_state(self, entity_id, is_on, brightness=None, - content_type='application/json'): - """Test the setting of a light state.""" - url = BRIDGE_URL_BASE.format( - '/api/username/lights/{}/state'.format(entity_id)) - - req_headers = {'Content-Type': content_type} - - data = {HUE_API_STATE_ON: is_on} - - if brightness is not None: - data[HUE_API_STATE_BRI] = brightness - - result = requests.put( - url, data=json.dumps(data), timeout=5, headers=req_headers) - return result - - -class MQTTBroker(object): - """Encapsulates an embedded MQTT broker.""" - - def __init__(self, host, port): - """Initialize a new instance.""" - from hbmqtt.broker import Broker - - self._loop = asyncio.new_event_loop() - - hbmqtt_config = { - 'listeners': { - 'default': { - 'max-connections': 50000, - 'type': 'tcp', - 'bind': '{}:{}'.format(host, port) - } - }, - 'auth': { - 'plugins': ['auth.anonymous'], - 'allow-anonymous': True - } - } - - self._broker = Broker(config=hbmqtt_config, loop=self._loop) - - self._thread = threading.Thread(target=self._run_loop) - self._started_ev = threading.Event() - - def start(self): - """Start the broker.""" - self._thread.start() - self._started_ev.wait() - - def stop(self): - """Stop the broker.""" - self._loop.call_soon_threadsafe(asyncio.async, self._broker.shutdown()) - self._loop.call_soon_threadsafe(self._loop.stop) - self._thread.join() - - def _run_loop(self): - """Run the loop.""" - asyncio.set_event_loop(self._loop) - self._loop.run_until_complete(self._broker_coroutine()) - - self._started_ev.set() - - self._loop.run_forever() - self._loop.close() - - @asyncio.coroutine - def _broker_coroutine(self): - """The Broker coroutine.""" - yield from self._broker.start() +"""The tests for the emulated Hue component.""" +import time +import json +import threading +import asyncio + +import unittest +import requests + +from homeassistant import bootstrap, const, core +import homeassistant.components as core_components +from homeassistant.components import emulated_hue, http, light +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.emulated_hue import ( + HUE_API_STATE_ON, HUE_API_STATE_BRI) +from homeassistant.util.async import run_coroutine_threadsafe + +from tests.common import get_test_instance_port, get_test_home_assistant + +HTTP_SERVER_PORT = get_test_instance_port() +BRIDGE_SERVER_PORT = get_test_instance_port() + +BRIDGE_URL_BASE = "http://127.0.0.1:{}".format(BRIDGE_SERVER_PORT) + "{}" +JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON} + + +def setup_hass_instance(emulated_hue_config): + """Setup the Home Assistant instance to test.""" + hass = get_test_home_assistant() + + # We need to do this to get access to homeassistant/turn_(on,off) + run_coroutine_threadsafe( + core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop + ).result() + + bootstrap.setup_component( + hass, http.DOMAIN, + {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}}) + + bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config) + + return hass + + +def start_hass_instance(hass): + """Start the Home Assistant instance to test.""" + hass.start() + time.sleep(0.05) + + +class TestEmulatedHue(unittest.TestCase): + """Test the emulated Hue component.""" + + hass = None + + @classmethod + def setUpClass(cls): + """Setup the class.""" + cls.hass = setup_hass_instance({ + emulated_hue.DOMAIN: { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT + }}) + + start_hass_instance(cls.hass) + + @classmethod + def tearDownClass(cls): + """Stop the class.""" + cls.hass.stop() + + def test_description_xml(self): + """Test the description.""" + import xml.etree.ElementTree as ET + + result = requests.get( + BRIDGE_URL_BASE.format('/description.xml'), timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('text/xml' in result.headers['content-type']) + + # Make sure the XML is parsable + try: + ET.fromstring(result.text) + except: + self.fail('description.xml is not valid XML!') + + def test_create_username(self): + """Test the creation of an username.""" + request_json = {'devicetype': 'my_device'} + + result = requests.post( + BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), + timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('application/json' in result.headers['content-type']) + + resp_json = result.json() + success_json = resp_json[0] + + self.assertTrue('success' in success_json) + self.assertTrue('username' in success_json['success']) + + def test_valid_username_request(self): + """Test request with a valid username.""" + request_json = {'invalid_key': 'my_device'} + + result = requests.post( + BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json), + timeout=5) + + self.assertEqual(result.status_code, 400) + + +class TestEmulatedHueExposedByDefault(unittest.TestCase): + """Test class for emulated hue component.""" + + @classmethod + def setUpClass(cls): + """Setup the class.""" + cls.hass = setup_hass_instance({ + emulated_hue.DOMAIN: { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, + emulated_hue.CONF_EXPOSE_BY_DEFAULT: True + } + }) + + bootstrap.setup_component(cls.hass, light.DOMAIN, { + 'light': [ + { + 'platform': 'demo', + } + ] + }) + + start_hass_instance(cls.hass) + + # Kitchen light is explicitly excluded from being exposed + kitchen_light_entity = cls.hass.states.get('light.kitchen_lights') + attrs = dict(kitchen_light_entity.attributes) + attrs[emulated_hue.ATTR_EMULATED_HUE] = False + cls.hass.states.set( + kitchen_light_entity.entity_id, kitchen_light_entity.state, + attributes=attrs) + + @classmethod + def tearDownClass(cls): + """Stop the class.""" + cls.hass.stop() + + def test_discover_lights(self): + """Test the discovery of lights.""" + result = requests.get( + BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5) + + self.assertEqual(result.status_code, 200) + self.assertTrue('application/json' in result.headers['content-type']) + + result_json = result.json() + + # Make sure the lights we added to the config are there + self.assertTrue('light.ceiling_lights' in result_json) + self.assertTrue('light.bed_light' in result_json) + self.assertTrue('light.kitchen_lights' not in result_json) + + def test_get_light_state(self): + """Test the getting of light state.""" + # Turn office light on and set to 127 brightness + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_ON, + { + const.ATTR_ENTITY_ID: 'light.ceiling_lights', + light.ATTR_BRIGHTNESS: 127 + }, + blocking=True) + + office_json = self.perform_get_light_state('light.ceiling_lights', 200) + + self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) + self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) + + # Turn bedroom light off + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_OFF, + { + const.ATTR_ENTITY_ID: 'light.bed_light' + }, + blocking=True) + + bedroom_json = self.perform_get_light_state('light.bed_light', 200) + + self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False) + self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0) + + # Make sure kitchen light isn't accessible + kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights') + kitchen_result = requests.get( + BRIDGE_URL_BASE.format(kitchen_url), timeout=5) + + self.assertEqual(kitchen_result.status_code, 404) + + def test_put_light_state(self): + """Test the seeting of light states.""" + self.perform_put_test_on_ceiling_lights() + + # Turn the bedroom light on first + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_ON, + {const.ATTR_ENTITY_ID: 'light.bed_light', + light.ATTR_BRIGHTNESS: 153}, + blocking=True) + + bed_light = self.hass.states.get('light.bed_light') + self.assertEqual(bed_light.state, STATE_ON) + self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153) + + # Go through the API to turn it off + bedroom_result = self.perform_put_light_state( + 'light.bed_light', False) + + bedroom_result_json = bedroom_result.json() + + self.assertEqual(bedroom_result.status_code, 200) + self.assertTrue( + 'application/json' in bedroom_result.headers['content-type']) + + self.assertEqual(len(bedroom_result_json), 1) + + # Check to make sure the state changed + bed_light = self.hass.states.get('light.bed_light') + self.assertEqual(bed_light.state, STATE_OFF) + + # Make sure we can't change the kitchen light state + kitchen_result = self.perform_put_light_state( + 'light.kitchen_light', True) + self.assertEqual(kitchen_result.status_code, 404) + + def test_put_with_form_urlencoded_content_type(self): + """Test the form with urlencoded content.""" + # Needed for Alexa + self.perform_put_test_on_ceiling_lights( + 'application/x-www-form-urlencoded') + + # Make sure we fail gracefully when we can't parse the data + data = {'key1': 'value1', 'key2': 'value2'} + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), data=data) + + self.assertEqual(result.status_code, 400) + + def test_entity_not_found(self): + """Test for entity which are not found.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format("not.existant_entity")), + timeout=5) + + self.assertEqual(result.status_code, 404) + + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format("non.existant_entity")), + timeout=5) + + self.assertEqual(result.status_code, 404) + + def test_allowed_methods(self): + """Test the allowed methods.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights"))) + + self.assertEqual(result.status_code, 405) + + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format("light.ceiling_lights")), + data={'key1': 'value1'}) + + self.assertEqual(result.status_code, 405) + + result = requests.put( + BRIDGE_URL_BASE.format('/api/username/lights'), + data={'key1': 'value1'}) + + self.assertEqual(result.status_code, 405) + + def test_proper_put_state_request(self): + """Test the request to set the state.""" + # Test proper on value parsing + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), + data=json.dumps({HUE_API_STATE_ON: 1234})) + + self.assertEqual(result.status_code, 400) + + # Test proper brightness value parsing + result = requests.put( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format( + "light.ceiling_lights")), data=json.dumps({ + HUE_API_STATE_ON: True, + HUE_API_STATE_BRI: 'Hello world!' + })) + + self.assertEqual(result.status_code, 400) + + def perform_put_test_on_ceiling_lights(self, + content_type='application/json'): + """Test the setting of a light.""" + # Turn the office light off first + self.hass.services.call( + light.DOMAIN, const.SERVICE_TURN_OFF, + {const.ATTR_ENTITY_ID: 'light.ceiling_lights'}, + blocking=True) + + ceiling_lights = self.hass.states.get('light.ceiling_lights') + self.assertEqual(ceiling_lights.state, STATE_OFF) + + # Go through the API to turn it on + office_result = self.perform_put_light_state( + 'light.ceiling_lights', True, 56, content_type) + + office_result_json = office_result.json() + + self.assertEqual(office_result.status_code, 200) + self.assertTrue( + 'application/json' in office_result.headers['content-type']) + + self.assertEqual(len(office_result_json), 2) + + # Check to make sure the state changed + ceiling_lights = self.hass.states.get('light.ceiling_lights') + self.assertEqual(ceiling_lights.state, STATE_ON) + self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56) + + def perform_get_light_state(self, entity_id, expected_status): + """Test the gettting of a light state.""" + result = requests.get( + BRIDGE_URL_BASE.format( + '/api/username/lights/{}'.format(entity_id)), timeout=5) + + self.assertEqual(result.status_code, expected_status) + + if expected_status == 200: + self.assertTrue( + 'application/json' in result.headers['content-type']) + + return result.json() + + return None + + def perform_put_light_state(self, entity_id, is_on, brightness=None, + content_type='application/json'): + """Test the setting of a light state.""" + url = BRIDGE_URL_BASE.format( + '/api/username/lights/{}/state'.format(entity_id)) + + req_headers = {'Content-Type': content_type} + + data = {HUE_API_STATE_ON: is_on} + + if brightness is not None: + data[HUE_API_STATE_BRI] = brightness + + result = requests.put( + url, data=json.dumps(data), timeout=5, headers=req_headers) + + return result + + +class MQTTBroker(object): + """Encapsulates an embedded MQTT broker.""" + + def __init__(self, host, port): + """Initialize a new instance.""" + from hbmqtt.broker import Broker + + self._loop = asyncio.new_event_loop() + + hbmqtt_config = { + 'listeners': { + 'default': { + 'max-connections': 50000, + 'type': 'tcp', + 'bind': '{}:{}'.format(host, port) + } + }, + 'auth': { + 'plugins': ['auth.anonymous'], + 'allow-anonymous': True + } + } + + self._broker = Broker(config=hbmqtt_config, loop=self._loop) + + self._thread = threading.Thread(target=self._run_loop) + self._started_ev = threading.Event() + + def start(self): + """Start the broker.""" + self._thread.start() + self._started_ev.wait() + + def stop(self): + """Stop the broker.""" + self._loop.call_soon_threadsafe(asyncio.async, self._broker.shutdown()) + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join() + + def _run_loop(self): + """Run the loop.""" + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._broker_coroutine()) + + self._started_ev.set() + + self._loop.run_forever() + self._loop.close() + + @asyncio.coroutine + def _broker_coroutine(self): + """The Broker coroutine.""" + yield from self._broker.start() diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index 060fdf01dca..b0517ec2f53 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -1,7 +1,6 @@ """The tests for the InfluxDB component.""" import unittest from unittest import mock -from unittest.mock import patch import influxdb as influx_client @@ -9,6 +8,8 @@ from homeassistant.bootstrap import setup_component import homeassistant.components.influxdb as influxdb from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from tests.common import get_test_home_assistant + @mock.patch('influxdb.InfluxDBClient') class TestInfluxDB(unittest.TestCase): @@ -16,9 +17,13 @@ class TestInfluxDB(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = mock.MagicMock() - self.hass.pool.worker_count = 2 + self.hass = get_test_home_assistant(2) self.handler_method = None + self.hass.bus.listen = mock.Mock() + + def tearDown(self): + """Clear data.""" + self.hass.stop() def test_setup_config_full(self, mock_client): """Test the setup with full configuration.""" @@ -61,8 +66,6 @@ class TestInfluxDB(unittest.TestCase): assert setup_component(self.hass, influxdb.DOMAIN, config) - @patch('homeassistant.components.persistent_notification.create', - mock.MagicMock()) def test_setup_missing_password(self, mock_client): """Test the setup with existing username and missing password.""" config = { diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 44a60ee986f..0bc105e3ad1 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,5 +1,6 @@ """The testd for Core components.""" # pylint: disable=protected-access,too-many-public-methods +import asyncio import unittest from unittest.mock import patch, Mock @@ -11,6 +12,7 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps from homeassistant.helpers import entity +from homeassistant.util.async import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, mock_service, patch_yaml_files) @@ -22,7 +24,9 @@ class TestComponentsCore(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.assertTrue(comps.setup(self.hass, {})) + self.assertTrue(run_coroutine_threadsafe( + comps.async_setup(self.hass, {}), self.hass.loop + ).result()) self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) @@ -66,6 +70,7 @@ class TestComponentsCore(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(calls)) + @asyncio.coroutine @patch('homeassistant.core.ServiceRegistry.call') def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): """Test if turn_on is blocking domain with no service.""" @@ -78,7 +83,7 @@ class TestComponentsCore(unittest.TestCase): 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] }) service = self.hass.services._services['homeassistant']['turn_on'] - service.func(service_call) + yield from service.func(service_call) self.assertEqual(2, mock_call.call_count) self.assertEqual( @@ -131,7 +136,7 @@ class TestComponentsCore(unittest.TestCase): @patch('homeassistant.config.os.path.isfile', Mock(return_value=True)) @patch('homeassistant.components._LOGGER.error') - @patch('homeassistant.config.process_ha_core_config') + @patch('homeassistant.config.async_process_ha_core_config') def test_reload_core_with_wrong_conf(self, mock_process, mock_error): """Test reload core conf service.""" files = { diff --git a/tests/components/test_logentries.py b/tests/components/test_logentries.py index 4bcef23ee7e..b7e40f7ebb6 100644 --- a/tests/components/test_logentries.py +++ b/tests/components/test_logentries.py @@ -7,10 +7,20 @@ from homeassistant.bootstrap import setup_component import homeassistant.components.logentries as logentries from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from tests.common import get_test_home_assistant + class TestLogentries(unittest.TestCase): """Test the Logentries component.""" + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant(2) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + def test_setup_config_full(self): """Test setup with all data.""" config = { @@ -18,12 +28,11 @@ class TestLogentries(unittest.TestCase): 'token': 'secret', } } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, logentries.DOMAIN, config)) - self.assertTrue(hass.bus.listen.called) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, logentries.DOMAIN, config)) + self.assertTrue(self.hass.bus.listen.called) self.assertEqual(EVENT_STATE_CHANGED, - hass.bus.listen.call_args_list[0][0][0]) + self.hass.bus.listen.call_args_list[0][0][0]) def test_setup_config_defaults(self): """Test setup with defaults.""" @@ -32,12 +41,11 @@ class TestLogentries(unittest.TestCase): 'token': 'token', } } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, logentries.DOMAIN, config)) - self.assertTrue(hass.bus.listen.called) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, logentries.DOMAIN, config)) + self.assertTrue(self.hass.bus.listen.called) self.assertEqual(EVENT_STATE_CHANGED, - hass.bus.listen.call_args_list[0][0][0]) + self.hass.bus.listen.call_args_list[0][0][0]) def _setup(self, mock_requests): """Test the setup.""" @@ -49,8 +57,7 @@ class TestLogentries(unittest.TestCase): 'token': 'token' } } - self.hass = mock.MagicMock() - self.hass.pool.worker_count = 2 + self.hass.bus.listen = mock.MagicMock() setup_component(self.hass, logentries.DOMAIN, config) self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] diff --git a/tests/components/test_logger.py b/tests/components/test_logger.py index e0243335dfa..6b290eec638 100644 --- a/tests/components/test_logger.py +++ b/tests/components/test_logger.py @@ -2,11 +2,12 @@ from collections import namedtuple import logging import unittest -from unittest.mock import MagicMock from homeassistant.bootstrap import setup_component from homeassistant.components import logger +from tests.common import get_test_home_assistant + RECORD = namedtuple('record', ('name', 'levelno')) @@ -15,18 +16,18 @@ class TestUpdater(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant(2) self.log_config = {'logger': {'default': 'warning', 'logs': {'test': 'info'}}} def tearDown(self): """Stop everything that was started.""" del logging.root.handlers[-1] + self.hass.stop() def test_logger_setup(self): """Use logger to create a logging filter.""" - hass = MagicMock() - hass.pool.worker_count = 2 - setup_component(hass, logger.DOMAIN, self.log_config) + setup_component(self.hass, logger.DOMAIN, self.log_config) self.assertTrue(len(logging.root.handlers) > 0) handler = logging.root.handlers[-1] @@ -39,9 +40,7 @@ class TestUpdater(unittest.TestCase): def test_logger_test_filters(self): """Test resulting filter operation.""" - hass = MagicMock() - hass.pool.worker_count = 2 - setup_component(hass, logger.DOMAIN, self.log_config) + setup_component(self.hass, logger.DOMAIN, self.log_config) log_filter = logging.root.handlers[-1].filters[0] diff --git a/tests/components/test_panel_custom.py b/tests/components/test_panel_custom.py index 1ef12161bcb..b07c62e441f 100644 --- a/tests/components/test_panel_custom.py +++ b/tests/components/test_panel_custom.py @@ -10,7 +10,8 @@ from homeassistant.components import panel_custom from tests.common import get_test_home_assistant -@patch('homeassistant.components.frontend.setup', return_value=True) +@patch('homeassistant.components.frontend.setup', + autospec=True, return_value=True) class TestPanelCustom(unittest.TestCase): """Test the panel_custom component.""" diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index b26483c8771..9ec3211f959 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -4,7 +4,7 @@ import unittest import pytest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx from tests.common import get_test_home_assistant @@ -27,14 +27,14 @@ class TestRFXTRX(unittest.TestCase): def test_default_config(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + self.assertTrue(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', 'dummy': True} })) - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': {}}})) @@ -43,7 +43,7 @@ class TestRFXTRX(unittest.TestCase): def test_valid_config(self): """Test configuration.""" - self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + self.assertTrue(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', @@ -51,7 +51,7 @@ class TestRFXTRX(unittest.TestCase): self.hass.config.components.remove('rfxtrx') - self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + self.assertTrue(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', @@ -60,11 +60,11 @@ class TestRFXTRX(unittest.TestCase): def test_invalid_config(self): """Test configuration.""" - self.assertFalse(_setup_component(self.hass, 'rfxtrx', { + self.assertFalse(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': {} })) - self.assertFalse(_setup_component(self.hass, 'rfxtrx', { + self.assertFalse(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', @@ -72,13 +72,13 @@ class TestRFXTRX(unittest.TestCase): def test_fire_event(self): """Test fire event.""" - self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + self.assertTrue(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', 'dummy': True} })) - self.assertTrue(_setup_component(self.hass, 'switch', { + self.assertTrue(setup_component(self.hass, 'switch', { 'switch': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': @@ -116,13 +116,13 @@ class TestRFXTRX(unittest.TestCase): def test_fire_event_sensor(self): """Test fire event.""" - self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + self.assertTrue(setup_component(self.hass, 'rfxtrx', { 'rfxtrx': { 'device': '/dev/serial/by-id/usb' + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', 'dummy': True} })) - self.assertTrue(_setup_component(self.hass, 'sensor', { + self.assertTrue(setup_component(self.hass, 'sensor', { 'sensor': {'platform': 'rfxtrx', 'automatic_add': True, 'devices': diff --git a/tests/components/test_sleepiq.py b/tests/components/test_sleepiq.py index 675673e1653..5bdfba4163d 100644 --- a/tests/components/test_sleepiq.py +++ b/tests/components/test_sleepiq.py @@ -65,11 +65,11 @@ class TestSleepIQ(unittest.TestCase): """Test the setup when no login is configured.""" conf = self.config.copy() del conf['sleepiq']['username'] - assert not bootstrap._setup_component(self.hass, sleepiq.DOMAIN, conf) + assert not bootstrap.setup_component(self.hass, sleepiq.DOMAIN, conf) def test_setup_component_no_password(self): """Test the setup when no password is configured.""" conf = self.config.copy() del conf['sleepiq']['password'] - assert not bootstrap._setup_component(self.hass, sleepiq.DOMAIN, conf) + assert not bootstrap.setup_component(self.hass, sleepiq.DOMAIN, conf) diff --git a/tests/components/test_splunk.py b/tests/components/test_splunk.py index d893a699602..1f6648ce582 100644 --- a/tests/components/test_splunk.py +++ b/tests/components/test_splunk.py @@ -6,10 +6,20 @@ from homeassistant.bootstrap import setup_component import homeassistant.components.splunk as splunk from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from tests.common import get_test_home_assistant + class TestSplunk(unittest.TestCase): """Test the Splunk component.""" + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant(2) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + def test_setup_config_full(self): """Test setup with all data.""" config = { @@ -21,12 +31,11 @@ class TestSplunk(unittest.TestCase): } } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, splunk.DOMAIN, config)) - self.assertTrue(hass.bus.listen.called) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, splunk.DOMAIN, config)) + self.assertTrue(self.hass.bus.listen.called) self.assertEqual(EVENT_STATE_CHANGED, - hass.bus.listen.call_args_list[0][0][0]) + self.hass.bus.listen.call_args_list[0][0][0]) def test_setup_config_defaults(self): """Test setup with defaults.""" @@ -37,12 +46,11 @@ class TestSplunk(unittest.TestCase): } } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, splunk.DOMAIN, config)) - self.assertTrue(hass.bus.listen.called) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, splunk.DOMAIN, config)) + self.assertTrue(self.hass.bus.listen.called) self.assertEqual(EVENT_STATE_CHANGED, - hass.bus.listen.call_args_list[0][0][0]) + self.hass.bus.listen.call_args_list[0][0][0]) def _setup(self, mock_requests): """Test the setup.""" @@ -57,8 +65,7 @@ class TestSplunk(unittest.TestCase): } } - self.hass = mock.MagicMock() - self.hass.pool.worker_count = 2 + self.hass.bus.listen = mock.MagicMock() setup_component(self.hass, splunk.DOMAIN, config) self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] diff --git a/tests/components/test_statsd.py b/tests/components/test_statsd.py index ccc494fbc24..eb8782b582c 100644 --- a/tests/components/test_statsd.py +++ b/tests/components/test_statsd.py @@ -9,10 +9,20 @@ import homeassistant.core as ha import homeassistant.components.statsd as statsd from homeassistant.const import (STATE_ON, STATE_OFF, EVENT_STATE_CHANGED) +from tests.common import get_test_home_assistant + class TestStatsd(unittest.TestCase): """Test the StatsD component.""" + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant(2) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + def test_invalid_config(self): """Test configuration with defaults.""" config = { @@ -37,18 +47,17 @@ class TestStatsd(unittest.TestCase): 'prefix': 'foo', } } - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, statsd.DOMAIN, config)) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, statsd.DOMAIN, config)) self.assertEqual(mock_connection.call_count, 1) self.assertEqual( mock_connection.call_args, mock.call(host='host', port=123, prefix='foo') ) - self.assertTrue(hass.bus.listen.called) + self.assertTrue(self.hass.bus.listen.called) self.assertEqual(EVENT_STATE_CHANGED, - hass.bus.listen.call_args_list[0][0][0]) + self.hass.bus.listen.call_args_list[0][0][0]) @mock.patch('statsd.StatsClient') def test_statsd_setup_defaults(self, mock_connection): @@ -62,15 +71,14 @@ class TestStatsd(unittest.TestCase): config['statsd'][statsd.CONF_PORT] = statsd.DEFAULT_PORT config['statsd'][statsd.CONF_PREFIX] = statsd.DEFAULT_PREFIX - hass = mock.MagicMock() - hass.pool.worker_count = 2 - self.assertTrue(setup_component(hass, statsd.DOMAIN, config)) + self.hass.bus.listen = mock.MagicMock() + self.assertTrue(setup_component(self.hass, statsd.DOMAIN, config)) self.assertEqual(mock_connection.call_count, 1) self.assertEqual( mock_connection.call_args, mock.call(host='host', port=8125, prefix='hass') ) - self.assertTrue(hass.bus.listen.called) + self.assertTrue(self.hass.bus.listen.called) @mock.patch('statsd.StatsClient') def test_event_listener_defaults(self, mock_client): @@ -83,11 +91,10 @@ class TestStatsd(unittest.TestCase): config['statsd'][statsd.CONF_RATE] = statsd.DEFAULT_RATE - hass = mock.MagicMock() - hass.pool.worker_count = 2 - setup_component(hass, statsd.DOMAIN, config) - self.assertTrue(hass.bus.listen.called) - handler_method = hass.bus.listen.call_args_list[0][0][1] + self.hass.bus.listen = mock.MagicMock() + setup_component(self.hass, statsd.DOMAIN, config) + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[0][0][1] valid = {'1': 1, '1.0': 1.0, @@ -128,11 +135,10 @@ class TestStatsd(unittest.TestCase): config['statsd'][statsd.CONF_RATE] = statsd.DEFAULT_RATE - hass = mock.MagicMock() - hass.pool.worker_count = 2 - setup_component(hass, statsd.DOMAIN, config) - self.assertTrue(hass.bus.listen.called) - handler_method = hass.bus.listen.call_args_list[0][0][1] + self.hass.bus.listen = mock.MagicMock() + setup_component(self.hass, statsd.DOMAIN, config) + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[0][0][1] valid = {'1': 1, '1.0': 1.0, diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 4664549fb77..a0868691e3f 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -127,4 +127,4 @@ class TestHelpersDiscovery: assert 'test_component' in self.hass.config.components assert 'switch' in self.hass.config.components assert len(component_calls) == 1 - assert len(platform_calls) == 2 + assert len(platform_calls) == 1 diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 0d7b8c46d86..3ef9bd1b03b 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -7,6 +7,7 @@ from unittest.mock import patch import homeassistant.core as ha import homeassistant.components as core_components from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF) +from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util import dt as dt_util from homeassistant.helpers import state from homeassistant.const import ( @@ -63,7 +64,8 @@ class TestStateHelpers(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Run when tests are started.""" self.hass = get_test_home_assistant() - core_components.setup(self.hass, {}) + run_coroutine_threadsafe(core_components.async_setup( + self.hass, {}), self.hass.loop).result() def tearDown(self): # pylint: disable=invalid-name """Stop when tests are finished.""" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index efe99f86ebd..f0ef9efb2d1 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -3,7 +3,6 @@ import asyncio import logging import os import unittest -from unittest.mock import patch import homeassistant.scripts.check_config as check_config from tests.common import patch_yaml_files, get_test_config_dir @@ -45,12 +44,22 @@ def tearDownModule(self): # pylint: disable=invalid-name os.remove(path) -@patch('asyncio.get_event_loop', return_value=asyncio.new_event_loop()) class TestCheckConfig(unittest.TestCase): """Tests for the homeassistant.scripts.check_config module.""" + def setUp(self): + """Prepare the test.""" + # Somewhere in the tests our event loop gets killed, + # this ensures we have one. + try: + asyncio.get_event_loop() + except (RuntimeError, AssertionError): + # Py35: RuntimeError + # Py34: AssertionError + asyncio.set_event_loop(asyncio.new_event_loop()) + # pylint: disable=no-self-use,invalid-name - def test_config_platform_valid(self, mock_get_loop): + def test_config_platform_valid(self): """Test a valid platform setup.""" files = { 'light.yaml': BASE_CONFIG + 'light:\n platform: demo', @@ -66,7 +75,7 @@ class TestCheckConfig(unittest.TestCase): 'yaml_files': ['.../light.yaml'] }, res) - def test_config_component_platform_fail_validation(self, mock_get_loop): + def test_config_component_platform_fail_validation(self): """Test errors if component & platform not found.""" files = { 'component.yaml': BASE_CONFIG + 'http:\n password: err123', @@ -104,7 +113,7 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({}, res['secrets']) self.assertListEqual(['.../platform.yaml'], res['yaml_files']) - def test_component_platform_not_found(self, mock_get_loop): + def test_component_platform_not_found(self): """Test errors if component or platform not found.""" files = { 'badcomponent.yaml': BASE_CONFIG + 'beer:', @@ -131,7 +140,7 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({}, res['secrets']) self.assertListEqual(['.../badplatform.yaml'], res['yaml_files']) - def test_secrets(self, mock_get_loop): + def test_secrets(self): """Test secrets config checking method.""" files = { get_test_config_dir('secret.yaml'): ( diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c84c95f396c..971a90896b7 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -48,11 +48,10 @@ class TestBootstrap: @mock.patch( # prevent .HA_VERISON file from being written 'homeassistant.bootstrap.conf_util.process_ha_config_upgrade', - mock.Mock() - ) + autospec=True) @mock.patch('homeassistant.util.location.detect_location_info', - return_value=None) - def test_from_config_file(self, mock_detect): + autospec=True, return_value=None) + def test_from_config_file(self, mock_upgrade, mock_detect): """Test with configuration file.""" components = ['browser', 'conversation', 'script'] files = { @@ -94,28 +93,33 @@ class TestBootstrap: loader.set_component( 'comp_conf', MockModule('comp_conf', config_schema=config_schema)) - assert not bootstrap._setup_component(self.hass, 'comp_conf', {}) + with assert_setup_component(0): + assert not bootstrap.setup_component(self.hass, 'comp_conf', {}) - assert not bootstrap._setup_component(self.hass, 'comp_conf', { - 'comp_conf': None - }) + with assert_setup_component(0): + assert not bootstrap.setup_component(self.hass, 'comp_conf', { + 'comp_conf': None + }) - assert not bootstrap._setup_component(self.hass, 'comp_conf', { - 'comp_conf': {} - }) + with assert_setup_component(0): + assert not bootstrap.setup_component(self.hass, 'comp_conf', { + 'comp_conf': {} + }) - assert not bootstrap._setup_component(self.hass, 'comp_conf', { - 'comp_conf': { - 'hello': 'world', - 'invalid': 'extra', - } - }) + with assert_setup_component(0): + assert not bootstrap.setup_component(self.hass, 'comp_conf', { + 'comp_conf': { + 'hello': 'world', + 'invalid': 'extra', + } + }) - assert bootstrap._setup_component(self.hass, 'comp_conf', { - 'comp_conf': { - 'hello': 'world', - } - }) + with assert_setup_component(1): + assert bootstrap.setup_component(self.hass, 'comp_conf', { + 'comp_conf': { + 'hello': 'world', + } + }) def test_validate_platform_config(self): """Test validating platform configuration.""" @@ -130,7 +134,7 @@ class TestBootstrap: 'platform_conf.whatever', MockPlatform('whatever')) with assert_setup_component(0): - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'hello': 'world', 'invalid': 'extra', @@ -140,7 +144,7 @@ class TestBootstrap: self.hass.config.components.remove('platform_conf') with assert_setup_component(1): - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', 'hello': 'world', @@ -153,7 +157,7 @@ class TestBootstrap: self.hass.config.components.remove('platform_conf') with assert_setup_component(0): - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'not_existing', 'hello': 'world', @@ -163,7 +167,7 @@ class TestBootstrap: self.hass.config.components.remove('platform_conf') with assert_setup_component(1): - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', 'hello': 'world', @@ -173,7 +177,7 @@ class TestBootstrap: self.hass.config.components.remove('platform_conf') with assert_setup_component(1): - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': [{ 'platform': 'whatever', 'hello': 'world', @@ -184,13 +188,13 @@ class TestBootstrap: # Any falsey platform config will be ignored (None, {}, etc) with assert_setup_component(0) as config: - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': None }) assert 'platform_conf' in self.hass.config.components assert not config['platform_conf'] # empty - assert bootstrap._setup_component(self.hass, 'platform_conf', { + assert bootstrap.setup_component(self.hass, 'platform_conf', { 'platform_conf': {} }) assert 'platform_conf' in self.hass.config.components @@ -235,10 +239,9 @@ class TestBootstrap: """Setup the component.""" result.append(bootstrap.setup_component(self.hass, 'comp')) - with bootstrap._SETUP_LOCK: - thread = threading.Thread(target=setup_component) - thread.start() - self.hass.config.components.append('comp') + thread = threading.Thread(target=setup_component) + thread.start() + self.hass.config.components.append('comp') thread.join() @@ -250,19 +253,19 @@ class TestBootstrap: deps = ['non_existing'] loader.set_component('comp', MockModule('comp', dependencies=deps)) - assert not bootstrap._setup_component(self.hass, 'comp', {}) + assert not bootstrap.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components - self.hass.config.components.append('non_existing') + loader.set_component('non_existing', MockModule('non_existing')) - assert bootstrap._setup_component(self.hass, 'comp', {}) + assert bootstrap.setup_component(self.hass, 'comp', {}) def test_component_failing_setup(self): """Test component that fails setup.""" loader.set_component( 'comp', MockModule('comp', setup=lambda hass, config: False)) - assert not bootstrap._setup_component(self.hass, 'comp', {}) + assert not bootstrap.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components def test_component_exception_setup(self): @@ -273,18 +276,17 @@ class TestBootstrap: loader.set_component('comp', MockModule('comp', setup=exception_setup)) - assert not bootstrap._setup_component(self.hass, 'comp', {}) + assert not bootstrap.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components def test_home_assistant_core_config_validation(self): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py - hass = get_test_home_assistant() assert None is bootstrap.from_config_dict({ 'homeassistant': { 'latitude': 'some string' } - }, hass=hass) + }) def test_component_setup_with_validation_and_dependency(self): """Test all config is passed to dependencies.""" @@ -316,7 +318,7 @@ class TestBootstrap: 'valid': True, }, extra=vol.PREVENT_EXTRA) - mock_setup = mock.MagicMock() + mock_setup = mock.MagicMock(spec_set=True) loader.set_component( 'switch.platform_a', diff --git a/tests/test_config.py b/tests/test_config.py index 4787da5bcde..7537351fb09 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) from homeassistant.util import location as location_util, dt as dt_util +from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from tests.common import ( @@ -34,6 +35,10 @@ def create_file(path): class TestConfig(unittest.TestCase): """Test the configutils.""" + def setUp(self): # pylint: disable=invalid-name + """Initialize a test Home Assistant instance.""" + self.hass = get_test_home_assistant() + def tearDown(self): # pylint: disable=invalid-name """Clean up.""" dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE @@ -44,8 +49,7 @@ class TestConfig(unittest.TestCase): if os.path.isfile(VERSION_PATH): os.remove(VERSION_PATH) - if hasattr(self, 'hass'): - self.hass.stop() + self.hass.stop() def test_create_default_config(self): """Test creation of default config.""" @@ -165,6 +169,7 @@ class TestConfig(unittest.TestCase): self.assertTrue(mock_print.called) def test_core_config_schema(self): + """Test core config schema.""" for value in ( {CONF_UNIT_SYSTEM: 'K'}, {'time_zone': 'non-exist'}, @@ -191,14 +196,14 @@ class TestConfig(unittest.TestCase): def test_entity_customization(self): """Test entity customization through configuration.""" - self.hass = get_test_home_assistant() - config = {CONF_LATITUDE: 50, CONF_LONGITUDE: 50, CONF_NAME: 'Test', CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} - config_util.process_ha_core_config(self.hass, config) + run_coroutine_threadsafe( + config_util.async_process_ha_core_config(self.hass, config), + self.hass.loop).result() entity = Entity() entity.entity_id = 'test.test' @@ -224,7 +229,6 @@ class TestConfig(unittest.TestCase): opened_file = mock_open.return_value opened_file.readline.return_value = ha_version - self.hass = get_test_home_assistant() self.hass.config.path = mock.Mock() config_util.process_ha_config_upgrade(self.hass) @@ -254,7 +258,6 @@ class TestConfig(unittest.TestCase): opened_file = mock_open.return_value opened_file.readline.return_value = ha_version - self.hass = get_test_home_assistant() self.hass.config.path = mock.Mock() config_util.process_ha_config_upgrade(self.hass) @@ -264,82 +267,91 @@ class TestConfig(unittest.TestCase): def test_loading_configuration(self): """Test loading core config onto hass object.""" - config = Config() - hass = mock.Mock(config=config) + self.hass.config = mock.Mock() - config_util.process_ha_core_config(hass, { - 'latitude': 60, - 'longitude': 50, - 'elevation': 25, - 'name': 'Huis', - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - 'time_zone': 'America/New_York', - }) + run_coroutine_threadsafe( + config_util.async_process_ha_core_config(self.hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'America/New_York', + }), self.hass.loop).result() - assert config.latitude == 60 - assert config.longitude == 50 - assert config.elevation == 25 - assert config.location_name == 'Huis' - assert config.units.name == CONF_UNIT_SYSTEM_IMPERIAL - assert config.time_zone.zone == 'America/New_York' + assert self.hass.config.latitude == 60 + assert self.hass.config.longitude == 50 + assert self.hass.config.elevation == 25 + assert self.hass.config.location_name == 'Huis' + assert self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL + assert self.hass.config.time_zone.zone == 'America/New_York' def test_loading_configuration_temperature_unit(self): """Test backward compatibility when loading core config.""" - config = Config() - hass = mock.Mock(config=config) + self.hass.config = mock.Mock() - config_util.process_ha_core_config(hass, { - 'latitude': 60, - 'longitude': 50, - 'elevation': 25, - 'name': 'Huis', - CONF_TEMPERATURE_UNIT: 'C', - 'time_zone': 'America/New_York', - }) + run_coroutine_threadsafe( + config_util.async_process_ha_core_config(self.hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_TEMPERATURE_UNIT: 'C', + 'time_zone': 'America/New_York', + }), self.hass.loop).result() - assert config.latitude == 60 - assert config.longitude == 50 - assert config.elevation == 25 - assert config.location_name == 'Huis' - assert config.units.name == CONF_UNIT_SYSTEM_METRIC - assert config.time_zone.zone == 'America/New_York' + assert self.hass.config.latitude == 60 + assert self.hass.config.longitude == 50 + assert self.hass.config.elevation == 25 + assert self.hass.config.location_name == 'Huis' + assert self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert self.hass.config.time_zone.zone == 'America/New_York' @mock.patch('homeassistant.util.location.detect_location_info', - return_value=location_util.LocationInfo( + autospec=True, return_value=location_util.LocationInfo( '0.0.0.0', 'US', 'United States', 'CA', 'California', 'San Diego', '92122', 'America/Los_Angeles', 32.8594, -117.2073, True)) - @mock.patch('homeassistant.util.location.elevation', return_value=101) + @mock.patch('homeassistant.util.location.elevation', + autospec=True, return_value=101) def test_discovering_configuration(self, mock_detect, mock_elevation): """Test auto discovery for missing core configs.""" - config = Config() - hass = mock.Mock(config=config) + self.hass.config.latitude = None + self.hass.config.longitude = None + self.hass.config.elevation = None + self.hass.config.location_name = None + self.hass.config.time_zone = None - config_util.process_ha_core_config(hass, {}) + run_coroutine_threadsafe( + config_util.async_process_ha_core_config( + self.hass, {}), self.hass.loop + ).result() - assert config.latitude == 32.8594 - assert config.longitude == -117.2073 - assert config.elevation == 101 - assert config.location_name == 'San Diego' - assert config.units.name == CONF_UNIT_SYSTEM_METRIC - assert config.units.is_metric - assert config.time_zone.zone == 'America/Los_Angeles' + assert self.hass.config.latitude == 32.8594 + assert self.hass.config.longitude == -117.2073 + assert self.hass.config.elevation == 101 + assert self.hass.config.location_name == 'San Diego' + assert self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert self.hass.config.units.is_metric + assert self.hass.config.time_zone.zone == 'America/Los_Angeles' @mock.patch('homeassistant.util.location.detect_location_info', - return_value=None) + autospec=True, return_value=None) @mock.patch('homeassistant.util.location.elevation', return_value=0) def test_discovering_configuration_auto_detect_fails(self, mock_detect, mock_elevation): """Test config remains unchanged if discovery fails.""" - config = Config() - hass = mock.Mock(config=config) + self.hass.config = Config() - config_util.process_ha_core_config(hass, {}) + run_coroutine_threadsafe( + config_util.async_process_ha_core_config( + self.hass, {}), self.hass.loop + ).result() blankConfig = Config() - assert config.latitude == blankConfig.latitude - assert config.longitude == blankConfig.longitude - assert config.elevation == blankConfig.elevation - assert config.location_name == blankConfig.location_name - assert config.units == blankConfig.units - assert config.time_zone == blankConfig.time_zone + assert self.hass.config.latitude == blankConfig.latitude + assert self.hass.config.longitude == blankConfig.longitude + assert self.hass.config.elevation == blankConfig.elevation + assert self.hass.config.location_name == blankConfig.location_name + assert self.hass.config.units == blankConfig.units + assert self.hass.config.time_zone == blankConfig.time_zone From 33439aaa2219cd712bc481c8b5102c50cbc60aea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Oct 2016 00:21:55 -0700 Subject: [PATCH 049/149] Update frontend --- homeassistant/components/frontend/version.py | 4 ++-- .../frontend/www_static/frontend.html | 4 ++-- .../frontend/www_static/frontend.html.gz | Bin 128945 -> 130411 bytes .../www_static/home-assistant-polymer | 2 +- .../panels/ha-panel-dev-service.html | 2 +- .../panels/ha-panel-dev-service.html.gz | Bin 17418 -> 17440 bytes .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 2327 -> 2325 bytes 8 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 1c437dedd5d..5acc1e53f30 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,12 +2,12 @@ FINGERPRINTS = { "core.js": "5ed5e063d66eb252b5b288738c9c2d16", - "frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d", + "frontend.html": "4022865a7890970c8cef71eb265a78d3", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", - "panels/ha-panel-dev-service.html": "d33657c964041d3ebf114e90a922a15e", + "panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 58990a9c766..309447a6320 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,5 +1,5 @@ \ No newline at end of file +},customStyle:null,getComputedStyleValue:function(e){return!i&&this._styleProperties&&this._styleProperties[e]||getComputedStyle(this).getPropertyValue(e)},_setupStyleProperties:function(){this.customStyle={},this._styleCache=null,this._styleProperties=null,this._scopeSelector=null,this._ownStyleProperties=null,this._customStyle=null},_needsStyleProperties:function(){return Boolean(!i&&this._ownStylePropertyNames&&this._ownStylePropertyNames.length)},_validateApplyShim:function(){if(this.__applyShimInvalid){Polymer.ApplyShim.transform(this._styles,this.__proto__);var e=n.elementStyles(this);if(s){var t=this._template.content.querySelector("style");t&&(t.textContent=e)}else{var r=this._scopeStyle&&this._scopeStyle.nextSibling;r&&(r.textContent=e)}}},_beforeAttached:function(){this._scopeSelector&&!this.__stylePropertiesInvalid||!this._needsStyleProperties()||(this.__stylePropertiesInvalid=!1,this._updateStyleProperties())},_findStyleHost:function(){for(var e,t=this;e=Polymer.dom(t).getOwnerRoot();){if(Polymer.isInstance(e.host))return e.host;t=e.host}return r},_updateStyleProperties:function(){var e,n=this._findStyleHost();n._styleProperties||n._computeStyleProperties(),n._styleCache||(n._styleCache=new Polymer.StyleCache);var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,c=this._applyStyleProperties(e);a||(c=c&&s?c.cloneNode(!0):c,e={style:c,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var c=a[i];void 0!==c&&this._detachAndRemoveInstance(c)}var h=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return h._filterFn(r.getItem(e))})),l.sort(function(e,t){return h._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 7e2be4a3b4c82f456f2cd25eea2f1be23794a764..b6b0768f5bfde7b98b2e2ce54b9e6af1c9067e66 100644 GIT binary patch delta 103178 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 ss#$Dg2vCR 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
uR`t04Bufs~v0(ptz>1 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 delta 101711 zcmV(*K;FOW`UkP^2L~UE2ncGf3$X{fYJVa!G6H@Vy%J-hCHAW5FgNP65Ow1Mc=Thn-4}(GnfH-W{gpww9vPKgALe; zG7H6kXbYfo_R$Qcixs%4t{0xg#($dqxW#L>8fX5b{rteljlzpuBlZglNN#j&^4ETW z{5)9pyr)F+Ho?2+Z+>}k_VcsXFZg9#pDAL|LOC0#T%c4StLIhsl#B|dK79=61LT`D z0znlv(|#AE9wyiH{4NMVzn>0zli#Pec%uoa(eI~IzlR>3;%NUAq@W+}Pk;B}h+*W} z!$)FW-^Zx+zwhq{=!MA(t|O04&i{eQxQT$R2tGnHUT_&P zFF9WD3&pH_n_Ry5yzF}a=zn^>B~G6oyo)^M*Aa~<&j?Yx;CX~1GW5rmcZZ3%RGKPV z-h08g#~oxnaoKwnm%Ne-pM;-unsTmc!Aa1GGSfMzx`mgacm01jhkp?}hcjhnoHC*1A- z8DqaWok7q4(jZU%Qi7A}k*qHeFvTBdft%d$nWW0^-oJhI3o|tn-H;gv-MH4rUh`=y zO#{s+CH(Re>Qks2^@*XXf}(DxhWaPxv+D?7TnE`{#VOG=N)+;OeW94fCiipVQnu0n zF~2AnyHdu0*WO`Mc_k8_nh!YT};Fsp5NqM3us!Km<8KhKK*U zy*-_(bY>}v<^k%=sOo2i*c-H{6faUM->o#Ws!B7)3ec9Sr&a``zp!qxoPdCDlditU zgbAmHusBb`>srIm?YXmp;%K`os?-z^iq@uyyPHG3ziv~TaDVM0+Zy5iUwCnJ(TKV> zAY#9l_b1E>h-NF?|MB!6!Sw#0_RX+|bQ$*UD!q=Hd^?ErP9nx556x?QwYXN!g@D(a zCrkF4uMF4FldN1B24+SJs*u5qV-t1PY3ctK85fC8%MSAOV` zCbVC|!1`%+m46gzh1jCHD$HA{f(3b&Y$jUP!qK|kpx6eoniAVk|4u?1i4JL0iW(u* zQ2q-D)|8@babv;=g~?R-M)dh}?-0%WVsrymsQZyv!C9)iiyKtwtO`UNF3ppRGrU}- z8=7)#yx~j+YAt+pT#*rqsl?M_%5Rbg0bXU~mKXVW)qj18dWs+qVit(%-o06y65bH@ z&@L3<>SjL4xgJwall+9Ui;+oV8MSlFbWaP6k-b8TXYOS&QwsckAE)XfDv)PiMi;Ny!AIaTfHwVt;uuaS`pEAr z{g2Vb>3@wDM&l!l3fPjA;UKL2-u3M*m4$!1zmPa|$>aR>=DnMq7sA+;`0@)XX+Z1) z;JZ5X(UL_8hgraR>aAcxdY2qq#vp28s#>62Nc!AJi!NsEc3qTa>1{r8aeH8WzU(2j1q2 zb2K_9Gek~?7#a-QLWqbDBYGNAGNc9o7L?B4lg*+;VsMTKEj||vfoF(~akm8I8z<4} zEPq%AtEmJ%xKc*`(CSjW_mBqSEVOm~Zs97Lk$sW68-lJWN!>yrLr*r8cCO-Wq9(W_*}uW1lCz4jU}Qt+{=rhdxV6~9PW;UiGRyH)|0>&RuhW=5_F z+q#O51Ei18lF>!W3yquCWR9BhK{1iqDAl7WyEo>;s`;>P3~z1Wk#rAfCN~;NU4JK4 z>jSLTaPdWfUohU4OUebRxz z8jC<-61q2jaN|aGB*@mPIIHN3BkJgBs*>gK%Ml!$#Xw?Xblhy(%BCQ55UnC=x=z*1 zf^^9{P*KDr^9p3SW@_Ey`Beiubbs*s*(_d`E3^`#e(&AmS7m#VFk=%pYbel)`&8y( zav&W!+BZvq;XFv&lR>-0(bEK&9M6ETKy8&kvG`B^JU=XKlVB#rdw!s85-#yLVy09F)tVV zFaTz&vA?}qFR9v-UA=845^^4@xj|v=$SPT{)L9})49a+lOAnz$GY&TEPq=sDXk@sl z<-li2GB0bnzkUK4(PbMiv*ah9DQ>8A0C zZRto43b!kFv^P=Frcm`$vv>9IuC*2%Dbs@^MVT9zAiD!F8My$YpdG}~Wy0bGmF=&& z>jU9Wq5B1KpU4#}xqsXtt0K);<#1d)(X*tsdWJ zIiUrR>2jpj>_u~7o48c84TZ827TG0jY0Ve@x*Uk!nMPIKJV$Y_2W0KjRgz%`ZwkCv zq%u{2!K`F6g`t@jZ&xzsV;nBaWjQ;F2x@Y!f~qUh(O{4V>`%Guwl()1j3bj@C*7vE zx7zhQnt?IY1%GC7AtZM=j+%jJ&bqy&_^0K7pQZ|3UuEJBL`AYfAJRgC!@$AD zB(#F$ci5b#Mmby*1JQ7G4MapkJbiq=YS1dV!z*lv)Y9FMNgP*go2sxXAMZe$iUQCt~}C| z&>?%;knhpc2JWs`_ZpM?<2Z^1nTZ?viD|B5%i|1cuzT@lgM(x*B5e;ebodJQKofO$ z1AnlD$nC4#u1n?XjS6896U$YKC8S`F~6)AU6{8E!|R$Z>L@_r*4}|g2<09?-3+8 zPP2_>I#O(8fX|b-ic#janOkYa%eh)+VvA#!f#%$A|GOIRiI3uX5#5n=CVb; zz0E9(y)kKG3}jqtDkHLGAiW_vR7J|^V3#twBB8Xm^8QnNI&^A9$Qgn_d1BEsisdC$Ykn}OGZp~jAhb9R{n zd05=u&P1%6d_1F@SW5?wT1^*X5|iyRo-Z7h^Hor^nC;VyA~>t4a_<3qY5{WK9R#|t z+wYyLmff|G*uqu7gt>4AKL~#p8}hA=%Kau}g0T`0hb(@*bUi37Mx`8hdHx5eh4-m&mpmmUlOvMDFh+ycsK=*NgDZO4f^)(Hw zoCfk{7Zj(t>AH`LIA?~BCQYk_OsevX&{)hRO*-C=s|$3?gW%iGfq(Hx=->5B@+xnR zre+9fwWg!5l^J-Kp5M}2WR>rEeXmyq?l(+{n;dFZjoY3qfR$fU;=z!g2NL$%^F7+3D5PA76pY zhQFd*urB^Yy+q1_g?}F|=xI+r*5-4@1(!eyF8Xu2#>o-vCO&mFwDbCS@FA#Ebx-en zVW&+}4Zg|`jc1~?r~anr66DRV&nepnoc?n?bjzPaunZRJ`E}(A9Ddd_G?<^WLWEywkx_rAbh>Zk-H`T*7UG z7{aI^g?Q;CEhKGO8D|zvLqK3=hFH{u9^^0$DsqcoN)dV72xC-|X?F%3Pw;pBRnceh zqR6t#F?bTXJBaL{kTXhM!|7Hpti0_<>(|fz9*%}Xe1EW4xL(>;cyf^a5^CYOAypX_ zW8FI!HhL6nJb@Xf5%8lQP_V;Zm!pbT(v=g*>jlrR3diWgnHT9>1g5B^|L(C2p%Hcze5j0-OVb>x*S zLMCYs(SJuEY6kJ2y{^YvQC=2N|G z24|XDglM9+3&S-sJhG1vdAmw-+o5%f`>BYRU4Ln}A$|yxKMXBX_vC2U|L`k0GxmAP z@3qL)P=^)OO?S+dP?hK%l6kS~Fi{E&0JOv47qj;|Ov#{ChGrH?11Vhw{OUEo}{# z_J6c%LCLi1J8Pb#BUKGq2SlH`%9y0!&&?brXUs{RVI9Ya5J>fB%psOEyDd=H!(y!X zV{$CnavV2qhx$@0^Veec$g(siGKezK==Zr~Uu{C6(SWSkn!+-)5ada>K5epRO?Rb1 z+l*0L&?}V|o%yFsFUPJHjYTv~3Hf4As(R7XcGtblfQ=naTR9h5=Z?v|k7ygP9nX~7PNZuR_rcrsgG}Gg6u!pT-@vlf zxkc%YaNxcr>}D35ltyBW-KJUOx4A$;!Js&`gF1-}@CHJ(5yr>?wPA2&zHkdAJp=gT zc;Y5mNi8W9M()npD-0_jqv)F&WkXW|i(y>*8hUuxXY3}6|2MFWBYWWWS)m&|o zqvf0>&c(({yWA>SdoaG^bnn2gCh~dXx7dt)Tbc0I+n@;5yT}h?eY(-cU0hI+Qz^Rr z+iCv}Y#~E5DdtjjAte^PL4U4^EQMo@D=woHegh*>sCB6CswQGhHj!vMz5RqITWaA1 z%2|*Mv!oKn!Fr7w*_le{{8?1gVc^`RjR<9Izp;y(N+!v`wR6*wU4R&j#Xy6l*OOGd zrkdujs$8RWv2+31g;7Cy>x2)4293&nqXVOzz!jE39JGT}|zQwZUj3Tjit4SsAH)0h_3Gkz9W6lI~*@aCFsqhgvTKc*r zaD4m92i+iJ!%j~@c7JMe0-JjQ zL5(~a(GK(lMoT08yK4l8)N_nCKV}x5xTe5fo0z%ul+Ht9xPSRhDfesm!T8jCEX*{u z3pw=Ds_FdOBYB<`$t7&iq~IxaS(sD{ugb2{oh3`O&aR3Tku*JBP8Z1*X?gW_r`F$d zLC$PTJ_|LHpUH5pW;%Um+EGVI;?r;FnyWQV9qcnoBoNFeyC#Eh#Bm4vkV_886aH z!~-L_&cSP$CkZ|RpCyHr&gY3I39MoaVPLz0Rw1wCnCgb_JYIa^hK zMpnu(wSZ(mf|8fUd*?PP7t9FGlEI=aKRy3-b^iuM}Qs3<<`8h=kY2h}LBbaUn z;lb(KXTe*f{{qc2LJLvS&?de(V#+<|xXE>RgVtMVx`_4pRX=^{{c@dLl>>2YMq%U< z)2xK?RSYqFO^1N00rfV5Apt&PXnnSdmfZ6ccz?z$15$w-TWf8DqHx30Qf5(l=H@Gv z3e{pmxE$s12h%vO`fzKC(9DhxqnE}<&^@C(4p09y6sXEmeCcq~P%szQ&8sXVl$bxl z)ZI~%@|U!9X3Q_+@!&vrb;}+pE}FeE6ljH_)Y7Nb)Nd~D+q0(JN5&e?FqKqrrC&kQ$i%PaXQeol|$y;Ks~W4WqByg49Iuw1y!G3xSt3u2LG*RSmk!Q1g|g z`K*V+6gIsKD%v94W`@%x*8?JMQteys$`Uz410sK6ct%{Gon58#MDROglbnPbe=M~{ zM_GPOkD%y!j>iqqw5Mc@aTqZ`je zna!tJf_cl+_;(4eCO}hFWrY-QWqoC76?H*Kcs>#FZB5yt3KBzTsC7#DZrMY%C~_)U z)?gmp0!dvO8Q=v%dU5khTwVRTf8^UjPkIHbfW>O66J4REr}GMoWsG!=QZ{8_k!Z3f<@ z$LexqPhOOu9fq4+_;gWWyC-@7;_u8i$t0Oj?AD#*c(yGpz>B_M zC`6)@`NXEF6dm_9s^;`csGTOtm2AdrToZ?u1dc#YwBrf-NyPdmer?0bq5E&{gs(ML)DIi+9M7rcSe<v(if0hJNK2{wKG|zH6 zD_Iz>^12kUnD-Dln?a_=IMXzb%?;g|W_$QJUT;7SXp@MJMN(icJ*I1)TzzJrqE5qW z-n-V8g;kRI0A%k%tCDtf3A{6x3u)IdgjmaEbo>a%Lzc~JjtFE#;NsC!*aU*0DXlTmM4VG zp^oe?`Bdq+UiiU978(GbiAGqVO~Bh`yoOgV&oWqP(eFC>s2jSXSF_jg5)SI?Wbr;f zi9w7(M`x+u^ZK)^IJ->dz@kpaX3GrjXEVe>WS7WG3NXrAf3&OiS(ipAjQOe|i^WOA zjv~xeyVNR@-bpGOtDt1e8myX_)5tb97rwa7VtBsr5+vI-u2K6#H5C)f1$pMHrfpi< zs6enWY;1R`VhSprM-u{0t6nmV3e~fV%5EDK3z$?|?2{a&-lX9C*kH{Sa#EKT|0)%E zSQ4ThY9bTne-zhBi)y9n_OhAsT~^S`N1x1PMesj-rjom1IZ4V{kuK@B7-PTjLUZ)U zsveUh^m2+j65op=3LR)c)h*D2lY{Ft<{vo$YhD3UYp*68#h^1+F9c_gG{*6>3v=T614b&1LxSQ;Yj~-POWn*te@PSWP1V|^wE;F9J~1w}>Bq~x zp?t**DEBDtqQVqYQ@pF`0n8}X5oV0M0Y194-&n49wc#pB%9yzz5h3&}SuI+|BUbf70({~&BCHvPE3D0aGys-LlEAq9y&Vj@KM{O~m7OL~db zEZVqbf7EfiZnBgjq2=*aAaoNt$^;k+gP$BPkc;>!&gP4x;MIXSgO^N9-Yw> zCOvG-rXc$_FoxMwdat7QM?%NJb-ge(G6P4LgLt ze_`^XgtPWPllTL0uCl;$nIE|%alJmi+!S-okbrz_;uE@vtJ}$1QwDY5rSL8vy2Nc% z?^SmAU~Mx{PvSXSe>7Ik*J^EZe;3#|1ToS4x?;(~RAGpNAG}_LH@>Kz|M$ea zGwUXb-z_m0FAC+YEUQD;Vlnq8Lcl@8e!%5JGZkGT>6L5~vDOi=7BS$E-ZmeJ7(vsl z!2!#_AM5H#IchpX=@p2Rvv7l^<}Xf;{Ag5B=$ap+ij{R|L`itp51_>fU_ zkJ^M)7Ea;?fi`8;)Y$igqZ^tp3(7_q_4O5P$Of9+qpQ+&WgL4)O@5U%cizlU7ehH% zMtYi6Uch}Dp`?@O&ZHA1>152^PcmYI@!Xxf3Vqn^mTmm1Q;X-l$u${m~a(&^ajiGro5fC+3JF^YQ*3uyN|3bcWVr|3c}llY4< zWpcIiaAF(!_ExtyR87%CIY$pVW<)L)s--W3m(m-8v4-rGTa6nik|pqgyYK>r$q(&@ zHSWrg79I{A@y*PDnETGq!;;=6Agp-SPz3X!OqmxIFYuBNJkW1kA^W+|WCa)L0wj}; zj2IEer>A5)kj#+`Fmrp>4$o$Cledf@e|8-&(Z3Hq2Q@3Rc{>=Q*^w&uKE1Ze@r(aC zbi#kif?cZvAMdZXaeLBqGCT#a5e4{cb?IYp-}j~glPetNQbK~H&}GU3QGG4a>$HkS z`??j=SPKBf>^EqVbd5bK{8bKGa=_9x*=(Jq1))teK5kp_TngKfM6WUpi-OOJGPIIfz2tt7AvGNqs;9}i8&d4_sETA!g; zud_2kVn+cc5XI$GiR+GhzUNgT?ibK0A_V&WZq%ICG#%pI3e(QTko(Q9m0;<{pn zqn(jkm>bv)WY$zFO*C5ayadAce>S-&lJZK9f^c1ME%v>iWNr~5nTfVWhVDErR{Ou_ z$ZV40QaHaRRbrAZx})T9KM{u1Cb_F&qfR!ZyuheKEnnO=Vq45JQd1FhhD==|un`<( zXp@F0JIL}+?EM5OSR^T_!==v;!Sh6=n=yja)v!jJJgI;C=PD^Htt%$mOW3LA(o(MU zC`{AdbyG*ynzI>3YiffJq^`s~PxiY>-!XgAsz=TGYo;^>K)QC`eRNhm(usU8vdcKv zNSMrRMMd-Mu2VOjj|y6@lb?QAc;J`et|L46#_b*c4Hq;)ukA9Cf^Mu!85DO7QQCzZC=3ca8sM}iPP(Z`CVmB~xUigUpM1vg z;LkcyXvEj{CPnm2!?35Te@O1uj)bW&9ne~OxAAx$iNR`!t)ez*4A4Y>tx~jodsE2BgvOSU4}EqZ9b-#hign)S$g{qhf2K$Mk(4hXgyr;NV8i2?S*f4zDePa~}n6}sST zo`1?Ni+G+mTk?bCRDPlE)M}V{9M^pj>P&P{QM5EAHVN-=Lv`2p)|OGb?z!XZbCDJa zJ2#kph-X|g=>k9E_a`-dzcgL2F}j$J^9gzZWk?jI!Cr1-XYs^9&fJAlajC(QVvHf+0gQ?h=f*%& zhfvSadS-SIV!`6(Z5uz0ig66vRu`$!*#zm}WM46PeFMp*e@JIYzueE$fjhOdse<0M zXjLsl74G9v%W3IZMbJJgLPkxon)ASZLYbOR1EfDuY3ou&eNr>pwJFh2chsc#EPX18 zv1HIOE!z+^GK#Nmf)P5?gkQS$6Si0^NCL%qXDH1NAvN{rV<&dF>g3OHVwk3}w2_%9 z`O&?wGqy>uf7m9aH)$M(b#1pUqD9MYz0!7TVei&OZGTpJMZQ1vYJvJdzYFYb!zvnT zO7MlQNnaVK$J_jqJZe|L0#S#*2r)C8Vi?u1vS5!MIXpqxxT9coU{#VQQ8||JGkWFI zr=y^@3jw(V@sKex_)MtIVn1N-$QDm9C{^AYF*=QQf1TaMQ=k>=G|I>4MG}A5lu_Ov zZNx2jN=o=NDtjZvU)zKtC6E2qwpAQO_yy4_*LHYHYkwA&2Ct5lJn9Lz3e|&L(iKFl zo%*nN3q4>`RYeMcNk-QfGoT|T)-!{B(LQmU5=hv_tjT2N;z>dY^5XKY+y|&N!{})GlU5CXdLuae zFn-luVd4=50}{AAoQ6E+X`7yWY~9Uanbdb;V_8EOygAN-3^CMZ>RYC@j-6tG>qv^R zx}r90Y6hv*(o_lWJ6#g!;*!Jmj5M~pn52@4L&PzM6QFTn95KR^kSTC7&5mKt=|H(e ze~D3HV(tUJ3zAA`iM(FHEtPjMxux4^v;Vm4p(;eInP$@}dyw#8$zH`ZI=nWYWjq5j zZT^(qcyk;>drR=^|3YU=3wiy&Rf0^o*cj9eRlg?;+Kkm85L&Lf$w-}ZRgZ{u6;)uG z^$vsV$#5bVgD_L9lEDOnK$X@APC3OVf6aTTEkt!Q;BhCyK!o8#PwzCt#Z6r!#i&jV z<$I&HdJK|e&Z{yTDs`o=3Xl;|Q=uxqf4p2I z4c%{ePy*SlbzSVZEfAghzT=WwWY(^zSJ@)XJ}@1zCrbhHa%FmRA))?pX5z>_;qwuv zd18Pe{Onp_39lj|S*Za<3f_6NUVx}s2(7ksAJBSh{H>T?7NO@}o>LzC%euhPb=7Qq zQlPDOq09k86Qn3X(*ySDRK!!$f9g{KBhBNg*8bQ6(Ed4L_Ka%zJ3g#(Ux ztW8(&RoEi-je~}4zw$!MSZ&FMfW1_VVqs zf1@Ax)?W)pD54LbPbD5S%WASC_>=si^G-Tg^acA|!7+kDi4-&1d{m_uym{sgoihzL z*{mxb*_12wh|_PrEZhCMe=EJ1bhlai1g_rIvn5*!5jWHvaRg&tpNU}4RZh%j;|m+{ zEZ|}iji61;7+A^z_&7nRf5|Of5BKoQP900B z5^1^%f2^4jtFe%mQme^aG>OJK*77?o@XYy2TIY*A4Qu*vmv>ZSUC=1ru*@@WbhwgI zxIfK|;C7X`{g*RB3`(nbZ#jE%zatYd4bqFT8~xSbTgClGadlg(UnqfYYt|uDx=yh3 z6s=pfpJ5I&Rn6*je~Zg7Jp|4okI{vY7ez9n4%t{@rDP)0;`yB3+KFd}UGL=0YyKGW zZ_orD3Qom??i~wWG>DS24J$d5I!-D1|8O+TCV;= zQ})OYgkAj58g6Z+(y|%ZceaZiCnl|suQ;W)S=)bBSCU=`e|q2&x@x+1DRt3XQC*jA zmgkR`nq0{4?v2}c&^@sp?l(W4#7D*K4Y01ma|Y4kE5u6I7h}Cq)UyPMUpj>KMvt2jfq=ZJeEj5#1IB$UyeMTwm!uQ~9b< zPE5azKjpd?f8j=F%9f`y^DVlLgKBo|0m)7GzDN=bphqI-^L%+jQ^nq0OU(DpyFQz( zs%#p*h1>YV2~QypF2ZSbs_ZR58}wdQIdVKmBcEqkR;{9|^blFd)6 zAvLage}iuL>)GyPts^QBuc}=25{^2f4kj6mt5oC5ZVETI))ao4%}|Q`WmUI;7%o3s zF!0XJVV}n(@Q7I0{Ip>^(3(Fm8$MtSAJ|n6wJL{Zl|xqL(5`Z%RXH-N9I+}#c9mnT z%CTAHm{mEpt31%EJTR*~U{xO2RUT?p9-37if3hkM?JAG7Dv!)6k64vQc9q9kmB(h4 z$E?a@yUJ*&ulvwi@*!XIVXfLouQsx(2~-)?s_AGjvhZNU5n-fb0x(wid{OSgc#&Ra zA*IUNl-6gL8dQj7yOXuHXTZ|oV@IoTmh5buXVsImR)N#0H3@0W&|;=5QEL;nRl*KV zf2(1U;cQ)Ix)SXvH`Az9zr3_&qff5#^ir|n#4s@;da#kC5~Ew1O(QtXE1F1Nv` zZ*?=zkyf7=5KL!Rwf1?st9g-CPqG^~Nr_gzfs~kejlF)h<`G(eyq=_0LeLMBveVzjDI zBnt|`5U!VbnKGG;C+WOUT4@F1^Acnl7;}{`!{N9{FRv>2_dKsa5QiU&Llo6ZeMt zW$qcPuN-W|i^}N_L}=8_$_vf;$JyBox`#3+`KPSeoDu4KY87}@u|ntan%5pJo4O%9tZ8G})T{8@=6u4^EA^rZs?mX88_%t{227~?c{8Lg8>fm0f%6+5KzW4Nrf6(&^?PZAZ;?|bV zk4W^mMqS6B(s^|i9z0rp7Nm@R_xmPnGl8@Hg6od^^Ho7V!qM@AN7rSoO7eLb!@-3$ z3z#CU{NdrDqdZdA{_JM9NL10|!@~ndQLztH!K2~uy4?K03&^W80g*hyi>^ZZ6P^LfNr9 zSy#Cnri_ggvb^r$gW){6^f#Nov^UUPaUkFZRD|WIAKncMlmp*^Ji~DO=?X^DCq5+1 za#BH_=lC=Srecv7p?i`bC+PPt^ZaswxW7vJ$HSq&84wu#f6;nt6TINorVe&(>cDQQ zfU{O_>2TMU4(*n1k_ECcdSgesHg;q;c3B_~%-Ukxpkpm!ZCCWO7#*&puzyz>{lm~; zTrVj%tP3Is1@IkhROvNP*S;Xb;cRuD&VY0NBTb6#;3yadgM(n?stK6HnuS556Ri3s zY#hqh4B_sefA0XY*de2aWs<1A^Ku0O=P%-U0z7;lc*5}))oKUn;Lac&d=W?oyMuIi zXOIrR2&BW^K{~oKNJn1;l2OZbV$^G{M+bxBADfSj(cv*s|AV$d2M^g1arm&ct)n4p z>v-5w==g}W^`ITD2M<|W58Dbo8nLz>wZrxJh_%&re|(OHkLu@V<8-!8#Iv(jIwVsELv3~y^o5p(B-1Cv~W%l*ZfaBqVJB;=F_t-SnqvoC;7_)5l z)rdo`f6;x1J_fV2K(=MJNSDCT+)aKww%K=~L1QAf$+4%|RZ^stB^!icd;z@9np8_i z$3IHR;73ur%9l(=4ncNE=QhN=fxz9YP-y=7&(PfGewqG(`aa$=+RrrU6?qW`jf>?g zt0Mo9umZv1^0V(8tw=25dAcfb!W!UTOC=i@f6a$#2DH(SCFoe1_eCU^86j$FR;!-@ z&9FY(a*(7VY-?-WeAXR2M%q4Z%BVrX)R8(E{gF0QO!+cM`i-FUB`kvRYQP|oeHkxd zvGGoc!+A%6rbz|zFPCb*5X$c%xHL_^ftC%dFChhjJ#NZqY~4V&hUOXiyV_SdPP#98 ze_szSd*97{w)9n3 z3<}NTzd|(_&>0U0d_zl?4%F{#-IV`*VRQwG< z9lW1i_DPjVn#{=ar15YzTdd|u!)3rzDhcsC{YW6$DCu=(c#TAKD)LTG8?TwVj4}ot z;QKJ)tCAnS1i-`ZG!qEG9p>TT_nL=A zJo~T{1dqShL?8rrn2E=CnF)_|(s`X{xz`HLHI(2WVd3v|ihLbB-|JBMa;U!38S|9@ zeXk?u%fb3?C(xIJ^t}$GFNf%Roljp2(f2y8z8tRab9#Lpe>`7(nC<*ZmI)rz0LZQY z3t8KTjsqz88UK`VR`$!JNH5e&DC+$h+Pgx%ew1Dqcc+`qU(41$uCAbZNuEuZm)H-6_!RI>h&{4KfU7SryQ zm=RRCb{Iz4Ki4kQgG@{oAR(4%X*J6)dXBUwD((f*-l{Y~#nGaKVT7a60X}cEX!C;N z)Dze^=>A}Mbd+3=>5fUfsKU1Z|AIu9stHFN&S9?g-KX8pRh`Tki*CN&eu*WxPLNSp(%LBM?c# zdBhib6A{fYgN|?MN~8|+`kS~7sSW(wy5$hf<7W7B2N$*|OcA zd^Y?opN9{yop0;rVj!J(e4!7Ie}Qz}aR<*GcfI~o8iFUecXCU|3y*ekM#l>u?Bs&( zi||#vy+A)3-vnLhwTk7QW}8#jf%IC_>a=Dcp1wF-)eJ7u>dp>oP{TTqu4NiL%M6&0 zn!Eb08R#Bif`M}IB7PH4;sfBc{Jw0o9H-+Ny_t1`e;Nabb<;Fxf55HX-e$I<7Pms& zlMXsEf9jTs*6~Lw``^zoOdoIq)W3Uk3N;jxgtMxCK~_ zKmI=}bmTv{hO^=-e_Ht))qA1Fltu4&WH28?Vc*^!jK#Q~RAWDjyrI|2WIVtP18%(u z@@Rj)9tB66+t=)$-+t>(!ry-LC%5k);c#<%lHdO4f8KUaf4X{Io?K7;-$#?%)A-r+ z);s-&H@)xO{`vGDf1cj|^ZsQJNBh6ef1gkGgA{+Q4>$Wke@VZN;MaAue>&_xp58zG zbKL*q_tkLtY}ki?j$ginzYmA_aqb-+gHoox!?{&_9yk8|H`}IGQc_x0pzgiXi z=gT5xKi;A1e`@?W4Q9~P|A@0yTu_PRyx>1y$HnXl{(ZJA(gl5E>i<|}3H`IcuV<@E zU~sU(WC^%^j^?-C8#un%ub*L4^Td;6#(qwNtElSEffSJYHh7%)_C0r?Q2{xAQB@e z^tuINe_77Qg0Gvpw=m*VuREohJ5;r6{q~y+FhP{C>`OTCeoNv4CRGr-10O58t6Ft7 z!Bu0apYj#DtN(FyaC-|3NpN-5GD&7(X!oaW?Lv{Cwl@gHuA5gd%~m{a(e-uP3W{XbNys=FaePbNEZd;Wxqy%h0pF-*F1$)OJ@2e_|N||#=u;RQLFFM z>*Nn$E8kpPU<_EnBy_8%!-?1TLa*ofJvi2f!}~O0z^UbY?tokDR{s8jq2JTKe|#_u ze~vKN#3fSA(>O3klp*zqstpqA@H9(4b>1eIFFr53@t}kO&XV3W|KU4pF5{w1US$<< zDX>D&%xhO4jM=FABPamdvsQw!P}n;XTVSMmT@NFay0`fd5CPbc8%T|y7|hLsN0XvI z3Jbr7Be`;ca0P>)ab7zCq>V!Pr7NGIe-z~tfjKQ}MF<=$@Gd+B7G>Yjhs4bY*N@7w z!fW6ywv^|^p?um}5{u1o2=gf&HK*733u!R?O1hp!tv!FrD72l688N2nodt;s#%{2c z;{u)SiES{UD7x3t9xRDUA5Ae5Ezcobfo*WQfBJjx_x=?5yl{el_g%OFDABEYf1c+L zuJJW>cfZT3-C_-Y|1t##p!Eb0=fUuP0e$&BiJHaf!4$DG$Oe_My&~960AGP1Ct*JU zm^S0f!F(RUp8$WbpL6^;If0)i@Z85w8|i=s}wuf0*{p$w>zhssl#?Q1Y_$f)bYnw?XK=-apy@?KdyD zCZkiHhLr-U%>-><^~98c;JNX`82w-JD%jh^^O~kN*o(~(l@iOwDI+@(jPc$* z44AeTrF10RyEi%jeCx5V3HwKU_C^PxsyV{4e~~N;Xv|9wS;W6)n#UN#;E3^bZ;YbEHG+=v!%fxZVUFaC*r_nk93E<4g~8?;iVikbI^nSQGOzT7{lKC`DFSTlem< ze}Cb`nNQeHJ-QuVpcNoiHlV!{tu`!Vlor8>{i{?xaL@tONf-fow^|YuZc2cfHi}H#0i4WMF3RExN^}O-xQ|2l(`QBXD<9>lquqWACcUH-P{t>>VLc zRlWp*PiZ#KKVewe^4BU|MArB1?Rpbfe`$l07cZav`nUIIub#j8Ir6UOX?ULJA21xC zS1bGG<;$~o@1MPY@h&>`@{0>E@Mep=r1-&Fg2cetviMKVQB7>FoKl zw6hQ60QoM$yb zsBmU(CbkXa=Qn3Ry?Xcl&D-B_fBr>p7RYSM>Mi~B>g0tn71qMkrrKDkdV8_(@l0+K zh-@&*Yy z*GBrD4A2!h9HRe5Cc33+;zd@a)eV5=*ZY~@z$Q&+Jj~~>Y;lb~gOo?|e+>#d<}bRX zELk+Yd-t@DK^0%(q@i2zNfv22Vb`jo2b|Tikb#haNm(FN9ybz)5pZ7eLvmAgQy;m% z7co#G-N!eEw2V^t16%~2-T+Hx#K|saq@y1F*Q4`# zIPA47Jn{9CbnJ)TL4TfJrWIi9CcHb0#+>j6V+l|PIj!iZeup!$WqR7DMmsdWYI@p- ze}|zfl+cIXau_!ZeLGfYr?qS7U%cLTTx+>WJO}v^x1_Z;S!C-pf0Al1!Hs}OzYl^= zpybV`$;A7y?8KM37Xo4L{0MSx5w-O+0BT&76eqTdhU4lrEaYKtBOos6#2Wz-fLguLG)##iu#cWsqTse4VT+qPw)iPYK6um|{wlZ4 zAsbnA`d|nXPrSJof8cMnx?XT_1e*^Q-U}WE9=oKnn?pt*c^((@_rQfaPaoi>;*W8< zh|d=ZGI|xOGx7K(x)*NcvE+Scg=a~eW+Z> zG>eNH;JdOC_%*#(g37 zFiJ3A|7_YzdKtCKQVguat6b&R2{e*ofbPnuiaDYXe=`0C;3;{Jo0A1eBD!Y`_&JM+ zW0ap4@h8-h>E$>hZ;Y4@m_A|PIq-K$C1=rK?)5rFd_&Q8lh3>)lbG}(lmhLYixMc# z=<6yaNwv0^psy^|Vbu*O%H9vK_wi>s2Q1%{twB9}mX>8OV(2E6iM+`yN2^BzXk02F ziG4Wqe+yA;S>&J7YoL#qbE6 zaIzq*g8-ZwLdJSVaBXVaH(MT6czp>W2*@VY62v&4%~o&%&uRlCf*H!6^6=%=01J== zT9sx6I=lYoDlSCb8uZgBhiMW5_#2+>3I>mhDQCOP*TJo z7=--r#lnd^bV@H|WCt%0`EMXI39c>sf4I&+)Ta3(`s+v~mE-iw!uv1a88!_}pc|F( zc4RN{n;1cDr(j6~uW{S?Qu4Ag*kz58j0fsjesP8eDWDzFx&TQnr*yQ7IA@4bi1+{f z-{(YuZ*Sq}OZw^Syax7UK%fANpp;qcTUzJJ!BZfNz(z;0&WzxaICby>df88(@ zHdP=Ukh`ejRwb_+a020PYKnQSwh9ag(-y<}&h^-8?2d{;s$pTia0oT9<0FVmZMGlQ38j22J1!pM8%WNLxum|T zD2O*Kgb>XAhV1exy{Ni=*ViNJZS^fwfV5ED^of<%gJIE0f(JT~WR>ZUe^r%p;LI=C zLy)G5b`h-3Wbv(is$#MxifeGt&)X}?Qd&KfkBXKWT0T}t5T_iW!RAS_?B@ku?eyb^ z!=XMK_0q7~>19r#!p5S*t))k-Gz|R1_QD(lkcX12auizvPt}hPbO5%L)w|zP^hj^k zz1-ms7xcecF`&V`z>xkoe{RAzquTycWa?a_`}0P{BPWb7vT?&&+)%;3o3*^ z%+K*ZF(D0uYy#1!qed*JM9bF(?98u?@O*~@vpq4rllu6d?dy_~rzX zM#nf4Huk(Ys1B@U2!Db5&WC;(6I; z1xV?t1K{K^a;Xfu3S(xvGRzCubGDmTAPE`)UXe}n#X}Bu5$AKUq~hWSd8}isDU6o? zq9J$`?e(;R!weO+f17~aKh0|~Ue9nnPU0#qqa;u%G=eh1fVpZOozZtGbnM&Y5}ysm z{Q3}T=@S{+hpe*}rbAy?S({ihMlU`Q!E)3(F&tQ2!wFS96Muw65RB=gTpt=0If>5?tG71aO{~dM^Wqb%W`_-EquSc3wa~oJ4Pruv^xnOX zc{=Y5nI5W(m!(k^#O!So-4<(rhxVN??lG9~d-uo;k6U%du(-(-BskQ^b?`TXD8@Z; zLaANsf3pFsf3YbZ8YW5VS2ywDx40>SN|!%&eLqMywMPHnRZ`qE(9-@tG4sC}MFnCr zf0HeU__Y(pZ%F;VFU1qJVIOMSsBPPBCruWyaS#^(MYOD-(rM*aP?e3p>DKMbblm$7 zG~#*%PjAY^Y94iD4jVv3Ao&kbvMcNOsIL)wr3_EKGd?u2X8hClrhyYoM0maXRJpdPeXTa&uD8adp!$h`q6 z+LyWaI0aWb&&Vy3f38wQ(RI_-V}?lBv$^j!flKQ}zi*=K2O9P8bHYMLN8Jy#;1e*J~D+fFG62t>o2z#X|ut2}Kko7U44E%ew8z9Gz~VA=?i5 zV7Co3t!*@2li>DbkT7Y1G}4hrHv{~xBaIU4m6z8+&3}4(`;lyifWew%7>FvK0fTs< zrzPdg3rA3v>trXu`|qtp37E$$O8qd4X0lSMjG90I$Ugz5ptsrJ?M4#+4FQebW0UR_E0mO|LG}q<{s_SQ-uozC2HN zJE0+9ita9uX?UH_scI=NA2PahB1XNtXO;o_wO29qJ{;*$vvA1B)cMi zAqg)Dl8@+-2?ZyNW9V%{fNZ%)t1iCetz8ajGf|0hA)q73u=tLufPY3)F(D};_YL6k z=?%y+Q+&T=P76M>47)UEQ3`nkOH{!03vm1(*#rV&twV8Dve8Byn}(xBdfoL!l@7hk zTy`A?4Nep^gM-SWHEglraQF!NKrJzUqZ|QCd<=;|^!VrKukeXfgnb?i;ZraE@Ff|* zE#UN?N9#vMdyjcTqeEySBaYyx?dEuuvmM4;WOzXb8XV19UghnkExk;uz@#h=8Gg$I;LW zVtf(Ga1Y;Nf_&w1gu};{(f(=N|6|yHe1EzhTt)l8{@&XUX3_pXh)nJWAK@nf**%X7 zf3hE3Kmv?4UM7FqH#Ei1Mk8c-OE>2VIPn4zH#LbG#yeYCpw)*&MBk4BbUTkEf_{I7_dW6dU+s0@eSDcRkrFlLDhr+S$~1iqg9tP=v$J2u;Tc_*bH|AebTiZwCE^}_Z#b6k)PO%llek&cFg(o$L59O@ zqLf6aMI#&KA4=Ijb9Iy`<40%^42L_{!rIwL-0ium&;1A2jMqoibw8c4REMlKAe*X* zCpwzPv!?yA^qJKW)(-A}<#)CvG>J`^s+zA<^B-!BYAFEPpv;TPc?jd{W#dQ%6shbq zok2+_!!VolN8yN+Lk92`?%{droYFdo{OF!W%~`j%r{bQ(mCZ2Z6grj*A#OWr`{z!S zh4q=1&p0g;8iN*6)Rb52*CfIB?$zvUjFolHC1boD^D+&%ARiGf=t|CnK=W%24lp42bs1Sjk9c^%S*Z^@m2f2c z2lM6S+)FlpHUTGzz&N-5SlEm*onm_N3T3ES0{yWq{IbZe#lhNK z8(;)~9DPP`s?J?^6@<%tUW{7{M+4d&#zHbDYt(!o&4zzC5%Gf65>i%6`CY}B0Dnb_ zMLppJWa0*miv}vFr|$&-3;`nWiG2kf2Z%MPoN0hUq4C0jl{eY!`64CX6D<6tmP-i$ z$ca82RSe(3sY1VM<2fVKdD`c1lW_kZ^tm5@XoIfpBaVF&&Sk@V&}i`J*wbz|_+<&d z>L97pF<)*QinmR;!+X$h5d=wMLp3n!Z89K$yhs}{4pyRHyNDYXY)``rH;BI|5W4b2 zqYQ?XW@5{LMg)H2Y}fi4x%#3;97qtlUNtGbw_^8zjn+@NYNr#9?nJ5@6BjT>`PDUl zy$!4qlVf&Gln(u`%YPy4>`?;2A&vsI(Yo%LCJ|W{s`Esc)wNS+tieq{D`ir22ZHhQ zOgmyo25qKp_VEv+*+Xm><_wAtKoP@vjY$S|2FY^q6vpm17z}O(SA($5X}GUZo9c^I zXqXen(Op{;!SzgB>lwB;UrVlU(Q}o5Y4H{HoUVQOsJm4exl+LRw3}f)47SiBHqf6>#onxTU>Lwg+bP(3obZy*#nNxiX z9k59nuxEmPNAJ(hqVBdgVfSf6rXqx<9|`4DmmY>HNI$A9>Qbie9MQmEYq#ct5^)hkg=2#G8{-|V5?ipqp9Y%|P??kA!%Bbom zK^~=j8R~&nv;1dKb<6u@FTbAyVb1`8`QiZ0BVP;9=DmCMv;h>IxRaS`rJm=9MAJ#nF^T36Fw8jP#Uu#ZvqBl zvS^Nw=%LQhOw~{vvh#X>B^~jDxTU&Y(3pGNNm)<^IT^+W>EQLVzn{H*cJk`ichTVx zeQjEEgu){tK-n-sGJg|barV(rmdD+qf+XG`fBa$*FY){r52W>qvd}vWR5qa(&ajkS zzwgKnRNAjgNC|(L(Ou0a5 zsV&P`b@i>fQ{TK)&(So`Vy&m>oF!hoVJBq6@|uNMMSiB9B(wPDKN4V5nyb5tvPRg6 z*fp+WB#C>Sph+4>{QE6=LDEo@?UVa$nL!qf1|#M(|2oaOijGyi#5cB)-#dn#4W;{o zgZtfV-^hbat){wvzFPYw?CR`(chsj2mVj=LhCx5~%}tCx%Fy0fo8UIQl^dTE5ObaS z&1=u5#I-J*Rd=p99Ib^y6t-l8!~4zM;H9Y6QH@>=13B+VQTf2rl6Qjsg&B0U?aNzy zG8m48s70vsG*T@RSn=n#lwq!<=;$rbc{l)`Rj)-kcRx=pBgd?KO~=m|gN=7B=zs># z-sWZI;DueMC6j&(3qHBnY#Ml=i@O&h7a zX_5BX__{IkV01D^`%NCK1Q#O`?d7E#$f~PTZL}yV;!n2C3h{wsoAKq_#Mtctr{^54 zy3KgCe}CW$N`c7KZz1*j2ZGq&G?!$Q9f~@bY6dS8Y2ORMHS&~5>-Ex}xBMJ1=SR!W zzSrDLP=P%Mn0N70xCV5nQv4paVOnA zw^`IaqvJN=!HLbBwc=<;Y$Cti;>esD$eg*5R)6P)RZYE~+5&0=_Cz$Bn!nkldDLXIPqq}*I3F(b>a^2SZKr?JNee6@<%Y3YNNVcMZ`nrq zGU~R?MZcML`1=RA#acEgu47a4w2)ubx~39wwy>qTt}bb=zX?`L$ByQQzdW)8f*3~! z-+zG8;?niv(XBzy99{c{i*1LpK`C6>>u5tI8b}MruvPmyHQrF0%$8xq!LKL>2gjSh z#l$)bKVtW#w3Qldk6C_WHXG3H2FX&7+H#%izMEEkUge9q<3?ECDr+~hqNuvY&hj+- z&}?bl0G}|DVO(l*C)*g-WMWyw5Hm`bMSoUt(-Lm8?TR+fUS!O(0W+(wU(>>I#BaIm z66}Un<-@nz)Os}HMiOcRkjpE4#XNC99ohTHNA1#^Pp~o2yrt_8P}>{2YFgclH96QU zIhK#p6BL%{p@Fwbf8}(+(_CY)0(|tV^hB})t&cl}kzv{wl9#YV1aTTeLzfQkthjJ?>trz0I-POoD9Mc2x{VpU?V@jS5eS_(X=X`Ko zG(zYw{F{UMV9RW9ucmJeU7O z$8Blp^sO^^XJ{+^PC`sF_t&DX#tj|`2lbq$ipvjufv7JnJ$$-JdCqx@>=DE+HOW^K;LzNrzWkLdP?>(ev>$frTk zZGVevmS2#&W$V=7?7_iyX?z-GD<$`7F(+p zdxwEuNi#GTSBR!fkI^HVi|Glr?XdI!A|RAbspy;ApRy}gQ6$hyuz%AfQU}dgqe1p` zavFkeJMj%bs+@ozeN7z~rMv}Sgka2S99}~w_;A9qV#aPc3zrSh4JNNrDk~u|C}RgC zf^V*$8^#@rvC_sQku#Xa+8iy-0i!XFHtUXRO&8*Zz=!SVI9|8D($-$S#xRyPX)_I` z{r4*SaKmiZ4bLEk^?xYe70QK1gNYy-hgyYgzjuwZ-7*wGm7>;}Wq0l^w8h>E+5^CO z^ond4qR;sAW)EL-+m-~}zcoGsRCZGCIW^6tGzt+kJs1gE1`=#vz-jxUxA}x= z@d&b;myChcf+Bj&OU-LcjCHrxf)!zYD~xhAFDIfX89=sE{eM<`saLIyMY(vXJ=8Ns z7U}lJHok20YT{n!9h{Z~JFfaRqdei-eMhrAQCv67BhM(yK1F>XT|(q}ZezVPL-+fA zX0yklL@?z;mVYYZ<#@I#%Df12|A-k%g=sm3+QRM zxfPKcbowJ~=`}AH)8j^#yU59X@%43{kq2IXOe5n~cEho>35=KhD@bFGfSDs(l6{y; zLzojXE4HaGX9}&8CCnix@t_E8#q9;ze6&x!FANHID}0@1{kt@N4g5fwxmSke1zh{- zk1^CtlWxo=9NQuj$J-q)KX>|_?zR~anCC;`oo375?;QBd8-K8B9Xx0@cqCg=E&i|Eh(<=wF0Dr+kFdb;~?#n*}}riW02rd!0jps%>t43n{vp zFgKwN0gXX8JXn4fHq|>V(ALE{gr@>wIR7RppeGRh;eUrFQK5tV(Q)Vgj+A9A z_cXp``08f1O&p;Gn>q|K;E!uK*3V%;aHKze_+#f0Oj2WpMwezRTrv`-7GhoBowc@) zhd;t5b9Blx6z=^kK2H`7YUyO4EY5>*72_p{kN`bfQRkE`uoL2Z@ii$iswOjBT*nK8 zq2x&|%fQ;)N`G}NHrA}}3wBhTL1K30#B2w^wT-?6(k-Bhj{I$iq1KcyOQEqN+tP>@B=d&Y;mn6`#`;7d{Zs75{g7TrV1I zaM-N52(#ZdF$ehGF6=r+jlO`QAm)=V4sq>BZcbk=@|c@_enA(kd3>*;g`s}5e|W$0 z$MKH`ct`c)!GFDbaj(bC|H#D+wSMX~swBK=6l+#fEDk1BD%q`+3wOi1ldjlu`5N~k zpLPezgh}9EeiX<6N@F% z80l%%@w+!V)0TQ_Q+q>CTrba=JE1!K^_@Bzn-8IrXsG*V;EGm0Hpo2+rG2LpV02R) z9cnl7YVO#5f@Apgn-=z%oXw3V-t_mm520iip9ZaWgpSINyFx>8F362Yu`aQnjM}cn zw!1Od34dH!bthmIB-*93r3yE9()YYKsqfk8?okVw*Ck!yu-5emNY$xCXK?TcgyPkC zI_sY&f245K4vvChFgU=={IP5ZdBSk5>M>;_^IZFUEtx z=*SO-9XPH!$Mw7e%sYg1%) zJQ_B1eZWD+gM62+AJn>j(9pc|4LpML86z&Xex(Z%7AkF*YfoWcI_ZXg`gk&k*m ze1GH`C4ul5aqI!d|2haGLK0ethYlDtvBY&1ho>oUy$b}V0A`ZPmOEa|iOP5Om?2ag zLWGm^6L}+V>aV33?xI(|qt-!tts{4>!}eMa+_jF{Ydvz;I&QBuqR!-6H93fIt|8}? zc3U%6c*m<$kU3|I)jZjPEqt-%=wQY3Re#lIdMiz-!qb@<1pmh$?|Mz3?(>deXC&9+ z!fpts;$!$!ss>2f`8IUta(MeHeY%iJLMfP6OPC!Dxi;q4q#V$pb>jdVd<^UNM<2a* z^~(g=b$;h?T{;puXtj)5f^~`gN1w~VLYEW{9wyhF!7=>ZeyeO1U>M`y?b`UXh<{UH zsHX#yxkaOK=`u3qNZ6cvKL4TtgbU75Gymfg+r%@D_7H1dzjS$0{v^OT- zYd#iYOH_Mh)Aq8)2>etN@4U@cseg~ZZpDr#TNw0#qp1O#d=lD2hm1FD5#^o4In9=< zO7jr28bB<>q*(A%Ct6wlD3( zWgOlwH(n31(~moF8E?sMxIHC@UU;oJZp_%SEON301ys{FNK17dCi9LtcXfyeBdkxy zO?s^&oZiRAwYimJjX)LbCvl072`reo>d=CB$7SV}<-+P3{_%gaF@G~$7K2q$+4a!q z!a4(xc~@j|9Hbo^VB@YBTYvA&cJMXA~AlS0G^g@l)8AsHCL5`bqTG(i2S%2!b-z-<>S4lkI zN}1W^_%4*dSUZliKnLmkgc5gI`PSMD7g`)o{?_e3JpR$|Fms#1!(*oxN=GS%YXbMAN!?KZ`EaxeoV{sJY67XEqtKvM9b-KGcDhijj>np41-hi z6VczeNaE)5qJ+b6N!p5!tt$vAstlV?L;K|B7NJbdvBMg&ZxbI~rwP69BhtR!c770fA#LK_u{6T_rH>6Dusg!6CmZPnZECx?y`v7GcqzVGBOs_@7Y_Iq?|*FB1#>I^ncQ*_1+v& z00u#HQua*#Y=3=LUrl|59@U_wQ1lvS@|>s2kJsi%l5F)QyQ%3nr6y9FK)^ORB~bj5 z)uIhnMUt>%fhP1P_C&;D6#9rv!Id%FRvw)zQ}*F{p??N0^|0JNS*FOVd=m4Q7>4)~ z8z_!^MJR6qqHLgO7_Ps`>jwkX0bA@Yv3-?_ytdOQnBO&m!^Ps=+FIG*Ak@AF*k(qR zNY7osFzVD7>rjC)g^%uC()MpH5|aOTw9KiHr}3V_d7%dKhipuRS*WuLTKK= z<31wkr0Ct6qnz9;fLFJ&byF-{!DRxHTdO_ic_Uh1&a@8K?f9jhb~G|4c^I zo`{xqAT<)%XVM|2Nej^^iHL>c)%YxslMA9+3x5q>;%vlzeNI0copY2s$8VIeD-NQ; zTzN;J+G01}$P&7;l)Phv^|#s85M?_@<@4+^Us2F?uW{kFlyOknuY1jDS5E)9&Z`@V ztHQq9!rxGX&LE9iqQJ2XU~A9oL^?b%%MJ;4o0b4^Vk>H&J@lC#pR|U6^8?6EM~_h8p{Ys-f)>lB-Vb4lPKCt{ZW{syCAhNN`b@t z&>+5jzoLA%F)D>hkuMW#2l>8_+E;NZ!@f6$BQTM#{Oqh{$X}~EJM~$&#$w$vZXUQ3 zL&}Jmq+-vFxFpfd&Vnki(*3|ptur0<0e{!klz&R4dhNeF^84ztv3Nw6$s%TH38?CqJNnH z<&hIl97d^)zKT8LLO2Xcvy?R|6}L9YF>-Bc_Hdk^O!I?-cr7~NdA8k$OczJcT2dq# zFT!;iSr^|3_5YhV1KMzXkVZX6CKPd8PO2(=*xM%_-)x{5Aa6Ev+K|%7NV>=hUxQT0 zOTJU5QQbhbd6X10YENIKqe&hZsDHZuMsYkTC6(K`H+hAR;*B_vuQg=t8<7Q8so5Q= zM;m2Pu%6}az|o>WCR#(LaO-WipKyn^5a3&T-Nf5m=)jN&OSKy-S4Cb71p||Iob&AV z&fy%@MzFSk>DQ-2kAZ|Df2DgxRq{qcWZf{_0h(R3i+B_?dOu<+Ilh$O0leY zJiZxZ>ostX^Bojp0(I_*Txv;|#n*f>ov(_kNrmx+SY3AHnDCLbYy||01SK%~97Q0< z$ESGzJvnJLf-|FcMZjT^et+d0$c5`=`B2UywbnWW9BTz}D8cLbRSDRKcRLzlun1~U zv5iuzD<*4bu$=5g9C+aEg*fPRqTG?4m5ZCuHGpO)Y8%%9Y*(ZjH(~NB$*FqCYl(X} z(=>{@T)%Re^(GmQMw&@E47Iry-r_MtyYL&2l?9t2htan(5z~_$V1GmINZZRv_mH2V z-Hho-`roiZZLZ=%SQmRsY53&Z-dAkLwBJhPwD=-Q0ItweJ&{+&7H8#x$wZBfFttN8 z5;8qQ3a3(SIUH{lv>hcaH$l@(QIT@Rovl_tpce1pSk0@oz17U9%vLeOxg>T`l-Kp& z+#cNR3Pri{;ff>;!hc$}O^Rm;jRlZ*x zvu1ul+xQ)Rl8Q`UMUp+m=W44|Dy{BxQsO5GPTNX*1x(Ab{QCN3d{0Uh*gzb}v~>c$ zwP5S+Ao!sp$-VJ3SJs5+&^Qmta-i~cm9rV`Ku(W!C!@p!F@LnMK{bh++uR0CA3YHB zukPl6KF;wIk7LP5H|Sh62-Fr+W;r3-SW?|!{q;5UWD~fB6VO~Ic4;o7<>Rdi@%qgtEyk zQvtgo$q<5M$eT@o?4^5qfh7n@ezXK4(2R`C6sBbsCbG9j(UBHFOmoBiIq&$_NC=;48hlMZd_b^a8 zy#g`6e@fFFjT@c=yLWf@SLDVs)aAbWxeqw+AU^o5vvGZ*gS3jRU zdbq!@zMeiFn|b5$*vuS1d@TL?_Z!*+`4HCzGxzrk=E~%OBrwM@(uE8cf zL~;NZ;eVEL3H%%MheMt>)k^%#Rt@}8j*nZGZY2B~^HDuqR^{b$F_*rcmhcpAE;7uA z>&)E zfSqn(9hGF%us_#L$FVC0%}t6NdVFIcQ`=hlH)*3KeLcFR%N!E&jvMu=JTKM*R2{S~ z(yXde2QJFKHM%zTqXD3L4=aVqdoxNDpQ$UddENHis&2l!f?<3qtHq0aURJmUj@1fV z?|&%;@E@+eZfxHpx1t0KTz*pdY>^~$NhL&zN#BKX;@R9S!c1d_j#t6?;W>dV-p zsq`hHW?xtS18g}6MK zH8Kek!(|Ym3S$b(*hs!nUzA_olo@L0k)}sb)+XeE&ca60H4(N`)B??^6%qwMdoOA{ zeZb#Osae86c}q-l?&0@pS~YUW zeNemNSn{KjX{mx)NC`0R*|Tl|MFzZ#AM(1quI71rAU0T-dHzvjRBOSywTyUiSxh4F z3!D4M2-kPpPkQD^xE zD|A>CdT>!eUeI~`>vhq*tX;LpEH8B%Ce0L>iy@Ou#wba3;BIw+Hxzwsm?@UfT7*8vL2x->8SGj@T=IM> z0XV!^bC9yFi9e0N2`ML&h=28xtsKJc=m4vHNVVv?avs?6q-RJrFY`r_4d@xymgiH_ zTNJIdZaphDp`r0dRokSGl&NKo%j;FU`Xejxy!~I&chhaB+8VVieKpzoaF_p(bt!yY z$Ll1&2MeY!jZoiPi1E#V_q)R5oysUxto!KaU;aaC)m;7q<#dillYc#FY{d`98EA+L zb+?ts(jttWIj8n{OpQAb)tIU)4=x|vc>Clp@OX3rWB_)1pQ0JZk`O$bw}^B7 zT-_=c3wgsdm#{DE;#-TE5q|6YUk1M7RhoZw?E<=RvF-vs{d=v}D(mmBh15xu-fYL$bHi3n;%74p58uJF{L=H~8WiVEg z{pj^&%ruH*3iDH&#sj!2l_<40w@i-Olej9>b_ff+@&R~u1WM8vYziN27mq?kQ>|y< zafsgi~^np{WIm-HSEUaS_)G)`B zQWlH!=sO*^GJhnN0(}&5XfIj-EbMr}L0EJ~5kAeolDK5?ri3g1ujsDc25(E{TX`Lt zj5izCBd1VNvlqYbAOpGC6dqV*k=o27zT}nMO&(m3g$=x{iDL|Q^s8b8*91fuAsbw( z1d|7p2ZE&C_-+%V(o%4bPez(_+b{fWT*9e@PilSQSbu7391IXnS7W0vjlUV7f|jK+ zw*x`rjI>X73%fhH5jj*lmK`gsU#~?jMij35b_Eq8F2Gp0-3yA_dPERz8+5syj!K-t z)0Ka}7w;J@Qdw@AMZQUs_Sx^OsU3b)zQ&IGR{L>07$4;EpYy5Rtf+6J_Kuxc!C$Z)H=fDHhUM11`1%9V5nFjhR31~FE1 zGkl6$vyftbx!(i2-k~-nJL~QO=AGWpas0Nx(z&#^IiQPkq(ksiux6V>o^1AngkdsNs>=_t`G>5$1LKPJfMK%2fA zeuNnhjBz@rz|QO1-@eQE6+`f+W@#EztjEyNIpx@lmD+vi4%o^MUL+KcXl^OFr(q!O zmKLuq?r|nC+1%&*`y5)VLG&c(C$Q`4NiQkzQO}_8$HmFC;0HZp0MHmO zN{s&MMRNsd>TA@x*I4qY@lwk@=O^;1PXq*Z`=0Q^N)U*Q!0UN_d>Do2n1f{Iq2F@i z?;#lZ0zB!K4GqJSH=UJkXW1;sP$Ak>A!umVr@JtD)Qo%bJ9=2O1#9Mjkbldax_Ct* zSEO!|I<9~#Sv_s!&20q?iO)DX(!Jx=2@3C&YUysg;$HAr64{ahw{WLE&p54HFzh&7 z;RHw@X|#qRvgMpLHMpD!OarTK9mlv1lYUDUPzmF5Vx8iI;100~aA2X$toKN=N}H#! z!aOt*G)Hz}41^*G6$_EQ{C@_e%889zTiNTpdtaL?oGaP9{ru2!7}?=P{QP9c1MOso z$HZC~g0@;lw6uS+<&owCI$vrROP?~kpi0>iPeUZ^lrtlNlME6Mt@=m| z2M>yreYq{}TjRSXEPsiVuUEMNWw`eh=j->*R`a2ItV7!j4~QX|%ZQZ4L#?#R#6v`9Mpq+7$@f6wvUB$E z&LRe7-u5wXP>TlUROcVblc9V+a2Cz@?%whYiPp_M-e?=ec7Hk+Sj~KhI=?gib38hk z$lK+#5Kq3{t0-X`qV0FfU?+=QX_ zy{yLE>5-aZs~2kuuReSv{Z6~7+G_xQhTT?wOG}#}0#3|B4Y+H{MtN)A92DGXh*pJy zo=7L)T}_)!m~A_la0$sx+7f}9t*p)- z*{$=gT~hUD%E#!lWBfk3HFm^CEQ?u)Q=DQ*L@sv+vDkaQR}LBZV`L(%NOZ>klZBaX zp1+sP0A`Qk&Q+$_z~Zj63Vjx{roKYaFU4vlM7DY_wGtq-fp4xNNN%((Dn5-PoHcdvy(yOdWOW9US6 zm|`TMs-Yo@@_~^ml3}{?@ptn;6hp~QsMBZP-+#)mZxj&DWDD%6+?yY;Pa~e4HG;Tj z`t3F+z_PFtE0&Qq6mzw|iEq37N7RF4N*D+gTKT|~BIj&s*-G(AkBXaqdr85Uin8&< zuQXtc(Od(&R~Mi2|3GFz#C&&0BE}}?+`~srw7yXbnnuMU6j@#ixXMa6f)*X0>H{I^ zNq=O5@!+DoAZR5$f$KP^o_t1PZw6>TrTacUmcOl-_lO5j41sG6>Mf*GNOCn8t5;qt z100^)W;$0rPt({X;U+CulU=3Jd{x#tw;P4+vADC)D7hMsvvo+i3E#o8p!^{R%QioC zAKyeWy_ahN?(vBd#uvQ;vf>;~u-<)Hzke4FQpt!pKQ2!8_ZxX7R1f&W|0;`hKk7yC zhTFp{?=;}n%DTL?$um8{q-_8uo)^3HWf^ZaiEvNxa&ex2{8C)C`)?Knw-gd6eow;< zW95TmefZ>utFJKFT#oW%PP^(el29uELqNR0>N{6>G!nXJOaNMk3+s2J4$4p=PhT}s z2Wx*+NG)_k-VHVOAHZaZSBc$P`=Tm%&8@}Iw>u--3SL2B?zt!q(F`ukk3alG3uBO$ zb}KlAjo~S2MdLO_Wk!=DTf-yl`^!I%B3Yj-pSm1-|0O#{)8Z5Mbl+rWBnwKT0Y>9Z z(ARfYKD!yBEFJJGTvWv@-Jz^>Grn7CHjXLXt!%NZvdcW2rxIEwdn{<~%SAyCD=C`J zhRgm1U&+jn?%N+Z;B=u!lg{iZe}iU7gDZ=VaOf(jmos(|QYrbF);~XK^N6iSh5|4{ z&StfwTtY28U+}^r<2{}ef!CXw&VQxD!!HnULace0N=qN(?8e7cUBEX`Q;di>&CxJO zRqiM*4jLEsfj3e|uZeU9kA%>6*>6%Mw~dgwjnK4ZD7}-TZsGoZM1;uCe-Tn@N6Kgr z^EA+wn5R^m)T)=(g8?eT!8J|zk4Dv@empJq_X}kyYIw+X8Aa2xYLQgQ8a?Dng~0x; zI!>!3vY78^(j<|2n~5gtBr>jP(PVFJVE?30hd{P5T=p95SKzrIVi6`8)lDN^OR<)g zQWRR`B4E($U$Q;bK}w^7e>VEK2(xGuO^O(sOVBGU=JO2&3(O*!87Gy|o#N^pB`gUj ztQYll;G|x3(4ZNhZi1MGcd*R?e812oI@lE2sx=Z-P33W^zsY!n7!eDXTe)<8hz=~I z-!1rE;Tp-EKL7L)Q}F%h4Cuk9DV3rOtJL3}5JR6tJRC5vg}uC1nD7r!UFmd-M|`TtCj7TOGIc^gB5qW zD7h8vjWH7O(i6Dp1t#atU$4Vw5kMh8%tMx@PBT1qT~iQ_KMgPcF*GfcNfE_>K%1>4 zPfcpom^?IjyfTS&e}FwC(&^G4>>05MS@!p^sl8dzCP>A&g|MSsE@35!k(0Ku0MZRjXzYF(5nz@?|C?&{;x8}r#>dbqlLc$+EzFxIXLl7+Wc4XF8PWp{m zAkNbBm%$NOm?PFe&{X*x?Vgl69Y=OlUXwFHTtPh&6Z}D|fBl3NepnC98;jbZ#`LKU zv$@Ri-0OTqm02ba)x6)Wl$r;*|lBkczq7fM2JkIe>e=l;E|= zhw@})pjz{cao9lJF6?`nwj&_3o`MR8NToNJRJ*%E#Csu-rmv!pwBn6f+QefOHO0a! z=_FDiW&a*qf7s+9$8jtfY7scPn3{CUTnHd}7ehE*`@pBp^zaVyNghu)vshDR zKG4~dT!H=XK_7N<&K?3SzABYPDI3E|)Khj=qYZh-TJtQXA)2wd59N#LoR!D!4l^js z89WfhTHOgg)DYL{a(V2d6iF>K3^#h=Nxd(9U^N+fG{veBdA`h>2tAvIETvvUexDjf|7%jS&hUOg1!#O; z0T`je@@np7W7o>j+Z>-N%>u9+Uc7vDfB4&*qtn;V-~IaW<&hX{l$bTWNMl8c&W)oe zCXLvp56pn_0u0qq$OeS?s=QMAegXt$j^vu15!hZXaN=~GRMQN^PIJn_iBiAj{@U*D zIGnpXzQ#b)+GKui+Ow+mb%XtQCbdN`^*!_Q+%PZnonj2DvIMS!&r=R6p({rjYzwnOu`o|(pi2@+BMLXu3?^3 z^tOIAAm7O38aa)k`$Q>HXOmRt`RVHG6b>^ii|Q>xB?@+-)YsrZ_N4a`NUi-uF98m; zx~wKwB`JMR|2ZJ85j6pEfKu5;oL`SeFd&gezm=9&cBP}93AH}D%P=+Af4N*xO-D0n zUi>b-4ly%-z3+fHi~B4dJG8bQu#~=YmnMXO__)D5TtA-R|f8ej7jfA;dxfOqBCT>K$s$5}^ZjmcrVkglEF5T^+(mvhL zRV&PA`&>?dJ%ZVEME}v=&?pLr@5TnbGKe=M>6=cAEF%s4C6wxp~>wvzv%yV1x=|ZlS%a`f8fRuK?WTP^gBF& zor+g5n)o5!>?B*n28M_Fvga3-$Fr9~vb}ifnSU#*TJmsVOXnj=%OV|AO2;S`&R=#l zpcM~C;TT2ipvnSKizqALZh;os@l!x)Xm91EP7P!8kf+eb*~Jo_0kK)^ID(}gj3ezL zG>q1^oztw1K3v+hf1-rlzFT6gZFTsW5f01J|^MlE5hG2px8l*?>Xd>DSv~E8#(>a z{soGpOr>{$WSx$trVp0m^~rQCVwx1Km}*gOm&m~!@R*nSf162$Qo8Eo?yj(jJPI=L z$_U?=EbQCsXJTAW%Xoi36W(S=Ng0P`girIg!5veGr}fRo1#l{(v5CbAtc}>+PO?aC zA=sp6VOIz_1yZA+_;!R&AQJi(=hhU`;s)gSbtlX(1kTD1r}7 z#`T5xKLnVA`u|$6bi3LD3K1ZG%B!rRdwC1dQzheMn_m{kr)luSk;`bYOYWHQL~J_^ z=nP15iWk*dW`sqw>^=#wCiXcYpYBLxK#x7jBT6eHf8p>UZSNT8;X%gJF2)o6^>EYg zF$x$*R}A8c@k-bYvW_4JttYR;Wo#ehc*AZ;1pnx%P~EG!7R3h?a0tCXr4`VGCLYBTC4R@w&v<}D1fKb|cLFKRvuW=c;mg=kcw zs3MPmf1LbaB{=N>uWOw%q{AXMz$cT9@f2Iw+W>RxZLRVM?$si*`+*Q(de+0U)pgZZ zvGEq}hNJRUS3*?_3fJg*;rgnFm3pxBk&-Kixi@@N4!j0#@npA^I@)!Ouxc&cZSKAK zS}UZxxWn<{F(c^0{(79ToOHXZgVJYNW#6mYe{Zr-)V(&c4k@hs8~8yNL!mVaNgwbR zrgh=FiAZx`AV2+4DF3?}Ca-Zt{o&}1*7>WN70UXBcLpf&T*L@sBPn*Jwvfu0G&lj2 zz<#8*XWrhN{C*lBp}tF}x{Jy0BNM-C-;-gxur=R_1O)b= zJTka0J=|cOw;8@dvV;zisWxAT;!YoFe+ct~2$kfOTtdBlIczP00(qCaJMjW$yfo!} z` z$Hn3#3J285kDDRnrp<=qsVYnf8%Ow9ProJcbGLwsHo6CK*v~ybh%KMf3|9QM*a0;Ke}j|tI2~0U%q@9et9%3tMdmBN2AdL zB8d_H8z)6tI@hbuMgHaI@+*?}Xw)0ADG2@xSNzC%UFy;EQcpfS_V~%MCyYqwfx4?568e*)=`f<14a)Y<5TbF^Jh=tf>l3zdQPuoK-oWI3uMcMp*Wk19wfgVKN&tA z_kM(b-i#mjepn5D=<)x?e~>g7Q~GH5)1%(_;XgS_XLA96&fRv|6wGXR)cfJhPs1mF zrN(>XN5k=t(8y}=c=+U}9{lrHXt+22Y4yY42dCu+Jn#oJi3je)Ti(tA1UtT&Be4D` zwXRlmum8HP(PbLks=X2z-FSfSt8vGJJAmdPzPa1}tK3KOuKM6OHSjvt!v~CMHfwe<&>=t7G;?YrRR!Ahe}| zf8go>HPt&Fwc~Y6WzxMp{c(3EzRdo8v#CZKY9L50sU2}#FPF=lr0R46LQDN9i}=k+ zhV&o-y>J|v>PR2cT;BCJ_tdB2WATqa6xvS%sgi{HQnX>|S~z|zKgIriG8%cL{T{kg zt@>B3CI2Y3e<+`z)q;HS-v3HEN>vJ6*-3ocPNWAYaH!xc+I)v?JeThBZv7L{JX=Vi zZ9p1rFuyNVcIAPwor3AE+#`!XPX*z81S&+zPY8K8&H1)k9M)vHiz2>&mpgytZI$7A zWW<9{%u;;#=%QT^j(2PS7Cl*M<*VH)8ArzVw-Qd1e=%GD7;*xc>{RP(Gvwv6-BzJm z|3EnHy48OE1k)ql3pSoc)~(P9{K?P>#NU!xAx=uV?n0k;o9~pr`k%9q`%lpd)^i;a zDo2H`^FZhDtp}EWl{dmtx?52*)ZpSX5DR`2m`ka6;mSm-Qj08gwdTfh+-dkSE#haI7v zVo~&`|1)U&`idOBnR&lme^Cx{FR6S4F!MXdx3=~WC8e2}>%;gCJ*2otg=1TaP^a!he zCOnn^w0zzWyr87*_C|8PuFtpcLvGaQ-+Lej-@A9i4l4UcqX#`rd=q}`dK36oTQ=u0 zf57JZiMQPztLc==X(>M;LaWoQb@_1xplo-_rC{D##KhWd-r>;Hx!v2EyQZxr4Xobc zPRtm2$O9PPfRMlo7PPfTRpdpEhkxMNWJC@t@~cqZ7&9-*)uOdF^kLA#)^V6SqP@_S zcu)gsW5pclx(VVnFbc*HAy@C>4tVyFe=H1?7IUkjZ(C6nB~NJFFy)fDDs-O8e<4$x z7hiT81WDId{AkC&x#A%@Rrz=N?ucN5>+%1TXQpvI$~&Qb5!9_xef1cDL*?gmqTmyAbKK|9?$}Jlnt40J7FAhu)*{G*^!ujaGW0o4powm;r9>ERGB1 zmhaLoUbFNgGD7rr*7k(s+<@Ur%RAmIy9V@*r7gsZS;V)EXwu9ggpI}%H_;fmf6HC8_9F)kU6}l|7`0CoJd`q2Y(q4}SC6HMsfQ`R z6=|sIN9x@qQmAW7-`A!(qP zHGNX(#t2sM{+_biL2>k zDR4{+!I$9$%PnBIhvKbKLBk!oh((2%oyetO!M2Q((2Ks(+kG0fy5<_cm_aRX6dyIj~e{UUxeRnr|Cu0W*n8#QUnTdLYM^)c*kue~NV6>t7Dw8ehNTp^n;*vWpx zg2qVsu-Z~w$_=4{5zAUii=ht616h--aFH9u=Rk^nMgcMMT&seDgw1h*(IpZ)TtPFi zwo-Lkaww33{%GqJe;7b2MBb7buIBIa3Z=XKQnb8|6S~v+C2Gjq$QX+xui&`I`oZmA z1x=MA=AgfEDh%i>&08V1GHsHKaIf=FdV}gvPo~{ths&a#C-r}kuAa`5MQSMDPBG%z zMY?wpCl|A}S=noZ^EnVpHOS}_R%K8nngmMi-R`Ei?m;G+e=5DE+?7A5r1gqAmGr^R zdsGG7UH+Qq3t)dGPSxg4c1s29%Hg0JY@_+b)6s0}0a8r*1so$=kK&|wHj?LFXL~WT za0)cxE7*86`o4L9r-`u$E)ShHaEK>)n&Te^r0^$cET%lgw3j{4f#09w9zF5kXY&BM zMxV-|ttI#8obJ5&Q_oiDyg=U{T?EJ_N z7oWjJo}qIN;i!AtTR+y|q7S^v_YOp;UZ|C;63^|M?!?!uDZZjNj`Av2^~X}#>LRNL zjIa0MR)^f%nY0rIGJM8~SnHp94^3fB67ExS3e5zbaidJ!#MntL$|N zXAV#w@@3*SEcMy~%36g|#WGviVXz%-uJW((#&4l(rCld|gpdqq9WO@%KN#7^?eKS~ zP5E2l8-v*ZE_{^h^?@+qzS>b8GH?E+;zgLFCWBAqZ$F~XH~eLL*D!MW+HqRa^9PQ; zHt;sce?4?MBjqApUPz5@+qAW~d+f>cqUTz703{`E48x5-4c#ro6;Qs75==v}Zr@5#rF2CUajQj?7-6oSzgD3Y6$aPLYq4OYZQL!8VD_;@L--dh ziy>nh=soG7wz=Ex=iBF_F?MCeW%>6pXD5ivFGp|Rh{3f*aq3yJ3bU7u=Xcb|R2~_P ze>BhYVol+{f5|@Q%9ZclC<(P=a)B#J2H>)6jz<|fvx{KPQqN96L4&p8a59Txk`=~d zk;_P43TFm4Z(_Z^;wjSM^0FurY*;F@S*{ zl9qE>xSTtgojX(1l6S2u&#o>6*uc`J8W(253J|>m|L=EnB22srL)9Zbz#C&=@z@_a zh`(=F4q_j+nO z0?mecaItPs&<`ZuI75|tdYkk)7@`D=KHZh9$tp0l$!dz;ZhUB&y>#nAA~)6sakY6pig=J>WuON1TCYFDk-kR(q_@!)uOw@Rv1;GSeu zF+Qlce4vbqGZS&67gAJ%x>%tdmqc0=*Ei)AN&)9Wa*w|YdaPQxgmz-XIX7u&ndBzT z+~AchsxQm3xj>Gu9Lk$Lx`?Y{Sr}%oFKFv@H{?mO^aWg0hU@YR+}p0~!J4d>R|o+f z3R?9_ag$w{0{}{we+U5=e>xAVa^N`Y=5SqH(xdMy45$f{k0(WcjxuV4FPy%CTMGgu z;?=U@H|wt)SwC>!_N!PcYT`S36(`;K_;@EC+|3u;mH#@@Y;8(tmW)Pm!t+$~?V?g2 z&3zPhrV+ZRCDuia_i^obpB1Ta;anEymSSYv)$JBS+>qKi73*{{{9kqVHs_q*Wy2dCO?%``u&{%Ay`W} zK#p!6oE;XH{Q8%ylF=ep#!d6&xDecsM9-xz;FQt6y)WUabwfBy^f3vJF!!xQ1B)i4uig#~;Oa^X1 zJ~>dOqa^x2)aqNVi z^90MF9*EU>ApcUE!;9u})tg0|&B^wgm~)ZoM_&NY)e?i`Cbt$TkTQ{r47gXJ8lnzk z^+I5uf4`3KyY(UZ+g_mCpPv0ENc%Cm!*CJ(cVwoq=Rii>?zH;UrBp7zw@xP6vI#vS zYm->EAsRfn`YNqujj#^l^%^t9Tg4=bf?hu!*TKjX+8~&MbI|fl(8E^K8}*>s-VgBK z;1T>&ou6f7t2s)Bk1^yGM$O{L&7u+g25h`Re{z%Rd6_>?VmWc;iesam*L%lv@v^i4 z_&2NkBr)AUMxYI+H{WR^K@6?U_dPp(QkorpYnF90w5Y^)|FZM*St0#LiJJ6da+(mn}^J?^>yrcF+Rwk=j(r zf6yUX5-+6}25@INHK9N;t>~>M5${~E3k3J9?m%Xj+HC;4U)?Qm+@Uc^Qw;Lg76o#{SMWUpw38}5FxpT zQi&u{b%)vOSMSEIbJ1BywrZ~}&VqYMe-w7pDp4axd-AV*v)@_41+7$tXg@h!{Gxs` z=Dp-+HvK6s@C~N|{LazSF#i5xy!z9XVsD(B7VDzo2{Viz10GPB9j{X?h~k9WaT~~>#>fFpT8jV`4F-Kl>sWf;Yr2(v*m-4}=F%3` zANwkyY=exOGu(XbR0ezZJp({IQZF>TG|&#FBtlb1685QC#_vYn|HXRh$iQ-im2Q)0 z5B=ol@^#+7=Zq2^!u>M4d5faXf7nK-)b4U-4sc-SInQMiyPc6UczUZ1we2*=TOlC$ zP!8b(_csd3+;7e1YnQp(3_w`Ilz!|&wAgIoO{e%!nm=gUvI_%AVCy6`{8vg78R)!9 zgJY5lqX|HqTu41@r|ddaA>l9LBukrTp;D$aUt27wrAI%VCJu`bicH>Tf91i&qye72 zLYO>Dt>x7<6-nOT-!rI`lI2*{k~*G z$3qF;N;_`xZxEdVM$hydX9 zEYqtsl1>YQh>20mnrEZg7`A%-z+To|Me4Hf&v@j$N@W~WcR|uD2}}o}-bZ6`ZLld$ z;JdpL(rQ6b^-{|Gw2&rhN-EVa5+zJk_d4EW{hEVCh&|CJOjTo~5WB;lR=tdnv1m zM+-Q720}qokzPsqUAP###W+4hDZ5RkGlg;$E`c2ny!xVZr|T*H!-Wf4RCd1f;cRI#=x}Bds;?E?g~?DTq;ih<6cvMN`Tg=XhD$u`w*_ zw4<(iQJ_md_%64hTcnHTj=fV^r+a---L-$J5x0d4w*10-4JIury;43KkoneMiUz%T z#JBUT&ch?0STw@NjlKhesne8RgL|}umE4BzQ(q-=BPdd%e-L-7jV-QE`jIJyi(+m@ z$;|HCmMd(FV%u&1x%36&z4(eZC3UT{&yWOfeTrUg?qvccBoX%&J&S6)d&1uCVmai{ zTHJttcx*Ui9QDRO;AcQWsGX*=KSg7t1C^IN99?~lH>MX#r!Dd_sPlc7k-DFeQLaOr z`^*yOhK3$Ce=9N?VEH)tcRJZFMwcEZ%@zPRTC1~{McIg7m*mIQ$t~L|kXJ!k70;B( zk|1rD9ZB@t6=g570mdB)#W^>SHGq{sHKlJ*`dh3}x+^5tTOBK2E{X|7?lpYn36DJF zV#RQjx6aTZM$)S<$}gCweEnrtX~gP-n7rMsU5AYXf5Xp#g%f)QN6{hk;=#4YlL^nx zc@mdilHi6jixhy_#gLwAixdO~yQT?5ozG(zB0lusEL)uC_gQhOU^uG{My;oa=Ij7p zI5>6O;^XB)V7=H|NPad$V^CWk+LZfPvT@`WfQQ2;=#R$hUV4wEY%_X2rx<>r>088? zud+pPe_c&xP%G z%jGisMq|Fy@#uQPM|CfU!xV#X0J*ck*&7lb8dUbLFfG?rGeJ%z=5Rr~s*5_*(D1A{ zHyZnK^k=6OeFTW2e3BgFO4ts3&4n3T%Yv&Ce`>!6&|3rj{3;wK{{(XiVq}YN;BQ(! zS`rIGaIo?u1^V6lDd1e3K&zbxUjv6)A?LH>8d%HiOvYb$XP5SdbV$mB2G1o2zJG;J z2BF#yV(gCsj?r6&Xs+KHCYh}^>^|`_gZ=Dns?9MscVfAmZhLSMF%6uVMOLOo)A+uC ze?N^f<{?AGH?RAI&+Ly@MK%#f)bTO?#lwb&YF7WHSa5_YERyu7m83^=<;HZJ6ri4f zr!8J-D&>c_{Fbb^W&P+2}5xwExKwns_SIn_Bn772D6UQ2`>*ySSU0|U*?Pps1S zJVVWuu+C6E4FeuanOjF_wdYc+?b*iYe;v(O0&81E!?`S#Q@&-@fh{nlH3S^Li9CBe zi>TEe&_+0~>35_Wk@#XA_o!;F-6F2?<)e)2=ApKo87EZ3 z*5lWtiZTsSWxRn?NE_;cgWAZSPQ-67;BHrhKpfuivaIsg>t%UJSOG=6KstqE77pc!A%bH0mTcb{@b&uB8ojfl zTRjHeEE+@;@mtTv3pw56W zMQagyFfplIkQW(d%JaF{gQr);9M^*#%Bd!Z5Z(`;L~hiAQZ7_Qf2DQV_$Y9Qcq4Iq z`b`*#X>E9Tch`n{l-!DNmma z0=?j?k0ADKZli_j0iLpTf16~7$bCJSXVt>yN+7}E(d4gQ0k;5bsmibNtbvmiJ@et2 zNW-e>01TaB=;VjWf8k#|QmF)8oKPq#&rPqMeR|&SAT{T=(-A5Y5333w-32}7Y%Y-$ z6xnLWKxMLcfXV9>>-p+>k-IIUVl$xMfC|+JB*8%$tYQB+BZErY9TzLs89`3sU8Tia zLF-K30xcZ$!kw~Z?AN8yhfyWO1n#K^*p1%v3`U&ogx;NNe-Q2HOU`(gaKt_sjLHfB ziLtxPfLEqSWV9KIu`ly{aqH_WlOWUU0&5W&x$TG|%JM_wwMWU@+#p%pL zsWB}(mnvJ?t~kxq?|&5QIA8dvj>JfaHw>uT;u5+)yeYE<>@j{&Siwgcsq~N6^En*Jl$E2&qmlOt zj!HnlRinrx5FMMUs~AUWAheJuX=$tAljLFv*{DCq-Pbws3dp@z%r0J%zfYVfQvo&9(sHxf2+DV&KJIk0Bz+(`NcQ%G~W?9*Q{fr zVGp(XuLkt+Nbim$Gz~e{wI0vDMeSr;#B<*^E2P2E8UuYO=hzi2lmpaiHp|{nKegz) zs9@obJKx;cn6oz={Zwy>JZi4S0Eo>+4p-!b5rXu)5yl*KBYL)1i}mzbbtbHOf`*rp zfBlos-6Kd6Pvs-c=2;Kj%8+x^8^gXuGtQA^zxhMl8L!lYOwtLqP+-&DYnwKt+HCgQ zmVy)PfUrWoz5BvYg1a`uMU|bO1BI~dO|^m9)cKYhLo|Cr@k@VTeOL&eM|2ToHH2ke!o{6}|LJx^&T2Sfiiq1HfAP??i+%7g zFvmG3Ci=WsyG3sF20q5bp-h5qRGt*-OMMo-wca?2NdHnKd~AO>z-}` zq}9O8jR$fC7ZC@rjl4NFvjv<-ddlf4>+2?J$(~0cqZ0 zf(Z#Bm9O8e`QJFL{iZK%Jh}MJTmn( zA*Vz42}3KR;uHOKf5TwAT2F03#DUh%^N;#f9!pva5Im&Vg|R#VF>1)3@@F;^lI_*% zBg~_i=Y8~9u$L?UX78kLJgW@K*Nq7WOl(bvr?3R>(ESGtt45j{{r3X}_WU$?D%oAj zW&l#O`Mi#u$%}H!Wp_bCi41Wf-~q4K?C-nEHt$}&I{kQbe|YrrqYg1BR9axlgE2ms zs80OFOp0VZjqpkqNeHP;&#Eu8KI};`k1^0oPn6!xcVRY&Ka0%$=>!uG<9F#O?#d=KT8t$X5^;S5R zdz7!Q*H&puZLqTvLF5A!M`!JEk(XZjVrjHyS6F zu&)i?khH1wi2r2qjhv>6P`X`0b4(AcZtu70OjO^#thUEOll zH$1Vpe|8tb>Y}4M1scVWywW!p!a}qrwq6>cATNrg&!d*E)KxLm0`FeYQ$*NI$COyG z{r!lwSsNuqFo#^7pPdrUw57vUc5w|yOE8jabC7S6QT+7BM=IK^F+{4LMrh~Y07|$1 zUvTZ|!>utU>y@@PC`gx0AY16QByQ-o)xjS`e+5jnEr>M!$b7|;CNiZ1>5L+}9{JZ^ zphm=z?oS^PboFji5B(q}kOtC>L$yP%MIgOUF7X4M+ifO4OL zNy@&em?eg%^|MlNI#Ex}hs77`7d*L>e@;b0me_a5FwAyrYfuIdt9)Bzu_w2dJFg*n z?h>2ruzq2l=2inioy2dmGBr@%Ce|cP?+`t-$Sou|?0TE+yABnE2J;oEI$OJsv+L_i z#M_`c80v&x*QjDk36WM1qk9MXE9t3U%p_k5m$JUXc-I!TRB4A#HhiL6J5wuYe=##o zPr@4^(AXyT+zjW}RRu%4-M(dpxK<>5L0Afx1VZiiN8eIDq$Fmle<@|8-hTloi)E27 zdcyao+>nSQ?R=}+UEZd6psQPbQnIsY2u#B8S|-{PwRWq)(x_6WSnA}YC#|)G4Y#-$ zay+0TKp`v+T%vhZZ8Z`X%OWkpf1mT`>Sp5h5*nGPeA?(vRad>IqDS*9+)Th)CTx=a ztwg}uffg|cZ|beYK;6bYpa+D0Q8W$R2pA&>cBiZIi^CR&`LnM4L2by|FXe|YXZ%B#sE zt^M)nN7HT7zk)oO6Eck)TDS#qg`|~(-7LvvZ< zJp}cXCuc*}Tfc%Eae?`9ce09VJC9~I$@QuCRza^f-0`0+#_MYP#z?NxHnZ*th z`6JF*Lho&lgTMFSHP$C2dqa^GJI4+2D^g%R8;{8AZC9BIzqM#FmpcT8?e7@G4nv@K zo`p3Cg}#%Z)lSD#P9#={CUz=5c6i&tJ|(h!d#5y&d-f`}%(r)ne|zvgeeOocg`o6b zM9Kxx?zq%#la6e?-TCs9|I0|e%doi8jo(Gc6$uXW#o}oc@uQ17oi>se-|UeJ*NZx1a_*5=ohd8 zR}BSWPN$c}n%(SK_LY#HeGR7r5JF6Ho?#!UMqJ=wO%Q^x$Ahs`4`8&F>#vhQVt-OIqF%fW}%r zS(rkZ5Ptu>x~{y~C%LwT!H9y-**OOy`NKJbp+=|$t@7Z`qBA&a! zxL%))X3Sk3Kmlsu+_E;18$A+^B;0N>KAN=Fe+gAfqQN-obcNVfu~9K?+#bj52l4cR zV0CDoy`G~EBU3=gTrS?#2@Y_f{l?z?Y+tcu^9z>y;2^;^QiUl_k4*Pmo$cHT zkNp8=CbenN<$q4pIJvFM>uR3AME-XIfV;(@j3fX_&aTg3chx{1#jj+Zt>-x@1VW;m9zHASVhqXRk|E!P=Vkg*+Utx-!>%)E#}F3|{uiK6&>cNaxd zX+Ofj6`GP8@5jQoxRElon{(o+s_RgMVe#^VGxH-_`Em zvi@%Xh4ggbd-8F^0ol#=)}=T}i8`S8&J=i{Y>%%PoIg;Wz1x_IzI!7vukqyk8xOI8 ze=|P>xmQ)bEWVyt5x_Zae@Q54Lyt_` z<&$|+UFT76NiGLzZ<)~?=*P3CltCh5HdEm}$cDJQ9kIogU@*>Zs6U74P#rJiox&_< zTr@f%LJ=e=mBVpC60*eu;otq1<34OQ2JCKc#ZOEXk~_nL0*oJ<3yAa>8Ayyb5VAO0 zJxj7DoXfNP3bWM*4HxJ)e~%V^V1j6e(ynw1Qm9Ai@ywMBk~2`J*C1pGmh*E@p&puZUi zEZh=6;16=IU(^jH;t%So_@^-JfFBA>O2(xDrIIu(NxWr%lD-}Le^IyE=@lPghCR=a1kJ2wjiH|4EU?Ex^&!y zbnxIO9d9PoTA?ohf7TNQATKaltodgfZ+y063Rof*;Qp2c+Dnw7I;`}MQv7$H^J%*`y-r*8Efx%CKnE*zQfE3%^7e=XSEh`8%FO4l+KcVL2fnx+imVwsuBmKaohVx>dNKckft zfav+E01$tm`0_*2e$pLt9N};8Sp@FhscRX>S@!(~?nk@!Jld@bx^k%wj>@Y)xP6S1 z%FPQU%J&vh%iesO8MJtTW`2cJaH{ROn<2_DjsPW>pDFUQq)AapaETHn-ZAzqz?Wa5((Qwx3)rl~C>(IeHu{by zIz1*D>cK-!e?UH8kTc;54jGj|cEcnxh9Qt15YZD`bS1>lg4jr8?3__jbmkAuqe9A? zMf>S33t*@;P&OG&n2i(R6R-UVDN`8f_)Qr~H_XVCX;^@t6k(c1AHeV#w9XuxXh0UA z9w?T(rcdGGT!PAA*|EBre?*925$VlgDEbH$D9uFnKsbx| zZ^X8Kdi&TdAjD?8C(UHqfHScVMSM7f7>z|JqA*rN@j_2EVFg$_i-3kN$_!qz&jU)> zFNQKL8IR(-yAhTVK(z~r#<>(eMU$UE!U|Qe8FDfZX4zb2HziAaFd9pQzecan&y+4p z5u$lzfAElzgWB+3vd?(=z9+F-)Wda4gBd)UG|*IHmp68Yo_A_syGkVN+gAAiML@d0 zXQSWROV>@*+dBNYbpqrUOi&z`b!vkXf;|iQ1owpu4W!f%U>IZrA$|mZ@!;~E5-QDeGx-6?c<`r;7?te8ym%zm>bmzD@N&V80>NZ$- zZRzTTUDGDzOg>PU(>4!#ROAGZ_EyoF4amp6NC?TpY^i^?v??Fr!mXx-CcUA@8;1`O z{D5_bkbH=Fn5r>ZM^PHVU7Kuhzb}AAWN}Sp+y&=X^LizL* z3j9ieXMd5>Q)Ykzij#axdBNx^S31t#P2h0nW1`_3!vSyf1F@?UalSNysXrikc_4`+ zT)d3_B?BhtN04t#{3S}tq`;T5_2BgK#s9K5QC4C%q%5B zzR1BQs{=WWI5~H~Ar4f13NG6V@Ci7rpYlL~V1L%B7;`nKpPo}fH8_vsWc8Fr4=JmZ z=aCIoPnYza4VH17Y-4bsp_<(|Zsl#D#g$o9<77Qx?!wDO+*}Qad7(VAS`aL0kmq!^ zoYaWeVb4tiv~t=EP1b6tIjg(7{`Ng;Z!h%6XtieYS-ZEVH*tJVQb02-I`{XqQHXRQ z_Y_Uvv4$uUVQfQN~3%>dAv0J3+mV&6__J8pw z678jY$i?HVu{&3uS$&A zbXet?v$N%zsKa@b(3uk-7%A$&?(=!1mMR7=#j1&ucB(Q*&=<#?ToUEem&#^mb$=ay zFES5QT8U{=XlZdW9R%w5)|OQ>ReyG++k%|pTJiBu>$Mj0%iWp~q}XeNr<_YkuiKbB zA3BF5ATGB@_wZUKcjBIb{u1%c06P3waL20Yq?sLN1> zm!XT}`XsK7vy&8m!Qbh{@anq0=vVOub!lmDUmTC=#{qC*Vq6!;4`tpX`hPJn^B$k@ z4!$`4fq!95cFuiq{6uB`$iD_Mb5o=&xq*#Z%rv&zoiOyQ#5aQeObU8hPUHWN*9bKh+{laSi1x7${0&SpB zDpu=f4(smj3L8JayIUs9;eT9{1A~?uM54chEmiVqut=>6gLCr>JL@l=j_&Tz0Clmy ze}x)BFpD_B1gk7&^4z4z`h2r#)%YQ0gOXr)GeDPfiwaGkx9I-ZwZzNY4o)ZOC?$Uj zBN@%e51q;Jox-6Yg2NrSg+&x58QNrIdh!;Dz2U<;7*0be%>}!X8h>->=mofn_|^`f z79O4`L(!I;E-5lgcb5av)$fT7yJzrP(RIzi!{$viwcc&RxL|3|@mMCe;jph^qb|cn zh^x1Fdsb_duNPzna%84N?Jo09MY|uer4aA5KF4rDsp@JCr*9O9$^HyqvRYf%lkVNb zW7qz^`4nbpyv9Se0e^UYQLL2XzS6RWw;HFwhGcu)T42nU?PRqTE+Z~KdVsDT?%kcI zX{`&#B6fi_WOqp{wPpZ-wAm2vY103-7$;i+m=Je^mJ3OqfPCMu(%AB}GmS>Ic!N;m zdvVQ|6-x`65RWzk9xpYlVuVsTWdKHly0%p3_sT| z^8D)1G!f`)#MP1Br=K5UYj3qugA;xx7mMt{PEq*)pepfgmZ5zst@3-luY zdQ`rGX*O*pNeZ2hb7tBlA8zLg_H{5$GPhnu^)h+t)EDLTYVk9MVF5%DV}bis20UFg z7$kfB{8?JX_LVXF%1bBwjfh(RDJA)8hsDHLa~iD1f5LE|Dwpl2#Av9xO|k|W`?Q+I z-bB5rAAgKbk{k`*+^MB#(VoC2c~^a)BSSQA9}a@xuwD%o7PHQU%0OjSrI1Ft(AI@f z){QxH<l7A;EV3LAGeYHa0Embm7mVyop@u{s1 zE4Hm=W0|FTy*9}>+$Tnuu(pYBWY)5Grxw~6`@wDTKiiBm#isMODh(@Nql3M-0@}V= zm_8_-<&x1x9GOYV3@DE`{sP84%0ma?*uR@Sn8# zY=7Jz2$sODA$i?BoHGjEM7|XiT%Rn+Y@~42dm3EZX>)MklDXz&_{EqOLWtxhUa{CX zLF6cR!GJLEVhmRj$_meG1SC2wox?qXivR@SR#1_8Ull7|I($^I$A5rUqq&1DY<#XskyBg{O+u!mIiO%|VxF(p z?_nR=M^Or>d#v>d7-w=5O>n}99H*DeZi)Mhge2y%ai_t7wt>;xh{{Oc8e-pKVq!9b zN2`2Nopie5h<`Tsdav$X*4JrJu(9`PvfpJ}IRt+BtjsEAqu-)Mab9BqSgt6q=YLT= zGl%rDo+DfE%`GQnS>zlC}C zE?L_oG-gBX9($yCrd8yUZ1uatcU-3tnW&mU(~)=RB&AL2vq?-MWHn}H1jmJ{^=MfC zD&TO&bfWW|M)htD7z2yHL&0~~O@Cc1#D;Avj)E6p7eC}#U9KHZh;Su^n;Gw#ckEzq zi~k-%7taZcAS@y4f_B7_4jytAB)J;)rY9DT-k@lGOS6pn>iT@tbN4|-+Ezc$z?supmB+LNS6a}G}O7mhV9TTjum#7Ug| zB|d1&IcTh!ItuuAbYr40dH?Hv(N~grf@gf-&ZWO42&8lkW^NNinry? zMb##!mhxjZnv5tetxUMP(|^C88RfrEG48Dym?G|Qs{UbnuZZf}Hsw}Thf))mrQDiaJOYstY zaeFN>TKtBt*P*6F$KF630PQC6tPELaWlK2~{jl%#LXOO0Ngok_g@1c44jPOTm{<~T z{q~2X^EKkHXc8DBz*n%>7T1H8p+#3ElU<|1LB&{{r8Wtt>{lGv*?m%MY zJr9G)WM^j?c>?jb(|vaaoGS*RPuj$}l&i*SCTcf4#I_5gei7Bm%yHsfb-o9PSlEm} zhKmEkusRaLBio6OS$`m5(`)G}D}iGP@|p#csXb6wOIfno-` zyT2cnkV_ji&3kDx1(`BDTU}TExb0+<4s;M6RVN^LpeC^Tx{~WdJ5)*dFejg6URz#O zzI`xzs)rxq3IfDdJZrgKwTv=EhfE43L=Mt!;&+WqI06ssYy%?=Y-ky*e$q6pHrN4* zevS>yMS1gWseeaV9Yi`(Al*L5AVfQ#I`22RZCh7fFjfw&?L#L|yHs zb~ca>c7I++<92qxPNXwdkEhpcfqD9nl5r~ngzcjP3x!(mEvsuk{MaIxY5V0Ohdq(B z-Z1jxTs^PR)@kGUEszP0G{m>{-8-i7oXoNeA00$eWi=zi?RH&Ox3wi19a@|ax&+?% z_=dgX1=}!dm>PHOot1-3iAOFRyT%wbBk2$<+kZ|J4d*IWB|=PG=U{F?R(;Z?UG!^S zu6Z72leTy}lkQb@N^?ry5C-to4H@ zx_|TTt>fajIZ4<2(gwWqIu}c>D@pyB+oFDs1yHh>D1!40Fr{4f+KZe1YsJ{VWR*4}Jzw@CWM9>&;ZFX3 z1y_l@hfd@E;xzgfbjS(}Ru4M8DX*)Z)_>gY{YCGmzw}DlIpn@I&lXs`WY!FS7R@k< znf{b`u$O5u3r*p;IGKdflf$aYZt#|lfBR*8cL!}8mnU&j#_D-H$?$sN82YJVw* znl@HdNe(B8Y$D%KzYQxAg7&;tjq@NL%%;X^QDd0G7O6M2Bkxui6Br+J7sx zg)Tl^qmh&Ss(30R?&YeOv18Xf?gEDlFkXuj{@KV_*oGDsw=g}b3r4U-(=exC&!h%b z!pc~~i-n_hx~iZv>?|#ZYIp$TM1OC#J-OL3ak2uNmFZ5=@jl8Xhq#;i864Z};2Qei z_R09S#OoyPzvwv%qt;E2V+%$7DB*!7Q9O)*YHhOp{enS%yEex>DH9&y1Tb7BW^nKl zeptN-Vb(!3(ai(v*IS}N!(ODcB?|R^{oGUiHV9~ea!ghmSv#5{5nt!w^ncnztUl3O zmHO8 z+&>IhwuG=8C}0eDDWjxFZhxgy@V)UyIxbQJNVTO^2>>pi%5J75DntRjFaV6h!&ZHj zZX56VTFuLVNE-~2dr!jBOW(*Bn81Q-3|y025bjt3H+B0iome93GfeCF`8eW74yMa{(sLRull3mT2R%VM=6%f|~g<8_ELDS*%jURC8~;Cw@oz>rJC)?%qtn|a{rAm+*f@Y~|+ zoT8hg&hasgY|F&Kd}H%*mBC)3!w((j>^O4dhNv7Mf4{2=^z3vTU6%hmvJ)wKi&asi z1HX#g3}F2wDwe_Z_kZ$adWqy$*u(={TTSH$%(4nw@&wLqts;~qXH4hj$T%$UpB(?; zd){=>%&TDp{Up?`<;w@bu3r zJN1>ANcvLGsWMbs;BbDCUqQoYeb$eJ|Kc8FC%XWy0U;ia#y<>3j|QWM@PV!lUy=*h z{408*#kusp!2Ek>NFb~7%Ubf?z)7H)i0ZoR=RgoE$uXfe*;+!q0J>Wa8IH6klX9pa z!$!8i4vFWAfdXwcI+t0t14_(hxA8N4h%Aim^=@<eH{k9LEv`zI)XS}A ziEyHxH=B53EM%UP8XYq29KqkXq)^KMY~q=w@1xB`e>5BPrKuW3Ey==&)zl23*}N*| zJiVim(E=H3LPY~LuaTl8bYGX71C5fKXJc4K{8c;~&t7UeRvFZ%$K3=vMJdrdOVY2l-b zZ8XT|OvFV2_hiL6imdUJuae^j%HXLuNb~Q3DIOGmUK>r44bum>_4vBWWd%IzjKzVqcVcDTeiKn19Uyy5d40a<7MMjLp%nhBc|ljpN~EcGYL)_gH#w z`D8Xw$Oh)3Sid+P!?obH#)ED0HMuMni&Z{3Owe%l{pPqhet06T2kZ2|@RlqZSkKlI z;~=^)e4{9&x`f0BjYVFJU3zI$;egeW6o0V)0HAW}d_{l2D*JmgN4K|-I~T9|z+k|N z$=QAs6DWps^dF$-5qa#ZWqGO`m55?MKW5J-`Loo`I6#xM*^9|5-T9RyId~%0UkY(u zpQG^W`9#n~1OnWp7d2uNoIG?i!cUkTaYR)5)kJQKLB%OAWPQMARWV-z`pK3#e7yJ9U=mY*kI+zwST9D($Se7LJ>@_)OkEGF}5l&vrx14h-A1yU;CuMTk32H#fg(cpkK z{P`jLL;?@FvX+#J$l?fySuugrLyz*t9;HO%q)HK~3Sy(kr;Wfq+Q^LHX%!o*%|g)h z6wy>{6{@8-3#^>+Nz5$z;ydq`UV5meY!!U~p7osq86ZV+oqopNm4|3ZR)2J_a2eVD zbxe*;zNC6*+({h2L+rjICY;oOf!f~z(|Dv{d#Bdu&R;FF%F8t5ziBgFn-kMMFcqZO3Q5IW2ASfBVD1rsn~9Yb=YBy21Nc1`Nh}uz$``8{~7|V=g=C z!CuSZdojU4rp)>~TYTzG#x9_z992(I6=& zIWv1VI~LLJuI{d`s(-Er#wUSBor!uTr5HKAgXG}cO(fFA6ePOxZ%W0s>1I_0w_TYb zHYlMx-KxXK&qE|}RR~XG?=r=xcP_W%?psQAHeCJro2~YOe>2#11D<_UckZEjwW15_ zD%p>pB!k2FHE=DzCO7zAI9i5-gTr{RALAdma1kU~KQ98zd4F&a5958ETrL|-M=O!R z;X|{y%;G9C6QdIKiQo-J({Mk&l4(r*a^e%2h)#Vda~eNGkFsJAZz)2rU8T`}bk6`*IgJarfW%?<2UgJ|uhCdHp`b zZm7lj>llC2xPNbXq_;3O%y=K|#cu(6PXUzAl7sk%uB&HG$%UEsLay@bsyxS-w8^!Z{rUpdCz+YuJZH(*8G1cMpYhL~v$6`O zz;VXiGDCy?q{sQQqcdPv;3B@g%|@0OPzx%Az9kWNl7Hzy-JlmjG83fwDVto@w@YZ% zP%L6f=8%l5Sj`FhlvFobYSRN^bhFgL*35R6S4zPtzBtEci7Yt<{tNy|a)QQ+pXn-W zfFZCl<#Q?~>csU+j<0MxA^x5yWF!5k#sR&RkAGgAy^Z%e86sU8fO za4#lJfB#stfGmUgjT}bvc>mr#S$1fZRA0$Z1Ajh)`^nW^s9C;sbsd~ zA$F2dzd8U%1`Ag8Gg`7@lI^CduY4EQAdkL=wSWTAI7@&dNXpe})nd0{y9vdwx7U}a z<^0|~{wv6m5_mw~Ns9g+oK@u|FnSZGAiij32^Yp9Xddk-Y-G3#v!ui<1r78ni=q(M z7=J1^pA6`ECsR&i@%;t~7_mnI?n;tCKqB1|Av6I_XD6?J$O{-KeE2Wc|H`h(+_T3m z8rLf|QV%#Tbcq7V^6~y18#&FIPgw?>b}ubvz5PKTu>sCOp8`3JAf3$u;=6-%I$d58 z8pZ_fmY?OGz54mNd1x7l}W;Dnk zzT^mTBkZIn`=&<+mWkD}e6S)AR~g0o3wC7p?yTk8g_=hX!xaMkQSJsfQ<`X*NPK`y zb?YjDbr}UvV_b#gx?Y`bepgFR|=vM}k|W4>KzSLawzleL@Fx6*C!$}Mv?&#s_B{PR~p zK~M&NzP+gO;)D439`GIl0)w_=QTX#idYR8r(+oev8K$3N`1t#8Xy8g`7Qgk_C9_3H28FXP*&%A``>-{-QLwjb2*2W z=+P>kCMXmrM!`OY`nb$syIqcg`y%eg^us7Pq$mVufMeaI98F_qDSuUp-UBL0?h{Cm z>ovLwuCDquT$l4;6nr!M3WlY>zdeL+`WOWdzxzs$4qYAx@f^LU!pDKAM&D3v|Bw@O z$^n*N#d-sbZupWO1rLTpjPEv|L3i+T6`P9$BSkM4Z!c0HBC$?^(e4*lJ^4Rky4VdY zRK+MCokd&4SI8Qd1%EoxepeC=i9ueO8ING!-{BL>3BCEwa)e$$ z03L9TfMURF{uxvNx^UnaWi;y$}`De6yA4twHeN>~=nF!b0TU!bA@7Zb3 zP=ZBa&2GRXQ*WjQNz@`Kc%X4v?^9WQKtc_8m0%Y=vEpdhT7Llg3z{92jybCehvnC*pZBjW3e3F>IKW0#*rb1isNl z$}&N3!zr+OvWEjmy}(Dyct1&|q)@ViB8AWpXDc$`wny`re!nY67cuQ)5rx?tO9rI4 zOkFzLKz~Z_MqZg-NXSyNH3fY@X-Vv-)#o|xbAfilscLw?^1uTz3f;y=}{Qzp-4~A?ZkLoqNSpFC~ zQA={~9v)<|Dk~56P=_<`7v`i$E~nPY$%7q;qJOvHs2DgewAEf2jWeli5*KPM;HR}r zWTha?SuEs|M%$tlG}oTafwv})zbnM^@ywO^wo#p0P$5+a_xj-=+B=WE>y9CWep$d2 zo~AYWRc}dbq5T>qgEcAr0fxdKd@?4Ge{5+$$^lub>$$1AE^t_mIE*`BP?-|L+*GK@ z$A5eMy+M=JO^DivJFc$ZkJpQfte~r0u0;>=n5o)%a3ECtrjRkmnc7O6tb_*bM{$5M zteo^ADp(Nxg&G>_lYL@V$#Rk&8+BmQpuPwvYw6oGsc<_@jz=|1uZLs6GG~vb5-?{H zFo3!yXUBll(S+$jeQ>lWr2;y?cTfGk;D3sjTz3CBoW~bb`jRtdNj9+XgxI_EY;);3 zilz!ub5x-ax;ZLGGqgWlY1D2v8UjrK^pH<<0(GlZB*h~uJUp|RHpKc7=&yNJCFPhE z^==$##V9a|)wnG`0qXXv9unFuh8tDni9iu-ke{U;aoo!ePS}&n6ghJy4ma-aQGYWf zW+lxH`)e7Di&^b021G#h{{x*S`C@-VmMFlQk1Y;xv+TY}hQg28@pPUqMghL;hEOH$ zS^q?k&x1Jy>;p`*;;{s~WN<&s^~g}r+&IOTQNNqUn6gwc(mb=|x=gNE%tLd=Rssty z%oLPC$Zr{rTPVA$Xx{Q$g_af-WPdnn{**ywg8czZ%jCgfq{Ob z#6bk~<|cr}nzfdc9r%bMh$Or;4s)NI?m+dn(X9)Vp~{*`BdANW`^A<_Qh$J)QVO4g zlcsFaxq9%;gfe8ln<-^VqMRfp#H5o!HQ_xMr-HTx$`)qhSB5Gh6p;96FuW+vOvA4! zG+XOYo()cx3v^TAQ~Clr7wx~8>`LlYe1>+kKykPY(lY>MU!?V0m>^tFkOL>ek8 zqRQm-+k({6Nx%~M42b))RezifUP5OssFak!G;u@LA00dJD}|bJ)}FE&9Xrm;+yP-E zzOJb$&%*mq(NBb-!v?xm9sXbW#GW*2L!+GrP#VWcfuaKk+O(C|)CqvO3TLkTMLaG_ z-d(>U3NGGhpV_b=FpN|3K|UizP5go3FqOqE>8ruEwD+*kv!*$s0Doe(UJ)TKIt-YA z!gAA;jxw1U6wz+>HEW!ovKwBzb}La;FCGmJkD5`&(iSGPl0-T+(KKp>9_T$bmKD>u zFn%CaW8}GUt`cU|in!|F=e)ioKi0FJMg9X@d`<$Dn&nQTU^iPeHqn$=IIL(J3sidB zhPhCmAU?p00fB= za4m3QC{bV1a@~Rf^MaV&U**b)WR9fqNj}s1W)}2ZaPIC(M}GnatfWz!5^9+)SxF?O zC+4?&%h9-k0ImI4N^|cY=Bf5>sf>J$9ympi&_>)Q)>@S-34I83YU{Kzk_@HCQUJx= z7=$s+*BM1lC9kRmpe=P__HYE0z{j>bC3E7BhWG9brB_6wkE3mdiiSVilye)8TEJHB zlw?Cv;n#${@_(9YFH&Y;M{S0zV^EM+As~!CJk=u@IoHKb(t@%!%{Dfe^i|2q6*W1{ z-03LV+GLN10;<+!H&P33teH|>Hd<_HDx!wnZP)G)0YcqeR>w43MeWaIw~4hvCWe2? zYF4nd&d%*EE7CZJOncr{$4<;su8YxMzu9X;CR(@2+kbhizN?{NeHaH^9C!s_XA{8M zW1bKnJt$&GmD% z<*m}f)6b4)^D8F6!3ARvGH(26JYdiTqIvaRfpQ_#i*2WeJ_NoW`!Wh9H3Y zFwhahe&odr%4Hv~90!qfcjX!!7A*KjFfyAtk{e;y+< zQXBjLD-Th@9vDd_)w2?h@G--9C^X-6!r+$zX@9R?Q}%caJ!Li^JML9z)vPYA9(t?< z;`NM__j=l9Kq{TlO7J_7RCaG)JN7EFrngJbISJGEX1To3ueRB7T9)_NC zSZ!0Np%A8CKhs63xq?zQL@qa2G|^hnxPOV_j!{p`VwwtrL&;?wO{~x3(d0N91NnhX zmCMHT)tP%$78-WiDPt?g^bQ*j{WxnVRF%E>OfVBJ2hk$QB{B-VH{+4QN5+}%x4^X5 zrr}0i*so11ZbK4Av6p>mk>7e-psk3YD2gnb)$cHvnoSufiUBUN8*zt^_G57)kAL=8 zir+JVi;$(&@%hTLqv|PYg~}^$7SdgoZUE74I~7=Sl&ZDTDW+8Qfkh)-eY;~sA6Om- z*{GSUc~EG1S$t00F`9)~t&g)E*}gcs86Dj%-=q0#PRCP{PdqvqI6QB1VzcTe7FSIg z3zd_a)Wk~Ji4poU&f13&gOcFGD1Z9TjZ>K5JmJ?<=cU_t1aM?Q{me}<-@KJiI&-z# zVl^S8U1SwLpWtP8@1F62f~+IwJB&s{^Ibf;nfY-IzT3w$DdZfQK4Th|LsPJqOY{&Z zO!}1!V_exJLVO`hl{k+S|DhQzQFBMVeR#plIDynTP@pbv>Z33f7mhqglz**vCpmR4 zePzr&97dF+Z~$rT07E^N7A~qojA*6HjWbJvQ^duQx&TK@brm86@wa#&pP~7_lafa9 z#l3qQy*EQs3q)j-I$j>H_+5LQ=jWJ?2C4Yg|`bZBIMS(D0aDK7Zh8O04PP)mB>KK%4$Y?Ve5c{>rbMSWdjm4Pm395n5>C zzR;qH#JKlesT`_o8X~4HO07UeCpN;BID$?C9~{+!=UM3j&_3?Pc$>X(NfM&VRo6HY zhYZ?TkVA0uAjpu>-pe7ENK-(v4h@N=NfN=#a^Wf`iY6Ft+OK^{dwq7U6I$YOtOu8zc&-%VI>{JK%ej*qpUigSgX#G>Y_JTqe2#L|k8H}H z%1=_kX@p7fBu6Jf%hS54LaCk<9j{#T*(}Ml7S+Gr*I)0&L7;%|^gW@L(S4Ao&aq{0 z%bLMWlC!@|<}3{7z<-Eo$I7D&t|V@QRCZLx9GYHWOjH6z?09QK6#i!kORMdtcwiLJx~Pq~6{RQB`kT2p_c}dfvf|9FUSS zyo*FCT8#DksDhc_y9ZU`9OXn2xvQ1&+C#M@L-VOkD*sl(2Y=pZQt8DjOvsk*L;%o9 zHpU*uQU16%%D+mAQSnuhkM(x>I@=5LUDHdB;km8XHkhPkH{AN+staU+CUExM>-PXA zEQn2%c2tZ2QRn8eEJI6|4i#r(eAKIS)MLYxm2~*)|8Wq0?TIHqu2_=P2H(Vu|Rz!!%d6UM|ikNtACBPX{w~PZebqp{cBH5tpbu6?_tYN zHf}7~zt)|rQDdvU4BCtL1U^GeX|!?HH(L)4{mm;@9m{d1fWm);SG)8obaUd|Nr=W# zW`p!*aW%qdpwx2yyIRiuEpzAt+5xDHQ;M{`R&ED>VL~G2nGuiPS=;%3E}*uFx}OB;rMvG~X2U3WYhF zi9O>W?%~srSFsx~K^dck?p41gJyQ{}TYkYVS+6~2h-I4c2Xq!5-j5W89Snx3LVpYt zwoPE09&44i$}CnV6h#!asROZEM6gf#yT?c2``Op;2hkC_GpfJ-Qc%8)^a0!xjxU<1 zMShC?Nn<#+s7fJC$2Upkw-jhye9vQNHGbNWn(}B`(qu>%J}kn0Y$irY>9OPY*P~a4 z))k^{)IjwpoaJGm6wZ#j+}hH>^nWPmj-R7u7T((4%}~X1!m+g4?&s(lq5nykWFsHg zzYqq|(S~-g6`^#E5#cY=MJAeSE`V9TDCe`ajifP~diT$@tv75;GWK3quf~3>#YKuP zz5*C}`Kex8VG0#~>vPOpDo5sDLes2LBntaMk&dWNQ4ODMPm|oE^w)OxgnyK!RJtY) zPR5sVMuV=xOm0NESWbpV%?Mo+A7xs1nN4~FzAy` zIbN+W25q=X9Q%D@$H}79zz#Wa?0ZyR)8z1#xce*=Cm0V`%;n>tIzLUrU^ioy2sS+2 zlo4(W=G#Bp?thvVAJZEBKo98j!>z&xKauk13RmZ8S>S~c1_!fsw|`6#oBme%Q_JN> zpAA^tRJ1j5)5u=JO>D}AN}AeE6;wLiYO`!wiq~;Yr_*jy-ie=~F)q|x@ghteC53zV zDUgJLlj$3F#A$LWPL?h7ii06wfKwow7gxcchCMyK2-67FPXxt6q{odOif*uNC4bk#y!YYWImTAvBY`0;#%7t+cV$f}JRIp({SxdpD=qVyCBZMS z$+ z4qp#3DjbSZl%K9#1vHpWE{@@fk&Coi*-pfbr~~w#WQD!REN{)kiqN96CB0OmFCv`? zH<6Mb7ZECv>3>4X(RJ1GS0=N_gW(Xp%(;d1K0^l$T&P7ni8xbE^M?BUzK-bxTnjl| z(~Mm_MB8x}Yi7}Ig|_@cY83Q#d)mFdZ0zy=QGn+LQ7LZkQ6^C6SpvuMHoLsdu5PCn zw-x;Q`04iJ#qCEx`0!5($(Og6m$&e>Xl@tN0QQb0zkkrneMZYowxb;dc7t2=N>Qnirmg^i3y3$JUr{)L@4)MeeIS_wLO_&4E5_GpUXculy^CLZUghR8 zI01_(GJiu%fsbe_U}RGtlVx=d=N9%5u&tYmA_ErSFu5Rxvt^Y(IbNX0Q~$Mj$;_F) zHZf)~-dP>xFJN2~=T7}(Ia6}TdGn%m()glv67iyyNkomGlD8#xn#7}vgA3DnQeS<+ zL9st%L-MAU-~4yoEVI@uX$F<;9ZTl%Ox#SIihs8jX0)mZuS!^f{-EJ&zh*N@<>#q? zYWscai{j#|vV25gKllN^G*sZjg^F@>DojUpCo)0wB0nqJBb4X|V2IB4aNog~EWiL8y4Wn_-7Qp97shzv;d8@el%A+ZvXwwvpnHD#bIyjaNpki_aS3?t6QYy4OTH0G#4gL&AID-++v@eaBl!q?jlVmxWA$2~5KiJ?R zIXSv{GHO$m#2;k>Tt^quKM#>3q$c}vWXWveckt@ffi%SqJ`CS@GLn2OQ7HOW2hzF9ub!Eu5TLY)e2wBx_B5JFC4tUH zwLe?!C0g2#;q%kcWU>SM5U|>@-%kl`+}S9jKY*3kvot|{h~BZ~2TC2SSAS9CiSNIc zX+!lU_m8Ox{>>(bNNXr>+hX`(Mhc5~I5Ic_fbp}*1ANp*k`GWr8EJuwa)!4$wxz^A zcR!P>>*GCpDVP3|;nnT+?QggDXVKA@P~-m0tTQ#O#XI4C?;dJBp?(w|?M%H;>?pOG z8=V+d9OO8UF$8twgt*OGet&|d=ZQ302&QJDWtUgaeqb%_84(dVdqgB=lBGR?M!5sF@K~T9K-*uEZ0?? zD-@r~HTp19l}81&VdgYIp&Ag5eyy6 zgr6d9i}eorgnj*-{(mf?_)fA6J121$P>ZPD(_|?Jv9AWPkAqMzf>*fgPw=1D_|LDf zD!)FumaFosT$MLTYoA|Femy?2-x=C;7hIKZLM$17f`6j%75tmv4B{vJ3vktIxdQmW zE7s;pEzA?WD%1Eil%TtdNA)t90Ynx6jF<8ecNQlcQx*CzA{vV3G2P zQs6O=r#@6v61AT8zrja#O?zm4H&GHlB@oW*DKHZ1@0D}(&TH*a(b%p*|s^#28zUzKS10J%>B`Eg}^ zU0YwjSzoeQOn))36eRuxDTHdV0I+3wNTbDbSBM;%ipbfg9R7J6i2~QEz`m%;KUIO> zi~?r592QNm{*ZmbL8n2?(kxEmNpcePdqLG9=i1J>;D*U{(VRi`u2B=3^Mq6`Hv3NG z&MB9FT1{N5LUYjs|FjDIMo{D&H;*Rx?%hu~!PR)}yMJM$V_ZjDy#i*^qG{3`M}A#y zyx2N@zmoSEj=^!9klm6{1X_*r&=^kKj=~)Wm$ea-8^IDZZV2v4R}5|{Zpjg3(bq;| ztC-vF>sA&&xf;%Ne2$9>k{LI`y~+yMrXAaUS5fHeqpxb$xzPaE0k!Rg(G6aGTiAg_ zY@yRu&wp##hphNw>nd-0NO9aJ{XxE9czRp;g86^`Z#jbj;&PKmr1ADHlME-$IotG+ zp#dh3%fSiH(R9I5s#Q-^5Xv+u1vJL;hq+v-Zdg7{llg<_iqyl=`lXOwL#$3614hnN z28{f<&VaF4XTX?C1IAn!F!D~SVM4BH>bRYhY=32~DmD>1n)J?3+UnE>{7GL%ND#__ z$BI=+ntYi_+r(85!ZvYr#?oQp>Xb4~TRL$S z=!N`pv4mqo+71{39@1o3_`jhK;81DF;EMtYJF25#KN!K!F)GYSoOL5z7*5K#JYK~* z7=Jm&I>`sNg>-S(&Xc5EP0?Kz&UST{7XXv%8}dKK01Qbouv19ehd=Nfn;K#NccU?h z(6kxFW%Bvjr+NksAk@5KzwSGmjVp80nlE>(`Em@x2sBkSvy40`n2%|%!<6q)37c!= zH)1Lxq42TJNNsxyHYunIOwi(hrn=1Q3`dWq6Y+Ls5osA_WXcM#z+g24j2J zZEp?h7R?27YsQ@;(q2|}lE&+YM<>!qZSzBQPF$NpCO`B`z1Mg!H;f1Wo}KEsy?@NE z8n#8pQ|3Y}HqV!E=io2rdP^}D0&TAO`qG}UIKZzkrzk`FEP|7FbhMM zujTPf_{hf6HBNUdZMSAD-)cFaH6<-?A4 z^vD&0#-^;Hx0QI0 zxFmlTEuSaK@eH3e$;K%+DSv1|<#(mjx%rKF^q=V6yD|@)ePp9pIL*m~a|Gjq;vitt zz={d>H|3=Xf*AMFzTtGzc2=kyc7GQ~jzgbfByy^w z76DtyJo_H-xfHZZ0EHrEL)$lux)HEQdWx3=wC3}FC&ANMj^FY;>ew*SElA;=w_PuNK)YDbuRGPdYF z$6@YzQn3RF(I~V-J!*d*N&>Qn3~pX!z5ktlOy5qce9_3fp2BMA(Nu=Ez_lj|a?`@w zWagi=>J}}(<&v8`!p8OPD*XEwTe2oQ8y4GUX?zp^vzA#pb?ihH%Izb2oL1!*nRVvTg|Y{b_eBr7O~FiJ={*{$9s!`r2Ak+H-NK z^@rQ;Q<^t#^$s-28ZXEU^E_)2)VOVMt=TahF1-B!^9A8ha?-9?A_hlQ{osZuVOw^zs=66sNH;|shE$yN<4 zNUtQK|Jz&S z7>QuKRJ&AMVaq{%k)NTjR;8byL_ip`aA&AIE(w39O0Ia&lrRZX->u}~8>HZlY|$%G zEJI6@NCDOu{68<}jg6CZdy`|IM?krmCOJBoJ>j6DN95&9_yCZJicMzeoJx+>Pd!+n zVdcrut_9FRkSf6MBPG6Zj|u~{DAY4Bns~y+=-Xl8z14ihkwteNGpIXs`)-w2@1t(> zDx-h>dSjPc`(#SfBWnR(h|F@$h=IFheSRJyT6o%S1I;EA}LlRr!>C zI=?GfF92P)EBaCTdPXEMFAK~4vs13iOSCMeN}*X27YM6ZP`p+d6%CINs?YJ}QHbgb zI-)o~$T;iC(Zmn?!*6snal6DoKJw_;;&OjZYAQ2@P2gG{b!T!U3w3jU(6u;Og=F}*IbS|bGqj*@vszNFkGbxS4Zb+ykXSVvUr;{ z4S+zk)k6a}UT1N$+JIt%9$mW7LX!MyWuA<2iAjTRwja z08gPI1b{W^+B;1hOvU$_E7XBe;iG+uTSr~*xyT65>)Z03n*nLr4^FWUI#ZIZwlbV? zS%stteJnMm(xh9PbPF}oX1fTwt(Pd)r}9P7ZA27Zp;!Ic?zG7IV7&F)e2d)Byf-iL z?QI>Ci+KfXACE#Vu*sNH8j70B4;OzvV4DWlf2AxHvZN;aPx3M@wY@})g$!M@7sqrt zt=ATC_#*YL>^U5YhV|^O%`fsw&3tk^c4ZpYAmr29zz9dzsHaeX(=bVvievC=;8n_x zbtAXLR4UxM%GTp`s&{eV7WZi0WproDuaBs$Z6Bj;G;9UeJryfRds;<5)&YN$LsGx! zDz>dUZ``ba6ZdMWB<}HwJC(yNEnSb@OiROX;KW#1qdY661o}Z-Za+ak5ejo&ZMR@i zXqPKt3I%$yzENtF%B>S)9HP2g(X)zL-cR{trsI_LAt{7}*G2j^;c?J8SLOp!qeZRO z+w8iBS!us1<16l<^weNp`DuUI0>wT>Thb}3NWezyz7*yIDMIOe1EfZ1(Ij}Z@b{tT zc!(H!9ll9g0*e@<^;ladP7|NBNER~afN`FrN4|_v*3wN9_mA$28U7_(G1XlZc`g0_ zIck*!9Cc%bryVaWq?IH^2*h-Kt_T6h{vw8dSZ+{6)E042kO7S^At8S~%F&=JmXnM| z_$|NUtgXZWl>q{)@zl~9i>J`c1b9-UR)c-u}v zGmw(HmnmE05q*$NRh(T{T(qkTz*&2SWm+z|VM~T_rK$=XcrvxWG@o;d_$}7yVt|tG*sbpV< zh-a&6m8Bw`*H55VKtt8YFWbTT@|b30;ciIzvECVu5#dX=MI`QPeOp)3t0nd0W*lii z4m?2gK+pwWj5|T}KK{L2ncABpTP$nx6h^cjkI1VbNsBlL%xQn0z@Bp?(oV3N>?nAe z7RZgE+`O3eQXq@PqV(YZ>ui1&jAWSw46>_`2;ZZIBtrB^%{;l%VT0j%m{s&>ef;?1 z`I{$ip8ojHlQ+-*-(Q};H8ntf%mFV~(~Ijjd}*>WNDNm?ot|f-T>DV*3`Ho2D}f!; zEI5{tov|!fgi3#TV;65kwhaM2wGiMuafb!$Lv;YihiNQ;xLt+)tr&LJA**Up-!A2d zT5UK?hQ}hH2DX8|o$lU*uSkDiw%ZaW7{_}nUS4r+#Uo!&ywUWdupU=&%ggv_$>9b%A_5hmK`k<#zL|Hob=Yv4h_P%&v28W?{vir1Psbu|b#goUSo1@p$@ z9FDz9>~r1FU{DH*R%paBfW54_A_H)Lz|_30{TcI0jQL;+y^7#O9h>Cw4^?^jlsYDp_MBbG3`BW!JYWS-ha0KH z)o<)U^6=cc0pNlbxJ(Z%!LrVJu$1|<38XK#1yCdZ7!70m`%m=3)JXqKljE_3;ESp( zo&kT_19VrsomS<1{&QMDg^J!lq!Qsz<@5tgQ965ZuK&4}TTi^H`a*{N!)Y&S?h#wh z+S&@XGK)QgvFVaV5#k&=L3*o`CTbUH{S?l8HsgNeBoH^aC4MR9P8q;mb2N4XLYg(# zcBHlgc{7~7tFjD(G^B8w6Hyz_kM#UiOVoe7^5r44Ws2@1G3x8$Zw(;}K7J5u{~V8# zAz<90V$b}GA-Mz0<&Ityh-{~};)YvUjH9LJ3az8d;V zidXUmU&23osPVQifN|*v%UjjG#Lx2D~8Z2<7zCTrOAU7Y2F&?^m!uj)r>ri4B%?0uHOP4K@ImO^LZFx zUG%ISiD+kYNtf^LmZbJkSZ&NP=otzHpXG=)Pw}PTC176M^&Dul&<#|YgY=&J0>h1( zV>V;}wYwvPpPZIKoZ&G_?y35sT+V-I!ZQ?aEs9eUQMh|fe#Y*j=*DIV>AP|X3sTDf zqfRoq7X~zgcSADgzcGLRQ+)q(B{pn(Gq;(=Q}mqA1;xaYt_*3H+yxO+g-=PETiSZT zr;AKE-SYH8lC3q>FH5*8e`g!dUj6(tHp7)Zznun`gdWU(XB&u1*Eg|>J28Khxu~im zpK{~fK<{ng?$Gx(^LY#4ZR+l8(A~^d%iurMqjscT2jx{|DLluK`+42r4tehe&Xj2s z5*sE3ka;{1sKC7-W?>ZPw7dZ36DZc5x$Q@LXQ(@3041^~o8T(lo?aAZr4F*X7I+=z@ul7;lN4(C7=s*grqlWniY6?rg@r^2oEG%0_M=8uL))qqF; zu3S7Gj*PEI^7oG!$MqOY=Q%)~KSzyG_owWvF)HJys{MD2%%LjNm*LMnwl`PHz0n+9 z&Ei%XHF8h!j*{R1qyS$5=LO{2S_f(;g!IhFnu|t~elsvKW0C$(JN=)!rSvMVohJ54 zw2v7_I2NE7=D?JxmJWY>*{VVBonH9vbk(1@-1~*N+*MEFmKPcSl>2Hl=9_MtDlxlG1;sv^e+IxyWa;tXRS1 zsPjj&{3A`uZUu%Z63om1-xE+Q5Desm`ZDYBqdnQ%qdjO8{xv4Lfjzgx95#4;G7jX> zkuo+UMTvn=2O)3TF=^29s3xhSjrHzacB zPcS^qh#yKs>`Q;vm=QsHs_YD5^{#x<2x@w?GhD?oyiIp?3WL*88wR4s$)Xj}21{*8 zs9$I6i=4r=UGu%^^~!vb7OSXPn}i5K;LCwH zR~@_=g%HCwPmFbSplIX>mctzYiXYOM$EHwSYRW^HzNu6a?AE+_C8T9PQ_QCoMI8xOm4Zr+Qdb!OKLTKMX0k6WBJ)1Ap11KOb!h?`kF*pLTi->B}8 z+UI{yyDPCV^}Tz7J#+90YdiYuL|s^aowHwVi>jEqu5zLksi3rQ6AA}fD9bCIsU;IA zt^_$@eSdWp)r?#FhTBvL3rUo0^rarJyQ3?*w0WSKY8__0)g02HlN2Ky8n`?WbAL;vRjQG6@w{aEhaf zWMp~t_YwG1DnOu@56BH#m+_*NB+I7VB1XCq7zPwv{DxnFwfNVQOV7OTU>t| zdO#hQ#wxNT2YXs{;D6IUOJ`X>FaEGc{akA9=XZM4&&)^t;kJ+Zx$@H_9`IKiG#F^M z<+Aw?)?pZ~*I~HRbQdX*1AW*T%l>&)eiCta|E$Tw`OlF>{2pmw82M8Y8@|E$QmBvt zcYhZcag9;2cd#2Qt`JT#g}?6rsd#@aWGD#nzYQ?d`(5Bd6-=;*gg*`{^+lBzAO45J z#eqs605$x>LGzz+m@t6q!U9o^@lG<5;wt6IJZ^`*3WmL5V9$EfF(eqqs-;n&;)ne+ z97g8x6g#t{Z2w@mxQad#(K*6!V_{C0O;c_bh_`BV3&m%7O`fjufJKx~^7Vq>$0i=GjM3HS?tbTrL%y{(I8_l;&nQh(p+3cFr?(ZEqX&nme$7G;h)0`0dGQ7-7g%$sxp z3FZ0uJTo3q+V3W^PW#=&+Bej`ersYPHVJNS_}6L`7&2x7f?zUn1k-=V$3f456BM$9 z*;r(DU8I-!RF@D5s({f|N%CyFFP3pzo#|q>XdDX_WxpgMdx7~y$rSejvv78XET8c; zmhvzpSe_UWZv`KVB04u>&rttbRD}hIC+G_!UC2L?tMHVU3(Jt41qoUdwDg(#!?JDp z3zim4cb{9vQR3LJyvTnc#^O~$f*yiG(WlM8P`d83j0V^L=J_FE2j z{@{K*`0g+s9LD=m5TjQ~UG3eApP!%n{C4zUI9$a_C!|Fth}(at>-});GoksoXutpQ zvFmv7ikX2$<9YeEgwIc=h!`&EfAbTfd8X}om_y@JD)L2;dv1KFXQ_v>WpQ=J-Hx|< zc!U9?%!+nvXCXRMuL?4PK%4Qda+_XKyPhQ{H*E6eVceOH8r_ijEL=0gScS_wfV~Rj z8CTuN{H$qc|8Rd8XK9Vu*}#94x;tu<%xA`Fai;i9osg(ZiMf9S-y}Mp&#G{xFK&`Q=*62! ztd0lNBQ2ejPbrvzGDVVYy0s+HE@X&bs+&+%W1(mJ=|>e5ozdfro_gCt!9-$GWa(5@ zxT{s!L2g2_Y1H>WE@x<%uRPW}dqqozjo6|rT+OR26emED2J-cgZNH^8wYR8vjpImx z1mE_^qMLuNdw1a0@=ILf%#rUpAn_AQ9^KMRsG0%iZ)t-Trk-gmmR(Tqs50o3MZR}j zY86b>{U;xWj&3^e)Y4(k2OvXG>D1@_lY?%CS(nI}PGycZ zZCTT=Kc4pxt0nG2A$kKcF#ip*4jus22PJ`$%+7!6$B)j5R!l3rKd?fd7?H=1aGpKL z`T5)4{&0TSga7)6zahnlIq>HkheA#4QFD9lSUN-r)~Jx}t$_I^hO6FEv|zRHyM+tH z_Y+4nLaJ=s=cwYy==Cq)DwxxCz??P7ScKbwt;-4aIY-0KzI?tA-y>IXZ~W;ZZ-CWU zq|<+FRFr7k$RkwcviUq;)Oii;>=j>&c>8@dJ}s~MtVuHxG6F~$JFKnC(_5hQiA~m( zH)J8AVgcQJ0N$Jn!+u@NGqnx{ETS&hpQoqUybn`K8@C2>=nsKGZzb-dyaQKhG#m@o z9%Vum3B`tMCnuVTRxK7n4N&Y5A#B>voRohw8H>9fSX4wvjqsG*+*JzYG&jZGm8int z(<}tB`3U|&hpv|w)A@3iZF9^Yt6{@FFM3F=B!S@PFb@(D0XgeL6S!1paVv?(ma>zL zf)S6rgFpmS3nJ>jS!_~Rlt1C-{sH%PMD2^^rKaWK+D1qrB?OX%x9KjG4P=vD;F*6& z@Jp0fnqp9VNpMG5hhGk%WnSjVr7OeyG>%MjT(9K=I}TB=M7_D(S2&=4cY! z2}h!oF_CAT&0fInq&2`?*rwAA9sFk%mN7mtH$z`0ec{Rk^C{tH{$?T2r}VmJOvPA> z!Gfu@#Hu43tMPj3oq#1&^|`2HT;YE`{KtEIM(*|8tCweI`4xb~ymu9#3A9q8^Ls=> ztuy%fTHij*(wPA#up>Qmh5k|Mxk)F(RYAT=eJ!k62AJ&g$t%s5N%6 z@o>+amO#r%P9C4Iq$34r2g#~#Cg0M5H}|Vp-q1`#?RB3{w;3$Q;v>ft`oU~M+3{x0 zTO&ENOGST%#K6n>e>u<;!$yB*-zd5*yYHDOvzddO=jmL?TeKiTm+=PjveJLeYE%wI zw!E0}dviJ$h5=b5toIGJne{B&i|_G35Cr2U$&RuCA)wn^c7-bUWEfC#5Mc&h+Cs*F z{PHD+dJa^0^5$=NbhO(pXO?utWz^9QI?88`pB?odj2^Oa9AR}->gj(n+t*^B9ig2{ z(c`2TNPgbCC;Vp6XVOo8FDSph6mls*lSoAXbU;k??nEE~mo@(N+Hx9l}!`iqO1WQ;7Yc6tlgM)8A zlkvO*zquuwxt_c9NP}!9|KZ}QH#FCytvlhUMXc4B=-9Y!#SMR|CXuayAh@jdg6a3( zyBcS9ys35)OU$ku`R8A;lhbverqbhPJw*yhG5kRAUH?ent2E5#8nP zqLu~v_cS~@&CUSL+wF_QyWL#It+dT0w#Ch54!zGq`doi;CpPu_j;ed>cu;}=)^UNT z*-~Q{h`ecrFlp@$`*3=oWPrtY9keb25Ar7n`H+7LQ68_hq)}@`xczBb%~%|DA_bDh zI&hhKd5O2;=7$`@iYu;kn$FI%88~WX+cSzup{CWJQWvaYRH!SQp0h?l&mxSaOz}oZp}_*FO)qG zU{T=+J>!|sIQ0G9sTig@B#k-dwV~ljNM8 zdiH|Zq9ZL(vr8$t8oR?ZR$}A{FVc9RtRJ5TG_J@UNQ-mDP~ZW_GL^>n1GS8`5v?6z zGr)1@Q}Q#5yg3Xk zdY6AW4pDk@}(WI!(e{$&WQe8Fl>x@=nA%F2c$gOKg#yPeWS>(tT)QOR;elx#`%#S7qY*(T5zUMPZOj{)MR(h zk60{Rn$M_g!akib9J`r35_&3B1JAQE$!z=gutpae(m2`!A#BmFHTXHr7$6+F4P1ZG zLU^d&lE9M1x4i8In{hBi{e2JN>v%odlFRr)j^e4*#&9_zSwQP+cx*8|?ZkwefsBK# zfy*HzCmxkxpnp`iGh^RhgjXMM=N z?%y8{r3T1=G+%Obg%I^xv+Ndx~&z`on zN57=k5n>T~%wUbzX0}=w)qohYe}uzm>`VKU&*%Lq(OY`kwI;CffN^{U&*^{4ivA%U zFAoGZwW(_Oo88x^)(~v;?mToQ;pm*hHBZ6Q*WD+jdRsR;3nc;aw|Z?)(Z-24otuc> zi#vCbl`Gp&+C3wy4`3Rro_BtBvFY^`W~8vlK^^r_(1UiY1-g!DvhknZ!)Jm~UM*5G z6a?L;lY!O=Svz|ZXwe6HgXMq!T=@R6-K_p1r=_B=_FC(CGyLkN6F==oJ!Jv>j}TAy zSJCQl`xGnFbt{&Us>)>HL_~sxU-2XNne77I1aD+E&(5*AL%@C3+vKCB( zZx0K6e}3W%;*V_Lc>6ixCKPUh5JLddxt5NouYasP*L*2~{Ox8Ci75 zFORxQ>vmB2t@p`|Qay*!?0TfMt( z5Y5@gnY6iaHVle@)P7g%X4pJcEMdW?aXt*eZnX!;WH*R{v5Y6AT1aFHW6*ah9fLJq z!kM5Y_bEn+zuN&QfgS2-d0~rQtmsoj!(ckuW(O3=&!;&uZw?Q7KKNS$`?d)xJL&%W0EY<5qUBChwr`YyJZLnnLP?r3Fe zB`6())fe2BmaVqswubVE5RD?{3h%?l>h1PC;}SyBgT;SU6j=|qRB!)qi+X##168%s zeQsU1H7}t`*gf$&ASXK4^KT!KBpy?o%yi)i6s>>v9fQiTvUytGA)Ff|I6i zt|WYbZ;+3jMm5B?2AG!5v6yVI|FG_%jF5X6unOy7R;B0Xo*s?rWd3JXB8kFakrz?- zI3M0|alU^Cwnpkb(aH|Zy|&g7stTsccK~p~qk%DNnXJj29UBL!e`J z6lQ=GaRjUdInT3zXJu@IL_R=Qu3TCZ7ec`jAl`#blFxdd*@C$>iOljJM1Sg5uWNBj z`hqLTF>hYSIVnKiVt#hHX{3vXj znzUM*x9^>Ihq;T}n~yr>L%ChITP~a_E)MsOyYad)~jvw_idAP$yEg+HHTY z*P2p_oMZPH47)>|r0W%BgA^P;sVDzG@QSqj$9x{K)_zKHwobrG*Zfw3F);D#CB8p}5^`~G zfl_s%bK_oqd6~^}sgWln?tBj8o}_<#!|BG>bV#WpL|YspM1cNt&0{xbAD zfME`LY&%XTEn!h7jtVQmA1r^FUeQ;2gmuy>c5tAGe@(Yt^|qzQ^5D`ujNcPj09F$;XX(m@tW`lWYG zop-!Vkd}MbGz}onFBEALe+9w`OhNB#?d`R0E86fD&2?`3H5(PZ_0r=2K#G4*7G-?M zuEjb~2mh5_Kcls9-Ms`Y2P}Waf^TDnBOU=h%cbo)7btASQ+(jaCf@4n9#8SCZ1jk` ziWhmYMXBwcV*Q?WTXnUM61mN~oy%HFeisFM)b9UTNR~Dk0th~}^BvcCI+mz@tHSy= z*sA!YHQXmvm0nwu<64trzwrR#0gnLzUterT5Cinpi3? zj8gdCFFsp}_J;PpU)g_Gn-pL)%g=77Z?}jM z`jBiB?W2})0oit#o2P#T0(LgalVw~?Pp;mb)pqqNj@6OTl&aac8y&e2x4R&xDSnL8 zj(FO7=JqdPs~paZv4*{fhIg3flpXF2MPxU=_&WljKFcsh<`hF`>ShDO?nAb=PkIW* zQrY%bviJ)NB%y1nqdu_(71s|fcw9>pua${|7b2+(VhDp-YXg4|vQ~5;+2L;Rm2fvB z*Zkf+(F@W##35WIy1dYl_#}d&NNyC^onegswYpYTNaox+t)?uqsIME-LP(HhR@M4b z-b^n-kDXgAgQ5%TzME-UX8}zo7%44`>4&$N5+c(CsQts3N>PSY>K{%C!fQr85Q1o0 z7aMJQHo~XGf)0Oh4FmRsNKxy>Qg;LU#9WM%tJuFvqLj!5&|M{cUq*T*D}3_gTbsv; zFDf^Rc^;mLG;zKdXl)S4s={}Wvjm2{nX+}GfjZW1G%LNpGkm78fG4K1n~gbI(U#|p zo7fXji45f*=lte8);ihdIDTzIS+Cc)b%bh;N8~?860Co)*E;Ou+9!UzZZT!t)%EMS zuLGG1oGI;5R`MdkSS@czy&f|m&?99trF<(6=4V`fA#skSK8X8n|!a3C=bBVK+}{EXeT)2gb5XL zdg>RyzFZ0q!j>l-Q?_+uzL=3)Dxa$}trmaOQ(CD#)&mNpcb2?3D?6RDz)<@$^60_N zq}jWN>9qx`OL-&@ZLOS9NDgn6H|H;ClOt2uX}@wf(}KokX?M_QMk0(GiPSVd;p4ItXIn@I?3dr~6~+#z1s=pCEqw zw_zaGfTt)R;!%EoaJXV1czxkb%gak>mg8^uJ#t}y_XDls)`m9mewWZh9e=8a{U*0g; zevDIa33>)#=y7OAP39fsW5dcl)@uenrYjFwF>J}#Fah*jfljTIy_<5;X@UkDbK3Vj zoo$QYU;p&vpD$ki_2lilC-0uW9ZiA-4+PHsB1NhtIF6l5z&d%>CGffauGD|@or(fx zGjdjrzh20K+Phd_y!{|FY;Reu7T%g=%~q7#EY*kCZx&)X8)hxHS%8T9`>Hu(!7a|s za6N5b4Fp}lp2$0Ji^40L;n>TZuQ^I$)`j<<;Br~#QzqhD?F+S}6NHto{}fiVZbm`# zvA!rjZPQ4Y`HH|*13QDK6MKL2)4IfHVC}4Ik=Nxc1JXC!O@iLrV7IgWZt&cjE1HM^ z+9N9)P}2#9v^w&pj)GvzO9+D&0IPhlL<`{m#9PR{HhgiQgeAklxBqUk*@JJzyEp7K z6|IM&nOx3t5xr!&(8O<9{9Jy_epz6I1S_bEI@&)h=ksd{is(8h+-rYzwZ3~x;RNVj#Yv~ zw<%tAUt?P|W$dgL^}Ug%b|4p^kog2dXx?l?K3iMi59*vY+qT_{o&Ec}rX#aeuiIhY zdYTp4zg7AmBE4B5*I0ivtf*{3V4EJL4~J~Hb6;!o-v9PLLuMh6lSD%@kk?y+^y4Q_ z-@W)7&^!{wIN0U$@CvWupN3DYDQx0B$gVG2!1z(612=b*3=LIj8kXsk9pfzzBspC&$uS`9E zdOMV=`PfO^lkLVmEowiG&4RW%6Dl()P+$|kG^o$W5mH4|$X`esik~^-fQ0bj%4TMe2X_DFEUf3_4V_BXl!p%3l@$k*6ufnO^rf3q@rsBKp#+iS+f+nr*@9hdze!TZ+Xwb(+cFZt-UZP%3Tw^AY)2eCYl0jiNMBNBW8J2cYHQr( zrfGknfKBL@L1Nlbtzu(Ay2xpI6zq6J%UDar(>@R{8URG<;@O6Q)yAd0h9Y_6^6xl-x;dd|ZdZf{}Nm5Mp4M z;4SJH?;luWry0N!X7hXZIIxQW?%P|hy##+gXi}9U@t0yRD|y1mZARs+(t{+`X@z-GN4;h!BFkLsYUYwi%*b+hcUM1=UR?1>>$d%Y)h z4d*8U;&INZ!)V4I)Y?hA$vp%4O9#hbkTy-mY;S)R@_Km>m!rbAcn z?35lL=1Q&(^{=}$?y}G89X8onelC1#?X{S>$4^B*j>87+UFU5$rEPcf80n=gEmtv}#2FH!1s-2?i$hXsvA-EdIw&iFB(WiRwD6sMof zoJg0chiHa9ftWLaebgfy@YsKv+2A_8oQsp)ON*J*OMs>_QQpMtvZPw9!5>I)wXcwk zL{}Ries*`b+HifD8gO^v`J5Qz$i)JS*;^}4OBO?i3zup)APZiIJbWsA2RDODoXLI z(~+`m<>Q^ioHChWV9!i->w`kRHn)Uz+ zy6@w{eJchsHvrZ4vUgaw;UvJ(ng<5*0W8r1TyH>HmD>oIjZqH{6eUXY9rhkb0=FvX z^}1(A159J(@e`rnwedvmy1l0XrpM50@o&O~+k*#M^#Fgb+68|-QNks%kn7#9rJecQ zc%zI9k7(Tu$S4KU)g0_09TrlK*h$?EwHX_#?VQzIkWJgIbIiu%w>&~EfJCcFUVO~! z9Q`rf(2snMP~^MCh>+QS=rRRw-_r({_MLo9D6K||<@t!(ws;xyPmg>`9FnyU3g%le z9wjWSdX?}oHSB-QY*z*Uw6dRK9Ttr?wve9qp7u8Ax-KwcgO-+E0P!`?m=k}^S!xB? z0>K#Qa)IElNjc)|%oLqv^D_~IL3>o|P#~o_t<{J9kT3dZ9-WuVdW+$_7DdU01rEy8 zan;51T8!DAx~#pmx~-V9SkyoaT$^P;hq(Qp@>0EkTzh{5>Iu<&wyt)!S=r5fj#e_R zLP16;8&}{Z6#G2rza%r1HHyaThBzymy8jXB^l3VGTSk4~pM`Ynv-auT-=Dqo{8j?o zwjqmxoNdzBvO;++%NZ6ZhT)@XMw>k@+B`xV|LU) z;FpETwSK$WOn&VeegpU!1=w0>8(Fp8jpc1e8EJnV>5;rf6R{({q3|aFUD-yAs0!JT zF&JSQqA0Y3%cBjNfHN2U*K2^ViZ%M#PEk-}vO*#|bPLeNHQP1!xgz~8g+z3O8+%dz zU8hGF4BH@p>p9AM@~-VI%-dNvXHXn>ZL!f+!%fcvfO%`uv1m@n2_oDSfAmYiT9Lk` zCT)K_7dY%*x4ho>^lMSF6rrmS3JpP9mTz98j+QUn)R%c(ldaLS7jJ)l@%C*?$@g=G zYHI=inRg<0^-~Fu>GELd=#dE63FsJRvSw4LN7C^w+M!yAr4SQYY``j>Lvyb3v2-HU za>r*3AlVsO4|SPY8dkD~IS#U_DyuN~IiG)4C3?>!SCOC68g7K9H>Y+6a7~ScM;b(` zKf=Wu22y->Y1Zuj3fQK(oCk43@c?Z=d0qu`bS;GrB*th*nM;KuweuiPyzp853>Ikx zSVVa{$Ok#zjz7Np`KP3Se$nUWb;}+|0}>I2-lh(w@VihVB+4rzQ7BwkS(DkwbF+W) zli2$G=|x%AuAv&{JWHFj%j1O1Hv(QdNqRDAbd%mqRq4kk{a`m8Oocv@t%9^0kP=f6 zr<<@NAw3%n8jmKwTaooFoi-);srIR*J5pyum}$PJvUKhtOE+idS$(5LP!A8kYR4@= zd^}%XBDTHiONafY$Nv76l2ilh%-Vk(mI_e&Nfo>Q3tf!8&}lZh;n~>Z9d@-1WUr^8 z@4mx^+5`UvdsumudXW-a|58hfD}dH^%Z{nr)K~ALuwC%C)X>~>zx7^$9B!X)GS*P> zjL5GAY*g*=Lf%$uwscAQrX*yuP7x1^K~JMf>H5S^S5|~+J=)p(+cix!RGWWz^I0vb zEfT1EUm_cNP3yyvPvsap0cKX)#*zCSwETAr#2f%MNI^WOO)!?}y~eyF@vW~sDkshH z=>Bk}%4E7sKuRvc9T;&Fw%t4jS8y&?z-~VwvB%hLMLH@8AYee$>F7ahvfQKlaR=dn zo|ntyMn6K9)u?|k>@42jKV*MBD)=>wQ}{P#-+9$&7}v`J*xFf!RzvJK5<^17={&72 zPlSx_WRVux90pWp1*#4n#&E-@)%A(^{$@4y-8S`BkB={mC(BMnTd}8oJo$m6IHsfy zIOe~xu*Yt96@SdLPaRn_@OApU3_4VwtwS*^sc{H@oe9M~a~N^Y=oo)VHKQ$CMLS)HM2(K6`1BO!r7?iRMFHN0zFUK~UMlEHSupbfb<& zZjqR-pF_>IhK=I&@Q__Zt4N=(R0|)5z*tn_BFV;!N7sXSR-88%)b`zMG?qo^xEV(0$5S_=%@o*#{b1mxhN?C;UuhiNLD}Q4Nqd!}Zvi5%~bapXx~u zmPp*PH%wAQjLCl`bmgP{;nA5OM`*eOX7XiPoQHGtP7C$a$TC#-XIG|CG}6~uaU_cx zC4F0n&*>CSQx%QkmeL^1744&lLj~v^eeuH&Ke#FYOJvm0(YYq;Tf;#@5ACAM z`J8x)>^y%j$$2ALH%ZGqU9Fd$ok8b78-79x>HGAou+FjSBO8Kf}z7peNg5vM{@6YOXBt%y&fHj}pO%M1l1*qbB0!dn$Cd|>#9 z$U+yr+HhgrOHY-ZRat!zv|oDPa1M*T+0-Jhf5?BD=>_{hbDxFY$d;G~4tz-BvA96jsAjV})3o@BXv_96IQ{87H^W2O&rjAuA!vcCnSMYItyU)lQ*lxa zgMQqTJ>EhoJvCyeD|P@$&|{c6~SxEy7VqBNX$<-2l$Tf^K2xC>PMZFno1wmo07+b^ZvhQ_4F9Rpd%15*nIe}XGS zof0(gOFjQ~7Tr^2w|3jm)s{6S!<^CJ;tU{qlU3}oJ&iuZbTF<=-MJMI;A{7g%<8Lw zCjnOKi^pTlQ+@l&nX-!Nx8+Pcc-Q&*#JoMX{KBL8-Bj!*WnC_ zUKZWx0#Bu=nm2ifZ(b;FU~kot=JJs}S2MVGuUKJJvGyssD=0RO$6}Q*f{l6M*(w)z z?96NkL9UPgU7AIhLlxPmi9~#3xV?W30s)gYibflZleC*fzzvp!_xj!7n-@yOAga7Rx42Q>q z%XDE)o~54hi7kb*{4>fu!ytsyC!Py>!ET08ZlXXj_63F&`bl^h2s;K9#f5((y~0op z(+kf`aky=!6~>o|kT)CqS}7F(GueGGm2n}kJJ+tzI?uZWLjx8l9?Gaf@;+0cjrQZL zAF{P3V|*dzllYx1BQ;Uj9?dW=l#c}fXh4_0y0nCe_(0Smi<{VZ=0$O26X$xMdr2Pm zo3FF4!>oT0?G}K7L_8G$MtKRme3ig|%Ex8$4EETdC_jZ5dZv&)l#4h`vVJY6$UF2& zkzW!jcrkPLRaJv&@>i!TMAm2D8oIWB$*qjq zM=O)@Nr)6Xx8ewzyijy|TP)}ESj2@gFa>9f3$%R|75^}vI|Fcg^=x3}yJ6@`J$ zTf6@)`7!Dv+&E?tKGWOL7sG>phYz3ak2{EWkeN>LnNbUPzI!+v8a;U}61ZmD+r{XMgMIvO?DvT_`r_LI{0}E8HRa9R z(;5CpxgT>CW*jE+4gROaQ>Mv#A5i7=Lxur3K4`FfcbXk;C@(+nQbh}Y!kqH0z~+9N zg$_nO^0x!mFLL&d$a=(CDKp!hs!U&}-?p%bGF%%BSAUTL=?~fU8D5NaPhjPn;a4{@ zQwxQw5-C=EknyCg&|t8;(*48lX4$!?rpQ>W{_-C&ih=A~wvkGI(qSZWdIxnc2R^5O zE4YVV57KJ9O(8=;rlsN}^puSE2<0!}^ZY_6=|yY^Xm7T&!7{ZzA6Q0yE2FWVEmz;2 zXqo!4^$2l*mTMRL-r-Vl!>MMMh^6BhhebUIs`*DJ9>Lw-bM*7ggr(NUO7RPyVhoF} z0eP0eXkuA)Bi&tpz@A$Pid&{(M9>g3di~xv00GBys398{!=D+3N=6@l?psJ(d@n}O zw%ihGOZ;cOJbH(;KvBRT-zvnFvLLm}9$#Pd&a-FbY%6+p+kcMVq+W_|akMLXqp(Yz z2%U+FZ3G4FcM4-M9xJz_pCR{icINLB02%^3))3Jl+=C5&!ol@bw-+3GDoapxiVV$F z>MX?iM|YEf6aX|-chfyt(9q4gb9XjtNHC`OIRQvrkj`kt%9 zoYB)ZTJ4x}z366pm046(#v#U}n@(FO*cHtxomUFk5-1mbN=-xT14DeE*Vhsw2$pqhw zrygGN>-)q8@ASigKr{_%MM|PLP`t{!z~;g3dVYD z(R&Q1775rJ+^=Kh!r^{xT99`aQl#{_O;f74-T%ak9{qBG%s$oJNE zKfAw*n^xR6(FBqMiq@TzzP^e^y|EQ#!J`~$_GESvw+-q$F%kt1ooN~yNTL$-@F z<+gi!NYh%k?H+r#ZTH#kncGtm+4#|%&*ZY&t z>1xr5|0Z(%4OF~nmMvG=6fZ842%Wh9hSM^e{f!{L;3y+w({nPnf)OH+dE6R;b1&Iyf>)7CYhwNMw)SnIhme+93EajSuDt1ch#51Wk%^D2#AgVFSN9axTHaf z$75u0aT?KHG^SO7XuISLNe{5l-!$v8Nm7Q{izjjSD8RlyzCX_*F`2KU_>Xt1yMM&z zx{yWzUSUauAN(%0V{I@s@)^`OQX@hJY?y3+9yg~v zG@HbmDHe->Bbgm69swL#B$2VUFKNNsMCt6zb3p`(#=~}b{uZWXwt7P!I^CiFNd!Zq zAl+O(5M>qzUgA~j`JjEQae+M{tGSa!hLyMHkTrFWI@s9**~?6_DB;^AGY&W%P*XAA z;)Ev*M_xyPDjPFbfuzx(z_9Uu>mf<3HPHpNEmMr|NU9s;D8~S|7$_W5sS_}gmx$&? z@xWKcm;@$>1T+PGq7%CtQ>-g+1DbgiusT^&hbz6U>BkQCLdW83vT^46k~Uc+tk=7a@XwEU{UH@l~*8T8MZ>Pp*YD*}YI0Z12Ht((c=AI^I4@f9~SoZg(DE!Gh zkXc@XRL5>d+h7-O?Aeml9}WFoE_gCDU4cy+l^X)|^tF-xDl1E%-8?6v#5S|wSVa)Ld;-pGH@O+4Shv=$fa1+KV zows{nJfXZ2sh)l$(F9+AwdVY=*hTn<)xxy0?`Pc$*vSMRx4sT)9O6h7{FVeuKQE{M zmR-B$IMpoi*StFa5l%O=72fq?`e!2rmb%$nO>>^`KGVs?DgPgl`lO*eUCc zCVo)RR9(Y2F}<5OR6|NIwWd0gIiDNZVpU*rkhXF0Yznpq5E5^HDMBs*w5~3!@i#OQ zm2d1PefhH9Bz`hGH({0IoVF8Vfmn@5WzIHBpZE!W zc3hl{a*VDF;!+6)+}5pxjt~(&@$Q^K!h<;k;W2FTV%at^th2S2#8m8(+=s#t@3znv zsX_dWn~rJ87P)zUvc(W1h%V1lypp7iv}|dY5k=+3*49EJPS|)(k0^wMHo#c67FoS( zo~gf_4_R!~px8yt5ETu`hUydBWqJx3sdxDO%n#7RjR_0I-b5m7tXrf);y-P$Pv`Rb;pO%wh%P^b0#^L?)0G*vq-sum!@ z!G)`#8JbN;+g^tUj7L(@3RsTU1 zS#|uxAGV%{f6`$t5ARQ7U-N!#?-U+?k2z!3L%O^nseb! zLGO$yBcT>`Qdqd`46j%>Nv*RRe||ovn2rtM)(rXt^H6k%zt{$9y7oFI&~FTe*B;@r z1;&@oMj4H(d?F9UI zAgHo`O6%njDqNhNf*g546Z%nI-TXOJxxdyLDZJZEz~k{23V>lI!#%cHb8PGE zo8l1#-GpBkGie_*3rIVozif%3gO+02;MhGf-@TD;)^#__yJ2_kR1sZ%4n~ z`K|xk__y9~$qB}Wo5bC_@dVmHlW#sYCmcQ($WM*&%VBvSY|H_FIOyb-6-V*?RSJRx z1zEQ%2+bmPqd)vX*7%1%L?D@=vxo?tmyuvRq3q~!1K~D*s>Unm z4+Aa6(4kzhz$X!5IlKuot`fH6AA6rpgxtNHPGMnq(+=dMKts&Cn^BGzc2$952vM+% zW{0uau{dF3QNF1ftifbl$5}C@3#C3^+X~4|n=TZHyU<{{nvP-I`k?g_myNew#B77& z5!>2Sr7*Ep*Zk}T2Srx;<%`OHftHg|Wo(cb1`VY5kEGjoaJD1{WG31OdEfD&HjQjX zeR>nJ(|so?&SE!*B1%3XqR#pf*_(P&e6r-kxMVmKu_eyvWs@}c!De*1q#JIY0zpJf zav)P4Wta16Om80bbm*7!e46o_D}7D4y;1R}*(f7sMzs@mlc)+kTGf(&_8$tO1TmJY z*%=!^EAfQGCb6WHKMKsEFJE-^N40GY9YP802+mO(>yR8egwVzD-%+gegnX#=j|mAM z``S)R7=^uVFj=M{$7Ig zKaxEfTe4|m^(s^;lqJG{ar?m282r|UFmADFQObt{I}I7c;*1d*7(yf0=_V*jAOE? z#0}?6O#*tCCbJM0tvuqaQhb@aVgO$G!^~zvKoa;~&Kt^%B7r)8QDR3aZenkX&z<8c znPKJ=Mnub!6n{b5-19ZllQ&_DAiC6jQvrnYyFm&K-oW zD>QsvMj~9?wTpTDDK9TEUK0ncdoSjIATQlo0O5Y$qmdvX20W#A$sHbl>ARHVpj$Ki zT)gRS%lM9h9f$LO;nkDMwW_w}BB!!$vY>9@L*Y)*7{ryIQ|FY3F@c$zG=ZL!SRNHe zQ5A^rT-Upx+pMgjA33^R(_h`Yhw$OC%tpY{s)A%T9wbHZu{o*1FmLAUM^>-{qk~46 zj)_PMEGGm?m?@{EIrdB#}3~-pyvV&y#)aBvByASSvJ4>YO8LrVC0+!Lw=>Ay_3k8P=8p@Na9Q`hf)kRtj z;}^q=3y>Y%?q;X6wY{n(r&khJ5Y8T>^ieiSfnWjMm@2G+l>_<8SG5EH{u4-Ygo>4+ zh4b=t`nrREp)m6DCwj^SKaTh$z1b>?Zz6^<2Bx*&5F85pJI|gGo@=es%-#Y#A*!D* z&S(Ukc-;SpqFoF+3Fb>FgpqMcX$WMeUFE{3J1bA2M$%S^`7>l<>VzVQtGYB-^%}Gq z^pMq8fH4LMAxM{OwJgw@{2Jy_iMANJM*Kx(#3yHeaq{WO6;7G~3}b)_n41cwfBD zKn6k@XZR&u#3#weEL*-@{D`)FqBy-GG+=ed7$+C$71s4~G3=xJk>I;IXOS;`W{dK* z8;j0=rhV!zib96vBaxZ*RoD91v+;w0mBs>S9RD!*Lu`EjR*xS-O18jl?Ip6Do(Lh- z+rc4%SSX<{w+rFDNbQz3u!-Se7Ti!kep&m|3Vx}5WMS_k3v(Y?82d;*_FFZGjG6ku%;$v0kug)I64!=9Iq ztL8xNU%prXJnUIK!E2n!Im{W_gKNIei_e<`Cy7a4ydWq17&gH;5@;F`W`~J|fsf#S z)SMr6YD;kaW`PLFh#MLS6amU&R5y=7io+T_7@Q=RV;8n{HO4>|M&AUr6Qd@atxhhN zLpUKDvH&TdE_ZN}z-r((Y*O_gk$jRA2>QAUON_qVX2*=L4HPrINu@NeOGM6{&hhRl z`|nxz-?Q%bn{_qRBb_nB^2dA(I7X6xt6BhOCY?jtXd3Ke!ip(?BUtBn5!g(+dQO5u zUv{SO1CgG`hlsl*c8H@KcA{afcOV}NOK(C=TLfxs{l6DEl}O^#OT4E&J{}~n(ZNaN zFrl}w{2uDfDRZdopdAjrrQvU&J|cP2o~V^PKE_-EY29s(Cjs?-hk!I8r;<&7U%Xb= z04r-RNQ~($rV7YY*T4&Du8c(?yBea(N@LYnSl*s2bH2nv2dhva532xwHAIJ?A>0${1s*vaa{ z8YKCsVyh}sLN-+lYqFRHj(joB{z-8BV&Z51^$TEho_HlagcwA5a{I4yek zER)^g?B{_pL&F0>5uek-VGAJ(cHOw0T`$9A;|UpqI#--fUqW!HRzz(>XQNRi4*)CCv{?zj#w}ZmWc?WoX>{g$hn&q&?Agu z$EbZRJ)+Gb^?IxHy)|=7AP@>!=s4f{&kgib1Md?nx*B!RYGjUN2LMh8{O`eFP*bcu zI-*q!&!77=*A^bqD%2A1(JK?cm26F;S(acN*vVvT34tYl($MJUpSaW;kOr$~`KQT1 zop$mC3jH2?((|k_tMDh4U)f>c8I6gu_8m;Z=x$_ID~FVFCHI$C-R{T=a4CODFLvXZ z(ZC6-3A+zh_iw#E~{^J^F2f!FE*BoD~ zF3+Ly3C{qKri@CafTEB{7^iz2Pei_F@JmNZlBpPfBOVhkjkI4miQ=E?)HjGlGyNz( zX-FfZGE7G26OYK3Xr3~9NIRU6!k`lU0ZM#W*_e|y2gFXIUm-dBOH+p%`es!ld)Pn$ zHuTjfNUuT^1V>k}aaqvac#p!Rmjt}3Es0dI(E*}S;vth|Q2r1hv;^@gw@toE#txKK zw&bIKO}50+7dgc-f`8~&dPTp|EBr-P9}1R)f4t&EEs@}G0a_6UBoZY=@AQ^xSUQP{ zufrdlUhVmlFM|BZ7s#IlH2jfbVC~N+&ax9PycKsCTyclt73bL!>mTb`N2faf)3=x@ z?0Cv0X|TemY}T`zQ}i-_UnqnHdc#Bg+P5%&@IM3e#sYG-+XP7Wvg7}B3+g(@=0;frb;sG zzN1;*B0KG;&l)fc??1#`>GCi5>-kkheo{f~L91F(;4NJ)wxL55cvXCAkv;HG$_Rje ztwZDx^zYyRgU+x+X1>YA^RdOtMP9vMflTO1r!pgLJdn>$_;Kzxomiiv;;xzSRNbU? z(W4mSZ=>VdSiTS?QG+jB^%u8F5dU>mn#DpDcA0X94#;NoOsvx#W%Dvq52B2IJvz+3 zGs7W`pg#26DK0BBrVM&IQ$?HPaM0a&pd874Ezx3TYK{2Zu^`u%<^<11+r@7~=gECkUZ-!~C5cZ^qlzwq5_hX*a^u7%kQ zIq8LfiYgW77Z(`!s>*sa)9_*nW9=oT4LPS~{vxC(%z+N1oQafDBngaZQ5$`KO6WNv zx+;IuS5)*T9IF~tua5OCy|TYa)Ie|keZ^jIC?dZr-5@nJJpDR6w1n`TI}J?TfpK4f zQH`YfV~nrq)f{%cX6s+mfzHHF>8!v~92y~seLGP^_Mqj}DAGX5=#bO=QOBrKv14)9&xy|?pt>Yf z3wh|Ky+EPvaQ@hz2J+#wnTD6MK#bos^kx_)-JKm5$lu=1q=y>ao%9d`aLA5W(}rMgyBjmgQb*Fj?s^|mW2pa2-%Ui;h5E{dWWeGsJ?yPk?28#xlsjcH1bl^R741~ZJ5@qEkEbg^t=-Q zWrWR6gpOp7(DgJevzSb~v4G^#hR^9odYcC|GM3|rGLw}rlo<|t9YaHU=vb?np-$? z&BCUqnHVyx|KrU?-TjKsH#X(UAYViIwrN~_m1*oy&Ice(xiCz7>bJLv*yo?bw)^`% zHceVbVIpMj=xj${ptP83o(5CXJlfBF&o=WMchsW3VNI7;_)yR9@$OCf92|CP=rY<& zJWfhf^d?Gw#py@6Lg#t@r$^tq-&S||4}9>0KF;1_ z^{je-XUM9KC%vRV%V1&g-G~GtqWIpa_ zAlY$jc=w2N5(NSH)i_FzkCmN7%H$L07)zpma;LLxgBh&bG!vS)@}$MyivU|Sc7d7} z@Ww?rvA{7bI1jm*nv9O-JZf>7W0}dFY7Y@4#4=8M88(o^x+hd|9^HORapMm9@!(`U z4$65i=b@c5y;8SxH8ZKk`k5G!lwA5aV3XN}7~fUWP`b7;IY}Soa?+sdMBY_)GHI)S z+F_obGxmxS25iolNi+SSksNPYWwWobf;I!T-BhBqcN)u-FP|)sQl=> zlIyi4@BjUc+&VJ4C4y!`>paON-rM)vUKv}HaSDk$?%G29-6=%(+_gpUt0*A1H$Su7 z>c+XeZ#iOsjv+l=h(}x1Ya9&n)=|QLT(~Wf?g{(S_AO%fDh!XpwnbcPT&w(sMb633 z^Um~>XgqSlLXlC3s151;*mWUw7#*c%qYn_m!_-(n)g9OA8Er^t=3i5xdfKUwb@Z&| z3quXNfP6b8T>ndS(xLiFFhaLcTnmXIDnh5c4%t=+TWCHAm{e`C^IBlVqRy_mJP{>j1dZ zbeUJ=p0XKj`Z{-dc{MrD(ke02Kv~4lWSN-?(6zmV?kUfcly~7-KzoLd7V{YBo zsD=ZW%LFgk!V-bS;L{(`Jj&ri{V0U<7&NNyYZ)90!(UUp>_Ifxh)5BuVu`-Ey^Cyd zX@qlzk%G}^{d%I{L!0#kti^j3IDlmfe!~TQDCVu((*<{F?gP3J@ryBk91iQGx!&mw zV5q-aUjf!p-tHZsBK~)K2e=UeySFMnug%C&gH2zHo(7?0z0&YOG%x`%e9Mk}a?1D_ zh9`P({ls>Kn#d#yKM>Xn2gQeK!$1}hByseG_t4Y}`eqZxU!(V$65;0UIla_(VuPhW~mnTY9B`vW1=fx0OQsj`E-%^dA}y@}Z&zv$3?#C}2;9d=SPglTGNv z&mL&N2gOfO8|Rm(kDm9vKSeb=?H9(k%hVe7^cq@mfT7fzgI+_EF5Z+bG=sw%w|2jY z-Si4zSDqV%JjR<$^`8uRi%{;K_9E2_kkjQtc_DI#R>_v}lMS_h7(+8F58$C1(8ft_ z7WDWhY2Pwow{pzhvwh*VD(0nnMq_Qp(H!w>L76Pl3j0Do$MG9kp(;h0<;xveg9sso z@&cXz;Y=N`*NT{z2==|%#BCsanguHMiUg|ATcMQdp4tfF#|+zZaw7SMzic!G-52%4 zVn+*JOYLbVe*TnyE}~bkE9}8eXz!+slq5SWyhVNmIiyP`-4QghO?W}E=vubWnP?i@4$={9{vtzXd)(CW97al^LSHlus1 z@y^tFJy6`2n^AmM;IyI4{I$yGx&h9R^~TBC_%*CH zAtL=FTaZ!#2K?S2m|w$$KcRQ*xT!nt+)CT##;GQ}q1ti4ZfRI0Zf(OI7ZrB4` zpl$7ky+1ei#?A|UvJ9)%L6NY|-(9(^wj9$4a#0__c%XoFd@%*S@7x`110xZK4t9^SMr5qvvBS z6gb_3xiv+By3Kr6{n7Sj|1>aRMEAd8aEKBnOc@TR)^<4%hRMUhK-n4BYZ@e3);OYn zY&O1e(m2}&ifu7D26|v|N?KweEn0^9f)|7G5RQMtbeiSEbOlFw#35v8LXB}%EI{ln zusTAy;wU+2j9f(6t_J(DP|kn4oX@XiL1zr^d2}?BhEK{&dfvfrmmW-GdUmg$yUwVK zgq>(M_H)dm$`9)iqxc4-qkmS03GfLfJvkVy`TMe)5 ztPOSu>HS*Q<~SIoLrFSawuX>!d22~G9>gj)!bRhutAaz9C-{;YNpjK+vbvT<+jmc% z5454v-v$j^!H5$D15sJ({-#?y(zi*e&6*bES_OhMhh$}FU_k;9+IQ=D;t8OCP_1q2 zv^Id7 zvaRMBq>AcHRHh8pRdz)m= z9<4ZXRh&TG!_}6EQkusU?PZ5p5R8h{o2IMT+6F48C{g=5ycp1zg}xwYk0aVbTA0R3`Kd5SI-cL;yTn`(|x(;lE}aA+u;` zq3)$6^GKNZpN1wEM*eIaKZ28GUYaur8X#U@PUl&=3apXis*j|-1j_!LRj?wIV!%n+J{zBWTO6(xD-O=jE)#_*0 zAUpG^h(W`Toy4Pm(r@~~<&?y)-n@O@d-?jOH$DGqhPd}ijQD$)B_LD!Rq^W*Cfn1r z%sSmfi{4d2Gf!zh81r!9g{H3~1jEy+81r}TEG+0V<5OG1Sb4eleYKy~>dG60EbpxH zk|S+pFkNA_h##IMYHt^5rOSHIBMgZ+v0h z5T7s$98kH;s`h68#c6(q&yQ+kmANb2KDKW*LtgiIO^LV`YjSrCV#A=ZlY%%hjAz&sBC>8}H1>$L1uAh%iqV zAL~e=5_s4`!Iz3yXp&ttXz^C+C|g74k8iV;!#ItzcoAcA05 zp-u4zg*Kh3q}=s|q;@@vy+f_Fuc-my#TYxW@K$MF;^l>Xk)hXy2)QdSM7WZt^Bmm) zYUAw&D&T1#V);7XC^)Z?=7;2kQR9ZwrQF4To$!3i8wPZ(k-Iyc%4*brbZP+7+d&q@ zdq9qjTCFnB?7e8N!Q-07g-6ImEGUm-Mn(KUc7v6hqHcTORWrAxl~_Yp8<*A^E4$ym z_G0F>+M=ocQapb(6d-PAy)i?;THW)R6w5KqVhSHl&1n6`A~(ol(_w*fPo|vx%NJCC zG$K7`@Nz)V?wv>#&R8xk14hDqQFav0-mv1w@{%C2R+|rNx>nutt;wcc-^j(3i4&Y| zc*B%f!#h40v~`NGR*b>yB(L$2a2gWcoyOD^K4skW^^r$(YFhU%Y>tUyY-^l3>}D1dpGX;>Dto#_;`N@3hSxYmN${& zwHNZ~l@+arQe>ba|7~KE*SGO*?*=apy#u7nuXYb1euqaM)$jWF!I<~Ae{nD7fpbqU z=7O8rFa?~f+nyn`xZZDkW>D?-%%Ey`W*|6W=aCOsjVuV87ka#U%RT|xrF0j6-|5!Z z>E@&gcm;32OE(>-y_@tZ?C}cJVW5`^A;nxr*6hxmxHvs+=cX&6f2lJQ>4A$dX2T2= z02u6pHr0qd&9CQ>;G^u++hB6oPq<39-$4IPo{gO0_qE|RnhS2jMoCf4d4P7bQc^f` zJ}tO*%9~g#naOSLQX2;CJnhkcNj95>VtpGSzH8%o+W~zAs)W26WDSPg>zqqN{4$bJ zUPScyboEgoR)|mCjYd78*Q-PH^@*s*v7PEu zDvg_>OgJ6F@p#Jiqeu7DH~V3|LqHr>tC|8JuGgQ0Phg#6-ww0MAAM_oI!yydJvb~* zy`&3o@4xW&{ZHNoZ$?XNo}y%c>@D%Uw95pYri)K$xt?CG%3?K~u8OiePxF-?WzGHcGlGroVVlIs48mwy3umOomlk`}%CeeU!WtCR4|^aV`QxK}8qEWtri_ z=OJucukZ)O-%Kd|a;Z{(e_h6~tzJ&gDNq9@zsFw`FF|}kbU~Sp;cC%aO4aCZU*aRe z0d^!vz8}Gg=)g-s0Sso8>Z%3;sK9)axMO9yOCfUU>lSi}5mpPTly1Sn^{=P5Rc$uf zq9pf1cI;*O{(J?J6PBt1%M5%|XKt8fQ^LZ93HFX2?RP*~qVX+%b=;ZoyUTFJ1M6fC zC)utZx!-~K+oIU7+aeq-FvVEBSi#j^2kJforIUXC&qoCTx`P@hQyz~w>2A@d)vQ1L zJ7k7U%SgbykOEnGkqd3ISlB}+YVL8xi7MWs((F+g>{3#g!4wHtEW4_(S1-}P{tVP1 z%#<7^hh6gj8_64gGODk$UJc2K;tc*dJV`#lKle{I42()SLN3*iEHN+n5icc4@CZ;_ zS7#rFIxkkY3Pw?sjf|~d=CC%6e`1WI=)MjCmzex;NBk)#cjEgq36?lprJw)2N@vJ+ z1co(#yEZ&zXqM!Oi17HCA+lNB7o*&%#?%hmO$qT@=-7#Wn15pGEK^|I%LPsXO2KfK zGBAOx%*ew@lqt7|oMZF-40=bpQXLs21E6(4@c?HtofC?$5Hbw9v6rEL z_`R70{y&uIyZfAJP@m<&4>AkZRFmoZ2WF-qJwIr2>hc_Vow68on!~A!0z&8h2U_*a ztqdbNv}5gmfHH?zn<`52e}_nnldu0Uka5tIky{L9Ika*dHgvt*()D&Hz&SrQ`pR$H zKG41K0>pX3f`qLs{mMVXUIvmyY{)Y#D(wGc0VL~-TZtsljb?)A~KF8DD zh{4y)2ejHu z!@1cJHsV}lmBP9^eE2~0=%M`*tg7#c{I+8cj2Y3~g}-9Ph*~y@2O*I4J{PMQ9Nw>j z!|?fkQ$5l0TN|RC2}#wMs)HDfU-+I{9nZ#NIl-eCCm zO=8W9nmpDpg!62%nWOzw7`Z8*)YpTQ+9qs6VDT1G)jx;R@{3iSPqkT%x!b)pu$bF1 zt{b9_wpSLabW^G%Mq({KG{VZ-C;XJv{(f(GHKH8v7GMgFyj~f)$R29t5K}SZb+99j`aG+BiNW$9aDRD)t%t@52c{eN?hJ_)3R5@G)OL)%lJ-6}h-^pwfbW z%^3V>*fCF&GJYB~pP2V8MmIZ($4L_y1Y;p5fi{Kt!66Y3k`9&6vJVCH547cAI2*5O zarV0vl0E7UJvrC*o`yogPf(xz9e^mPu^x4>0b$yELVt|lK{O*zEU^7_idXjv*!oj3 zzr4u&7fhoY>i?uLbLEqH_9>f3x+ztE@dH$qmBqhb7MH7O#@>4hX8WJDw=21JJX9{S z7G|Yg*u0-O#72&p@Ds~rea1Jv;S`Y*CX2qu|6{z#2;hJ855pMc#bxPo;TM%|dDV(u$*&Y_b!Vgv)g7lR zFQ>)AgPb;Lva=iGIga-M`@sI=0|K|5*OV6OH-2gEv+o(|-OKXI+MrB_x?h~ zdGeL+?6W@B%>cWzT!bJ;G;5fD#vZyYwC3wfc8#uil#j1iYOm0wD27zs4B0erIiybA z28A+sRH78Wyx4=#Uzhuk`RlTg$X`Q@#EH6X{oC`S-Ml!k2mW1F?KOOB}ixL3eyy;O=|Uh@!1bb2P)p((N(hGoH-dVbt5{&jiDz0Ub^{3FwnS3-ZIw2N zu5;ubjE0_qZvmm(GTlbq5oqYumoLWCu)xtiTnyoBRk6Sk+VuP|&74Z|9{Ey=dq?74 zbc~=d`A_j~Fl+#^XiQ3jVF%UlptOovJJ=(oP12< zv)<8;l>K=q{(^a+>XRa}v9eg`mLl(@LP==UVGn1ks-Vb(M^06PGPX@wf=_Tru++T( zI`e^L75?&t#ZJ@f24i4g#y=Xs|2=M%Tfo_nXHuK|+aaJSuMhu>z|T0>CY%9rpLx!i z$kpo9?7M`2d0TC$2}6;91{}>|cwj0a8>dm*4b||S`=&2V86%-O=|X=kRv*#VQ#6CX zPg!y>aH_UlXoHnwmaWy#$hkMr0O#|>M5mNf2L}xJC1dSkYhAfjnG9^cYz>fxE#Zq7 z?=HeV!_?!~(>2;pczEvExc}sB^>}bp-2=sPI2hG`4mJUetj%p|SZ-_-@c<$lRQ~15 zdNV3-3rpNS&}P8@mcSse)2`ATpxgNUg@*I^p6(&~Tu+bi6U=v~>TUo*7*t3%oi~=q z3{q>LZ{#KV>uTPiORC7V7{ho$C#d|-@F!Ys=-Izvj9~T&96vT(?e~D6`LiEj8XV*` zjl(v7+Efx`LvSaBt#OuJ6i4wr{O8{1>;ur+|NHVR9#)W;F7ta#JZwe9)%Ap5_TP)| zY8bM+@ljQLL?nf~_VdCidMi)p+mIeF4XGpd&hpdIhqTPTe~`Q#%>Vr6+5G&;|9JA_ z6a3H9&*Qk=oV#S4v0d;25qd--!VJE9@~5AF9$2?dj~}J#DhYB*;FI&JT9(6m_YfS7|ytDvPUKu`Z_x4K)d3=IGiX$YO-i$(2! zGDaqMgp!+)!x)eVzlG_0miht|OD8C2b24>A|@$?m3|DOERb+iEa1`c~ zf^s=a(i%ttgV-_1l-(f+@Oj364qC0D5h&NHd;tn<)(gPuY{R}Y#D$AJx3e;|y?$3i zauuA0jr`XIcFx<%^YZ6H0Mz)?xY_GRBk592#IEz3}|Vl*>MQXFFKZ=nQ8#?ST3dlg-nna1xa>r%7Q(> zJd*Y(*dNRxdlChEhdJziMGZa1+`6w(4F@on(f4QzOL$KR&>tzkq54q>=P_v1h$jNQ z_x}CmRfK>k(Ew3=eLoz33tmo#a97U94F7uzvGqb+v9q`Vjc&-t=O9w*ub`_ftm2 zzLjoXUPUjUvX;HnA;E*4bRvd-b%0S)sBixqW=q_V_iOLa^P&w!^hWsRbf;SEnC9rs ztJY-88r=BX@pepq)5IOu_L!%cIG@!5mu3yXrO+%+jCGrK!UMvsGe!VW_+ zG`o0>tf~OEX7xKKw`Wh1aYO!a+jaJ=^jx%SR3v?D zzqSvcvy3A@oK&4tpn*jiD`A+#77lEzlwJxKYHD*ythA7SBsR9$UC?Tk2Z`-AGHP^A zG6-xAP;VpJT7b11MPyLdy&;8?Y0czm)~@BBOqjRH1;~+54_hH_lT?!~p>~n@w3)at zP;E-&Ios1d)JZ|KCvj@FuH~OhT1fZ@T=QZ?2gaClYCxu`$=`t6P zF8{Qg=R(_m-wk_UuiLHtumx&!Z|uC#Cp*FFnY*Fhuvc2*Z1-kVV!F9EIv+YVsvn{k zULnUcTHTg8l(jLe^L8g*+q(T>}Vs(v2j8#>xRSa|)Q*iX3J<@Q7ha>Hj$c_17RvoAYL|-1+ z)NcNTJSiiX))6fJsG}==fQW0|#QGwz7#<>SS$A!5L6}hoBW)%%aUo#gOgoqu^`h^g z;qFL(b?r32%4Qm0VI(8ojtG)rFj~355-^y>e7@1PR+A7KS2>!W^Xj~Z?&o-_rl1$g zY{6%L&Vu3D@iNdDn{TXxKBv*i{``K+D7a8A5C}Jdf#62@aepu>F1FEtnoyn>pI`6{ zc`8wviMG(>iL69LohVNfmE>EB{&2_ud5cJYDIyzUmzq(0?I!GKS;i>zc#O~$g9b$Z zgsMDQ)|!XVo>y2~PTZEAFZ&+4#bo*_L58;k6ppq80h0@C^c3DS9_!8F-lf%TVT?tU ziC^;I0ObC0MWKk3&{WzUKg%!EnPuqbJ$t5BY!a!uEs8KVhuW(9+9p1j$^Fc%K!*tn zRZn--f2F%RcWsaZKKf7%>%4mHcyq4)!EqQ>`J28LM15}I z`8>mb8uBd)CmK)u0?Q>7^3{K4=kYciDORXQe{OD2<9%iA*_&5SUcP=e`RmK~FD9?w zOkTWv_x{b>e>co4yfb#kcmmiY?x+vzlwk?c9HN6WdJ|U{DeVPmMK*hzp-H$7ey>?u zRq@v80Z)@T$mwQB0#~bf;zOc$hZ@<;%!i?Cys2j^(MJuWcr!HA@`2*2Vt=Rc%~4sZ ze|0apJInaagqOV&Jq|Ij>&Q9mZV5R7e3^m@#1DRVRKeW~d=cOF2YQ_&)L)?;4+uIK zvPhKPfvpEq87KP5rjPC;7C6|0AG~0?1oumiyz-0ph;v3=l`l8aKjDA3s@VU9^l0w} zhvNqsiOA;I59>nky#!cS^vY$HP0tURoMNW@5yy!2XNUGs-jtib&s~tPlzm`HG zW$^g*`ZQmpb3O3HO8gR02GwVGf>7*tH&Ml%kN)rf{y+9z(f(R&-6FfGGJDhQ)nT0N z{TI|s%s0~1C8?HpV~}{Py-D>j-mCPbTO+Br=Im*sT-CN=SH-F`MK-y4#j2hSf2(2q zoBDH_PPfLhedibH7?q+TZj#=O%>#*F*0eR%Hej0T7P@C&zAPH|OJKr*v>O^En#u$5 zMs90}2Y@jq>d6e(tw}iYxAdWM{IoQJ#*NTG7()*MYXj`}?|s1c{1YPxlXfNuI#EiO zNu8}%!eJd@z%fHv<3af^>2f*0f9~{pe2w1I7wg@T`;G73jdX5{x75oI=b#XKACVA0 zQFgkkHf`S1_o0S2OyL8nJ5OzJRF8+>NUwlD;th@EY%Cw4Mr!XO<9 z_7znhhqF>f3KHe|r~bRg+6b*&*oo=C(f6tn?xgYKRKX?C2nN!(rc%9kzRWIB&Yi?-*Czv>j}Zt1f$fvznn6 zr)sJc1Dw}HHdRrsRXG#Zf1T3o9b#DjIbC6tqUJMBna{Eh=?)R~T88E1G^zaKb1|_f zd$LJ#fFHJ5Pkb2qtc?5aCBev!$f6DqcyBT4U6j4c5|w5Si$wvpp|TGj^D6M+qWDiQ zbpsL^rhPF7s@Enh4}g|7^nWN;81qqvyJWif}7{6A#_{O?Blf7z{_;F*JsBVgCw z-ZYfK4F#UZX5?)hL-klcOmHAjrd^5jN*r1*8D`mOdO5FDfB7-P2oGXsD~y-c6U}Bn z;D1%ihntbxtv*c`+044O2aOFk)F1V!DmpZ)4c459-DZl+^12zf2xwG=hV0hi+hK2| z2=HR>fe8>`5{`{He_cO7U;WTPiHgj4rVJQR*QvhHccAfb4jHXgQks3DnO>@Q1B2*? z*3nhfzGf^K)>hCK*P|$^c)Ns;Bcmr0_rUUX3`0l5ZuJR=15E|i2DL~*o_;o=t68qT zg0)l!0BjLw_~hbEKPZW-p7z0|dpCiipi_^J<=>)VW2ykXe^3`AY1zoYGz}${nCaCVpn>%_}OCMva6g7 zNicxzdh>H}e-^#DtQxvuI%>NnAt5tjspB`@-!iV&FPG)H<18*K^Zl7MUbFq2h2>KG z7%PijW0YUess&z4t#7lTuL^uND9t)jtR0`%>m?*xZmMQ1WLH70@ovA3&M74=VhkDC zH%rkC+!9;g9TFRc&Lly~_4x5KgtQeqbwX%8?H)q_U%Ns*XJvpAlyZ^|mZFLZ@$+_Qz~GJrFq8sTxKw1H=otr5 z!5Y7!LjJrRDkvb^)y3Q=K~8Z&#e4Bn6Jn&?4lU@1fG^Qub@yO?;c43^3o{9lUecd} z8vfCqe~^8jDHw;Ho1Q#ri$6yoKgV%j@BPYYU&cpgO~oxxhB9wIWiEiuGvc>AY4WWC zZP4hM4F^o#rA5V0R;%=SyI+N0FV~U;si=y7eFq0jbfz$Njyj9`)=r^NH#Z97crGz` zv0JX_-}QuE-^>%@;ikdUaDp+PX6zeCu~J14f7nYS#khXdsIfd2e&8@TE5IQk0p;OC zQG6WUPlW%(@H?0>{UWgr};|a4nwT_tj~CvVe&L$NMNtit!?pE~&}y+$oN-W4G){FFSV2oecA%-r=w~ ze>xmiOx@!J|Ep6T$rQ*qKyuB6H1sc)Iq+5|Hn}e1s`~?!;61h>Rjfe(K6V5Er*Iz! zu0KrjafT6~pGeo=j(HI1juvJ>1^v<>FS_J+K?r*dRDa!=P(KdRd)ID&}G^8|{A`6Tl*mj_>^zcFQG85K&vCv^D9c z4#=9zJ1MQrR2a3QDmkaSd-yP1u z^oWPoyWLtj6?W*wP-VWBzE=^ib3rrUL4z zD$s)M(qfLHz2Z~$a*r^xFv(N^y_N=4m}LDif#zxS`Uu{bn(diHqFY5rLNn) z<|eC5(*|s9vkhjtIr&<%4EI-TNL>55EWyyw)`8I&M@Yk?z9+qSf0rW}CZ24#dmVYw z@TU%H#q2PikQJfiA0nikZpM~gdkeBS!J9{=mBUJ;Mu(`oZf+*ugrqPEZW6BCKK<(+ zw>hpBYSI;bbcEGM)Mu)mjc6!XtHx@bhdz6LRKk0|Zd?q9C>K4wFd)ziGnj}w=(R;a z&m}5fPOZQ#d_XP6OWw`6rpxgt=3@(cS-eQyh?rT?LrUE?x@Iv)Q&(G7{_g%Ah;b-{Y9 diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index f3081ed48fd..898a8acdc0d 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit f3081ed48fd11fa89586701dba3792d028473a15 +Subproject commit 898a8acdc0d61a609536774fcd5e20522bb58b82 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index 458e136f7b1..4f93b83631b 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 458b49d4f44a7700c09d6c248238ed075e352d52..3b51da813668d51f0b656a45eb358b1481bdde6c 100644 GIT binary patch delta 2637 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 delta 2615 zcmV-73dr@KhyjX-0S6z82nbu^3b6ZQfD4GQZ0P9T z^u|Y|g-vzOXRPxY_<|_rsuJ{&kr&#ibkxT-@YsmZ-9a!RaZdPaka8ZIwZu6le18>z zBjuDv#8sM6tHL}b9}EHTfB&d+mbd(MucSh)b@Ol3>Y(G+u-^x?fqRwtqI1>t)}Z zhQXq3my^T8ySuyL-M7Q4zCL_58jTJKB?kB}Uh=IFiD;pRkX`+$`V`>v>1gN*4%&kN z|HY?Xg}EjnF07dh5OGWg65|i}1H=PPnFsi4?{pw2GXDzHMM;cm2~e4${D7OB&$XS(uFDv475!=&3NT+6t&7XBFpe-V$%20s(m5ECGPXmJRFk>2*H; zJN$oe!&u8(B&0cOnh)$#n>LW5`$|=j8}KQgV?_fC=#EWTXa|UVKc9#s>Vq z;rHJ~{_Wa*GyK&zS9;)!_6*j@5$T>hoOrhu+Woqw^gox7Wei~hy47GwF$ zx%ZCk#~*ea+e7F4vHptkCX)nRDR7%je897N-maGqkM%-?E_@C|Q)ZC%(K%7QA zTt0^b|MJk|^O*KRCItg2Mv~Q9$&}|ns;Y#Hfyq@-Wn_~tD>5AYg5P8+FYdDYhUfFL zWce0G$>)?yg@3sLEVB|~80(BJsscmVQhTemN+`+z%z?&(DZhyXERmO#jalL+c?MA5 z=&OK~(&MLqHMn7Oz+FhF@jXgXHh-Rz25r4!ng<5%&U56A(O55P939e+<0A>RT-gPU zLeE$ECmhqE$OCo&SA3A8vjUMpy06^rMKC~49r^{dD1Y5Fr0t!riF`;T4XUdvk(4d- zmTo!`}5rdi*JR_NL(Hn#2NkfM{zPwk9Lh1ZgHi$cbl;?oTdwoKvH2bv7 zAi?yI_*o=|9Hcvc3Ue0d#vn3azhGUIOIR;3s*Qhm5I=D~VxJR{#FrMUii}r0{S_v2 zfGvQDn12J|Xn~@X1Ytbs9Z$hRB9n%&?2^U%1bk@nLqaXteiX#g7auirjCY~F<`65s z=yU-iNU8XPy6p}F@4EZu-E$3%=w7I6(9s28fIGRs58<*S)`0^7?Y>lgVD4qHsu8fu zpWp|j%qZ96ZZrU?kqZUNH7D>ueY3i9px!QI(0|21VWU^DvsU|{cn3-rUWb&|#a2c{l) z>vYwl8;GcoJI?{thn~hrBy3& zp?@K)M6592JPTJ5h7c^R8-$X&NtcSCMMO|VE}Fop*8M5O`4ihz zmBMxECG9I@+*H_}e-ou+cEaySfV_x%*0&&E<^X0me(9GH3hS4&a z8dUAv3$l{V`is8f%23hbtRpLv=Fq(j~)dgeCA0w&Q9_Y zrkfSYS>2m=Q~mDCSPunFlG4xQ*>@o13}Gaw5qvNNAaW^32YRk|_(m;JyOZ9tAh$g`d>y z8gCCGqtq20&?PsO*uts0%z)m(qyRWql4h603~;s(e-VSX0QLY25t9cWui-b5;X|`F z_lv&CaJD#R?Cq5Aa3h_Djm{6d@$q3%JS4p44%kQCYeOC|4IH2Wut4?#z<&!ma>6!`_@eU&80sel<^Z0oBMp9pfvHgKQvL8eG-Y4SXuy z<`DQJqppDcz-5pILhgPk_CYz^3rxk+n@|p#e-gu^ghhSN?DqS6Je7N4cPU}c>N6hj ze$2j&4Rkx_=MY8X3<#6e^?v}jU=aTM>DxE)fO~FcMdUW#?UdP^Y+f8bDe_~eH8r_9 z%Nu@hY0pHuY)P@v9MyIkrePq|8)%=McYk*x4RrD}&d!U^b?#1BlmSOIem)IzESGqW zeEK;6frR?GE81v3hoPC}c`?l{&xgOYo+#&3#C`5#YqTBc^U9Yerhh9L;@U{KjpcET zv>rKVixw&RV2mQ)n-wt)esV49{)Hij*bL#k=k5J#phCb%xnnS`hPBoXKrne)F`Ok@ zQ_uR_j=dv+>4g|%nQ{N{i2lgsZGwmj)X@o^upxhPF2A}DdyC_Djo#P)ej4%^H}SJ* z61=Gf+VT&w+bl1T`+pAB7`x0wRQ@HSeLQ9V2zy-)*ic}xXxLPB8_H3r_9a&8VbxB0 zCj|t8_uN)Iykx`yN(Wxo5rfwUybspRnZvY-utG4&lO_+Po%@$q#NWgU3Q*LXj#7*>P?E?_H3RELl==H=T(?3Iui+-#)$toK*{h z)-;7Q&P1;&X^OfUv|!%oXI#!feQe6s1@EQ-vudgV1mEDgb-lpMp>j71_9iU&^7w~U zQ{H)ZrE=iAX6-mN<~ zLI2__ZaDthl@r)8-|*SPaUAl^^%BnE&Ofpw_k_ejR2@gZsp@4eQJj*0aB9Vw#@q{sj3q#I1J}A-gX+1@y>k-J5L~)o($bg0_ zk1|viMJh>Bnn=bsY8X()W1O66mF8(A;)H}a4RBJ3qo>uh&|DT0MllZ)!ZIduD$stx zM2J7LSa2F=yWX+{Jv9iq`%OPM7y0tqYwk!E~P`a%_>IOUjA7&TnxEM_dvaf2#ixaUk_ zZfyi}h7l8oj4G8NV2Uv=GYlp`cR`x>$+xi@jc&(HEoxJb-*u1CuXU*cV>F6V5zVD! zB8t;A(}cu4#|)vEV2TLD?`NW37PGlo%gRKe1!I~}0g@36oiGGSgp4SqgvLLU^$Jw6 zTC9a>AQ8kQ3o*bY)iOcELjqZrVVt1sp9y;hvsKBPwKR^=pXIqqgy0%|jCD$ogbSvC z#SxLa(h6I*$C_W-ml4V7cl-;M)ePmtn7Cmi7(#>waV#n<#2q5?At5A7PRa@JKU|N2 z5G6ukn4qWZwm=Z{8x<0(j?{Y)6$?It7KxxyUls@r|AJ|G4b2xZTU*1Um@=Y>BrL*o zi5O2~il*dI2r1!wSK1W_Sud7#h1Itmm4!iKFRDHbM5rUm6;^c2TMR4}vosAOjlkrJ z35=)De#Zf?`2wa+do|jS!swEYM$VbrLaBoWW(5*R$1qP0MuJw>NW_lg`Ngcl9K}xL zEpJewK?-}tkq41_rWfPiS7z~L{MpCK_!|1tx35o}p<`h<)$67kR z-dool__kYEzUZ8(_PqJWJcZJFM5ddUwXcX5wAjBG7m@N#_n+%IQ^z);x+qcev56+Y zmZQ18|713w9%1Ez78}%y@Uz#ovL|(Yt&3`-s((k*9>mX_xAP8zK25&t6Ja5v;!@TSI@|QNKD#X+&u#d%d)bV#MgW!r6=3`}w((QK82AJQKyDb!_4+e+F zkq1+^9vZxD>P68)fa=|+O~bDPq~V480L|c%mn*d>^;3XGo9Sv5(3nLhMFA4XjPt)`e3NJ@QAHhF|6 zyUG&G^nkWp%N_nHVZAgtMcV3w$l=hbuxFUr0NLLoQo?X}Q1#sl=XckA^2qIp{uWP? zx3(mdx9g+Hm#ez@1`SgE?SfZ@wzBM*5R=9G>tC=(*n^~F$!&>|4@%5#$vC+q-`FE? z11(aQ`c#o6u@8;uh)Z|bYT^QNgWf`2VwBcTduJLsJ zOY015z3MGEAN=y>@jlwfj_WIc?QoY6r!!)?YUf7^$E3ecK<6P_tZyn>n|l6ky*Sd8 z&FibzJJ%vEQet6BOWTJqAKuM$cPkD?I~13oob?rqy>(gN(%6T+dbz)C@qU!=yEimg zC+zjd;Z{%IAUWL{YY5nl@7?#vvTngW817L!GHBhihV$sRKFW7$@w%*aq5BjI=o*|W zEEDyWbL^h1>x=wqg0X@*7f$%!HqF|EpPhLa)fI9+N6xnju^6CAN7r)IV(l4Jn~1N1 vZp`&2xME|5&9zUR>dmynFYcdn2jaKyjvqP#XLo+M^CtfTmIbm=l@tH~UaEvT literal 2327 zcmV+y3F!78iwFp7t_xTK19N3^c4=c}Uw3bEYh`jSYI6XkSZj0JxE1{?3a6`(HAMm7 z3$&`qWV4wzZC;bdsgd$ZEvg6wR@4X-;S+b;LcRG_q3<=!FxsP)JXVt=> zHBBLnGtsL`nxd`-Etog@nUHf(ADgmu!Mka|teR>7!8f>WT`w?msNBtly#))gJpN(T zly~0U>8%x94c>#nH@IwTOI4df7gE^&Jimm>JQHP^3;E3j-*`8=XyEOtop0aXh;HAx z3Hk?L2_x`-yK(~i&NpKA@IAir&Giz_;m$v@CHQ(}c1%0a-C3ld1Ow>rW`|Fv)9IW1 zD@fC$*MIVY5~`V*4~OfevaT=^D{1OxyJ0xo6X$C%a_%}~u`I@&dDp?*EXsH+*ETr* z%>vB4R_D&!H}5|<{v3pYraial{-b|(Z=7d!Wf05J2Xxjscf@j8t{okJZR^VMpRQ0y zQ~bAkckcWOqJd`MoS8e%&uevk{?GSsUI#XFsu^0*2klZcgJlEKW)5o5#cXtX4GQdRy-=0>}LUUP27^yH!;#6iyL?Ws} z$Tg)AWJF|)2^zyjlT@LISO$`5&a{fTRwT&@0gfnYs(+x#2m_-8ZPIB*rHqm^M$>4P z>o}vDhCCs0w9%7t8uOGVB8jsUG*4uRuIVV_u~d+;(|QanmZeptSeQkj79>oGrU_3e zh9Dy7Bq13U+ZZvaS;SD25D`EosnThbu|&xzjdRJ5tHQQ2iZVW{G)vJtjGiK?-jlm=U_>xem`nh{B_ib*NgVLYk(SCn)=8!rsAbRf=XUO|N4D;mRP0-33iP&*GznE26 zqd1AY6%9%>NMWx8@*q;r^kV$`$}GN&Kl?(d-;j_keO|w)6nvlF&lr}MEjfv)rNNKy zULs#j`mqD)3E5TzOJU{}N)K9OLU2da8xGz3*Jo4X`64j&`;PKl@6MMEVB71)q<_Kn zo;F|jRj*MVY8+IYiR0t2 z+u z28YLy2TQjR8oX`lMbScl>fNVJBd!Ca;f4DE&EQg$E5KvSg9wVQ3NXwFgRP)#IMGMf z!uSsUv$7c={2PQ$q1S~d+jFNy`$sq~&x*=HV7fmCC!HOC5VrPPvHXOH^vPg1_$T1B z=a@?j(>|Vkq&H`#lMYV)58O{2_QX-N#gCx0XTKdd`!fx$`6EEdvalS4zoJ7;r#vi2d8UoO&`CYKO(I`#7a5YncVw5=)#Yf$eWny!zlD=)HTp*ie-VM)X!v- zM|iTUEWu04xI{ThM7%}eIJn$hQoud?_M~+yY7=mZcp^L zc#^!eEup+!A5Feo)y+3(km_$2qAIkNWzU3IEZ$%Lf-}M%Bppj`Ta0|rVs=}`NsoME zkH8JINL}hvVdj56PkWYmD1QbW4<7EroPcdMPRzWiuLjSXrp65VFo!{>QVn{GS{RtQ zFoP@64&eI|1BIW8c7TcBqGN&6XRz!nIMR{qR9Ba_3Y*G({H$J;s`~e1Gq7mGy$)UD z>H3$}8Q6Z+TW~)3<;~-Lw2>XxR{-1BT|u0#h~=uCA88zu{yqVNhitLFsc3EK`Mdq% z$WS(~uU_w5i?~RMg()p)DP?~!HQf_w0~NA1W<8=f_sN5A!1zSE1>ZLLe)r`SN( z;9OywsIQ!3_hj8%$oT>}-zvmrfGQnb%T Date: Thu, 27 Oct 2016 00:37:02 -0700 Subject: [PATCH 050/149] Lint --- homeassistant/components/thingspeak.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thingspeak.py b/homeassistant/components/thingspeak.py index f1689c1833e..6f01475372b 100644 --- a/homeassistant/components/thingspeak.py +++ b/homeassistant/components/thingspeak.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from requests.exceptions import RequestException from homeassistant.const import ( CONF_API_KEY, CONF_ID, CONF_WHITELIST, @@ -41,7 +42,7 @@ def setup(hass, config): channel = thingspeak.Channel( channel_id, api_key=api_key, timeout=TIMEOUT) channel.get() - except: + except RequestException: _LOGGER.error("Error while accessing the ThingSpeak channel. " "Please check that the channel exists and your " "API key is correct.") @@ -60,7 +61,7 @@ def setup(hass, config): return try: channel.update({'field1': _state}) - except: + except RequestException: _LOGGER.error( 'Error while sending value "%s" to Thingspeak', _state) From bba323d226964d9c29f0490854b3757520557dfe Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 27 Oct 2016 01:33:35 -0700 Subject: [PATCH 051/149] [media_player/onkyo] host should be optional (#4073) --- homeassistant/components/media_player/onkyo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 3d4a6fb553b..fd9e6d7427c 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -34,7 +34,7 @@ DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', 'video7': 'Video 7'} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, From b75c103db42380b819cf259922a14619ba01f7cc Mon Sep 17 00:00:00 2001 From: Benoit BESSET Date: Thu, 27 Oct 2016 12:10:38 +0200 Subject: [PATCH 052/149] fixed Up/Down (#4064) --- homeassistant/components/cover/zwave.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index a3db374ddf1..9caac4522ff 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -122,7 +122,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ 'Open' or value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down': + 'Up': self._lozwmgr.pressButton(value.value_id) break @@ -132,7 +132,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): if value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Up' or value.command_class == \ + 'Down' or value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ 'Close': self._lozwmgr.pressButton(value.value_id) From 91d682d02c4d6631c43f6189fb4c24d732f7927b Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 27 Oct 2016 06:54:03 -0700 Subject: [PATCH 053/149] Adding ssl option to zoneminder (#4074) --- homeassistant/components/zoneminder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 0ed985fb427..d4001a7c40f 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -14,7 +14,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_PATH, CONF_HOST, CONF_PASSWORD, CONF_USERNAME) + CONF_PATH, CONF_HOST, CONF_SSL, CONF_PASSWORD, CONF_USERNAME) _LOGGER = logging.getLogger(__name__) @@ -26,6 +26,7 @@ DOMAIN = 'zoneminder' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_PATH, default="/zm/"): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string @@ -42,7 +43,12 @@ def setup(hass, config): ZM = {} conf = config[DOMAIN] - url = urljoin("http://" + conf[CONF_HOST], conf[CONF_PATH]) + if conf[CONF_SSL]: + schema = "https" + else: + schema = "http" + + url = urljoin(schema + "://" + conf[CONF_HOST], conf[CONF_PATH]) username = conf.get(CONF_USERNAME, None) password = conf.get(CONF_PASSWORD, None) From 7d407756c32cb198350b54b607014a4c1afdc812 Mon Sep 17 00:00:00 2001 From: bestlibre Date: Thu, 27 Oct 2016 17:50:36 +0200 Subject: [PATCH 054/149] Converting unit_of_measurement variable to optional, to be consistent with other sensors (#4076) --- homeassistant/components/sensor/influxdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 59f13405808..1dbbd502571 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -40,14 +40,14 @@ REQUIREMENTS = ['influxdb==3.0.0'] _QUERY_SCHEME = vol.Schema({ vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Required(CONF_MEASUREMENT_NAME): cv.string, vol.Required(CONF_WHERE): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, vol.Optional(CONF_GROUP_FUNCTION, default=DEFAULT_GROUP_FUNCTION): cv.string, - vol.Optional(CONF_FIELD, default=DEFAULT_FIELD): cv.string, + vol.Optional(CONF_FIELD, default=DEFAULT_FIELD): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From 09db875acef2460a23199bd1a3b367e0c8bf1d72 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 27 Oct 2016 18:26:55 +0200 Subject: [PATCH 055/149] Fix async bug in automation (#4078) --- homeassistant/components/automation/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index df0026a45ab..a5c18258781 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,7 +11,7 @@ import os import voluptuous as vol -from homeassistant.bootstrap import prepare_setup_platform +from homeassistant.bootstrap import async_prepare_setup_platform from homeassistant import config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -401,9 +401,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action): removes = [] for conf in trigger_configs: - platform = yield from hass.loop.run_in_executor( - None, prepare_setup_platform, hass, config, DOMAIN, - conf.get(CONF_PLATFORM)) + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, conf.get(CONF_PLATFORM)) if platform is None: return None From 85747fe2ef3ac37922943cfe45fb6d95e19b1d61 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 28 Oct 2016 06:28:09 +0200 Subject: [PATCH 056/149] Upgrade python-telegram-bot to 5.2.0 (#4080) --- homeassistant/components/notify/telegram.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 9b0b735b128..91164adec58 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -19,7 +19,7 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==5.1.1'] +REQUIREMENTS = ['python-telegram-bot==5.2.0'] ATTR_PHOTO = 'photo' ATTR_DOCUMENT = 'document' diff --git a/requirements_all.txt b/requirements_all.txt index 818ff621b0f..9a4f0e2aec8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ python-nmap==0.6.1 python-pushover==0.2 # homeassistant.components.notify.telegram -python-telegram-bot==5.1.1 +python-telegram-bot==5.2.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 3324995e701438afc0ba099e36bb2b50b94895d8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Oct 2016 06:40:10 +0200 Subject: [PATCH 057/149] Async clientsession / fix stuff on aiohttp and camera platform (#4084) * add websession * convert to websession * convert camera to async * fix lint * fix spell * add import * create task to loop * fix test * update aiohttp * fix tests part 2 * Update aiohttp.py --- homeassistant/components/camera/__init__.py | 6 +- homeassistant/components/camera/generic.py | 6 +- homeassistant/components/camera/mjpeg.py | 13 +- homeassistant/components/camera/synology.py | 292 ++++++++++++-------- homeassistant/core.py | 3 +- 5 files changed, 199 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ce811780856..35a922ee0f1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -27,8 +27,9 @@ STATE_IDLE = 'idle' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' +@asyncio.coroutine # pylint: disable=too-many-branches -def setup(hass, config): +def async_setup(hass, config): """Setup the camera component.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) @@ -36,8 +37,7 @@ def setup(hass, config): hass.http.register_view(CameraImageView(hass, component.entities)) hass.http.register_view(CameraMjpegStream(hass, component.entities)) - component.setup(config) - + yield from component.async_setup(config) return True diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index e6dc8968030..861a7cab758 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -73,7 +73,6 @@ class GenericCamera(Camera): self._last_url = None self._last_image = None - self._session = aiohttp.ClientSession(loop=hass.loop, auth=self._auth) def camera_image(self): """Return bytes of camera image.""" @@ -111,7 +110,10 @@ class GenericCamera(Camera): else: try: with async_timeout.timeout(10, loop=self.hass.loop): - respone = yield from self._session.get(url) + respone = yield from self.hass.websession.get( + url, + auth=self._auth + ) self._last_image = yield from respone.read() self.hass.loop.create_task(respone.release()) except asyncio.TimeoutError: diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index e1c39a62572..e92274247de 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -71,11 +71,11 @@ class MjpegCamera(Camera): self._password = device_info.get(CONF_PASSWORD) self._mjpeg_url = device_info[CONF_MJPEG_URL] - auth = None + self._auth = None if self._authentication == HTTP_BASIC_AUTHENTICATION: - auth = aiohttp.BasicAuth(self._username, password=self._password) - - self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth) + self._auth = aiohttp.BasicAuth( + self._username, password=self._password + ) def camera_image(self): """Return a still image response from the camera.""" @@ -103,7 +103,10 @@ class MjpegCamera(Camera): # connect to stream try: with async_timeout.timeout(10, loop=self.hass.loop): - stream = yield from self._session.get(self._mjpeg_url) + stream = yield from self.hass.websession.get( + self._mjpeg_url, + auth=self._auth + ) except asyncio.TimeoutError: raise HTTPGatewayTimeout() diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index dedf91a0031..3b3ba2e41a8 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -4,11 +4,14 @@ Support for Synology Surveillance Station Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.synology/ """ +import asyncio import logging import voluptuous as vol -import requests +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, @@ -16,6 +19,7 @@ from homeassistant.const import ( from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -38,6 +42,7 @@ WEBAPI_PATH = '/webapi/' AUTH_PATH = 'auth.cgi' CAMERA_PATH = 'camera.cgi' STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' +CONTENT_TYPE_HEADER = 'Content-Type' SYNO_API_URL = '{0}{1}{2}' @@ -51,77 +56,126 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" # Determine API to use for authentication - syno_api_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - QUERY_CGI) - query_payload = {'api': QUERY_API, - 'method': 'Query', - 'version': '1', - 'query': 'SYNO.'} - query_req = requests.get(syno_api_url, - params=query_payload, - verify=config.get(CONF_VALID_CERT), - timeout=TIMEOUT) - query_resp = query_req.json() + syno_api_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) + + query_payload = { + 'api': QUERY_API, + 'method': 'Query', + 'version': '1', + 'query': 'SYNO.' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + query_req = yield from hass.websession.get( + syno_api_url, + params=query_payload, + verify=config.get(CONF_VALID_CERT) + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", syno_api_url) + return False + + query_resp = yield from query_req.json() auth_path = query_resp['data'][AUTH_API]['path'] camera_api = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path'] + # cleanup + yield from query_req.release() + # Authticate to NAS to get a session id - syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - auth_path) - session_id = get_session_id(config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - syno_auth_url, - config.get(CONF_VALID_CERT)) + syno_auth_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, auth_path) + + session_id = yield from get_session_id( + hass, + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + syno_auth_url, + config.get(CONF_VALID_CERT) + ) # Use SessionID to get cameras in system - syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - camera_api) - camera_payload = {'api': CAMERA_API, - 'method': 'List', - 'version': '1'} - camera_req = requests.get(syno_camera_url, - params=camera_payload, - verify=config.get(CONF_VALID_CERT), - timeout=TIMEOUT, - cookies={'id': session_id}) - camera_resp = camera_req.json() + syno_camera_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, camera_api) + + camera_payload = { + 'api': CAMERA_API, + 'method': 'List', + 'version': '1' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + camera_req = yield from hass.websession.get( + syno_camera_url, + params=camera_payload, + verify_ssl=config.get(CONF_VALID_CERT), + cookies={'id': session_id} + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", syno_camera_url) + return False + + camera_resp = yield from camera_req.json() cameras = camera_resp['data']['cameras'] + yield from camera_req.release() + + # add cameras + devices = [] + tasks = [] for camera in cameras: if not config.get(CONF_WHITELIST): camera_id = camera['id'] snapshot_path = camera['snapshot_path'] - add_devices([SynologyCamera(config, - camera_id, - camera['name'], - snapshot_path, - streaming_path, - camera_path, - auth_path)]) + device = SynologyCamera( + config, + camera_id, + camera['name'], + snapshot_path, + streaming_path, + camera_path, + auth_path + ) + tasks.append(device.async_read_sid()) + devices.append(device) + + yield from asyncio.gather(*tasks, loop=hass.loop) + hass.loop.create_task(async_add_devices(devices)) -def get_session_id(username, password, login_url, valid_cert): +@asyncio.coroutine +def get_session_id(hass, username, password, login_url, valid_cert): """Get a session id.""" - auth_payload = {'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': username, - 'passwd': password, - 'session': 'SurveillanceStation', - 'format': 'sid'} - auth_req = requests.get(login_url, - params=auth_payload, - verify=valid_cert, - timeout=TIMEOUT) - auth_resp = auth_req.json() + auth_payload = { + 'api': AUTH_API, + 'method': 'Login', + 'version': '2', + 'account': username, + 'passwd': password, + 'session': 'SurveillanceStation', + 'format': 'sid' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + auth_req = yield from hass.websession.get( + login_url, + params=auth_payload, + verify_ssl=valid_cert + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", login_url) + return False + + auth_resp = yield from auth_req.json() + yield from auth_req.release() + return auth_resp['data']['sid'] @@ -148,74 +202,92 @@ class SynologyCamera(Camera): self._streaming_path = streaming_path self._camera_path = camera_path self._auth_path = auth_path + self._session_id = None - self._session_id = get_session_id(self._username, - self._password, - self._login_url, - self._valid_cert) - - def get_sid(self): + @asyncio.coroutine + def async_read_sid(self): """Get a session id.""" - auth_payload = {'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': self._username, - 'passwd': self._password, - 'session': 'SurveillanceStation', - 'format': 'sid'} - auth_req = requests.get(self._login_url, - params=auth_payload, - verify=self._valid_cert, - timeout=TIMEOUT) - auth_resp = auth_req.json() - self._session_id = auth_resp['data']['sid'] + self._session_id = yield from get_session_id( + self.hass, + self._username, + self._password, + self._login_url, + self._valid_cert + ) def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" - image_url = SYNO_API_URL.format(self._synology_url, - WEBAPI_PATH, - self._camera_path) - image_payload = {'api': CAMERA_API, - 'method': 'GetSnapshot', - 'version': '1', - 'cameraId': self._camera_id} + image_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._camera_path) + + image_payload = { + 'api': CAMERA_API, + 'method': 'GetSnapshot', + 'version': '1', + 'cameraId': self._camera_id + } try: - response = requests.get(image_url, - params=image_payload, - timeout=TIMEOUT, - verify=self._valid_cert, - cookies={'id': self._session_id}) - except requests.exceptions.RequestException as error: - _LOGGER.error('Error getting camera image: %s', error) + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + response = yield from self.hass.websession.get( + image_url, + params=image_payload, + verify_ssl=self._valid_cert, + cookies={'id': self._session_id} + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", image_url) return None - return response.content + image = yield from response.read() + yield from response.release() - def camera_stream(self): + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Return a MJPEG stream image response directly from the camera.""" - streaming_url = SYNO_API_URL.format(self._synology_url, - WEBAPI_PATH, - self._streaming_path) - streaming_payload = {'api': STREAMING_API, - 'method': 'Stream', - 'version': '1', - 'cameraId': self._camera_id, - 'format': 'mjpeg'} - response = requests.get(streaming_url, - payload=streaming_payload, - stream=True, - timeout=TIMEOUT, - cookies={'id': self._session_id}) - return response + streaming_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._streaming_path) - def mjpeg_steam(self, response): - """Generate an HTTP MJPEG Stream from the Synology NAS.""" - stream = self.camera_stream() - return response( - stream.iter_content(chunk_size=1024), - mimetype=stream.headers['CONTENT_TYPE_HEADER'], - direct_passthrough=True - ) + streaming_payload = { + 'api': STREAMING_API, + 'method': 'Stream', + 'version': '1', + 'cameraId': self._camera_id, + 'format': 'mjpeg' + } + try: + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + stream = yield from self.hass.websession.get( + streaming_url, + payload=streaming_payload, + verify_ssl=self._valid_cert, + cookies={'id': self._session_id} + ) + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.content.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.release()) + self.hass.loop.create_task(response.write_eof()) @property def name(self): diff --git a/homeassistant/core.py b/homeassistant/core.py index bd59db59f05..19f1436ffa3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,9 +17,9 @@ import threading import time from types import MappingProxyType - from typing import Optional, Any, Callable, List # NOQA +import aiohttp import voluptuous as vol from voluptuous.humanize import humanize_error @@ -143,6 +143,7 @@ class HomeAssistant(object): self.config = Config() # type: Config self.state = CoreState.not_running self.exit_code = None + self.websession = aiohttp.ClientSession(loop=self.loop) @property def is_running(self) -> bool: From 726d9505228e18ac9944d2dd61b652d9e458dc46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Oct 2016 21:45:35 -0700 Subject: [PATCH 058/149] Update aiohttp.py --- tests/test_util/aiohttp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index f2a33a3ac3c..9de94b50df8 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -15,6 +15,7 @@ class AiohttpClientMocker: self.mock_calls = [] def request(self, method, url, *, + auth=None, status=200, text=None, content=None, @@ -56,7 +57,7 @@ class AiohttpClientMocker: return len(self.mock_calls) @asyncio.coroutine - def match_request(self, method, url): + def match_request(self, method, url, *, auth=None): """Match a request against pre-registered requests.""" for response in self._mocks: if response.match_request(method, url): From 02d1dc62470293e13961aa3937a9c5d249946dfd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 28 Oct 2016 07:22:43 +0200 Subject: [PATCH 059/149] Upgrade psutil to 4.4.2 (#4079) --- homeassistant/components/sensor/systemmonitor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index e7a1db077aa..8bd9ad491b6 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==4.4.0'] +REQUIREMENTS = ['psutil==4.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9a4f0e2aec8..fd530d64088 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ pmsensor==0.3 proliphix==0.4.0 # homeassistant.components.sensor.systemmonitor -psutil==4.4.0 +psutil==4.4.2 # homeassistant.components.wink pubnub==3.8.2 From d8c1013b0986bce47a13bbfc8a3bcef099579c5d Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Fri, 28 Oct 2016 07:25:17 +0200 Subject: [PATCH 060/149] Zwave climate, add operating state to attributes (#4069) * Zwave climate, add operating state to attributes * Reversed assisgnment --- homeassistant/components/climate/zwave.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 3e12d4c6006..3b7fce9ace1 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -84,6 +84,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_temperature = None self._current_operation = None self._operation_list = None + self._operating_state = None self._current_fan_mode = None self._fan_list = None self._current_swing_mode = None @@ -120,6 +121,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self.update_ha_state() _LOGGER.debug("Value changed on network %s", value) + # pylint: disable=too-many-branches def update_properties(self): """Callback on data change for the registered node/value pair.""" # Operation Mode @@ -182,6 +184,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Device can't set setpoint based on operation mode." " Defaulting to index=1") self._target_temperature = int(value.data) + # Operating state + for value in (self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE) + .values()): + self._operating_state = value.data @property def should_poll(self): @@ -323,3 +330,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): value.index == 33: value.data = bytes(swing_mode, 'utf-8') break + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + if self._operating_state: + return { + "operating_state": self._operating_state, + } + else: + return {} From 65bd7d2326f09c732dea172f5f1ca01c19e7b560 Mon Sep 17 00:00:00 2001 From: Abhishek Anand Date: Fri, 28 Oct 2016 01:34:22 -0400 Subject: [PATCH 061/149] Generalized REST switch to enable templating and configurable timeout. (#3329) * successfully tested the "remote temperature mode" switch for the radio thermostat * removed logging and interpreted None as Off. * turn_off value is also templated now -- can depend on state Also, undid accidental removal of error logging. * ensured backward compatibility of config file if value_template is not provided, the update function behaves as before * ran autopep8 --in-place * fixed another complaint of tox * addressed the comments of balloob * undid acccidental log.error to log.info * timeout : 50 -> 10 * added a timeout parameter * removed the stray '-', better names for the failure case * string comparisons after .lower(), as suggested by balloob * addressed balloob's latest requests * making flake happy * value_template --> is_on_template in config file * moved CONF_IS_ON_TEMPLATE to local file * null checks * addressed flake error * properly comparing template text when is_on is not a template. --- homeassistant/components/switch/rest.py | 64 +++++++++++++++++++------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index ee29dd13adb..e6ac231e3e1 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -10,7 +10,8 @@ import requests import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_RESOURCE) +from homeassistant.const import ( + CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv CONF_BODY_OFF = 'body_off' @@ -18,12 +19,16 @@ CONF_BODY_ON = 'body_on' DEFAULT_BODY_OFF = 'OFF' DEFAULT_BODY_ON = 'ON' DEFAULT_NAME = 'REST Switch' +DEFAULT_TIMEOUT = 10 +CONF_IS_ON_TEMPLATE = 'is_on_template' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.string, - vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.string, + vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, + vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_IS_ON_TEMPLATE): cv.template, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) _LOGGER = logging.getLogger(__name__) @@ -36,6 +41,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): resource = config.get(CONF_RESOURCE) body_on = config.get(CONF_BODY_ON) body_off = config.get(CONF_BODY_OFF) + is_on_template = config.get(CONF_IS_ON_TEMPLATE) + + if is_on_template is not None: + is_on_template.hass = hass + if body_on is not None: + body_on.hass = hass + if body_off is not None: + body_off.hass = hass + timeout = config.get(CONF_TIMEOUT) try: requests.get(resource, timeout=10) @@ -47,14 +61,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGER.error("No route to resource/endpoint: %s", resource) return False - add_devices_callback([RestSwitch(hass, name, resource, body_on, body_off)]) + add_devices_callback( + [RestSwitch(hass, name, resource, + body_on, body_off, is_on_template, timeout)]) # pylint: disable=too-many-arguments class RestSwitch(SwitchDevice): """Representation of a switch that can be toggled using REST.""" - def __init__(self, hass, name, resource, body_on, body_off): + # pylint: disable=too-many-instance-attributes + def __init__(self, hass, name, resource, body_on, body_off, + is_on_template, timeout): """Initialize the REST switch.""" self._state = None self._hass = hass @@ -62,6 +80,8 @@ class RestSwitch(SwitchDevice): self._resource = resource self._body_on = body_on self._body_off = body_off + self._is_on_template = is_on_template + self._timeout = timeout @property def name(self): @@ -75,9 +95,10 @@ class RestSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the device on.""" + body_on_t = self._body_on.render() request = requests.post(self._resource, - data=self._body_on, - timeout=10) + data=body_on_t, + timeout=self._timeout) if request.status_code == 200: self._state = True else: @@ -86,9 +107,10 @@ class RestSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the device off.""" + body_off_t = self._body_off.render() request = requests.post(self._resource, - data=self._body_off, - timeout=10) + data=body_off_t, + timeout=self._timeout) if request.status_code == 200: self._state = False else: @@ -97,10 +119,22 @@ class RestSwitch(SwitchDevice): def update(self): """Get the latest data from REST API and update the state.""" - request = requests.get(self._resource, timeout=10) - if request.text == self._body_on: - self._state = True - elif request.text == self._body_off: - self._state = False + request = requests.get(self._resource, timeout=self._timeout) + + if self._is_on_template is not None: + response = self._is_on_template.render_with_possible_json_value( + request.text, 'None') + response = response.lower() + if response == 'true': + self._state = True + elif response == 'false': + self._state = False + else: + self._state = None else: - self._state = None + if request.text == self._body_on.template: + self._state = True + elif request.text == self._body_off.template: + self._state = False + else: + self._state = None From 825ee3612d1f862c30d9e39535694136754b1497 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Oct 2016 21:26:52 +0200 Subject: [PATCH 062/149] fix some comments spell (#4082) * fix some comments * fix in an executor * address paulus comments --- homeassistant/bootstrap.py | 7 +++---- homeassistant/config.py | 14 ++++++++++---- homeassistant/loader.py | 3 +-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 26dc2b977c6..6a0d6540688 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -48,7 +48,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, config: Optional[Dict]=None) -> bool: """Setup a component and all its dependencies. - This method need to run in a executor. + This method is a coroutine. """ if domain in hass.config.components: _LOGGER.debug('Component %s already set up.', domain) @@ -79,8 +79,7 @@ def _handle_requirements(hass: core.HomeAssistant, component, name: str) -> bool: """Install the requirements for a component. - Asyncio don't support file operation jet. - This method need to run in a executor. + This method needs to run in an executor. """ if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'): return True @@ -539,7 +538,7 @@ def log_exception(ex, domain, config, hass): def async_log_exception(ex, domain, config, hass): """Generate log exception for config validation. - Need to run in a async loop. + This method must be run in the event loop. """ message = 'Invalid config for [{}]: '.format(domain) _PERSISTENT_VALIDATION.add(domain) diff --git a/homeassistant/config.py b/homeassistant/config.py index 0e2192b3152..bde0f648354 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -133,6 +133,7 @@ def create_default_config(config_dir, detect_location=True): """Create a default configuration file in given configuration directory. Return path to new config file if success, None if failed. + This method needs to run in an executor. """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) version_path = os.path.join(config_dir, VERSION_FILE) @@ -200,14 +201,20 @@ def async_hass_config_yaml(hass): def find_config_file(config_dir): - """Look in given directory for supported configuration files.""" + """Look in given directory for supported configuration files. + + Async friendly. + """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) return config_path if os.path.isfile(config_path) else None def load_yaml_config_file(config_path): - """Parse a YAML configuration file.""" + """Parse a YAML configuration file. + + This method needs to run in an executor. + """ conf_dict = load_yaml(config_path) if not isinstance(conf_dict, dict): @@ -222,8 +229,7 @@ def load_yaml_config_file(config_path): def process_ha_config_upgrade(hass): """Upgrade config if necessary. - Asyncio don't support file operation jet. - This method need to run in a executor. + This method needs to run in an executor. """ version_path = hass.config.path(VERSION_FILE) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a19d835543d..dc68d3f1d46 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -42,8 +42,7 @@ _LOGGER = logging.getLogger(__name__) def prepare(hass: 'HomeAssistant'): """Prepare the loading of components. - Asyncio don't support file operation jet. - This method need to run in a executor. + This method needs to run in an executor. """ global PREPARED # pylint: disable=global-statement From 66541a6a19efed529f1d1ad7c639725274a95fb5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 29 Oct 2016 00:12:53 +0200 Subject: [PATCH 063/149] Update ha-ffmpeg to version 0.15 (#4096) --- homeassistant/components/camera/ffmpeg.py | 2 +- homeassistant/components/ffmpeg.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 85567eca18e..9bcb0c735a9 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from aiohttp import web -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index dea9e2f1bcf..f345153e666 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==0.14"] +REQUIREMENTS = ["ha-ffmpeg==0.15"] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fd530d64088..83c165524e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -131,7 +131,7 @@ gps3==0.33.3 ha-alpr==0.3 # homeassistant.components.ffmpeg -ha-ffmpeg==0.14 +ha-ffmpeg==0.15 # homeassistant.components.mqtt.server hbmqtt==0.7.1 From 9afe066ec85042dc7cbbd59983d1bd6d284298a7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 29 Oct 2016 04:01:14 +0200 Subject: [PATCH 064/149] Fix name in openalpr cloud api (#4097) --- homeassistant/components/openalpr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openalpr.py b/homeassistant/components/openalpr.py index 35793c89144..700437cedf1 100644 --- a/homeassistant/components/openalpr.py +++ b/homeassistant/components/openalpr.py @@ -26,7 +26,7 @@ DOMAIN = 'openalpr' DEPENDENCIES = ['ffmpeg'] REQUIREMENTS = [ 'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/' - 'python-1.0.2.zip#cloud_api==1.0.2', + 'python-1.0.2.zip#openalpr_api==1.0.2', 'ha-alpr==0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 83c165524e6..d777397e2f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a8 https://github.com/nkgilley/python-join-api/archive/3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1 # homeassistant.components.openalpr -https://github.com/pvizeli/cloudapi/releases/download/1.0.2/python-1.0.2.zip#cloud_api==1.0.2 +https://github.com/pvizeli/cloudapi/releases/download/1.0.2/python-1.0.2.zip#openalpr_api==1.0.2 # homeassistant.components.switch.edimax https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 From 230c3815f2e1356d1b551aef34c07d62de73f593 Mon Sep 17 00:00:00 2001 From: devdelay Date: Fri, 28 Oct 2016 22:03:40 -0400 Subject: [PATCH 065/149] Update command_line sensor to use STATE_UNKNOWN (#4093) --- homeassistant/components/sensor/command_line.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index ff376c8d02f..7409ae1de26 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND) + CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND, + STATE_UNKNOWN) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -79,9 +80,11 @@ class CommandSensor(Entity): self.data.update() value = self.data.value - if self._value_template is not None: + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( - value, 'N/A') + value, STATE_UNKNOWN) else: self._state = value From bf92aedd38c9052a18d9a9836b501485d684c86a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 29 Oct 2016 04:06:24 +0200 Subject: [PATCH 066/149] Add hddtemp sensor (#4092) --- .coveragerc | 1 + homeassistant/components/sensor/hddtemp.py | 124 +++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 homeassistant/components/sensor/hddtemp.py diff --git a/.coveragerc b/.coveragerc index 03e145a74de..ab5ead454e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -252,6 +252,7 @@ omit = homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/haveibeenpwned.py + homeassistant/components/sensor/hddtemp.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py diff --git a/homeassistant/components/sensor/hddtemp.py b/homeassistant/components/sensor/hddtemp.py new file mode 100644 index 00000000000..a4308535fe2 --- /dev/null +++ b/homeassistant/components/sensor/hddtemp.py @@ -0,0 +1,124 @@ +""" +Support for getting the disk temperature of a host. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.hddtemp/ +""" +import logging +from datetime import timedelta +from telnetlib import Telnet + +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEVICE = 'device' +ATTR_MODEL = 'model' + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 7634 +DEFAULT_NAME = 'HD Temperature' +DEFAULT_TIMEOUT = 5 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the HDDTemp sensor.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + + hddtemp = HddTempData(host, port) + hddtemp.update() + + if hddtemp.data is None: + _LOGGER.error("Unable to fetch the data from %s:%s", host, port) + return False + + add_devices([HddTempSensor(name, hddtemp)]) + + +class HddTempSensor(Entity): + """Representation of a HDDTemp sensor.""" + + def __init__(self, name, hddtemp): + """Initialize a HDDTemp sensor.""" + self.hddtemp = hddtemp + self._name = name + self._state = False + self._details = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if self.details[4] == 'C': + return TEMP_CELSIUS + else: + return TEMP_FAHRENHEIT + + @property + def state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_DEVICE: self.details[1], + ATTR_MODEL: self.details[2], + } + + def update(self): + """Get the latest data from HDDTemp daemon and updates the state.""" + self.hddtemp.update() + + if self.hddtemp.data is not None: + self.details = self.hddtemp.data.split('|') + self._state = self.details[3] + else: + self._state = STATE_UNKNOWN + + +# pylint: disable=too-few-public-methods +class HddTempData(object): + """Get the latest data from HDDTemp and update the states.""" + + def __init__(self, host, port): + """Initialize the data object.""" + self.host = host + self.port = port + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from hhtemp running as daemon.""" + try: + connection = Telnet( + host=self.host, port=self.port, timeout=DEFAULT_TIMEOUT) + self.data = connection.read_all().decode('ascii') + except ConnectionRefusedError: + _LOGGER.error('HDDTemp is not available at %s:%s', self.host, + self.port) + self.data = None From 9d836a115a093b8434586d747d1baea08f55c5e3 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 28 Oct 2016 22:18:31 -0400 Subject: [PATCH 067/149] Add zone_ignore option for yamaha. (#4091) * Add zone_ignore option for yamaha. We attempt to discover all zones for yamaha receivers. There are times when users may want to suppress some zones from showing up. When a Zone isn't actually connected to speakers, or on some newer receivers where Zone_4 is an HDMI only zone, that doesn't support even basic media_player UI. This provide a mechanism for users to do that. Fixes #4088 * Update yamaha.py --- homeassistant/components/media_player/yamaha.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 40ca9151e50..b7dfa15cede 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -26,6 +26,7 @@ SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' +CONF_ZONE_IGNORE = 'zone_ignore' DEFAULT_NAME = 'Yamaha Receiver' @@ -34,10 +35,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_SOURCE_IGNORE, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_ZONE_IGNORE, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SOURCE_NAMES, default={}): {cv.string: cv.string}, }) +# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yamaha platform.""" import rxv @@ -46,6 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = config.get(CONF_HOST) source_ignore = config.get(CONF_SOURCE_IGNORE) source_names = config.get(CONF_SOURCE_NAMES) + zone_ignore = config.get(CONF_ZONE_IGNORE) if discovery_info is not None: name = discovery_info[0] @@ -66,9 +71,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host) receivers = rxv.RXV(ctrl_url, name).zone_controllers() - add_devices( - YamahaDevice(name, receiver, source_ignore, source_names) - for receiver in receivers) + for receiver in receivers: + if receiver.zone not in zone_ignore: + add_devices([ + YamahaDevice(name, receiver, source_ignore, source_names)]) class YamahaDevice(MediaPlayerDevice): From 5a2b4a5376ce52fd77f8b8220a1e6cdaaa38107d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2016 08:57:59 -0700 Subject: [PATCH 068/149] Core Async improvements (#4087) * Clean up HomeAssistant.start * Add missing pieces to remote HA constructor * Make HomeAssistant constructor async safe * Code cleanup * Init websession lazy --- homeassistant/core.py | 110 ++++++++++++++++++++-------------------- homeassistant/remote.py | 7 ++- tests/common.py | 29 +++++------ 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 19f1436ffa3..f6743a40ef5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -143,69 +143,33 @@ class HomeAssistant(object): self.config = Config() # type: Config self.state = CoreState.not_running self.exit_code = None - self.websession = aiohttp.ClientSession(loop=self.loop) + self._websession = None @property def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) + @property + def websession(self): + """Return an aiohttp session to make web requests.""" + if self._websession is None: + self._websession = aiohttp.ClientSession(loop=self.loop) + + return self._websession + def start(self) -> None: """Start home assistant.""" - _LOGGER.info( - "Starting Home Assistant (%d threads)", self.pool.worker_count) - self.state = CoreState.starting - # Register the async start self.loop.create_task(self.async_start()) - @callback - def stop_homeassistant(*args): - """Stop Home Assistant.""" - self.exit_code = 0 - self.async_add_job(self.async_stop) - - @callback - def restart_homeassistant(*args): - """Restart Home Assistant.""" - self.exit_code = RESTART_EXIT_CODE - self.async_add_job(self.async_stop) - - # Register the restart/stop event - self.loop.call_soon( - self.services.async_register, - DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant - ) - self.loop.call_soon( - self.services.async_register, - DOMAIN, SERVICE_HOMEASSISTANT_RESTART, restart_homeassistant - ) - - # Setup signal handling - if sys.platform != 'win32': - try: - self.loop.add_signal_handler( - signal.SIGTERM, - stop_homeassistant - ) - except ValueError: - _LOGGER.warning('Could not bind to SIGTERM.') - - try: - self.loop.add_signal_handler( - signal.SIGHUP, - restart_homeassistant - ) - except ValueError: - _LOGGER.warning('Could not bind to SIGHUP.') - # Run forever and catch keyboard interrupt try: # Block until stopped _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() except KeyboardInterrupt: - self.loop.call_soon(stop_homeassistant) + self.loop.call_soon(self._async_stop_handler) self.loop.run_forever() finally: self.loop.close() @@ -216,6 +180,31 @@ class HomeAssistant(object): This method is a coroutine. """ + _LOGGER.info( + "Starting Home Assistant (%d threads)", self.pool.worker_count) + + self.state = CoreState.starting + + # Register the restart/stop event + self.services.async_register( + DOMAIN, SERVICE_HOMEASSISTANT_STOP, self._async_stop_handler) + self.services.async_register( + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, self._async_restart_handler) + + # Setup signal handling + if sys.platform != 'win32': + try: + self.loop.add_signal_handler( + signal.SIGTERM, self._async_stop_handler) + except ValueError: + _LOGGER.warning('Could not bind to SIGTERM.') + + try: + self.loop.add_signal_handler( + signal.SIGHUP, self._async_restart_handler) + except ValueError: + _LOGGER.warning('Could not bind to SIGHUP.') + # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() _async_create_timer(self) @@ -301,10 +290,7 @@ class HomeAssistant(object): # sleep in the loop executor, this forces execution back into # the event loop to avoid the block thread from starving the # async loop - run_coroutine_threadsafe( - sleep_wait(), - self.loop - ).result() + run_coroutine_threadsafe(sleep_wait(), self.loop).result() complete.set() @@ -326,10 +312,13 @@ class HomeAssistant(object): yield from self.loop.run_in_executor(None, self.pool.block_till_done) yield from self.loop.run_in_executor(None, self.pool.stop) self.executor.shutdown() + if self._websession is not None: + yield from self._websession.close() self.state = CoreState.not_running self.loop.stop() # pylint: disable=no-self-use + @callback def _async_exception_handler(self, loop, context): """Handle all exception inside the core loop.""" message = context.get('message') @@ -348,6 +337,18 @@ class HomeAssistant(object): exc_info=exc_info ) + @callback + def _async_stop_handler(self, *args): + """Stop Home Assistant.""" + self.exit_code = 0 + self.async_add_job(self.async_stop) + + @callback + def _async_restart_handler(self, *args): + """Restart Home Assistant.""" + self.exit_code = RESTART_EXIT_CODE + self.async_add_job(self.async_stop) + class EventOrigin(enum.Enum): """Represent the origin of an event.""" @@ -877,10 +878,7 @@ class ServiceRegistry(object): self._bus = bus self._loop = loop self._cur_id = 0 - run_callback_threadsafe( - loop, - bus.async_listen, EVENT_CALL_SERVICE, self._event_to_service_call, - ) + self._async_unsub_call_event = None @property def services(self): @@ -947,6 +945,10 @@ class ServiceRegistry(object): else: self._services[domain] = {service: service_obj} + if self._async_unsub_call_event is None: + self._async_unsub_call_event = self._bus.async_listen( + EVENT_CALL_SERVICE, self._event_to_service_call) + self._bus.async_fire( EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} diff --git a/homeassistant/remote.py b/homeassistant/remote.py index ce20eb4ce0d..94ac2899c69 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -8,6 +8,7 @@ For more details about the Python API, please refer to the documentation at https://home-assistant.io/developers/python_api/ """ import asyncio +from concurrent.futures import ThreadPoolExecutor from datetime import datetime import enum import json @@ -124,14 +125,18 @@ class HomeAssistant(ha.HomeAssistant): self.remote_api = remote_api self.loop = loop or asyncio.get_event_loop() + self.executor = ThreadPoolExecutor(max_workers=5) + self.loop.set_default_executor(self.executor) + self.loop.set_exception_handler(self._async_exception_handler) self.pool = ha.create_worker_pool() self.bus = EventBus(remote_api, self) self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) self.states = StateMachine(self.bus, self.loop, self.remote_api) self.config = ha.Config() - self.state = ha.CoreState.not_running + self._websession = None + self.state = ha.CoreState.not_running self.config.api = local_api def start(self): diff --git a/tests/common.py b/tests/common.py index 8896a97881b..4f2f447c1b0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -92,26 +92,21 @@ def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" loop._thread_ident = threading.get_ident() - def get_hass(): - """Temp while we migrate core HASS over to be async constructors.""" - hass = ha.HomeAssistant(loop) + hass = ha.HomeAssistant(loop) - hass.config.location_name = 'test home' - hass.config.config_dir = get_test_config_dir() - hass.config.latitude = 32.87336 - hass.config.longitude = -117.22743 - hass.config.elevation = 0 - hass.config.time_zone = date_util.get_time_zone('US/Pacific') - hass.config.units = METRIC_SYSTEM - hass.config.skip_pip = True + hass.config.location_name = 'test home' + hass.config.config_dir = get_test_config_dir() + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 + hass.config.elevation = 0 + hass.config.time_zone = date_util.get_time_zone('US/Pacific') + hass.config.units = METRIC_SYSTEM + hass.config.skip_pip = True - if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: - loader.prepare(hass) + if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: + yield from loop.run_in_executor(None, loader.prepare, hass) - hass.state = ha.CoreState.running - return hass - - hass = yield from loop.run_in_executor(None, get_hass) + hass.state = ha.CoreState.running return hass From d4b3f56d5344c80ecdd0f79836b9120368e619e1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 29 Oct 2016 18:12:43 +0200 Subject: [PATCH 069/149] Maintenance (#4101) * UPdate ordering, fix typos, and align logger messages * Update import style, fix PEP257 issue, and align logger messages * Updaate import style and align logger messages * Update import style and align logger messages * Update ordering * Update import style and ordering * Update quotes * Make logger messages more clear * Fix indentation --- .../alarm_control_panel/concord232.py | 34 +++--- .../alarm_control_panel/envisalink.py | 51 +++++---- .../components/binary_sensor/concord232.py | 58 +++++----- .../components/notify/message_bird.py | 2 +- .../components/notify/nfandroidtv.py | 100 ++++++++---------- homeassistant/components/notify/telstra.py | 37 +++---- homeassistant/components/notify/twitter.py | 23 ++-- homeassistant/components/notify/webostv.py | 18 ++-- homeassistant/components/notify/xmpp.py | 2 +- 9 files changed, 150 insertions(+), 175 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 0e0fd026b60..0bdcf274c08 100755 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -4,23 +4,19 @@ Support for Concord232 alarm control panels. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.concord232/ """ - import datetime - import logging +import requests +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_UNKNOWN) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -import requests - -import voluptuous as vol - REQUIREMENTS = ['concord232==0.14'] _LOGGER = logging.getLogger(__name__) @@ -29,17 +25,17 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'CONCORD232' DEFAULT_PORT = 5007 +SCAN_INTERVAL = 1 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, }) -SCAN_INTERVAL = 1 - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup concord232 platform.""" + """Set up the Concord232 alarm control panel platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -49,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: add_devices([Concord232Alarm(hass, url, name)]) except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False @@ -57,7 +53,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): """Represents the Concord232-based alarm panel.""" def __init__(self, hass, url, name): - """Initalize the concord232 alarm panel.""" + """Initialize the Concord232 alarm panel.""" from concord232 import client as concord232_client self._state = STATE_UNKNOWN @@ -68,7 +64,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): try: client = concord232_client.Client(self._url) except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) self._alarm = client self._alarm.partitions = self._alarm.list_partitions() @@ -100,16 +96,16 @@ class Concord232Alarm(alarm.AlarmControlPanel): try: part = self._alarm.list_partitions()[0] except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to %(host)s: %(reason)s', + _LOGGER.error("Unable to connect to %(host)s: %(reason)s", dict(host=self._url, reason=ex)) newstate = STATE_UNKNOWN except IndexError: - _LOGGER.error('concord232 reports no partitions') + _LOGGER.error("Concord232 reports no partitions") newstate = STATE_UNKNOWN - if part['arming_level'] == "Off": + if part['arming_level'] == 'Off': newstate = STATE_ALARM_DISARMED - elif "Home" in part['arming_level']: + elif 'Home' in part['arming_level']: newstate = STATE_ALARM_ARMED_HOME else: newstate = STATE_ALARM_ARMED_AWAY diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index ff1ec2cc7b7..5c5dd1729b2 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -5,25 +5,22 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.envisalink/ """ import logging + import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.envisalink import (EVL_CONTROLLER, - EnvisalinkDevice, - PARTITION_SCHEMA, - CONF_CODE, - CONF_PANIC, - CONF_PARTITIONNAME, - SIGNAL_PARTITION_UPDATE, - SIGNAL_KEYPAD_UPDATE) +from homeassistant.components.envisalink import ( + EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, + CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, STATE_ALARM_TRIGGERED) -DEPENDENCIES = ['envisalink'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['envisalink'] + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Envisalink alarm panels.""" _configured_partitions = discovery_info['partitions'] _code = discovery_info[CONF_CODE] @@ -38,30 +35,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _panic_type, EVL_CONTROLLER.alarm_state['partition'][part_num], EVL_CONTROLLER) - add_devices_callback([_device]) + add_devices([_device]) return True class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): - """Represents the Envisalink-based alarm panel.""" + """Representation of an Envisalink-based alarm panel.""" # pylint: disable=too-many-arguments - def __init__(self, partition_number, alarm_name, - code, panic_type, info, controller): + def __init__(self, partition_number, alarm_name, code, panic_type, info, + controller): """Initialize the alarm panel.""" from pydispatch import dispatcher self._partition_number = partition_number self._code = code self._panic_type = panic_type - _LOGGER.debug('Setting up alarm: ' + alarm_name) + _LOGGER.debug("Setting up alarm: %s", alarm_name) EnvisalinkDevice.__init__(self, alarm_name, info, controller) - dispatcher.connect(self._update_callback, - signal=SIGNAL_PARTITION_UPDATE, - sender=dispatcher.Any) - dispatcher.connect(self._update_callback, - signal=SIGNAL_KEYPAD_UPDATE, - sender=dispatcher.Any) + dispatcher.connect( + self._update_callback, signal=SIGNAL_PARTITION_UPDATE, + sender=dispatcher.Any) + dispatcher.connect( + self._update_callback, signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) def _update_callback(self, partition): """Update HA state, if needed.""" @@ -90,20 +87,20 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" if self._code: - EVL_CONTROLLER.disarm_partition(str(code), - self._partition_number) + EVL_CONTROLLER.disarm_partition( + str(code), self._partition_number) def alarm_arm_home(self, code=None): """Send arm home command.""" if self._code: - EVL_CONTROLLER.arm_stay_partition(str(code), - self._partition_number) + EVL_CONTROLLER.arm_stay_partition( + str(code), self._partition_number) def alarm_arm_away(self, code=None): """Send arm away command.""" if self._code: - EVL_CONTROLLER.arm_away_partition(str(code), - self._partition_number) + EVL_CONTROLLER.arm_away_partition( + str(code), self._partition_number) def alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index bc1eab4694a..48f36c00697 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -5,20 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.concord232/ """ import datetime - import logging +import requests +import voluptuous as vol + from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES) from homeassistant.const import (CONF_HOST, CONF_PORT) - import homeassistant.helpers.config_validation as cv -import requests - -import voluptuous as vol - - REQUIREMENTS = ['concord232==0.14'] _LOGGER = logging.getLogger(__name__) @@ -27,9 +23,12 @@ CONF_EXCLUDE_ZONES = 'exclude_zones' CONF_ZONE_TYPES = 'zone_types' DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Alarm' DEFAULT_PORT = '5007' DEFAULT_SSL = False +SCAN_INTERVAL = 1 + ZONE_TYPES_SCHEMA = vol.Schema({ cv.positive_int: vol.In(SENSOR_CLASSES), }) @@ -42,14 +41,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, }) -SCAN_INTERVAL = 1 - -DEFAULT_NAME = "Alarm" - # pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Concord232 binary sensor platform.""" + """Set up the Concord232 binary sensor platform.""" from concord232 import client as concord232_client host = config.get(CONF_HOST) @@ -59,24 +54,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] try: - _LOGGER.debug('Initializing Client.') - client = concord232_client.Client('http://{}:{}' - .format(host, port)) + _LOGGER.debug("Initializing Client") + client = concord232_client.Client('http://{}:{}'.format(host, port)) client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False for zone in client.zones: - _LOGGER.info('Loading Zone found: %s', zone['name']) + _LOGGER.info("Loading Zone found: %s", zone['name']) if zone['number'] not in exclude: - sensors.append(Concord232ZoneSensor( - hass, - client, - zone, - zone_types.get(zone['number'], get_opening_type(zone)))) + sensors.append( + Concord232ZoneSensor( + hass, client, zone, zone_types.get(zone['number'], + get_opening_type(zone))) + ) add_devices(sensors) @@ -84,16 +78,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def get_opening_type(zone): - """Helper function to try to guess sensor type frm name.""" - if "MOTION" in zone["name"]: - return "motion" - if "KEY" in zone["name"]: - return "safety" - if "SMOKE" in zone["name"]: - return "smoke" - if "WATER" in zone["name"]: - return "water" - return "opening" + """Helper function to try to guess sensor type from name.""" + if 'MOTION' in zone['name']: + return 'motion' + if 'KEY' in zone['name']: + return 'safety' + if 'SMOKE' in zone['name']: + return 'smoke' + if 'WATER' in zone['name']: + return 'water' + return 'opening' class Concord232ZoneSensor(BinarySensorDevice): diff --git a/homeassistant/components/notify/message_bird.py b/homeassistant/components/notify/message_bird.py index 5e7cc9eaf17..11106024111 100644 --- a/homeassistant/components/notify/message_bird.py +++ b/homeassistant/components/notify/message_bird.py @@ -13,9 +13,9 @@ from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_API_KEY, CONF_SENDER -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['messagebird==1.2.0'] +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 598493d8fd0..9874733d4ef 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -6,14 +6,14 @@ https://home-assistant.io/components/notify.nfandroidtv/ """ import os import logging + import requests import voluptuous as vol -from homeassistant.components.notify import (ATTR_TITLE, - ATTR_TITLE_DEFAULT, - ATTR_DATA, - BaseNotificationService, - PLATFORM_SCHEMA) +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, BaseNotificationService, + PLATFORM_SCHEMA) +from homeassistant.const import CONF_TIMEOUT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,6 @@ CONF_POSITION = 'position' CONF_TRANSPARENCY = 'transparency' CONF_COLOR = 'color' CONF_INTERRUPT = 'interrupt' -CONF_TIMEOUT = 'timeout' DEFAULT_DURATION = 5 DEFAULT_POSITION = 'bottom-right' @@ -41,32 +40,32 @@ ATTR_BKGCOLOR = 'bkgcolor' ATTR_INTERRUPT = 'interrupt' POSITIONS = { - "bottom-right": 0, - "bottom-left": 1, - "top-right": 2, - "top-left": 3, - "center": 4, + 'bottom-right': 0, + 'bottom-left': 1, + 'top-right': 2, + 'top-left': 3, + 'center': 4, } TRANSPARENCIES = { - "default": 0, - "0%": 1, - "25%": 2, - "50%": 3, - "75%": 4, - "100%": 5, + 'default': 0, + '0%': 1, + '25%': 2, + '50%': 3, + '75%': 4, + '100%': 5, } COLORS = { - "grey": "#607d8b", - "black": "#000000", - "indigo": "#303F9F", - "green": "#4CAF50", - "red": "#F44336", - "cyan": "#00BCD4", - "teal": "#009688", - "amber": "#FFC107", - "pink": "#E91E63", + 'grey': '#607d8b', + 'black': '#000000', + 'indigo': '#303F9F', + 'green': '#4CAF50', + 'red': '#F44336', + 'cyan': '#00BCD4', + 'teal': '#009688', + 'amber': '#FFC107', + 'pink': '#E91E63', } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -95,13 +94,8 @@ def get_service(hass, config): interrupt = config.get(CONF_INTERRUPT) timeout = config.get(CONF_TIMEOUT) - return NFAndroidTVNotificationService(remoteip, - duration, - position, - transparency, - color, - interrupt, - timeout) + return NFAndroidTVNotificationService( + remoteip, duration, position, transparency, color, interrupt, timeout) # pylint: disable=too-many-instance-attributes @@ -109,20 +103,19 @@ class NFAndroidTVNotificationService(BaseNotificationService): """Notification service for Notifications for Android TV.""" # pylint: disable=too-many-arguments,too-few-public-methods - def __init__(self, remoteip, duration, position, transparency, - color, interrupt, timeout): + def __init__(self, remoteip, duration, position, transparency, color, + interrupt, timeout): """Initialize the service.""" - self._target = "http://%s:7676" % remoteip + self._target = 'http://{}:7676'.format(remoteip) self._default_duration = duration self._default_position = position self._default_transparency = transparency self._default_color = color self._default_interrupt = interrupt self._timeout = timeout - self._icon_file = os.path.join(os.path.dirname(__file__), "..", - "frontend", - "www_static", "icons", - "favicon-192x192.png") + self._icon_file = os.path.join( + os.path.dirname(__file__), '..', 'frontend', 'www_static', 'icons', + 'favicon-192x192.png') # pylint: disable=too-many-branches def send_message(self, message="", **kwargs): @@ -132,36 +125,36 @@ class NFAndroidTVNotificationService(BaseNotificationService): payload = dict(filename=('icon.png', open(self._icon_file, 'rb'), 'application/octet-stream', - {'Expires': '0'}), type="0", + {'Expires': '0'}), type='0', title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), msg=message, duration="%i" % self._default_duration, - position="%i" % POSITIONS.get(self._default_position), - bkgcolor="%s" % COLORS.get(self._default_color), - transparency="%i" % TRANSPARENCIES.get( + position='%i' % POSITIONS.get(self._default_position), + bkgcolor='%s' % COLORS.get(self._default_color), + transparency='%i' % TRANSPARENCIES.get( self._default_transparency), - offset="0", app=ATTR_TITLE_DEFAULT, force="true", - interrupt="%i" % self._default_interrupt) + offset='0', app=ATTR_TITLE_DEFAULT, force='true', + interrupt='%i' % self._default_interrupt) data = kwargs.get(ATTR_DATA) if data: if ATTR_DURATION in data: duration = data.get(ATTR_DURATION) try: - payload[ATTR_DURATION] = "%i" % int(duration) + payload[ATTR_DURATION] = '%i' % int(duration) except ValueError: _LOGGER.warning("Invalid duration-value: %s", str(duration)) if ATTR_POSITION in data: position = data.get(ATTR_POSITION) if position in POSITIONS: - payload[ATTR_POSITION] = "%i" % POSITIONS.get(position) + payload[ATTR_POSITION] = '%i' % POSITIONS.get(position) else: _LOGGER.warning("Invalid position-value: %s", str(position)) if ATTR_TRANSPARENCY in data: transparency = data.get(ATTR_TRANSPARENCY) if transparency in TRANSPARENCIES: - payload[ATTR_TRANSPARENCY] = "%i" % TRANSPARENCIES.get( + payload[ATTR_TRANSPARENCY] = '%i' % TRANSPARENCIES.get( transparency) else: _LOGGER.warning("Invalid transparency-value: %s", @@ -169,22 +162,21 @@ class NFAndroidTVNotificationService(BaseNotificationService): if ATTR_COLOR in data: color = data.get(ATTR_COLOR) if color in COLORS: - payload[ATTR_BKGCOLOR] = "%s" % COLORS.get(color) + payload[ATTR_BKGCOLOR] = '%s' % COLORS.get(color) else: _LOGGER.warning("Invalid color-value: %s", str(color)) if ATTR_INTERRUPT in data: interrupt = data.get(ATTR_INTERRUPT) try: - payload[ATTR_INTERRUPT] = "%i" % cv.boolean(interrupt) + payload[ATTR_INTERRUPT] = '%i' % cv.boolean(interrupt) except vol.Invalid: _LOGGER.warning("Invalid interrupt-value: %s", str(interrupt)) try: _LOGGER.debug("Payload: %s", str(payload)) - response = requests.post(self._target, - files=payload, - timeout=self._timeout) + response = requests.post( + self._target, files=payload, timeout=self._timeout) if response.status_code != 200: _LOGGER.error("Error sending message: %s", str(response)) except requests.exceptions.ConnectionError as err: diff --git a/homeassistant/components/notify/telstra.py b/homeassistant/components/notify/telstra.py index 2bd76989eaa..2fd554a278c 100644 --- a/homeassistant/components/notify/telstra.py +++ b/homeassistant/components/notify/telstra.py @@ -9,12 +9,13 @@ import logging import requests import voluptuous as vol -from homeassistant.components.notify import (BaseNotificationService, - ATTR_TITLE, - PLATFORM_SCHEMA) +from homeassistant.components.notify import ( + BaseNotificationService, ATTR_TITLE, PLATFORM_SCHEMA) from homeassistant.const import CONTENT_TYPE_JSON import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + CONF_CONSUMER_KEY = 'consumer_key' CONF_CONSUMER_SECRET = 'consumer_secret' CONF_PHONE_NUMBER = 'phone_number' @@ -25,8 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PHONE_NUMBER): cv.string, }) -_LOGGER = logging.getLogger(__name__) - def get_service(hass, config): """Get the Telstra SMS API notification service.""" @@ -34,14 +33,12 @@ def get_service(hass, config): consumer_secret = config.get(CONF_CONSUMER_SECRET) phone_number = config.get(CONF_PHONE_NUMBER) - # Attempt an initial authentication to confirm credentials if _authenticate(consumer_key, consumer_secret) is False: _LOGGER.exception('Error obtaining authorization from Telstra API') return None - return TelstraNotificationService(consumer_key, - consumer_secret, - phone_number) + return TelstraNotificationService( + consumer_key, consumer_secret, phone_number) # pylint: disable=too-few-public-methods, too-many-arguments @@ -59,10 +56,10 @@ class TelstraNotificationService(BaseNotificationService): title = kwargs.get(ATTR_TITLE) # Retrieve authorization first - token_response = _authenticate(self._consumer_key, - self._consumer_secret) + token_response = _authenticate( + self._consumer_key, self._consumer_secret) if token_response is False: - _LOGGER.exception('Error obtaining authorization from Telstra API') + _LOGGER.exception("Error obtaining authorization from Telstra API") return # Send the SMS @@ -73,17 +70,16 @@ class TelstraNotificationService(BaseNotificationService): message_data = { 'to': self._phone_number, - 'body': text + 'body': text, } message_resource = 'https://api.telstra.com/v1/sms/messages' message_headers = { 'Content-Type': CONTENT_TYPE_JSON, - 'Authorization': 'Bearer ' + token_response['access_token'] + 'Authorization': 'Bearer ' + token_response['access_token'], } - message_response = requests.post(message_resource, - headers=message_headers, - json=message_data, - timeout=10) + message_response = requests.post( + message_resource, headers=message_headers, json=message_data, + timeout=10) if message_response.status_code != 202: _LOGGER.exception("Failed to send SMS. Status code: %d", @@ -99,9 +95,8 @@ def _authenticate(consumer_key, consumer_secret): 'scope': 'SMS' } token_resource = 'https://api.telstra.com/v1/oauth/token' - token_response = requests.get(token_resource, - params=token_data, - timeout=10).json() + token_response = requests.get( + token_resource, params=token_data, timeout=10).json() if 'error' in token_response: return False diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index bafdc2403be..9a438df41da 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -9,16 +9,17 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (PLATFORM_SCHEMA, - BaseNotificationService) +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_ACCESS_TOKEN -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['TwitterAPI==2.4.2'] -CONF_CONSUMER_KEY = "consumer_key" -CONF_CONSUMER_SECRET = "consumer_secret" -CONF_ACCESS_TOKEN_SECRET = "access_token_secret" +_LOGGER = logging.getLogger(__name__) + +CONF_CONSUMER_KEY = 'consumer_key' +CONF_CONSUMER_SECRET = 'consumer_secret' +CONF_ACCESS_TOKEN_SECRET = 'access_token_secret' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CONSUMER_KEY): cv.string, @@ -30,15 +31,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config): """Get the Twitter notification service.""" - return TwitterNotificationService(config[CONF_CONSUMER_KEY], - config[CONF_CONSUMER_SECRET], - config[CONF_ACCESS_TOKEN], - config[CONF_ACCESS_TOKEN_SECRET]) + return TwitterNotificationService( + config[CONF_CONSUMER_KEY], config[CONF_CONSUMER_SECRET], + config[CONF_ACCESS_TOKEN], config[CONF_ACCESS_TOKEN_SECRET] + ) # pylint: disable=too-few-public-methods class TwitterNotificationService(BaseNotificationService): - """Implement notification service for the Twitter service.""" + """Implementation of a notification service for the Twitter service.""" def __init__(self, consumer_key, consumer_secret, access_token_key, access_token_secret): diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index e8276255925..8a9dee1e49d 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -9,15 +9,15 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import (BaseNotificationService, - PLATFORM_SCHEMA) +from homeassistant.components.notify import ( + BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import CONF_HOST -_LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' - '/archive/v0.1.2.zip' +REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip' '#pylgtv==0.1.2'] +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -34,10 +34,10 @@ def get_service(hass, config): try: client.register() except PyLGTVPairException: - _LOGGER.error('Pairing failed.') + _LOGGER.error("Pairing with TV failed") return None except OSError: - _LOGGER.error('Host unreachable.') + _LOGGER.error("TV unreachable") return None return LgWebOSNotificationService(client) @@ -58,6 +58,6 @@ class LgWebOSNotificationService(BaseNotificationService): try: self._client.send_message(message) except PyLGTVPairException: - _LOGGER.error('Pairing failed.') + _LOGGER.error("Pairing with TV failed") except OSError: - _LOGGER.error('Host unreachable.') + _LOGGER.error("TV unreachable") diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index cbe6da89d81..ed46060a410 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -53,7 +53,7 @@ class XmppNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - data = "{}: {}".format(title, message) if title else message + data = '{}: {}'.format(title, message) if title else message send_message(self._sender + '/home-assistant', self._password, self._recipient, self._tls, data) From 08a65a3b31eba44ad094a8473bc13df3662eb4d3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 29 Oct 2016 21:19:27 +0200 Subject: [PATCH 070/149] Async input_*/zone migration (#4095) * Async input_* * Async zone component * rename service callback --- homeassistant/components/input_boolean.py | 29 ++++++++------- homeassistant/components/input_select.py | 45 +++++++++++++---------- homeassistant/components/input_slider.py | 21 ++++++----- homeassistant/components/zone.py | 15 +++++--- homeassistant/helpers/__init__.py | 6 ++- 5 files changed, 68 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 0477bb1dbfb..fdc514f957f 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -4,10 +4,12 @@ Component to keep track of user controlled booleans for within automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_boolean/ """ +import asyncio import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, STATE_ON) @@ -55,7 +57,8 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Set up input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -74,9 +77,10 @@ def setup(hass, config): if not entities: return False - def handler_service(service): + @callback + def async_handler_service(service): """Handle a calls to the input boolean services.""" - target_inputs = component.extract_from_service(service) + target_inputs = component.async_extract_from_service(service) for input_b in target_inputs: if service.service == SERVICE_TURN_ON: @@ -86,15 +90,14 @@ def setup(hass, config): else: input_b.toggle() - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handler_service, - schema=SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handler_service, - schema=SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TOGGLE, handler_service, - schema=SERVICE_SCHEMA) - - component.add_entities(entities) + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_handler_service, schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, async_handler_service, schema=SERVICE_SCHEMA) + yield from component.async_add_entities(entities) return True @@ -131,9 +134,9 @@ class InputBoolean(ToggleEntity): def turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) def turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index f94d8200d00..d309bf5c709 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -4,10 +4,12 @@ Component to offer a way to select an option from a list. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_select/ """ +import asyncio import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -86,7 +88,8 @@ def select_previous(hass, entity_id): }) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -102,41 +105,43 @@ def setup(hass, config): if not entities: return False - def select_option_service(call): + @callback + def async_select_option_service(call): """Handle a calls to the input select option service.""" - target_inputs = component.extract_from_service(call) + target_inputs = component.async_extract_from_service(call) for input_select in target_inputs: input_select.select_option(call.data[ATTR_OPTION]) - hass.services.register(DOMAIN, SERVICE_SELECT_OPTION, - select_option_service, - schema=SERVICE_SELECT_OPTION_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service, + schema=SERVICE_SELECT_OPTION_SCHEMA) - def select_next_service(call): + @callback + def async_select_next_service(call): """Handle a calls to the input select next service.""" - target_inputs = component.extract_from_service(call) + target_inputs = component.async_extract_from_service(call) for input_select in target_inputs: input_select.offset_index(1) - hass.services.register(DOMAIN, SERVICE_SELECT_NEXT, - select_next_service, - schema=SERVICE_SELECT_NEXT_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service, + schema=SERVICE_SELECT_NEXT_SCHEMA) - def select_previous_service(call): + @callback + def async_select_previous_service(call): """Handle a calls to the input select previous service.""" - target_inputs = component.extract_from_service(call) + target_inputs = component.async_extract_from_service(call) for input_select in target_inputs: input_select.offset_index(-1) - hass.services.register(DOMAIN, SERVICE_SELECT_PREVIOUS, - select_previous_service, - schema=SERVICE_SELECT_PREVIOUS_SCHEMA) - - component.add_entities(entities) + hass.services.async_register( + DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, + schema=SERVICE_SELECT_PREVIOUS_SCHEMA) + yield from component.async_add_entities(entities) return True @@ -186,11 +191,11 @@ class InputSelect(Entity): option, ', '.join(self._options)) return self._current_option = option - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) def offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index 91ebbd844fc..f83d643cb5d 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -4,10 +4,12 @@ Component to offer a way to select a value from a slider. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_slider/ """ +import asyncio import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -71,7 +73,8 @@ def select_value(hass, entity_id, value): }) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Set up input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -92,19 +95,19 @@ def setup(hass, config): if not entities: return False - def select_value_service(call): + @callback + def async_select_value_service(call): """Handle a calls to the input slider services.""" - target_inputs = component.extract_from_service(call) + target_inputs = component.async_extract_from_service(call) for input_slider in target_inputs: input_slider.select_value(call.data[ATTR_VALUE]) - hass.services.register(DOMAIN, SERVICE_SELECT_VALUE, - select_value_service, - schema=SERVICE_SELECT_VALUE_SCHEMA) - - component.add_entities(entities) + hass.services.async_register( + DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service, + schema=SERVICE_SELECT_VALUE_SCHEMA) + yield from component.async_add_entities(entities) return True @@ -166,4 +169,4 @@ class InputSlider(Entity): num_value, self._minimum, self._maximum) return self._current_value = num_value - self.update_ha_state() + self.hass.loop.create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index ba5e1d6c9fc..a7c98dcd91c 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -4,6 +4,7 @@ Support for the definition of zones. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zone/ """ +import asyncio import logging import voluptuous as vol @@ -12,7 +13,7 @@ from homeassistant.const import ( ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON) from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity import Entity, generate_entity_id +from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.util.location import distance import homeassistant.helpers.config_validation as cv @@ -87,16 +88,19 @@ def in_zone(zone, latitude, longitude, radius=0): return zone_dist - radius < zone.attributes[ATTR_RADIUS] -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup zone.""" entities = set() + tasks = [] for _, entry in config_per_platform(config, DOMAIN): name = entry.get(CONF_NAME) zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) - zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, entities) - zone.update_ha_state() + zone.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, + entities) + tasks.append(zone.async_update_ha_state()) entities.add(zone.entity_id) if ENTITY_ID_HOME not in entities: @@ -104,8 +108,9 @@ def setup(hass, config): hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME - zone.update_ha_state() + tasks.append(zone.async_update_ha_state()) + yield from asyncio.gather(*tasks, loop=hass.loop) return True diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 3e45d3ecb83..0fc75a476f6 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -19,6 +19,7 @@ def config_per_platform(config: ConfigType, """Generator to break a component config into different platforms. For example, will find 'switch', 'switch 2', 'switch 3', .. etc + Async friendly. """ for config_key in extract_domain_configs(config, domain): platform_config = config[config_key] @@ -38,6 +39,9 @@ def config_per_platform(config: ConfigType, def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: - """Extract keys from config for given domain name.""" + """Extract keys from config for given domain name. + + Async friendly. + """ pattern = re.compile(r'^{}(| .+)$'.format(domain)) return [key for key in config.keys() if pattern.match(key)] From 9ea1101aba438035879ecaa8b4b8f83ec5d67841 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2016 12:54:47 -0700 Subject: [PATCH 071/149] Fix bootstrap circular imports (#4108) * Fix bootstrap circular imports * fix test * Lint --- homeassistant/bootstrap.py | 94 +++++++++++++++----------- homeassistant/helpers/discovery.py | 59 ++++++++++++---- tests/common.py | 10 +++ tests/helpers/test_discovery.py | 33 +++++++-- tests/helpers/test_entity_component.py | 6 +- 5 files changed, 140 insertions(+), 62 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 6a0d6540688..179c819f611 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -102,56 +102,68 @@ def _async_setup_component(hass: core.HomeAssistant, """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements + if not hasattr(hass, 'setup_lock'): + hass.setup_lock = asyncio.Lock(loop=hass.loop) + if domain in hass.config.components: return True - if domain in _CURRENT_SETUP: - _LOGGER.error('Attempt made to setup %s during setup of %s', - domain, domain) - return False - - config = yield from async_prepare_setup_component(hass, config, domain) - - if config is None: - return False - - component = loader.get_component(domain) - _CURRENT_SETUP.append(domain) + did_lock = False + if not hass.setup_lock.locked(): + yield from hass.setup_lock.acquire() + did_lock = True try: - if hasattr(component, 'async_setup'): - result = yield from component.async_setup(hass, config) - else: - result = yield from hass.loop.run_in_executor( - None, component.setup, hass, config) + if domain in _CURRENT_SETUP: + _LOGGER.error('Attempt made to setup %s during setup of %s', + domain, domain) + return False - if result is False: - _LOGGER.error('component %s failed to initialize', domain) + config = yield from async_prepare_setup_component(hass, config, domain) + + if config is None: return False - elif result is not True: - _LOGGER.error('component %s did not return boolean if setup ' - 'was successful. Disabling component.', domain) - loader.set_component(domain, None) + + component = loader.get_component(domain) + _CURRENT_SETUP.append(domain) + + try: + if hasattr(component, 'async_setup'): + result = yield from component.async_setup(hass, config) + else: + result = yield from hass.loop.run_in_executor( + None, component.setup, hass, config) + + if result is False: + _LOGGER.error('component %s failed to initialize', domain) + return False + elif result is not True: + _LOGGER.error('component %s did not return boolean if setup ' + 'was successful. Disabling component.', domain) + loader.set_component(domain, None) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error during setup of component %s', domain) return False - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error during setup of component %s', domain) - return False + finally: + _CURRENT_SETUP.remove(domain) + + hass.config.components.append(component.DOMAIN) + + # Assumption: if a component does not depend on groups + # it communicates with devices + if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ + hass.pool.worker_count <= 10: + hass.pool.add_worker() + + hass.bus.async_fire( + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + ) + + return True finally: - _CURRENT_SETUP.remove(domain) - - hass.config.components.append(component.DOMAIN) - - # Assumption: if a component does not depend on groups - # it communicates with devices - if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ - hass.pool.worker_count <= 10: - hass.pool.add_worker() - - hass.bus.async_fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} - ) - - return True + if did_lock: + hass.setup_lock.release() def prepare_setup_component(hass: core.HomeAssistant, config: dict, diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index eb36fc9e1d5..de432804e9c 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -1,9 +1,11 @@ """Helper methods to help with platform discovery.""" +import asyncio from homeassistant import bootstrap, core from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED) -from homeassistant.util.async import run_callback_threadsafe +from homeassistant.util.async import ( + run_callback_threadsafe, fire_coroutine_threadsafe) EVENT_LOAD_PLATFORM = 'load_platform.{}' ATTR_PLATFORM = 'platform' @@ -87,20 +89,51 @@ def load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. """ - def discover_platform(): - """Discover platform job.""" + fire_coroutine_threadsafe( + async_load_platform(hass, component, platform, + discovered, hass_config), hass.loop) + + +@asyncio.coroutine +def async_load_platform(hass, component, platform, discovered=None, + hass_config=None): + """Load a component and platform dynamically. + + Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be + fired to load the platform. The event will contain: + { ATTR_SERVICE = LOAD_PLATFORM + '.' + <> + ATTR_PLATFORM = <> + ATTR_DISCOVERED = <> } + + Use `listen_platform` to register a callback for these events. + + Warning: Do not yield from this inside a setup method to avoid a dead lock. + Use `hass.loop.create_task(async_load_platform(..))` instead. + + This method is a coroutine. + """ + did_lock = False + if hasattr(hass, 'setup_lock') and hass.setup_lock.locked(): + did_lock = True + yield from hass.setup_lock.acquire() + + try: # No need to fire event if we could not setup component - if not bootstrap.setup_component(hass, component, hass_config): - return + res = yield from bootstrap.async_setup_component( + hass, component, hass_config) + finally: + if did_lock: + hass.setup_lock.release() - data = { - ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), - ATTR_PLATFORM: platform, - } + if not res: + return - if discovered is not None: - data[ATTR_DISCOVERED] = discovered + data = { + ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), + ATTR_PLATFORM: platform, + } - hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data) + if discovered is not None: + data[ATTR_DISCOVERED] = discovered - hass.add_job(discover_platform) + hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data) diff --git a/tests/common.py b/tests/common.py index 4f2f447c1b0..9ee4dda5bfe 100644 --- a/tests/common.py +++ b/tests/common.py @@ -348,6 +348,16 @@ def patch_yaml_files(files_dict, endswith=True): return patch.object(yaml, 'open', mock_open_f, create=True) +def mock_coro(return_value=None): + """Helper method to return a coro that returns a value.""" + @asyncio.coroutine + def coro(): + """Fake coroutine.""" + return return_value + + return coro + + @contextmanager def assert_setup_component(count, domain=None): """Collect valid configuration from setup_component. diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index a0868691e3f..6851874ca37 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -3,8 +3,10 @@ from unittest.mock import patch from homeassistant import loader, bootstrap from homeassistant.helpers import discovery +from homeassistant.util.async import run_coroutine_threadsafe -from tests.common import get_test_home_assistant, MockModule, MockPlatform +from tests.common import ( + get_test_home_assistant, MockModule, MockPlatform, mock_coro) class TestHelpersDiscovery: @@ -12,7 +14,7 @@ class TestHelpersDiscovery: def setup_method(self, method): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(1) + self.hass = get_test_home_assistant() def teardown_method(self, method): """Stop everything that was started.""" @@ -55,7 +57,8 @@ class TestHelpersDiscovery: assert ['test service', 'another service'] == [info[0] for info in calls_multi] - @patch('homeassistant.bootstrap.setup_component') + @patch('homeassistant.bootstrap.async_setup_component', + return_value=mock_coro(True)()) def test_platform(self, mock_setup_component): """Test discover platform method.""" calls = [] @@ -91,7 +94,17 @@ class TestHelpersDiscovery: assert len(calls) == 1 def test_circular_import(self): - """Test we don't break doing circular import.""" + """Test we don't break doing circular import. + + This test will have test_component discover the switch.test_circular + component while setting up. + + The supplied config will load test_component and will load + switch.test_circular. + + That means that after startup, we will have test_component and switch + setup. The test_circular platform has been loaded twice. + """ component_calls = [] platform_calls = [] @@ -122,9 +135,17 @@ class TestHelpersDiscovery: 'platform': 'test_circular', }], }) + + # We wait for the setup_lock to finish + run_coroutine_threadsafe( + self.hass.setup_lock.acquire(), self.hass.loop).result() + self.hass.block_till_done() + # test_component will only be setup once + assert len(component_calls) == 1 + # The platform will be setup once via the config in `setup_component` + # and once via the discovery inside test_component. + assert len(platform_calls) == 2 assert 'test_component' in self.hass.config.components assert 'switch' in self.hass.config.components - assert len(component_calls) == 1 - assert len(platform_calls) == 1 diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 6f658a70518..bc94c9f44dc 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -13,7 +13,8 @@ from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockPlatform, MockModule, fire_time_changed) + get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, + mock_coro) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -226,7 +227,8 @@ class TestHelpersEntityComponent(unittest.TestCase): @patch('homeassistant.helpers.entity_component.EntityComponent' '._async_setup_platform') - @patch('homeassistant.bootstrap.setup_component', return_value=True) + @patch('homeassistant.bootstrap.async_setup_component', + return_value=mock_coro(True)()) def test_setup_does_discovery(self, mock_setup_component, mock_setup): """Test setup for discovery.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) From 942d6307627a3f6d7902790e7e1af5c5bc1c9853 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 29 Oct 2016 22:10:42 +0200 Subject: [PATCH 072/149] Maintenance zoneminder (#4102) * Add timeout to requests, fix typos, and defaults * Clean-up --- homeassistant/components/sensor/zoneminder.py | 15 ++--- homeassistant/components/switch/zoneminder.py | 14 ++--- homeassistant/components/zoneminder.py | 59 +++++++++---------- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/sensor/zoneminder.py b/homeassistant/components/sensor/zoneminder.py index 50446f735c3..32a77a8f32f 100644 --- a/homeassistant/components/sensor/zoneminder.py +++ b/homeassistant/components/sensor/zoneminder.py @@ -1,13 +1,14 @@ """ -Support for Zoneminder Sensors. +Support for ZoneMinder Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.zoneminder/ """ import logging -import homeassistant.components.zoneminder as zoneminder +from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.entity import Entity +import homeassistant.components.zoneminder as zoneminder _LOGGER = logging.getLogger(__name__) @@ -15,7 +16,7 @@ DEPENDENCIES = ['zoneminder'] def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Zoneminder platform.""" + """Set up the ZoneMinder sensor platform.""" sensors = [] monitors = zoneminder.get_state('api/monitors.json') @@ -31,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZMSensorMonitors(Entity): - """Get the status of each monitor.""" + """Get the status of each ZoneMinder monitor.""" def __init__(self, monitor_id, monitor_name): """Initiate monitor sensor.""" @@ -42,7 +43,7 @@ class ZMSensorMonitors(Entity): @property def name(self): """Return the name of the sensor.""" - return "%s Status" % self._monitor_name + return '{} Status'.format(self._monitor_name) @property def state(self): @@ -55,7 +56,7 @@ class ZMSensorMonitors(Entity): 'api/monitors/%i.json' % self._monitor_id ) if monitor['monitor']['Monitor']['Function'] is None: - self._state = "None" + self._state = STATE_UNKNOWN else: self._state = monitor['monitor']['Monitor']['Function'] @@ -72,7 +73,7 @@ class ZMSensorEvents(Entity): @property def name(self): """Return the name of the sensor.""" - return "%s Events" % self._monitor_name + return '{} Events'.format(self._monitor_name) @property def unit_of_measurement(self): diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py index ab9adbca97d..5dffd99c324 100644 --- a/homeassistant/components/switch/zoneminder.py +++ b/homeassistant/components/switch/zoneminder.py @@ -1,5 +1,5 @@ """ -Support for Zoneminder switches. +Support for ZoneMinder switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.zoneminder/ @@ -10,23 +10,21 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_COMMAND_ON, CONF_COMMAND_OFF) +import homeassistant.components.zoneminder as zoneminder import homeassistant.helpers.config_validation as cv -import homeassistant.components.zoneminder as zoneminder +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['zoneminder'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COMMAND_ON): cv.string, vol.Required(CONF_COMMAND_OFF): cv.string, }) -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['zoneminder'] - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Zoneminder switch.""" + """Set up the ZoneMinder switch platform.""" on_state = config.get(CONF_COMMAND_ON) off_state = config.get(CONF_COMMAND_OFF) @@ -47,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZMSwitchMonitors(SwitchDevice): - """Representation of an zoneminder switch.""" + """Representation of a ZoneMinder switch.""" icon = 'mdi:record-rec' diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index d4001a7c40f..4920a5a6ce2 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -1,10 +1,9 @@ """ -Support for Zoneminder. +Support for ZoneMinder. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zoneminder/ """ - import logging import json from urllib.parse import urljoin @@ -12,43 +11,44 @@ from urllib.parse import urljoin import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PATH, CONF_HOST, CONF_SSL, CONF_PASSWORD, CONF_USERNAME) - +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = [] - +DEFAULT_PATH = '/zm/' +DEFAULT_SSL = False +DEFAULT_TIMEOUT = 10 DOMAIN = 'zoneminder' +LOGIN_RETRIES = 2 + +ZM = {} + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_PATH, default="/zm/"): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string }) }, extra=vol.ALLOW_EXTRA) -LOGIN_RETRIES = 2 -ZM = {} - def setup(hass, config): - """Setup the zonminder platform.""" + """Set up the ZoneMinder component.""" global ZM ZM = {} conf = config[DOMAIN] if conf[CONF_SSL]: - schema = "https" + schema = 'https' else: - schema = "http" + schema = 'http' - url = urljoin(schema + "://" + conf[CONF_HOST], conf[CONF_PATH]) + url = urljoin('{}://{}'.format(schema, conf[CONF_HOST]), conf[CONF_PATH]) username = conf.get(CONF_USERNAME, None) password = conf.get(CONF_PASSWORD, None) @@ -61,8 +61,8 @@ def setup(hass, config): # pylint: disable=no-member def login(): - """Login to the zoneminder api.""" - _LOGGER.debug("Attempting to login to zoneminder") + """Login to the ZoneMinder API.""" + _LOGGER.debug("Attempting to login to ZoneMinder") login_post = {'view': 'console', 'action': 'login'} if ZM['username']: @@ -73,12 +73,11 @@ def login(): req = requests.post(ZM['url'] + '/index.php', data=login_post) ZM['cookies'] = req.cookies - # Login calls returns a 200 repsonse on both failure and success.. + # Login calls returns a 200 response on both failure and success. # The only way to tell if you logged in correctly is to issue an api call. req = requests.get( - ZM['url'] + 'api/host/getVersion.json', - cookies=ZM['cookies'] - ) + ZM['url'] + 'api/host/getVersion.json', cookies=ZM['cookies'], + timeout=DEFAULT_TIMEOUT) if req.status_code != requests.codes.ok: _LOGGER.error("Connection error logging into ZoneMinder") @@ -89,18 +88,19 @@ def login(): # pylint: disable=no-member def get_state(api_url): - """Get a state from the zoneminder API service.""" - # Since the API uses sessions that expire, sometimes we need - # to re-auth if the call fails. + """Get a state from the ZoneMinder API service.""" + # Since the API uses sessions that expire, sometimes we need to re-auth + # if the call fails. for _ in range(LOGIN_RETRIES): - req = requests.get(urljoin(ZM['url'], api_url), cookies=ZM['cookies']) + req = requests.get(urljoin(ZM['url'], api_url), cookies=ZM['cookies'], + timeout=DEFAULT_TIMEOUT) if req.status_code != requests.codes.ok: login() else: break else: - _LOGGER.exception("Unable to get API response") + _LOGGER.exception("Unable to get API response from ZoneMinder") return json.loads(req.text) @@ -110,9 +110,8 @@ def change_state(api_url, post_data): """Update a state using the Zoneminder API.""" for _ in range(LOGIN_RETRIES): req = requests.post( - urljoin(ZM['url'], api_url), - data=post_data, - cookies=ZM['cookies']) + urljoin(ZM['url'], api_url), data=post_data, cookies=ZM['cookies'], + timeout=DEFAULT_TIMEOUT) if req.status_code != requests.codes.ok: login() @@ -120,6 +119,6 @@ def change_state(api_url, post_data): break else: - _LOGGER.exception("Unable to get API response") + _LOGGER.exception("Unable to get API response from ZoneMinder") return json.loads(req.text) From 892f455aeec0647f15f49f2362d0c4f36ae09115 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 29 Oct 2016 22:21:09 +0200 Subject: [PATCH 073/149] Maintenance (sensor.bitcoin, sensor.yahoo_finance) (#4104) * Add attribution * Update ordering * Update ordering --- homeassistant/components/sensor/bitcoin.py | 12 ++++++++++-- .../components/sensor/coinmarketcap.py | 6 +++--- .../components/sensor/yahoo_finance.py | 18 +++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 51b5f9bba3b..c67b0d9e94b 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DISPLAY_OPTIONS +from homeassistant.const import (CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -19,6 +19,7 @@ REQUIREMENTS = ['blockchain==1.3.3'] _LOGGER = logging.getLogger(__name__) +CONF_ATTRIBUTION = "Data provided by blockchain.info" CONF_CURRENCY = 'currency' DEFAULT_CURRENCY = 'USD' @@ -59,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Bitcoin sensors.""" + """Set up the Bitcoin sensors.""" from blockchain import exchangerates currency = config.get(CONF_CURRENCY) @@ -111,6 +112,13 @@ class BitcoinSensor(Entity): """Return the icon to use in the frontend, if any.""" return ICON + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + # pylint: disable=too-many-branches def update(self): """Get the latest data and updates the states.""" diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index a166ec91d10..05e69c2e3d6 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -46,13 +46,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the CoinMarketCap sensor.""" + """Set up the CoinMarketCap sensor.""" currency = config.get(CONF_CURRENCY) try: CoinMarketCapData(currency).update() except HTTPError: - _LOGGER.warning('Currency "%s" is not available. Using "bitcoin"', + _LOGGER.warning("Currency '%s' is not available. Using 'bitcoin'", currency) currency = DEFAULT_CURRENCY @@ -95,13 +95,13 @@ class CoinMarketCapSensor(Entity): """Return the state attributes of the sensor.""" return { ATTR_24H_VOLUME_USD: self._ticker.get('24h_volume_usd'), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), ATTR_MARKET_CAP: self._ticker.get('market_cap_usd'), ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } # pylint: disable=too-many-branches diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index 1cf275c28e7..c1f09284882 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -19,20 +19,20 @@ REQUIREMENTS = ['yahoo-finance==1.3.2'] _LOGGER = logging.getLogger(__name__) +ATTR_CHANGE = 'Change' +ATTR_OPEN = 'open' +ATTR_PREV_CLOSE = 'prev_close' + CONF_ATTRIBUTION = "Stock market information provided by Yahoo! Inc." CONF_SYMBOL = 'symbol' -DEFAULT_SYMBOL = 'YHOO' DEFAULT_NAME = 'Yahoo Stock' +DEFAULT_SYMBOL = 'YHOO' ICON = 'mdi:currency-usd' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -ATTR_CHANGE = 'Change' -ATTR_OPEN = 'open' -ATTR_PREV_CLOSE = 'prev_close' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SYMBOL, default=DEFAULT_SYMBOL): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Yahoo Finance sensor.""" + """Set up the Yahoo Finance sensor.""" name = config.get(CONF_NAME) symbol = config.get(CONF_SYMBOL) @@ -81,10 +81,10 @@ class YahooFinanceSensor(Entity): """Return the state attributes.""" if self._state is not None: return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CHANGE: self.data.price_change, ATTR_OPEN: self.data.price_open, ATTR_PREV_CLOSE: self.data.prev_close, - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } @property @@ -94,7 +94,7 @@ class YahooFinanceSensor(Entity): def update(self): """Get the latest data and updates the states.""" - _LOGGER.debug('Updating sensor %s - %s', self._name, self._state) + _LOGGER.debug("Updating sensor %s - %s", self._name, self._state) self.data.update() self._state = self.data.state From 54d19e3c5386a3f3c806761436aa3f86da1a3d77 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 29 Oct 2016 22:27:02 +0200 Subject: [PATCH 074/149] Maintenance (sensor.currencylayer, sensor.fixer) (#4103) * Add new const (base) * Use constant * Remove second error message, use const, add attribution, add link to docs, remove unused vars, and a little simplification * Add quote * Use const * Add attribution, simplify the code, and use consts --- .../components/sensor/currencylayer.py | 87 +++++++++++-------- homeassistant/components/sensor/fixer.py | 3 +- .../components/sensor/openexchangerates.py | 40 +++++---- homeassistant/const.py | 2 + 4 files changed, 74 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index b1686e8302d..71d49cafc46 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -1,24 +1,34 @@ -"""Support for currencylayer.com exchange rates service.""" +""" +Support for currencylayer.com exchange rates service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.currencylayer/ +""" from datetime import timedelta import logging + import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_BASE, CONF_QUOTE, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_PAYLOAD) +import homeassistant.helpers.config_validation as cv -_RESOURCE = 'http://apilayer.net/api/live' _LOGGER = logging.getLogger(__name__) -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) -CONF_BASE = 'base' -CONF_QUOTE = 'quote' +_RESOURCE = 'http://apilayer.net/api/live' + +CONF_ATTRIBUTION = "Data provided by currencylayer.com" + DEFAULT_BASE = 'USD' DEFAULT_NAME = 'CurrencyLayer Sensor' + ICON = 'mdi:currency' +MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), @@ -28,25 +38,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Currencylayer sensor.""" - payload = config.get(CONF_PAYLOAD) - rest = CurrencylayerData( - _RESOURCE, - config.get(CONF_API_KEY), - config.get(CONF_BASE, 'USD'), - payload - ) - response = requests.get(_RESOURCE, params={'source': - config.get(CONF_BASE, 'USD'), - 'access_key': - config.get(CONF_API_KEY), - 'format': 1}, timeout=10) + """Set up the Currencylayer sensor.""" + base = config.get(CONF_BASE) + api_key = config.get(CONF_API_KEY) + parameters = { + 'source': base, + 'access_key': api_key, + 'format': 1, + } + + rest = CurrencylayerData(_RESOURCE, parameters) + + response = requests.get(_RESOURCE, params=parameters, timeout=10) sensors = [] for variable in config['quote']: - sensors.append(CurrencylayerSensor(rest, config.get(CONF_BASE, 'USD'), - variable)) - if "error" in response.json(): - _LOGGER.error("Check your Currencylayer API") + sensors.append(CurrencylayerSensor(rest, base, variable)) + if 'error' in response.json(): return False else: add_devices(sensors) @@ -66,7 +73,7 @@ class CurrencylayerSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return str(self._base) + str(self._quote) + return '{} {}'.format(self._base, self._quote) @property def icon(self): @@ -78,12 +85,20 @@ class CurrencylayerSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + def update(self): - """Update current conditions.""" + """Update current date.""" self.rest.update() value = self.rest.data if value is not None: - self._state = round(value[str(self._base) + str(self._quote)], 4) + self._state = round( + value['{}{}'.format(self._base, self._quote)], 4) # pylint: disable=too-few-public-methods @@ -91,24 +106,20 @@ class CurrencylayerData(object): """Get data from Currencylayer.org.""" # pylint: disable=too-many-arguments - def __init__(self, resource, api_key, base, data): + def __init__(self, resource, parameters): """Initialize the data object.""" self._resource = resource - self._api_key = api_key - self._base = base + self._parameters = parameters self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Currencylayer.""" try: - result = requests.get(self._resource, params={'source': self._base, - 'access_key': - self._api_key, - 'format': 1}, - timeout=10) - if "error" in result.json(): - raise ValueError(result.json()["error"]["info"]) + result = requests.get( + self._resource, params=self._parameters, timeout=10) + if 'error' in result.json(): + raise ValueError(result.json()['error']['info']) else: self.data = result.json()['quotes'] _LOGGER.debug("Currencylayer data updated: %s", diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index c8fe3b06c4e..a9f53811c59 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION, CONF_BASE) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -24,7 +24,6 @@ ATTR_EXCHANGE_RATE = 'Exchange rate' ATTR_TARGET = 'Target currency' CONF_ATTRIBUTION = "Data provided by the European Central Bank (ECB)" -CONF_BASE = 'base' CONF_TARGET = 'target' DEFAULT_BASE = 'USD' diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index b4e7033bcc0..d0e3ffb2cdd 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -11,7 +11,8 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_API_KEY, CONF_NAME, CONF_PAYLOAD) +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_BASE, CONF_QUOTE, ATTR_ATTRIBUTION) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -19,8 +20,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://openexchangerates.org/api/latest.json' -CONF_BASE = 'base' -CONF_QUOTE = 'quote' +CONF_ATTRIBUTION = "Data provided by openexchangerates.org" DEFAULT_BASE = 'USD' DEFAULT_NAME = 'Exchange Rate Sensor' @@ -36,20 +36,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Open Exchange Rates sensor.""" + """Set up the Open Exchange Rates sensor.""" name = config.get(CONF_NAME) api_key = config.get(CONF_API_KEY) base = config.get(CONF_BASE) quote = config.get(CONF_QUOTE) - payload = config.get(CONF_PAYLOAD) - rest = OpenexchangeratesData(_RESOURCE, api_key, base, quote, payload) - response = requests.get(_RESOURCE, params={'base': base, - 'app_id': api_key}, - timeout=10) + parameters = { + 'base': base, + 'app_id': api_key, + } + + rest = OpenexchangeratesData(_RESOURCE, parameters, quote) + response = requests.get(_RESOURCE, params=parameters, timeout=10) + if response.status_code != 200: _LOGGER.error("Check your OpenExchangeRates API key") return False + rest.update() add_devices([OpenexchangeratesSensor(rest, name, quote)]) @@ -77,7 +81,10 @@ class OpenexchangeratesSensor(Entity): @property def device_state_attributes(self): """Return other attributes of the sensor.""" - return self.rest.data + attr = self.rest.data + attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + + return attr def update(self): """Update current conditions.""" @@ -91,11 +98,10 @@ class OpenexchangeratesData(object): """Get data from Openexchangerates.org.""" # pylint: disable=too-many-arguments - def __init__(self, resource, api_key, base, quote, data): + def __init__(self, resource, parameters, quote): """Initialize the data object.""" self._resource = resource - self._api_key = api_key - self._base = base + self._parameters = parameters self._quote = quote self.data = None @@ -103,12 +109,10 @@ class OpenexchangeratesData(object): def update(self): """Get the latest data from openexchangerates.org.""" try: - result = requests.get(self._resource, params={'base': self._base, - 'app_id': - self._api_key}, - timeout=10) + result = requests.get( + self._resource, params=self._parameters, timeout=10) self.data = result.json()['rates'] except requests.exceptions.HTTPError: - _LOGGER.error("Check the Openexchangerates API Key") + _LOGGER.error("Check the Openexchangerates API key") self.data = None return False diff --git a/homeassistant/const.py b/homeassistant/const.py index 56712b9a04d..69c340db3db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -58,6 +58,7 @@ CONF_AFTER = 'after' CONF_ALIAS = 'alias' CONF_API_KEY = 'api_key' CONF_AUTHENTICATION = 'authentication' +CONF_BASE = 'base' CONF_BEFORE = 'before' CONF_BELOW = 'below' CONF_BLACKLIST = 'blacklist' @@ -113,6 +114,7 @@ CONF_PIN = 'pin' CONF_PLATFORM = 'platform' CONF_PORT = 'port' CONF_PREFIX = 'prefix' +CONF_QUOTE = 'quote' CONF_RECIPIENT = 'recipient' CONF_RESOURCE = 'resource' CONF_RESOURCES = 'resources' From edeb31d74e27e6f8d8c7d58830b7902b6b45db2d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 29 Oct 2016 22:47:46 +0200 Subject: [PATCH 075/149] Fix bug with aioHTTP and none authentification (#4116) --- homeassistant/components/camera/mjpeg.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index e92274247de..42baab0bbf3 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -72,10 +72,11 @@ class MjpegCamera(Camera): self._mjpeg_url = device_info[CONF_MJPEG_URL] self._auth = None - if self._authentication == HTTP_BASIC_AUTHENTICATION: - self._auth = aiohttp.BasicAuth( - self._username, password=self._password - ) + if self._username and self._password: + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth( + self._username, password=self._password + ) def camera_image(self): """Return a still image response from the camera.""" From 3cc4fdaa34843950588181e39a8d67b7573b4e87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2016 14:45:31 -0700 Subject: [PATCH 076/149] Fix HTTP static file singular (#4118) --- 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 b13e786f50d..680ae9cfeda 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -328,8 +328,8 @@ class HomeAssistantWSGI(object): @asyncio.coroutine def serve_file(request): """Redirect to location.""" - yield from _GZIP_FILE_SENDER.send(request, filepath) - return + res = yield from _GZIP_FILE_SENDER.send(request, filepath) + return res # aiohttp supports regex matching for variables. Using that as temp # to work around cache busting MD5. From 4163e55dbd01fa8be2599dae117e0e9d86e0e190 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2016 14:51:17 -0700 Subject: [PATCH 077/149] Introducing hass.data (#4121) * Hello hass.data * Migrate setup_component to hass.data --- homeassistant/bootstrap.py | 55 ++++++++++++++++-------------- homeassistant/core.py | 2 ++ homeassistant/helpers/discovery.py | 7 ++-- homeassistant/remote.py | 6 ++-- tests/helpers/test_discovery.py | 2 +- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 179c819f611..e0664ee5b6e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -27,7 +27,6 @@ from homeassistant.helpers import ( event_decorators, service, config_per_platform, extract_domain_configs) _LOGGER = logging.getLogger(__name__) -_CURRENT_SETUP = [] ATTR_COMPONENT = 'component' @@ -102,30 +101,37 @@ def _async_setup_component(hass: core.HomeAssistant, """ # pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-statements - if not hasattr(hass, 'setup_lock'): - hass.setup_lock = asyncio.Lock(loop=hass.loop) - if domain in hass.config.components: return True - did_lock = False - if not hass.setup_lock.locked(): - yield from hass.setup_lock.acquire() - did_lock = True + setup_lock = hass.data.get('setup_lock') + if setup_lock is None: + setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) + + setup_progress = hass.data.get('setup_progress') + if setup_progress is None: + setup_progress = hass.data['setup_progress'] = [] + + if domain in setup_progress: + _LOGGER.error('Attempt made to setup %s during setup of %s', + domain, domain) + return False try: - if domain in _CURRENT_SETUP: - _LOGGER.error('Attempt made to setup %s during setup of %s', - domain, domain) - return False + # Used to indicate to discovery that a setup is ongoing and allow it + # to wait till it is done. + did_lock = False + if not setup_lock.locked(): + yield from setup_lock.acquire() + did_lock = True + setup_progress.append(domain) config = yield from async_prepare_setup_component(hass, config, domain) if config is None: return False component = loader.get_component(domain) - _CURRENT_SETUP.append(domain) try: if hasattr(component, 'async_setup'): @@ -133,20 +139,18 @@ def _async_setup_component(hass: core.HomeAssistant, else: result = yield from hass.loop.run_in_executor( None, component.setup, hass, config) - - if result is False: - _LOGGER.error('component %s failed to initialize', domain) - return False - elif result is not True: - _LOGGER.error('component %s did not return boolean if setup ' - 'was successful. Disabling component.', domain) - loader.set_component(domain, None) - return False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error during setup of component %s', domain) return False - finally: - _CURRENT_SETUP.remove(domain) + + if result is False: + _LOGGER.error('component %s failed to initialize', domain) + return False + elif result is not True: + _LOGGER.error('component %s did not return boolean if setup ' + 'was successful. Disabling component.', domain) + loader.set_component(domain, None) + return False hass.config.components.append(component.DOMAIN) @@ -162,8 +166,9 @@ def _async_setup_component(hass: core.HomeAssistant, return True finally: + setup_progress.remove(domain) if did_lock: - hass.setup_lock.release() + setup_lock.release() def prepare_setup_component(hass: core.HomeAssistant, config: dict, diff --git a/homeassistant/core.py b/homeassistant/core.py index f6743a40ef5..d8720f26cd5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -141,6 +141,8 @@ class HomeAssistant(object): self.services = ServiceRegistry(self.bus, self.add_job, self.loop) self.states = StateMachine(self.bus, self.loop) self.config = Config() # type: Config + # This is a dictionary that any component can store any data on. + self.data = {} self.state = CoreState.not_running self.exit_code = None self._websession = None diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index de432804e9c..551aabc1573 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -113,9 +113,10 @@ def async_load_platform(hass, component, platform, discovered=None, This method is a coroutine. """ did_lock = False - if hasattr(hass, 'setup_lock') and hass.setup_lock.locked(): + setup_lock = hass.data.get('setup_lock') + if setup_lock and setup_lock.locked(): did_lock = True - yield from hass.setup_lock.acquire() + yield from setup_lock.acquire() try: # No need to fire event if we could not setup component @@ -123,7 +124,7 @@ def async_load_platform(hass, component, platform, discovered=None, hass, component, hass_config) finally: if did_lock: - hass.setup_lock.release() + setup_lock.release() if not res: return diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 94ac2899c69..42afa91170c 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -134,9 +134,11 @@ class HomeAssistant(ha.HomeAssistant): self.services = ha.ServiceRegistry(self.bus, self.add_job, self.loop) self.states = StateMachine(self.bus, self.loop, self.remote_api) self.config = ha.Config() - self._websession = None - + # This is a dictionary that any component can store any data on. + self.data = {} self.state = ha.CoreState.not_running + self.exit_code = None + self._websession = None self.config.api = local_api def start(self): diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 6851874ca37..a81db2074fb 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -138,7 +138,7 @@ class TestHelpersDiscovery: # We wait for the setup_lock to finish run_coroutine_threadsafe( - self.hass.setup_lock.acquire(), self.hass.loop).result() + self.hass.data['setup_lock'].acquire(), self.hass.loop).result() self.hass.block_till_done() From 5d43d3eb1c4fb0bfeb7b559311c927a8c3f8b85a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 00:30:23 +0200 Subject: [PATCH 078/149] Fix error message (#4122) --- homeassistant/components/media_player/webostv.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index a283c8ce9b2..4cc284f5672 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -71,9 +71,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): else: host = config.get(CONF_HOST) - if host is None: - _LOGGER.error("No host found in configuration") - return False + if host is None: + _LOGGER.error("No TV found in configuration file or with discovery") + return False # Only act if we are not already configuring this host if host in _CONFIGURING: @@ -98,14 +98,14 @@ def setup_tv(host, name, customize, hass, add_devices): client.register() except PyLGTVPairException: _LOGGER.warning( - "Connected to LG WebOS TV at %s but not paired", host) + "Connected to LG WebOS TV %s but not paired", host) return except OSError: _LOGGER.error("Unable to connect to host %s", host) return else: # Not registered, request configuration. - _LOGGER.warning('LG WebOS TV at %s needs to be paired.', host) + _LOGGER.warning("LG WebOS TV %s needs to be paired", host) request_configuration(host, name, customize, hass, add_devices) return @@ -135,7 +135,7 @@ def request_configuration(host, name, customize, hass, add_devices): _CONFIGURING[host] = configurator.request_config( hass, 'LG WebOS TV', lgtv_configuration_callback, - description='Click start and accept the pairing request on your tv.', + description='Click start and accept the pairing request on your TV.', description_image='/static/images/config_webos.png', submit_caption='Start pairing request' ) From 9c0455e3dcc3ecec1f5a81797013e60f8e20faf1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Oct 2016 00:33:11 +0200 Subject: [PATCH 079/149] Allow update entities on add_entities callback (#4114) * Allow udpate entities on add_entities callback * fix wrong position * update force_update to update_before_add * add unittest for update_befor_add * fix unittest * change mocking --- .../components/binary_sensor/template.py | 8 +---- homeassistant/components/sensor/template.py | 9 +----- homeassistant/components/switch/template.py | 8 +---- homeassistant/helpers/entity_component.py | 30 +++++++++++++------ tests/helpers/test_entity_component.py | 26 ++++++++++++++++ 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 365b29eb308..5d4d31a57a4 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error('No sensors added') return False - hass.loop.create_task(async_add_devices(sensors)) + hass.loop.create_task(async_add_devices(sensors, True)) return True @@ -82,8 +82,6 @@ class BinarySensorTemplate(BinarySensorDevice): self._template = value_template self._state = None - self._async_render() - @callback def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" @@ -115,10 +113,6 @@ class BinarySensorTemplate(BinarySensorDevice): @asyncio.coroutine def async_update(self): """Update the state from the template.""" - self._async_render() - - def _async_render(self): - """Render the state from the template.""" try: self._state = self._template.async_render().lower() == 'true' except TemplateError as ex: diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 600d188bdc0..a34d7615447 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -61,7 +61,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No sensors added") return False - hass.loop.create_task(async_add_devices(sensors)) + hass.loop.create_task(async_add_devices(sensors, True)) return True @@ -80,9 +80,6 @@ class SensorTemplate(Entity): self._template = state_template self._state = None - # update state - self._async_render() - @callback def template_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" @@ -114,10 +111,6 @@ class SensorTemplate(Entity): @asyncio.coroutine def async_update(self): """Update the state from the template.""" - self._async_render() - - def _async_render(self): - """Render the state from the template.""" try: self._state = self._template.async_render() except TemplateError as ex: diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 2bac825b5b4..83c82ae6ad1 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -70,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("No switches added") return False - hass.loop.create_task(async_add_devices(switches)) + hass.loop.create_task(async_add_devices(switches, True)) return True @@ -90,8 +90,6 @@ class SwitchTemplate(SwitchDevice): self._off_script = Script(hass, off_action) self._state = False - self._async_render() - @callback def template_switch_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" @@ -131,10 +129,6 @@ class SwitchTemplate(SwitchDevice): @asyncio.coroutine def async_update(self): """Update the state from the template.""" - self._async_render() - - def _async_render(self): - """Render the state from the template.""" try: state = self._template.async_render().lower() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ed450bb1a14..d877f8b0b70 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -155,14 +155,15 @@ class EntityComponent(object): self.logger.exception( 'Error while setting up platform %s', platform_type) - def add_entity(self, entity, platform=None): + def add_entity(self, entity, platform=None, update_before_add=False): """Add entity to component.""" return run_coroutine_threadsafe( - self.async_add_entity(entity, platform), self.hass.loop + self.async_add_entity(entity, platform, update_before_add), + self.hass.loop ).result() @asyncio.coroutine - def async_add_entity(self, entity, platform=None): + def async_add_entity(self, entity, platform=None, update_before_add=False): """Add entity to component. This method must be run in the event loop. @@ -172,6 +173,13 @@ class EntityComponent(object): entity.hass = self.hass + # update/init entity data + if update_before_add: + if hasattr(entity, 'async_update'): + yield from entity.async_update() + else: + yield from self.hass.loop.run_in_executor(None, entity.update) + if getattr(entity, 'entity_id', None) is None: object_id = entity.name or DEVICE_DEFAULT_NAME @@ -274,19 +282,21 @@ class EntityPlatform(object): self.platform_entities = [] self._async_unsub_polling = None - def add_entities(self, new_entities): + def add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform.""" run_coroutine_threadsafe( - self.async_add_entities(new_entities), self.component.hass.loop + self.async_add_entities(new_entities, update_before_add), + self.component.hass.loop ).result() @asyncio.coroutine - def async_add_entities(self, new_entities): + def async_add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform async. This method must be run in the event loop. """ - tasks = [self._async_process_entity(entity) for entity in new_entities] + tasks = [self._async_process_entity(entity, update_before_add) + for entity in new_entities] yield from asyncio.gather(*tasks, loop=self.component.hass.loop) yield from self.component.async_update_group() @@ -301,9 +311,11 @@ class EntityPlatform(object): second=range(0, 60, self.scan_interval)) @asyncio.coroutine - def _async_process_entity(self, new_entity): + def _async_process_entity(self, new_entity, update_before_add): """Add entities to StateMachine.""" - ret = yield from self.component.async_add_entity(new_entity, self) + ret = yield from self.component.async_add_entity( + new_entity, self, update_before_add=update_before_add + ) if ret: self.platform_entities.append(new_entity) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index bc94c9f44dc..3ba9bf3d1ce 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -126,6 +126,32 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 2 == len(self.hass.states.entity_ids()) + def test_update_state_adds_entities_with_update_befor_add_true(self): + """Test if call update befor add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = EntityTest() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], True) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert ent.update.called + + def test_update_state_adds_entities_with_update_befor_add_false(self): + """Test if not call update befor add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + ent = EntityTest() + ent.update = Mock(spec_set=True) + + component.add_entities([ent], False) + self.hass.block_till_done() + + assert 1 == len(self.hass.states.entity_ids()) + assert not ent.update.called + def test_not_adding_duplicate_entities(self): """Test for not adding duplicate entities.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) From 3317b4916bd5e3c6f1ab09950bc67a409029b5dc Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Sun, 30 Oct 2016 00:33:56 +0200 Subject: [PATCH 080/149] OSError is alias for IOException and base class for many other exceptions - no need to catch redundant exceptions if OSError already present in except-clause (#4111) --- homeassistant/components/climate/radiotherm.py | 3 +-- .../components/media_player/panasonic_viera.py | 15 +++++++-------- .../components/media_player/samsungtv.py | 4 +--- .../components/media_player/squeezebox.py | 2 +- homeassistant/components/notify/telegram.py | 4 ++-- homeassistant/components/tellduslive.py | 2 +- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 74778682540..90a1701536c 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/ """ import datetime import logging -from urllib.error import URLError import voluptuous as vol @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: tstat = radiotherm.get_thermostat(host) tstats.append(RadioThermostat(tstat, hold_temp)) - except (URLError, OSError): + except OSError: _LOGGER.exception("Unable to connect to Radio Thermostat: %s", host) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 488e4e6b9d8..7ae1eb9d79e 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.panasonic_viera/ """ import logging -import socket import voluptuous as vol @@ -60,9 +59,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: remote.get_mute() - except (socket.timeout, TimeoutError, OSError): - _LOGGER.error('Panasonic Viera TV is not available at %s:%d', - host, port) + except OSError as error: + _LOGGER.error('Panasonic Viera TV is not available at %s:%d: %s', + host, port, error) return False add_devices([PanasonicVieraTVDevice(name, remote)]) @@ -88,7 +87,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): try: self._muted = self._remote.get_mute() self._state = STATE_ON - except (socket.timeout, TimeoutError, OSError): + except OSError: self._state = STATE_OFF return False return True @@ -98,7 +97,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): try: self._remote.send_key(key) self._state = STATE_ON - except (socket.timeout, TimeoutError, OSError): + except OSError: self._state = STATE_OFF return False return True @@ -120,7 +119,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): try: volume = self._remote.get_volume() / 100 self._state = STATE_ON - except (socket.timeout, TimeoutError, OSError): + except OSError: self._state = STATE_OFF return volume @@ -156,7 +155,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): try: self._remote.set_volume(volume) self._state = STATE_ON - except (socket.timeout, TimeoutError, OSError): + except OSError: self._state = STATE_OFF def media_play_pause(self): diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 5c096c86bb0..6c429eff54e 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ import logging -import socket import voluptuous as vol @@ -101,8 +100,7 @@ class SamsungTVDevice(MediaPlayerDevice): self._state = STATE_ON self._remote = None return False - except (self._remote_class.ConnectionClosed, socket.timeout, - TimeoutError, OSError): + except (self._remote_class.ConnectionClosed, OSError): self._state = STATE_OFF self._remote = None return False diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 4f994461a26..30df5a11f99 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -163,7 +163,7 @@ class LogitechMediaServer(object): return response - except (OSError, ConnectionError, EOFError) as error: + except (OSError, EOFError) as error: _LOGGER.error("Could not communicate with %s:%d: %s", self.host, self.port, diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 91164adec58..a5f0adcbfc2 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -71,8 +71,8 @@ def load_data(url=None, file=None, username=None, password=None): else: _LOGGER.warning("Can't load photo no photo found in params!") - except (OSError, IOError, requests.exceptions.RequestException): - _LOGGER.error("Can't load photo into ByteIO") + except OSError as error: + _LOGGER.error("Can't load photo into ByteIO: %s", error) return None diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index c83c0c4d25d..961e4edd891 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -157,7 +157,7 @@ class TelldusLiveData(object): response = self._client.request(what, params) _LOGGER.debug("got response %s", response) return response - except (ConnectionError, TimeoutError, OSError) as error: + except OSError as error: _LOGGER.error("failed to make request to Tellduslive servers: %s", error) return None From 3f6a5564ad9c94f3c572e65227e54bf67c112888 Mon Sep 17 00:00:00 2001 From: wokar Date: Sun, 30 Oct 2016 01:52:53 +0200 Subject: [PATCH 081/149] lg_netcast platform fails to load if no channels defined (#4083) * fixes loading of lg_netcast platform if no channels are defined * turned list comprehension into for loop --- homeassistant/components/media_player/lg_netcast.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 26b7341f747..4402d8b93b8 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -103,8 +103,11 @@ class LgTVDevice(MediaPlayerDevice): channel_list = client.query_data('channel_list') if channel_list: - channel_names = [str(c.find('chname').text) for - c in channel_list] + channel_names = [] + for channel in channel_list: + channel_name = channel.find('chname') + if channel_name is not None: + channel_names.append(str(channel_name.text)) self._sources = dict(zip(channel_names, channel_list)) # sort source names by the major channel number source_tuples = [(k, self._sources[k].find('major').text) From 33e46b484fc7c5890f5c2e98fd40c165d67e0193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sun, 30 Oct 2016 01:54:26 +0200 Subject: [PATCH 082/149] Add service to change visibility of a group (#3998) --- homeassistant/components/group.py | 38 ++++++++++++++++++++++- homeassistant/components/services.yaml | 12 +++++++ homeassistant/helpers/entity_component.py | 9 +++--- homeassistant/helpers/service.py | 18 ++++++++--- tests/components/test_group.py | 20 ++++++++++++ tests/helpers/test_entity_component.py | 15 +++++++++ tests/helpers/test_service.py | 3 ++ 7 files changed, 106 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 59683247d5b..ecd79cae3ab 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -33,6 +33,13 @@ CONF_VIEW = 'view' ATTR_AUTO = 'auto' ATTR_ORDER = 'order' ATTR_VIEW = 'view' +ATTR_VISIBLE = 'visible' + +SERVICE_SET_VISIBILITY = 'set_visibility' +SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_VISIBLE): cv.boolean +}) SERVICE_RELOAD = 'reload' RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -89,6 +96,12 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) +def set_visibility(hass, entity_id=None, visible=True): + """Hide or shows a group.""" + data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} + hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) + + def expand_entity_ids(hass, entity_ids): """Return entity_ids with group entity ids replaced by their members. @@ -164,6 +177,18 @@ def async_setup(hass, config): return hass.loop.create_task(_async_process_config(hass, conf, component)) + @callback + def visibility_service_handler(service): + """Change visibility of a group.""" + visible = service.data.get(ATTR_VISIBLE) + for group in component.async_extract_from_service( + service, expand_group=False): + group.async_set_visible(visible) + + hass.services.async_register( + DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, + descriptions[DOMAIN][SERVICE_SET_VISIBILITY], + schema=SET_VISIBILITY_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA) @@ -212,6 +237,7 @@ class Group(Entity): self.group_off = None self._assumed_state = False self._async_unsub_state_changed = None + self._visible = True @staticmethod # pylint: disable=too-many-arguments @@ -268,10 +294,20 @@ class Group(Entity): """Return the icon of the group.""" return self._icon + @callback + def async_set_visible(self, visible): + """Change visibility of the group.""" + if self._visible != visible: + self._visible = visible + self.hass.loop.create_task(self.async_update_ha_state()) + @property def hidden(self): """If group should be hidden or not.""" - return not self._user_defined or self._view + # Visibility from set_visibility service overrides + if self._visible: + return not self._user_defined or self._view + return True @property def state_attributes(self): diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 3df736647cb..fb114f27dd3 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -44,6 +44,18 @@ group: description: "Reload group configuration." fields: + set_visibility: + description: Hide or show a group + + fields: + entity_id: + description: Name(s) of entities to set value + example: 'group.travel' + + visible: + description: True if group should be shown or False if it should be hidden. + example: True + persistent_notification: create: description: Show a notification in the frontend diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index d877f8b0b70..7740f32e4b2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -86,17 +86,18 @@ class EntityComponent(object): discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered) - def extract_from_service(self, service): + def extract_from_service(self, service, expand_group=True): """Extract all known entities from a service call. Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. """ return run_callback_threadsafe( - self.hass.loop, self.async_extract_from_service, service + self.hass.loop, self.async_extract_from_service, service, + expand_group ).result() - def async_extract_from_service(self, service): + def async_extract_from_service(self, service, expand_group=True): """Extract all known entities from a service call. Will return all entities if no entities specified in call. @@ -108,7 +109,7 @@ class EntityComponent(object): return list(self.entities.values()) return [self.entities[entity_id] for entity_id - in extract_entity_ids(self.hass, service) + in extract_entity_ids(self.hass, service, expand_group) if entity_id in self.entities] @asyncio.coroutine diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ccfeb707fea..21df1244872 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -94,7 +94,7 @@ def async_call_from_config(hass, config, blocking=False, variables=None, domain, service_name, service_data, blocking) -def extract_entity_ids(hass, service_call): +def extract_entity_ids(hass, service_call, expand_group=True): """Helper method to extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. @@ -109,7 +109,17 @@ def extract_entity_ids(hass, service_call): # Entity ID attr can be a list or a string service_ent_id = service_call.data[ATTR_ENTITY_ID] - if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id]) + if expand_group: - return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] + if isinstance(service_ent_id, str): + return group.expand_entity_ids(hass, [service_ent_id]) + + return [ent_id for ent_id in + group.expand_entity_ids(hass, service_ent_id)] + + else: + + if isinstance(service_ent_id, str): + return [service_ent_id] + + return service_ent_id diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 5fe14c6377e..5e8f7f38ae8 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -352,3 +352,23 @@ class TestComponentsGroup(unittest.TestCase): assert self.hass.states.entity_ids() == ['group.light'] grp.stop() assert self.hass.states.entity_ids() == [] + + def test_changing_group_visibility(self): + """Test that a group can be hidden and shown.""" + setup_component(self.hass, 'group', { + 'group': { + 'test_group': 'hello.world,sensor.happy' + } + }) + + group_entity_id = group.ENTITY_ID_FORMAT.format('test_group') + + # Hide the group + group.set_visibility(self.hass, group_entity_id, False) + group_state = self.hass.states.get(group_entity_id) + self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) + + # Show it again + group.set_visibility(self.hass, group_entity_id, True) + group_state = self.hass.states.get(group_entity_id) + self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3ba9bf3d1ce..47e4e6c7d2f 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,6 +7,7 @@ from unittest.mock import patch, Mock import homeassistant.core as ha import homeassistant.loader as loader +from homeassistant.components import group from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import discovery @@ -205,6 +206,20 @@ class TestHelpersEntityComponent(unittest.TestCase): assert ['test_domain.test_2'] == \ [ent.entity_id for ent in component.extract_from_service(call)] + def test_extract_from_service_no_group_expand(self): + """Test not expanding a group.""" + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + test_group = group.Group.create_group( + self.hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) + component.add_entities([test_group]) + + call = ha.ServiceCall('test', 'service', { + 'entity_id': ['group.test_group'] + }) + + extracted = component.extract_from_service(call, expand_group=False) + self.assertEqual([test_group], extracted) + def test_setup_loads_platforms(self): """Test the loading of the platforms.""" component_setup = Mock(return_value=True) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index efe21f95d9b..45b9a4919f4 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -153,3 +153,6 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual(['light.ceiling', 'light.kitchen'], service.extract_entity_ids(self.hass, call)) + + self.assertEqual(['group.test'], service.extract_entity_ids( + self.hass, call, expand_group=False)) From aea2d1b31710bf84175fe822b41034ec7206b84b Mon Sep 17 00:00:00 2001 From: Hydreliox Date: Sun, 30 Oct 2016 02:03:26 +0200 Subject: [PATCH 083/149] Add support for Yeelight Wifi bulbs (#4065) * Add support for Yeelight Wifi bulbs * Fix cache property in instance --- .coveragerc | 1 + homeassistant/components/light/yeelight.py | 137 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 141 insertions(+) create mode 100644 homeassistant/components/light/yeelight.py diff --git a/.coveragerc b/.coveragerc index ab5ead454e7..4a9c8788ef3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -172,6 +172,7 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py homeassistant/components/light/x10.py + homeassistant/components/light/yeelight.py homeassistant/components/lirc.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py new file mode 100644 index 00000000000..394006f3ab2 --- /dev/null +++ b/homeassistant/components/light/yeelight.py @@ -0,0 +1,137 @@ +""" +Support for Xiaomi Yeelight Wifi color bulb. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.yeelight/ +""" +import logging +import socket + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_RGB_COLOR, Light, + PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyyeelight==1.0-beta'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'yeelight' + +SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) + +DEVICE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string, }) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, }) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Yeelight bulbs.""" + lights = [] + for ipaddr, device_config in config[CONF_DEVICES].items(): + device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr} + lights.append(YeelightLight(device)) + + add_devices(lights) + + +class YeelightLight(Light): + """Representation of a Yeelight light.""" + + # pylint: disable=too-many-arguments + def __init__(self, device): + """Initialize the light.""" + import pyyeelight + + self._name = device['name'] + self._ipaddr = device['ipaddr'] + self.is_valid = True + self._bulb = None + self._state = None + self._bright = None + self._rgb = None + try: + self._bulb = pyyeelight.YeelightBulb(self._ipaddr) + except socket.error: + self.is_valid = False + _LOGGER.error("Failed to connect to bulb %s, %s", self._ipaddr, + self._name) + + @property + def unique_id(self): + """Return the ID of this light.""" + return "{}.{}".format(self.__class__, self._ipaddr) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state == self._bulb.POWER_ON + + @property + def brightness(self): + """Return the brightness of this light between 1..255.""" + return self._bright + + @property + def rgb_color(self): + """Return the color property.""" + return self._rgb + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_YEELIGHT + + def turn_on(self, **kwargs): + """Turn the specified or all lights on.""" + if not self.is_on: + self._bulb.turn_on() + + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs[ATTR_RGB_COLOR] + self._bulb.set_rgb_color(rgb[0], rgb[1], rgb[2]) + self._rgb = [rgb[0], rgb[1], rgb[2]] + + if ATTR_BRIGHTNESS in kwargs: + bright = int(kwargs[ATTR_BRIGHTNESS] * 100 / 255) + self._bulb.set_brightness(bright) + self._bright = kwargs[ATTR_BRIGHTNESS] + + def turn_off(self, **kwargs): + """Turn the specified or all lights off.""" + self._bulb.turn_off() + + def update(self): + """Synchronize state with bulb.""" + self._bulb.refresh_property() + + # Update power state + self._state = self._bulb.get_property(self._bulb.PROPERTY_NAME_POWER) + + # Update Brightness value + bright_percent = self._bulb.get_property( + self._bulb.PROPERTY_NAME_BRIGHTNESS) + bright = int(bright_percent) * 255 / 100 + # Handle 0 + if int(bright) == 0: + self._bright = 1 + else: + self._bright = int(bright) + + # Update RGB Value + raw_rgb = int( + self._bulb.get_property(self._bulb.PROPERTY_NAME_RGB_COLOR)) + red = int(raw_rgb / 65536) + green = int((raw_rgb - (red * 65536)) / 256) + blue = raw_rgb - (red * 65536) - (green * 256) + self._rgb = [red, green, blue] diff --git a/requirements_all.txt b/requirements_all.txt index d777397e2f6..7c6a303d0e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,6 +436,9 @@ pyvera==0.2.20 # homeassistant.components.wemo pywemo==0.4.7 +# homeassistant.components.light.yeelight +pyyeelight==1.0-beta + # homeassistant.components.climate.radiotherm radiotherm==1.2 From e6ece4bf6d9d38a5361342a22943233f51485aaa Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sat, 29 Oct 2016 20:14:28 -0400 Subject: [PATCH 084/149] Fix initialization of zwave color bulbs (#4085) * Fix initialization of zwave color bulbs Zwave values can be added to the node in any order. This change allows proper initialization when the multilevel value is added before the color value. * Fix incorrect rename of color command class --- homeassistant/components/light/zwave.py | 72 ++++++++++++++++--------- homeassistant/components/zwave/const.py | 2 + 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 346482d3ff6..7e838f97270 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -89,13 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value.set_change_verified(False) if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): - try: - add_devices([ZwaveColorLight(value)]) - except ValueError as exception: - _LOGGER.warning( - "Error initializing as color bulb: %s " - "Initializing as standard dimmer.", exception) - add_devices([ZwaveDimmer(value)]) + add_devices([ZwaveColorLight(value)]) else: add_devices([ZwaveDimmer(value)]) @@ -222,33 +216,63 @@ class ZwaveColorLight(ZwaveDimmer): def __init__(self, value): """Initialize the light.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + self._value_color = None self._value_color_channels = None self._color_channels = None self._rgb = None self._ct = None - # Currently zwave nodes only exist with one color element per node. - for value_color in value.node.get_rgbbulbs().values(): - self._value_color = value_color - - if self._value_color is None: - raise ValueError("No color command found.") - - for value_color_channels in value.node.get_values( - class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR, - genre='System', type="Int").values(): - self._value_color_channels = value_color_channels - - if self._value_color_channels is None: - raise ValueError("Color Channels not found.") - super().__init__(value) + # Create a listener so the color values can be linked to this entity + dispatcher.connect( + self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED) + self._get_color_values() + + def _get_color_values(self): + """Search for color values available on this node.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + + _LOGGER.debug("Searching for zwave color values") + # Currently zwave nodes only exist with one color element per node. + if self._value_color is None: + for value_color in self._value.node.get_rgbbulbs().values(): + self._value_color = value_color + + if self._value_color_channels is None: + for value_color_channels in self._value.node.get_values( + class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR, + genre=zwave.const.GENRE_SYSTEM, + type=zwave.const.TYPE_INT).values(): + self._value_color_channels = value_color_channels + + if self._value_color and self._value_color_channels: + _LOGGER.debug("Zwave node color values found.") + dispatcher.disconnect( + self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED) + self.update_properties() + + def _value_added(self, value): + """Called when a value has been added to the network.""" + if self._value.node != value.node: + return + # Check for the missing color values + self._get_color_values() + + # pylint: disable=too-many-branches def update_properties(self): """Update internal properties based on zwave values.""" super().update_properties() + if self._value_color is None: + return + if self._value_color_channels is None: + return + # Color Channels self._color_channels = self._value_color_channels.data @@ -346,9 +370,7 @@ class ZwaveColorLight(ZwaveDimmer): rgbw += format(colorval, '02x').encode('utf-8') rgbw += b'0000' - if rgbw is None: - _LOGGER.warning("rgbw string was not generated for turn_on") - else: + if rgbw and self._value_color: self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) super().turn_on(**kwargs) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 698dad8e063..d30cb6c5f92 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -294,8 +294,10 @@ SPECIFIC_TYPE_NOTIFICATION_SENSOR = 1 GENRE_WHATEVER = None GENRE_USER = "User" +GENRE_SYSTEM = "System" TYPE_WHATEVER = None TYPE_BYTE = "Byte" TYPE_BOOL = "Bool" TYPE_DECIMAL = "Decimal" +TYPE_INT = "Int" From 9f2aae135740ca2bf417a844d6be8bbba40e1bee Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 09:58:34 +0100 Subject: [PATCH 085/149] Maintenance 2nd (#4106) * Add link to docs * Fix link * Update line breaks * Update ordering * Align vera platofrm to only use add_devices (instead od add_devices_callback) * Remove line break * Use consts * Update ordering * Update ordering * Use const, create default name, use string formatting * Update ordering * Use const * Update import style * Update ordering and line breaks * update line breaks * Set default port * Set defaults and update ordering * Update ordering * Minor style updates * Update ordering, defaults, line breaks, and readability * Use constants * Add line breaks * use string formatting * Update line breaks * Update logger --- .../components/binary_sensor/vera.py | 4 +- homeassistant/components/cover/vera.py | 4 +- .../components/device_tracker/bbox.py | 13 ++-- .../components/device_tracker/nmap_tracker.py | 14 ++--- homeassistant/components/emoncms_history.py | 32 +++++----- .../components/light/limitlessled.py | 51 +++++++-------- homeassistant/components/sensor/bbox.py | 23 ++++--- homeassistant/components/sensor/emoncms.py | 58 +++++++++-------- homeassistant/components/sensor/loopenergy.py | 14 +++-- homeassistant/components/sensor/mhz19.py | 5 +- homeassistant/components/sensor/miflora.py | 62 ++++++++++--------- homeassistant/components/sensor/modbus.py | 24 ++++--- .../components/sensor/mold_indicator.py | 13 ++-- homeassistant/components/sensor/nzbget.py | 19 +++--- homeassistant/components/sensor/plex.py | 3 +- homeassistant/components/sensor/serial_pm.py | 24 +++---- .../components/sensor/systemmonitor.py | 9 +-- homeassistant/components/sensor/ted5000.py | 30 +++++---- .../components/sensor/thinkingcleaner.py | 6 +- homeassistant/components/sensor/vera.py | 4 +- homeassistant/components/sensor/wink.py | 21 +++---- homeassistant/components/sensor/xbox_live.py | 17 +++-- .../components/switch/anel_pwrctrl.py | 16 +++-- homeassistant/components/switch/rest.py | 27 ++++---- homeassistant/components/switch/vera.py | 7 +-- .../components/switch/wake_on_lan.py | 17 +++-- homeassistant/components/tellstick.py | 2 +- homeassistant/components/thingspeak.py | 24 +++---- 28 files changed, 272 insertions(+), 271 deletions(-) diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py index 8673c0e4696..ce2b8b715bd 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -16,9 +16,9 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Vera controller devices.""" - add_devices_callback( + add_devices( VeraBinarySensor(device, VERA_CONTROLLER) for device in VERA_DEVICES['binary_sensor']) diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index 0a9e8abb243..57b85eca981 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -15,9 +15,9 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Vera covers.""" - add_devices_callback( + add_devices( VeraCover(device, VERA_CONTROLLER) for device in VERA_DEVICES['cover']) diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py index c851b622592..50864f47be1 100644 --- a/homeassistant/components/device_tracker/bbox.py +++ b/homeassistant/components/device_tracker/bbox.py @@ -7,15 +7,16 @@ https://home-assistant.io/components/device_tracker.bbox/ from collections import namedtuple import logging from datetime import timedelta + import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import DOMAIN from homeassistant.util import Throttle -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) +REQUIREMENTS = ['pybbox==0.0.5-alpha'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybbox==0.0.5-alpha'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) def get_scanner(hass, config): @@ -36,7 +37,7 @@ class BboxDeviceScanner(object): self.last_results = [] # type: List[Device] self.success_init = self._update_info() - _LOGGER.info('Bbox scanner initialized') + _LOGGER.info("Bbox scanner initialized") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -60,7 +61,7 @@ class BboxDeviceScanner(object): Returns boolean if scanning successful. """ - _LOGGER.info('Scanning') + _LOGGER.info("Scanning...") import pybbox @@ -78,5 +79,5 @@ class BboxDeviceScanner(object): self.last_results = last_results - _LOGGER.info('Bbox scan successful') + _LOGGER.info("Bbox scan successful") return True diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 68155910ffc..e8a6f2b7371 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -18,16 +18,16 @@ from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOSTS from homeassistant.util import Throttle -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +REQUIREMENTS = ['python-nmap==0.6.1'] _LOGGER = logging.getLogger(__name__) +CONF_EXCLUDE = 'exclude' # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = 'home_interval' -CONF_EXCLUDE = 'exclude' -REQUIREMENTS = ['python-nmap==0.6.1'] +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOSTS): cv.ensure_list, @@ -73,7 +73,7 @@ class NmapDeviceScanner(object): self.home_interval = timedelta(minutes=minutes) self.success_init = self._update_info() - _LOGGER.info('nmap scanner initialized') + _LOGGER.info("nmap scanner initialized") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -97,7 +97,7 @@ class NmapDeviceScanner(object): Returns boolean if scanning successful. """ - _LOGGER.info('Scanning') + _LOGGER.info("Scanning...") from nmap import PortScanner, PortScannerError scanner = PortScanner() @@ -138,5 +138,5 @@ class NmapDeviceScanner(object): self.last_results = last_results - _LOGGER.info('nmap scan successful') + _LOGGER.info("nmap scan successful") return True diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py index 4e07447b027..b2bc3967bc8 100644 --- a/homeassistant/components/emoncms_history.py +++ b/homeassistant/components/emoncms_history.py @@ -11,9 +11,7 @@ import voluptuous as vol import requests from homeassistant.const import ( - CONF_API_KEY, CONF_WHITELIST, - CONF_URL, STATE_UNKNOWN, - STATE_UNAVAILABLE, + CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE, CONF_SCAN_INTERVAL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper @@ -22,8 +20,8 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -DOMAIN = "emoncms_history" -CONF_INPUTNODE = "inputnode" +DOMAIN = 'emoncms_history' +CONF_INPUTNODE = 'inputnode' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -37,20 +35,19 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Setup the emoncms_history component.""" + """Set up the Emoncms history component.""" conf = config[DOMAIN] whitelist = conf.get(CONF_WHITELIST) def send_data(url, apikey, node, payload): - """Send payload data to emoncms.""" + """Send payload data to Emoncms.""" try: - fullurl = "{}/input/post.json".format(url) - req = requests.post(fullurl, - params={"node": node}, - data={"apikey": apikey, - "data": payload}, - allow_redirects=True, - timeout=5) + fullurl = '{}/input/post.json'.format(url) + data = {"apikey": apikey, "data": payload} + parameters = {"node": node} + req = requests.post( + fullurl, params=parameters, data=data, allow_redirects=True, + timeout=5) except requests.exceptions.RequestException: _LOGGER.error("Error saving data '%s' to '%s'", @@ -63,14 +60,14 @@ def setup(hass, config): fullurl, req.status_code) def update_emoncms(time): - """Send whitelisted entities states reguarly to emoncms.""" + """Send whitelisted entities states reguarly to Emoncms.""" payload_dict = {} for entity_id in whitelist: state = hass.states.get(entity_id) if state is None or state.state in ( - STATE_UNKNOWN, "", STATE_UNAVAILABLE): + STATE_UNKNOWN, '', STATE_UNAVAILABLE): continue try: @@ -88,8 +85,7 @@ def setup(hass, config): str(conf.get(CONF_INPUTNODE)), payload) track_point_in_time(hass, update_emoncms, time + - timedelta(seconds=conf.get( - CONF_SCAN_INTERVAL))) + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))) update_emoncms(dt_util.utcnow()) return True diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 3709c8c45da..421696d22ba 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, @@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) CONF_BRIDGES = 'bridges' CONF_GROUPS = 'groups' CONF_NUMBER = 'number' -CONF_TYPE = 'type' CONF_VERSION = 'version' DEFAULT_LED_TYPE = 'rgbw' @@ -66,7 +65,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def rewrite_legacy(config): """Rewrite legacy configuration to new format.""" - bridges = config.get('bridges', [config]) + bridges = config.get(CONF_BRIDGES, [config]) new_bridges = [] for bridge_conf in bridges: groups = [] @@ -84,32 +83,33 @@ def rewrite_legacy(config): 'name': bridge_conf.get(name_key) }) new_bridges.append({ - 'host': bridge_conf.get('host'), + 'host': bridge_conf.get(CONF_HOST), 'groups': groups }) return {'bridges': new_bridges} -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the LimitlessLED lights.""" from limitlessled.bridge import Bridge - # Two legacy configuration formats are supported to - # maintain backwards compatibility. + # Two legacy configuration formats are supported to maintain backwards + # compatibility. config = rewrite_legacy(config) # Use the expanded configuration format. lights = [] - for bridge_conf in config.get('bridges'): - bridge = Bridge(bridge_conf.get('host'), - port=bridge_conf.get('port', DEFAULT_PORT), - version=bridge_conf.get('version', DEFAULT_VERSION)) - for group_conf in bridge_conf.get('groups'): - group = bridge.add_group(group_conf.get('number'), - group_conf.get('name'), - group_conf.get('type', DEFAULT_LED_TYPE)) + for bridge_conf in config.get(CONF_BRIDGES): + bridge = Bridge(bridge_conf.get(CONF_HOST), + port=bridge_conf.get(CONF_PORT, DEFAULT_PORT), + version=bridge_conf.get(CONF_VERSION, DEFAULT_VERSION)) + for group_conf in bridge_conf.get(CONF_GROUPS): + group = bridge.add_group( + group_conf.get(CONF_NUMBER), + group_conf.get(CONF_NAME), + group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE)) lights.append(LimitlessLEDGroup.factory(group)) - add_devices_callback(lights) + add_devices(lights) def state(new_state): @@ -225,11 +225,11 @@ class LimitlessLEDWhiteGroup(LimitlessLEDGroup): if ATTR_COLOR_TEMP in kwargs: self._temperature = kwargs[ATTR_COLOR_TEMP] # Set up transition. - pipeline.transition(transition_time, - brightness=_from_hass_brightness( - self._brightness), - temperature=_from_hass_temperature( - self._temperature)) + pipeline.transition( + transition_time, + brightness=_from_hass_brightness(self._brightness), + temperature=_from_hass_temperature(self._temperature) + ) class LimitlessLEDRGBWGroup(LimitlessLEDGroup): @@ -270,10 +270,11 @@ class LimitlessLEDRGBWGroup(LimitlessLEDGroup): pipeline.white() self._color = WHITE # Set up transition. - pipeline.transition(transition_time, - brightness=_from_hass_brightness( - self._brightness), - color=_from_hass_color(self._color)) + pipeline.transition( + transition_time, + brightness=_from_hass_brightness(self._brightness), + color=_from_hass_color(self._color) + ) # Flash. if ATTR_FLASH in kwargs: duration = 0 diff --git a/homeassistant/components/sensor/bbox.py b/homeassistant/components/sensor/bbox.py index c79fa904c5d..b29e2ebb9ec 100644 --- a/homeassistant/components/sensor/bbox.py +++ b/homeassistant/components/sensor/bbox.py @@ -6,31 +6,30 @@ https://home-assistant.io/components/sensor.bbox/ """ import logging from datetime import timedelta + import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_MONITORED_VARIABLES, - ATTR_ATTRIBUTION) +from homeassistant.const import ( + CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - REQUIREMENTS = ['pybbox==0.0.5-alpha'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Powered by Bouygues Telecom" -DEFAULT_NAME = 'Bbox' - -# Bandwidth units BANDWIDTH_MEGABITS_SECONDS = 'Mb/s' # type: str -# Sensor types are defined like so: -# Name, unit, icon +CONF_ATTRIBUTION = "Powered by Bouygues Telecom" + +DEFAULT_NAME = 'Bbox' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +# Sensor types are defined like so: Name, unit, icon SENSOR_TYPES = { 'down_max_bandwidth': ['Maximum Download Bandwidth', BANDWIDTH_MEGABITS_SECONDS, 'mdi:download'], @@ -51,7 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=too-many-arguments def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Bbox sensor.""" + """Set up the Bbox sensor.""" # Create a data fetcher to support all of the configured sensors. Then make # the first call to init the data. try: diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index 6ed269bc467..ac00946a27c 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -13,43 +13,47 @@ import requests import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_URL, CONF_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_ID, CONF_SCAN_INTERVAL, - STATE_UNKNOWN) + CONF_API_KEY, CONF_URL, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, + CONF_ID, CONF_SCAN_INTERVAL, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity from homeassistant.helpers import template from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) +ATTR_FEEDID = 'FeedId' +ATTR_FEEDNAME = 'FeedName' +ATTR_LASTUPDATETIME = 'LastUpdated' +ATTR_LASTUPDATETIMESTR = 'LastUpdatedStr' +ATTR_SIZE = 'Size' +ATTR_TAG = 'Tag' +ATTR_USERID = 'UserId' + +CONF_EXCLUDE_FEEDID = 'exclude_feed_id' +CONF_ONLY_INCLUDE_FEEDID = 'include_only_feed_id' +CONF_SENSOR_NAMES = 'sensor_names' + DECIMALS = 2 -CONF_EXCLUDE_FEEDID = "exclude_feed_id" -CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" -CONF_SENSOR_NAMES = "sensor_names" +DEFAULT_UNIT = 'W' + MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +ONLY_INCL_EXCL_NONE = 'only_include_exclude_or_none' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_URL): cv.string, vol.Required(CONF_ID): cv.positive_int, - vol.Exclusive(CONF_ONLY_INCLUDE_FEEDID, 'only_include_exclude_or_none'): + vol.Exclusive(CONF_ONLY_INCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): vol.All(cv.ensure_list, [cv.positive_int]), - vol.Exclusive(CONF_EXCLUDE_FEEDID, 'only_include_exclude_or_none'): + vol.Exclusive(CONF_EXCLUDE_FEEDID, ONLY_INCL_EXCL_NONE): vol.All(cv.ensure_list, [cv.positive_int]), vol.Optional(CONF_SENSOR_NAMES): vol.All({cv.positive_int: vol.All(cv.string, vol.Length(min=1))}), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="W"): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=DEFAULT_UNIT): cv.string, }) -ATTR_SIZE = 'Size' -ATTR_LASTUPDATETIME = 'LastUpdated' -ATTR_TAG = 'Tag' -ATTR_FEEDID = 'FeedId' -ATTR_USERID = 'UserId' -ATTR_FEEDNAME = 'FeedName' -ATTR_LASTUPDATETIMESTR = 'LastUpdatedStr' - def get_id(sensorid, feedtag, feedname, feedid, feeduserid): """Return unique identifier for feed / sensor.""" @@ -59,7 +63,7 @@ def get_id(sensorid, feedtag, feedname, feedid, feeduserid): # pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Emoncms sensor.""" + """Set up the Emoncms sensor.""" apikey = config.get(CONF_API_KEY) url = config.get(CONF_URL) sensorid = config.get(CONF_ID) @@ -104,7 +108,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes class EmonCmsSensor(Entity): - """Implementation of an EmonCmsSensor sensor.""" + """Implementation of an Emoncms sensor.""" # pylint: disable=too-many-arguments def __init__(self, hass, data, name, value_template, @@ -115,9 +119,8 @@ class EmonCmsSensor(Entity): sensorid, elem["id"]) else: self._name = name - self._identifier = get_id(sensorid, elem["tag"], - elem["name"], elem["id"], - elem["userid"]) + self._identifier = get_id( + sensorid, elem["tag"], elem["name"], elem["id"], elem["userid"]) self._hass = hass self._data = data self._value_template = value_template @@ -192,17 +195,18 @@ class EmonCmsData(object): def __init__(self, hass, url, apikey, interval): """Initialize the data object.""" self._apikey = apikey - self._url = "{}/feed/list.json".format(url) + self._url = '{}/feed/list.json'.format(url) self._interval = interval self._hass = hass self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Get the latest data.""" + """Get the latest data from Emoncms.""" try: - req = requests.get(self._url, params={"apikey": self._apikey}, - allow_redirects=True, timeout=5) + parameters = {"apikey": self._apikey} + req = requests.get( + self._url, params=parameters, allow_redirects=True, timeout=5) except requests.exceptions.RequestException as exception: _LOGGER.error(exception) return @@ -210,6 +214,6 @@ class EmonCmsData(object): if req.status_code == 200: self.data = req.json() else: - _LOGGER.error("please verify if the specified config value " + _LOGGER.error("Please verify if the specified config value " "'%s' is correct! (HTTP Status_code = %d)", CONF_URL, req.status_code) diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index bc295c3a105..f636b039c4e 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -10,6 +10,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -29,19 +31,23 @@ CONF_GAS_CALORIFIC = 'gas_calorific' CONF_GAS_TYPE = 'gas_type' +DEFAULT_CALORIFIC = 39.11 +DEFAULT_UNIT = 'kW' + ELEC_SCHEMA = vol.Schema({ vol.Required(CONF_ELEC_SERIAL): cv.string, vol.Required(CONF_ELEC_SECRET): cv.string, }) -GAS_TYPE_SCHEMA = vol.In(['imperial', 'metric']) +GAS_TYPE_SCHEMA = vol.In([CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]) GAS_SCHEMA = vol.Schema({ vol.Required(CONF_GAS_SERIAL): cv.string, vol.Required(CONF_GAS_SECRET): cv.string, - vol.Optional(CONF_GAS_TYPE, default='metric'): + vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA, - vol.Optional(CONF_GAS_CALORIFIC, default=39.11): vol.Coerce(float) + vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): + vol.Coerce(float) }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -92,7 +98,7 @@ class LoopEnergyDevice(Entity): def __init__(self, controller): """Initialize the sensor.""" self._state = None - self._unit_of_measurement = 'kW' + self._unit_of_measurement = DEFAULT_UNIT self._controller = controller self._name = None diff --git a/homeassistant/components/sensor/mhz19.py b/homeassistant/components/sensor/mhz19.py index c811a193335..2ca15898b18 100644 --- a/homeassistant/components/sensor/mhz19.py +++ b/homeassistant/components/sensor/mhz19.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mhz19/ """ import logging + import voluptuous as vol from homeassistant.const import CONF_NAME @@ -14,10 +15,10 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA REQUIREMENTS = ['pmsensor==0.3'] - _LOGGER = logging.getLogger(__name__) -CONF_SERIAL_DEVICE = "serial_device" +CONF_SERIAL_DEVICE = 'serial_device' + DEFAULT_NAME = 'CO2 Sensor' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 68e103f3363..9cf80c81fd3 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -13,21 +13,27 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME - +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC) REQUIREMENTS = ['miflora==0.1.9'] -LOGGER = logging.getLogger(__name__) -UPDATE_INTERVAL = 1200 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=UPDATE_INTERVAL) -CONF_MAC = 'mac' +_LOGGER = logging.getLogger(__name__) + +CONF_CACHE = 'cache_value' CONF_FORCE_UPDATE = 'force_update' CONF_MEDIAN = 'median' -CONF_TIMEOUT = 'timeout' CONF_RETRIES = 'retries' -CONF_CACHE = 'cache_value' +CONF_TIMEOUT = 'timeout' + +DEFAULT_FORCE_UPDATE = False +DEFAULT_MEDIAN = 3 DEFAULT_NAME = 'Mi Flora' +DEFAULT_RETRIES = 2 +DEFAULT_TIMEOUT = 10 + +UPDATE_INTERVAL = 1200 +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=UPDATE_INTERVAL) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -42,10 +48,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=3): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int, - vol.Optional(CONF_RETRIES, default=2): cv.positive_int, + vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, vol.Optional(CONF_CACHE, default=UPDATE_INTERVAL): cv.positive_int, }) @@ -55,8 +61,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from miflora import miflora_poller cache = config.get(CONF_CACHE) - poller = miflora_poller.MiFloraPoller(config.get(CONF_MAC), - cache_timeout=cache) + poller = miflora_poller.MiFloraPoller( + config.get(CONF_MAC), cache_timeout=cache) force_update = config.get(CONF_FORCE_UPDATE) median = config.get(CONF_MEDIAN) poller.ble_timeout = config.get(CONF_TIMEOUT) @@ -72,12 +78,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if len(prefix) > 0: name = "{} {}".format(prefix, name) - devs.append(MiFloraSensor(poller, - parameter, - name, - unit, - force_update, - median)) + devs.append(MiFloraSensor( + poller, parameter, name, unit, force_update, median)) add_devices(devs) @@ -85,8 +87,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MiFloraSensor(Entity): """Implementing the MiFlora sensor.""" -# pylint: disable=too-many-instance-attributes,too-many-arguments - def __init__(self, poller, parameter, name, unit, force_update, median=3): + # pylint: disable=too-many-instance-attributes,too-many-arguments + def __init__(self, poller, parameter, name, unit, force_update, median): """Initialize the sensor.""" self.poller = poller self.parameter = parameter @@ -128,19 +130,19 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ try: - LOGGER.debug("Polling data for %s", self.name) + _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) except IOError as ioerr: - LOGGER.info("Polling error %s", ioerr) + _LOGGER.info("Polling error %s", ioerr) data = None return if data is not None: - LOGGER.debug("%s = %s", self.name, data) + _LOGGER.debug("%s = %s", self.name, data) self.data.append(data) else: - LOGGER.info("Did not receive any data from Mi Flora sensor %s", - self.name) + _LOGGER.info("Did not receive any data from Mi Flora sensor %s", + self.name) # Remove old data from median list or set sensor value to None # if no data is available anymore if len(self.data) > 0: @@ -149,13 +151,13 @@ class MiFloraSensor(Entity): self._state = None return - LOGGER.debug("Data collected: %s", self.data) + _LOGGER.debug("Data collected: %s", self.data) if len(self.data) > self.median_count: self.data = self.data[1:] if len(self.data) == self.median_count: median = sorted(self.data)[int((self.median_count - 1) / 2)] - LOGGER.debug("Median is: %s", median) + _LOGGER.debug("Median is: %s", median) self._state = median else: - LOGGER.debug("Not yet enough data for median calculation") + _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 266583d4dd1..44ee73ddfa0 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.modbus/ """ import logging + import voluptuous as vol import homeassistant.components.modbus as modbus @@ -17,12 +18,12 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['modbus'] -CONF_COUNT = "count" -CONF_PRECISION = "precision" -CONF_REGISTER = "register" -CONF_REGISTERS = "registers" -CONF_SCALE = "scale" -CONF_SLAVE = "slave" +CONF_COUNT = 'count' +CONF_PRECISION = 'precision' +CONF_REGISTER = 'register' +CONF_REGISTERS = 'registers' +CONF_SCALE = 'scale' +CONF_SLAVE = 'slave' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_REGISTERS): [{ @@ -39,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Modbus sensors.""" + """Set up the Modbus sensors.""" sensors = [] for register in config.get(CONF_REGISTERS): sensors.append(ModbusRegisterSensor( @@ -94,13 +95,10 @@ class ModbusRegisterSensor(Entity): self._count) val = 0 if not result: - _LOGGER.error( - 'No response from modbus slave %s register %s', - self._slave, - self._register) + _LOGGER.error("No response from modbus slave %s register %s", + self._slave, self._register) return for i, res in enumerate(result.registers): val += res * (2**(i*16)) self._value = format( - self._scale * val + self._offset, - ".{}f".format(self._precision)) + self._scale * val + self._offset, '.{}f'.format(self._precision)) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index b8f635ec593..6ee5c465265 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -19,18 +19,19 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Mold Indicator' +ATTR_CRITICAL_TEMP = 'Est. Crit. Temp' +ATTR_DEWPOINT = 'Dewpoint' + +CONF_CALIBRATION_FACTOR = 'calibration_factor' +CONF_INDOOR_HUMIDITY = 'indoor_humidity_sensor' CONF_INDOOR_TEMP = 'indoor_temp_sensor' CONF_OUTDOOR_TEMP = 'outdoor_temp_sensor' -CONF_INDOOR_HUMIDITY = 'indoor_humidity_sensor' -CONF_CALIBRATION_FACTOR = 'calibration_factor' + +DEFAULT_NAME = 'Mold Indicator' MAGNUS_K2 = 17.62 MAGNUS_K3 = 243.12 -ATTR_DEWPOINT = 'Dewpoint' -ATTR_CRITICAL_TEMP = 'Est. Crit. Temp' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_INDOOR_TEMP): cv.entity_id, vol.Required(CONF_OUTDOOR_TEMP): cv.entity_id, diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py index f7a13645c59..f6b4fed1ac1 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -61,9 +61,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): url = "http://{}:{}/jsonrpc".format(host, port) try: - nzbgetapi = NZBGetAPI(api_url=url, - username=username, - password=password) + nzbgetapi = NZBGetAPI( + api_url=url, username=username, password=password) nzbgetapi.update() except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as conn_err: @@ -72,9 +71,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for ng_type in monitored_types: - new_sensor = NZBGetSensor(api=nzbgetapi, - sensor_type=SENSOR_TYPES.get(ng_type), - client_name=name) + new_sensor = NZBGetSensor( + api=nzbgetapi, sensor_type=SENSOR_TYPES.get(ng_type), + client_name=name) devices.append(new_sensor) add_devices(devices) @@ -159,11 +158,9 @@ class NZBGetAPI(object): if params: payload['params'] = params try: - response = requests.post(self.api_url, - json=payload, - auth=self.auth, - headers=self.headers, - timeout=5) + response = requests.post( + self.api_url, json=payload, auth=self.auth, + headers=self.headers, timeout=5) response.raise_for_status() return response.json() except requests.exceptions.ConnectionError as conn_exc: diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 7ead3175371..7370841acfe 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -23,6 +23,7 @@ CONF_SERVER = 'server' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Plex' +DEFAULT_PORT = 32400 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) @@ -30,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=32400): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_USERNAME): cv.string, }) diff --git a/homeassistant/components/sensor/serial_pm.py b/homeassistant/components/sensor/serial_pm.py index b5e200eaa24..9704991e959 100644 --- a/homeassistant/components/sensor/serial_pm.py +++ b/homeassistant/components/sensor/serial_pm.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.serial_pm/ """ import logging + import voluptuous as vol from homeassistant.const import CONF_NAME @@ -14,26 +15,27 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA REQUIREMENTS = ['pmsensor==0.3'] - _LOGGER = logging.getLogger(__name__) -CONF_SERIAL_DEVICE = "serial_device" -CONF_BRAND = "brand" +CONF_SERIAL_DEVICE = 'serial_device' +CONF_BRAND = 'brand' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=""): cv.string, - vol.Required(CONF_SERIAL_DEVICE): cv.string, vol.Required(CONF_BRAND): cv.string, + vol.Required(CONF_SERIAL_DEVICE): cv.string, + vol.Optional(CONF_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the available PM sensors.""" + """Set up the available PM sensors.""" from pmsensor import serial_pm as pm try: - coll = pm.PMDataCollector(config.get(CONF_SERIAL_DEVICE), - pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)]) + coll = pm.PMDataCollector( + config.get(CONF_SERIAL_DEVICE), + pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)] + ) except KeyError: _LOGGER.error("Brand %s not supported\n supported brands: %s", config.get(CONF_BRAND), pm.SUPPORTED_SENSORS.keys()) @@ -46,10 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for pmname in coll.supported_values(): - if config.get("name") != "": - name = "{} PM{}".format(config.get("name"), pmname) + if config.get(CONF_NAME) is None: + name = '{} PM{}'.format(config.get(CONF_NAME), pmname) else: - name = "PM{}".format(pmname) + name = 'PM{}'.format(pmname) dev.append(ParticulateMatterSensor(coll, name, pmname)) add_devices(dev) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 8bd9ad491b6..ba5fa3efdbf 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_RESOURCES, STATE_OFF, STATE_ON) +from homeassistant.const import ( + CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -43,7 +44,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_RESOURCES, default=['disk_use']): vol.All(cv.ensure_list, [vol.Schema({ - vol.Required('type'): vol.In(SENSOR_TYPES), + vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), vol.Optional('arg'): cv.string, })]) }) @@ -56,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for resource in config[CONF_RESOURCES]: if 'arg' not in resource: resource['arg'] = '' - dev.append(SystemMonitorSensor(resource['type'], resource['arg'])) + dev.append(SystemMonitorSensor(resource[CONF_TYPE], resource['arg'])) add_devices(dev) @@ -66,7 +67,7 @@ class SystemMonitorSensor(Entity): def __init__(self, sensor_type, argument=''): """Initialize the sensor.""" - self._name = SENSOR_TYPES[sensor_type][0] + ' ' + argument + self._name = '{} {}'.format(SENSOR_TYPES[sensor_type][0], argument) self.argument = argument self.type = sensor_type self._state = None diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index f995b9232aa..5376b89199a 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -3,14 +3,10 @@ Support gahtering ted500 information. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ted5000/ - -Ted5000 collection from -https://github.com/weirded/ted5000-collectd-plugin/blob/master/ted5000.py - -Ted500 framework from glances plugin. """ import logging from datetime import timedelta + import requests import voluptuous as vol @@ -20,26 +16,29 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +REQUIREMENTS = ['xmltodict==0.10.2'] + _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['xmltodict==0.10.2'] +DEFAULT_NAME = 'ted' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=80): cv.port, - vol.Optional(CONF_NAME, default='ted'): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -_LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Ted5000 sensor.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "http://{}:{}/api/LiveData.xml".format(host, port) + name = config.get(CONF_NAME) + url = 'http://{}:{}/api/LiveData.xml'.format(host, port) gateway = Ted5000Gateway(url) @@ -48,8 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for mtu in gateway.data: - dev.append(Ted5000Sensor(gateway, config.get('name'), mtu, 'W')) - dev.append(Ted5000Sensor(gateway, config.get('name'), mtu, 'V')) + dev.append(Ted5000Sensor(gateway, name, mtu, 'W')) + dev.append(Ted5000Sensor(gateway, name, mtu, 'V')) add_devices(dev) return True @@ -62,7 +61,7 @@ class Ted5000Sensor(Entity): """Initialize the sensor.""" units = {'W': 'power', 'V': 'voltage'} self._gateway = gateway - self._name = '%s mtu%d %s' % (name, mtu, units[unit]) + self._name = '{} mtu{} {}'.format(name, mtu, units[unit]) self._mtu = mtu self._unit = unit self.update() @@ -120,5 +119,4 @@ class Ted5000Gateway(object): if power == 0 or voltage == 0: continue else: - self.data[mtu] = {'W': power, - 'V': voltage / 10} + self.data[mtu] = {'W': power, 'V': voltage / 10} diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py index 3683fb15bf2..35462466f2a 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -54,7 +54,7 @@ STATES = { def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the ThinkingCleaner platform.""" + """Set up the ThinkingCleaner platform.""" from pythinkingcleaner import Discovery discovery = Discovery() @@ -76,7 +76,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ThinkingCleanerSensor(Entity): - """ThinkingCleaner Sensor.""" + """Representation of a ThinkingCleaner Sensor.""" def __init__(self, tc_object, sensor_type, update_devices): """Initialize the ThinkingCleaner.""" @@ -90,7 +90,7 @@ class ThinkingCleanerSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return self._tc_object.name + ' ' + SENSOR_TYPES[self.type][0] + return '{} {}'.format(self._tc_object.name, SENSOR_TYPES[self.type][0]) @property def icon(self): diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 927c1863cce..eeec43bfb40 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -17,9 +17,9 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Vera controller devices.""" - add_devices_callback( + add_devices( VeraSensor(device, VERA_CONTROLLER) for device in VERA_DEVICES['sensor']) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8ba2e09c6c9..569beba4866 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -6,8 +6,8 @@ at https://home-assistant.io/components/sensor.wink/ """ import logging -from homeassistant.const import (STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS) +from homeassistant.const import ( + STATE_CLOSED, STATE_OPEN, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity from homeassistant.components.wink import WinkDevice from homeassistant.loader import get_component @@ -18,7 +18,7 @@ SENSOR_TYPES = ['temperature', 'humidity', 'balance', 'proximity'] def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Wink platform.""" + """Set up the Wink platform.""" import pywink for sensor in pywink.get_sensors(): @@ -32,8 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if piggy_bank.capability() in SENSOR_TYPES: add_devices([WinkSensorDevice(piggy_bank)]) except AttributeError: - logging.getLogger(__name__).error( - "Device is not a sensor.") + logging.getLogger(__name__).error("Device is not a sensor") class WinkSensorDevice(WinkDevice, Entity): @@ -44,7 +43,7 @@ class WinkSensorDevice(WinkDevice, Entity): super().__init__(wink) wink = get_component('wink') self.capability = self.wink.capability() - if self.wink.UNIT == "°": + if self.wink.UNIT == '°': self._unit_of_measurement = TEMP_CELSIUS else: self._unit_of_measurement = self.wink.UNIT @@ -52,13 +51,13 @@ class WinkSensorDevice(WinkDevice, Entity): @property def state(self): """Return the state.""" - if self.capability == "humidity": + if self.capability == 'humidity': return round(self.wink.humidity_percentage()) - elif self.capability == "temperature": + elif self.capability == 'temperature': return round(self.wink.temperature_float(), 1) - elif self.capability == "balance": + elif self.capability == 'balance': return round(self.wink.balance() / 100, 2) - elif self.capability == "proximity": + elif self.capability == 'proximity': return self.wink.proximity_float() else: return STATE_OPEN if self.is_open else STATE_CLOSED @@ -71,7 +70,7 @@ class WinkSensorDevice(WinkDevice, Entity): Always return true for Wink porkfolio due to bug in API. """ - if self.capability == "balance": + if self.capability == 'balance': return True return self.wink.available diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py index 36e100394e9..b9dac2948c6 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.xbox_live/ """ import logging + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,14 +13,14 @@ from homeassistant.const import (CONF_API_KEY, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) - -ICON = 'mdi:xbox' - REQUIREMENTS = ['xboxapi==0.1.1'] +_LOGGER = logging.getLogger(__name__) + CONF_XUID = 'xuid' +ICON = 'mdi:xbox' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_XUID): vol.All(cv.ensure_list, [cv.string]) @@ -28,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Xbox platform.""" + """Set up the Xbox platform.""" from xboxapi import xbox_api api = xbox_api.XboxApi(config.get(CONF_API_KEY)) devices = [] @@ -59,8 +60,7 @@ class XboxSensor(Entity): # get profile info profile = self._api.get_user_profile(self._xuid) - if profile.get('success', True) \ - and profile.get('code', 0) != 28: + if profile.get('success', True) and profile.get('code', 0) != 28: self.success_init = True self._gamertag = profile.get('Gamertag') self._picture = profile.get('GameDisplayPicRaw') @@ -84,8 +84,7 @@ class XboxSensor(Entity): for device in self._presence: for title in device.get('titles'): attributes[ - '{} {}'.format(device.get('type'), - title.get('placement')) + '{} {}'.format(device.get('type'), title.get('placement')) ] = title.get('name') return attributes diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 024fefe2bc5..8cea062ca47 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -15,18 +15,17 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME) from homeassistant.util import Throttle - REQUIREMENTS = ['https://github.com/mweinelt/anel-pwrctrl/archive/' 'ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip' '#anel_pwrctrl==0.0.1'] +_LOGGER = logging.getLogger(__name__) + CONF_PORT_RECV = "port_recv" CONF_PORT_SEND = "port_send" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PORT_RECV): cv.port, vol.Required(CONF_PORT_SEND): cv.port, @@ -48,13 +47,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from anel_pwrctrl import DeviceMaster try: - master = DeviceMaster(username=username, - password=password, - read_port=port_send, - write_port=port_recv) + master = DeviceMaster( + username=username, password=password, read_port=port_send, + write_port=port_recv) master.query(ip_addr=host) except socket.error as ex: - _LOGGER.error('Unable to discover PwrCtrl device: %s', str(ex)) + _LOGGER.error("Unable to discover PwrCtrl device: %s", str(ex)) return False devices = [] @@ -84,7 +82,7 @@ class PwrCtrlSwitch(SwitchDevice): @property def unique_id(self): """Return the unique ID of the device.""" - return "{device}-{switch_idx}".format( + return '{device}-{switch_idx}'.format( device=self._port.device.host, switch_idx=self._port.get_index() ) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index e6ac231e3e1..9d5ea639704 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -10,8 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) +from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv CONF_BODY_OFF = 'body_off' @@ -35,8 +34,8 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument, -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup the RESTful switch.""" +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RESTful switch.""" name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) body_on = config.get(CONF_BODY_ON) @@ -61,9 +60,9 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGER.error("No route to resource/endpoint: %s", resource) return False - add_devices_callback( - [RestSwitch(hass, name, resource, - body_on, body_off, is_on_template, timeout)]) + add_devices( + [RestSwitch( + hass, name, resource, body_on, body_off, is_on_template, timeout)]) # pylint: disable=too-many-arguments @@ -71,8 +70,8 @@ class RestSwitch(SwitchDevice): """Representation of a switch that can be toggled using REST.""" # pylint: disable=too-many-instance-attributes - def __init__(self, hass, name, resource, body_on, body_off, - is_on_template, timeout): + def __init__(self, hass, name, resource, body_on, body_off, is_on_template, + timeout): """Initialize the REST switch.""" self._state = None self._hass = hass @@ -96,9 +95,8 @@ class RestSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the device on.""" body_on_t = self._body_on.render() - request = requests.post(self._resource, - data=body_on_t, - timeout=self._timeout) + request = requests.post( + self._resource, data=body_on_t, timeout=self._timeout) if request.status_code == 200: self._state = True else: @@ -108,9 +106,8 @@ class RestSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the device off.""" body_off_t = self._body_off.render() - request = requests.post(self._resource, - data=body_off_t, - timeout=self._timeout) + request = requests.post( + self._resource, data=body_off_t, timeout=self._timeout) if request.status_code == 200: self._state = False else: diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index e88e45a5171..a8b360e2339 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -8,8 +8,7 @@ import logging from homeassistant.util import convert from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - STATE_OFF, STATE_ON) +from homeassistant.const import (STATE_OFF, STATE_ON) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -18,9 +17,9 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Vera switches.""" - add_devices_callback( + add_devices( VeraSwitch(device, VERA_CONTROLLER) for device in VERA_DEVICES['switch']) diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index 0ecbd51a11b..66652fb106c 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -30,13 +30,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Add wake on lan switch.""" +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a wake on lan switch.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) mac_address = config.get(CONF_MAC_ADDRESS) - add_devices_callback([WOLSwitch(hass, name, host, mac_address)]) + add_devices([WOLSwitch(hass, name, host, mac_address)]) class WOLSwitch(SwitchDevice): @@ -79,13 +79,12 @@ class WOLSwitch(SwitchDevice): def update(self): """Check if device is on and update the state.""" - if platform.system().lower() == "windows": - ping_cmd = "ping -n 1 -w {} {}"\ - .format(DEFAULT_PING_TIMEOUT * 1000, self._host) + if platform.system().lower() == 'windows': + ping_cmd = 'ping -n 1 -w {} {}'.format( + DEFAULT_PING_TIMEOUT * 1000, self._host) else: - ping_cmd = "ping -c 1 -W {} {}"\ - .format(DEFAULT_PING_TIMEOUT, self._host) + ping_cmd = 'ping -c 1 -W {} {}'.format( + DEFAULT_PING_TIMEOUT, self._host) status = sp.getstatusoutput(ping_cmd)[0] - self._state = not bool(status) diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index cccba502dc4..d2e296d61b6 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -2,7 +2,7 @@ Tellstick Component. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/Tellstick/ +https://home-assistant.io/components/tellstick/ """ import logging import threading diff --git a/homeassistant/components/thingspeak.py b/homeassistant/components/thingspeak.py index 6f01475372b..5f0ce2dc596 100644 --- a/homeassistant/components/thingspeak.py +++ b/homeassistant/components/thingspeak.py @@ -1,12 +1,16 @@ -"""A component to submit data to thingspeak.""" +""" +A component to submit data to thingspeak. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/thingspeak/ +""" import logging -import voluptuous as vol from requests.exceptions import RequestException +import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_ID, CONF_WHITELIST, - STATE_UNAVAILABLE, STATE_UNKNOWN) + CONF_API_KEY, CONF_ID, CONF_WHITELIST, STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as event @@ -16,23 +20,22 @@ REQUIREMENTS = ['thingspeak==0.4.0'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'thingspeak' + TIMEOUT = 5 -# Validate the config CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_ID): int, vol.Required(CONF_WHITELIST): cv.string }), - }, extra=vol.ALLOW_EXTRA) +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): - """Setup the thingspeak environment.""" + """Set up the Thingspeak environment.""" import thingspeak - # Read out config values conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) channel_id = conf.get(CONF_ID) @@ -62,9 +65,8 @@ def setup(hass, config): try: channel.update({'field1': _state}) except RequestException: - _LOGGER.error( - 'Error while sending value "%s" to Thingspeak', - _state) + _LOGGER.error("Error while sending value '%s' to Thingspeak", + _state) event.track_state_change(hass, entity, thingspeak_listener) From 27abac85b6080ca1625332a1bec7352ed6132609 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 15:21:23 +0100 Subject: [PATCH 086/149] Migrate to async (sensor.time_date) (#4100) * Migrate to async * Update acc. #4114 --- homeassistant/components/sensor/time_date.py | 40 ++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index 9281080b3b4..a6c61959734 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -4,20 +4,21 @@ Support for showing the date and the time. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.time_date/ """ -import logging from datetime import timedelta +import asyncio +import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DISPLAY_OPTIONS -import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -TIME_STR_FORMAT = "%H:%M" +TIME_STR_FORMAT = '%H:%M' OPTION_TYPES = { 'time': 'Time', @@ -34,17 +35,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the Time and Date sensor.""" if hass.config.time_zone is None: - _LOGGER.error("Timezone is not set in Home Assistant config") + _LOGGER.error("Timezone is not set in Home Assistant configuration") return False devices = [] for variable in config[CONF_DISPLAY_OPTIONS]: devices.append(TimeDateSensor(variable)) - add_devices(devices) + hass.loop.create_task(async_add_devices(devices, True)) + return True # pylint: disable=too-few-public-methods @@ -56,7 +59,6 @@ class TimeDateSensor(Entity): self._name = OPTION_TYPES[option_type] self.type = option_type self._state = None - self.update() @property def name(self): @@ -71,14 +73,15 @@ class TimeDateSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - if "date" in self.type and "time" in self.type: - return "mdi:calendar-clock" - elif "date" in self.type: - return "mdi:calendar" + if 'date' in self.type and 'time' in self.type: + return 'mdi:calendar-clock' + elif 'date' in self.type: + return 'mdi:calendar' else: - return "mdi:clock" + return 'mdi:clock' - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and updates the states.""" time_date = dt_util.utcnow() time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT) @@ -87,10 +90,9 @@ class TimeDateSensor(Entity): # Calculate Swatch Internet Time. time_bmt = time_date + timedelta(hours=1) - delta = timedelta(hours=time_bmt.hour, - minutes=time_bmt.minute, - seconds=time_bmt.second, - microseconds=time_bmt.microsecond) + delta = timedelta( + hours=time_bmt.hour, minutes=time_bmt.minute, + seconds=time_bmt.second, microseconds=time_bmt.microsecond) beat = int((delta.seconds + delta.microseconds / 1000000.0) / 86.4) if self.type == 'time': @@ -98,9 +100,9 @@ class TimeDateSensor(Entity): elif self.type == 'date': self._state = date elif self.type == 'date_time': - self._state = date + ', ' + time + self._state = '{}, {}'.format(date, time) elif self.type == 'time_date': - self._state = time + ', ' + date + self._state = '{}, {}'.format(time, date) elif self.type == 'time_utc': self._state = time_utc elif self.type == 'beat': From 5e76a51db4030de3bda65a75cf501fcd101901c7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 15:23:47 +0100 Subject: [PATCH 087/149] Migrate to async (#4135) --- homeassistant/components/sensor/worldclock.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 7f4d91a78f9..22bedfcd21c 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -4,6 +4,7 @@ Support for showing the time in a different time zone. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.worldclock/ """ +import asyncio import logging import voluptuous as vol @@ -28,12 +29,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the World clock sensor.""" +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the World clock sensor.""" name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) - add_devices([WorldClockSensor(time_zone, name)]) + hass.loop.create_task(async_add_devices( + [WorldClockSensor(time_zone, name)], True)) + return True class WorldClockSensor(Entity): @@ -44,7 +48,6 @@ class WorldClockSensor(Entity): self._name = name self._time_zone = time_zone self._state = None - self.update() @property def name(self): @@ -61,7 +64,8 @@ class WorldClockSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + @asyncio.coroutine + def async_update(self): """Get the time and updates the states.""" self._state = dt_util.now(time_zone=self._time_zone).strftime( TIME_STR_FORMAT) From 9649097b32ef8c59871415dfe113ec1c3019fdef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 16:45:53 +0100 Subject: [PATCH 088/149] Migrate to async (sensor.min_max) (#4136) * Migrate to async * Add async_ prefix --- homeassistant/components/sensor/min_max.py | 30 ++++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index e88c2dffe6a..19698ebf868 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -4,16 +4,18 @@ Support for displaying the minimal and the maximal value. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.min_max/ """ +import asyncio import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, STATE_UNKNOWN, CONF_TYPE, ATTR_UNIT_OF_MEASUREMENT) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) @@ -31,7 +33,7 @@ ATTR_TO_PROPERTY = [ CONF_ENTITY_IDS = 'entity_ids' -DEFAULT_NAME = 'Min/Max Sensor' +DEFAULT_NAME = 'Min/Max/Avg Sensor' ICON = 'mdi:calculator' @@ -49,13 +51,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the min/max sensor.""" +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the min/max/mean sensor.""" entity_ids = config.get(CONF_ENTITY_IDS) name = config.get(CONF_NAME) sensor_type = config.get(CONF_TYPE) - add_devices([MinMaxSensor(hass, entity_ids, name, sensor_type)]) + hass.loop.create_task(async_add_devices( + [MinMaxSensor(hass, entity_ids, name, sensor_type)], True)) + return True # pylint: disable=too-many-instance-attributes @@ -74,9 +79,10 @@ class MinMaxSensor(Entity): self.min_value = self.max_value = self.mean = STATE_UNKNOWN self.count_sensors = len(self._entity_ids) self.states = {} - self.update() - def min_max_sensor_state_listener(entity, old_state, new_state): + @callback + # pylint: disable=invalid-name + def async_min_max_sensor_state_listener(entity, old_state, new_state): """Called when the sensor changes state.""" if new_state.state is None or new_state.state in STATE_UNKNOWN: return @@ -95,9 +101,10 @@ class MinMaxSensor(Entity): _LOGGER.warning("Unable to store state. " "Only numerical states are supported") - self.update_ha_state(True) + hass.loop.create_task(self.async_update_ha_state(True)) - track_state_change(hass, entity_ids, min_max_sensor_state_listener) + async_track_state_change( + hass, entity_ids, async_min_max_sensor_state_listener) @property def name(self): @@ -134,7 +141,8 @@ class MinMaxSensor(Entity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and updates the states.""" sensor_values = [self.states[k] for k in self._entity_ids if k in self.states] From b910a9917d59f77906b3cf1146e3761c8f8d0172 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 18:56:26 +0100 Subject: [PATCH 089/149] Migrate to async (sensor.statistics) (#4138) * Migrate to async * Add async_ prefix and remove stale print --- homeassistant/components/sensor/statistics.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 6e75c105ec3..10614fb9c93 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -4,18 +4,20 @@ Support for statistics for sensor values. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.statistics/ """ +import asyncio import logging import statistics from collections import deque import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, CONF_ENTITY_ID, STATE_UNKNOWN, ATTR_UNIT_OF_MEASUREMENT) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) @@ -41,13 +43,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Statistics sensor.""" +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Statistics sensor.""" entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) sampling_size = config.get(CONF_SAMPLING_SIZE) - add_devices([StatisticsSensor(hass, entity_id, name, sampling_size)]) + hass.loop.create_task(async_add_devices( + [StatisticsSensor(hass, entity_id, name, sampling_size)], True)) + return True # pylint: disable=too-many-instance-attributes @@ -72,9 +77,10 @@ class StatisticsSensor(Entity): self.states = deque(maxlen=self._sampling_size) self.median = self.mean = self.variance = self.stdev = 0 self.min = self.max = self.total = self.count = 0 - self.update() - def stats_sensor_state_listener(entity, old_state, new_state): + @callback + # pylint: disable=invalid-name + def async_stats_sensor_state_listener(entity, old_state, new_state): """Called when the sensor changes state.""" self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT) @@ -85,9 +91,10 @@ class StatisticsSensor(Entity): except ValueError: self.count = self.count + 1 - self.update_ha_state(True) + hass.loop.create_task(self.async_update_ha_state(True)) - track_state_change(hass, entity_id, stats_sensor_state_listener) + async_track_state_change( + hass, entity_id, async_stats_sensor_state_listener) @property def name(self): @@ -131,7 +138,8 @@ class StatisticsSensor(Entity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and updates the states.""" if not self.is_binary: try: From be272ac64a691f04bbd8ea92d7914521cc59508e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 30 Oct 2016 22:18:53 +0100 Subject: [PATCH 090/149] Disable too-many-* (#4107) * Disable too-many-* and too-few-public-methods * Remove globally disabled pylint warnings --- homeassistant/bootstrap.py | 5 +--- .../alarm_control_panel/alarmdotcom.py | 1 - .../alarm_control_panel/envisalink.py | 1 - .../components/alarm_control_panel/manual.py | 1 - .../components/alarm_control_panel/mqtt.py | 1 - homeassistant/components/alexa.py | 1 - .../components/automation/__init__.py | 1 - .../components/binary_sensor/arest.py | 2 -- .../components/binary_sensor/command_line.py | 1 - .../components/binary_sensor/concord232.py | 1 - .../components/binary_sensor/envisalink.py | 1 - .../components/binary_sensor/mqtt.py | 1 - .../components/binary_sensor/octoprint.py | 2 -- .../components/binary_sensor/rest.py | 2 -- .../components/binary_sensor/rpi_gpio.py | 1 - .../components/binary_sensor/sleepiq.py | 1 - .../components/binary_sensor/template.py | 1 - .../components/binary_sensor/trend.py | 1 - homeassistant/components/bloomsky.py | 2 +- homeassistant/components/camera/__init__.py | 1 - homeassistant/components/camera/foscam.py | 1 - homeassistant/components/camera/generic.py | 1 - homeassistant/components/camera/mjpeg.py | 1 - homeassistant/components/camera/synology.py | 3 -- homeassistant/components/climate/__init__.py | 4 +-- homeassistant/components/climate/demo.py | 2 -- homeassistant/components/climate/ecobee.py | 2 +- .../components/climate/eq3btsmart.py | 2 +- .../components/climate/generic_thermostat.py | 3 +- homeassistant/components/climate/heatmiser.py | 2 +- homeassistant/components/climate/honeywell.py | 2 +- homeassistant/components/climate/mysensors.py | 1 - homeassistant/components/climate/nest.py | 2 +- homeassistant/components/climate/zwave.py | 2 -- homeassistant/components/configurator.py | 2 -- .../components/cover/command_line.py | 2 -- homeassistant/components/cover/demo.py | 2 +- homeassistant/components/cover/mqtt.py | 1 - homeassistant/components/cover/rpi_gpio.py | 1 - homeassistant/components/cover/scsgate.py | 1 - .../components/device_sun_light_trigger.py | 1 - .../components/device_tracker/__init__.py | 3 -- .../components/device_tracker/asuswrt.py | 2 -- .../components/device_tracker/automatic.py | 2 -- .../components/device_tracker/ddwrt.py | 1 - .../components/device_tracker/fritz.py | 1 - .../components/device_tracker/locative.py | 2 +- .../components/device_tracker/luci.py | 1 - .../components/device_tracker/owntracks.py | 3 +- .../components/device_tracker/snmp.py | 1 - .../components/device_tracker/ubus.py | 1 - homeassistant/components/digital_ocean.py | 1 - homeassistant/components/downloader.py | 1 - homeassistant/components/dweet.py | 1 - homeassistant/components/ecobee.py | 1 - homeassistant/components/emulated_hue.py | 1 - homeassistant/components/enocean.py | 5 ++-- homeassistant/components/envisalink.py | 3 +- homeassistant/components/fan/__init__.py | 2 -- homeassistant/components/fan/mqtt.py | 2 -- homeassistant/components/feedreader.py | 1 - homeassistant/components/frontend/__init__.py | 2 -- homeassistant/components/group.py | 4 --- homeassistant/components/history.py | 1 - homeassistant/components/homematic.py | 5 +--- homeassistant/components/http.py | 10 +++---- homeassistant/components/influxdb.py | 1 - homeassistant/components/input_select.py | 1 - homeassistant/components/input_slider.py | 1 - homeassistant/components/isy994.py | 6 ++-- homeassistant/components/joaoapps_join.py | 1 - homeassistant/components/light/__init__.py | 2 -- homeassistant/components/light/demo.py | 1 - homeassistant/components/light/flux_led.py | 1 - homeassistant/components/light/hue.py | 1 - homeassistant/components/light/lifx.py | 4 --- homeassistant/components/light/mqtt.py | 2 -- homeassistant/components/light/mqtt_json.py | 1 - homeassistant/components/light/wink.py | 1 - homeassistant/components/light/yeelight.py | 1 - homeassistant/components/light/zwave.py | 2 -- homeassistant/components/lock/mqtt.py | 1 - homeassistant/components/logbook.py | 5 +--- .../components/media_player/__init__.py | 4 +-- .../components/media_player/braviatv.py | 4 +-- homeassistant/components/media_player/cast.py | 1 - homeassistant/components/media_player/cmus.py | 2 +- .../components/media_player/denon.py | 2 +- .../components/media_player/directv.py | 1 - .../components/media_player/gpmdp.py | 3 +- .../components/media_player/itunes.py | 3 -- homeassistant/components/media_player/kodi.py | 3 +- .../components/media_player/lg_netcast.py | 3 +- homeassistant/components/media_player/mpd.py | 2 +- .../components/media_player/onkyo.py | 3 +- .../media_player/panasonic_viera.py | 1 - .../components/media_player/pandora.py | 1 - .../components/media_player/pioneer.py | 3 +- homeassistant/components/media_player/plex.py | 3 +- homeassistant/components/media_player/roku.py | 1 - .../components/media_player/russound_rnet.py | 3 +- .../components/media_player/samsungtv.py | 1 - .../components/media_player/sonos.py | 3 -- .../components/media_player/squeezebox.py | 4 +-- .../components/media_player/universal.py | 2 -- .../components/media_player/webostv.py | 2 -- .../components/media_player/yamaha.py | 4 +-- homeassistant/components/mqtt/__init__.py | 2 -- homeassistant/components/mysensors.py | 7 +---- homeassistant/components/notify/__init__.py | 2 -- homeassistant/components/notify/apns.py | 2 -- homeassistant/components/notify/aws_lambda.py | 1 - homeassistant/components/notify/aws_sns.py | 1 - homeassistant/components/notify/aws_sqs.py | 1 - .../components/notify/command_line.py | 1 - homeassistant/components/notify/demo.py | 1 - homeassistant/components/notify/ecobee.py | 1 - homeassistant/components/notify/file.py | 1 - .../components/notify/free_mobile.py | 1 - homeassistant/components/notify/gntp.py | 2 -- homeassistant/components/notify/group.py | 1 - homeassistant/components/notify/html5.py | 4 --- homeassistant/components/notify/instapush.py | 1 - homeassistant/components/notify/ios.py | 1 - .../components/notify/joaoapps_join.py | 1 - homeassistant/components/notify/kodi.py | 1 - .../components/notify/llamalab_automate.py | 1 - homeassistant/components/notify/matrix.py | 3 -- .../components/notify/message_bird.py | 1 - .../components/notify/nfandroidtv.py | 3 -- homeassistant/components/notify/nma.py | 1 - homeassistant/components/notify/pushbullet.py | 1 - homeassistant/components/notify/pushetta.py | 1 - homeassistant/components/notify/pushover.py | 1 - homeassistant/components/notify/rest.py | 1 - homeassistant/components/notify/sendgrid.py | 1 - homeassistant/components/notify/simplepush.py | 1 - homeassistant/components/notify/slack.py | 1 - homeassistant/components/notify/smtp.py | 2 -- homeassistant/components/notify/syslog.py | 2 -- homeassistant/components/notify/telegram.py | 1 - homeassistant/components/notify/telstra.py | 1 - homeassistant/components/notify/twilio_sms.py | 1 - homeassistant/components/notify/twitter.py | 1 - homeassistant/components/notify/webostv.py | 1 - homeassistant/components/notify/xmpp.py | 1 - homeassistant/components/nuimo_controller.py | 14 +++++---- homeassistant/components/openalpr.py | 5 ---- homeassistant/components/proximity.py | 3 -- homeassistant/components/qwikswitch.py | 1 - homeassistant/components/recorder/__init__.py | 1 - homeassistant/components/recorder/models.py | 3 -- homeassistant/components/rfxtrx.py | 2 -- homeassistant/components/script.py | 1 - homeassistant/components/sensor/arest.py | 3 -- homeassistant/components/sensor/bbox.py | 2 -- homeassistant/components/sensor/bitcoin.py | 2 -- homeassistant/components/sensor/bom.py | 1 - .../components/sensor/coinmarketcap.py | 2 -- .../components/sensor/command_line.py | 2 -- .../components/sensor/currencylayer.py | 2 -- homeassistant/components/sensor/darksky.py | 4 --- .../components/sensor/deutsche_bahn.py | 2 -- homeassistant/components/sensor/dht.py | 1 - .../components/sensor/dte_energy_bridge.py | 1 - homeassistant/components/sensor/dweet.py | 2 -- homeassistant/components/sensor/efergy.py | 2 -- homeassistant/components/sensor/emoncms.py | 4 --- homeassistant/components/sensor/fastdotcom.py | 1 - homeassistant/components/sensor/fitbit.py | 4 --- homeassistant/components/sensor/fixer.py | 1 - .../components/sensor/fritzbox_callmonitor.py | 2 -- homeassistant/components/sensor/glances.py | 2 +- .../components/sensor/google_travel_time.py | 3 -- homeassistant/components/sensor/gtfs.py | 2 -- homeassistant/components/sensor/hp_ilo.py | 1 - homeassistant/components/sensor/imap.py | 1 - .../components/sensor/imap_email_content.py | 2 -- homeassistant/components/sensor/influxdb.py | 1 - homeassistant/components/sensor/knx.py | 3 +- homeassistant/components/sensor/lastfm.py | 2 +- .../components/sensor/linux_battery.py | 1 - homeassistant/components/sensor/loopenergy.py | 6 ---- homeassistant/components/sensor/miflora.py | 1 - homeassistant/components/sensor/min_max.py | 1 - homeassistant/components/sensor/modbus.py | 1 - .../components/sensor/mold_indicator.py | 2 -- homeassistant/components/sensor/mqtt.py | 1 - homeassistant/components/sensor/mqtt_room.py | 1 - homeassistant/components/sensor/netatmo.py | 4 --- .../components/sensor/neurio_energy.py | 2 -- homeassistant/components/sensor/nzbget.py | 2 +- homeassistant/components/sensor/octoprint.py | 2 -- .../components/sensor/openexchangerates.py | 2 -- .../components/sensor/openweathermap.py | 2 -- homeassistant/components/sensor/pilight.py | 1 - homeassistant/components/sensor/plex.py | 1 - homeassistant/components/sensor/rest.py | 3 -- homeassistant/components/sensor/rfxtrx.py | 1 - homeassistant/components/sensor/sabnzbd.py | 1 - homeassistant/components/sensor/scrape.py | 3 -- homeassistant/components/sensor/sleepiq.py | 1 - homeassistant/components/sensor/snmp.py | 2 -- homeassistant/components/sensor/speedtest.py | 1 - homeassistant/components/sensor/statistics.py | 1 - .../sensor/swiss_hydrological_data.py | 3 -- .../sensor/swiss_public_transport.py | 3 -- .../components/sensor/systemmonitor.py | 1 - homeassistant/components/sensor/ted5000.py | 1 - homeassistant/components/sensor/template.py | 1 - homeassistant/components/sensor/time_date.py | 1 - homeassistant/components/sensor/torque.py | 1 - homeassistant/components/sensor/uber.py | 4 --- homeassistant/components/sensor/vasttrafik.py | 1 - .../components/sensor/wunderground.py | 1 - homeassistant/components/sensor/xbox_live.py | 1 - .../components/sensor/yahoo_finance.py | 1 - homeassistant/components/sensor/yr.py | 2 -- homeassistant/components/sensor/yweather.py | 2 -- homeassistant/components/sleepiq.py | 2 -- .../components/switch/anel_pwrctrl.py | 1 - .../components/switch/command_line.py | 2 -- homeassistant/components/switch/flux.py | 3 -- .../components/switch/hikvisioncam.py | 2 -- homeassistant/components/switch/modbus.py | 1 - homeassistant/components/switch/mqtt.py | 1 - homeassistant/components/switch/pilight.py | 1 - .../components/switch/pulseaudio_loopback.py | 1 - homeassistant/components/switch/rest.py | 6 ++-- homeassistant/components/switch/rpi_rf.py | 1 - homeassistant/components/switch/template.py | 1 - homeassistant/components/vera.py | 2 +- homeassistant/components/verisure.py | 1 - homeassistant/components/weather/__init__.py | 2 +- homeassistant/components/weather/demo.py | 1 - .../components/weather/openweathermap.py | 2 -- homeassistant/components/zone.py | 1 - homeassistant/config.py | 1 - homeassistant/core.py | 8 ----- homeassistant/helpers/condition.py | 1 - homeassistant/helpers/entity_component.py | 3 -- homeassistant/helpers/event.py | 2 -- homeassistant/helpers/event_decorators.py | 2 -- homeassistant/helpers/script.py | 1 - homeassistant/helpers/state.py | 2 +- homeassistant/helpers/template.py | 1 - homeassistant/remote.py | 11 ++++--- homeassistant/scripts/check_config.py | 10 ++++--- homeassistant/scripts/db_migrator.py | 3 +- homeassistant/util/__init__.py | 6 ++-- homeassistant/util/dt.py | 1 - homeassistant/util/location.py | 2 +- homeassistant/util/unit_system.py | 1 - pylintrc | 14 +++++++-- tests/common.py | 4 +-- tests/components/device_tracker/test_init.py | 26 +++++++++------- .../device_tracker/test_locative.py | 5 ++-- .../device_tracker/test_owntracks.py | 2 -- tests/components/light/test_demo.py | 8 +++-- tests/components/light/test_init.py | 8 +++-- tests/components/media_player/test_cast.py | 2 +- tests/components/sensor/test_pilight.py | 6 ++-- tests/components/sensor/test_wunderground.py | 1 - tests/components/switch/test_init.py | 8 +++-- tests/components/test_alexa.py | 8 +++-- tests/components/test_api.py | 8 +++-- tests/components/test_configurator.py | 8 +++-- tests/components/test_conversation.py | 8 +++-- .../test_device_sun_light_trigger.py | 8 +++-- tests/components/test_frontend.py | 8 +++-- tests/components/test_group.py | 8 +++-- tests/components/test_history.py | 8 +++-- tests/components/test_http.py | 5 ++-- tests/components/test_init.py | 8 +++-- tests/components/test_input_boolean.py | 8 +++-- tests/components/test_input_select.py | 8 +++-- tests/components/test_input_slider.py | 8 +++-- tests/components/test_logbook.py | 2 +- tests/components/test_rfxtrx.py | 2 +- tests/components/test_script.py | 8 +++-- tests/components/test_sun.py | 8 +++-- tests/helpers/test_entity.py | 2 +- tests/helpers/test_entity_component.py | 2 +- tests/helpers/test_event.py | 9 +++--- tests/helpers/test_event_decorators.py | 9 +++--- tests/helpers/test_init.py | 8 +++-- tests/helpers/test_location.py | 1 - tests/helpers/test_script.py | 8 +++-- tests/helpers/test_template.py | 7 +++-- tests/test_bootstrap.py | 3 +- tests/test_config.py | 8 +++-- tests/test_core.py | 30 ++++++++++++------- tests/test_loader.py | 8 +++-- tests/test_remote.py | 20 +++++++------ tests/util/test_dt.py | 1 - tests/util/test_init.py | 1 - tests/util/test_location.py | 1 - tests/util/test_yaml.py | 9 +++--- 298 files changed, 271 insertions(+), 570 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e0664ee5b6e..b9a3d89251a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -99,8 +99,7 @@ def _async_setup_component(hass: core.HomeAssistant, This method is a coroutine. """ - # pylint: disable=too-many-return-statements,too-many-branches - # pylint: disable=too-many-statements + # pylint: disable=too-many-return-statements if domain in hass.config.components: return True @@ -312,7 +311,6 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, return platform -# pylint: disable=too-many-branches, too-many-statements, too-many-arguments def from_config_dict(config: Dict[str, Any], hass: Optional[core.HomeAssistant]=None, config_dir: Optional[str]=None, @@ -352,7 +350,6 @@ def from_config_dict(config: Dict[str, Any], @asyncio.coroutine -# pylint: disable=too-many-branches, too-many-statements, too-many-arguments def async_from_config_dict(config: Dict[str, Any], hass: core.HomeAssistant, config_dir: Optional[str]=None, diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index a986d911115..714741d7e1e 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -42,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([AlarmDotCom(hass, name, code, username, password)]) -# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=abstract-method class AlarmDotCom(alarm.AlarmControlPanel): """Represent an Alarm.com status.""" diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 5c5dd1729b2..fa013cd3ffe 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Representation of an Envisalink-based alarm panel.""" - # pylint: disable=too-many-arguments def __init__(self, partition_number, alarm_name, code, panic_type, info, controller): """Initialize the alarm panel.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 986f1966797..2af0c1499f6 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=abstract-method class ManualAlarm(alarm.AlarmControlPanel): """ diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index b5bdf478add..558653aa6a6 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -55,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_CODE))]) -# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 3093b0eb12f..72c0b2a8705 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -286,7 +286,6 @@ class AlexaFlashBriefingView(HomeAssistantView): self.flash_briefings = copy.deepcopy(flash_briefings) template.attach(hass, self.flash_briefings) - # pylint: disable=too-many-branches @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a5c18258781..27b1fa9cd13 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -219,7 +219,6 @@ class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" # pylint: disable=abstract-method - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, name, async_attach_triggers, cond_func, async_action, hidden): """Initialize an automation entity.""" diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 02f9d75c404..1c7058cd1b0 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_class, pin)]) -# pylint: disable=too-many-instance-attributes, too-many-arguments class ArestBinarySensor(BinarySensorDevice): """Implement an aREST binary sensor for a pin.""" @@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice): self.arest.update() -# pylint: disable=too-few-public-methods class ArestData(object): """Class for handling the data retrieval for pins.""" diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 57b3c7c03f7..6950e0b80c4 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value_template)]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class CommandBinarySensor(BinarySensorDevice): """Represent a command line binary sensor.""" diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index 48f36c00697..d9cb11ba6a7 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Concord232 binary sensor platform.""" from concord232 import client as concord232_client diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index b0dad12522b..5bbc15aefcf 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): """Representation of an Envisalink binary sensor.""" - # pylint: disable=too-many-arguments def __init__(self, zone_number, zone_name, zone_type, info, controller): """Initialize the binary_sensor.""" from pydispatch import dispatcher diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index e26ee7d08dc..53c8e9a60b6 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -51,7 +51,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttBinarySensor(BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 6763eaafa55..4284d2844bd 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=too-many-instance-attributes class OctoPrintBinarySensor(BinarySensorDevice): """Representation an OctoPrint binary sensor.""" - # pylint: disable=too-many-arguments def __init__(self, api, condition, sensor_type, sensor_name, unit, endpoint, group, tool=None): """Initialize a new OctoPrint sensor.""" diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 36b455f9dfe..72f506eaefc 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the REST binary sensor.""" name = config.get(CONF_NAME) @@ -76,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, rest, name, sensor_class, value_template)]) -# pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): """Representation of a REST binary sensor.""" diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 7bc05cd6764..13dd7d0b860 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(binary_sensors) -# pylint: disable=too-many-arguments, too-many-instance-attributes class RPiGPIOBinarySensor(BinarySensorDevice): """Represent a binary sensor that uses Raspberry Pi GPIO.""" diff --git a/homeassistant/components/binary_sensor/sleepiq.py b/homeassistant/components/binary_sensor/sleepiq.py index 08d575ebcb8..0409d04f9a5 100644 --- a/homeassistant/components/binary_sensor/sleepiq.py +++ b/homeassistant/components/binary_sensor/sleepiq.py @@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice): """Implementation of a SleepIQ presence sensor.""" diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 5d4d31a57a4..6d470f335e3 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -70,7 +70,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BinarySensorTemplate(BinarySensorDevice): """A virtual binary sensor that triggers from another sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, device, friendly_name, sensor_class, value_template, entity_ids): """Initialize the Template binary sensor.""" diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index e258d6cf443..2ef9c487d82 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SensorTrend(BinarySensorDevice): """Representation of a trend Sensor.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, device_id, friendly_name, target_entity, attribute, sensor_class, invert): """Initialize the sensor.""" diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index dd27a0ba954..13225773b3a 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument,too-few-public-methods +# pylint: disable=unused-argument def setup(hass, config): """Setup BloomSky component.""" api_key = config[DOMAIN][CONF_API_KEY] diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 35a922ee0f1..d2ca0b50801 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -28,7 +28,6 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' @asyncio.coroutine -# pylint: disable=too-many-branches def async_setup(hass, config): """Setup the camera component.""" component = EntityComponent( diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 987b8c51af5..e84794356b2 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([FoscamCamera(config)]) -# pylint: disable=too-many-instance-attributes class FoscamCamera(Camera): """An implementation of a Foscam IP camera.""" diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 861a7cab758..b1502778878 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -46,7 +46,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass.loop.create_task(async_add_devices([GenericCamera(hass, config)])) -# pylint: disable=too-many-instance-attributes class GenericCamera(Camera): """A generic implementation of an IP camera.""" diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 42baab0bbf3..ea83ded4371 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -58,7 +58,6 @@ def extract_image_from_mjpeg(stream): return jpg -# pylint: disable=too-many-instance-attributes class MjpegCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 3b3ba2e41a8..77e1b3ee4d0 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -23,7 +23,6 @@ from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-locals DEFAULT_NAME = 'Synology Camera' DEFAULT_STREAM_ID = '0' TIMEOUT = 5 @@ -179,11 +178,9 @@ def get_session_id(hass, username, password, login_url, valid_cert): return auth_resp['data']['sid'] -# pylint: disable=too-many-instance-attributes class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" -# pylint: disable=too-many-arguments def __init__(self, config, camera_id, camera_name, snapshot_path, streaming_path, camera_path, auth_path): """Initialize a Synology Surveillance Station camera.""" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 714581ba331..5ae10fca303 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) -# pylint: disable=too-many-arguments def set_temperature(hass, temperature=None, entity_id=None, target_temp_high=None, target_temp_low=None, operation_mode=None): @@ -181,7 +180,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) -# pylint: disable=too-many-branches def setup(hass, config): """Setup climate devices.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) @@ -364,7 +362,7 @@ def setup(hass, config): class ClimateDevice(Entity): """Representation of a climate device.""" - # pylint: disable=too-many-public-methods,no-self-use + # pylint: disable=no-self-use @property def state(self): """Return the current state.""" diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 0104d9d01af..04053febf90 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-arguments, too-many-public-methods class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" - # pylint: disable=too-many-instance-attributes def __init__(self, name, target_temperature, unit_of_measurement, away, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 1ed826e55dc..6193b955a61 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -72,7 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=SET_FAN_MIN_ON_TIME_SCHEMA) -# pylint: disable=too-many-public-methods, abstract-method +# pylint: disable=abstract-method class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 87d9e322405..f6f0497c4af 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=too-many-instance-attributes, import-error, abstract-method +# pylint: disable=import-error, abstract-method class EQ3BTSmartThermostat(ClimateDevice): """Representation of a eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index c5c38d624f5..64448e9677c 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -62,11 +62,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): target_temp, ac_mode, min_cycle_duration)]) -# pylint: disable=too-many-instance-attributes, abstract-method +# pylint: disable=abstract-method class GenericThermostat(ClimateDevice): """Representation of a GenericThermostat device.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): """Initialize the thermostat.""" diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index a6dd01af4ab..9f589b4c015 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - # pylint: disable=too-many-instance-attributes, abstract-method + # pylint: disable=abstract-method def __init__(self, heatmiser, device, name, serport): """Initialize the thermostat.""" self.heatmiser = heatmiser diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 3af4f62246d..09b5d92b9b6 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -100,7 +100,7 @@ def _setup_us(username, password, config, add_devices): class RoundThermostat(ClimateDevice): """Representation of a Honeywell Round Connected thermostat.""" - # pylint: disable=too-many-instance-attributes, abstract-method + # pylint: disable=abstract-method def __init__(self, device, zone_id, master, away_temp): """Initialize the thermostat.""" self.device = device diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index c93a69ac0b9..2a815625434 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, MySensorsHVAC)) -# pylint: disable=too-many-arguments, too-many-public-methods class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): """Representation of a MySensorsHVAC hvac.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index fb7e4b7ec11..f9ac15e7d80 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for structure, device in nest.devices()]) -# pylint: disable=abstract-method,too-many-public-methods +# pylint: disable=abstract-method class NestThermostat(ClimateDevice): """Representation of a Nest thermostat.""" diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 3b7fce9ace1..7dcd72cd37c 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Represents a ZWave Climate device.""" - # pylint: disable=too-many-instance-attributes def __init__(self, value, temp_unit): """Initialize the zwave climate device.""" from openzwave.network import ZWaveNetwork @@ -121,7 +120,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self.update_ha_state() _LOGGER.debug("Value changed on network %s", value) - # pylint: disable=too-many-branches def update_properties(self): """Callback on data change for the registered node/value pair.""" # Operation Mode diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index d205b45e446..5e99e02d371 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -34,7 +34,6 @@ STATE_CONFIGURE = 'configure' STATE_CONFIGURED = 'configured' -# pylint: disable=too-many-arguments def request_config( hass, name, callback, description=None, description_image=None, submit_caption=None, fields=None, link_name=None, link_url=None, @@ -102,7 +101,6 @@ class Configurator(object): hass.services.register( DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) - # pylint: disable=too-many-arguments def request_config( self, name, callback, description, description_image, submit_caption, diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index 27e1c810cf4..778496ec6fc 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=too-many-arguments, too-many-instance-attributes class CommandCover(CoverDevice): """Representation a command line cover.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, command_open, command_close, command_stop, command_state, value_template): """Initialize the cover.""" diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index acddfcf7c73..5929ab1851a 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoCover(CoverDevice): """Representation of a demo cover.""" - # pylint: disable=no-self-use, too-many-instance-attributes + # pylint: disable=no-self-use def __init__(self, hass, name, position=None, tilt_position=None): """Initialize the cover.""" self.hass = hass diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9b61f52b67c..27b30e5e013 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttCover(CoverDevice): """Representation of a cover that can be controlled using MQTT.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 00034bd718b..39a82b5b3fc 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RPiGPIOCover(CoverDevice): """Representation of a Raspberry GPIO cover.""" - # pylint: disable=too-many-arguments def __init__(self, name, relay_pin, state_pin, state_pull_mode, relay_time): """Initialize the cover.""" diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index c491f3bf548..f2047b03230 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=too-many-arguments, too-many-instance-attributes class SCSGateCover(CoverDevice): """Representation of SCSGate cover.""" diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 1bf921c2e06..ed31e624b91 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -41,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def setup(hass, config): """The triggers to turn lights on or off based on device presence.""" logger = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index aefc220c809..b7f4e839d7b 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -4,8 +4,6 @@ Provide functionality to keep track of devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_tracker/ """ -# pylint: disable=too-many-instance-attributes, too-many-arguments -# pylint: disable=too-many-locals import asyncio from datetime import timedelta import logging @@ -88,7 +86,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None): return hass.states.is_state(entity, STATE_HOME) -# pylint: disable=too-many-arguments def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 4fd2771db4f..50411591cb7 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -90,9 +90,7 @@ AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp') class AsusWrtDeviceScanner(object): """This class queries a router running ASUSWRT firmware.""" - # pylint: disable=too-many-instance-attributes, too-many-branches # Eighth attribute needed for mode (AP mode vs router mode) - def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 27bd9c6b477..a07db8ec404 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see): return True -# pylint: disable=too-many-instance-attributes -# pylint: disable=too-few-public-methods class AutomaticDeviceScanner(object): """A class representing an Automatic device.""" diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 9ccb15a1707..a67ee3d1d39 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -41,7 +41,6 @@ def get_scanner(hass, config): return None -# pylint: disable=too-many-instance-attributes class DdWrtDeviceScanner(object): """This class queries a wireless router running DD-WRT firmware.""" diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 5832fa425be..fd30946c919 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -38,7 +38,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class FritzBoxScanner(object): """This class queries a FRITZ!Box router.""" diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index adbd1dd13d4..f6419ae2490 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -51,9 +51,9 @@ class LocativeView(HomeAssistantView): return res @asyncio.coroutine + # pylint: disable=too-many-return-statements def _handle(self, data): """Handle locative request.""" - # pylint: disable=too-many-return-statements if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index b97993f9afa..f9e90fee6c7 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -37,7 +37,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class LuciDeviceScanner(object): """This class queries a wireless router running OpenWrt firmware. diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index d903c25a5c8..566b62fb171 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -114,10 +114,9 @@ def setup_scanner(hass, config, see): 'for topic %s.', topic) return None + # pylint: disable=too-many-return-statements def validate_payload(topic, payload, data_type): """Validate the OwnTracks payload.""" - # pylint: disable=too-many-return-statements - try: data = json.loads(payload) except ValueError: diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 33c89110da0..39315ebfd7a 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -49,7 +49,6 @@ def get_scanner(hass, config): class SnmpScanner(object): """Queries any SNMP capable Access Point for connected devices.""" - # pylint: disable=too-many-instance-attributes def __init__(self, config): """Initialize the scanner.""" from pysnmp.entity.rfc3413.oneliner import cmdgen diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 5eaa4bf2fca..9d9b8e718d6 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -37,7 +37,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class UbusDeviceScanner(object): """ This class queries a wireless router running OpenWrt firmware. diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index b507d9448e5..f976c17ae9d 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -42,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument,too-few-public-methods def setup(hass, config): """Set up the Digital Ocean component.""" conf = config[DOMAIN] diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 57b6bd4dc6d..4330ad5be2f 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-branches def setup(hass, config): """Listen for download events to download files.""" download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet.py index d56d9d2ef93..d812daf50a6 100644 --- a/homeassistant/components/dweet.py +++ b/homeassistant/components/dweet.py @@ -32,7 +32,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def setup(hass, config): """Setup the Dweet.io component.""" conf = config[DOMAIN] diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 24d47365a54..825e7b700a5 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -86,7 +86,6 @@ def setup_ecobee(hass, network, config): discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) -# pylint: disable=too-few-public-methods class EcobeeData(object): """Get the latest data and update the states.""" diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 62680d84d36..6aebb91f72f 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -105,7 +105,6 @@ def setup(hass, yaml_config): return True -# pylint: disable=too-few-public-methods class Config(object): """Holds configuration variables for the emulated hue bridge.""" diff --git a/homeassistant/components/enocean.py b/homeassistant/components/enocean.py index 7c36e173510..33c6359d43f 100644 --- a/homeassistant/components/enocean.py +++ b/homeassistant/components/enocean.py @@ -56,14 +56,14 @@ class EnOceanDongle: """Send a command from the EnOcean dongle.""" self.__communicator.send(command) - def _combine_hex(self, data): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def _combine_hex(self, data): """Combine list of integer values to one big integer.""" output = 0x00 for i, j in enumerate(reversed(data)): output |= (j << i * 8) return output - # pylint: disable=too-many-branches def callback(self, temp): """Callback function for EnOcean Device. @@ -112,7 +112,6 @@ class EnOceanDongle: device.value_changed(value) -# pylint: disable=too-few-public-methods class EnOceanDevice(): """Parent class for all devices associated with the EnOcean component.""" diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 0572de9aba6..21bc081224b 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -77,8 +77,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument, too-many-function-args, too-many-locals -# pylint: disable=too-many-return-statements +# pylint: disable=unused-argument def setup(hass, base_config): """Common setup for Envisalink devices.""" from pyenvisalink import EnvisalinkAlarmPanel diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a129ece3609..b3c210285e8 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -86,7 +86,6 @@ def is_on(hass, entity_id: str=None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -# pylint: disable=too-many-arguments def turn_on(hass, entity_id: str=None, speed: str=None) -> None: """Turn all or specified fan on.""" data = { @@ -141,7 +140,6 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) -# pylint: disable=too-many-branches, too-many-locals, too-many-statements def setup(hass, config: dict) -> None: """Expose fan control via statemachine and services.""" component = EntityComponent( diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index b6703e22c19..08db5ead26b 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -110,11 +110,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-instance-attributes class MqttFan(FanEntity): """A MQTT fan component.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, topic, templates, qos, retain, payload, speed_list, optimistic): """Initialize the MQTT fan.""" diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index ce3d46b4751..a563b51402e 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -44,7 +44,6 @@ def setup(hass, config): return len(feeds) > 0 -# pylint: disable=too-few-public-methods class FeedManager(object): """Abstraction over feedparser module.""" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 494e3aee401..d95ba8f981f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -41,7 +41,6 @@ _LOGGER = logging.getLogger(__name__) def register_built_in_panel(hass, component_name, sidebar_title=None, sidebar_icon=None, url_path=None, config=None): """Register a built-in panel.""" - # pylint: disable=too-many-arguments path = 'panels/ha-panel-{}.html'.format(component_name) if hass.http.development: @@ -70,7 +69,6 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, Warning: this API will probably change. Use at own risk. """ - # pylint: disable=too-many-arguments if url_path is None: url_path = component_name diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index ecd79cae3ab..3843c1b4854 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -218,7 +218,6 @@ def _async_process_config(hass, config, component): class Group(Entity): """Track a group of entity ids.""" - # pylint: disable=too-many-instance-attributes, too-many-arguments def __init__(self, hass, name, order=None, user_defined=True, icon=None, view=False): """Initialize a group. @@ -240,7 +239,6 @@ class Group(Entity): self._visible = True @staticmethod - # pylint: disable=too-many-arguments def create_group(hass, name, entity_ids=None, user_defined=True, icon=None, view=False, object_id=None): """Initialize a group.""" @@ -251,7 +249,6 @@ class Group(Entity): @staticmethod @asyncio.coroutine - # pylint: disable=too-many-arguments def async_create_group(hass, name, entity_ids=None, user_defined=True, icon=None, view=False, object_id=None): """Initialize a group. @@ -420,7 +417,6 @@ class Group(Entity): This method must be run in the event loop. """ - # pylint: disable=too-many-branches # To store current states of group entities. Might not be needed. states = None gr_state = self._state diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index c8230386aa0..c3dd0bd3f5a 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -247,7 +247,6 @@ class HistoryPeriodView(HomeAssistantView): return self.json(result.values()) -# pylint: disable=too-few-public-methods class Filters(object): """Container for the configured include and exclude filters.""" diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6e4727f8188..42124c643b9 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -183,7 +183,7 @@ def set_value(hass, entity_id, value): hass.services.call(DOMAIN, SERVICE_SET_VALUE, data) -# pylint: disable=unused-argument,too-many-locals +# pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" global HOMEMATIC, HOMEMATIC_LINK_DELAY @@ -271,7 +271,6 @@ def setup(hass, config): return True -# pylint: disable=too-many-branches def _system_callback_handler(hass, config, src, *args): """Callback handler.""" if src == 'newDevices': @@ -323,7 +322,6 @@ def _get_devices(device_type, keys): """Get the Homematic devices.""" device_arr = [] - # pylint: disable=too-many-nested-blocks for key in keys: device = HOMEMATIC.devices[key] class_name = device.__class__.__name__ @@ -585,7 +583,6 @@ class HMVariable(Entity): class HMDevice(Entity): """The Homematic device base object.""" - # pylint: disable=too-many-instance-attributes def __init__(self, config): """Initialize a generic Homematic device.""" self._name = config.get(ATTR_NAME) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 680ae9cfeda..da2f0ac06f0 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -97,7 +97,6 @@ def request_class(): class HideSensitiveFilter(logging.Filter): """Filter API password calls.""" - # pylint: disable=too-few-public-methods def __init__(self, hass): """Initialize sensitive data filter.""" super().__init__() @@ -247,9 +246,6 @@ class HAStaticRoute(StaticRoute): class HomeAssistantWSGI(object): """WSGI server for Home Assistant.""" - # pylint: disable=too-many-instance-attributes, too-many-locals - # pylint: disable=too-many-arguments - def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, trusted_networks): @@ -405,7 +401,8 @@ class HomeAssistantView(object): self.hass = hass - def json(self, result, status_code=200): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def json(self, result, status_code=200): """Return a JSON response.""" msg = json.dumps( result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') @@ -417,7 +414,8 @@ class HomeAssistantView(object): return self.json({'message': error}, status_code) @asyncio.coroutine - def file(self, request, fil): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def file(self, request, fil): """Return a file.""" assert isinstance(fil, str), 'only string paths allowed' response = yield from _GZIP_FILE_SENDER.send(request, Path(fil)) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 420781bcb74..d96fb8c384f 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -49,7 +49,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def setup(hass, config): """Setup the InfluxDB component.""" from influxdb import InfluxDBClient, exceptions diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index d309bf5c709..d725a1129cf 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -148,7 +148,6 @@ def async_setup(hass, config): class InputSelect(Entity): """Representation of a select input.""" - # pylint: disable=too-many-arguments def __init__(self, object_id, name, state, options, icon): """Initialize a select input.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index f83d643cb5d..f5ac8ead91c 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -114,7 +114,6 @@ def async_setup(hass, config): class InputSlider(Entity): """Represent an slider.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, object_id, name, state, minimum, maximum, step, icon, unit): """Initialize a select input.""" diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 379712fa989..0539469f198 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -93,15 +93,16 @@ def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None: NODES = [] GROUPS = [] + # pylint: disable=no-member for (path, node) in ISY.nodes: hidden = hidden_identifier in path or hidden_identifier in node.name if hidden: node.name += hidden_identifier if sensor_identifier in path or sensor_identifier in node.name: SENSOR_NODES.append(node) - elif isinstance(node, PYISY.Nodes.Node): # pylint: disable=no-member + elif isinstance(node, PYISY.Nodes.Node): NODES.append(node) - elif isinstance(node, PYISY.Nodes.Group): # pylint: disable=no-member + elif isinstance(node, PYISY.Nodes.Group): GROUPS.append(node) @@ -131,7 +132,6 @@ def _categorize_programs() -> None: PROGRAMS[component].append(program) -# pylint: disable=too-many-locals def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ISY 994 platform.""" isy_config = config.get(DOMAIN) diff --git a/homeassistant/components/joaoapps_join.py b/homeassistant/components/joaoapps_join.py index 654a13cb269..4f01a3cf411 100644 --- a/homeassistant/components/joaoapps_join.py +++ b/homeassistant/components/joaoapps_join.py @@ -27,7 +27,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def register_device(hass, device_id, api_key, name): """Method to register services for each join device listed.""" from pyjoin import (ring_device, set_wallpaper, send_sms, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d3abd3c2c1a..8cd4292908a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -124,7 +124,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -# pylint: disable=too-many-arguments def turn_on(hass, entity_id=None, transition=None, brightness=None, rgb_color=None, xy_color=None, color_temp=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): @@ -172,7 +171,6 @@ def toggle(hass, entity_id=None, transition=None): hass.services.call(DOMAIN, SERVICE_TOGGLE, data) -# pylint: disable=too-many-branches, too-many-locals, too-many-statements def setup(hass, config): """Expose light control via statemachine and services.""" component = EntityComponent( diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index ac8e017cb7d..e68bde8f379 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -34,7 +34,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class DemoLight(Light): """Represenation of a demo light.""" - # pylint: disable=too-many-arguments def __init__( self, name, state, rgb=None, ct=None, brightness=180, xy_color=(.5, .5), white=200): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index ce84072b5bb..e504242200e 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -76,7 +76,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class FluxLight(Light): """Representation of a Flux light.""" - # pylint: disable=too-many-arguments def __init__(self, device): """Initialize the light.""" import flux_led diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 46ba1099ef7..249cc30498f 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -199,7 +199,6 @@ def request_configuration(host, hass, add_devices, filename, class HueLight(Light): """Representation of a Hue light.""" - # pylint: disable=too-many-arguments def __init__(self, light_id, info, bridge, update_lights, bridge_type, allow_unreachable): """Initialize the light.""" diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index ed0d55b92f4..51feceb2691 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -80,7 +80,6 @@ class LIFX(object): break return bulb - # pylint: disable=too-many-arguments def on_device(self, ipaddr, name, power, hue, sat, bri, kel): """Initialize the light.""" bulb = self.find_bulb(ipaddr) @@ -99,7 +98,6 @@ class LIFX(object): bulb.set_color(hue, sat, bri, kel) bulb.update_ha_state() - # pylint: disable=too-many-arguments def on_color(self, ipaddr, hue, sat, bri, kel): """Initialize the light.""" bulb = self.find_bulb(ipaddr) @@ -137,11 +135,9 @@ def convert_rgb_to_hsv(rgb): int(brightness * SHORT_MAX)] -# pylint: disable=too-many-instance-attributes class LIFXLight(Light): """Representation of a LIFX light.""" - # pylint: disable=too-many-arguments def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness, kelvin): """Initialize the light.""" diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index e0464533a1b..424d0a5451c 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -101,8 +101,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MqttLight(Light): """MQTT light.""" - # pylint: disable=too-many-arguments,too-many-instance-attributes - # pylint: disable=too-many-locals,too-many-branches def __init__(self, hass, name, topic, templates, qos, retain, payload, optimistic, brightness_scale): """Initialize MQTT light.""" diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 01fd03955fd..6f1d4e13e7b 100755 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -85,7 +85,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MqttJson(Light): """Representation of a MQTT JSON light.""" - # pylint: disable=too-many-arguments,too-many-instance-attributes def __init__(self, hass, name, topic, qos, retain, optimistic, brightness, rgb, flash_times): """Initialize MQTT JSON light.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 76610451808..d117b66df79 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -78,7 +78,6 @@ class WinkLight(WinkDevice, Light): """Flag supported features.""" return SUPPORT_WINK - # pylint: disable=too-few-public-methods def turn_on(self, **kwargs): """Turn the switch on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 394006f3ab2..d8aa138af47 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class YeelightLight(Light): """Representation of a Yeelight light.""" - # pylint: disable=too-many-arguments def __init__(self, device): """Initialize the light.""" import pyyeelight diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 7e838f97270..fe965efd107 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -105,7 +105,6 @@ def brightness_state(value): class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): """Representation of a Z-Wave dimmer.""" - # pylint: disable=too-many-arguments def __init__(self, value): """Initialize the light.""" from openzwave.network import ZWaveNetwork @@ -263,7 +262,6 @@ class ZwaveColorLight(ZwaveDimmer): # Check for the missing color values self._get_color_values() - # pylint: disable=too-many-branches def update_properties(self): """Update internal properties based on zwave values.""" super().update_properties() diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 28b2d1f05c7..da6e595914b 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -58,7 +58,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttLock(LockDevice): """Represents a lock that can be toggled using MQTT.""" diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 9d9936bd474..18e80c4c761 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -154,7 +154,6 @@ class LogbookView(HomeAssistantView): class Entry(object): """A human readable version of the log.""" - # pylint: disable=too-many-arguments, too-few-public-methods def __init__(self, when=None, name=None, message=None, domain=None, entity_id=None): """Initialize the entry.""" @@ -182,7 +181,6 @@ def humanify(events): - if 2+ sensor updates in GROUP_BY_MINUTES, show last - if home assistant stop and start happen in same minute call it restarted """ - # pylint: disable=too-many-branches # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( events, @@ -288,7 +286,6 @@ def humanify(events): def _exclude_events(events, config): """Get lists of excluded entities and platforms.""" - # pylint: disable=too-many-branches excluded_entities = [] excluded_domains = [] included_entities = [] @@ -355,10 +352,10 @@ def _exclude_events(events, config): return filtered_events +# pylint: disable=too-many-return-statements def _entry_message_from_state(domain, state): """Convert a state to a message for the logbook.""" # We pass domain in so we don't have to split entity_id again - # pylint: disable=too-many-return-statements if domain == 'device_tracker': if state.state == STATE_NOT_HOME: return 'is away' diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 838202fdcab..cd135803ad9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -398,10 +398,8 @@ def setup(hass, config): class MediaPlayerDevice(Entity): """ABC for media player devices.""" - # pylint: disable=too-many-public-methods,no-self-use - + # pylint: disable=no-self-use # Implement these for your media player - @property def state(self): """State of the player.""" diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 1550c487433..ee23a707d0c 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -117,7 +117,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): setup_bravia(config, pin, hass, add_devices) -# pylint: disable=too-many-branches def setup_bravia(config, pin, hass, add_devices): """Setup a Sony Bravia TV based on host parameter.""" host = config.get(CONF_HOST) @@ -181,8 +180,7 @@ def request_configuration(config, hass, add_devices): ) -# pylint: disable=abstract-method, too-many-public-methods, -# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=abstract-method class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 831f9857e4b..9ee259a54ab 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -89,7 +89,6 @@ class CastDevice(MediaPlayerDevice): """Representation of a Cast device on the network.""" # pylint: disable=abstract-method - # pylint: disable=too-many-public-methods def __init__(self, chromecast): """Initialize the Cast device.""" self.cast = chromecast diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index dde2e1d28e6..dc623a274b5 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): class CmusDevice(MediaPlayerDevice): """Representation of a running cmus.""" - # pylint: disable=no-member, too-many-public-methods, abstract-method + # pylint: disable=no-member, abstract-method def __init__(self, server, password, port, name): """Initialize the CMUS device.""" from pycmus import remote diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 78df50dde76..e04b9ee3931 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DenonDevice(MediaPlayerDevice): """Representation of a Denon device.""" - # pylint: disable=too-many-public-methods, abstract-method + # pylint: disable=abstract-method def __init__(self, name, host): """Initialize the Denon device.""" self._name = name diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 0a53ffbbed6..f1f22693e6b 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -66,7 +66,6 @@ class DirecTvDevice(MediaPlayerDevice): """Representation of a DirecTV reciever on the network.""" # pylint: disable=abstract-method - # pylint: disable=too-many-public-methods def __init__(self, name, host, port): """Initialize the device.""" from DirectPy import DIRECTV diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 6f7e7cb1e82..db1732f4288 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -170,8 +170,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class GPMDP(MediaPlayerDevice): """Representation of a GPMDP.""" - # pylint: disable=too-many-public-methods, abstract-method - # pylint: disable=too-many-instance-attributes + # pylint: disable=abstract-method def __init__(self, name, url, code): """Initialize the media player.""" from websocket import create_connection diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index b46007a8d17..2ccc95c3243 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -154,7 +154,6 @@ class Itunes(object): # pylint: disable=unused-argument, abstract-method -# pylint: disable=too-many-instance-attributes def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the iTunes platform.""" add_devices([ @@ -172,7 +171,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ItunesDevice(MediaPlayerDevice): """Representation of an iTunes API instance.""" - # pylint: disable=too-many-public-methods, too-many-arguments def __init__(self, name, host, port, use_ssl, add_devices): """Initialize the iTunes device.""" self._name = name @@ -353,7 +351,6 @@ class ItunesDevice(MediaPlayerDevice): class AirPlayDevice(MediaPlayerDevice): """Representation an AirPlay device via an iTunes API instance.""" - # pylint: disable=too-many-public-methods def __init__(self, device_id, client): """Initialize the AirPlay device.""" self._id = device_id diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index b98303a3a63..e88770a22e7 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -63,8 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class KodiDevice(MediaPlayerDevice): """Representation of a XBMC/Kodi device.""" - # pylint: disable=too-many-public-methods, abstract-method - # pylint: disable=too-many-instance-attributes + # pylint: disable=abstract-method def __init__(self, name, url, auth=None, turn_off_action=None): """Initialize the Kodi device.""" import jsonrpc_requests diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 4402d8b93b8..0def17a7dca 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -52,8 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([LgTVDevice(client, config[CONF_NAME])]) -# pylint: disable=too-many-public-methods, abstract-method -# pylint: disable=too-many-instance-attributes +# pylint: disable=abstract-method class LgTVDevice(MediaPlayerDevice): """Representation of a LG TV.""" diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 56af3cd88f9..844be4a7a08 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MpdDevice(MediaPlayerDevice): """Representation of a MPD server.""" - # pylint: disable=no-member, too-many-public-methods, abstract-method + # pylint: disable=no-member, abstract-method def __init__(self, server, port, location, password): """Initialize the MPD device.""" import mpd diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index fd9e6d7427c..44afc0f8ad2 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -65,11 +65,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(hosts) -# pylint: disable=too-many-instance-attributes class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - # pylint: disable=too-many-public-methods, abstract-method + # pylint: disable=abstract-method def __init__(self, receiver, sources, name=None): """Initialize the Onkyo Receiver.""" self._receiver = receiver diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 7ae1eb9d79e..509ba5f49eb 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - # pylint: disable=too-many-public-methods def __init__(self, name, remote): """Initialize the samsung device.""" # Save a reference to the imported class diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index 8628a3125f8..d10b9f685b5 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -60,7 +60,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([pandora]) -# pylint: disable=too-many-instance-attributes class PandoraMediaPlayer(MediaPlayerDevice): """A media player that uses the Pianobar interface to Pandora.""" diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 8930057857d..524c2c4520e 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -54,8 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PioneerDevice(MediaPlayerDevice): """Representation of a Pioneer device.""" - # pylint: disable=too-many-public-methods, abstract-method - # pylint: disable=too-many-instance-attributes + # pylint: disable=abstract-method def __init__(self, name, host, port, timeout): """Initialize the Pioneer device.""" self._name = name diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index aac38180f94..827665929c5 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -83,7 +83,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): setup_plexserver(host, token, hass, add_devices_callback) -# pylint: disable=too-many-branches def setup_plexserver(host, token, hass, add_devices_callback): """Setup a plexserver based on host parameter.""" import plexapi.server @@ -192,7 +191,7 @@ def request_configuration(host, hass, add_devices_callback): class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" - # pylint: disable=too-many-public-methods, attribute-defined-outside-init + # pylint: disable=attribute-defined-outside-init def __init__(self, device, plex_sessions, update_devices, update_sessions): """Initialize the Plex device.""" from plexapi.utils import NA diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index e7a87d2d773..aff49d0a5be 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -66,7 +66,6 @@ class RokuDevice(MediaPlayerDevice): """Representation of a Roku device on the network.""" # pylint: disable=abstract-method - # pylint: disable=too-many-public-methods def __init__(self, host): """Initialize the Roku device.""" from roku import Roku diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index 91aecb57a10..df8e66457c5 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -72,8 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('Not connected to %s:%s', host, port) -# pylint: disable=abstract-method, too-many-public-methods, -# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=abstract-method class RussoundRNETDevice(MediaPlayerDevice): """Representation of a Russound RNET device.""" diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 6c429eff54e..a680b32b9b0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -60,7 +60,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - # pylint: disable=too-many-public-methods def __init__(self, name, config): """Initialize the Samsung device.""" from samsungctl import Remote diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 3bc6778ce39..f8e9f36c1e8 100755 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -64,7 +64,6 @@ SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({ DEVICES = [] -# pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Sonos platform.""" import soco @@ -224,12 +223,10 @@ def only_if_coordinator(func): return wrapper -# pylint: disable=too-many-instance-attributes, too-many-public-methods # pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """Representation of a Sonos device.""" - # pylint: disable=too-many-arguments def __init__(self, hass, player): """Initialize the Sonos device.""" from soco.snapshot import Snapshot diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 30df5a11f99..2e09087f012 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -171,12 +171,10 @@ class LogitechMediaServer(object): return None -# pylint: disable=too-many-instance-attributes -# pylint: disable=too-many-public-methods class SqueezeBoxDevice(MediaPlayerDevice): """Representation of a SqueezeBox device.""" - # pylint: disable=too-many-arguments, abstract-method + # pylint: disable=abstract-method def __init__(self, lms, player_id): """Initialize the SqeezeBox device.""" super(SqueezeBoxDevice, self).__init__() diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index e5b9f0321b1..9923782872a 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -132,10 +132,8 @@ def validate_attributes(config): class UniversalMediaPlayer(MediaPlayerDevice): """Representation of an universal media player.""" - # pylint: disable=too-many-public-methods def __init__(self, hass, name, children, commands, attributes): """Initialize the Universal media device.""" - # pylint: disable=too-many-arguments self.hass = hass self._name = name self._children = children diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 4cc284f5672..bc3133d0564 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -142,11 +142,9 @@ def request_configuration(host, name, customize, hass, add_devices): # pylint: disable=abstract-method -# pylint: disable=too-many-instance-attributes class LgWebOSDevice(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - # pylint: disable=too-many-public-methods def __init__(self, host, name, customize): """Initialize the webos device.""" from pylgtv import WebOsClient diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index b7dfa15cede..08a79e9767f 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yamaha platform.""" import rxv @@ -80,8 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class YamahaDevice(MediaPlayerDevice): """Representation of a Yamaha device.""" - # pylint: disable=too-many-public-methods, abstract-method - # pylint: disable=too-many-instance-attributes + # pylint: disable=abstract-method def __init__(self, name, receiver, source_ignore, source_names): """Initialize the Yamaha Receiver.""" self._receiver = receiver diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 307b287ea0d..4595a3c79c4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -206,7 +206,6 @@ def _setup_server(hass, config): def setup(hass, config): """Start the MQTT protocol service.""" - # pylint: disable=too-many-locals conf = config.get(DOMAIN, {}) client_id = conf.get(CONF_CLIENT_ID) @@ -292,7 +291,6 @@ def setup(hass, config): return True -# pylint: disable=too-many-arguments class MQTT(object): """Home Assistant MQTT client.""" diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index be5a19bf7c0..b86bed57b82 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -69,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): # pylint: disable=too-many-locals +def setup(hass, config): """Setup the MySensors component.""" import mysensors.mysensors as mysensors @@ -79,7 +79,6 @@ def setup(hass, config): # pylint: disable=too-many-locals def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix): """Return gateway after setup of the gateway.""" - # pylint: disable=too-many-arguments if device == MQTT_COMPONENT: if not setup_component(hass, MQTT_COMPONENT, config): return @@ -201,8 +200,6 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class): class GatewayWrapper(object): """Gateway wrapper class.""" - # pylint: disable=too-few-public-methods - def __init__(self, gateway, optimistic, device): """Setup class attributes on instantiation. @@ -256,8 +253,6 @@ class GatewayWrapper(object): class MySensorsDeviceEntity(object): """Represent a MySensors entity.""" - # pylint: disable=too-many-arguments - def __init__( self, gateway, node_id, child_id, name, value_type, child_type): """ diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index b30777a0cf2..fb016e20617 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -64,7 +64,6 @@ def send_message(hass, message, title=None, data=None): hass.services.call(DOMAIN, SERVICE_NOTIFY, info) -# pylint: disable=too-many-locals def setup(hass, config): """Setup the notify services.""" success = False @@ -134,7 +133,6 @@ def setup(hass, config): return success -# pylint: disable=too-few-public-methods class BaseNotificationService(object): """An abstract class for notification services.""" diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 5e5a8088aa7..26d20f3bc89 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -130,8 +130,6 @@ class ApnsDevice(object): class ApnsNotificationService(BaseNotificationService): """Implement the notification service for the APNS service.""" - # pylint: disable=too-many-arguments - # pylint: disable=too-many-instance-attributes def __init__(self, hass, app_name, topic, sandbox, cert_file): """Initialize APNS application.""" self.hass = hass diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index 9ac9c4cbc18..8db48b0000e 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -62,7 +62,6 @@ def get_service(hass, config): return AWSLambda(lambda_client, context) -# pylint: disable=too-few-public-methods class AWSLambda(BaseNotificationService): """Implement the notification service for the AWS Lambda service.""" diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index fb6ae8984c6..f3af26cd8b4 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -54,7 +54,6 @@ def get_service(hass, config): return AWSSNS(sns_client) -# pylint: disable=too-few-public-methods class AWSSNS(BaseNotificationService): """Implement the notification service for the AWS SNS service.""" diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index edd8c4bec69..84826a2f32f 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -53,7 +53,6 @@ def get_service(hass, config): return AWSSQS(sqs_client) -# pylint: disable=too-few-public-methods class AWSSQS(BaseNotificationService): """Implement the notification service for the AWS SQS service.""" diff --git a/homeassistant/components/notify/command_line.py b/homeassistant/components/notify/command_line.py index 9b637d71188..d59994e37ed 100644 --- a/homeassistant/components/notify/command_line.py +++ b/homeassistant/components/notify/command_line.py @@ -29,7 +29,6 @@ def get_service(hass, config): return CommandLineNotificationService(command) -# pylint: disable=too-few-public-methods class CommandLineNotificationService(BaseNotificationService): """Implement the notification service for the Command Line service.""" diff --git a/homeassistant/components/notify/demo.py b/homeassistant/components/notify/demo.py index 65e9c3ef9a0..d3c4f9b8026 100644 --- a/homeassistant/components/notify/demo.py +++ b/homeassistant/components/notify/demo.py @@ -14,7 +14,6 @@ def get_service(hass, config): return DemoNotificationService(hass) -# pylint: disable=too-few-public-methods class DemoNotificationService(BaseNotificationService): """Implement demo notification service.""" diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py index f67e6245e0c..befde9271ca 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/notify/ecobee.py @@ -30,7 +30,6 @@ def get_service(hass, config): return EcobeeNotificationService(index) -# pylint: disable=too-few-public-methods class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py index 82ec2420df8..6b435ace6d4 100644 --- a/homeassistant/components/notify/file.py +++ b/homeassistant/components/notify/file.py @@ -33,7 +33,6 @@ def get_service(hass, config): return FileNotificationService(hass, filename, timestamp) -# pylint: disable=too-few-public-methods class FileNotificationService(BaseNotificationService): """Implement the notification service for the File service.""" diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py index e5209e06582..06126e4fbc2 100644 --- a/homeassistant/components/notify/free_mobile.py +++ b/homeassistant/components/notify/free_mobile.py @@ -29,7 +29,6 @@ def get_service(hass, config): config[CONF_ACCESS_TOKEN]) -# pylint: disable=too-few-public-methods class FreeSMSNotificationService(BaseNotificationService): """Implement a notification service for the Free Mobile SMS service.""" diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py index fa7db0d6e6e..ee6d203a47a 100644 --- a/homeassistant/components/notify/gntp.py +++ b/homeassistant/components/notify/gntp.py @@ -55,11 +55,9 @@ def get_service(hass, config): config.get(CONF_PORT)) -# pylint: disable=too-few-public-methods class GNTPNotificationService(BaseNotificationService): """Implement the notification service for GNTP.""" - # pylint: disable=too-many-arguments def __init__(self, app_name, app_icon, hostname, password, port): """Initialize the service.""" import gntp.notifier diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 0d480a9ddac..9a7d8b69681 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -42,7 +42,6 @@ def get_service(hass, config): return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) -# pylint: disable=too-few-public-methods class GroupNotifyPlatform(BaseNotificationService): """Implement the notification service for the group notify playform.""" diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 4ded65ba3ed..35f59af1135 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -255,7 +255,6 @@ class HTML5PushCallbackView(HomeAssistantView): # The following is based on code from Auth0 # https://auth0.com/docs/quickstart/backend/python - # pylint: disable=too-many-return-statements def check_authorization_header(self, request): """Check the authorization header.""" import jwt @@ -320,11 +319,9 @@ class HTML5PushCallbackView(HomeAssistantView): 'event': event_payload[ATTR_TYPE]}) -# pylint: disable=too-few-public-methods class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" - # pylint: disable=too-many-arguments def __init__(self, gcm_key, registrations): """Initialize the service.""" self._gcm_key = gcm_key @@ -338,7 +335,6 @@ class HTML5NotificationService(BaseNotificationService): targets[registration] = registration return targets - # pylint: disable=too-many-locals def send_message(self, message="", **kwargs): """Send a message to a user.""" import jwt diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py index ef06fe87b24..d5f32d66a5e 100644 --- a/homeassistant/components/notify/instapush.py +++ b/homeassistant/components/notify/instapush.py @@ -60,7 +60,6 @@ def get_service(hass, config): config.get(CONF_EVENT), config.get(CONF_TRACKER)) -# pylint: disable=too-few-public-methods class InstapushNotificationService(BaseNotificationService): """Implementation of the notification service for Instapush.""" diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 940804ab49c..8dc4c7d9701 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -55,7 +55,6 @@ def get_service(hass, config): return iOSNotificationService() -# pylint: disable=too-few-public-methods, too-many-arguments, invalid-name class iOSNotificationService(BaseNotificationService): """Implement the notification service for iOS.""" diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py index 1478c2330ed..6f0afddcca2 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/notify/joaoapps_join.py @@ -39,7 +39,6 @@ def get_service(hass, config): return JoinNotificationService(device_id, api_key) -# pylint: disable=too-few-public-methods class JoinNotificationService(BaseNotificationService): """Implement the notification service for Join.""" diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 6db4d9cde04..6f725d63d47 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -41,7 +41,6 @@ def get_service(hass, config): ) -# pylint: disable=too-few-public-methods class KODINotificationService(BaseNotificationService): """Implement the notification service for Kodi.""" diff --git a/homeassistant/components/notify/llamalab_automate.py b/homeassistant/components/notify/llamalab_automate.py index 7a00b5ba237..e7b6ab80455 100644 --- a/homeassistant/components/notify/llamalab_automate.py +++ b/homeassistant/components/notify/llamalab_automate.py @@ -35,7 +35,6 @@ def get_service(hass, config): return AutomateNotificationService(secret, recipient, device) -# pylint: disable=too-few-public-methods class AutomateNotificationService(BaseNotificationService): """Implement the notification service for LlamaLab Automate.""" diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py index 566bd1a4652..b7ce54f8838 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/notify/matrix.py @@ -48,11 +48,9 @@ def get_service(hass, config): ) -# pylint: disable=too-few-public-methods class MatrixNotificationService(BaseNotificationService): """Wrapper for the MatrixNotificationClient.""" - # pylint: disable=too-many-arguments def __init__(self, homeserver, default_room, verify_ssl, username, password): """Buffer configuration data for send_message.""" @@ -94,7 +92,6 @@ def store_token(mx_id, token): handle.write(json.dumps(AUTH_TOKENS)) -# pylint: disable=too-many-locals, too-many-arguments def send_message(message, homeserver, target_rooms, verify_tls, username, password): """Do everything thats necessary to send a message to a Matrix room.""" diff --git a/homeassistant/components/notify/message_bird.py b/homeassistant/components/notify/message_bird.py index 11106024111..6d1d50d8a1a 100644 --- a/homeassistant/components/notify/message_bird.py +++ b/homeassistant/components/notify/message_bird.py @@ -40,7 +40,6 @@ def get_service(hass, config): return MessageBirdNotificationService(config.get(CONF_SENDER), client) -# pylint: disable=too-few-public-methods class MessageBirdNotificationService(BaseNotificationService): """Implement the notification service for MessageBird.""" diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 9874733d4ef..2ba9e7c72be 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -98,11 +98,9 @@ def get_service(hass, config): remoteip, duration, position, transparency, color, interrupt, timeout) -# pylint: disable=too-many-instance-attributes class NFAndroidTVNotificationService(BaseNotificationService): """Notification service for Notifications for Android TV.""" - # pylint: disable=too-many-arguments,too-few-public-methods def __init__(self, remoteip, duration, position, transparency, color, interrupt, timeout): """Initialize the service.""" @@ -117,7 +115,6 @@ class NFAndroidTVNotificationService(BaseNotificationService): os.path.dirname(__file__), '..', 'frontend', 'www_static', 'icons', 'favicon-192x192.png') - # pylint: disable=too-many-branches def send_message(self, message="", **kwargs): """Send a message to a Android TV device.""" _LOGGER.debug("Sending notification to: %s", self._target) diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index ffa4ae229c7..a21a37bb323 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -37,7 +37,6 @@ def get_service(hass, config): return NmaNotificationService(config[CONF_API_KEY]) -# pylint: disable=too-few-public-methods class NmaNotificationService(BaseNotificationService): """Implement the notification service for NMA.""" diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 3fe6492525b..ec9c7ec4f54 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -41,7 +41,6 @@ def get_service(hass, config): return PushBulletNotificationService(pushbullet) -# pylint: disable=too-few-public-methods, too-many-branches class PushBulletNotificationService(BaseNotificationService): """Implement the notification service for Pushbullet.""" diff --git a/homeassistant/components/notify/pushetta.py b/homeassistant/components/notify/pushetta.py index 15a750e5ad8..b786fb5ba98 100644 --- a/homeassistant/components/notify/pushetta.py +++ b/homeassistant/components/notify/pushetta.py @@ -37,7 +37,6 @@ def get_service(hass, config): return pushetta_service -# pylint: disable=too-few-public-methods class PushettaNotificationService(BaseNotificationService): """Implement the notification service for Pushetta.""" diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 04aa7627963..c77e5f7b85e 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -40,7 +40,6 @@ def get_service(hass, config): return None -# pylint: disable=too-few-public-methods class PushoverNotificationService(BaseNotificationService): """Implement the notification service for Pushover.""" diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py index a7d31593ce3..20dbb4afaa1 100644 --- a/homeassistant/components/notify/rest.py +++ b/homeassistant/components/notify/rest.py @@ -52,7 +52,6 @@ def get_service(hass, config): target_param_name) -# pylint: disable=too-few-public-methods, too-many-arguments class RestNotificationService(BaseNotificationService): """Implementation of a notification service for REST.""" diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index c8afe601ae5..c771240f80a 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -34,7 +34,6 @@ def get_service(hass, config): return SendgridNotificationService(api_key, sender, recipient) -# pylint: disable=too-few-public-methods class SendgridNotificationService(BaseNotificationService): """Implementation the notification service for email via Sendgrid.""" diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py index af75ffeadd3..b3c2686f3aa 100644 --- a/homeassistant/components/notify/simplepush.py +++ b/homeassistant/components/notify/simplepush.py @@ -30,7 +30,6 @@ def get_service(hass, config): return SimplePushNotificationService(config.get(CONF_DEVICE_KEY)) -# pylint: disable=too-few-public-methods class SimplePushNotificationService(BaseNotificationService): """Implementation of the notification service for SimplePush.""" diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 8dedee2a127..7ced616c9d2 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -45,7 +45,6 @@ def get_service(hass, config): return None -# pylint: disable=too-few-public-methods class SlackNotificationService(BaseNotificationService): """Implement the notification service for Slack.""" diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 84aae3f2c8f..3171509b008 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -63,11 +63,9 @@ def get_service(hass, config): return None -# pylint: disable=too-few-public-methods, too-many-instance-attributes class MailNotificationService(BaseNotificationService): """Implement the notification service for E-Mail messages.""" - # pylint: disable=too-many-arguments def __init__(self, server, port, sender, starttls, username, password, recipient, debug): """Initialize the service.""" diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 792ed2ad631..4065b47f480 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -78,11 +78,9 @@ def get_service(hass, config): return SyslogNotificationService(facility, option, priority) -# pylint: disable=too-few-public-methods class SyslogNotificationService(BaseNotificationService): """Implement the syslog notification service.""" - # pylint: disable=too-many-arguments def __init__(self, facility, option, priority): """Initialize the service.""" self._facility = facility diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index a5f0adcbfc2..11719f8758a 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -77,7 +77,6 @@ def load_data(url=None, file=None, username=None, password=None): return None -# pylint: disable=too-few-public-methods class TelegramNotificationService(BaseNotificationService): """Implement the notification service for Telegram.""" diff --git a/homeassistant/components/notify/telstra.py b/homeassistant/components/notify/telstra.py index 2fd554a278c..ca727db9711 100644 --- a/homeassistant/components/notify/telstra.py +++ b/homeassistant/components/notify/telstra.py @@ -41,7 +41,6 @@ def get_service(hass, config): consumer_key, consumer_secret, phone_number) -# pylint: disable=too-few-public-methods, too-many-arguments class TelstraNotificationService(BaseNotificationService): """Implementation of a notification service for the Telstra SMS API.""" diff --git a/homeassistant/components/notify/twilio_sms.py b/homeassistant/components/notify/twilio_sms.py index d7e78ac7d1a..3438ce92ee3 100644 --- a/homeassistant/components/notify/twilio_sms.py +++ b/homeassistant/components/notify/twilio_sms.py @@ -40,7 +40,6 @@ def get_service(hass, config): config[CONF_FROM_NUMBER]) -# pylint: disable=too-few-public-methods class TwilioSMSNotificationService(BaseNotificationService): """Implement the notification service for the Twilio SMS service.""" diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 9a438df41da..666133c4c57 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -37,7 +37,6 @@ def get_service(hass, config): ) -# pylint: disable=too-few-public-methods class TwitterNotificationService(BaseNotificationService): """Implementation of a notification service for the Twitter service.""" diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 8a9dee1e49d..9b6514559ca 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -43,7 +43,6 @@ def get_service(hass, config): return LgWebOSNotificationService(client) -# pylint: disable=too-few-public-methods class LgWebOSNotificationService(BaseNotificationService): """Implement the notification service for LG WebOS TV.""" diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index ed46060a410..5873e3997fa 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -39,7 +39,6 @@ def get_service(hass, config): config.get('tls')) -# pylint: disable=too-few-public-methods class XmppNotificationService(BaseNotificationService): """Implement the notification service for Jabber (XMPP).""" diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py index e3d8f0238cf..756ae1cf223 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -51,7 +51,7 @@ def setup(hass, config): return True -class NuimoLogger(object): # pylint: disable=too-few-public-methods +class NuimoLogger(object): """Handle Nuimo Controller event callbacks.""" def __init__(self, hass, name): @@ -94,7 +94,8 @@ class NuimoThread(threading.Thread): self._nuimo.disconnect() self._nuimo = None - def stop(self, event): # pylint: disable=unused-argument + # pylint: disable=unused-argument + def stop(self, event): """Terminate Thread by unsetting flag.""" _LOGGER.debug('Stopping thread for Nuimo %s', self._mac) self._hass_is_running = False @@ -169,14 +170,17 @@ HOMEASSIST_LOGO = ( class DiscoveryLogger(object): """Handle Nuimo Discovery callbacks.""" - def discovery_started(self): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def discovery_started(self): """Discovery startet.""" _LOGGER.info("started discovery") - def discovery_finished(self): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def discovery_finished(self): """Discovery finished.""" _LOGGER.info("finished discovery") - def controller_added(self, nuimo): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def controller_added(self, nuimo): """Controller found.""" _LOGGER.info("added Nuimo: %s", nuimo) diff --git a/homeassistant/components/openalpr.py b/homeassistant/components/openalpr.py index 700437cedf1..27a573b1dbf 100644 --- a/homeassistant/components/openalpr.py +++ b/homeassistant/components/openalpr.py @@ -117,7 +117,6 @@ def restart(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_RESTART, data) -# pylint: disable=too-many-locals def setup(hass, config): """Setup the OpenAlpr component.""" engine = config[DOMAIN].get(CONF_ENGINE) @@ -291,7 +290,6 @@ class OpenalprDevice(Entity): class OpenalprDeviceFFmpeg(OpenalprDevice): """Represent a openalpr device object for processing stream/images.""" - # pylint: disable=too-many-arguments def __init__(self, name, interval, api, input_source, extra_arguments=None): """Init image processing.""" @@ -362,7 +360,6 @@ class OpenalprDeviceFFmpeg(OpenalprDevice): class OpenalprDeviceImage(OpenalprDevice): """Represent a openalpr device object for processing stream/images.""" - # pylint: disable=too-many-arguments def __init__(self, name, interval, api, input_source, username=None, password=None): """Init image processing.""" @@ -403,7 +400,6 @@ class OpenalprDeviceImage(OpenalprDevice): self._next = time() + self._interval -# pylint: disable=too-few-public-methods class OpenalprApi(object): """OpenAlpr api class.""" @@ -417,7 +413,6 @@ class OpenalprApi(object): raise NotImplementedError() -# pylint: disable=too-few-public-methods class OpenalprApiCloud(OpenalprApi): """Use the cloud openalpr api to parse licences plate.""" diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py index fceec21dd5d..60a0a8c547b 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity.py @@ -87,11 +87,9 @@ def setup(hass, config): return True -# pylint: disable=too-many-instance-attributes class Proximity(Entity): """Representation of a Proximity.""" - # pylint: disable=too-many-arguments def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel, nearest, ignored_zones, proximity_devices, tolerance, proximity_zone, unit_of_measurement): @@ -130,7 +128,6 @@ class Proximity(Entity): ATTR_NEAREST: self.nearest, } - # pylint: disable=too-many-branches,too-many-statements,too-many-locals def check_proximity_state_change(self, entity, old_state, new_state): """Function to perform the proximity checking.""" entity_name = new_state.name diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 0519ac6f40d..bb10d72b45b 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -117,7 +117,6 @@ class QSLight(QSToggleEntity, Light): return SUPPORT_QWIKSWITCH -# pylint: disable=too-many-locals def setup(hass, config): """Setup the QSUSB component.""" from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3ce05e8f72b..858ed2c1cf3 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -153,7 +153,6 @@ def log_error(e: Exception, retry_wait: Optional[float]=0, class Recorder(threading.Thread): """A threaded recorder class.""" - # pylint: disable=too-many-instance-attributes def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) -> None: """Initialize the recorder.""" threading.Thread.__init__(self) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 671623ec564..3b7b5aca1cb 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -20,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) class Events(Base): # type: ignore - # pylint: disable=too-few-public-methods """Event history data.""" __tablename__ = 'events' @@ -55,7 +54,6 @@ class Events(Base): # type: ignore class States(Base): # type: ignore - # pylint: disable=too-few-public-methods """State change history.""" __tablename__ = 'states' @@ -115,7 +113,6 @@ class States(Base): # type: ignore class RecorderRuns(Base): # type: ignore - # pylint: disable=too-few-public-methods """Representation of recorder run.""" __tablename__ = 'recorder_runs' diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index f891c8e0ec0..d026002e408 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -279,7 +279,6 @@ class RfxtrxDevice(Entity): """Represents a Rfxtrx device. Contains the common logic for Rfxtrx lights and switches. - """ def __init__(self, name, event, datas, signal_repetitions): @@ -327,7 +326,6 @@ class RfxtrxDevice(Entity): self.update_ha_state() def _send_command(self, command, brightness=0): - # pylint: disable=too-many-return-statements,too-many-branches if not self._event: return diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 21511eea1b9..bc66e562e0a 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -122,7 +122,6 @@ def setup(hass, config): class ScriptEntity(ToggleEntity): """Representation of a script entity.""" - # pylint: disable=too-many-instance-attributes def __init__(self, hass, object_id, name, sequence): """Initialize the script.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index cad9a5fc416..30caa80bc53 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the aREST sensor.""" resource = config.get(CONF_RESOURCE) @@ -108,7 +107,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes, too-many-arguments class ArestSensor(Entity): """Implementation of an aREST sensor for exposed variables.""" @@ -163,7 +161,6 @@ class ArestSensor(Entity): return self.arest.available -# pylint: disable=too-few-public-methods class ArestData(object): """The Class for handling the data retrieval for variables.""" diff --git a/homeassistant/components/sensor/bbox.py b/homeassistant/components/sensor/bbox.py index b29e2ebb9ec..a5c01a48d0d 100644 --- a/homeassistant/components/sensor/bbox.py +++ b/homeassistant/components/sensor/bbox.py @@ -48,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-arguments def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Bbox sensor.""" # Create a data fetcher to support all of the configured sensors. Then make @@ -128,7 +127,6 @@ class BboxSensor(Entity): 2) -# pylint: disable=too-few-public-methods class BboxData(object): """Get data from the Bbox.""" diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index c67b0d9e94b..48f3c66a4c1 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -78,7 +78,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods class BitcoinSensor(Entity): """Representation of a Bitcoin sensor.""" @@ -119,7 +118,6 @@ class BitcoinSensor(Entity): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } - # pylint: disable=too-many-branches def update(self): """Get the latest data and updates the states.""" self.data.update() diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index a49ac48ba6f..3b6beab0510 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -145,7 +145,6 @@ class BOMCurrentSensor(Entity): self.rest.update() -# pylint: disable=too-few-public-methods class BOMCurrentData(object): """Get data from BOM.""" diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 05e69c2e3d6..61545fa3944 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -59,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([CoinMarketCapSensor(CoinMarketCapData(currency))]) -# pylint: disable=too-few-public-methods class CoinMarketCapSensor(Entity): """Representation of a CoinMarketCap sensor.""" @@ -104,7 +103,6 @@ class CoinMarketCapSensor(Entity): ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } - # pylint: disable=too-many-branches def update(self): """Get the latest data and updates the states.""" self.data.update() diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index 7409ae1de26..e3e361c1ae2 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -46,7 +46,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([CommandSensor(hass, data, name, unit, value_template)]) -# pylint: disable=too-many-arguments class CommandSensor(Entity): """Representation of a sensor that is using shell commands.""" @@ -89,7 +88,6 @@ class CommandSensor(Entity): self._state = value -# pylint: disable=too-few-public-methods class CommandSensorData(object): """The class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index 71d49cafc46..f605dda3099 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -101,11 +101,9 @@ class CurrencylayerSensor(Entity): value['{}{}'.format(self._base, self._quote)], 4) -# pylint: disable=too-few-public-methods class CurrencylayerData(object): """Get data from Currencylayer.org.""" - # pylint: disable=too-many-arguments def __init__(self, resource, parameters): """Initialize the data object.""" self._resource = resource diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index f092959ba1d..5b0631e2830 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -90,7 +90,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-arguments def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Dark Sky sensor.""" # Validate the configuration @@ -128,7 +127,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors) -# pylint: disable=too-few-public-methods class DarkSkySensor(Entity): """Implementation of a Dark Sky sensor.""" @@ -186,7 +184,6 @@ class DarkSkySensor(Entity): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } - # pylint: disable=too-many-branches,too-many-statements def update(self): """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this @@ -259,7 +256,6 @@ def convert_to_camel(data): class DarkSkyData(object): """Get the latest data from Darksky.""" - # pylint: disable=too-many-instance-attributes def __init__(self, api_key, latitude, longitude, units, interval): """Initialize the data object.""" self._api_key = api_key diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index 17c14bb5df1..e2fd3575e05 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([DeutscheBahnSensor(start, destination)]) -# pylint: disable=too-few-public-methods class DeutscheBahnSensor(Entity): """Implementation of a Deutsche Bahn sensor.""" @@ -81,7 +80,6 @@ class DeutscheBahnSensor(Entity): self._state += " + {}".format(self.data.connections[0]['delay']) -# pylint: disable=too-few-public-methods class SchieneData(object): """Pull data from the bahn.de web page.""" diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 461c2fb1eeb..0e10199134c 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -80,7 +80,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods class DHTSensor(Entity): """Implementation of the DHT sensor.""" diff --git a/homeassistant/components/sensor/dte_energy_bridge.py b/homeassistant/components/sensor/dte_energy_bridge.py index 90b484f46dc..4a57bddfb9d 100644 --- a/homeassistant/components/sensor/dte_energy_bridge.py +++ b/homeassistant/components/sensor/dte_energy_bridge.py @@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([DteEnergyBridgeSensor(ip_address, name)]) -# pylint: disable=too-many-instance-attributes class DteEnergyBridgeSensor(Entity): """Implementation of a DTE Energy Bridge sensor.""" diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index d794d3bad95..0f9ea017571 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -60,7 +60,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([DweetSensor(hass, dweet, name, value_template, unit)]) -# pylint: disable=too-many-arguments class DweetSensor(Entity): """Representation of a Dweet sensor.""" @@ -100,7 +99,6 @@ class DweetSensor(Entity): self.dweet.update() -# pylint: disable=too-few-public-methods class DweetData(object): """The class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py index 3a1bcfbf5a4..1fe5e7217de 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes class EfergySensor(Entity): """Implementation of an Efergy sensor.""" - # pylint: disable=too-many-arguments def __init__(self, sensor_type, app_token, utc_offset, period, currency): """Initialize the sensor.""" self._name = SENSOR_TYPES[sensor_type][0] diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index ac00946a27c..8178d0cc46f 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -61,7 +61,6 @@ def get_id(sensorid, feedtag, feedname, feedid, feeduserid): sensorid, feedtag, feedname, feedid, feeduserid) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Emoncms sensor.""" apikey = config.get(CONF_API_KEY) @@ -106,11 +105,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors) -# pylint: disable=too-many-instance-attributes class EmonCmsSensor(Entity): """Implementation of an Emoncms sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, data, name, value_template, unit_of_measurement, sensorid, elem): """Initialize the sensor.""" @@ -188,7 +185,6 @@ class EmonCmsSensor(Entity): self._state = round(float(elem["value"]), DECIMALS) -# pylint: disable=too-few-public-methods class EmonCmsData(object): """The class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index ad6aa2ca630..0390ea0e9d6 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -49,7 +49,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, 'update_fastdotcom', update) -# pylint: disable=too-few-public-methods class SpeedtestSensor(Entity): """Implementation of a FAst.com sensor.""" diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 2c73bb764fb..cd0346c2469 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -213,8 +213,6 @@ def request_oauth_completion(hass): submit_caption="I have authorized Fitbit." ) -# pylint: disable=too-many-locals - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Fitbit sensor.""" @@ -348,7 +346,6 @@ class FitbitAuthCallbackView(HomeAssistantView): return html_response -# pylint: disable=too-few-public-methods class FitbitSensor(Entity): """Implementation of a Fitbit sensor.""" @@ -400,7 +397,6 @@ class FitbitSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - # pylint: disable=too-many-branches @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Fitbit API and update the states.""" diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index a9f53811c59..b30b660516d 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -58,7 +58,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ExchangeRateSensor(data, name, target)]) -# pylint: disable=too-few-public-methods class ExchangeRateSensor(Entity): """Representation of a Exchange sensor.""" diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index 82f6ae839fb..a8b125ae54b 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=too-few-public-methods class FritzBoxCallSensor(Entity): """Implementation of a Fritz!Box call monitor.""" @@ -95,7 +94,6 @@ class FritzBoxCallSensor(Entity): return self._attributes -# pylint: disable=too-few-public-methods class FritzBoxCallMonitor(object): """Event listener to monitor calls on the Fritz!Box.""" diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 22071fb518f..30af601f63b 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -104,7 +104,7 @@ class GlancesSensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement - # pylint: disable=too-many-branches, too-many-return-statements + # pylint: disable=too-many-return-statements @property def state(self): """Return the state of the resources.""" diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 98cfb469faa..847ce5e6fd8 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -82,7 +82,6 @@ def convert_time_to_utc(timestr): def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the Google travel time platform.""" - # pylint: disable=too-many-locals def run_setup(event): """Delay the setup until Home Assistant is fully initialized. @@ -120,11 +119,9 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) -# pylint: disable=too-many-instance-attributes class GoogleTravelTimeSensor(Entity): """Representation of a Google travel time sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, api_key, origin, destination, options): """Initialize the sensor.""" self._hass = hass diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 5fcf46832db..e76b8ed07ed 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def get_next_departure(sched, start_station_id, end_station_id): """Get the next departure for the given schedule.""" origin_station = sched.stops_by_id(start_station_id)[0] @@ -176,7 +175,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([GTFSDepartureSensor(gtfs, name, origin, destination)]) -# pylint: disable=too-many-instance-attributes,too-few-public-methods class GTFSDepartureSensor(Entity): """Implementation of an GTFS departures sensor.""" diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py index bc7afe1bf3c..d71e8f5ad6e 100644 --- a/homeassistant/components/sensor/hp_ilo.py +++ b/homeassistant/components/sensor/hp_ilo.py @@ -141,7 +141,6 @@ class HpIloSensor(Entity): self._state = ilo_data -# pylint: disable=too-few-public-methods class HpIloData(object): """Gets the latest data from HP ILO.""" diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 47a85cd582f..69fc2eb88a7 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -47,7 +47,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ImapSensor(Entity): """Representation of an IMAP sensor.""" - # pylint: disable=too-many-arguments def __init__(self, name, user, password, server, port): """Initialize the sensor.""" self._name = name or user diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 5af26c072b9..b5845ab78ed 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -130,8 +130,6 @@ class EmailReader: class EmailContentSensor(Entity): """Representation of an EMail sensor.""" - # pylint: disable=too-many-arguments - # pylint: disable=too-many-instance-attributes def __init__(self, hass, email_reader, diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 1dbbd502571..24b8ae591f1 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -156,7 +156,6 @@ class InfluxSensor(Entity): self._state = value -# pylint: disable=too-few-public-methods class InfluxSensorData(object): """Class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index cebd1397366..007291f5fb1 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -84,7 +84,7 @@ def update_and_define_min_max(config, minimum_default, return minimum_value, maximum_value -class KNXSensorBaseClass(): # pylint: disable=too-few-public-methods +class KNXSensorBaseClass(): """Sensor Base Class for all KNX Sensors.""" _unit_of_measurement = None @@ -107,7 +107,6 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass): Defined in KNX 3.7.2 - 3.10 """ - # pylint: disable=too-many-arguments def __init__(self, hass, config, unit_of_measurement, minimum_sensor_value, maximum_sensor_value): """Initialize a KNX Float Sensor.""" diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 2e493399d5b..038e8389d47 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LastfmSensor(Entity): """A class for the Last.fm account.""" - # pylint: disable=abstract-method, too-many-instance-attributes + # pylint: disable=abstract-method def __init__(self, user, lastfm): """Initialize the sensor.""" self._user = lastfm.get_user(user) diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index c1d145953e3..348b7d1bef6 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([LinuxBatterySensor(name, battery_id)]) -# pylint: disable=too-few-public-methods class LinuxBatterySensor(Entity): """Representation of a Linux Battery sensor.""" diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index f636b039c4e..c7217044c26 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -90,11 +90,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors) -# pylint: disable=too-many-instance-attributes class LoopEnergyDevice(Entity): """Implementation of an Loop Energy base sensor.""" - # pylint: disable=too-many-arguments def __init__(self, controller): """Initialize the sensor.""" self._state = None @@ -126,11 +124,9 @@ class LoopEnergyDevice(Entity): self.update_ha_state(True) -# pylint: disable=too-many-instance-attributes class LoopEnergyElec(LoopEnergyDevice): """Implementation of an Loop Energy Electricity sensor.""" - # pylint: disable=too-many-arguments def __init__(self, controller): """Initialize the sensor.""" super(LoopEnergyElec, self).__init__(controller) @@ -142,11 +138,9 @@ class LoopEnergyElec(LoopEnergyDevice): self._state = round(self._controller.electricity_useage, 2) -# pylint: disable=too-many-instance-attributes class LoopEnergyGas(LoopEnergyDevice): """Implementation of an Loop Energy Gas sensor.""" - # pylint: disable=too-many-arguments def __init__(self, controller): """Initialize the sensor.""" super(LoopEnergyGas, self).__init__(controller) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 9cf80c81fd3..e917162d095 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -87,7 +87,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MiFloraSensor(Entity): """Implementing the MiFlora sensor.""" - # pylint: disable=too-many-instance-attributes,too-many-arguments def __init__(self, poller, parameter, name, unit, force_update, median): """Initialize the sensor.""" self.poller = poller diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 19698ebf868..9fd8cad4c38 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -63,7 +63,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes class MinMaxSensor(Entity): """Representation of a min/max sensor.""" diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 44ee73ddfa0..5b30f52d926 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -58,7 +58,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ModbusRegisterSensor(Entity): """Modbus resgister sensor.""" - # pylint: disable=too-many-instance-attributes, too-many-arguments def __init__(self, name, slave, register, unit_of_measurement, count, scale, offset, precision): """Initialize the modbus register sensor.""" diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 6ee5c465265..0457d8a9fa2 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -55,11 +55,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): indoor_humidity_sensor, calib_factor)]) -# pylint: disable=too-many-instance-attributes class MoldIndicator(Entity): """Represents a MoldIndication sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor, indoor_humidity_sensor, calib_factor): """Initialize the sensor.""" diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index c3cc9e3003f..ae9eeeafb32 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -42,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSensor(Entity): """Representation of a sensor that can be updated using MQTT.""" diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index ba01c50cdd6..4156f668093 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MQTTRoomSensor(Entity): """Representation of a room sensor that is updated via MQTT.""" diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 2d321752483..c3503207d8b 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -89,7 +89,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods class NetAtmoSensor(Entity): """Implementation of a Netatmo sensor.""" @@ -134,9 +133,6 @@ class NetAtmoSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - # pylint: disable=too-many-branches - # Fix for pylint too many statements error - # pylint: disable=too-many-statements def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() diff --git a/homeassistant/components/sensor/neurio_energy.py b/homeassistant/components/sensor/neurio_energy.py index c8a53f819e8..2315615ca54 100644 --- a/homeassistant/components/sensor/neurio_energy.py +++ b/homeassistant/components/sensor/neurio_energy.py @@ -52,11 +52,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([NeurioEnergy(api_key, api_secret, name, sensor_id)]) -# pylint: disable=too-many-instance-attributes class NeurioEnergy(Entity): """Implementation of an Neurio energy.""" - # pylint: disable=too-many-arguments def __init__(self, api_key, api_secret, name, sensor_id): """Initialize the sensor.""" self._name = name diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py index f6b4fed1ac1..f007ea034fc 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument, too-many-locals +# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the NZBGet sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index 3b55200e647..cdbe46cc9ec 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -71,11 +71,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=too-many-instance-attributes class OctoPrintSensor(Entity): """Representation of an OctoPrint sensor.""" - # pylint: disable=too-many-arguments def __init__(self, api, condition, sensor_type, sensor_name, unit, endpoint, group, tool=None): """Initialize a new OctoPrint sensor.""" diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index d0e3ffb2cdd..63c57048b1d 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -93,11 +93,9 @@ class OpenexchangeratesSensor(Entity): self._state = round(value[str(self._quote)], 4) -# pylint: disable=too-few-public-methods class OpenexchangeratesData(object): """Get data from Openexchangerates.org.""" - # pylint: disable=too-many-arguments def __init__(self, resource, parameters, quote): """Initialize the data object.""" self._resource = resource diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index b59bfa7dab5..42e076d61c8 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -84,7 +84,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods class OpenWeatherMapSensor(Entity): """Implementation of an OpenWeatherMap sensor.""" @@ -121,7 +120,6 @@ class OpenWeatherMapSensor(Entity): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } - # pylint: disable=too-many-branches def update(self): """Get the latest data from OWM and updates the states.""" self.owa_client.update() diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 99caebd708c..0266862d529 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class PilightSensor(Entity): """Representation of a sensor that can be updated using pilight.""" diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 7370841acfe..33da15ac836 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -55,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - # pylint: disable=too-many-arguments def __init__(self, name, plex_url, plex_user, plex_password, plex_server): """Initialize the sensor.""" from plexapi.utils import NA diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index cb856ae992a..bc731660676 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the RESTful sensor.""" name = config.get(CONF_NAME) @@ -74,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([RestSensor(hass, rest, name, unit, value_template)]) -# pylint: disable=too-many-arguments class RestSensor(Entity): """Implementation of a REST sensor.""" @@ -117,7 +115,6 @@ class RestSensor(Entity): self._state = value -# pylint: disable=too-few-public-methods class RestData(object): """Class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 60afd80997d..663fd8899d3 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -29,7 +29,6 @@ PLATFORM_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the RFXtrx platform.""" - # pylint: disable=too-many-locals from RFXtrx import SensorEvent sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 0f33a39bbcc..d27f7945e55 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -50,7 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the SABnzbd sensors.""" from pysabnzbd import SabnzbdApi, SabnzbdApiException diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 4789703d051..082c6a1fcfd 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Web scrape sensor.""" name = config.get(CONF_NAME) @@ -61,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-instance-attributes class ScrapeSensor(Entity): """Representation of a web scrape sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, rest, name, select, value_template, unit): """Initialize a web scrape sensor.""" self.rest = rest diff --git a/homeassistant/components/sensor/sleepiq.py b/homeassistant/components/sensor/sleepiq.py index 067c7eefb6a..ff6fb945e83 100644 --- a/homeassistant/components/sensor/sleepiq.py +++ b/homeassistant/components/sensor/sleepiq.py @@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods, too-many-instance-attributes class SleepNumberSensor(sleepiq.SleepIQSensor): """Implementation of a SleepIQ sensor.""" diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index f9f059c8f55..b5e3b16b5d1 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -40,7 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the SNMP sensor.""" from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine, @@ -104,7 +103,6 @@ class SnmpSensor(Entity): class SnmpData(object): """Get the latest data and update the states.""" - # pylint: disable=too-few-public-methods def __init__(self, host, port, community, baseoid): """Initialize the data object.""" self._host = host diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 9cf7bfdd208..814e5a1bfa0 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, 'update_speedtest', update) -# pylint: disable=too-few-public-methods class SpeedtestSensor(Entity): """Implementation of a speedtest.net sensor.""" diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 10614fb9c93..e5672f3a510 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -55,7 +55,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes class StatisticsSensor(Entity): """Representation of a Statistics sensor.""" diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index c8e0be68062..c25d400efcd 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -71,7 +71,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([SwissHydrologicalDataSensor(name, data)]) -# pylint: disable=too-few-public-methods class SwissHydrologicalDataSensor(Entity): """Implementation of an Swiss hydrological sensor.""" @@ -138,7 +137,6 @@ class SwissHydrologicalDataSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - # pylint: disable=too-many-branches def update(self): """Get the latest data and update the states.""" self.data.update() @@ -149,7 +147,6 @@ class SwissHydrologicalDataSensor(Entity): self._state = self.data.measurings['03']['current'] -# pylint: disable=too-few-public-methods class HydrologicalData(object): """The Class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 823a96cc01f..a730e5d16cf 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -65,7 +65,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([SwissPublicTransportSensor(data, journey, name)]) -# pylint: disable=too-few-public-methods class SwissPublicTransportSensor(Entity): """Implementation of an Swiss public transport sensor.""" @@ -106,7 +105,6 @@ class SwissPublicTransportSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - # pylint: disable=too-many-branches def update(self): """Get the latest data from opendata.ch and update the states.""" self.data.update() @@ -117,7 +115,6 @@ class SwissPublicTransportSensor(Entity): pass -# pylint: disable=too-few-public-methods class PublicTransportData(object): """The Class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index ba5fa3efdbf..864a3352f8b 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -94,7 +94,6 @@ class SystemMonitorSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - # pylint: disable=too-many-branches def update(self): """Get the latest system information.""" import psutil diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 5376b89199a..e8a3dcf302b 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -89,7 +89,6 @@ class Ted5000Sensor(Entity): self._gateway.update() -# pylint: disable=too-few-public-methods class Ted5000Gateway(object): """The class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index a34d7615447..4262eb3d55f 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -68,7 +68,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class SensorTemplate(Entity): """Representation of a Template Sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, device_id, friendly_name, unit_of_measurement, state_template, entity_ids): """Initialize the sensor.""" diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index a6c61959734..79127c60063 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -50,7 +50,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return True -# pylint: disable=too-few-public-methods class TimeDateSensor(Entity): """Implementation of a Time and Date sensor.""" diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index c1cb0cd98ca..fd08e0bc511 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -69,7 +69,6 @@ class TorqueReceiveDataView(HomeAssistantView): 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) diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index 5a3f931d76b..08617e824ec 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -64,7 +64,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-few-public-methods class UberSensor(Entity): """Implementation of an Uber sensor.""" @@ -154,7 +153,6 @@ class UberSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - # pylint: disable=too-many-branches def update(self): """Get the latest data from the Uber API and update the states.""" self.data.update() @@ -171,11 +169,9 @@ class UberSensor(Entity): self._state = 0 -# pylint: disable=too-few-public-methods class UberEstimate(object): """The class for handling the time and price estimate.""" - # pylint: disable=too-many-arguments def __init__(self, session, start_latitude, start_longitude, end_latitude=None, end_longitude=None): """Initialize the UberEstimate object.""" diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index de14b1813e0..8c7d4274495 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -62,7 +62,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - # pylint: disable=too-many-arguments def __init__(self, planner, name, departure, heading, delay): """Initialize the sensor.""" self._planner = planner diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 98a06c7545a..93401dfe263 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -165,7 +165,6 @@ class WUndergroundSensor(Entity): self.rest.update() -# pylint: disable=too-few-public-methods class WUndergroundData(object): """Get data from WUnderground.""" diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py index b9dac2948c6..010812b58de 100644 --- a/homeassistant/components/sensor/xbox_live.py +++ b/homeassistant/components/sensor/xbox_live.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False -# pylint: disable=too-many-instance-attributes class XboxSensor(Entity): """A class for the Xbox account.""" diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index c1f09284882..f4278a46d44 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -48,7 +48,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([YahooFinanceSensor(name, data, symbol)]) -# pylint: disable=too-few-public-methods class YahooFinanceSensor(Entity): """Representation of a Yahoo Finance sensor.""" diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index d73a016003c..6fe6b429990 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -75,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes class YrSensor(Entity): """Representation of an Yr.no sensor.""" @@ -164,7 +163,6 @@ class YrSensor(Entity): break -# pylint: disable=too-few-public-methods class YrData(object): """Get the latest data and updates the states.""" diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index f59913facb8..b45da4121bb 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -97,7 +97,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes class YahooWeatherSensor(Entity): """Implementation of an Yahoo! weather sensor.""" @@ -179,7 +178,6 @@ class YahooWeatherSensor(Entity): self._state = self._data.yahoo.Atmosphere["visibility"] -# pylint: disable=too-few-public-methods class YahooWeatherData(object): """Handle yahoo api object and limit updates.""" diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq.py index 8d660264ea6..7016cd72492 100644 --- a/homeassistant/components/sleepiq.py +++ b/homeassistant/components/sleepiq.py @@ -75,7 +75,6 @@ def setup(hass, config): return True -# pylint: disable=too-few-public-methods class SleepIQData(object): """Gets the latest data from SleepIQ.""" @@ -95,7 +94,6 @@ class SleepIQData(object): self.beds = {bed.bed_id: bed for bed in beds} -# pylint: disable=too-few-public-methods, too-many-instance-attributes class SleepIQSensor(Entity): """Implementation of a SleepIQ sensor.""" diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 8cea062ca47..ff3eaf387ab 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -110,7 +110,6 @@ class PwrCtrlSwitch(SwitchDevice): self._port.off() -# pylint: disable=too-few-public-methods class PwrCtrlDevice(object): """Device representation for per device throttling.""" diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index d6bef02cdac..e2954f90945 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(switches) -# pylint: disable=too-many-instance-attributes class CommandSwitch(SwitchDevice): """Representation a switch that can be toggled using shell commands.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, command_on, command_off, command_state, value_template): """Initialize the switch.""" diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index d0814010d1e..bd226ac087a 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -102,11 +102,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.services.register(DOMAIN, name + '_update', update) -# pylint: disable=too-many-instance-attributes class FluxSwitch(SwitchDevice): """Representation of a Flux switch.""" - # pylint: disable=too-many-arguments def __init__(self, name, hass, state, lights, start_time, stop_time, start_colortemp, sunset_colortemp, stop_colortemp, brightness, mode): @@ -150,7 +148,6 @@ class FluxSwitch(SwitchDevice): self._state = False self.update_ha_state() - # pylint: disable=too-many-locals def flux_update(self, now=None): """Update all the lights using flux.""" if now is None: diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 5a911ee3d74..220011b2fb0 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -33,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=too-many-arguments -# pylint: disable=too-many-instance-attributes def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Hikvision camera.""" import hikvision.api diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 18247b452d2..93406c869d4 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ModbusCoilSwitch(ToggleEntity): """Representation of a Modbus switch.""" - # pylint: disable=too-many-arguments def __init__(self, name, slave, coil): """Initialize the switch.""" self._name = name diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 27e33838021..2283b8539ba 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSwitch(SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 47e040ddb67..c143ae5c887 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class PilightSwitch(SwitchDevice): """Representation of a pilight switch.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, name, code_on, code_off, code_on_receive, code_off_receive): """Initialize the switch.""" diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index c9ee19aa0e3..250111ecfb5 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -141,7 +141,6 @@ class PAServer(): return -1 -# pylint: disable=too-many-arguments class PALoopbackSwitch(SwitchDevice): """Representation the presence or absence of a PA loopback module.""" diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 9d5ea639704..056bcef0281 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -65,13 +65,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, name, resource, body_on, body_off, is_on_template, timeout)]) -# pylint: disable=too-many-arguments class RestSwitch(SwitchDevice): """Representation of a switch that can be toggled using REST.""" - # pylint: disable=too-many-instance-attributes - def __init__(self, hass, name, resource, body_on, body_off, is_on_template, - timeout): + def __init__(self, hass, name, resource, body_on, body_off, + is_on_template, timeout): """Initialize the REST switch.""" self._state = None self._hass = hass diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 61a9fdb0333..2822f2fc9d4 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RPiRFSwitch(SwitchDevice): """Representation of a GPIO RF switch.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, name, rfdevice, protocol, pulselength, code_on, code_off): """Initialize the switch.""" diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 83c82ae6ad1..5383caf7f54 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -77,7 +77,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class SwitchTemplate(SwitchDevice): """Representation of a Template switch.""" - # pylint: disable=too-many-arguments def __init__(self, hass, device_id, friendly_name, state_template, on_action, off_action, entity_ids): """Initialize the Template switch.""" diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index c78c4461f88..6dcea6c9354 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -93,9 +93,9 @@ def setup(hass, base_config): return True +# pylint: disable=too-many-return-statements def map_vera_device(vera_device, remap): """Map vera classes to HA types.""" - # pylint: disable=too-many-return-statements import pyvera as veraApi if isinstance(vera_device, veraApi.VeraDimmer): return 'light' diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 0c760d899c8..c8241d8fae5 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -63,7 +63,6 @@ def setup(hass, config): return True -# pylint: disable=too-many-instance-attributes class VerisureHub(object): """A Verisure hub wrapper class.""" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index dcb5dc49233..50173840657 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -36,7 +36,7 @@ def setup(hass, config): return True -# pylint: disable=no-member, no-self-use, too-many-return-statements +# pylint: disable=no-member, no-self-use class WeatherEntity(Entity): """ABC for a weather data.""" diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index dec4dcf2450..f7617bd0075 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -33,7 +33,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-arguments class DemoWeather(WeatherEntity): """Representation of a weather condition.""" diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 4133509de89..f8f7e2f3747 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -70,7 +70,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name, data, hass.config.units.temperature_unit)]) -# pylint: disable=too-few-public-methods class OpenWeatherMapWeather(WeatherEntity): """Implementation of an OpenWeatherMap sensor.""" @@ -131,7 +130,6 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the attribution.""" return ATTRIBUTION - # pylint: disable=too-many-branches def update(self): """Get the latest data from OWM and updates the states.""" self._owm.update() diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index a7c98dcd91c..2514dfc0083 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -117,7 +117,6 @@ def async_setup(hass, config): class Zone(Entity): """Representation of a Zone.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, name, latitude, longitude, radius, icon, passive): """Initialize the zone.""" self.hass = hass diff --git a/homeassistant/config.py b/homeassistant/config.py index bde0f648354..d56027e20f4 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -260,7 +260,6 @@ def async_process_ha_core_config(hass, config): This method is a coroutine. """ - # pylint: disable=too-many-branches config = CORE_CONFIG_SCHEMA(config) hac = hass.config diff --git a/homeassistant/core.py b/homeassistant/core.py index d8720f26cd5..4d61b33eb65 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -128,8 +128,6 @@ class JobPriority(util.OrderedEnum): class HomeAssistant(object): """Root object of the Home Assistant home automation.""" - # pylint: disable=too-many-instance-attributes - def __init__(self, loop=None): """Initialize new Home Assistant object.""" self.loop = loop or asyncio.get_event_loop() @@ -364,7 +362,6 @@ class EventOrigin(enum.Enum): class Event(object): - # pylint: disable=too-few-public-methods """Represents an event within the Bus.""" __slots__ = ['event_type', 'data', 'origin', 'time_fired'] @@ -581,7 +578,6 @@ class State(object): __slots__ = ['entity_id', 'state', 'attributes', 'last_changed', 'last_updated'] - # pylint: disable=too-many-arguments def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): """Initialize a new state.""" @@ -824,7 +820,6 @@ class StateMachine(object): self._bus.async_fire(EVENT_STATE_CHANGED, event_data) -# pylint: disable=too-few-public-methods class Service(object): """Represents a callable service.""" @@ -848,7 +843,6 @@ class Service(object): } -# pylint: disable=too-few-public-methods class ServiceCall(object): """Represents a call to a service.""" @@ -906,7 +900,6 @@ class ServiceRegistry(object): """ return service.lower() in self._services.get(domain.lower(), []) - # pylint: disable=too-many-arguments def register(self, domain, service, service_func, description=None, schema=None): """ @@ -1091,7 +1084,6 @@ class ServiceRegistry(object): class Config(object): """Configuration settings for Home Assistant.""" - # pylint: disable=too-many-instance-attributes def __init__(self): """Initialize a new config object.""" self.latitude = None # type: Optional[float] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a6db4f9150d..781ef37dc9d 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -131,7 +131,6 @@ def async_or_from_config(config: ConfigType, config_validation: bool=True): or_from_config = _threaded_factory(async_or_from_config) -# pylint: disable=too-many-arguments def numeric_state(hass: HomeAssistant, entity, below=None, above=None, value_template=None, variables=None): """Test a numeric state condition.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 7740f32e4b2..44d0b8891d5 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -23,8 +23,6 @@ DEFAULT_SCAN_INTERVAL = 15 class EntityComponent(object): """Helper class that will help a component manage its entities.""" - # pylint: disable=too-many-instance-attributes - # pylint: disable=too-many-arguments def __init__(self, logger, domain, hass, scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None): """Initialize an entity component.""" @@ -274,7 +272,6 @@ class EntityComponent(object): class EntityPlatform(object): """Keep track of entities for a single platform and stay in loop.""" - # pylint: disable=too-few-public-methods def __init__(self, component, scan_interval, entity_namespace): """Initalize the entity platform.""" self.component = component diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 390af3c7ad1..dd00cfee30e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -203,7 +203,6 @@ def async_track_sunset(hass, action, offset=None): track_sunset = threaded_listener_factory(async_track_sunset) -# pylint: disable=too-many-arguments def async_track_utc_time_change(hass, action, year=None, month=None, day=None, hour=None, minute=None, second=None, local=False): @@ -248,7 +247,6 @@ def async_track_utc_time_change(hass, action, year=None, month=None, day=None, track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) -# pylint: disable=too-many-arguments def async_track_time_change(hass, action, year=None, month=None, day=None, hour=None, minute=None, second=None): """Add a listener that will fire if UTC time matches a pattern.""" diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index aed90599a75..90a85628e59 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -46,7 +46,6 @@ def track_sunset(offset=None): return track_sunset_decorator -# pylint: disable=too-many-arguments def track_time_change(year=None, month=None, day=None, hour=None, minute=None, second=None): """Decorator factory to track time changes.""" @@ -60,7 +59,6 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, return track_time_change_decorator -# pylint: disable=too-many-arguments def track_utc_time_change(year=None, month=None, day=None, hour=None, minute=None, second=None): """Decorator factory to track time changes.""" diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index f6a2f482fc1..09e9e15a5dc 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -36,7 +36,6 @@ def call_from_config(hass: HomeAssistant, config: ConfigType, class Script(): """Representation of a script.""" - # pylint: disable=too-many-instance-attributes def __init__(self, hass: HomeAssistant, sequence, name: str=None, change_listener=None) -> None: """Initialize the script.""" diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 21c35332797..10364eff815 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -77,7 +77,6 @@ SERVICE_TO_STATE = { } -# pylint: disable=too-few-public-methods, attribute-defined-outside-init class AsyncTrackStates(object): """ Record the time when the with-block is entered. @@ -93,6 +92,7 @@ class AsyncTrackStates(object): self.hass = hass self.states = [] + # pylint: disable=attribute-defined-outside-init def __enter__(self): """Record time from which to track changes.""" self.now = dt_util.utcnow() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2a72fc1a088..105260475e4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,5 +1,4 @@ """Template helper methods for rendering strings with HA data.""" -# pylint: disable=too-few-public-methods import json import logging import re diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 42afa91170c..ad616de5544 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -40,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) class APIStatus(enum.Enum): """Represent API status.""" - # pylint: disable=no-init,invalid-name,too-few-public-methods + # pylint: disable=no-init, invalid-name OK = "ok" INVALID_PASSWORD = "invalid_password" CANNOT_CONNECT = "cannot_connect" @@ -54,7 +54,6 @@ class APIStatus(enum.Enum): class API(object): """Object to pass around Home Assistant API location and credentials.""" - # pylint: disable=too-few-public-methods def __init__(self, host: str, api_password: Optional[str]=None, port: Optional[int]=None, use_ssl: bool=False) -> None: """Initalize the API.""" @@ -114,7 +113,7 @@ class API(object): class HomeAssistant(ha.HomeAssistant): """Home Assistant that forwards work.""" - # pylint: disable=super-init-not-called,too-many-instance-attributes + # pylint: disable=super-init-not-called def __init__(self, remote_api, local_api=None, loop=None): """Initalize the forward instance.""" if not remote_api.validate_api(): @@ -150,7 +149,8 @@ class HomeAssistant(ha.HomeAssistant): 'Unable to setup local API to receive events') self.state = ha.CoreState.starting - ha._async_create_timer(self) # pylint: disable=protected-access + # pylint: disable=protected-access + ha._async_create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) @@ -186,7 +186,6 @@ class HomeAssistant(ha.HomeAssistant): class EventBus(ha.EventBus): """EventBus implementation that forwards fire_event to remote API.""" - # pylint: disable=too-few-public-methods def __init__(self, api, hass): """Initalize the eventbus.""" super().__init__(hass) @@ -300,7 +299,7 @@ class StateMachine(ha.StateMachine): class JSONEncoder(json.JSONEncoder): """JSONEncoder that supports Home Assistant objects.""" - # pylint: disable=too-few-public-methods,method-hidden + # pylint: disable=method-hidden def default(self, obj): """Convert Home Assistant objects. diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 736498cf935..ace1b4efe83 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -56,7 +56,6 @@ def color(the_color, *args, reset=None): raise ValueError("Invalid color {} in {}".format(str(k), the_color)) -# pylint: disable=too-many-locals, too-many-branches def run(script_args: List) -> int: """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( @@ -160,12 +159,14 @@ def check(config_path): 'secret_cache': OrderedDict(), } - def mock_load(filename): # pylint: disable=unused-variable + # pylint: disable=unused-variable + def mock_load(filename): """Mock hass.util.load_yaml to save config files.""" res['yaml_files'][filename] = True return MOCKS['load'][1](filename) - def mock_get(comp_name): # pylint: disable=unused-variable + # pylint: disable=unused-variable + def mock_get(comp_name): """Mock hass.loader.get_component to replace setup & setup_platform.""" def mock_setup(*kwargs): """Mock setup, only record the component name & config.""" @@ -196,7 +197,8 @@ def check(config_path): return module - def mock_secrets(ldr, node): # pylint: disable=unused-variable + # pylint: disable=unused-variable + def mock_secrets(ldr, node): """Mock _get_secrets.""" try: val = MOCKS['secrets'][1](ldr, node) diff --git a/homeassistant/scripts/db_migrator.py b/homeassistant/scripts/db_migrator.py index 7e48bb3a1d7..ee3ee253b65 100644 --- a/homeassistant/scripts/db_migrator.py +++ b/homeassistant/scripts/db_migrator.py @@ -23,7 +23,6 @@ def ts_to_dt(timestamp: Optional[float]) -> Optional[datetime]: # Based on code at # http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console -# pylint: disable=too-many-arguments def print_progress(iteration: int, total: int, prefix: str='', suffix: str='', decimals: int=2, bar_length: int=68) -> None: """Print progress bar. @@ -49,7 +48,7 @@ def print_progress(iteration: int, total: int, prefix: str='', suffix: str='', def run(script_args: List) -> int: """The actual script body.""" - # pylint: disable=too-many-locals,invalid-name,too-many-statements + # pylint: disable=invalid-name from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from homeassistant.components.recorder import models diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index c331dabcab2..1f5a285a117 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -193,7 +193,8 @@ class OrderedSet(MutableSet): yield curr[0] curr = curr[1] - def pop(self, last=True): # pylint: disable=arguments-differ + # pylint: disable=arguments-differ + def pop(self, last=True): """Pop element of the end of the set. Set last=False to pop from the beginning. @@ -240,7 +241,6 @@ class Throttle(object): Adds a datetime attribute `last_call` to the method. """ - # pylint: disable=too-few-public-methods def __init__(self, min_time, limit_no_throttle=None): """Initialize the throttle.""" self.min_time = min_time @@ -307,7 +307,6 @@ class Throttle(object): class ThreadPool(object): """A priority queue-based thread pool.""" - # pylint: disable=too-many-instance-attributes def __init__(self, job_handler, worker_count=0): """Initialize the pool. @@ -416,7 +415,6 @@ class ThreadPool(object): class PriorityQueueItem(object): """Holds a priority and a value. Used within PriorityQueue.""" - # pylint: disable=too-few-public-methods def __init__(self, priority, item): """Initialize the queue.""" self.priority = priority diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 828281aa897..52e85081599 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -169,7 +169,6 @@ def parse_time(time_str): # Found in this gist: https://gist.github.com/zhangsen/1199964 def get_age(date: dt.datetime) -> str: - # pylint: disable=too-many-return-statements """ Take a datetime and return its "age" as a string. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 1fb1c22c2cd..a053056dc81 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -80,7 +80,7 @@ def elevation(latitude, longitude): # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE -# pylint: disable=too-many-locals, invalid-name, unused-variable +# pylint: disable=invalid-name, unused-variable def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], miles: bool=False) -> Optional[float]: """ diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index a83a2b9a2ba..ae3630c27a9 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -64,7 +64,6 @@ def is_valid_unit(unit: str, unit_type: str) -> bool: class UnitSystem(object): """A container for units of measure.""" - # pylint: disable=too-many-arguments def __init__(self: object, name: str, temperature: str, length: str, volume: str, mass: str) -> None: """Initialize the unit system object.""" diff --git a/pylintrc b/pylintrc index 768cd3d46ff..627524fc240 100644 --- a/pylintrc +++ b/pylintrc @@ -5,11 +5,14 @@ reports=no # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load -# abstract-class-little-used - Prevents from setting right foundation +# abstract-class-little-used - prevents from setting right foundation # abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation # redefined-variable-type - this is Python, we're duck typing! +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* + disable= locally-disabled, duplicate-code, @@ -18,7 +21,14 @@ disable= abstract-class-not-used, unused-argument, global-statement, - redefined-variable-type + redefined-variable-type, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-locals, + too-many-public-methods, + too-many-statements, + too-few-public-methods, [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError diff --git a/tests/common.py b/tests/common.py index 9ee4dda5bfe..275beb6be94 100644 --- a/tests/common.py +++ b/tests/common.py @@ -223,7 +223,7 @@ def mock_mqtt_component(hass): class MockModule(object): """Representation of a fake module.""" - # pylint: disable=invalid-name,too-few-public-methods,too-many-arguments + # pylint: disable=invalid-name def __init__(self, domain=None, dependencies=None, setup=None, requirements=None, config_schema=None, platform_schema=None): """Initialize the mock module.""" @@ -248,7 +248,7 @@ class MockModule(object): class MockPlatform(object): """Provide a fake platform.""" - # pylint: disable=invalid-name,too-few-public-methods + # pylint: disable=invalid-name def __init__(self, setup_platform=None, dependencies=None, platform_schema=None): """Initialize the platform.""" diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index cb2da4c3f98..42778244d7a 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,5 @@ """The tests for the device tracker component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import logging import unittest from unittest.mock import call, patch @@ -30,12 +30,14 @@ class TestComponentsDeviceTracker(unittest.TestCase): hass = None # HomeAssistant yaml_devices = None # type: str - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.yaml_devices = self.hass.config.path(device_tracker.YAML_DEVICES) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" try: os.remove(self.yaml_devices) @@ -56,7 +58,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertFalse(device_tracker.is_on(self.hass, entity_id)) - def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def test_reading_broken_yaml_config(self): """Test when known devices contains invalid data.""" files = {'empty.yaml': '', 'nodict.yaml': '100', @@ -101,9 +104,9 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) + # pylint: disable=invalid-name @patch('homeassistant.components.device_tracker._LOGGER.warning') - def test_track_with_duplicate_mac_dev_id(self, mock_warning): \ - # pylint: disable=invalid-name + def test_track_with_duplicate_mac_dev_id(self, mock_warning): """Test adding duplicate MACs or device IDs to DeviceTracker.""" devices = [ device_tracker.Device(self.hass, True, True, 'my_device', 'AB:01', @@ -138,8 +141,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) - def test_adding_unknown_device_to_config(self): \ - # pylint: disable=invalid-name + # pylint: disable=invalid-name + def test_adding_unknown_device_to_config(self): """Test the adding of unknown devices to configuration file.""" scanner = get_component('device_tracker.test').SCANNER scanner.reset() @@ -303,8 +306,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(mock_see.call_count, 1) self.assertEqual(mock_see.call_args, call(**params)) - def test_not_write_duplicate_yaml_keys(self): \ - # pylint: disable=invalid-name + # pylint: disable=invalid-name + def test_not_write_duplicate_yaml_keys(self): """Test that the device tracker will not generate invalid YAML.""" self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) @@ -318,7 +321,8 @@ class TestComponentsDeviceTracker(unittest.TestCase): timedelta(seconds=0)) assert len(config) == 2 - def test_not_allow_invalid_dev_id(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def test_not_allow_invalid_dev_id(self): """Test that the device tracker will not allow invalid dev ids.""" self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 5cc857070e8..a724b5d26d5 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -26,9 +26,10 @@ def _url(data=None): return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Initalize a Home Assistant server.""" - global hass # pylint: disable=invalid-name + global hass hass = get_test_home_assistant() bootstrap.setup_component(hass, http.DOMAIN, { diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 2a269a65212..85529c6ed96 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -242,12 +242,10 @@ class BaseMQTT(unittest.TestCase): self.assertEqual(state.attributes.get('gps_accuracy'), accuracy) -# pylint: disable=too-many-public-methods class TestDeviceTrackerOwnTracks(BaseMQTT): """Test the OwnTrack sensor.""" # pylint: disable=invalid-name - def setup_method(self, _): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index effca08a20e..abb7cc2ac12 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -1,5 +1,5 @@ """The tests for the demo light component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from homeassistant.bootstrap import setup_component @@ -13,14 +13,16 @@ ENTITY_LIGHT = 'light.bed_light' class TestDemoClimate(unittest.TestCase): """Test the demo climate hvac.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.assertTrue(setup_component(self.hass, light.DOMAIN, {'light': { 'platform': 'demo', }})) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down everything that was started.""" self.hass.stop() diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 04139e88feb..60e5b4d9ec2 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,5 +1,5 @@ """The tests for the Light component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest import os @@ -16,11 +16,13 @@ from tests.common import mock_service, get_test_home_assistant class TestLight(unittest.TestCase): """Test the light module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 3fd4ab9929d..582f5f8eb1c 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,5 +1,5 @@ """The tests for the Cast Media player platform.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from unittest.mock import patch diff --git a/tests/components/sensor/test_pilight.py b/tests/components/sensor/test_pilight.py index c78c91545ab..08e174df4cc 100644 --- a/tests/components/sensor/test_pilight.py +++ b/tests/components/sensor/test_pilight.py @@ -17,7 +17,8 @@ def fire_pilight_message(protocol, data): HASS.bus.fire(pilight.EVENT, message) -def setup_function(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setup_function(): """Initialize a Home Assistant server.""" global HASS @@ -25,7 +26,8 @@ def setup_function(): # pylint: disable=invalid-name HASS.config.components = ['pilight'] -def teardown_function(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def teardown_function(): """Stop the Home Assistant server.""" HASS.stop() diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index f7f2e958ef7..27df73d098c 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -31,7 +31,6 @@ ALERT_MESSAGE = 'This is a test alert message' def mocked_requests_get(*args, **kwargs): """Mock requests.get invocations.""" - # pylint: disable=too-few-public-methods class MockResponse: """Class to represent a mocked response.""" diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 4909ae20112..464bc21dd4e 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -1,5 +1,5 @@ """The tests for the Switch component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from homeassistant.bootstrap import setup_component @@ -13,7 +13,8 @@ from tests.common import get_test_home_assistant class TestSwitch(unittest.TestCase): """Test the switch module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() platform = loader.get_component('switch.test') @@ -22,7 +23,8 @@ class TestSwitch(unittest.TestCase): self.switch_1, self.switch_2, self.switch_3 = \ platform.DEVICES - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 31d5d6eec5c..e5ae13d91c6 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,5 +1,5 @@ """The tests for the Alexa component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import json import time import datetime @@ -36,7 +36,8 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" STATIC_TIME = datetime.datetime.utcfromtimestamp(1476129102) -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Initialize a Home Assistant server for testing this module.""" global hass @@ -117,7 +118,8 @@ def setUpModule(): # pylint: disable=invalid-name time.sleep(0.05) -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Stop the Home Assistant server.""" hass.stop() diff --git a/tests/components/test_api.py b/tests/components/test_api.py index ee00c42b8cc..f14fc84ae63 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,5 +1,5 @@ """The tests for the Home Assistant API component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import asyncio from contextlib import closing import json @@ -32,7 +32,8 @@ def _url(path=""): return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Initialize a Home Assistant server.""" global hass @@ -52,7 +53,8 @@ def setUpModule(): # pylint: disable=invalid-name time.sleep(0.05) -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Stop the Home Assistant server.""" hass.stop() diff --git a/tests/components/test_configurator.py b/tests/components/test_configurator.py index 72c0de358ce..66466656835 100644 --- a/tests/components/test_configurator.py +++ b/tests/components/test_configurator.py @@ -1,5 +1,5 @@ """The tests for the Configurator component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest import homeassistant.components.configurator as configurator @@ -11,11 +11,13 @@ from tests.common import get_test_home_assistant class TestConfigurator(unittest.TestCase): """Test the Configurator component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 6235fafc495..454b088dc5a 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -1,5 +1,5 @@ """The tests for the Conversation component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from unittest.mock import patch @@ -15,7 +15,8 @@ from tests.common import get_test_home_assistant, assert_setup_component class TestConversation(unittest.TestCase): """Test the conversation component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.ent_id = 'light.kitchen_lights' self.hass = get_test_home_assistant(3) @@ -28,7 +29,8 @@ class TestConversation(unittest.TestCase): conversation.DOMAIN: {} })) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 20de44f6f29..c42b50ef390 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -1,5 +1,5 @@ """The tests device sun light trigger component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import os import unittest @@ -19,7 +19,8 @@ KNOWN_DEV_YAML_PATH = os.path.join(get_test_config_dir(), device_tracker.YAML_DEVICES) -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Write a device tracker known devices file to be used.""" device_tracker.update_config( KNOWN_DEV_YAML_PATH, 'device_1', device_tracker.Device( @@ -32,7 +33,8 @@ def setUpModule(): # pylint: disable=invalid-name picture='http://example.com/dev2.jpg')) -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Remove device tracker known devices file.""" os.remove(KNOWN_DEV_YAML_PATH) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 765b3e3f35c..46686a5c66f 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,5 +1,5 @@ """The tests for Home Assistant frontend.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import re import time import unittest @@ -25,7 +25,8 @@ def _url(path=""): return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Initialize a Home Assistant server.""" global hass @@ -45,7 +46,8 @@ def setUpModule(): # pylint: disable=invalid-name time.sleep(0.05) -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Stop everything that was started.""" hass.stop() frontend.PANELS = {} diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 5e8f7f38ae8..9b6d96d898f 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -1,5 +1,5 @@ """The tests for the Group components.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access from collections import OrderedDict import unittest from unittest.mock import patch @@ -16,11 +16,13 @@ from tests.common import get_test_home_assistant class TestComponentsGroup(unittest.TestCase): """Test Group component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 520afed81d9..a79f56b0829 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -1,5 +1,5 @@ """The tests the History component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access from datetime import timedelta import unittest from unittest.mock import patch, sentinel @@ -16,11 +16,13 @@ from tests.common import ( class TestComponentHistory(unittest.TestCase): """Test History component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 5ef26d5d5ab..565809a8cc3 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,5 +1,5 @@ """The tests for the Home Assistant HTTP component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import logging import time from ipaddress import ip_network @@ -64,7 +64,8 @@ def setUpModule(): time.sleep(0.05) -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Stop the Home Assistant server.""" hass.stop() diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 0bc105e3ad1..833319646a2 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,5 +1,5 @@ """The testd for Core components.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import asyncio import unittest from unittest.mock import patch, Mock @@ -21,7 +21,8 @@ from tests.common import ( class TestComponentsCore(unittest.TestCase): """Test homeassistant.components module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.assertTrue(run_coroutine_threadsafe( @@ -31,7 +32,8 @@ class TestComponentsCore(unittest.TestCase): self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py index cf5a33fc8ad..1e261ccbcc8 100644 --- a/tests/components/test_input_boolean.py +++ b/tests/components/test_input_boolean.py @@ -1,5 +1,5 @@ """The tests for the input_boolean component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest import logging @@ -17,11 +17,13 @@ _LOGGER = logging.getLogger(__name__) class TestInputBoolean(unittest.TestCase): """Test the input boolean module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 8231390410e..04ab4ceed58 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -1,5 +1,5 @@ """The tests for the Input select component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from tests.common import get_test_home_assistant @@ -14,11 +14,13 @@ from homeassistant.const import ( class TestInputSelect(unittest.TestCase): """Test the input select component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_input_slider.py b/tests/components/test_input_slider.py index 85c6a6f08ca..b927ec48a25 100644 --- a/tests/components/test_input_slider.py +++ b/tests/components/test_input_slider.py @@ -1,5 +1,5 @@ """The tests for the Input slider component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from tests.common import get_test_home_assistant @@ -11,11 +11,13 @@ from homeassistant.components.input_slider import (DOMAIN, select_value) class TestInputSlider(unittest.TestCase): """Test the input slider component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 2dcc47549df..8ffb2146319 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -1,5 +1,5 @@ """The tests for the logbook component.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access from datetime import timedelta import unittest from unittest.mock import patch diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index 9ec3211f959..95eaf54cd6b 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -1,5 +1,5 @@ """The tests for the Rfxtrx component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest import pytest diff --git a/tests/components/test_script.py b/tests/components/test_script.py index bbdcac22593..de13d43fe82 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from homeassistant.bootstrap import setup_component @@ -14,12 +14,14 @@ ENTITY_ID = 'script.test' class TestScriptComponent(unittest.TestCase): """Test the Script component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.components.append('group') - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down everything that was started.""" self.hass.stop() diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 5163c926048..15b79465b8f 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -1,5 +1,5 @@ """The tests for the Sun component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest from unittest.mock import patch from datetime import timedelta, datetime @@ -15,11 +15,13 @@ from tests.common import get_test_home_assistant class TestSun(unittest.TestCase): """Test the sun module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index f63e80ec1f9..b2db8277085 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,5 +1,5 @@ """Test the entity helper.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import asyncio from unittest.mock import MagicMock diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 47e4e6c7d2f..7c6b94963cf 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,5 +1,5 @@ """The tests for the Entity component helper.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access from collections import OrderedDict import logging import unittest diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 89c97434f8d..77518241080 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,6 +1,5 @@ """Test event helpers.""" -# pylint: disable=protected-access,too-many-public-methods -# pylint: disable=too-few-public-methods +# pylint: disable=protected-access import asyncio import unittest from datetime import datetime, timedelta @@ -28,11 +27,13 @@ from tests.common import get_test_home_assistant class TestEventHelpers(unittest.TestCase): """Test the Home Assistant event helpers.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py index ebfd57aa76b..798db1128a5 100644 --- a/tests/helpers/test_event_decorators.py +++ b/tests/helpers/test_event_decorators.py @@ -1,6 +1,5 @@ """Test event decorator helpers.""" -# pylint: disable=protected-access,too-many-public-methods -# pylint: disable=too-few-public-methods +# pylint: disable=protected-access import unittest from datetime import datetime, timedelta @@ -20,7 +19,8 @@ from tests.common import get_test_home_assistant class TestEventDecoratorHelpers(unittest.TestCase): """Test the Home Assistant event helpers.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.states.set("light.Bowl", "on") @@ -28,7 +28,8 @@ class TestEventDecoratorHelpers(unittest.TestCase): event_decorators.HASS = self.hass - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() event_decorators.HASS = None diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 1791e5622a9..f702c1a5dc7 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -1,5 +1,5 @@ """Test component helpers.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access from collections import OrderedDict import unittest @@ -11,11 +11,13 @@ from tests.common import get_test_home_assistant class TestHelpers(unittest.TestCase): """Tests homeassistant.helpers module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Init needed objects.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index ddecf368dc7..068e1a58ac2 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -1,5 +1,4 @@ """Tests Home Assistant location helpers.""" -# pylint: disable=too-many-public-methods import unittest from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 93bf0268337..8744170fc40 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access from datetime import timedelta from unittest import mock import unittest @@ -18,11 +18,13 @@ ENTITY_ID = 'script.test' class TestScriptHelper(unittest.TestCase): """Test the Script component.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down everything that was started.""" self.hass.stop() diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e1e08b02b16..31f90233701 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,5 +1,4 @@ """Test Home Assistant template helper methods.""" -# pylint: disable=too-many-public-methods import unittest from unittest.mock import patch @@ -22,14 +21,16 @@ from tests.common import get_test_home_assistant class TestHelpersTemplate(unittest.TestCase): """Test the Template.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup the tests.""" self.hass = get_test_home_assistant() self.hass.config.units = UnitSystem('custom', TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, MASS_GRAMS) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down stuff we started.""" self.hass.stop() diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 971a90896b7..def2cbb68d4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,5 +1,5 @@ """Test the bootstrapping.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access from unittest import mock import threading import logging @@ -26,7 +26,6 @@ class TestBootstrap: backup_cache = None # pylint: disable=invalid-name, no-self-use - def setup_method(self, method): """Setup the test.""" self.backup_cache = loader._COMPONENT_CACHE diff --git a/tests/test_config.py b/tests/test_config.py index 7537351fb09..ff0498d06af 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,5 @@ """Test config utils.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import os import unittest import unittest.mock as mock @@ -35,11 +35,13 @@ def create_file(path): class TestConfig(unittest.TestCase): """Test the configutils.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Initialize a test Home Assistant instance.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Clean up.""" dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE diff --git a/tests/test_core.py b/tests/test_core.py index b3ab2ba4dbd..3c7cfd32ef7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,5 @@ """Test to verify that Home Assistant core works.""" -# pylint: disable=protected-access,too-many-public-methods -# pylint: disable=too-few-public-methods +# pylint: disable=protected-access import asyncio import unittest from unittest.mock import patch, MagicMock @@ -89,11 +88,13 @@ def test_async_run_job_delegates_non_async(): class TestHomeAssistant(unittest.TestCase): """Test the Home Assistant core classes.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant(0) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() @@ -163,13 +164,15 @@ class TestEvent(unittest.TestCase): class TestEventBus(unittest.TestCase): """Test EventBus methods.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.bus = self.hass.bus self.bus.listen('test_event', lambda x: len) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down stuff we started.""" self.hass.stop() @@ -360,14 +363,16 @@ class TestState(unittest.TestCase): class TestStateMachine(unittest.TestCase): """Test State machine methods.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant(0) self.states = self.hass.states self.states.set("light.Bowl", "on") self.states.set("switch.AC", "off") - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down stuff we started.""" self.hass.stop() @@ -485,13 +490,15 @@ class TestServiceCall(unittest.TestCase): class TestServiceRegistry(unittest.TestCase): """Test ServicerRegistry methods.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.services = self.hass.services self.services.register("Test_Domain", "TEST_SERVICE", lambda x: None) - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop down stuff we started.""" self.hass.stop() @@ -572,7 +579,8 @@ class TestServiceRegistry(unittest.TestCase): class TestConfig(unittest.TestCase): """Test configuration methods.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup things to be run when tests are started.""" self.config = ha.Config() self.assertIsNone(self.config.config_dir) diff --git a/tests/test_loader.py b/tests/test_loader.py index 261dafdd187..93e24b57205 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,5 @@ """Test to verify that we can load components.""" -# pylint: disable=too-many-public-methods,protected-access +# pylint: disable=protected-access import unittest import homeassistant.loader as loader @@ -11,11 +11,13 @@ from tests.common import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): """Test the loader module.""" - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def setUp(self): """Setup tests.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Stop everything that was started.""" self.hass.stop() diff --git a/tests/test_remote.py b/tests/test_remote.py index 316f13c5fc2..df41c2ebd10 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,5 +1,5 @@ """Test Home Assistant remote methods and classes.""" -# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=protected-access import asyncio import threading import time @@ -16,11 +16,11 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_instance_port, get_test_home_assistant, get_test_config_dir) -API_PASSWORD = "test1234" +API_PASSWORD = 'test1234' MASTER_PORT = get_test_instance_port() SLAVE_PORT = get_test_instance_port() BROKEN_PORT = get_test_instance_port() -HTTP_BASE_URL = "http://127.0.0.1:{}".format(MASTER_PORT) +HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(MASTER_PORT) HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} @@ -28,12 +28,13 @@ broken_api = remote.API('127.0.0.1', "bladiebla") hass, slave, master_api = None, None, None -def _url(path=""): +def _url(path=''): """Helper method to generate URLs.""" return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def setUpModule(): """Initalization of a Home Assistant server and Slave instance.""" global hass, slave, master_api @@ -52,13 +53,13 @@ def setUpModule(): # pylint: disable=invalid-name hass.start() time.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 loop = asyncio.new_event_loop() # FIXME: should not be a daemon - threading.Thread(name="SlaveThread", daemon=True, + threading.Thread(name='SlaveThread', daemon=True, target=loop.run_forever).start() slave = remote.HomeAssistant(master_api, loop=loop) @@ -73,7 +74,8 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() -def tearDownModule(): # pylint: disable=invalid-name +# pylint: disable=invalid-name +def tearDownModule(): """Stop the Home Assistant server and slave.""" slave.stop() hass.stop() @@ -94,7 +96,7 @@ class TestRemoteMethods(unittest.TestCase): self.assertEqual( remote.APIStatus.INVALID_PASSWORD, remote.validate_api( - remote.API("127.0.0.1", API_PASSWORD + "A", MASTER_PORT))) + remote.API('127.0.0.1', API_PASSWORD + 'A', MASTER_PORT))) self.assertEqual( remote.APIStatus.CANNOT_CONNECT, remote.validate_api(broken_api)) diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index e8114e93e24..ab2e7dd5244 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -1,5 +1,4 @@ """Test Home Assistant date util methods.""" -# pylint: disable=too-many-public-methods import unittest from datetime import datetime, timedelta diff --git a/tests/util/test_init.py b/tests/util/test_init.py index c8827f91c58..9bfd6ebd6ed 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,5 +1,4 @@ """Test Home Assistant util methods.""" -# pylint: disable=too-many-public-methods import unittest from unittest.mock import patch from datetime import datetime, timedelta diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 6d099ebcfac..d83979affd0 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,4 @@ """Test Home Assistant location util methods.""" -# pylint: disable=too-many-public-methods from unittest import TestCase from unittest.mock import patch diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 9ead3c858a5..11a006524a9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -12,7 +12,7 @@ from tests.common import get_test_config_dir, patch_yaml_files class TestYaml(unittest.TestCase): """Test util.yaml loader.""" - # pylint: disable=no-self-use,invalid-name + # pylint: disable=no-self-use, invalid-name def test_simple_list(self): """Test simple list.""" @@ -254,7 +254,7 @@ def load_yaml(fname, string): return load_yaml_config_file(fname) -class FakeKeyring(): # pylint: disable=too-few-public-methods +class FakeKeyring(): """Fake a keyring class.""" def __init__(self, secrets_dict): @@ -270,7 +270,7 @@ class FakeKeyring(): # pylint: disable=too-few-public-methods class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" - # pylint: disable=protected-access,invalid-name + # pylint: disable=protected-access, invalid-name def setUp(self): # pylint: disable=invalid-name """Create & load secrets file.""" @@ -295,7 +295,8 @@ class TestSecrets(unittest.TestCase): ' password: !secret comp1_pw\n' '') - def tearDown(self): # pylint: disable=invalid-name + # pylint: disable=invalid-name + def tearDown(self): """Clean up secrets.""" yaml.clear_secret_cache() FILES.clear() From 8e695d1eb0e7d01c352eaafeb9d9f9141fe80f25 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 30 Oct 2016 18:13:27 -0400 Subject: [PATCH 091/149] Fixed typo (#4145) --- homeassistant/components/media_player/panasonic_viera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 509ba5f49eb..a98e54fd6c9 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -73,7 +73,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" def __init__(self, name, remote): - """Initialize the samsung device.""" + """Initialize the Panasonic device.""" # Save a reference to the imported class self._name = name self._muted = False From 705814cb08a4683a8316170fc5c901ac10ac9135 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 30 Oct 2016 15:17:41 -0700 Subject: [PATCH 092/149] Catch all errors when doing mqtt message unicode-decode. (#4143) * catch all errors when doing mqtt message unicode-decode. * added AttributeError and UnicodeDecodeError to exception when decoding an mqtt message payload --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 4595a3c79c4..85d99e5f7ee 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -399,7 +399,7 @@ class MQTT(object): """Message received callback.""" try: payload = msg.payload.decode('utf-8') - except AttributeError: + except (AttributeError, UnicodeDecodeError): _LOGGER.error("Illegal utf-8 unicode payload from " "MQTT topic: %s, Payload: %s", msg.topic, msg.payload) From 5ce9aea65d0a9775d9f901306ec940b3b240e45b Mon Sep 17 00:00:00 2001 From: Jared Beckham Date: Mon, 31 Oct 2016 00:51:03 -0400 Subject: [PATCH 093/149] Added tests for REST sensors (#4115) --- .coveragerc | 1 - homeassistant/components/sensor/rest.py | 2 +- tests/components/sensor/test_rest.py | 207 ++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 tests/components/sensor/test_rest.py diff --git a/.coveragerc b/.coveragerc index 4a9c8788ef3..29de998f2e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -272,7 +272,6 @@ omit = homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py - homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/serial_pm.py diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index bc731660676..16b50f8b901 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -126,7 +126,7 @@ class RestData(object): self.data = None def update(self): - """Get the latest data from REST service with GET method.""" + """Get the latest data from REST service with provided method.""" try: with requests.Session() as sess: response = sess.send( diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py new file mode 100644 index 00000000000..ab5a255c885 --- /dev/null +++ b/tests/components/sensor/test_rest.py @@ -0,0 +1,207 @@ +"""The tests for the REST switch platform.""" +import unittest +from unittest.mock import patch, Mock + +import requests +from requests.exceptions import Timeout, MissingSchema, RequestException +import requests_mock + +from homeassistant.bootstrap import setup_component +import homeassistant.components.sensor.rest as rest +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.config_validation import template +from tests.common import get_test_home_assistant, assert_setup_component + + +class TestRestSwitchSetup(unittest.TestCase): + """Tests for setting up the REST switch platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_missing_config(self): + """Test setup with configuration missing required entries.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost' + }, None)) + + def test_setup_missing_schema(self): + """Test setup with resource missing schema.""" + with self.assertRaises(MissingSchema): + rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'localhost', + 'method': 'GET' + }, None) + + @patch('requests.get', side_effect=requests.exceptions.ConnectionError()) + def test_setup_failed_connect(self, mock_req): + """Test setup when connection error occurs.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, None)) + + @patch('requests.get', side_effect=Timeout()) + def test_setup_timeout(self, mock_req): + """Test setup when connection timeout occurs.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, None)) + + @requests_mock.Mocker() + def test_setup_minimum(self, mock_req): + """Test setup with minimum configuration.""" + mock_req.get('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'rest', + 'resource': 'http://localhost' + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'switch') + + @requests_mock.Mocker() + def test_setup_get(self, mock_req): + """Test setup with valid configuration.""" + mock_req.get('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'rest', + 'resource': 'http://localhost', + 'method': 'GET', + 'value_template': '{{ value_json.key }}', + 'name': 'foo', + 'unit_of_measurement': 'MB', + 'verify_ssl': 'true', + 'authentication': 'basic', + 'username': 'my username', + 'password': 'my password', + 'headers': {'Accept': 'application/json'} + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'sensor') + + @requests_mock.Mocker() + def test_setup_post(self, mock_req): + """Test setup with valid configuration.""" + mock_req.post('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'rest', + 'resource': 'http://localhost', + 'method': 'POST', + 'value_template': '{{ value_json.key }}', + 'payload': '{ "device": "toaster"}', + 'name': 'foo', + 'unit_of_measurement': 'MB', + 'verify_ssl': 'true', + 'authentication': 'basic', + 'username': 'my username', + 'password': 'my password', + 'headers': {'Accept': 'application/json'} + } + })) + self.assertEqual(2, mock_req.call_count) + assert_setup_component(1, 'sensor') + + +class TestRestSensor(unittest.TestCase): + """Tests for REST sensor platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.initial_state = 'initial_state' + self.rest = Mock('rest.RestData') + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "' + self.initial_state + '" }')) + self.name = 'foo' + self.unit_of_measurement = 'MB' + self.value_template = template('{{ value_json.key }}') + self.value_template.hass = self.hass + + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def update_side_effect(self, data): + """Side effect function for mocking RestData.update().""" + self.rest.data = data + + def test_name(self): + """Test the name.""" + self.assertEqual(self.name, self.sensor.name) + + def test_unit_of_measurement(self): + """Test the unit of measurement.""" + self.assertEqual(self.unit_of_measurement, + self.sensor.unit_of_measurement) + + def test_state(self): + """Test the initial state.""" + self.assertEqual(self.initial_state, self.sensor.state) + + def test_update_when_value_is_none(self): + """Test state gets updated to unknown when sensor returns no data.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect(None)) + self.sensor.update() + self.assertEqual(STATE_UNKNOWN, self.sensor.state) + + def test_update_when_value_changed(self): + """Test state gets updated when sensor returns a new status.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "updated_state" }')) + self.sensor.update() + self.assertEqual('updated_state', self.sensor.state) + + def test_update_with_no_template(self): + """Test update when there is no value template.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + 'plain_state')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None) + self.sensor.update() + self.assertEqual('plain_state', self.sensor.state) + + +class TestRestData(unittest.TestCase): + """Tests for RestData.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.method = "GET" + self.resource = "http://localhost" + self.verify_ssl = True + self.rest = rest.RestData(self.method, self.resource, None, None, None, + self.verify_ssl) + + @requests_mock.Mocker() + def test_update(self, mock_req): + """Test update.""" + mock_req.get('http://localhost', text='test data') + self.rest.update() + self.assertEqual('test data', self.rest.data) + + @patch('requests.get', side_effect=RequestException) + def test_update_request_exception(self, mock_req): + """Test update when a request exception occurs.""" + self.rest.update() + self.assertEqual(None, self.rest.data) From 274e9799b3c29ac3d1eed799bce54301de0b950b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 31 Oct 2016 08:01:25 +0100 Subject: [PATCH 094/149] Add random number sensor (#4139) --- homeassistant/components/sensor/random.py | 73 +++++++++++++++++++++++ tests/components/sensor/test_random.py | 36 +++++++++++ 2 files changed, 109 insertions(+) create mode 100644 homeassistant/components/sensor/random.py create mode 100644 tests/components/sensor/test_random.py diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py new file mode 100644 index 00000000000..bb06f131f2f --- /dev/null +++ b/homeassistant/components/sensor/random.py @@ -0,0 +1,73 @@ +""" +Support for showing random numbers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.random/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_MINIMUM, CONF_MAXIMUM) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Random Sensor' +DEFAULT_MIN = 0 +DEFAULT_MAX = 20 + +ICON = 'mdi:hanger' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MAXIMUM, default=DEFAULT_MAX): cv.positive_int, + vol.Optional(CONF_MINIMUM, default=DEFAULT_MIN): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Random number sensor.""" + name = config.get(CONF_NAME) + minimum = config.get(CONF_MINIMUM) + maximum = config.get(CONF_MAXIMUM) + + hass.loop.create_task(async_add_devices( + [RandomSensor(name, minimum, maximum)], True)) + return True + + +class RandomSensor(Entity): + """Representation of a Random number sensor.""" + + def __init__(self, name, minimum, maximum): + """Initialize the sensor.""" + self._name = name + self._minimum = minimum + self._maximum = maximum + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @asyncio.coroutine + def async_update(self): + """Get a new number and updates the states.""" + from random import randrange + self._state = randrange(self._minimum, self._maximum) diff --git a/tests/components/sensor/test_random.py b/tests/components/sensor/test_random.py new file mode 100644 index 00000000000..3e66d5003ce --- /dev/null +++ b/tests/components/sensor/test_random.py @@ -0,0 +1,36 @@ +"""The test for the random number sensor platform.""" +import unittest + +from homeassistant.bootstrap import setup_component + +from tests.common import get_test_home_assistant + + +class TestRandomSensor(unittest.TestCase): + """Test the Random number sensor.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_random_sensor(self): + """Test the Randowm number sensor.""" + config = { + 'sensor': { + 'platform': 'random', + 'name': 'test', + 'minimum': 10, + 'maximum': 20, + } + } + + assert setup_component(self.hass, 'sensor', config) + + state = self.hass.states.get('sensor.test') + + self.assertLessEqual(int(state.state), config['sensor']['maximum']) + self.assertGreater(int(state.state), config['sensor']['minimum']) From 5ba815ab21bf85fa4433a67af0ff19648f296c96 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 31 Oct 2016 09:23:34 +0100 Subject: [PATCH 095/149] flux led lib --- homeassistant/components/light/flux_led.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index e504242200e..095733afd8e 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip' +REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip' '#flux_led==0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7c6a303d0e5..b5c68ff4e7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -146,7 +146,7 @@ hikvision==0.4 # http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0 # homeassistant.components.light.flux_led -https://github.com/Danielhiversen/flux_led/archive/0.7.zip#flux_led==0.8 +https://github.com/Danielhiversen/flux_led/archive/0.8.zip#flux_led==0.8 # homeassistant.components.switch.tplink https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0 From a89e635bf31fcf5050425e8d71d7a40054611113 Mon Sep 17 00:00:00 2001 From: Jared Beckham Date: Mon, 31 Oct 2016 08:14:23 -0400 Subject: [PATCH 096/149] Added tests for REST switches (#4016) * Added tests for REST switches * Remove REST switch from test coverage exclusions --- .coveragerc | 1 - tests/components/switch/test_rest.py | 186 +++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 tests/components/switch/test_rest.py diff --git a/.coveragerc b/.coveragerc index 29de998f2e5..5695c4fad0e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -303,7 +303,6 @@ omit = homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py - homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py new file mode 100644 index 00000000000..3bc862218c8 --- /dev/null +++ b/tests/components/switch/test_rest.py @@ -0,0 +1,186 @@ +"""The tests for the REST switch platform.""" +import unittest +from unittest.mock import patch + +import requests +from requests.exceptions import Timeout +import requests_mock + +import homeassistant.components.switch.rest as rest +from homeassistant.bootstrap import setup_component +from tests.common import get_test_home_assistant, assert_setup_component + + +class TestRestSwitchSetup(unittest.TestCase): + """Tests for setting up the REST switch platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_missing_config(self): + """Test setup with configuration missing required entries.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest' + }, None)) + + def test_setup_missing_schema(self): + """Test setup with resource missing schema.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'localhost' + }, None)) + + @patch('requests.get', side_effect=requests.exceptions.ConnectionError()) + def test_setup_failed_connect(self, mock_req): + """Test setup when connection error occurs.""" + self.assertFalse(rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, None)) + + @patch('requests.get', side_effect=Timeout()) + def test_setup_timeout(self, mock_req): + """Test setup when connection timeout occurs.""" + with self.assertRaises(Timeout): + rest.setup_platform(self.hass, { + 'platform': 'rest', + 'resource': 'http://localhost', + }, None) + + @requests_mock.Mocker() + def test_setup_minimum(self, mock_req): + """Test setup with minimum configuration.""" + mock_req.get('http://localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'switch', { + 'switch': { + 'platform': 'rest', + 'resource': 'http://localhost' + } + })) + self.assertEqual(1, mock_req.call_count) + assert_setup_component(1, 'switch') + + @requests_mock.Mocker() + def test_setup(self, mock_req): + """Test setup with valid configuration.""" + mock_req.get('localhost', status_code=200) + self.assertTrue(setup_component(self.hass, 'switch', { + 'switch': { + 'platform': 'rest', + 'name': 'foo', + 'resource': 'http://localhost', + 'body_on': 'custom on text', + 'body_off': 'custom off text', + } + })) + self.assertEqual(1, mock_req.call_count) + assert_setup_component(1, 'switch') + + +class TestRestSwitch(unittest.TestCase): + """Tests for REST switch platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.name = 'foo' + self.resource = 'http://localhost/' + self.body_on = 'on' + self.body_off = 'off' + self.switch = rest.RestSwitch(self.hass, self.name, self.resource, + self.body_on, self.body_off) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_name(self): + """Test the name.""" + self.assertEqual(self.name, self.switch.name) + + def test_is_on_before_update(self): + """Test is_on in initial state.""" + self.assertEqual(None, self.switch.is_on) + + @requests_mock.Mocker() + def test_turn_on_success(self, mock_req): + """Test turn_on.""" + mock_req.post(self.resource, status_code=200) + self.switch.turn_on() + + self.assertEqual(self.body_on, mock_req.last_request.text) + self.assertEqual(True, self.switch.is_on) + + @requests_mock.Mocker() + def test_turn_on_status_not_ok(self, mock_req): + """Test turn_on when error status returned.""" + mock_req.post(self.resource, status_code=500) + self.switch.turn_on() + + self.assertEqual(self.body_on, mock_req.last_request.text) + self.assertEqual(None, self.switch.is_on) + + @patch('requests.post', side_effect=Timeout()) + def test_turn_on_timeout(self, mock_req): + """Test turn_on when timeout occurs.""" + with self.assertRaises(Timeout): + self.switch.turn_on() + + @requests_mock.Mocker() + def test_turn_off_success(self, mock_req): + """Test turn_off.""" + mock_req.post(self.resource, status_code=200) + self.switch.turn_off() + + self.assertEqual(self.body_off, mock_req.last_request.text) + self.assertEqual(False, self.switch.is_on) + + @requests_mock.Mocker() + def test_turn_off_status_not_ok(self, mock_req): + """Test turn_off when error status returned.""" + mock_req.post(self.resource, status_code=500) + self.switch.turn_off() + + self.assertEqual(self.body_off, mock_req.last_request.text) + self.assertEqual(None, self.switch.is_on) + + @patch('requests.post', side_effect=Timeout()) + def test_turn_off_timeout(self, mock_req): + """Test turn_off when timeout occurs.""" + with self.assertRaises(Timeout): + self.switch.turn_on() + + @requests_mock.Mocker() + def test_update_when_on(self, mock_req): + """Test update when switch is on.""" + mock_req.get(self.resource, text=self.body_on) + self.switch.update() + + self.assertEqual(True, self.switch.is_on) + + @requests_mock.Mocker() + def test_update_when_off(self, mock_req): + """Test update when switch is off.""" + mock_req.get(self.resource, text=self.body_off) + self.switch.update() + + self.assertEqual(False, self.switch.is_on) + + @requests_mock.Mocker() + def test_update_when_unknown(self, mock_req): + """Test update when unknown status returned.""" + mock_req.get(self.resource, text='unknown status') + self.switch.update() + + self.assertEqual(None, self.switch.is_on) + + @patch('requests.get', side_effect=Timeout()) + def test_update_timeout(self, mock_req): + """Test update when timeout occurs.""" + with self.assertRaises(Timeout): + self.switch.update() From 4484a7a94b8ba91f77b60df2bb35a944cb164e7f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 31 Oct 2016 13:18:47 +0100 Subject: [PATCH 097/149] Use voluptuous for Pilight switch (#3819) * Migrate to voluptuous * Add protocol * Update --- homeassistant/components/pilight.py | 45 +++++------ homeassistant/components/sensor/pilight.py | 15 ++-- homeassistant/components/switch/pilight.py | 90 ++++++++++++++-------- homeassistant/const.py | 1 + tests/components/sensor/test_pilight.py | 19 +++-- 5 files changed, 96 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 2cfbc0063a1..de4e56c925f 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -1,5 +1,5 @@ """ -Component to create an interface to a Pilight daemon (https://pilight.org/). +Component to create an interface to a Pilight daemon. For more details about this component, please refer to the documentation at https://home-assistant.io/components/pilight/ @@ -12,40 +12,38 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, - CONF_WHITELIST) + CONF_WHITELIST, CONF_PROTOCOL) REQUIREMENTS = ['pilight==0.1.1'] _LOGGER = logging.getLogger(__name__) -ATTR_PROTOCOL = 'protocol' - DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 5000 DOMAIN = 'pilight' EVENT = 'pilight_received' -# The pilight code schema depends on the protocol -# Thus only require to have the protocol information -# Ensure that protocol is in a list otherwise segfault in pilight-daemon -# https://github.com/pilight/pilight/issues/296 -RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): - vol.All(cv.ensure_list, [cv.string])}, - extra=vol.ALLOW_EXTRA) +# The Pilight code schema depends on the protocol. Thus only require to have +# the protocol information. Ensure that protocol is in a list otherwise +# segfault in pilight-daemon, https://github.com/pilight/pilight/issues/296 +RF_CODE_SCHEMA = vol.Schema({ + vol.Required(CONF_PROTOCOL): vol.All(cv.ensure_list, [cv.string]), +}, extra=vol.ALLOW_EXTRA) + SERVICE_NAME = 'send' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]} }), }, extra=vol.ALLOW_EXTRA) def setup(hass, config): - """Setup the pilight component.""" + """Setup the Pilight component.""" from pilight import pilight host = config[DOMAIN][CONF_HOST] @@ -58,23 +56,22 @@ def setup(hass, config): host, port, err) return False - # Start / stop pilight-daemon connection with HA start/stop def start_pilight_client(_): - """Called once when home assistant starts.""" + """Called once when Home Assistant starts.""" pilight_client.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_pilight_client) def stop_pilight_client(_): - """Called once when home assistant stops.""" + """Called once when Home Assistant stops.""" pilight_client.stop() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client) def send_code(call): """Send RF code to the pilight-daemon.""" - # Change type to dict from mappingproxy - # since data has to be JSON serializable + # Change type to dict from mappingproxy since data has to be JSON + # serializable message_data = dict(call.data) try: @@ -82,8 +79,8 @@ def setup(hass, config): except IOError: _LOGGER.error('Pilight send failed for %s', str(message_data)) - hass.services.register(DOMAIN, SERVICE_NAME, - send_code, schema=RF_CODE_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA) # Publish received codes on the HA event bus # A whitelist of codes to be published in the event bus @@ -93,10 +90,8 @@ def setup(hass, config): """Called when RF codes are received.""" # Unravel dict of dicts to make event_data cut in automation rule # possible - data = dict( - {'protocol': data['protocol'], - 'uuid': data['uuid']}, - **data['message']) + data = dict({'protocol': data['protocol'], 'uuid': data['uuid']}, + **data['message']) # No whitelist defined, put data on event bus if not whitelist: diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py index 0266862d529..8a403b4adbf 100644 --- a/homeassistant/components/sensor/pilight.py +++ b/homeassistant/components/sensor/pilight.py @@ -1,5 +1,5 @@ """ -Support for pilight sensors. +Support for Pilight sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.pilight/ @@ -9,8 +9,7 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_NAME, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, - CONF_PAYLOAD) + CONF_NAME, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.components.pilight as pilight @@ -18,11 +17,13 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +CONF_VARIABLE = 'variable' + DEFAULT_NAME = 'Pilight Sensor' DEPENDENCIES = ['pilight'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required("variable"): cv.string, + vol.Required(CONF_VARIABLE): cv.string, vol.Required(CONF_PAYLOAD): vol.Schema(dict), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string, @@ -31,18 +32,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup pilight Sensor.""" + """Set up Pilight Sensor.""" add_devices([PilightSensor( hass=hass, name=config.get(CONF_NAME), - variable=config.get("variable"), + variable=config.get(CONF_VARIABLE), payload=config.get(CONF_PAYLOAD), unit_of_measurement=config.get(CONF_UNIT_OF_MEASUREMENT) )]) class PilightSensor(Entity): - """Representation of a sensor that can be updated using pilight.""" + """Representation of a sensor that can be updated using Pilight.""" def __init__(self, hass, name, variable, payload, unit_of_measurement): """Initialize the sensor.""" diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index c143ae5c887..1818372f1dc 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -1,44 +1,77 @@ """ -Support for switching devices via pilight to on and off. +Support for switching devices via Pilight to on and off. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.pilight/ """ import logging -from homeassistant.helpers.config_validation import ensure_list -import homeassistant.components.pilight as pilight -from homeassistant.components.switch import SwitchDevice +import voluptuous as vol -DEPENDENCIES = ['pilight'] +import homeassistant.helpers.config_validation as cv +import homeassistant.components.pilight as pilight +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE) _LOGGER = logging.getLogger(__name__) +CONF_OFF_CODE = 'off_code' +CONF_OFF_CODE_RECIEVE = 'off_code_receive' +CONF_ON_CODE = 'on_code' +CONF_ON_CODE_RECIEVE = 'on_code_receive' +CONF_SYSTEMCODE = 'systemcode' +CONF_UNIT = 'unit' -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup the pilight platform.""" - # Find and return switches controlled by pilight - switches = config.get('switches', {}) +DEPENDENCIES = ['pilight'] + +COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({ + vol.Optional('on'): cv.positive_int, + vol.Optional('off'): cv.positive_int, + vol.Optional(CONF_UNIT): cv.string, + vol.Optional(CONF_ID): cv.positive_int, + vol.Optional(CONF_STATE): cv.string, + vol.Optional(CONF_SYSTEMCODE): cv.string, +}) + +SWITCHES_SCHEMA = vol.Schema({ + vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, + vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OFF_CODE_RECIEVE): COMMAND_SCHEMA, + vol.Optional(CONF_ON_CODE_RECIEVE): COMMAND_SCHEMA, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SWITCHES): + vol.Schema({cv.string: SWITCHES_SCHEMA}), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Pilight platform.""" + switches = config.get(CONF_SWITCHES) devices = [] for dev_name, properties in switches.items(): devices.append( PilightSwitch( hass, - properties.get('name', dev_name), - properties.get('on_code'), - properties.get('off_code'), - ensure_list(properties.get('on_code_receive', False)), - ensure_list(properties.get('off_code_receive', False)))) + properties.get(CONF_NAME, dev_name), + properties.get(CONF_ON_CODE), + properties.get(CONF_OFF_CODE), + properties.get(CONF_ON_CODE_RECIEVE), + properties.get(CONF_OFF_CODE_RECIEVE) + ) + ) - add_devices_callback(devices) + add_devices(devices) class PilightSwitch(SwitchDevice): - """Representation of a pilight switch.""" + """Representation of a Pilight switch.""" - def __init__(self, hass, name, code_on, code_off, - code_on_receive, code_off_receive): + def __init__(self, hass, name, code_on, code_off, code_on_receive, + code_off_receive): """Initialize the switch.""" self._hass = hass self._name = name @@ -69,29 +102,22 @@ class PilightSwitch(SwitchDevice): def _handle_code(self, call): """Check if received code by the pilight-daemon. - If the code matches the receive on / off codes of this switch - the switch state is changed accordingly. + If the code matches the receive on/off codes of this switch the switch + state is changed accordingly. """ - # Check if a on code is defined to turn this switch on + # - True if off_code/on_code is contained in received code dict, not + # all items have to match. + # - Call turn on/off only once, even if more than one code is received if any(self._code_on_receive): - for on_code in self._code_on_receive: # Loop through codes - # True if on_code is contained in received code dict, not - # all items have to match + for on_code in self._code_on_receive: if on_code.items() <= call.data.items(): self.turn_on() - # Call turn on only once, even when more than one on - # code is received break - # Check if a off code is defined to turn this switch off if any(self._code_off_receive): - for off_code in self._code_off_receive: # Loop through codes - # True if off_code is contained in received code dict, not - # all items have to match + for off_code in self._code_off_receive: if off_code.items() <= call.data.items(): self.turn_off() - # Call turn off only once, even when more than one off - # code is received break def turn_on(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index 69c340db3db..fcba511fe10 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -114,6 +114,7 @@ CONF_PIN = 'pin' CONF_PLATFORM = 'platform' CONF_PORT = 'port' CONF_PREFIX = 'prefix' +CONF_PROTOCOL = 'protocol' CONF_QUOTE = 'quote' CONF_RECIPIENT = 'recipient' CONF_RESOURCE = 'resource' diff --git a/tests/components/sensor/test_pilight.py b/tests/components/sensor/test_pilight.py index 08e174df4cc..943369f209c 100644 --- a/tests/components/sensor/test_pilight.py +++ b/tests/components/sensor/test_pilight.py @@ -11,8 +11,8 @@ HASS = None def fire_pilight_message(protocol, data): - """Fire the fake pilight message.""" - message = {pilight.ATTR_PROTOCOL: protocol} + """Fire the fake Pilight message.""" + message = {pilight.CONF_PROTOCOL: protocol} message.update(data) HASS.bus.fire(pilight.EVENT, message) @@ -67,23 +67,23 @@ def test_disregard_wrong_payload(): 'platform': 'pilight', 'name': 'test_2', 'variable': 'test', - 'payload': {'uuid': '1-2-3-4', - 'protocol': 'test-protocol_2'} + 'payload': { + 'uuid': '1-2-3-4', + 'protocol': 'test-protocol_2' + } } }) # Try set value from data with incorrect payload fire_pilight_message(protocol='test-protocol_2', - data={'test': 'data', - 'uuid': '0-0-0-0'}) + data={'test': 'data', 'uuid': '0-0-0-0'}) HASS.block_till_done() state = HASS.states.get('sensor.test_2') assert state.state == 'unknown' # Try set value from data with partially matched payload fire_pilight_message(protocol='wrong-protocol', - data={'test': 'data', - 'uuid': '1-2-3-4'}) + data={'test': 'data', 'uuid': '1-2-3-4'}) HASS.block_till_done() state = HASS.states.get('sensor.test_2') assert state.state == 'unknown' @@ -113,8 +113,7 @@ def test_variable_missing(caplog): # Create code without sensor variable fire_pilight_message(protocol='test-protocol', - data={'uuid': '1-2-3-4', - 'other_variable': 3.141}) + data={'uuid': '1-2-3-4', 'other_variable': 3.141}) HASS.block_till_done() logs = caplog.text From 06de7053ce3667ef26e9b8f3ba41894051855f89 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 31 Oct 2016 08:29:08 -0400 Subject: [PATCH 098/149] Add Emby Server media_player component (#3862) * Add Emby Server media_player component * Code cleanup, move to request sessions, generate UUID per session * Make media image fetch more robust * Allow for http or https * Cleanup some Keyerror conditions found through more testing * Move EmbyRemote to pip, update requirements * Code cleanup, add SSL config option --- .coveragerc | 1 + homeassistant/components/media_player/emby.py | 304 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 308 insertions(+) create mode 100644 homeassistant/components/media_player/emby.py diff --git a/.coveragerc b/.coveragerc index 5695c4fad0e..3bf0f7c8249 100644 --- a/.coveragerc +++ b/.coveragerc @@ -179,6 +179,7 @@ omit = homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/directv.py + homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/itunes.py diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py new file mode 100644 index 00000000000..3422fadbc10 --- /dev/null +++ b/homeassistant/components/media_player/emby.py @@ -0,0 +1,304 @@ +""" +Support to interface with the Emby API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.emby/ +""" +import logging + +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.media_player import ( + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice, + PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME, + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.helpers.event import (track_utc_time_change) +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyemby==0.1'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) + +DEFAULT_PORT = 8096 + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_STOP | SUPPORT_SEEK + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default='localhost'): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Emby platform.""" + from pyemby.emby import EmbyRemote + + _host = config.get(CONF_HOST) + _key = config.get(CONF_API_KEY) + _port = config.get(CONF_PORT) + + if config.get(CONF_SSL): + _protocol = "https" + else: + _protocol = "http" + + _url = '{}://{}:{}'.format(_protocol, _host, _port) + + _LOGGER.debug('Setting up Emby server at: %s', _url) + + embyserver = EmbyRemote(_key, _url) + + emby_clients = {} + emby_sessions = {} + track_utc_time_change(hass, lambda now: update_devices(), second=30) + + @Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update_devices(): + """Update the devices objects.""" + devices = embyserver.get_sessions() + if devices is None: + _LOGGER.error('Error listing Emby devices.') + return + + new_emby_clients = [] + for device in devices: + if device['DeviceId'] == embyserver.unique_id: + break + + if device['DeviceId'] not in emby_clients: + _LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.', + device['DeviceId']) + new_client = EmbyClient(embyserver, device, emby_sessions, + update_devices, update_sessions) + emby_clients[device['DeviceId']] = new_client + new_emby_clients.append(new_client) + else: + emby_clients[device['DeviceId']].set_device(device) + + if new_emby_clients: + add_devices_callback(new_emby_clients) + + @Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update_sessions(): + """Update the sessions objects.""" + sessions = embyserver.get_sessions() + if sessions is None: + _LOGGER.error('Error listing Emby sessions') + return + + emby_sessions.clear() + for session in sessions: + emby_sessions[session['DeviceId']] = session + + update_devices() + update_sessions() + + +class EmbyClient(MediaPlayerDevice): + """Representation of a Emby device.""" + + # pylint: disable=too-many-arguments, too-many-public-methods, + # pylint: disable=abstract-method + def __init__(self, client, device, emby_sessions, update_devices, + update_sessions): + """Initialize the Emby device.""" + self.emby_sessions = emby_sessions + self.update_devices = update_devices + self.update_sessions = update_sessions + self.client = client + self.set_device(device) + + def set_device(self, device): + """Set the device property.""" + self.device = device + + @property + def unique_id(self): + """Return the id of this emby client.""" + return '{}.{}'.format( + self.__class__, self.device['DeviceId']) + + @property + def supports_remote_control(self): + """Return control ability.""" + return self.device['SupportsRemoteControl'] + + @property + def name(self): + """Return the name of the device.""" + return 'emby_{}'.format(self.device['DeviceName']) or \ + DEVICE_DEFAULT_NAME + + @property + def session(self): + """Return the session, if any.""" + if self.device['DeviceId'] not in self.emby_sessions: + return None + + return self.emby_sessions[self.device['DeviceId']] + + @property + def now_playing_item(self): + """Return the currently playing item, if any.""" + session = self.session + if session is not None and 'NowPlayingItem' in session: + return session['NowPlayingItem'] + + @property + def state(self): + """Return the state of the device.""" + session = self.session + if session: + if 'NowPlayingItem' in session: + if session['PlayState']['IsPaused']: + return STATE_PAUSED + else: + return STATE_PLAYING + else: + return STATE_IDLE + # This is nasty. Need to find a way to determine alive + else: + return STATE_OFF + + return STATE_UNKNOWN + + def update(self): + """Get the latest details.""" + self.update_devices(no_throttle=True) + self.update_sessions(no_throttle=True) + + def play_percent(self): + """Return current media percent complete.""" + if self.now_playing_item['RunTimeTicks'] and \ + self.session['PlayState']['PositionTicks']: + try: + return int(self.session['PlayState']['PositionTicks']) / \ + int(self.now_playing_item['RunTimeTicks']) * 100 + except KeyError: + return 0 + else: + return 0 + + @property + def app_name(self): + """Return current user as app_name.""" + # Ideally the media_player object would have a user property. + try: + return self.device['UserName'] + except KeyError: + return None + + @property + def media_content_id(self): + """Content ID of current playing media.""" + if self.now_playing_item is not None: + try: + return self.now_playing_item['Id'] + except KeyError: + return None + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self.now_playing_item is None: + return None + try: + media_type = self.now_playing_item['Type'] + if media_type == 'Episode': + return MEDIA_TYPE_TVSHOW + elif media_type == 'Movie': + return MEDIA_TYPE_VIDEO + return None + except KeyError: + return None + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self.now_playing_item and self.media_content_type: + try: + return int(self.now_playing_item['RunTimeTicks']) / 10000000 + except KeyError: + return None + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self.now_playing_item is not None: + try: + return self.client.get_image( + self.now_playing_item['ThumbItemId'], 'Thumb', + self.play_percent()) + except KeyError: + try: + return self.client.get_image( + self.now_playing_item['PrimaryImageItemId'], 'Primary', + self.play_percent()) + except KeyError: + return None + + @property + def media_title(self): + """Title of current playing media.""" + # find a string we can use as a title + if self.now_playing_item is not None: + return self.now_playing_item['Name'] + + @property + def media_season(self): + """Season of curent playing media (TV Show only).""" + if self.now_playing_item is not None and \ + 'ParentIndexNumber' in self.now_playing_item: + return self.now_playing_item['ParentIndexNumber'] + + @property + def media_series_title(self): + """The title of the series of current playing media (TV Show only).""" + if self.now_playing_item is not None and \ + 'SeriesName' in self.now_playing_item: + return self.now_playing_item['SeriesName'] + + @property + def media_episode(self): + """Episode of current playing media (TV Show only).""" + if self.now_playing_item is not None and \ + 'IndexNumber' in self.now_playing_item: + return self.now_playing_item['IndexNumber'] + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + if self.supports_remote_control: + return SUPPORT_EMBY + else: + return None + + def media_play(self): + """Send play command.""" + if self.supports_remote_control: + self.client.play(self.session) + + def media_pause(self): + """Send pause command.""" + if self.supports_remote_control: + self.client.pause(self.session) + + def media_next_track(self): + """Send next track command.""" + self.client.next_track(self.session) + + def media_previous_track(self): + """Send previous track command.""" + self.client.previous_track(self.session) diff --git a/requirements_all.txt b/requirements_all.txt index b5c68ff4e7e..0c296981896 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -349,6 +349,9 @@ pycmus==0.1.0 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.media_player.emby +pyemby==0.1 + # homeassistant.components.envisalink pyenvisalink==1.7 From b4899ec46902741a209788d05cbc2214fcc391e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 31 Oct 2016 13:31:09 +0100 Subject: [PATCH 099/149] Allow multiple symbols (sensor.yahoo_finance) (#4126) * Allow multiple symbols * Update test --- .../components/sensor/yahoo_finance.py | 35 +++++++++++-------- tests/components/sensor/test_yahoo_finance.py | 15 ++++---- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sensor/yahoo_finance.py b/homeassistant/components/sensor/yahoo_finance.py index f4278a46d44..7316528849c 100644 --- a/homeassistant/components/sensor/yahoo_finance.py +++ b/homeassistant/components/sensor/yahoo_finance.py @@ -10,7 +10,7 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -24,36 +24,44 @@ ATTR_OPEN = 'open' ATTR_PREV_CLOSE = 'prev_close' CONF_ATTRIBUTION = "Stock market information provided by Yahoo! Inc." -CONF_SYMBOL = 'symbol' +CONF_SYMBOLS = 'symbols' DEFAULT_NAME = 'Yahoo Stock' DEFAULT_SYMBOL = 'YHOO' ICON = 'mdi:currency-usd' -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SYMBOL, default=DEFAULT_SYMBOL): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]): + vol.All(cv.ensure_list, [cv.string]), }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Yahoo Finance sensor.""" - name = config.get(CONF_NAME) - symbol = config.get(CONF_SYMBOL) + from yahoo_finance import Share - data = YahooFinanceData(name, symbol) - add_devices([YahooFinanceSensor(name, data, symbol)]) + symbols = config.get(CONF_SYMBOLS) + + dev = [] + for symbol in symbols: + if Share(symbol).get_price() is None: + _LOGGER.warning("Symbol %s unknown", symbol) + break + data = YahooFinanceData(symbol) + dev.append(YahooFinanceSensor(data, symbol)) + + add_devices(dev) class YahooFinanceSensor(Entity): """Representation of a Yahoo Finance sensor.""" - def __init__(self, name, data, symbol): + def __init__(self, data, symbol): """Initialize the sensor.""" - self._name = name + self._name = symbol self.data = data self._symbol = symbol self._state = None @@ -101,17 +109,16 @@ class YahooFinanceSensor(Entity): class YahooFinanceData(object): """Get data from Yahoo Finance.""" - def __init__(self, name, symbol): + def __init__(self, symbol): """Initialize the data object.""" from yahoo_finance import Share - self._name = name self._symbol = symbol self.state = None self.price_change = None self.price_open = None self.prev_close = None - self.stock = Share(symbol) + self.stock = Share(self._symbol) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/tests/components/sensor/test_yahoo_finance.py b/tests/components/sensor/test_yahoo_finance.py index 5cbbf50dcab..4823458652b 100644 --- a/tests/components/sensor/test_yahoo_finance.py +++ b/tests/components/sensor/test_yahoo_finance.py @@ -11,10 +11,13 @@ from tests.common import ( VALID_CONFIG = { 'platform': 'yahoo_finance', - 'symbol': 'YHOO' + 'symbols': [ + 'YHOO', + ] } +# pylint: disable=invalid-name class TestYahooFinanceSetup(unittest.TestCase): """Test the Yahoo Finance platform.""" @@ -29,13 +32,13 @@ class TestYahooFinanceSetup(unittest.TestCase): @patch('yahoo_finance.Base._request', return_value=json.loads(load_fixture('yahoo_finance.json'))) - def test_default_setup(self, m): # pylint: disable=invalid-name + def test_default_setup(self, mock_request): """Test the default setup.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, { 'sensor': VALID_CONFIG}) - state = self.hass.states.get('sensor.yahoo_stock') - self.assertEqual("41.69", state.attributes.get('open')) - self.assertEqual("41.79", state.attributes.get('prev_close')) - self.assertEqual("YHOO", state.attributes.get('unit_of_measurement')) + state = self.hass.states.get('sensor.yhoo') + self.assertEqual('41.69', state.attributes.get('open')) + self.assertEqual('41.79', state.attributes.get('prev_close')) + self.assertEqual('YHOO', state.attributes.get('unit_of_measurement')) From a1e910f1cf3f3081bf8470dc188505d75a9ce1ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2016 08:22:49 -0700 Subject: [PATCH 100/149] Disable rest switch tests --- .coveragerc | 4 ++-- tests/components/switch/test_rest.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3bf0f7c8249..b4c34555e16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -98,8 +98,6 @@ omit = homeassistant/components/homematic.py homeassistant/components/*/homematic.py - homeassistant/components/switch/pilight.py - homeassistant/components/knx.py homeassistant/components/*/knx.py @@ -303,7 +301,9 @@ omit = homeassistant/components/switch/neato.py homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py + homeassistant/components/switch/pilight.py homeassistant/components/switch/pulseaudio_loopback.py + homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index 3bc862218c8..85a178dcc42 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +import pytest import requests from requests.exceptions import Timeout import requests_mock @@ -11,6 +12,7 @@ from homeassistant.bootstrap import setup_component from tests.common import get_test_home_assistant, assert_setup_component +@pytest.mark.skip class TestRestSwitchSetup(unittest.TestCase): """Tests for setting up the REST switch platform.""" @@ -82,6 +84,7 @@ class TestRestSwitchSetup(unittest.TestCase): assert_setup_component(1, 'switch') +@pytest.mark.skip class TestRestSwitch(unittest.TestCase): """Tests for REST switch platform.""" From 7f699b4261d821a0d6699ce0615aa633f94228b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2016 08:47:29 -0700 Subject: [PATCH 101/149] Lazy initialise the worker pool (#4110) * Lazy initialise the worker pool * Minimize pool initialization in core tests * Fix tests on Python 3.4 * Remove passing in thread count to mock HASS * Tests: Allow pool by default for threaded, disable for async * Remove JobPriority for thread pool * Fix wrong block_till_done * EmulatedHue: Remove unused test code * Zigbee: do not touch hass.pool * Init loop in add_job * Fix core test * Fix random sensor test --- homeassistant/bootstrap.py | 12 ++- homeassistant/components/sensor/zigbee.py | 4 +- homeassistant/components/zigbee.py | 7 +- homeassistant/core.py | 78 ++++++++----------- homeassistant/util/__init__.py | 27 ++----- tests/common.py | 45 ++++++----- tests/components/camera/test_generic.py | 2 + tests/components/camera/test_local_file.py | 2 + tests/components/climate/test_demo.py | 4 +- tests/components/cover/test_rfxtrx.py | 2 +- tests/components/light/test_demo.py | 10 +-- tests/components/light/test_rfxtrx.py | 2 +- tests/components/mqtt/test_init.py | 4 +- tests/components/notify/test_demo.py | 2 +- .../sensor/test_imap_email_content.py | 2 +- tests/components/sensor/test_random.py | 2 +- tests/components/sensor/test_rfxtrx.py | 2 +- tests/components/switch/test_rfxtrx.py | 2 +- tests/components/test_conversation.py | 2 +- tests/components/test_emulated_hue.py | 57 -------------- tests/components/test_influxdb.py | 2 +- tests/components/test_logentries.py | 2 +- tests/components/test_logger.py | 2 +- tests/components/test_splunk.py | 2 +- tests/components/test_statsd.py | 2 +- tests/test_core.py | 47 ++++++++--- 26 files changed, 140 insertions(+), 185 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b9a3d89251a..294645f693b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -131,9 +131,10 @@ def _async_setup_component(hass: core.HomeAssistant, return False component = loader.get_component(domain) + async_comp = hasattr(component, 'async_setup') try: - if hasattr(component, 'async_setup'): + if async_comp: result = yield from component.async_setup(hass, config) else: result = yield from hass.loop.run_in_executor( @@ -155,9 +156,12 @@ def _async_setup_component(hass: core.HomeAssistant, # Assumption: if a component does not depend on groups # it communicates with devices - if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ - hass.pool.worker_count <= 10: - hass.pool.add_worker() + if (not async_comp and + 'group' not in getattr(component, 'DEPENDENCIES', [])): + if hass.pool is None: + hass.async_init_pool() + if hass.pool.worker_count <= 10: + hass.pool.add_worker() hass.bus.async_fire( EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 6b455230aa6..7d4ead138e3 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.components import zigbee from homeassistant.components.zigbee import PLATFORM_SCHEMA from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import JobPriority from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,8 +55,7 @@ class ZigBeeTemperatureSensor(Entity): self._config = config self._temp = None # Get initial state - hass.pool.add_job( - JobPriority.EVENT_STATE, (self.update_ha_state, True)) + hass.add_job(self.update_ha_state, True) @property def name(self): diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 8a9271745c9..a428d03efc1 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -13,7 +13,6 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN) -from homeassistant.core import JobPriority from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv @@ -308,8 +307,7 @@ class ZigBeeDigitalIn(Entity): subscribe(hass, handle_frame) # Get initial state - hass.pool.add_job( - JobPriority.EVENT_STATE, (self.update_ha_state, True)) + hass.add_job(self.update_ha_state, True) @property def name(self): @@ -435,8 +433,7 @@ class ZigBeeAnalogIn(Entity): subscribe(hass, handle_frame) # Get initial state - hass.pool.add_job( - JobPriority.EVENT_STATE, (self.update_ha_state, True)) + hass.add_job(self.update_ha_state, True) @property def name(self): diff --git a/homeassistant/core.py b/homeassistant/core.py index 4d61b33eb65..5fb7d2761cc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -102,29 +102,6 @@ class CoreState(enum.Enum): return self.value -class JobPriority(util.OrderedEnum): - """Provides job priorities for event bus jobs.""" - - EVENT_CALLBACK = 0 - EVENT_SERVICE = 1 - EVENT_STATE = 2 - EVENT_TIME = 3 - EVENT_DEFAULT = 4 - - @staticmethod - def from_event_type(event_type): - """Return a priority based on event type.""" - if event_type == EVENT_TIME_CHANGED: - return JobPriority.EVENT_TIME - elif event_type == EVENT_STATE_CHANGED: - return JobPriority.EVENT_STATE - elif event_type == EVENT_CALL_SERVICE: - return JobPriority.EVENT_SERVICE - elif event_type == EVENT_SERVICE_EXECUTED: - return JobPriority.EVENT_CALLBACK - return JobPriority.EVENT_DEFAULT - - class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -134,9 +111,10 @@ class HomeAssistant(object): self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) - self.pool = create_worker_pool() + self.pool = None self.bus = EventBus(self) - self.services = ServiceRegistry(self.bus, self.add_job, self.loop) + self.services = ServiceRegistry(self.bus, self.async_add_job, + self.loop) self.states = StateMachine(self.bus, self.loop) self.config = Config() # type: Config # This is a dictionary that any component can store any data on. @@ -180,8 +158,7 @@ class HomeAssistant(object): This method is a coroutine. """ - _LOGGER.info( - "Starting Home Assistant (%d threads)", self.pool.worker_count) + _LOGGER.info("Starting Home Assistant") self.state = CoreState.starting @@ -208,24 +185,24 @@ class HomeAssistant(object): # pylint: disable=protected-access self.loop._thread_ident = threading.get_ident() _async_create_timer(self) - _async_monitor_worker_pool(self) self.bus.async_fire(EVENT_HOMEASSISTANT_START) - yield from self.loop.run_in_executor(None, self.pool.block_till_done) + if self.pool is not None: + yield from self.loop.run_in_executor( + None, self.pool.block_till_done) self.state = CoreState.running - def add_job(self, - target: Callable[..., None], - *args: Any, - priority: JobPriority=JobPriority.EVENT_DEFAULT) -> None: + def add_job(self, target: Callable[..., None], *args: Any) -> None: """Add job to the worker pool. target: target to call. args: parameters for method to call. """ - self.pool.add_job(priority, (target,) + args) + if self.pool is None: + run_callback_threadsafe(self.pool, self.async_init_pool).result() + self.pool.add_job((target,) + args) @callback - def async_add_job(self, target: Callable[..., None], *args: Any): + def async_add_job(self, target: Callable[..., None], *args: Any) -> None: """Add a job from within the eventloop. This method must be run in the event loop. @@ -238,10 +215,12 @@ class HomeAssistant(object): elif asyncio.iscoroutinefunction(target): self.loop.create_task(target(*args)) else: - self.add_job(target, *args) + if self.pool is None: + self.async_init_pool() + self.pool.add_job((target,) + args) @callback - def async_run_job(self, target: Callable[..., None], *args: Any): + def async_run_job(self, target: Callable[..., None], *args: Any) -> None: """Run a job from within the event loop. This method must be run in the event loop. @@ -254,7 +233,7 @@ class HomeAssistant(object): else: self.async_add_job(target, *args) - def _loop_empty(self): + def _loop_empty(self) -> bool: """Python 3.4.2 empty loop compatibility function.""" # pylint: disable=protected-access if sys.version_info < (3, 4, 3): @@ -264,7 +243,7 @@ class HomeAssistant(object): return self.loop._current_handle is None and \ len(self.loop._ready) == 0 - def block_till_done(self): + def block_till_done(self) -> None: """Block till all pending work is done.""" complete = threading.Event() @@ -278,7 +257,8 @@ class HomeAssistant(object): count = 0 while True: # Wait for the work queue to empty - self.pool.block_till_done() + if self.pool is not None: + self.pool.block_till_done() # Verify the loop is empty if self._loop_empty(): @@ -309,8 +289,10 @@ class HomeAssistant(object): """ self.state = CoreState.stopping self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - yield from self.loop.run_in_executor(None, self.pool.block_till_done) - yield from self.loop.run_in_executor(None, self.pool.stop) + if self.pool is not None: + yield from self.loop.run_in_executor( + None, self.pool.block_till_done) + yield from self.loop.run_in_executor(None, self.pool.stop) self.executor.shutdown() if self._websession is not None: yield from self._websession.close() @@ -337,6 +319,12 @@ class HomeAssistant(object): exc_info=exc_info ) + @callback + def async_init_pool(self): + """Initialize the worker pool.""" + self.pool = create_worker_pool() + _async_monitor_worker_pool(self) + @callback def _async_stop_handler(self, *args): """Stop Home Assistant.""" @@ -867,10 +855,10 @@ class ServiceCall(object): class ServiceRegistry(object): """Offers services over the eventbus.""" - def __init__(self, bus, add_job, loop): + def __init__(self, bus, async_add_job, loop): """Initialize a service registry.""" self._services = {} - self._add_job = add_job + self._async_add_job = async_add_job self._bus = bus self._loop = loop self._cur_id = 0 @@ -1073,7 +1061,7 @@ class ServiceRegistry(object): service_handler.func(service_call) fire_service_executed() - self._add_job(execute_service, priority=JobPriority.EVENT_SERVICE) + self._async_add_job(execute_service) def _generate_unique_id(self): """Generate a unique service call id.""" diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 1f5a285a117..69ff5d7a61f 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -319,7 +319,7 @@ class ThreadPool(object): self._job_handler = job_handler self.worker_count = 0 - self._work_queue = queue.PriorityQueue() + self._work_queue = queue.Queue() self.current_jobs = [] self._quit_task = object() @@ -349,24 +349,24 @@ class ThreadPool(object): if not self.running: raise RuntimeError("ThreadPool not running") - self._work_queue.put(PriorityQueueItem(0, self._quit_task)) + self._work_queue.put(self._quit_task) self.worker_count -= 1 - def add_job(self, priority, job): + def add_job(self, job): """Add a job to the queue.""" if not self.running: raise RuntimeError("ThreadPool not running") - self._work_queue.put(PriorityQueueItem(priority, job)) + self._work_queue.put(job) def add_many_jobs(self, jobs): """Add a list of jobs to the queue.""" if not self.running: raise RuntimeError("ThreadPool not running") - for priority, job in jobs: - self._work_queue.put(PriorityQueueItem(priority, job)) + for job in jobs: + self._work_queue.put(job) def block_till_done(self): """Block till current work is done.""" @@ -392,7 +392,7 @@ class ThreadPool(object): """Handle jobs for the thread pool.""" while True: # Get new item from work_queue - job = self._work_queue.get().item + job = self._work_queue.get() if job is self._quit_task: self._work_queue.task_done() @@ -410,16 +410,3 @@ class ThreadPool(object): # Tell work_queue the task is done self._work_queue.task_done() - - -class PriorityQueueItem(object): - """Holds a priority and a value. Used within PriorityQueue.""" - - def __init__(self, priority, item): - """Initialize the queue.""" - self.priority = priority - self.item = item - - def __lt__(self, other): - """Return the ordering.""" - return self.priority < other.priority diff --git a/tests/common.py b/tests/common.py index 275beb6be94..af65a93f216 100644 --- a/tests/common.py +++ b/tests/common.py @@ -31,18 +31,12 @@ def get_test_config_dir(*add_path): return os.path.join(os.path.dirname(__file__), "testing_config", *add_path) -def get_test_home_assistant(num_threads=None): +def get_test_home_assistant(): """Return a Home Assistant object pointing at test config dir.""" loop = asyncio.new_event_loop() - if num_threads: - orig_num_threads = ha.MIN_WORKER_THREAD - ha.MIN_WORKER_THREAD = num_threads - hass = loop.run_until_complete(async_test_home_assistant(loop)) - - if num_threads: - ha.MIN_WORKER_THREAD = orig_num_threads + hass.allow_pool = True # FIXME should not be a daemon. Means hass.stop() not called in teardown stop_event = threading.Event() @@ -60,17 +54,10 @@ def get_test_home_assistant(num_threads=None): orig_start = hass.start orig_stop = hass.stop - @asyncio.coroutine - def fake_stop(): - """Fake stop.""" - yield None - - @patch.object(ha, '_async_create_timer') - @patch.object(ha, '_async_monitor_worker_pool') @patch.object(hass.loop, 'add_signal_handler') + @patch.object(ha, '_async_create_timer') @patch.object(hass.loop, 'run_forever') @patch.object(hass.loop, 'close') - @patch.object(hass, 'async_stop', return_value=fake_stop()) def start_hass(*mocks): """Helper to start hass.""" orig_start() @@ -108,6 +95,20 @@ def async_test_home_assistant(loop): hass.state = ha.CoreState.running + hass.allow_pool = False + orig_init = hass.async_init_pool + + @ha.callback + def mock_async_init_pool(): + """Prevent worker pool from being initialized.""" + if hass.allow_pool: + with patch('homeassistant.core._async_monitor_worker_pool'): + orig_init() + else: + assert False, 'Thread pool not allowed. Set hass.allow_pool = True' + + hass.async_init_pool = mock_async_init_pool + return hass @@ -225,7 +226,8 @@ class MockModule(object): # pylint: disable=invalid-name def __init__(self, domain=None, dependencies=None, setup=None, - requirements=None, config_schema=None, platform_schema=None): + requirements=None, config_schema=None, platform_schema=None, + async_setup=None): """Initialize the mock module.""" self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] @@ -238,8 +240,15 @@ class MockModule(object): if platform_schema is not None: self.PLATFORM_SCHEMA = platform_schema + if async_setup is not None: + self.async_setup = async_setup + def setup(self, hass, config): - """Setup the component.""" + """Setup the component. + + We always define this mock because MagicMock setups will be seen by the + executor as a coroutine, raising an exception. + """ if self._setup is not None: return self._setup(hass, config) return True diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index e2ce9c15936..fde4bb2fbd4 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -8,6 +8,7 @@ from homeassistant.bootstrap import setup_component @asyncio.coroutine def test_fetching_url(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" + hass.allow_pool = True aioclient_mock.get('http://example.com', text='hello world') def setup_platform(): @@ -39,6 +40,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): @asyncio.coroutine def test_limit_refetch(aioclient_mock, hass, test_client): """Test that it fetches the given url.""" + hass.allow_pool = True aioclient_mock.get('http://example.com/5a', text='hello world') aioclient_mock.get('http://example.com/10a', text='hello world') aioclient_mock.get('http://example.com/15a', text='hello planet') diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index d43c138c570..9a692b0a4ee 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -14,6 +14,8 @@ from tests.common import assert_setup_component, mock_http_component @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" + hass.allow_pool = True + @mock.patch('os.path.isfile', mock.Mock(return_value=True)) @mock.patch('os.access', mock.Mock(return_value=True)) def setup_platform(): diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index aa94bdf63c9..04fc2e33247 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -86,7 +86,7 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(24.0, state.attributes.get('target_temp_high')) climate.set_temperature(self.hass, target_temp_high=25, target_temp_low=20, entity_id=ENTITY_ECOBEE) - self.hass.pool.block_till_done() + self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(20.0, state.attributes.get('target_temp_low')) @@ -102,7 +102,7 @@ class TestDemoClimate(unittest.TestCase): climate.set_temperature(self.hass, temperature=None, entity_id=ENTITY_ECOBEE, target_temp_low=None, target_temp_high=None) - self.hass.pool.block_till_done() + self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) diff --git a/tests/components/cover/test_rfxtrx.py b/tests/components/cover/test_rfxtrx.py index 85ff26145ed..5f6ecd01e4e 100644 --- a/tests/components/cover/test_rfxtrx.py +++ b/tests/components/cover/test_rfxtrx.py @@ -15,7 +15,7 @@ class TestCoverRfxtrx(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() self.hass.config.components = ['rfxtrx'] def tearDown(self): diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index abb7cc2ac12..759127c75f9 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -30,7 +30,7 @@ class TestDemoClimate(unittest.TestCase): """Test light state attributes.""" light.turn_on( self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25) - self.hass.pool.block_till_done() + self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) self.assertEqual((.4, .6), state.attributes.get(light.ATTR_XY_COLOR)) @@ -40,21 +40,21 @@ class TestDemoClimate(unittest.TestCase): light.turn_on( self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), white_value=254) - self.hass.pool.block_till_done() + self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) self.assertEqual( (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400) - self.hass.pool.block_till_done() + self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) def test_turn_off(self): """Test light turn off method.""" light.turn_on(self.hass, ENTITY_LIGHT) - self.hass.pool.block_till_done() + self.hass.block_till_done() self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) light.turn_off(self.hass, ENTITY_LIGHT) - self.hass.pool.block_till_done() + self.hass.block_till_done() self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index c87e562c4ff..6a9311b7892 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -15,7 +15,7 @@ class TestLightRfxtrx(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() self.hass.config.components = ['rfxtrx'] def tearDown(self): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 5b65df9e1da..9626f1a878b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -21,7 +21,7 @@ class TestMQTT(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(1) + self.hass = get_test_home_assistant() mock_mqtt_component(self.hass) self.calls = [] @@ -217,7 +217,7 @@ class TestMQTTCallbacks(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(1) + self.hass = get_test_home_assistant() # mock_mqtt_component(self.hass) with mock.patch('paho.mqtt.client.Client'): diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 3ec00a84bda..61baabed69f 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -111,7 +111,7 @@ class TestNotifyDemo(unittest.TestCase): } script.call_from_config(self.hass, conf) - self.hass.pool.block_till_done() + self.hass.block_till_done() self.assertTrue(len(self.events) == 1) assert { 'message': 'Test 123 4', diff --git a/tests/components/sensor/test_imap_email_content.py b/tests/components/sensor/test_imap_email_content.py index 1f0b81ce8eb..17619f1efa6 100644 --- a/tests/components/sensor/test_imap_email_content.py +++ b/tests/components/sensor/test_imap_email_content.py @@ -178,7 +178,7 @@ class EmailContentSensor(unittest.TestCase): sensor.entity_id = "sensor.emailtest" sensor.update() - self.hass.pool.block_till_done() + self.hass.block_till_done() states_received.wait(5) self.assertEqual("Test Message", states[0].state) diff --git a/tests/components/sensor/test_random.py b/tests/components/sensor/test_random.py index 3e66d5003ce..902edfc3ee4 100644 --- a/tests/components/sensor/test_random.py +++ b/tests/components/sensor/test_random.py @@ -33,4 +33,4 @@ class TestRandomSensor(unittest.TestCase): state = self.hass.states.get('sensor.test') self.assertLessEqual(int(state.state), config['sensor']['maximum']) - self.assertGreater(int(state.state), config['sensor']['minimum']) + self.assertGreaterEqual(int(state.state), config['sensor']['minimum']) diff --git a/tests/components/sensor/test_rfxtrx.py b/tests/components/sensor/test_rfxtrx.py index 1de6cf19419..e70f8b5641d 100644 --- a/tests/components/sensor/test_rfxtrx.py +++ b/tests/components/sensor/test_rfxtrx.py @@ -16,7 +16,7 @@ class TestSensorRfxtrx(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() self.hass.config.components = ['rfxtrx'] def tearDown(self): diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/switch/test_rfxtrx.py index b45342336e3..f0d38ca20c3 100644 --- a/tests/components/switch/test_rfxtrx.py +++ b/tests/components/switch/test_rfxtrx.py @@ -15,7 +15,7 @@ class TestSwitchRfxtrx(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() self.hass.config.components = ['rfxtrx'] def tearDown(self): diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 454b088dc5a..1172221f16f 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -19,7 +19,7 @@ class TestConversation(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.ent_id = 'light.kitchen_lights' - self.hass = get_test_home_assistant(3) + self.hass = get_test_home_assistant() self.hass.states.set(self.ent_id, 'on') self.assertTrue(run_coroutine_threadsafe( core_components.async_setup(self.hass, {}), self.hass.loop diff --git a/tests/components/test_emulated_hue.py b/tests/components/test_emulated_hue.py index e280ba827ea..ef4ea7f234e 100755 --- a/tests/components/test_emulated_hue.py +++ b/tests/components/test_emulated_hue.py @@ -1,8 +1,6 @@ """The tests for the emulated Hue component.""" import time import json -import threading -import asyncio import unittest import requests @@ -372,58 +370,3 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): url, data=json.dumps(data), timeout=5, headers=req_headers) return result - - -class MQTTBroker(object): - """Encapsulates an embedded MQTT broker.""" - - def __init__(self, host, port): - """Initialize a new instance.""" - from hbmqtt.broker import Broker - - self._loop = asyncio.new_event_loop() - - hbmqtt_config = { - 'listeners': { - 'default': { - 'max-connections': 50000, - 'type': 'tcp', - 'bind': '{}:{}'.format(host, port) - } - }, - 'auth': { - 'plugins': ['auth.anonymous'], - 'allow-anonymous': True - } - } - - self._broker = Broker(config=hbmqtt_config, loop=self._loop) - - self._thread = threading.Thread(target=self._run_loop) - self._started_ev = threading.Event() - - def start(self): - """Start the broker.""" - self._thread.start() - self._started_ev.wait() - - def stop(self): - """Stop the broker.""" - self._loop.call_soon_threadsafe(asyncio.async, self._broker.shutdown()) - self._loop.call_soon_threadsafe(self._loop.stop) - self._thread.join() - - def _run_loop(self): - """Run the loop.""" - asyncio.set_event_loop(self._loop) - self._loop.run_until_complete(self._broker_coroutine()) - - self._started_ev.set() - - self._loop.run_forever() - self._loop.close() - - @asyncio.coroutine - def _broker_coroutine(self): - """The Broker coroutine.""" - yield from self._broker.start() diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index b0517ec2f53..de90e86c0bf 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -17,7 +17,7 @@ class TestInfluxDB(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(2) + self.hass = get_test_home_assistant() self.handler_method = None self.hass.bus.listen = mock.Mock() diff --git a/tests/components/test_logentries.py b/tests/components/test_logentries.py index b7e40f7ebb6..5d3a9d79f97 100644 --- a/tests/components/test_logentries.py +++ b/tests/components/test_logentries.py @@ -15,7 +15,7 @@ class TestLogentries(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(2) + self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" diff --git a/tests/components/test_logger.py b/tests/components/test_logger.py index 6b290eec638..e4e8c75d1bd 100644 --- a/tests/components/test_logger.py +++ b/tests/components/test_logger.py @@ -16,7 +16,7 @@ class TestUpdater(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(2) + self.hass = get_test_home_assistant() self.log_config = {'logger': {'default': 'warning', 'logs': {'test': 'info'}}} diff --git a/tests/components/test_splunk.py b/tests/components/test_splunk.py index 1f6648ce582..78720850317 100644 --- a/tests/components/test_splunk.py +++ b/tests/components/test_splunk.py @@ -14,7 +14,7 @@ class TestSplunk(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(2) + self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" diff --git a/tests/components/test_statsd.py b/tests/components/test_statsd.py index eb8782b582c..b0cba0e41f9 100644 --- a/tests/components/test_statsd.py +++ b/tests/components/test_statsd.py @@ -17,7 +17,7 @@ class TestStatsd(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(2) + self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" diff --git a/tests/test_core.py b/tests/test_core.py index 3c7cfd32ef7..8a9fb8f6d4a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -56,7 +56,7 @@ def test_async_add_job_add_threaded_job_to_pool(mock_iscoro): ha.HomeAssistant.async_add_job(hass, job) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 0 - assert len(hass.add_job.mock_calls) == 1 + assert len(hass.pool.add_job.mock_calls) == 1 def test_async_run_job_calls_callback(): @@ -91,7 +91,7 @@ class TestHomeAssistant(unittest.TestCase): # pylint: disable=invalid-name def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() # pylint: disable=invalid-name def tearDown(self): @@ -169,7 +169,6 @@ class TestEventBus(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.bus = self.hass.bus - self.bus.listen('test_event', lambda x: len) # pylint: disable=invalid-name def tearDown(self): @@ -178,6 +177,7 @@ class TestEventBus(unittest.TestCase): def test_add_remove_listener(self): """Test remove_listener method.""" + self.hass.allow_pool = False old_count = len(self.bus.listeners) def listener(_): pass @@ -195,8 +195,10 @@ class TestEventBus(unittest.TestCase): def test_unsubscribe_listener(self): """Test unsubscribe listener from returned function.""" + self.hass.allow_pool = False calls = [] + @ha.callback def listener(event): """Mock listener.""" calls.append(event) @@ -217,6 +219,7 @@ class TestEventBus(unittest.TestCase): def test_listen_once_event_with_callback(self): """Test listen_once_event method.""" + self.hass.allow_pool = False runs = [] @ha.callback @@ -234,6 +237,7 @@ class TestEventBus(unittest.TestCase): def test_listen_once_event_with_coroutine(self): """Test listen_once_event method.""" + self.hass.allow_pool = False runs = [] @asyncio.coroutine @@ -279,6 +283,7 @@ class TestEventBus(unittest.TestCase): def test_callback_event_listener(self): """Test a event listener listeners.""" + self.hass.allow_pool = False callback_calls = [] @ha.callback @@ -292,6 +297,7 @@ class TestEventBus(unittest.TestCase): def test_coroutine_event_listener(self): """Test a event listener listeners.""" + self.hass.allow_pool = False coroutine_calls = [] @asyncio.coroutine @@ -366,10 +372,11 @@ class TestStateMachine(unittest.TestCase): # pylint: disable=invalid-name def setUp(self): """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant(0) + self.hass = get_test_home_assistant() self.states = self.hass.states self.states.set("light.Bowl", "on") self.states.set("switch.AC", "off") + self.hass.allow_pool = False # pylint: disable=invalid-name def tearDown(self): @@ -413,8 +420,12 @@ class TestStateMachine(unittest.TestCase): def test_remove(self): """Test remove method.""" events = [] - self.hass.bus.listen(EVENT_STATE_CHANGED, - lambda event: events.append(event)) + + @ha.callback + def callback(event): + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) self.assertIn('light.bowl', self.states.entity_ids()) self.assertTrue(self.states.remove('light.bowl')) @@ -436,8 +447,11 @@ class TestStateMachine(unittest.TestCase): """Test insensitivty.""" runs = [] - self.hass.bus.listen(EVENT_STATE_CHANGED, - lambda event: runs.append(event)) + @ha.callback + def callback(event): + runs.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) self.states.set('light.BOWL', 'off') self.hass.block_till_done() @@ -462,7 +476,12 @@ class TestStateMachine(unittest.TestCase): def test_force_update(self): """Test force update option.""" events = [] - self.hass.bus.listen(EVENT_STATE_CHANGED, lambda ev: events.append(ev)) + + @ha.callback + def callback(event): + events.append(event) + + self.hass.bus.listen(EVENT_STATE_CHANGED, callback) self.states.set('light.bowl', 'on') self.hass.block_till_done() @@ -504,6 +523,7 @@ class TestServiceRegistry(unittest.TestCase): def test_has_service(self): """Test has_service method.""" + self.hass.allow_pool = False self.assertTrue( self.services.has_service("tesT_domaiN", "tesT_servicE")) self.assertFalse( @@ -513,6 +533,7 @@ class TestServiceRegistry(unittest.TestCase): def test_services(self): """Test services.""" + self.hass.allow_pool = False expected = { 'test_domain': {'test_service': {'description': '', 'fields': {}}} } @@ -535,6 +556,7 @@ class TestServiceRegistry(unittest.TestCase): def test_call_non_existing_with_blocking(self): """Test non-existing with blocking.""" + self.hass.allow_pool = False prior = ha.SERVICE_CALL_LIMIT try: ha.SERVICE_CALL_LIMIT = 0.01 @@ -545,6 +567,7 @@ class TestServiceRegistry(unittest.TestCase): def test_async_service(self): """Test registering and calling an async service.""" + self.hass.allow_pool = False calls = [] @asyncio.coroutine @@ -561,6 +584,7 @@ class TestServiceRegistry(unittest.TestCase): def test_callback_service(self): """Test registering and calling an async service.""" + self.hass.allow_pool = False calls = [] @ha.callback @@ -629,8 +653,9 @@ class TestWorkerPool(unittest.TestCase): def register_call(_): calls.append(1) - pool.add_job(ha.JobPriority.EVENT_DEFAULT, (malicious_job, None)) - pool.add_job(ha.JobPriority.EVENT_DEFAULT, (register_call, None)) + pool.add_job((malicious_job, None)) + pool.block_till_done() + pool.add_job((register_call, None)) pool.block_till_done() self.assertEqual(1, len(calls)) From 1d9ac5f8b3189b4448055acae2db38349fa7cd1c Mon Sep 17 00:00:00 2001 From: Nicholas Sideras Date: Mon, 31 Oct 2016 15:04:54 -0500 Subject: [PATCH 102/149] Update __init__.py (#4155) Changed manifest.json to respect Android screen rotate lock. --- homeassistant/components/frontend/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d95ba8f981f..a96e871c42f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -27,7 +27,6 @@ MANIFEST_JSON = { "icons": [], "lang": "en-US", "name": "Home Assistant", - "orientation": "any", "short_name": "Assistant", "start_url": "/", "theme_color": "#03A9F4" From 0211cf29eb39af3f98f4efc387d144d00c724c4e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 1 Nov 2016 10:43:48 +0100 Subject: [PATCH 103/149] Change behavior to be more natural and fix test (#4150) --- homeassistant/components/sensor/random.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py index bb06f131f2f..5a48ea40863 100644 --- a/homeassistant/components/sensor/random.py +++ b/homeassistant/components/sensor/random.py @@ -70,4 +70,4 @@ class RandomSensor(Entity): def async_update(self): """Get a new number and updates the states.""" from random import randrange - self._state = randrange(self._minimum, self._maximum) + self._state = randrange(self._minimum, self._maximum + 1) From dad54bb99343ef2114d8822a46a40cb9cbefca0f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 1 Nov 2016 15:11:42 +0100 Subject: [PATCH 104/149] Update to make the sample file validate (#4168) --- config/configuration.yaml.example | 80 +++++++++---------------------- 1 file changed, 22 insertions(+), 58 deletions(-) diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 73c03f94144..08b0324371f 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -1,7 +1,7 @@ homeassistant: # Omitted values in this section will be auto detected using freegeoip.io - # Location required to calculate the time the sun rises and sets. + # Location required to calculate the time the sun rises and sets. # Coordinates are also used for location for weather related components. # Google Maps can be used to determine more precise GPS coordinates. latitude: 32.87336 @@ -43,12 +43,10 @@ device_tracker: username: admin password: PASSWORD -chromecast: - switch: platform: wemo -thermostat: +climate: platform: nest # Required: username and password that are used to login to the Nest thermostat. username: myemail@mydomain.com @@ -79,7 +77,6 @@ group: entities: - group.awesome_people - group.climate - kitchen: name: Kitchen entities: @@ -92,52 +89,23 @@ group: - input_boolean.notify_home - camera.demo_camera -example: - -simple_alarm: - # Which light/light group has to flash when a known device comes home - known_light: light.Bowl - # Which light/light group has to flash red when light turns on while no one home - unknown_light: group.living_room - browser: - keyboard: # https://home-assistant.io/getting-started/automation/ automation: -- alias: 'Rule 1 Light on in the evening' - trigger: - - platform: sun + - alias: Turn on light when sun sets + trigger: + platform: sun event: sunset offset: "-01:00:00" - - platform: state + condition: + condition: state entity_id: group.all_devices - state: home - condition: - - platform: state - entity_id: group.all_devices - state: home - - platform: time - after: "16:00:00" - before: "23:00:00" - action: - service: homeassistant.turn_on - entity_id: group.living_room + state: 'home' + action: + service: light.turn_on -- alias: 'Rule 2 - Away Mode' - trigger: - - platform: state - entity_id: group.all_devices - state: 'not_home' - - condition: use_trigger_values - action: - service: light.turn_off - entity_id: group.all_lights - -# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc. -# Each sensor label should be unique or your sensors might not load correctly. # Another way to do is to collect all entries under one "sensor:" # sensor: # - platform: mqtt @@ -154,34 +122,30 @@ sensor: arg: '/' - type: 'disk_use_percent' arg: '/home' - - type: 'disk_use' - arg: '/home' sensor 2: - platform: forecast - api_key: - monitored_conditions: - - summary - - precip_type - - precip_intensity - - temperature + platform: cpuspeed script: - # Turns on the bedroom lights and then the living room lights 1 minute later wakeup: alias: Wake Up sequence: - # alias is optional + - event: LOGBOOK_ENTRY + event_data: + name: Paulus + message: is waking up + entity_id: device_tracker.paulus + domain: light - alias: Bedroom lights on - execute_service: light.turn_on - service_data: + service: light.turn_on + data: entity_id: group.bedroom + brightness: 100 - delay: - # supports seconds, milliseconds, minutes, hours, etc. minutes: 1 - alias: Living room lights on - execute_service: light.turn_on - service_data: + service: light.turn_on + data: entity_id: group.living_room scene: From c549ea115d0fd8f90cc2092d0700bf86a7cdcfac Mon Sep 17 00:00:00 2001 From: Bjarni Ivarsson Date: Tue, 1 Nov 2016 18:42:38 +0100 Subject: [PATCH 105/149] Sonos responsiveness improvements + enhancements (#4063) * Sonos responsiveness improvements (async_ coroutines, event based updating, album art caching) + Better radio station information * Docstring fixes. * Docstring fixes. * Updated SoCo dependency + fixed file permissions. * Only fetch speaker info if needed. * PEP8 fixes * Fixed SoCoMock.get_speaker_info to get test to pass. * Regenerated requirements_all.txt + async fetching of album art with caching + added http_session to HomeAssistant object. * Unit test fixed. * Add blank line as per flake8 * Fixed media image proxy unit test. * Removed async stuff. * Removed last remnants of async stuff. --- .../components/media_player/__init__.py | 77 +++- .../components/media_player/sonos.py | 371 ++++++++++++++---- requirements_all.txt | 6 +- tests/components/media_player/test_demo.py | 33 +- tests/components/media_player/test_sonos.py | 2 +- 5 files changed, 390 insertions(+), 99 deletions(-) mode change 100755 => 100644 homeassistant/components/media_player/sonos.py mode change 100755 => 100644 tests/components/media_player/test_sonos.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index cd135803ad9..7988064183a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -8,9 +8,9 @@ import asyncio import hashlib import logging import os -import requests from aiohttp import web +import async_timeout import voluptuous as vol from homeassistant.config import load_yaml_config_file @@ -19,6 +19,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.const import ( STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -36,6 +37,16 @@ SCAN_INTERVAL = 10 ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}&cache={2}' +ATTR_CACHE_IMAGES = 'images' +ATTR_CACHE_URLS = 'urls' +ATTR_CACHE_MAXSIZE = 'maxsize' +ENTITY_IMAGE_CACHE = { + ATTR_CACHE_IMAGES: {}, + ATTR_CACHE_URLS: [], + ATTR_CACHE_MAXSIZE: 16 +} + +CONTENT_TYPE_HEADER = 'Content-Type' SERVICE_PLAY_MEDIA = 'play_media' SERVICE_SELECT_SOURCE = 'select_source' @@ -672,6 +683,51 @@ class MediaPlayerDevice(Entity): return state_attr + def preload_media_image_url(self, url): + """Preload and cache a media image for future use.""" + run_coroutine_threadsafe( + _async_fetch_image(self.hass, url), self.hass.loop + ).result() + + +@asyncio.coroutine +def _async_fetch_image(hass, url): + """Helper method to fetch image. + + Images are cached in memory (the images are typically 10-100kB in size). + """ + cache_images = ENTITY_IMAGE_CACHE[ATTR_CACHE_IMAGES] + cache_urls = ENTITY_IMAGE_CACHE[ATTR_CACHE_URLS] + cache_maxsize = ENTITY_IMAGE_CACHE[ATTR_CACHE_MAXSIZE] + + if url in cache_images: + return cache_images[url] + + content, content_type = (None, None) + try: + with async_timeout.timeout(10, loop=hass.loop): + response = yield from hass.websession.get(url) + if response.status == 200: + content = yield from response.read() + content_type = response.headers.get(CONTENT_TYPE_HEADER) + hass.loop.create_task(response.release()) + except asyncio.TimeoutError: + pass + + if content: + cache_images[url] = (content, content_type) + cache_urls.append(url) + + while len(cache_urls) > cache_maxsize: + # remove oldest item from cache + oldest_url = cache_urls[0] + if oldest_url in cache_images: + del cache_images[oldest_url] + + cache_urls = cache_urls[1:] + + return content, content_type + class MediaPlayerImageView(HomeAssistantView): """Media player view to serve an image.""" @@ -698,21 +754,10 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return web.Response(status=401) - image_url = player.media_image_url + data, content_type = yield from _async_fetch_image( + self.hass, player.media_image_url) - if image_url is None: - return web.Response(status=404) - - def fetch_image(): - """Helper method to fetch image.""" - try: - return requests.get(image_url).content - except requests.RequestException: - return None - - response = yield from self.hass.loop.run_in_executor(None, fetch_image) - - if response is None: + if data is None: return web.Response(status=500) - return web.Response(body=response) + return web.Response(body=data, content_type=content_type) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py old mode 100755 new mode 100644 index f8e9f36c1e8..76d33feeb1b --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -17,12 +17,14 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF, - ATTR_ENTITY_ID) + STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['SoCo==0.12'] +REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/' + 'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#' + 'SoCo==0.12'] + _LOGGER = logging.getLogger(__name__) @@ -223,6 +225,29 @@ def only_if_coordinator(func): return wrapper +def _parse_timespan(timespan): + """Parse a time-span into number of seconds.""" + if timespan in ('', 'NOT_IMPLEMENTED', None): + return None + else: + return sum(60 ** x[0] * int(x[1]) for x in enumerate( + reversed(timespan.split(':')))) + + +# pylint: disable=too-few-public-methods +class _ProcessSonosEventQueue(): + """Queue like object for dispatching sonos events.""" + + def __init__(self, sonos_device): + self._sonos_device = sonos_device + + def put(self, item, block=True, timeout=None): + """Queue up event for processing.""" + # Instead of putting events on a queue, dispatch them to the event + # processing method. + self._sonos_device.process_sonos_event(item) + + # pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """Representation of a Sonos device.""" @@ -234,8 +259,11 @@ class SonosDevice(MediaPlayerDevice): self.hass = hass self.volume_increment = 5 self._player = player + self._player_volume = None + self._player_volume_muted = None self._speaker_info = None self._name = None + self._status = None self._coordinator = None self._media_content_id = None self._media_duration = None @@ -243,6 +271,15 @@ class SonosDevice(MediaPlayerDevice): self._media_artist = None self._media_album_name = None self._media_title = None + self._media_radio_show = None + self._media_next_title = None + self._support_previous_track = False + self._support_next_track = False + self._support_pause = False + self._current_track_uri = None + self._current_track_is_radio_stream = False + self._queue = None + self._last_avtransport_event = None self.update() self.soco_snapshot = Snapshot(self._player) @@ -268,36 +305,95 @@ class SonosDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - if self._status == 'PAUSED_PLAYBACK': + if self._coordinator: + return self._coordinator.state + if self._status in ('PAUSED_PLAYBACK', 'STOPPED'): return STATE_PAUSED - if self._status == 'PLAYING': + if self._status in ('PLAYING', 'TRANSITIONING'): return STATE_PLAYING - if self._status == 'STOPPED': - return STATE_IDLE if self._status == 'OFF': return STATE_OFF - return STATE_UNKNOWN + return STATE_IDLE @property def is_coordinator(self): """Return true if player is a coordinator.""" - return self._player.is_coordinator + return self._coordinator is None + def _is_available(self): + try: + sock = socket.create_connection( + address=(self._player.ip_address, 1443), + timeout=3) + sock.close() + return True + except socket.error: + return False + + # pylint: disable=invalid-name + def _subscribe_to_player_events(self): + if self._queue is None: + self._queue = _ProcessSonosEventQueue(self) + self._player.avTransport.subscribe( + auto_renew=True, + event_queue=self._queue) + self._player.renderingControl.subscribe( + auto_renew=True, + event_queue=self._queue) + + # pylint: disable=too-many-branches, too-many-statements def update(self): """Retrieve latest state.""" - self._speaker_info = self._player.get_speaker_info() - self._name = self._speaker_info['zone_name'].replace( - ' (R)', '').replace(' (L)', '') + if self._speaker_info is None: + self._speaker_info = self._player.get_speaker_info(True) + self._name = self._speaker_info['zone_name'].replace( + ' (R)', '').replace(' (L)', '') - if self.available: - self._status = self._player.get_current_transport_info().get( - 'current_transport_state') - trackinfo = self._player.get_current_track_info() + if self._last_avtransport_event: + is_available = True + else: + is_available = self._is_available() - if trackinfo['uri'].startswith('x-rincon:'): + if is_available: + + if self._queue is None or self._player_volume is None: + self._player_volume = self._player.volume + + if self._queue is None or self._player_volume_muted is None: + self._player_volume_muted = self._player.mute + + track_info = None + if self._last_avtransport_event: + variables = self._last_avtransport_event.variables + current_track_metadata = variables.get( + 'current_track_meta_data', {} + ) + + self._status = variables.get('transport_state') + + if current_track_metadata: + # no need to ask speaker for information we already have + current_track_metadata = current_track_metadata.__dict__ + + track_info = { + 'uri': variables.get('current_track_uri'), + 'artist': current_track_metadata.get('creator'), + 'album': current_track_metadata.get('album'), + 'title': current_track_metadata.get('title'), + 'playlist_position': variables.get('current_track'), + 'duration': variables.get('current_track_duration') + } + else: + transport_info = self._player.get_current_transport_info() + self._status = transport_info.get('current_transport_state') + + if not track_info: + track_info = self._player.get_current_track_info() + + if track_info['uri'].startswith('x-rincon:'): # this speaker is a slave, find the coordinator # the uri of the track is 'x-rincon:{coordinator-id}' - coordinator_id = trackinfo['uri'][9:] + coordinator_id = track_info['uri'][9:] coordinators = [device for device in DEVICES if device.unique_id == coordinator_id] self._coordinator = coordinators[0] if coordinators else None @@ -305,39 +401,44 @@ class SonosDevice(MediaPlayerDevice): self._coordinator = None if not self._coordinator: - mediainfo = self._player.avTransport.GetMediaInfo([ - ('InstanceID', 0) - ]) + media_info = self._player.avTransport.GetMediaInfo( + [('InstanceID', 0)] + ) - duration = trackinfo.get('duration', '0:00') - # if the speaker is playing from the "line-in" source, getting - # track metadata can return NOT_IMPLEMENTED, which breaks the - # volume logic below - if duration == 'NOT_IMPLEMENTED': - duration = None - else: - duration = sum(60 ** x[0] * int(x[1]) for x in enumerate( - reversed(duration.split(':')))) + current_media_uri = media_info['CurrentURI'] + media_artist = track_info.get('artist') + media_album_name = track_info.get('album') + media_title = track_info.get('title') - media_image_url = trackinfo.get('album_art', None) - media_artist = trackinfo.get('artist', None) - media_album_name = trackinfo.get('album', None) - media_title = trackinfo.get('title', None) + is_radio_stream = \ + current_media_uri.startswith('x-sonosapi-stream:') or \ + current_media_uri.startswith('x-rincon-mp3radio:') - if media_image_url in ('', 'NOT_IMPLEMENTED', None): - # fallback to asking the speaker directly - media_image_url = \ - 'http://{host}:{port}/getaa?s=1&u={uri}'.format( - host=self._player.ip_address, - port=1400, - uri=urllib.parse.quote(mediainfo['CurrentURI']) + if is_radio_stream: + is_radio_stream = True + media_image_url = self._format_media_image_url( + current_media_uri + ) + support_previous_track = False + support_next_track = False + support_pause = False + + # for radio streams we set the radio station name as the + # title. + if media_artist and media_title: + # artist and album name are in the data, concatenate + # that do display as artist. + # "Information" field in the sonos pc app + + media_artist = '{artist} - {title}'.format( + artist=media_artist, + title=media_title ) + else: + # "On Now" field in the sonos pc app + media_artist = self._media_radio_show - if media_artist in ('', 'NOT_IMPLEMENTED', None): - # if listening to a radio stream the media_artist field - # will be empty and the title field will contain the - # filename that is being streamed - current_uri_metadata = mediainfo["CurrentURIMetaData"] + current_uri_metadata = media_info["CurrentURIMetaData"] if current_uri_metadata not in \ ('', 'NOT_IMPLEMENTED', None): @@ -350,16 +451,80 @@ class SonosDevice(MediaPlayerDevice): './/{http://purl.org/dc/elements/1.1/}title') if md_title not in ('', 'NOT_IMPLEMENTED', None): - media_artist = '' media_title = md_title - self._media_content_id = trackinfo.get('title', None) - self._media_duration = duration + if media_artist and media_title: + # some radio stations put their name into the artist + # name, e.g.: + # media_title = "Station" + # media_artist = "Station - Artist - Title" + # detect this case and trim from the front of + # media_artist for cosmetics + str_to_trim = '{title} - '.format( + title=media_title + ) + chars = min(len(media_artist), len(str_to_trim)) + + if media_artist[:chars].upper() == \ + str_to_trim[:chars].upper(): + + media_artist = media_artist[chars:] + + else: + # not a radio stream + media_image_url = self._format_media_image_url( + track_info['uri'] + ) + support_previous_track = True + support_next_track = True + support_pause = True + + playlist_position = track_info.get('playlist_position') + if playlist_position in ('', 'NOT_IMPLEMENTED', None): + playlist_position = None + else: + playlist_position = int(playlist_position) + + playlist_size = media_info.get('NrTracks') + if playlist_size in ('', 'NOT_IMPLEMENTED', None): + playlist_size = None + else: + playlist_size = int(playlist_size) + + if playlist_position is not None and \ + playlist_size is not None: + + if playlist_position == 1: + support_previous_track = False + + if playlist_position == playlist_size: + support_next_track = False + + self._media_content_id = track_info.get('title') + self._media_duration = _parse_timespan( + track_info.get('duration') + ) self._media_image_url = media_image_url self._media_artist = media_artist self._media_album_name = media_album_name self._media_title = media_title + self._current_track_uri = track_info['uri'] + self._current_track_is_radio_stream = is_radio_stream + self._support_previous_track = support_previous_track + self._support_next_track = support_next_track + self._support_pause = support_pause + + # update state of the whole group + # pylint: disable=protected-access + for device in [x for x in DEVICES if x._coordinator == self]: + if device.entity_id: + device.update_ha_state(False) + + if self._queue is None and self.entity_id: + self._subscribe_to_player_events() else: + self._player_volume = None + self._player_volume_muted = None self._status = 'OFF' self._coordinator = None self._media_content_id = None @@ -368,16 +533,79 @@ class SonosDevice(MediaPlayerDevice): self._media_artist = None self._media_album_name = None self._media_title = None + self._media_radio_show = None + self._media_next_title = None + self._current_track_uri = None + self._current_track_is_radio_stream = False + self._support_previous_track = False + self._support_next_track = False + self._support_pause = False + + self._last_avtransport_event = None + + def _format_media_image_url(self, uri): + return 'http://{host}:{port}/getaa?s=1&u={uri}'.format( + host=self._player.ip_address, + port=1400, + uri=urllib.parse.quote(uri) + ) + + def process_sonos_event(self, event): + """Process a service event coming from the speaker.""" + next_track_image_url = None + if event.service == self._player.avTransport: + self._last_avtransport_event = event + + self._media_radio_show = None + if self._current_track_is_radio_stream: + current_track_metadata = event.variables.get( + 'current_track_meta_data' + ) + if current_track_metadata: + self._media_radio_show = \ + current_track_metadata.radio_show.split(',')[0] + + next_track_uri = event.variables.get('next_track_uri') + if next_track_uri: + next_track_image_url = self._format_media_image_url( + next_track_uri + ) + + next_track_metadata = event.variables.get('next_track_meta_data') + if next_track_metadata: + next_track = '{title} - {creator}'.format( + title=next_track_metadata.title, + creator=next_track_metadata.creator + ) + if next_track != self._media_next_title: + self._media_next_title = next_track + else: + self._media_next_title = None + + elif event.service == self._player.renderingControl: + if 'volume' in event.variables: + self._player_volume = int( + event.variables['volume'].get('Master') + ) + + if 'mute' in event.variables: + self._player_volume_muted = \ + event.variables['mute'].get('Master') == '1' + + self.update_ha_state(True) + + if next_track_image_url: + self.preload_media_image_url(next_track_image_url) @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._player.volume / 100.0 + return self._player_volume / 100.0 @property def is_volume_muted(self): """Return true if volume is muted.""" - return self._player.mute + return self._player_volume_muted @property def media_content_id(self): @@ -427,11 +655,6 @@ class SonosDevice(MediaPlayerDevice): @property def media_title(self): """Title of current playing media.""" - if self._player.is_playing_line_in: - return SUPPORT_SOURCE_LINEIN - if self._player.is_playing_tv: - return SUPPORT_SOURCE_TV - if self._coordinator: return self._coordinator.media_title else: @@ -440,11 +663,25 @@ class SonosDevice(MediaPlayerDevice): @property def supported_media_commands(self): """Flag of media commands that are supported.""" + if self._coordinator: + return self._coordinator.supported_media_commands + + supported = SUPPORT_SONOS + if not self.source_list: # some devices do not allow source selection - return SUPPORT_SONOS ^ SUPPORT_SELECT_SOURCE + supported = supported ^ SUPPORT_SELECT_SOURCE - return SUPPORT_SONOS + if not self._support_previous_track: + supported = supported ^ SUPPORT_PREVIOUS_TRACK + + if not self._support_next_track: + supported = supported ^ SUPPORT_NEXT_TRACK + + if not self._support_pause: + supported = supported ^ SUPPORT_PAUSE + + return supported def volume_up(self): """Volume up media player.""" @@ -489,10 +726,9 @@ class SonosDevice(MediaPlayerDevice): return None - @only_if_coordinator def turn_off(self): """Turn off media player.""" - self._player.pause() + self.media_pause() def media_play(self): """Send play command.""" @@ -536,10 +772,9 @@ class SonosDevice(MediaPlayerDevice): else: self._player.clear_queue() - @only_if_coordinator def turn_on(self): """Turn the media player on.""" - self._player.play() + self.media_play() def play_media(self, media_type, media_id, **kwargs): """ @@ -592,15 +827,3 @@ class SonosDevice(MediaPlayerDevice): def clear_sleep_timer(self): """Clear the timer on the player.""" self._player.set_sleep_timer(None) - - @property - def available(self): - """Return True if player is reachable, False otherwise.""" - try: - sock = socket.create_connection( - address=(self._player.ip_address, 1443), - timeout=3) - sock.close() - return True - except socket.error: - return False diff --git a/requirements_all.txt b/requirements_all.txt index 0c296981896..4a26817b2e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,9 +24,6 @@ PyMata==2.13 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 -# homeassistant.components.media_player.sonos -SoCo==0.12 - # homeassistant.components.notify.twitter TwitterAPI==2.4.2 @@ -154,6 +151,9 @@ https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e4273 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5 +# homeassistant.components.media_player.sonos +https://github.com/SoCo/SoCo/archive/cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#SoCo==0.12 + # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 2bbfaa77b8d..fc9c64d7fcd 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -1,6 +1,7 @@ """The tests for the Demo Media player platform.""" import unittest from unittest.mock import patch +import asyncio from homeassistant.bootstrap import setup_component from homeassistant.const import HTTP_HEADER_HA_AUTH @@ -8,7 +9,6 @@ import homeassistant.components.media_player as mp import homeassistant.components.http as http import requests -import requests_mock import time from tests.common import get_test_home_assistant, get_test_instance_port @@ -260,12 +260,35 @@ class TestMediaPlayerWeb(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @requests_mock.Mocker(real_http=True) - def test_media_image_proxy(self, m): + def test_media_image_proxy(self): """Test the media server image proxy server .""" fake_picture_data = 'test.test' - m.get('https://graph.facebook.com/v2.5/107771475912710/' - 'picture?type=large', text=fake_picture_data) + + class MockResponse(): + def __init__(self): + self.status = 200 + self.headers = {'Content-Type': 'sometype'} + + @asyncio.coroutine + def read(self): + return fake_picture_data.encode('ascii') + + @asyncio.coroutine + def release(self): + pass + + class MockWebsession(): + + @asyncio.coroutine + def get(self, url): + return MockResponse() + + @asyncio.coroutine + def close(self): + pass + + self.hass._websession = MockWebsession() + self.hass.block_till_done() assert setup_component( self.hass, mp.DOMAIN, diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py old mode 100755 new mode 100644 index 42f39ca5572..c8950030d72 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -42,7 +42,7 @@ class SoCoMock(): """Clear the sleep timer.""" return - def get_speaker_info(self): + def get_speaker_info(self, force): """Return a dict with various data points about the speaker.""" return {'serial_number': 'B8-E9-37-BO-OC-BA:2', 'software_version': '32.11-30071', From df7d9c3bb2f220e8046405d988679b09c4a01992 Mon Sep 17 00:00:00 2001 From: Bjarni Ivarsson Date: Tue, 1 Nov 2016 23:12:18 +0100 Subject: [PATCH 106/149] Fallback to read volume and mute state from speaker. (#4173) --- homeassistant/components/media_player/sonos.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 76d33feeb1b..1b8a0160e56 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -356,12 +356,6 @@ class SonosDevice(MediaPlayerDevice): if is_available: - if self._queue is None or self._player_volume is None: - self._player_volume = self._player.volume - - if self._queue is None or self._player_volume_muted is None: - self._player_volume_muted = self._player.mute - track_info = None if self._last_avtransport_event: variables = self._last_avtransport_event.variables @@ -384,6 +378,8 @@ class SonosDevice(MediaPlayerDevice): 'duration': variables.get('current_track_duration') } else: + self._player_volume = self._player.volume + self._player_volume_muted = self._player.mute transport_info = self._player.get_current_transport_info() self._status = transport_info.get('current_transport_state') From ba13951fff42ee0bf4ad744d174bf751291a0ea4 Mon Sep 17 00:00:00 2001 From: Jon Caruana Date: Tue, 1 Nov 2016 20:44:25 -0700 Subject: [PATCH 107/149] Add LiteJet (a lighting control system) component (#4125) * Initial submission of LiteJet integration. * Add LiteJet switch pressed automation trigger. (State changes are too slow to catch a press-release.) Add LiteJet scene, replacing commented out code that treated these as lights. Include LiteJet numbers in the device state so that it is easy to lookup entity -> number. * Fix missing global. * Allow light's brightness to be set explicitly. * Support optional 'ignore' key to ignore prefixes of loads, switches, and scenes that weren't configured for use in the LiteJet system. * Fix lint errors and warnings. * Cleanup header comments. Default to not creating LiteJet switches as these are generally not useful. * Lint fixes. * Fixes from pull request feedback. * Use hass.data instead of globals for data storage. * Fix lint warnings. --- .coveragerc | 3 + .../components/automation/litejet.py | 41 ++++++++ homeassistant/components/light/litejet.py | 94 +++++++++++++++++++ homeassistant/components/litejet.py | 53 +++++++++++ homeassistant/components/scene/litejet.py | 58 ++++++++++++ homeassistant/components/switch/litejet.py | 84 +++++++++++++++++ requirements_all.txt | 3 + 7 files changed, 336 insertions(+) create mode 100644 homeassistant/components/automation/litejet.py create mode 100644 homeassistant/components/light/litejet.py create mode 100644 homeassistant/components/litejet.py create mode 100644 homeassistant/components/scene/litejet.py create mode 100644 homeassistant/components/switch/litejet.py diff --git a/.coveragerc b/.coveragerc index b4c34555e16..3b821a8eeaf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,9 @@ omit = homeassistant/components/isy994.py homeassistant/components/*/isy994.py + homeassistant/components/litejet.py + homeassistant/components/*/litejet.py + homeassistant/components/modbus.py homeassistant/components/*/modbus.py diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py new file mode 100644 index 00000000000..875a24540ee --- /dev/null +++ b/homeassistant/components/automation/litejet.py @@ -0,0 +1,41 @@ +""" +Trigger an automation when a LiteJet switch is released. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/automation.litejet/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['litejet'] + +_LOGGER = logging.getLogger(__name__) + +CONF_NUMBER = 'number' + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'litejet', + vol.Required(CONF_NUMBER): cv.positive_int +}) + + +def async_trigger(hass, config, action): + """Listen for events based on configuration.""" + number = config.get(CONF_NUMBER) + + @callback + def call_action(): + """Call action with right context.""" + hass.async_run_job(action, { + 'trigger': { + CONF_PLATFORM: 'litejet', + CONF_NUMBER: number + }, + }) + + hass.data['litejet_system'].on_switch_released(number, call_action) diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py new file mode 100644 index 00000000000..c278cdc1332 --- /dev/null +++ b/homeassistant/components/light/litejet.py @@ -0,0 +1,94 @@ +""" +Support for LiteJet lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.litejet/ +""" + +import logging + +import homeassistant.components.litejet as litejet +from homeassistant.components.light import ATTR_BRIGHTNESS, Light + +DEPENDENCIES = ['litejet'] + +ATTR_NUMBER = 'number' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup lights for the LiteJet platform.""" + litejet_ = hass.data['litejet_system'] + + devices = [] + for i in litejet_.loads(): + name = litejet_.get_load_name(i) + if not litejet.is_ignored(hass, name): + devices.append(LiteJetLight(hass, litejet_, i, name)) + add_devices(devices) + + +class LiteJetLight(Light): + """Represents a single LiteJet light.""" + + def __init__(self, hass, lj, i, name): + """Initialize a LiteJet light.""" + self._hass = hass + self._lj = lj + self._index = i + self._brightness = 0 + self._name = name + + lj.on_load_activated(i, self._on_load_changed) + lj.on_load_deactivated(i, self._on_load_changed) + + self.update() + + def _on_load_changed(self): + """Called on a LiteJet thread when a load's state changes.""" + _LOGGER.debug("Updating due to notification for %s", self._name) + self._hass.loop.create_task(self.async_update_ha_state(True)) + + @property + def name(self): + """The light's name.""" + return self._name + + @property + def brightness(self): + """Return the light's brightness.""" + return self._brightness + + @property + def is_on(self): + """Return if the light is on.""" + return self._brightness != 0 + + @property + def should_poll(self): + """Return that lights do not require polling.""" + return False + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return { + ATTR_NUMBER: self._index + } + + def turn_on(self, **kwargs): + """Turn on the light.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 99) + self._lj.activate_load_at(self._index, brightness, 0) + else: + self._lj.activate_load(self._index) + + def turn_off(self, **kwargs): + """Turn off the light.""" + self._lj.deactivate_load(self._index) + + def update(self): + """Retrieve the light's brightness from the LiteJet system.""" + self._brightness = self._lj.get_load_level(self._index) / 99 * 255 diff --git a/homeassistant/components/litejet.py b/homeassistant/components/litejet.py new file mode 100644 index 00000000000..70c3755144b --- /dev/null +++ b/homeassistant/components/litejet.py @@ -0,0 +1,53 @@ +"""Allows the LiteJet lighting system to be controlled by Home Assistant. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/litejet/ +""" +import logging +import voluptuous as vol + +from homeassistant.helpers import discovery +from homeassistant.const import CONF_URL +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'litejet' + +REQUIREMENTS = ['pylitejet==0.1'] + +CONF_EXCLUDE_NAMES = 'exclude_names' +CONF_INCLUDE_SWITCHES = 'include_switches' + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_EXCLUDE_NAMES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Initialize the LiteJet component.""" + from pylitejet import LiteJet + + url = config[DOMAIN].get(CONF_URL) + + hass.data['litejet_system'] = LiteJet(url) + hass.data['litejet_config'] = config[DOMAIN] + + discovery.load_platform(hass, 'light', DOMAIN, {}, config) + if config[DOMAIN].get(CONF_INCLUDE_SWITCHES): + discovery.load_platform(hass, 'switch', DOMAIN, {}, config) + discovery.load_platform(hass, 'scene', DOMAIN, {}, config) + + return True + + +def is_ignored(hass, name): + """Determine if a load, switch, or scene should be ignored.""" + for prefix in hass.data['litejet_config'].get(CONF_EXCLUDE_NAMES, []): + if name.startswith(prefix): + return True + return False diff --git a/homeassistant/components/scene/litejet.py b/homeassistant/components/scene/litejet.py new file mode 100644 index 00000000000..6e08ebfbee9 --- /dev/null +++ b/homeassistant/components/scene/litejet.py @@ -0,0 +1,58 @@ +""" +Support for LiteJet scenes. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/scene.litejet/ +""" +import logging +import homeassistant.components.litejet as litejet +from homeassistant.components.scene import Scene + +DEPENDENCIES = ['litejet'] + +ATTR_NUMBER = 'number' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup scenes for the LiteJet platform.""" + litejet_ = hass.data['litejet_system'] + + devices = [] + for i in litejet_.scenes(): + name = litejet_.get_scene_name(i) + if not litejet.is_ignored(hass, name): + devices.append(LiteJetScene(litejet_, i, name)) + add_devices(devices) + + +class LiteJetScene(Scene): + """Represents a single LiteJet scene.""" + + def __init__(self, lj, i, name): + """Initialize the scene.""" + self._lj = lj + self._index = i + self._name = name + + @property + def name(self): + """Return the name of the scene.""" + return self._name + + @property + def should_poll(self): + """Return that polling is not necessary.""" + return False + + @property + def device_state_attributes(self): + """Return the device-specific state attributes.""" + return { + ATTR_NUMBER: self._index + } + + def activate(self, **kwargs): + """Activate the scene.""" + self._lj.activate_scene(self._index) diff --git a/homeassistant/components/switch/litejet.py b/homeassistant/components/switch/litejet.py new file mode 100644 index 00000000000..d058d648540 --- /dev/null +++ b/homeassistant/components/switch/litejet.py @@ -0,0 +1,84 @@ +""" +Support for LiteJet switch. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.litejet/ +""" +import logging +import homeassistant.components.litejet as litejet +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['litejet'] + +ATTR_NUMBER = 'number' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the LiteJet switch platform.""" + litejet_ = hass.data['litejet_system'] + + devices = [] + for i in litejet_.button_switches(): + name = litejet_.get_switch_name(i) + if not litejet.is_ignored(hass, name): + devices.append(LiteJetSwitch(hass, litejet_, i, name)) + add_devices(devices) + + +class LiteJetSwitch(SwitchDevice): + """Represents a single LiteJet switch.""" + + def __init__(self, hass, lj, i, name): + """Initialize a LiteJet switch.""" + self._hass = hass + self._lj = lj + self._index = i + self._state = False + self._name = name + + lj.on_switch_pressed(i, self._on_switch_pressed) + lj.on_switch_released(i, self._on_switch_released) + + self.update() + + def _on_switch_pressed(self): + _LOGGER.debug("Updating pressed for %s", self._name) + self._state = True + self._hass.loop.create_task(self.async_update_ha_state()) + + def _on_switch_released(self): + _LOGGER.debug("Updating released for %s", self._name) + self._state = False + self._hass.loop.create_task(self.async_update_ha_state()) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return if the switch is pressed.""" + return self._state + + @property + def should_poll(self): + """Return that polling is not necessary.""" + return False + + @property + def device_state_attributes(self): + """Return the device-specific state attributes.""" + return { + ATTR_NUMBER: self._index + } + + def turn_on(self, **kwargs): + """Press the switch.""" + self._lj.press_switch(self._index) + + def turn_off(self, **kwargs): + """Release the switch.""" + self._lj.release_switch(self._index) diff --git a/requirements_all.txt b/requirements_all.txt index 04317134ad4..d492c41c59b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -367,6 +367,9 @@ pyicloud==0.9.1 # homeassistant.components.sensor.lastfm pylast==1.6.0 +# homeassistant.components.litejet +pylitejet==0.1 + # homeassistant.components.sensor.loopenergy pyloopenergy==0.0.15 From 90d894a499a8471181fb44b00b073fdaeadc6104 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Wed, 2 Nov 2016 00:49:27 -0400 Subject: [PATCH 108/149] Garadget (#4031) * Initial attempt at implementation * Adding Garadget cover component * Updating Device to be Required * Updating .coveragerc to exclude from testing * Updating code review items * Updating per 2nd code review * Updating configuration to be more like command-line --- .coveragerc | 1 + homeassistant/components/cover/garadget.py | 275 +++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 homeassistant/components/cover/garadget.py diff --git a/.coveragerc b/.coveragerc index 3b821a8eeaf..37add2fb2ec 100644 --- a/.coveragerc +++ b/.coveragerc @@ -132,6 +132,7 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py + homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/scsgate.py diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py new file mode 100644 index 00000000000..813ddea7170 --- /dev/null +++ b/homeassistant/components/cover/garadget.py @@ -0,0 +1,275 @@ +""" +Platform for the garadget cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/garadget/ +""" +import logging + +import voluptuous as vol + +import requests + +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\ + CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\ + CONF_COVERS +import homeassistant.helpers.config_validation as cv + +DEFAULT_NAME = 'Garadget' + +ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)" +ATTR_TIME_IN_STATE = "time in state" +ATTR_SENSOR_STRENGTH = "sensor reflection rate" +ATTR_AVAILABLE = "available" + +STATE_OPENING = "opening" +STATE_CLOSING = "closing" +STATE_STOPPED = "stopped" +STATE_OFFLINE = "offline" + +STATES_MAP = { + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, + "stopped": STATE_STOPPED +} + + +# Validation of the user's configuration +COVER_SCHEMA = vol.Schema({ + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo covers.""" + covers = [] + devices = config.get(CONF_COVERS, {}) + + _LOGGER.debug(devices) + + for device_id, device_config in devices.items(): + args = { + "name": device_config.get(CONF_NAME), + "device_id": device_config.get(CONF_DEVICE, device_id), + "username": device_config.get(CONF_USERNAME), + "password": device_config.get(CONF_PASSWORD), + "access_token": device_config.get(CONF_ACCESS_TOKEN) + } + + covers.append(GaradgetCover(hass, args)) + + add_devices(covers) + + +class GaradgetCover(CoverDevice): + """Representation of a demo cover.""" + + # pylint: disable=no-self-use, too-many-instance-attributes + def __init__(self, hass, args): + """Initialize the cover.""" + self.particle_url = 'https://api.particle.io' + self.hass = hass + self._name = args['name'] + self.device_id = args['device_id'] + self.access_token = args['access_token'] + self.obtained_token = False + self._username = args['username'] + self._password = args['password'] + self._state = STATE_UNKNOWN + self.time_in_state = None + self.signal = None + self.sensor = None + self._unsub_listener_cover = None + self._available = True + + if self.access_token is None: + self.access_token = self.get_token() + self._obtained_token = True + + # Lets try to get the configured name if not provided. + try: + if self._name is None: + doorconfig = self._get_variable("doorConfig") + if doorconfig["nme"] is not None: + self._name = doorconfig["nme"] + self.update() + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + self._available = False + self._name = DEFAULT_NAME + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._name = DEFAULT_NAME + self._state = STATE_OFFLINE + self._available = False + + def __del__(self): + """Try to remove token.""" + if self._obtained_token is True: + if self.access_token is not None: + self.remove_token() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo cover.""" + return True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + + if self.signal is not None: + data[ATTR_SIGNAL_STRENGTH] = self.signal + + if self.time_in_state is not None: + data[ATTR_TIME_IN_STATE] = self.time_in_state + + if self.sensor is not None: + data[ATTR_SENSOR_STRENGTH] = self.sensor + + if self.access_token is not None: + data[CONF_ACCESS_TOKEN] = self.access_token + + return data + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._state == STATE_UNKNOWN: + return None + else: + return self._state == STATE_CLOSED + + def get_token(self): + """Get new token for usage during this session.""" + args = { + 'grant_type': 'password', + 'username': self._username, + 'password': self._password + } + url = '{}/oauth/token'.format(self.particle_url) + ret = requests.post(url, + auth=('particle', 'particle'), + data=args) + + return ret.json()['access_token'] + + def remove_token(self): + """Remove authorization token from API.""" + ret = requests.delete('{}/v1/access_tokens/{}'.format( + self.particle_url, + self.access_token), + auth=(self._username, self._password)) + return ret.text + + def _start_watcher(self, command): + """Start watcher.""" + _LOGGER.debug("Starting Watcher for command: %s ", command) + if self._unsub_listener_cover is None: + self._unsub_listener_cover = track_utc_time_change( + self.hass, self._check_state) + + def _check_state(self, now): + """Check the state of the service during an operation.""" + self.update() + self.update_ha_state() + + def close_cover(self): + """Close the cover.""" + if self._state not in ["close", "closing"]: + ret = self._put_command("setState", "close") + self._start_watcher('close') + return ret.get('return_value') == 1 + + def open_cover(self): + """Open the cover.""" + if self._state not in ["open", "opening"]: + ret = self._put_command("setState", "open") + self._start_watcher('open') + return ret.get('return_value') == 1 + + def stop_cover(self): + """Stop the door where it is.""" + if self._state not in ["stopped"]: + ret = self._put_command("setState", "stop") + self._start_watcher('stop') + return ret['return_value'] == 1 + + def update(self): + """Get updated status from API.""" + try: + status = self._get_variable("doorStatus") + _LOGGER.debug("Current Status: %s", status['status']) + self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN) + self.time_in_state = status['time'] + self.signal = status['signal'] + self.sensor = status['sensor'] + self._availble = True + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._state = STATE_OFFLINE + + if self._state not in [STATE_CLOSING, STATE_OPENING]: + if self._unsub_listener_cover is not None: + self._unsub_listener_cover() + self._unsub_listener_cover = None + + def _get_variable(self, var): + """Get latest status.""" + url = '{}/v1/devices/{}/{}?access_token={}'.format( + self.particle_url, + self.device_id, + var, + self.access_token, + ) + ret = requests.get(url) + result = {} + for pairs in ret.json()['result'].split('|'): + key = pairs.split('=') + result[key[0]] = key[1] + return result + + def _put_command(self, func, arg=None): + """Send commands to API.""" + params = {'access_token': self.access_token} + if arg: + params['command'] = arg + url = '{}/v1/devices/{}/{}'.format( + self.particle_url, + self.device_id, + func) + ret = requests.post(url, data=params) + return ret.json() From 52eb816c622c29fc00ef42c0e3ccb6c3614e0659 Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Wed, 2 Nov 2016 05:50:27 +0100 Subject: [PATCH 109/149] Introduce a send_delay for pilight component (#4051) * Add a method to throttle calls to services This adds CallRateDelayThrottle. This is a class that provides an decorator to throttle calls to services. Instead of the Throttle in homeassistant.util it does this by delaying all subsequent calls instead of just dropping them. Dropping of calls would be bad if we call services to actual change the state of a connected hardware (like rf controlled power plugs). Ihe delay is done by rescheduling the call using track_point_in_utc_time from homeassistant.helpers.event so it should not block the mainloop at all. * Add unittests for CallRateDelayThrottle Signed-off-by: Jan Losinski * Introduce a send_delay for pilight component If pilight is used with a "pilight USB Nano" between the daemon and the hardware, we must use a delay between sending multiple signals. Otherwise the hardware will just skip random codes. We hit this condition for example, if we switch a group of pilight switches on or off. Without the delay, random switch signals will not be transmitted by the RF transmitter. As this seems not necessary, if the transmitter is directly connected via GPIO, we introduce a optional configuration to set the delay. * Add unittests for pilight send_delay handling This adds an unittest to test the delayed calls to the send_code service. --- homeassistant/components/pilight.py | 72 ++++++++++++++++++- tests/components/test_pilight.py | 104 +++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index de4e56c925f..2c92fec3513 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -5,10 +5,16 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/pilight/ """ import logging +import functools import socket +import threading + +from datetime import timedelta import voluptuous as vol +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, @@ -18,8 +24,12 @@ REQUIREMENTS = ['pilight==0.1.1'] _LOGGER = logging.getLogger(__name__) + +CONF_SEND_DELAY = "send_delay" + DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 5000 +DEFAULT_SEND_DELAY = 0.0 DOMAIN = 'pilight' EVENT = 'pilight_received' @@ -37,7 +47,9 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]} + vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]}, + vol.Optional(CONF_SEND_DELAY, default=DEFAULT_SEND_DELAY): + vol.Coerce(float), }), }, extra=vol.ALLOW_EXTRA) @@ -48,6 +60,8 @@ def setup(hass, config): host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] + send_throttler = CallRateDelayThrottle(hass, + config[DOMAIN][CONF_SEND_DELAY]) try: pilight_client = pilight.Client(host=host, port=port) @@ -68,6 +82,7 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client) + @send_throttler.limited def send_code(call): """Send RF code to the pilight-daemon.""" # Change type to dict from mappingproxy since data has to be JSON @@ -103,3 +118,58 @@ def setup(hass, config): pilight_client.set_callback(handle_received_code) return True + + +# pylint: disable=too-few-public-methods +class CallRateDelayThrottle(object): + """Helper class to provide service call rate throttling. + + This class provides a decorator to decorate service methods that need + to be throttled to not exceed a certain call rate per second. + One instance can be used on multiple service methods to archive + an overall throttling. + + As this uses track_point_in_utc_time to schedule delayed executions + it should not block the mainloop. + """ + + def __init__(self, hass, delay_seconds: float): + """Initialize the delay handler.""" + self._delay = timedelta(seconds=max(0.0, delay_seconds)) + self._queue = [] + self._active = False + self._lock = threading.Lock() + self._next_ts = dt_util.utcnow() + self._schedule = functools.partial(track_point_in_utc_time, hass) + + def limited(self, method): + """Decorator to delay calls on a certain method.""" + @functools.wraps(method) + def decorated(*args, **kwargs): + """The decorated function.""" + if self._delay.total_seconds() == 0.0: + method(*args, **kwargs) + return + + def action(event): + """The action wrapper that gets scheduled.""" + method(*args, **kwargs) + + with self._lock: + self._next_ts = dt_util.utcnow() + self._delay + + if len(self._queue) == 0: + self._active = False + else: + next_action = self._queue.pop(0) + self._schedule(next_action, self._next_ts) + + with self._lock: + if self._active: + self._queue.append(action) + else: + self._active = True + schedule_ts = max(dt_util.utcnow(), self._next_ts) + self._schedule(action, schedule_ts) + + return decorated diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py index ca491ee838d..0fe68b4fbe5 100644 --- a/tests/components/test_pilight.py +++ b/tests/components/test_pilight.py @@ -3,9 +3,12 @@ import logging import unittest from unittest.mock import patch import socket +from datetime import timedelta +from homeassistant import core as ha from homeassistant.bootstrap import setup_component from homeassistant.components import pilight +from homeassistant.util import dt as dt_util from tests.common import get_test_home_assistant, assert_setup_component @@ -70,7 +73,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_failed_error(self, mock_error): """Try to connect at 127.0.0.1:5000 with socket error.""" - with assert_setup_component(3): + with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.error) as mock_client: self.assertFalse(setup_component( @@ -82,7 +85,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_timeout_error(self, mock_error): """Try to connect at 127.0.0.1:5000 with socket timeout.""" - with assert_setup_component(3): + with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.timeout) as mock_client: self.assertFalse(setup_component( @@ -96,7 +99,7 @@ class TestPilight(unittest.TestCase): @patch('tests.components.test_pilight._LOGGER.error') def test_send_code_no_protocol(self, mock_pilight_error, mock_error): """Try to send data without protocol information, should give error.""" - with assert_setup_component(3): + with assert_setup_component(4): self.assertTrue(setup_component( self.hass, pilight.DOMAIN, {pilight.DOMAIN: {}})) @@ -115,7 +118,7 @@ class TestPilight(unittest.TestCase): @patch('tests.components.test_pilight._LOGGER.error') def test_send_code(self, mock_pilight_error): """Try to send proper data.""" - with assert_setup_component(3): + with assert_setup_component(4): self.assertTrue(setup_component( self.hass, pilight.DOMAIN, {pilight.DOMAIN: {}})) @@ -134,7 +137,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_send_code_fail(self, mock_pilight_error): """Check IOError exception error message.""" - with assert_setup_component(3): + with assert_setup_component(4): with patch('pilight.pilight.Client.send_code', side_effect=IOError): self.assertTrue(setup_component( @@ -150,11 +153,47 @@ class TestPilight(unittest.TestCase): error_log_call = mock_pilight_error.call_args_list[-1] self.assertTrue('Pilight send failed' in str(error_log_call)) + @patch('pilight.pilight.Client', PilightDaemonSim) + @patch('tests.components.test_pilight._LOGGER.error') + def test_send_code_delay(self, mock_pilight_error): + """Try to send proper data with delay afterwards.""" + with assert_setup_component(4): + self.assertTrue(setup_component( + self.hass, pilight.DOMAIN, + {pilight.DOMAIN: {pilight.CONF_SEND_DELAY: 5.0}})) + + # Call with protocol info, should not give error + service_data1 = {'protocol': 'test11', + 'value': 42} + service_data2 = {'protocol': 'test22', + 'value': 42} + self.hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, + service_data=service_data1, + blocking=True) + self.hass.services.call(pilight.DOMAIN, pilight.SERVICE_NAME, + service_data=service_data2, + blocking=True) + service_data1['protocol'] = [service_data1['protocol']] + service_data2['protocol'] = [service_data2['protocol']] + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, + {ha.ATTR_NOW: dt_util.utcnow()}) + self.hass.block_till_done() + error_log_call = mock_pilight_error.call_args_list[-1] + self.assertTrue(str(service_data1) in str(error_log_call)) + + new_time = dt_util.utcnow() + timedelta(seconds=5) + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, + {ha.ATTR_NOW: new_time}) + self.hass.block_till_done() + error_log_call = mock_pilight_error.call_args_list[-1] + self.assertTrue(str(service_data2) in str(error_log_call)) + @patch('pilight.pilight.Client', PilightDaemonSim) @patch('tests.components.test_pilight._LOGGER.error') def test_start_stop(self, mock_pilight_error): """Check correct startup and stop of pilight daemon.""" - with assert_setup_component(3): + with assert_setup_component(4): self.assertTrue(setup_component( self.hass, pilight.DOMAIN, {pilight.DOMAIN: {}})) @@ -178,7 +217,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.core._LOGGER.info') def test_receive_code(self, mock_info): """Check if code receiving via pilight daemon works.""" - with assert_setup_component(3): + with assert_setup_component(4): self.assertTrue(setup_component( self.hass, pilight.DOMAIN, {pilight.DOMAIN: {}})) @@ -201,7 +240,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.core._LOGGER.info') def test_whitelist_exact_match(self, mock_info): """Check whitelist filter with matched data.""" - with assert_setup_component(3): + with assert_setup_component(4): whitelist = { 'protocol': [PilightDaemonSim.test_message['protocol']], 'uuid': [PilightDaemonSim.test_message['uuid']], @@ -229,7 +268,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.core._LOGGER.info') def test_whitelist_partial_match(self, mock_info): """Check whitelist filter with partially matched data, should work.""" - with assert_setup_component(3): + with assert_setup_component(4): whitelist = { 'protocol': [PilightDaemonSim.test_message['protocol']], 'id': [PilightDaemonSim.test_message['message']['id']]} @@ -255,7 +294,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.core._LOGGER.info') def test_whitelist_or_match(self, mock_info): """Check whitelist filter with several subsection, should work.""" - with assert_setup_component(3): + with assert_setup_component(4): whitelist = { 'protocol': [PilightDaemonSim.test_message['protocol'], 'other_protocoll'], @@ -282,7 +321,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.core._LOGGER.info') def test_whitelist_no_match(self, mock_info): """Check whitelist filter with unmatched data, should not work.""" - with assert_setup_component(3): + with assert_setup_component(4): whitelist = { 'protocol': ['wrong_protocoll'], 'id': [PilightDaemonSim.test_message['message']['id']]} @@ -296,3 +335,46 @@ class TestPilight(unittest.TestCase): info_log_call = mock_info.call_args_list[-1] self.assertFalse('Event pilight_received' in info_log_call) + + +class TestPilightCallrateThrottler(unittest.TestCase): + """Test the Throttler used to throttle calls to send_code.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def test_call_rate_delay_throttle_disabled(self): + """Test that the limiter is a noop if no delay set.""" + runs = [] + + limit = pilight.CallRateDelayThrottle(self.hass, 0.0) + action = limit.limited(lambda x: runs.append(x)) + + for i in range(3): + action(i) + + self.assertEqual(runs, [0, 1, 2]) + + def test_call_rate_delay_throttle_enabled(self): + """Test that throttling actually work.""" + runs = [] + delay = 5.0 + + limit = pilight.CallRateDelayThrottle(self.hass, delay) + action = limit.limited(lambda x: runs.append(x)) + + for i in range(3): + action(i) + + self.assertEqual(runs, []) + + exp = [] + now = dt_util.utcnow() + for i in range(3): + exp.append(i) + shifted_time = now + (timedelta(seconds=delay + 0.1) * i) + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, + {ha.ATTR_NOW: shifted_time}) + self.hass.block_till_done() + self.assertEqual(runs, exp) From e487a09190df1f1325ca583896d4b785ed92c623 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 2 Nov 2016 06:51:31 +0200 Subject: [PATCH 110/149] Remove None value before writing known_devices (#4098) * Remove None * Replace null --- .../components/device_tracker/__init__.py | 7 +++--- homeassistant/util/yaml.py | 6 +++++ tests/util/test_yaml.py | 23 ++++++++++++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b7f4e839d7b..c87af93eeb5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -12,7 +12,6 @@ import threading from typing import Any, Sequence, Callable import voluptuous as vol -import yaml from homeassistant.bootstrap import ( prepare_setup_platform, log_exception) @@ -27,6 +26,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util as util from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( @@ -464,8 +464,6 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, def update_config(path: str, dev_id: str, device: Device): """Add device to YAML configuration file.""" with open(path, 'a') as out: - out.write('\n') - device = {device.dev_id: { 'name': device.name, 'mac': device.mac, @@ -473,7 +471,8 @@ def update_config(path: str, dev_id: str, device: Device): 'track': device.track, CONF_AWAY_HIDE: device.away_hide }} - yaml.dump(device, out, default_flow_style=False) + out.write('\n') + out.write(dump(device)) def get_gravatar_for_email(email: str): diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 4a73ac7c8dd..a91130338f5 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -48,6 +48,12 @@ def load_yaml(fname: str) -> Union[List, Dict]: raise HomeAssistantError(exc) +def dump(_dict: dict) -> str: + """Dump yaml to a string and remove null.""" + return yaml.safe_dump(_dict, default_flow_style=False) \ + .replace(': null\n', ':\n') + + def clear_secret_cache() -> None: """Clear the secret cache. diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 11a006524a9..3305fbea6c9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -1,7 +1,7 @@ """Test Home Assistant yaml loader.""" import io -import unittest import os +import unittest from unittest.mock import patch from homeassistant.exceptions import HomeAssistantError @@ -12,6 +12,7 @@ from tests.common import get_test_config_dir, patch_yaml_files class TestYaml(unittest.TestCase): """Test util.yaml loader.""" + # pylint: disable=no-self-use, invalid-name def test_simple_list(self): @@ -196,10 +197,12 @@ class TestYaml(unittest.TestCase): """Test include dir merge named yaml.""" mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] - with patch_yaml_files({ - '/tmp/first.yaml': 'key1: one', - '/tmp/second.yaml': 'key2: two\nkey3: three' - }): + files = { + '/tmp/first.yaml': 'key1: one', + '/tmp/second.yaml': 'key2: two\nkey3: three', + } + + with patch_yaml_files(files): conf = "key: !include_dir_merge_named /tmp" with io.StringIO(conf) as file: doc = yaml.yaml.safe_load(file) @@ -243,6 +246,10 @@ class TestYaml(unittest.TestCase): mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '') self.assertRaises(HomeAssistantError, yaml.load_yaml, 'test') + def test_dump(self): + """The that the dump method returns empty None values.""" + assert yaml.dump({'a': None, 'b': 'b'}) == 'a:\nb: b\n' + FILES = {} @@ -270,9 +277,10 @@ class FakeKeyring(): class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" - # pylint: disable=protected-access, invalid-name - def setUp(self): # pylint: disable=invalid-name + # pylint: disable=protected-access,invalid-name + + def setUp(self): """Create & load secrets file.""" config_dir = get_test_config_dir() yaml.clear_secret_cache() @@ -295,7 +303,6 @@ class TestSecrets(unittest.TestCase): ' password: !secret comp1_pw\n' '') - # pylint: disable=invalid-name def tearDown(self): """Clean up secrets.""" yaml.clear_secret_cache() From a5fb284717b6d1002965c05cdd23a43454c68829 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 2 Nov 2016 04:52:27 +0000 Subject: [PATCH 111/149] Add new_device_discovered event (#4132) --- .../components/device_tracker/__init__.py | 5 +++++ tests/components/device_tracker/test_init.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c87af93eeb5..6c6ae3adea6 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -55,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12 CONF_AWAY_HIDE = 'hide_if_away' DEFAULT_AWAY_HIDE = False +EVENT_NEW_DEVICE = 'device_tracker_new_device' + SERVICE_SEE = 'see' ATTR_MAC = 'mac' @@ -236,9 +238,12 @@ class DeviceTracker(object): device.seen(host_name, location_name, gps, gps_accuracy, battery, attributes) + if device.track: device.update_ha_state() + self.hass.bus.async_fire(EVENT_NEW_DEVICE, device) + # During init, we ignore the group if self.group is not None: self.group.update_tracked_entity_ids( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 42778244d7a..e9045ecd02e 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -306,6 +306,24 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(mock_see.call_count, 1) self.assertEqual(mock_see.call_args, call(**params)) + def test_new_device_event_fired(self): + """Test that the device tracker will fire an event.""" + self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, + TEST_PLATFORM)) + test_events = [] + + def listener(event): + """Helper method that will verify our event got called.""" + test_events.append(event) + + self.hass.bus.listen("device_tracker_new_device", listener) + + device_tracker.see(self.hass, 'mac_1', host_name='hello') + device_tracker.see(self.hass, 'mac_1', host_name='hello') + + self.hass.block_till_done() + self.assertEqual(1, len(test_events)) + # pylint: disable=invalid-name def test_not_write_duplicate_yaml_keys(self): """Test that the device tracker will not generate invalid YAML.""" From afde5a6b2606e8d992e92f38d89912c97d71c548 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Wed, 2 Nov 2016 06:01:00 +0100 Subject: [PATCH 112/149] extracted logic into an external package. monitor more attributes. support for more than one vehicle (#4170) --- .../components/device_tracker/volvooncall.py | 101 +++++++++--------- requirements_all.txt | 3 + 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 746bbea6ccf..0fea3eadd65 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.volvooncall/ """ import logging from datetime import timedelta -from urllib.parse import urljoin import voluptuous as vol -import requests import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time @@ -27,10 +25,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1) _LOGGER = logging.getLogger(__name__) -SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/' -HEADERS = {"X-Device-Id": "Device", - "X-OS-Type": "Android", - "X-Originator-Type": "App"} +REQUIREMENTS = ['volvooncall==0.1.1'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, @@ -40,62 +35,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_scanner(hass, config, see): """Validate the configuration and return a scanner.""" - session = requests.Session() - session.headers.update(HEADERS) - session.auth = (config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + from volvooncall import Connection + connection = Connection( + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) interval = max(MIN_TIME_BETWEEN_SCANS.seconds, config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) - def query(ref, rel=SERVICE_URL): - """Perform a query to the online service.""" - url = urljoin(rel, ref) - _LOGGER.debug("Request for %s", url) - res = session.get(url, timeout=15) - res.raise_for_status() - _LOGGER.debug("Received %s", res.json()) - return res.json() + def _see_vehicle(vehicle): + position = vehicle["position"] + dev_id = "volvo_" + slugify(vehicle["registrationNumber"]) + host_name = "%s (%s/%s)" % ( + vehicle["registrationNumber"], + vehicle["vehicleType"], + vehicle["modelYear"]) + + def any_opened(door): + """True if any door/window is opened.""" + return any([door[key] for key in door if "Open" in key]) + + see(dev_id=dev_id, + host_name=host_name, + gps=(position["latitude"], + position["longitude"]), + attributes=dict( + unlocked=not vehicle["carLocked"], + tank_volume=vehicle["fuelTankVolume"], + average_fuel_consumption=round( + vehicle["averageFuelConsumption"] / 10, 1), # l/100km + washer_fluid_low=vehicle["washerFluidLevel"] != "Normal", + brake_fluid_low=vehicle["brakeFluid"] != "Normal", + service_warning=vehicle["serviceWarningStatus"] != "Normal", + bulb_failures=len(vehicle["bulbFailures"]) > 0, + doors_open=any_opened(vehicle["doors"]), + windows_open=any_opened(vehicle["windows"]), + heater_on=vehicle["heater"]["status"] != "off", + fuel=vehicle["fuelAmount"], + odometer=round(vehicle["odometer"] / 1000), # km + range=vehicle["distanceToEmpty"])) def update(now): """Update status from the online service.""" + _LOGGER.info("Updating") try: - _LOGGER.debug("Updating") - status = query("status", vehicle_url) - position = query("position", vehicle_url) - see(dev_id=dev_id, - host_name=host_name, - gps=(position["position"]["latitude"], - position["position"]["longitude"]), - attributes=dict( - tank_volume=attributes["fuelTankVolume"], - washer_fluid=status["washerFluidLevel"], - brake_fluid=status["brakeFluid"], - service_warning=status["serviceWarningStatus"], - fuel=status["fuelAmount"], - odometer=status["odometer"], - range=status["distanceToEmpty"])) - except requests.exceptions.RequestException as error: - _LOGGER.error("Could not query server: %s", error) + res, vehicles = connection.update() + if not res: + _LOGGER.error("Could not query server") + return False + + for vehicle in vehicles: + _see_vehicle(vehicle) + + return True finally: track_point_in_utc_time(hass, update, now + timedelta(seconds=interval)) - try: - _LOGGER.info('Logging in to service') - user = query("customeraccounts") - rel = query(user["accountVehicleRelations"][0]) - vehicle_url = rel["vehicle"] + '/' - attributes = query("attributes", vehicle_url) - - dev_id = "volvo_" + slugify(attributes["registrationNumber"]) - host_name = "%s %s/%s" % (attributes["registrationNumber"], - attributes["vehicleType"], - attributes["modelYear"]) - update(utcnow()) - return True - except requests.exceptions.RequestException as error: - _LOGGER.error("Could not log in to service. " - "Please check configuration: " - "%s", error) - return False + _LOGGER.info('Logging in to service') + return update(utcnow()) diff --git a/requirements_all.txt b/requirements_all.txt index d492c41c59b..26dd8acf22b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,6 +526,9 @@ urllib3 # homeassistant.components.camera.uvc uvcclient==0.9.0 +# homeassistant.components.device_tracker.volvooncall +volvooncall==0.1.1 + # homeassistant.components.verisure vsure==0.11.1 From cc0d0a38d7f24885e5146bd0826fa8ba3e2b39a1 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 2 Nov 2016 13:20:44 +0000 Subject: [PATCH 113/149] Get temparature units from vera controller. (#4130) Alrighty :dancing_women: --- homeassistant/components/climate/vera.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index 447d2e4f720..b8b03f8dda9 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -8,7 +8,10 @@ import logging from homeassistant.util import convert from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE +from homeassistant.const import ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + ATTR_TEMPERATURE) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -95,7 +98,13 @@ class VeraThermostat(VeraDevice, ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_FAHRENHEIT + vera_temp_units = ( + self.vera_device.vera_controller.temperature_units) + + if vera_temp_units == 'F': + return TEMP_FAHRENHEIT + + return TEMP_CELSIUS @property def current_temperature(self): From e4a713207ddcecba285319104a19d1e4caffc993 Mon Sep 17 00:00:00 2001 From: Georgi Kirichkov Date: Wed, 2 Nov 2016 21:23:43 +0200 Subject: [PATCH 114/149] Fixes in TP-Link Switch logging 0 values on init (#4026) * Fixes in TP-Link Switch logging 0 values on init On init of component the emeter would log to influxdb and possibly other inputs a 0 value, instead of not logging anything. Initial polling should circumvent that behavior and avoid logging inconsistencies. * Refactors update call in __init__ --- homeassistant/components/switch/tplink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 06c67dcf5ea..3554e0b933f 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = config.get(CONF_HOST) name = config.get(CONF_NAME) - add_devices([SmartPlugSwitch(SmartPlug(host), name)]) + add_devices([SmartPlugSwitch(SmartPlug(host), name)], True) class SmartPlugSwitch(SwitchDevice): From 26490109acac7f08b95264b1cb5bc303a85fcca9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 2 Nov 2016 21:53:52 +0100 Subject: [PATCH 115/149] Change event loop on windows (#4075) * Change event loop on windows * fix * split PR * remove set event loop * Add paulus suggestion * fix missing import * revert stuff from PR Splitting * fix event loop on test --- homeassistant/__main__.py | 8 +++++++- homeassistant/const.py | 1 + homeassistant/core.py | 6 +++++- tests/common.py | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 30dfa6b6db0..e5305245b18 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, REQUIRED_PYTHON_VER, + REQUIRED_PYTHON_VER_WIN, RESTART_EXIT_CODE, ) from homeassistant.util.async import run_callback_threadsafe @@ -64,7 +65,12 @@ def monkey_patch_asyncio(): def validate_python() -> None: """Validate we're running the right Python version.""" - if sys.version_info[:3] < REQUIRED_PYTHON_VER: + if sys.platform == "win32" and \ + sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN: + print("Home Assistant requires at least Python {}.{}.{}".format( + *REQUIRED_PYTHON_VER_WIN)) + sys.exit(1) + elif sys.version_info[:3] < REQUIRED_PYTHON_VER: print("Home Assistant requires at least Python {}.{}.{}".format( *REQUIRED_PYTHON_VER)) sys.exit(1) diff --git a/homeassistant/const.py b/homeassistant/const.py index fcba511fe10..abe932fad15 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,6 +6,7 @@ PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) +REQUIRED_PYTHON_VER_WIN = (3, 5, 2) PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' diff --git a/homeassistant/core.py b/homeassistant/core.py index 5fb7d2761cc..8de1e2b2535 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -107,7 +107,11 @@ class HomeAssistant(object): def __init__(self, loop=None): """Initialize new Home Assistant object.""" - self.loop = loop or asyncio.get_event_loop() + if sys.platform == "win32": + self.loop = loop or asyncio.ProactorEventLoop() + else: + self.loop = loop or asyncio.get_event_loop() + self.executor = ThreadPoolExecutor(max_workers=5) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(self._async_exception_handler) diff --git a/tests/common.py b/tests/common.py index af65a93f216..ee84cc7c642 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,6 +1,7 @@ """Test the helper method for writing tests.""" import asyncio import os +import sys from datetime import timedelta from unittest import mock from unittest.mock import patch @@ -33,7 +34,10 @@ def get_test_config_dir(*add_path): def get_test_home_assistant(): """Return a Home Assistant object pointing at test config dir.""" - loop = asyncio.new_event_loop() + if sys.platform == "win32": + loop = asyncio.ProactorEventLoop() + else: + loop = asyncio.new_event_loop() hass = loop.run_until_complete(async_test_home_assistant(loop)) hass.allow_pool = True From 4e820ea30aecfd1b5ea9d99ac91f60f4533aa867 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2016 19:16:59 -0700 Subject: [PATCH 116/149] Move mocks to async_start (#4182) --- tests/common.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index ee84cc7c642..d665e17a503 100644 --- a/tests/common.py +++ b/tests/common.py @@ -58,8 +58,6 @@ def get_test_home_assistant(): orig_start = hass.start orig_stop = hass.stop - @patch.object(hass.loop, 'add_signal_handler') - @patch.object(ha, '_async_create_timer') @patch.object(hass.loop, 'run_forever') @patch.object(hass.loop, 'close') def start_hass(*mocks): @@ -100,6 +98,19 @@ def async_test_home_assistant(loop): hass.state = ha.CoreState.running hass.allow_pool = False + + # Mock async_start + orig_start = hass.async_start + + @asyncio.coroutine + def mock_async_start(): + with patch.object(loop, 'add_signal_handler'), \ + patch('homeassistant.core._async_create_timer'): + yield from orig_start() + + hass.async_start = mock_async_start + + # Mock async_init_pool orig_init = hass.async_init_pool @ha.callback From 8e0838adebfe91dfc3ce14bca086c29e1e4f080d Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 3 Nov 2016 03:19:53 +0100 Subject: [PATCH 117/149] Added support for Philips TVs with jointSPACE API (#4157) * Added support for Philips Tvs with JointSpace API * Flake + Lint fixes * Lint be like "lol fu" * Changes as requested by reviewers, except lib-requirement * Switched to library-usage * lint... newline-bingo... --- .coveragerc | 1 + .../components/media_player/philips_js.py | 170 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 174 insertions(+) create mode 100644 homeassistant/components/media_player/philips_js.py diff --git a/.coveragerc b/.coveragerc index 37add2fb2ec..88e446a30ea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -192,6 +192,7 @@ omit = homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/pandora.py + homeassistant/components/media_player/philips_js.py homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/roku.py diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py new file mode 100644 index 00000000000..af438d7dbec --- /dev/null +++ b/homeassistant/components/media_player/philips_js.py @@ -0,0 +1,170 @@ +""" +Media Player component to integrate TVs exposing the Joint Space API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.philips_js/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice) +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_HOST, CONF_NAME) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['ha-philipsjs==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + +DEFAULT_DEVICE = 'default' +DEFAULT_HOST = '127.0.0.1' +DEFAULT_NAME = 'Philips TV' +BASE_URL = 'http://{0}:1925/1/{1}' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Philips TV platform.""" + import haphilipsjs + + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + + tvapi = haphilipsjs.PhilipsTV(host) + + add_devices([PhilipsTV(tvapi, name)]) + + +# pylint: disable=abstract-method +class PhilipsTV(MediaPlayerDevice): + """Representation of a Philips TV exposing the JointSpace API.""" + + def __init__(self, tv, name): + """Initialize the Philips TV.""" + self._tv = tv + self._name = name + self._state = STATE_UNKNOWN + self._min_volume = None + self._max_volume = None + self._volume = None + self._muted = False + self._program_name = None + self._channel_name = None + self._source = None + self._source_list = [] + self._connfail = 0 + self._source_mapping = {} + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def should_poll(self): + """Device should be polled.""" + return True + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_PHILIPS_JS + + @property + def state(self): + """Get the device state. An exception means OFF state.""" + return self._state + + @property + def source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + def select_source(self, source): + """Set the input source.""" + if source in self._source_mapping: + self._tv.setSource(self._source_mapping.get(source)) + self._source = source + if not self._tv.on: + self._state = STATE_OFF + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + def turn_off(self): + """Turn off the device.""" + self._tv.sendKey('Standby') + if not self._tv.on: + self._state = STATE_OFF + + def volume_up(self): + """Send volume up command.""" + self._tv.sendKey('VolumeUp') + if not self._tv.on: + self._state = STATE_OFF + + def volume_down(self): + """Send volume down command.""" + self._tv.sendKey('VolumeDown') + if not self._tv.on: + self._state = STATE_OFF + + def mute_volume(self, mute): + """Send mute command.""" + self._tv.sendKey('Mute') + if not self._tv.on: + self._state = STATE_OFF + + @property + def media_title(self): + """Title of current playing media.""" + return self._source + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data and update device state.""" + self._tv.update() + self._min_volume = self._tv.min_volume + self._max_volume = self._tv.max_volume + self._volume = self._tv.volume + self._muted = self._tv.muted + if self._tv.source_id: + src = self._tv.sources.get(self._tv.source_id, None) + if src: + self._source = src.get('name', None) + if self._tv.sources and not self._source_list: + for srcid in sorted(self._tv.sources): + srcname = self._tv.sources.get(srcid, dict()).get('name', None) + self._source_list.append(srcname) + self._source_mapping[srcname] = srcid + if self._tv.on: + self._state = STATE_ON + else: + self._state = STATE_OFF diff --git a/requirements_all.txt b/requirements_all.txt index 26dd8acf22b..9ba9390c03c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -130,6 +130,9 @@ ha-alpr==0.3 # homeassistant.components.ffmpeg ha-ffmpeg==0.15 +# homeassistant.components.media_player.philips_js +ha-philipsjs==0.0.1 + # homeassistant.components.mqtt.server hbmqtt==0.7.1 From 2940fb72fbef2b20e3274981b3dee160fc2d9de0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2016 19:24:25 -0700 Subject: [PATCH 118/149] EntityComponent.add_entities now converts generators to a list (#4183) --- homeassistant/helpers/entity_component.py | 2 +- tests/helpers/test_entity_component.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 44d0b8891d5..30d62608f9b 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -283,7 +283,7 @@ class EntityPlatform(object): def add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform.""" run_coroutine_threadsafe( - self.async_add_entities(new_entities, update_before_add), + self.async_add_entities(list(new_entities), update_before_add), self.component.hass.loop ).result() diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 7c6b94963cf..02d8d36dafa 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -8,7 +8,7 @@ from unittest.mock import patch, Mock import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.components import group -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util @@ -356,3 +356,20 @@ class TestHelpersEntityComponent(unittest.TestCase): assert sorted(self.hass.states.entity_ids()) == \ ['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device'] + + def test_adding_entities_with_generator_and_thread_callback(self): + """Test generator in add_entities that calls thread method. + + We should make sure we resolve the generator to a list before passing + it into an async context. + """ + component = EntityComponent(_LOGGER, DOMAIN, self.hass) + + def create_entity(number): + """Create entity helper.""" + entity = EntityTest() + entity.entity_id = generate_entity_id(component.entity_id_format, + 'Number', hass=self.hass) + return entity + + component.add_entities(create_entity(i) for i in range(2)) From 0d14920758a15b32db3289339beb16baf813114c Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 3 Nov 2016 04:31:09 +0200 Subject: [PATCH 119/149] Component setup error messages with markdown (#3919) * Remove_dev_link_async * callback --- homeassistant/bootstrap.py | 48 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 294645f693b..fdcdb5d4fe2 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -31,8 +31,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = 'component' ERROR_LOG_FILENAME = 'home-assistant.log' -_PERSISTENT_PLATFORMS = set() -_PERSISTENT_VALIDATION = set() +_PERSISTENT_ERRORS = {} +HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)' def setup_component(hass: core.HomeAssistant, domain: str, @@ -63,12 +63,14 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, # OrderedSet is empty if component or dependencies could not be resolved if not components: + _async_persistent_notification(hass, domain, True) return False for component in components: res = yield from _async_setup_component(hass, component, config) if not res: _LOGGER.error('Component %s failed to setup', component) + _async_persistent_notification(hass, component, True) return False return True @@ -87,6 +89,7 @@ def _handle_requirements(hass: core.HomeAssistant, component, if not pkg_util.install_package(req, target=hass.config.path('deps')): _LOGGER.error('Not initializing %s because could not install ' 'dependency %s', name, req) + _async_persistent_notification(hass, name) return False return True @@ -114,6 +117,7 @@ def _async_setup_component(hass: core.HomeAssistant, if domain in setup_progress: _LOGGER.error('Attempt made to setup %s during setup of %s', domain, domain) + _async_persistent_notification(hass, domain, True) return False try: @@ -131,6 +135,10 @@ def _async_setup_component(hass: core.HomeAssistant, return False component = loader.get_component(domain) + if component is None: + _async_persistent_notification(hass, domain) + return False + async_comp = hasattr(component, 'async_setup') try: @@ -141,14 +149,17 @@ def _async_setup_component(hass: core.HomeAssistant, None, component.setup, hass, config) except Exception: # pylint: disable=broad-except _LOGGER.exception('Error during setup of component %s', domain) + _async_persistent_notification(hass, domain, True) return False if result is False: _LOGGER.error('component %s failed to initialize', domain) + _async_persistent_notification(hass, domain, True) return False elif result is not True: _LOGGER.error('component %s did not return boolean if setup ' 'was successful. Disabling component.', domain) + _async_persistent_notification(hass, domain, True) loader.set_component(domain, None) return False @@ -284,13 +295,7 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, # Not found if platform is None: _LOGGER.error('Unable to find platform %s', platform_path) - - _PERSISTENT_PLATFORMS.add(platform_path) - message = ('Unable to find the following platforms: ' + - ', '.join(list(_PERSISTENT_PLATFORMS)) + - '(please check your configuration)') - persistent_notification.async_create( - hass, message, 'Invalid platforms', 'platform_errors') + _async_persistent_notification(hass, platform_path) return None # Already loaded @@ -305,6 +310,7 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, 'Unable to prepare setup for platform %s because ' 'dependency %s could not be initialized', platform_path, component) + _async_persistent_notification(hass, platform_path, True) return None res = yield from hass.loop.run_in_executor( @@ -552,6 +558,22 @@ def log_exception(ex, domain, config, hass): hass.loop, async_log_exception, ex, domain, config, hass).result() +@core.callback +def _async_persistent_notification(hass: core.HomeAssistant, component: str, + link: Optional[bool]=False): + """Print a persistent notification. + + This method must be run in the event loop. + """ + _PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link + _lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name) + if link else name for name, link in _PERSISTENT_ERRORS.items()] + message = ('The following components and platforms could not be set up:\n' + '* ' + '\n* '.join(list(_lst)) + '\nPlease check your config') + persistent_notification.async_create( + hass, message, 'Invalid config', 'invalid_config') + + @core.callback def async_log_exception(ex, domain, config, hass): """Generate log exception for config validation. @@ -559,12 +581,8 @@ def async_log_exception(ex, domain, config, hass): This method must be run in the event loop. """ message = 'Invalid config for [{}]: '.format(domain) - _PERSISTENT_VALIDATION.add(domain) - message = ('The following platforms contain invalid configuration: ' + - ', '.join(list(_PERSISTENT_VALIDATION)) + - ' (please check your configuration). ') - persistent_notification.async_create( - hass, message, 'Invalid config', 'invalid_config') + if hass is not None: + _async_persistent_notification(hass, domain, True) if 'extra keys not allowed' in ex.error_message: message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ From f3595f790a637055fe0ff6ebb3558037badcaa8b Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 3 Nov 2016 04:34:12 +0200 Subject: [PATCH 120/149] Async version of Yr.no (#4158) * initial * feedback * More feedback. Still need to fix match_url * url_match * split_lines --- homeassistant/components/sensor/yr.py | 181 +++++++++++++++----------- pylintrc | 1 + tests/components/sensor/test_yr.py | 108 +++++++-------- tests/test_util/aiohttp.py | 36 ++++- 4 files changed, 186 insertions(+), 140 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 6fe6b429990..05412131679 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -4,9 +4,13 @@ Support for Yr.no weather service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.yr/ """ +import asyncio +from datetime import timedelta import logging +from xml.parsers.expat import ExpatError -import requests +import async_timeout +from aiohttp.web import HTTPException import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -15,8 +19,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util + REQUIREMENTS = ['xmltodict==0.10.2'] _LOGGER = logging.getLogger(__name__) @@ -43,15 +49,15 @@ SENSOR_TYPES = { } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - [vol.In(SENSOR_TYPES.keys())], + vol.Optional(CONF_MONITORED_CONDITIONS, default=['symbol']): vol.All( + cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]), vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_ELEVATION): vol.Coerce(int), }) -def setup_platform(hass, config, add_devices, discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the Yr.no sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -63,32 +69,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): coordinates = dict(lat=latitude, lon=longitude, msl=elevation) - weather = YrData(coordinates) - dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(YrSensor(sensor_type, weather)) + dev.append(YrSensor(sensor_type)) + yield from async_add_devices(dev) - # add symbol as default sensor - if len(dev) == 0: - dev.append(YrSensor("symbol", weather)) - add_devices(dev) + weather = YrData(hass, coordinates, dev) + yield from weather.async_update() class YrSensor(Entity): """Representation of an Yr.no sensor.""" - def __init__(self, sensor_type, weather): + def __init__(self, sensor_type): """Initialize the sensor.""" self.client_name = 'yr' self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None - self._weather = weather self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._update = None - - self.update() @property def name(self): @@ -100,6 +99,11 @@ class YrSensor(Entity): """Return the state of the device.""" return self._state + @property + def should_poll(self): # pylint: disable=no-self-use + """No polling needed.""" + return False + @property def entity_picture(self): """Weather symbol if type is symbol.""" @@ -120,78 +124,97 @@ class YrSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - def update(self): - """Get the latest data from yr.no and updates the states.""" - now = dt_util.utcnow() - # Check if data should be updated - if self._update is not None and now <= self._update: - return - - self._weather.update() - - # Find sensor - for time_entry in self._weather.data['product']['time']: - valid_from = dt_util.parse_datetime(time_entry['@from']) - valid_to = dt_util.parse_datetime(time_entry['@to']) - - loc_data = time_entry['location'] - - if self.type not in loc_data or now >= valid_to: - continue - - self._update = valid_to - - if self.type == 'precipitation' and valid_from < now: - self._state = loc_data[self.type]['@value'] - break - elif self.type == 'symbol' and valid_from < now: - self._state = loc_data[self.type]['@number'] - break - elif self.type in ('temperature', 'pressure', 'humidity', - 'dewpointTemperature'): - self._state = loc_data[self.type]['@value'] - break - elif self.type in ('windSpeed', 'windGust'): - self._state = loc_data[self.type]['@mps'] - break - elif self.type == 'windDirection': - self._state = float(loc_data[self.type]['@deg']) - break - elif self.type in ('fog', 'cloudiness', 'lowClouds', - 'mediumClouds', 'highClouds'): - self._state = loc_data[self.type]['@percent'] - break - class YrData(object): """Get the latest data and updates the states.""" - def __init__(self, coordinates): + def __init__(self, hass, coordinates, devices): """Initialize the data object.""" self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) - self._nextrun = None + self.devices = devices self.data = {} - self.update() + self.hass = hass - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data from yr.no.""" - # Check if new will be available - if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: - return - try: - with requests.Session() as sess: - response = sess.get(self._url) - except requests.RequestException: - return - if response.status_code != 200: - return - data = response.text + def try_again(err: str): + """Schedule again later.""" + _LOGGER.warning('Retrying in 15 minutes: %s', err) + nxt = dt_util.utcnow() + timedelta(minutes=15) + async_track_point_in_utc_time(self.hass, self.async_update, nxt) - import xmltodict - self.data = xmltodict.parse(data)['weatherdata'] - model = self.data['meta']['model'] - if '@nextrun' not in model: - model = model[0] - self._nextrun = dt_util.parse_datetime(model['@nextrun']) + try: + with async_timeout.timeout(10, loop=self.hass.loop): + resp = yield from self.hass.websession.get(self._url) + if resp.status != 200: + try_again('{} returned {}'.format(self._url, resp.status)) + return + text = yield from resp.text() + self.hass.loop.create_task(resp.release()) + except asyncio.TimeoutError as err: + try_again(err) + return + except HTTPException as err: + resp.close() + try_again(err) + return + + try: + import xmltodict + self.data = xmltodict.parse(text)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + next_run = dt_util.parse_datetime(model['@nextrun']) + except (ExpatError, IndexError) as err: + try_again(err) + return + + # Schedule next execution + async_track_point_in_utc_time(self.hass, self.async_update, next_run) + + now = dt_util.utcnow() + + tasks = [] + # Update all devices + for dev in self.devices: + # Find sensor + for time_entry in self.data['product']['time']: + valid_from = dt_util.parse_datetime(time_entry['@from']) + valid_to = dt_util.parse_datetime(time_entry['@to']) + + loc_data = time_entry['location'] + + if dev.type not in loc_data or now >= valid_to: + continue + + if dev.type == 'precipitation' and valid_from < now: + new_state = loc_data[dev.type]['@value'] + break + elif dev.type == 'symbol' and valid_from < now: + new_state = loc_data[dev.type]['@number'] + break + elif dev.type in ('temperature', 'pressure', 'humidity', + 'dewpointTemperature'): + new_state = loc_data[dev.type]['@value'] + break + elif dev.type in ('windSpeed', 'windGust'): + new_state = loc_data[dev.type]['@mps'] + break + elif dev.type == 'windDirection': + new_state = float(loc_data[dev.type]['@deg']) + break + elif dev.type in ('fog', 'cloudiness', 'lowClouds', + 'mediumClouds', 'highClouds'): + new_state = loc_data[dev.type]['@percent'] + break + + # pylint: disable=protected-access + if new_state != dev._state: + dev._state = new_state + tasks.append(dev.async_update_ha_state()) + + yield from asyncio.gather(*tasks, loop=self.hass.loop) diff --git a/pylintrc b/pylintrc index 627524fc240..710f392e95f 100644 --- a/pylintrc +++ b/pylintrc @@ -27,6 +27,7 @@ disable= too-many-instance-attributes, too-many-locals, too-many-public-methods, + too-many-return-statements, too-many-statements, too-few-public-methods, diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 7df47a99688..8d54037a379 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -1,77 +1,69 @@ """The tests for the Yr sensor platform.""" +import asyncio from datetime import datetime from unittest.mock import patch -from homeassistant.bootstrap import setup_component +from homeassistant.bootstrap import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, load_fixture +from tests.common import assert_setup_component, load_fixture -class TestSensorYr: - """Test the Yr sensor.""" +NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) - def setup_method(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() +@asyncio.coroutine +def test_default_setup(hass, aioclient_mock): + """Test the default setup.""" + aioclient_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) + config = {'platform': 'yr', + 'elevation': 0} + hass.allow_pool = True + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=NOW), assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', {'sensor': config}) - def test_default_setup(self, requests_mock): - """Test the default setup.""" - requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', - text=load_fixture('yr.no.json')) - now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) + state = hass.states.get('sensor.yr_symbol') - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0}}) + assert state.state == '3' + assert state.attributes.get('unit_of_measurement') is None - state = self.hass.states.get('sensor.yr_symbol') - assert '3' == state.state - assert state.state.isnumeric() - assert state.attributes.get('unit_of_measurement') is None +@asyncio.coroutine +def test_custom_setup(hass, aioclient_mock): + """Test a custom setup.""" + aioclient_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) - def test_custom_setup(self, requests_mock): - """Test a custom setup.""" - requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/', - text=load_fixture('yr.no.json')) - now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) + config = {'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': [ + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed']} + hass.allow_pool = True + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=NOW), assert_setup_component(1): + yield from async_setup_component(hass, 'sensor', {'sensor': config}) - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': [ - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed']}}) + state = hass.states.get('sensor.yr_pressure') + assert state.attributes.get('unit_of_measurement') == 'hPa' + assert state.state == '1009.3' - state = self.hass.states.get('sensor.yr_pressure') - assert 'hPa' == state.attributes.get('unit_of_measurement') - assert '1009.3' == state.state + state = hass.states.get('sensor.yr_wind_direction') + assert state.attributes.get('unit_of_measurement') == '°' + assert state.state == '103.6' - state = self.hass.states.get('sensor.yr_wind_direction') - assert '°' == state.attributes.get('unit_of_measurement') - assert '103.6' == state.state + state = hass.states.get('sensor.yr_humidity') + assert state.attributes.get('unit_of_measurement') == '%' + assert state.state == '55.5' - state = self.hass.states.get('sensor.yr_humidity') - assert '%' == state.attributes.get('unit_of_measurement') - assert '55.5' == state.state + state = hass.states.get('sensor.yr_fog') + assert state.attributes.get('unit_of_measurement') == '%' + assert state.state == '0.0' - state = self.hass.states.get('sensor.yr_fog') - assert '%' == state.attributes.get('unit_of_measurement') - assert '0.0' == state.state - - state = self.hass.states.get('sensor.yr_wind_speed') - assert 'm/s', state.attributes.get('unit_of_measurement') - assert '3.5' == state.state + state = hass.states.get('sensor.yr_wind_speed') + assert state.attributes.get('unit_of_measurement') == 'm/s' + assert state.state == '3.5' diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 9de94b50df8..7cf0fe9378d 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -4,6 +4,7 @@ from contextlib import contextmanager import functools import json as _json from unittest import mock +from urllib.parse import urlparse, parse_qs class AiohttpClientMocker: @@ -57,7 +58,8 @@ class AiohttpClientMocker: return len(self.mock_calls) @asyncio.coroutine - def match_request(self, method, url, *, auth=None): + def match_request(self, method, url, *, auth=None): \ + # pylint: disable=unused-variable """Match a request against pre-registered requests.""" for response in self._mocks: if response.match_request(method, url): @@ -74,13 +76,41 @@ class AiohttpClientMockResponse: def __init__(self, method, url, status, response): """Initialize a fake response.""" self.method = method - self.url = url + self._url = url + self._url_parts = (None if hasattr(url, 'search') + else urlparse(url.lower())) self.status = status self.response = response def match_request(self, method, url): """Test if response answers request.""" - return method == self.method and url == self.url + if method.lower() != self.method.lower(): + return False + + # regular expression matching + if self._url_parts is None: + return self._url.search(url) is not None + + req = urlparse(url.lower()) + + if self._url_parts.scheme and req.scheme != self._url_parts.scheme: + return False + if self._url_parts.netloc and req.netloc != self._url_parts.netloc: + return False + if (req.path or '/') != (self._url_parts.path or '/'): + return False + + # Ensure all query components in matcher are present in the request + request_qs = parse_qs(req.query) + matcher_qs = parse_qs(self._url_parts.query) + for key, vals in matcher_qs.items(): + for val in vals: + try: + request_qs.get(key, []).remove(val) + except ValueError: + return False + + return True @asyncio.coroutine def read(self): From df68de80325e0664d64fad78d0174aadca4622b6 Mon Sep 17 00:00:00 2001 From: bestlibre Date: Thu, 3 Nov 2016 03:50:18 +0100 Subject: [PATCH 121/149] Influxdb sensor state set to unknown if query return no points (#4148) * Influxdb sensor state set to unknown if query return no points * Update influxdb.py --- homeassistant/components/sensor/influxdb.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index 24b8ae591f1..89c9bbad4b2 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -172,9 +172,11 @@ class InfluxSensorData(object): points = list(self.influx.query(self.query).get_points()) if len(points) == 0: - _LOGGER.error('Query returned no points : %s', self.query) - return - if len(points) > 1: - _LOGGER.warning('Query returned multiple points, only first one' - ' shown : %s', self.query) - self.value = points[0].get('value') + _LOGGER.warning('Query returned no points, sensor state set' + ' to UNKNOWN : %s', self.query) + self.value = None + else: + if len(points) > 1: + _LOGGER.warning('Query returned multiple points, only first' + ' one shown : %s', self.query) + self.value = points[0].get('value') From 1e28851280477294249972d2c9caa09e956b5c09 Mon Sep 17 00:00:00 2001 From: Nicolas Graziano Date: Thu, 3 Nov 2016 03:51:53 +0100 Subject: [PATCH 122/149] Media player BraviaTv : Try to connect only if tv is not in off state. (#4140) When HA is restart with the TV in off state there was error log every 10s until the TV is set ON. --- homeassistant/components/media_player/braviatv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index ee23a707d0c..f55f1e6021c 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -219,7 +219,8 @@ class BraviaTVDevice(MediaPlayerDevice): def update(self): """Update TV info.""" if not self._braviarc.is_connected(): - self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) + if self._braviarc.get_power_status() != 'off': + self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) if not self._braviarc.is_connected(): return From d7dd7df5e78f1b7d535eef8a13f567f205dbfbec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2016 20:39:27 -0700 Subject: [PATCH 123/149] Update frontend --- homeassistant/components/frontend/version.py | 4 ++-- .../frontend/www_static/frontend.html | 2 +- .../frontend/www_static/frontend.html.gz | Bin 130411 -> 130460 bytes .../www_static/home-assistant-polymer | 2 +- .../panels/ha-panel-dev-template.html | 2 +- .../panels/ha-panel-dev-template.html.gz | Bin 7310 -> 7309 bytes .../frontend/www_static/service_worker.js | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 5acc1e53f30..1a0dac2f3bc 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,14 +2,14 @@ FINGERPRINTS = { "core.js": "5ed5e063d66eb252b5b288738c9c2d16", - "frontend.html": "4022865a7890970c8cef71eb265a78d3", + "frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", "panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", - "panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2", + "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 309447a6320..c6c2fed44be 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2,4 +2,4 @@ },_distributeDirtyRoots:function(){for(var e,t=this.shadyRoot._dirtyRoots,o=0,i=t.length;o0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(e<0)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;t \ No newline at end of file +this.currentTarget=t,this.defaultPrevented=!1,this.eventPhase=Event.AT_TARGET,this.timeStamp=Date.now()},i=window.Element.prototype.animate;window.Element.prototype.animate=function(n,r){var o=i.call(this,n,r);o._cancelHandlers=[],o.oncancel=null;var a=o.cancel;o.cancel=function(){a.call(this);var i=new e(this,null,t()),n=this._cancelHandlers.concat(this.oncancel?[this.oncancel]:[]);setTimeout(function(){n.forEach(function(t){t.call(i.target,i)})},0)};var s=o.addEventListener;o.addEventListener=function(t,e){"function"==typeof e&&"cancel"==t?this._cancelHandlers.push(e):s.call(this,t,e)};var u=o.removeEventListener;return o.removeEventListener=function(t,e){if("cancel"==t){var i=this._cancelHandlers.indexOf(e);i>=0&&this._cancelHandlers.splice(i,1)}else u.call(this,t,e)},o}}}(),function(t){var e=document.documentElement,i=null,n=!1;try{var r=getComputedStyle(e).getPropertyValue("opacity"),o="0"==r?"1":"0";i=e.animate({opacity:[o,o]},{duration:1}),i.currentTime=0,n=getComputedStyle(e).getPropertyValue("opacity")==o}catch(t){}finally{i&&i.cancel()}if(!n){var a=window.Element.prototype.animate;window.Element.prototype.animate=function(e,i){return window.Symbol&&Symbol.iterator&&Array.prototype.from&&e[Symbol.iterator]&&(e=Array.from(e)),Array.isArray(e)||null===e||(e=t.convertToArrayForm(e)),a.call(this,e,i)}}}(c),!function(t,e,i){function n(t){var i=e.timeline;i.currentTime=t,i._discardAnimations(),0==i._animations.length?o=!1:requestAnimationFrame(n)}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(i){e.timeline._updateAnimationsPromises(),t(i),e.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(c,e,f),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChildDuration(i.effect))}.bind(this))}},_setExternalAnimation:function(t){if(this.effect&&this._isGroup)for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index b6b0768f5bfde7b98b2e2ce54b9e6af1c9067e66..975f1668bd41f653de649b6f07bd65e86202fd1e 100644 GIT binary patch delta 46886 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

(^F?fyvLbjkB-!1 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 CGDY+N delta 46852 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 z

\ No newline at end of file + clear: both;white-space:pre-wrap}.rendered.error{color:red}
Templates
\ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index 5421d69c0208a1f2341dda24d4845c2661490ee0..6a2f68ab114ee7826ad79285cb7a78fb8173c8f3 100644 GIT binary patch delta 608 zcmV-m0-ybkIgL38ABzYGPO%zTu?RaFfAgkLdJ|2Dw$M$`RajNCx)SQc6uM!o#U1f{ z3_8l8PQ?t`-BDqqk#%oWjjU@OV@vI|%~8Ir?rqnp+&Ik&e70&;ZCH{v%Cf=s_k#$Q8pOjd z4nJo|{XhAZWtFz|r|2IYPZoES&w`q1wyi)%(uj zKw|$uZF!VfH5th&QL+R&Ns0pY&Pnrp0~0aE9ZV>ZoF$YKM@+Yptif~1N+_mAIF9OT zB*v&yDS#ui2+E8xe>8KymmvBJ zMf?3wA;(piY_tVm;5uJkuUTAf3waWEsG7Z$;*b7foI7iUs!yG(Y1w;d*S~%B65Cwhfcf!am_Aeb-EgRv*{VOH_TSK4F+tgx4mN z9jr}SjC3Wh30b45+cOI0k3eOzc1~S2 uUi!?!sAGCCB|JttTFYHxV0OynI78ody5+y@F{XbBpg!z|qZ delta 609 zcmV-n0-pViIgU99ABzYGyBzsfu?RaFe{(}9lu7TQ>C6^-33>`^YSvalb(ldnjJ32Q zo{vFAIaH~tgJyTs*Jxwi8C4tWTF2N>duel&E2}%(bt*4TvjUf`T2!0&tBk=jW@ojC ztJ&8;QKjGjQMf`nQwV_;N^P{Lffe{inims1| zunSjTaK?+Wq50gkgG%_Py$!kK^XzKEeg83s*nXH{s vSB;lGvM}nH9!v?3k&f1KlNgwtGC9uBw|#E;4||O19|HM5+eK&W4`cuUcV{Zc diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index efe2add5a0f..e4ba63ad1bc 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","9a3c76f013fc7646eea39d16fa295ba1"],["/frontend/panels/dev-event-550bf85345c454274a40d15b2795a002.html","6977c253b5b4da588d50b0aaa50b21f4"],["/frontend/panels/dev-info-ec613406ce7e20d93754233d55625c8a.html","8e28a4c617fd6963b45103d5e5c80617"],["/frontend/panels/dev-service-4a051878b92b002b8b018774ba207769.html","57123d199ea22cbaaddc46c36b18075f"],["/frontend/panels/dev-state-65e5f791cc467561719bf591f1386054.html","78158786a6597ef86c3fd6f4985cde92"],["/frontend/panels/dev-template-d23943fa0370f168714da407c90091a2.html","2cf2426a6aa4ee9c1df74926dc475bc8"],["/frontend/panels/map-49ab2d6f180f8bdea7cffaa66b8a5d3e.html","6e6c9c74e0b2424b62d4cc55b8e89be3"],["/static/core-5ed5e063d66eb252b5b288738c9c2d16.js","59dabb570c57dd421d5197009bf1d07f"],["/static/frontend-4022865a7890970c8cef71eb265a78d3.html","bac271be8f0424ffcc75c3ece87eb67a"],["/static/mdi-46a76f877ac9848899b8ed382427c16f.html","a846c4082dd5cffd88ac72cbe943e691"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;a Date: Thu, 3 Nov 2016 00:00:32 -0400 Subject: [PATCH 124/149] yet another command_line sensor update (#4184) --- homeassistant/components/sensor/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index e3e361c1ae2..b7372edb0dc 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -54,7 +54,7 @@ class CommandSensor(Entity): self._hass = hass self.data = data self._name = name - self._state = False + self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template self.update() From a3ae96440bde7535bf64ad33aac72f881b681792 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Thu, 3 Nov 2016 05:07:23 +0100 Subject: [PATCH 125/149] Update the icloud device_tracker (#4081) * Update the icloud device_tracker * addressed @kellerza 's comments * GMTT config needs an entity_id * renamed services * fix cookiedir and clean up keep_alive function * fix travis errors * forgot a self. * update devices after initializing the API * changed wording * addressed changes from @kellerza * Syntax error solved * Update icloud.py * Only use account of username instead of whole username as default for account name * use slugify instead of slug for schema * remove Google Maps Travel Time * Add comment from original tracker back --- .../components/device_tracker/icloud.py | 469 +++++++++++++++--- .../components/device_tracker/services.yaml | 45 ++ .../www_static/images/config_icloud.png | Bin 0 -> 171148 bytes 3 files changed, 443 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/images/config_icloud.png diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 7c585244400..b5ae5ded01a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -1,100 +1,427 @@ """ -Support for iCloud connected devices. +Platform that supports scanning iCloud. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.icloud/ """ import logging +import random +import os + import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_START) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT) +from homeassistant.components.zone import active_zone from homeassistant.helpers.event import track_utc_time_change +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify -from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT, - PLATFORM_SCHEMA) +import homeassistant.util.dt as dt_util +from homeassistant.util.location import distance +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyicloud==0.9.1'] -CONF_INTERVAL = 'interval' -KEEPALIVE_INTERVAL = 4 +CONF_IGNORED_DEVICES = 'ignored_devices' +CONF_ACCOUNTNAME = 'account_name' + +# entity attributes +ATTR_ACCOUNTNAME = 'account_name' +ATTR_INTERVAL = 'interval' +ATTR_DEVICENAME = 'device_name' +ATTR_BATTERY = 'battery' +ATTR_DISTANCE = 'distance' +ATTR_DEVICESTATUS = 'device_status' +ATTR_LOWPOWERMODE = 'low_power_mode' +ATTR_BATTERYSTATUS = 'battery_status' + +ICLOUDTRACKERS = {} + +_CONFIGURING = {} + +DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare', + 'deviceStatus', 'remoteLock', 'activationLocked', + 'deviceClass', 'id', 'deviceModel', 'rawDeviceModel', + 'passcodeLength', 'canWipeAfterLock', 'trackingInfo', + 'location', 'msg', 'batteryLevel', 'remoteWipe', + 'thisDevice', 'snd', 'prsId', 'wipeInProgress', + 'lowPowerMode', 'lostModeEnabled', 'isLocating', + 'lostModeCapable', 'mesg', 'name', 'batteryStatus', + 'lockedTimestamp', 'lostTimestamp', 'locationCapable', + 'deviceDisplayName', 'lostDevice', 'deviceColor', + 'wipedTimestamp', 'modelDisplayName', 'locationEnabled', + 'isMac', 'locFoundEnabled'] + +DEVICESTATUSCODES = {'200': 'online', '201': 'offline', '203': 'pending', + '204': 'unregistered'} + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), + vol.Optional(ATTR_DEVICENAME): cv.slugify, + vol.Optional(ATTR_INTERVAL): cv.positive_int, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): vol.Coerce(str), - vol.Required(CONF_PASSWORD): vol.Coerce(str), - vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int), - vol.Range(min=1)) - }) + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, +}) -def setup_scanner(hass, config, see): - """Setup the iCloud Scanner.""" - from pyicloud import PyiCloudService - from pyicloud.exceptions import PyiCloudFailedLoginException - from pyicloud.exceptions import PyiCloudNoDevicesException - logging.getLogger("pyicloud.base").setLevel(logging.WARNING) +def setup_scanner(hass, config: dict, see): + """Set up the iCloud Scanner.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] + icloudaccount = Icloud(hass, username, password, account, see) - try: - _LOGGER.info('Logging into iCloud Account') - # Attempt the login to iCloud - api = PyiCloudService(username, password, verify=True) - except PyiCloudFailedLoginException as error: - _LOGGER.exception('Error logging into iCloud Service: %s', error) + if icloudaccount.api is not None: + ICLOUDTRACKERS[account] = icloudaccount + + else: + _LOGGER.error("No ICLOUDTRACKERS added") return False - def keep_alive(now): - """Keep authenticating iCloud connection. + def lost_iphone(call): + """Call the lost iphone function if the device is found.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].lost_iphone(devicename) + hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, + schema=SERVICE_SCHEMA) - The session timeouts if we are not using it so we - have to re-authenticate & this will send an email. - """ - api.authenticate() - _LOGGER.info("Authenticate against iCloud") + def update_icloud(call): + """Call the update function of an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].update_icloud(devicename) + hass.services.register(DOMAIN, 'icloud_update', update_icloud, + schema=SERVICE_SCHEMA) - seen_devices = {} + def reset_account_icloud(call): + """Reset an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].reset_account_icloud() + hass.services.register(DOMAIN, 'icloud_reset_account', + reset_account_icloud, schema=SERVICE_SCHEMA) - def update_icloud(now): - """Authenticate against iCloud and scan for devices.""" - try: - keep_alive(None) - # Loop through every device registered with the iCloud account - for device in api.devices: - status = device.status() - dev_id = slugify(status['name'].replace(' ', '', 99)) + def setinterval(call): + """Call the update function of an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + interval = call.data.get(ATTR_INTERVAL) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].setinterval(interval, devicename) - # An entity will not be created by see() when track=false in - # 'known_devices.yaml', but we need to see() it at least once - entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id)) - if entity is None and dev_id in seen_devices: - continue - seen_devices[dev_id] = True - - location = device.location() - # If the device has a location add it. If not do nothing - if location: - see( - dev_id=dev_id, - host_name=status['name'], - gps=(location['latitude'], location['longitude']), - battery=status['batteryLevel']*100, - gps_accuracy=location['horizontalAccuracy'] - ) - except PyiCloudNoDevicesException: - _LOGGER.info('No iCloud Devices found!') - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud) - - update_minutes = list(range(0, 60, config[CONF_INTERVAL])) - # Schedule keepalives between the updates - keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL) - if x not in update_minutes) - - track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes) - track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes) + hass.services.register(DOMAIN, 'icloud_set_interval', setinterval, + schema=SERVICE_SCHEMA) + # Tells the bootstrapper that the component was successfully initialized return True + + +class Icloud(object): + """Represent an icloud account in Home Assistant.""" + + def __init__(self, hass, username, password, name, see): + """Initialize an iCloud account.""" + self.hass = hass + self.username = username + self.password = password + self.api = None + self.accountname = name + self.devices = {} + self.seen_devices = {} + self._overridestates = {} + self._intervals = {} + self.see = see + + self._trusted_device = None + self._verification_code = None + + self._attrs = {} + self._attrs[ATTR_ACCOUNTNAME] = name + + self.reset_account_icloud() + + randomseconds = random.randint(10, 59) + track_utc_time_change( + self.hass, self.keep_alive, + second=randomseconds + ) + + def reset_account_icloud(self): + """Reset an icloud account.""" + from pyicloud import PyiCloudService + from pyicloud.exceptions import ( + PyiCloudFailedLoginException, PyiCloudNoDevicesException) + + icloud_dir = self.hass.config.path('icloud') + if not os.path.exists(icloud_dir): + os.makedirs(icloud_dir) + + try: + self.api = PyiCloudService( + self.username, self.password, + cookie_directory=icloud_dir, + verify=True) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error('Error logging into iCloud Service: %s', error) + return + + try: + self.devices = {} + self._overridestates = {} + self._intervals = {} + for device in self.api.devices: + status = device.status(DEVICESTATUSSET) + devicename = slugify(status['name'].replace(' ', '', 99)) + if devicename not in self.devices: + self.devices[devicename] = device + self._intervals[devicename] = 1 + self._overridestates[devicename] = None + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + def icloud_trusted_device_callback(self, callback_data): + """The trusted device is chosen.""" + self._trusted_device = int(callback_data.get('0', '0')) + self._trusted_device = self.api.trusted_devices[self._trusted_device] + if self.accountname in _CONFIGURING: + request_id = _CONFIGURING.pop(self.accountname) + configurator = get_component('configurator') + configurator.request_done(request_id) + + def icloud_need_trusted_device(self): + """We need a trusted device.""" + configurator = get_component('configurator') + if self.accountname in _CONFIGURING: + return + + devicesstring = '' + devices = self.api.trusted_devices + for i, device in enumerate(devices): + devicesstring += "{}: {};".format(i, device.get('deviceName')) + + _CONFIGURING[self.accountname] = configurator.request_config( + self.hass, 'iCloud {}'.format(self.accountname), + self.icloud_trusted_device_callback, + description=( + 'Please choose your trusted device by entering' + ' the index from this list: ' + devicesstring), + entity_picture="/static/images/config_icloud.png", + submit_caption='Confirm', + fields=[{'id': '0'}] + ) + + def icloud_verification_callback(self, callback_data): + """The trusted device is chosen.""" + self._verification_code = callback_data.get('0') + if self.accountname in _CONFIGURING: + request_id = _CONFIGURING.pop(self.accountname) + configurator = get_component('configurator') + configurator.request_done(request_id) + + def icloud_need_verification_code(self): + """We need a verification code.""" + configurator = get_component('configurator') + if self.accountname in _CONFIGURING: + return + + if self.api.send_verification_code(self._trusted_device): + self._verification_code = 'waiting' + + _CONFIGURING[self.accountname] = configurator.request_config( + self.hass, 'iCloud {}'.format(self.accountname), + self.icloud_verification_callback, + description=('Please enter the validation code:'), + entity_picture="/static/images/config_icloud.png", + submit_caption='Confirm', + fields=[{'code': '0'}] + ) + + def keep_alive(self, now): + """Keep the api alive.""" + from pyicloud.exceptions import PyiCloud2FARequiredError + + if self.api is None: + self.reset_account_icloud() + + if self.api is None: + return + + if self.api.requires_2fa: + try: + self.api.authenticate() + except PyiCloud2FARequiredError: + if self._trusted_device is None: + self.icloud_need_trusted_device() + return + + if self._verification_code is None: + self.icloud_need_verification_code() + return + + if self._verification_code == 'waiting': + return + + if self.api.validate_verification_code( + self._trusted_device, self._verification_code): + self._verification_code = None + else: + self.api.authenticate() + + currentminutes = dt_util.now().hour * 60 + dt_util.now().minute + for devicename in self.devices: + interval = self._intervals.get(devicename, 1) + if ((currentminutes % interval == 0) or + (interval > 10 and + currentminutes % interval in [2, 4])): + self.update_device(devicename) + + def determine_interval(self, devicename, latitude, longitude, battery): + """Calculate new interval.""" + distancefromhome = None + zone_state = self.hass.states.get('zone.home') + zone_state_lat = zone_state.attributes['latitude'] + zone_state_long = zone_state.attributes['longitude'] + distancefromhome = distance(latitude, longitude, zone_state_lat, + zone_state_long) + distancefromhome = round(distancefromhome / 1000, 1) + + currentzone = active_zone(self.hass, latitude, longitude) + + if ((currentzone is not None and + currentzone == self._overridestates.get(devicename)) or + (currentzone is None and + self._overridestates.get(devicename) == 'away')): + return + + self._overridestates[devicename] = None + + if currentzone is not None: + self._intervals[devicename] = 30 + return + + if distancefromhome is None: + return + if distancefromhome > 25: + self._intervals[devicename] = round(distancefromhome / 2, 0) + elif distancefromhome > 10: + self._intervals[devicename] = 5 + else: + self._intervals[devicename] = 1 + if battery is not None and battery <= 33 and distancefromhome > 3: + self._intervals[devicename] = self._intervals[devicename] * 2 + + def update_device(self, devicename): + """Update the device_tracker entity.""" + from pyicloud.exceptions import PyiCloudNoDevicesException + + # An entity will not be created by see() when track=false in + # 'known_devices.yaml', but we need to see() it at least once + entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) + if entity is None and devicename in self.seen_devices: + return + attrs = {} + kwargs = {} + + if self.api is None: + return + + try: + for device in self.api.devices: + if str(device) != str(self.devices[devicename]): + continue + + status = device.status(DEVICESTATUSSET) + dev_id = status['name'].replace(' ', '', 99) + dev_id = slugify(dev_id) + attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( + status['deviceStatus'], 'error') + attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode'] + attrs[ATTR_BATTERYSTATUS] = status['batteryStatus'] + attrs[ATTR_ACCOUNTNAME] = self.accountname + status = device.status(DEVICESTATUSSET) + battery = status.get('batteryLevel', 0) * 100 + location = status['location'] + if location: + self.determine_interval( + devicename, location['latitude'], + location['longitude'], battery) + interval = self._intervals.get(devicename, 1) + attrs[ATTR_INTERVAL] = interval + accuracy = location['horizontalAccuracy'] + kwargs['dev_id'] = dev_id + kwargs['host_name'] = status['name'] + kwargs['gps'] = (location['latitude'], + location['longitude']) + kwargs['battery'] = battery + kwargs['gps_accuracy'] = accuracy + kwargs[ATTR_ATTRIBUTES] = attrs + self.see(**kwargs) + self.seen_devices[devicename] = True + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + def lost_iphone(self, devicename): + """Call the lost iphone function if the device is found.""" + if self.api is None: + return + + self.api.authenticate() + + for device in self.api.devices: + if devicename is None or device == self.devices[devicename]: + device.play_sound() + + def update_icloud(self, devicename=None): + """Authenticate against iCloud and scan for devices.""" + from pyicloud.exceptions import PyiCloudNoDevicesException + + if self.api is None: + return + + try: + if devicename is not None: + if devicename in self.devices: + self.devices[devicename].update_icloud() + else: + _LOGGER.error("devicename %s unknown for account %s", + devicename, self._attrs[ATTR_ACCOUNTNAME]) + else: + for device in self.devices: + self.devices[device].update_icloud() + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + def setinterval(self, interval=None, devicename=None): + """Set the interval of the given devices.""" + devs = [devicename] if devicename else self.devices + for device in devs: + devid = DOMAIN + '.' + device + devicestate = self.hass.states.get(devid) + if interval is not None: + if devicestate is not None: + self._overridestates[device] = active_zone( + self.hass, + float(devicestate.attributes.get('latitude', 0)), + float(devicestate.attributes.get('longitude', 0))) + if self._overridestates[device] is None: + self._overridestates[device] = 'away' + self._intervals[device] = interval + else: + self._overridestates[device] = None + self.update_device(device) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index dc573ae0275..2d3315b319a 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -31,3 +31,48 @@ see: battery: description: Battery level of device example: '100' + +icloud: + icloud_lost_iphone: + description: Service to play the lost iphone sound on an iDevice + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' + + icloud_set_interval: + description: Service to set the interval of an iDevice + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 + + icloud_update: + description: Service to ask for an update of an iDevice. + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' + + icloud_reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' diff --git a/homeassistant/components/frontend/www_static/images/config_icloud.png b/homeassistant/components/frontend/www_static/images/config_icloud.png new file mode 100644 index 0000000000000000000000000000000000000000..2058986018b9f475cce6cec27698123e5f309512 GIT binary patch literal 171148 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 z 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 literal 0 HcmV?d00001 From 1d100dcac969afe3edb1319e8b5554dc988a37eb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2016 21:15:23 -0700 Subject: [PATCH 126/149] Bugfix/frontend group urls (#4185) * Remove unnecessary sleeps * Frontend: fix serving index when refreshing view page. --- homeassistant/components/frontend/__init__.py | 13 +++++++++--- .../device_tracker/test_locative.py | 2 -- tests/components/media_player/test_demo.py | 2 -- tests/components/test_alexa.py | 2 -- tests/components/test_api.py | 2 -- tests/components/test_emulated_hue.py | 2 -- tests/components/test_frontend.py | 20 +++++++++++-------- tests/components/test_http.py | 2 -- tests/test_remote.py | 2 -- 9 files changed, 22 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index a96e871c42f..195d79ec5da 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -8,8 +8,8 @@ import os from aiohttp import web from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.components import api +from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND +from homeassistant.components import api, group from homeassistant.components.http import HomeAssistantView from .version import FINGERPRINTS @@ -184,7 +184,7 @@ class IndexView(HomeAssistantView): url = '/' name = "frontend:index" requires_auth = False - extra_urls = ['/states', '/states/'] + extra_urls = ['/states', '/states/{entity_id}'] def __init__(self, hass, extra_urls): """Initialize the frontend view.""" @@ -202,6 +202,13 @@ class IndexView(HomeAssistantView): @asyncio.coroutine def get(self, request, entity_id=None): """Serve the index view.""" + if entity_id is not None: + state = self.hass.states.get(entity_id) + + if (not state or state.domain != 'group' or + not state.attributes.get(group.ATTR_VIEW)): + return self.json_message('Entity not found', HTTP_NOT_FOUND) + if self.hass.http.development: core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index a724b5d26d5..b1977556c63 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -1,5 +1,4 @@ """The tests the for Locative device tracker platform.""" -import time import unittest from unittest.mock import patch @@ -46,7 +45,6 @@ def setUpModule(): }) hass.start() - time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index fc9c64d7fcd..8bcda323010 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -9,7 +9,6 @@ import homeassistant.components.media_player as mp import homeassistant.components.http as http import requests -import time from tests.common import get_test_home_assistant, get_test_instance_port @@ -254,7 +253,6 @@ class TestMediaPlayerWeb(unittest.TestCase): }) self.hass.start() - time.sleep(0.05) def tearDown(self): """Stop everything that was started.""" diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index e5ae13d91c6..28a80868163 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,7 +1,6 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access import json -import time import datetime import unittest @@ -115,7 +114,6 @@ def setUpModule(): }) hass.start() - time.sleep(0.05) # pylint: disable=invalid-name diff --git a/tests/components/test_api.py b/tests/components/test_api.py index f14fc84ae63..a70048956eb 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -3,7 +3,6 @@ import asyncio from contextlib import closing import json -import time import unittest from unittest.mock import Mock, patch @@ -50,7 +49,6 @@ def setUpModule(): bootstrap.setup_component(hass, 'api') hass.start() - time.sleep(0.05) # pylint: disable=invalid-name diff --git a/tests/components/test_emulated_hue.py b/tests/components/test_emulated_hue.py index ef4ea7f234e..edb2181c813 100755 --- a/tests/components/test_emulated_hue.py +++ b/tests/components/test_emulated_hue.py @@ -1,5 +1,4 @@ """The tests for the emulated Hue component.""" -import time import json import unittest @@ -43,7 +42,6 @@ def setup_hass_instance(emulated_hue_config): def start_hass_instance(hass): """Start the Home Assistant instance to test.""" hass.start() - time.sleep(0.05) class TestEmulatedHue(unittest.TestCase): diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 46686a5c66f..3ff366babd9 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,7 +1,6 @@ """The tests for Home Assistant frontend.""" # pylint: disable=protected-access import re -import time import unittest import requests @@ -32,9 +31,6 @@ def setUpModule(): hass = get_test_home_assistant() - hass.bus.listen('test_event', lambda _: _) - hass.states.set('test.test', 'a_state') - assert bootstrap.setup_component( hass, http.DOMAIN, {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, @@ -43,7 +39,6 @@ def setUpModule(): assert bootstrap.setup_component(hass, 'frontend') hass.start() - time.sleep(0.05) # pylint: disable=invalid-name @@ -63,7 +58,6 @@ class TestFrontend(unittest.TestCase): def test_frontend_and_static(self): """Test if we can get the frontend.""" req = requests.get(_url("")) - self.assertEqual(200, req.status_code) # Test we can retrieve frontend.js @@ -72,9 +66,7 @@ class TestFrontend(unittest.TestCase): req.text) self.assertIsNotNone(frontendjs) - req = requests.get(_url(frontendjs.groups(0)[0])) - self.assertEqual(200, req.status_code) def test_404(self): @@ -84,3 +76,15 @@ class TestFrontend(unittest.TestCase): def test_we_cannot_POST_to_root(self): """Test that POST is not allow to root.""" self.assertEqual(405, requests.post(_url("")).status_code) + + def test_states_routes(self): + """All served by index.""" + req = requests.get(_url("/states")) + self.assertEqual(200, req.status_code) + + req = requests.get(_url("/states/group.non_existing")) + self.assertEqual(404, req.status_code) + + hass.states.set('group.existing', 'on', {'view': True}) + req = requests.get(_url("/states/group.existing")) + self.assertEqual(200, req.status_code) diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 565809a8cc3..42a0498ae60 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,7 +1,6 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access import logging -import time from ipaddress import ip_network from unittest.mock import patch @@ -61,7 +60,6 @@ def setUpModule(): for trusted_network in TRUSTED_NETWORKS] hass.start() - time.sleep(0.05) # pylint: disable=invalid-name diff --git a/tests/test_remote.py b/tests/test_remote.py index df41c2ebd10..8692fd4a133 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access import asyncio import threading -import time import unittest from unittest.mock import patch @@ -51,7 +50,6 @@ def setUpModule(): bootstrap.setup_component(hass, 'api') hass.start() - time.sleep(0.05) master_api = remote.API('127.0.0.1', API_PASSWORD, MASTER_PORT) From ded2ea8b19fd2a144186c32558511bf7dbe85a58 Mon Sep 17 00:00:00 2001 From: Ferry van Zeelst Date: Thu, 3 Nov 2016 05:17:29 +0100 Subject: [PATCH 127/149] Synology DSM sensor (#4156) * Added Synology DSM Sensor * Fixed balloobbot's comments * Fixed mistake (should have run lint and flake8 before committing * Fixed update mechanisme according to balloobs feedback * Requesting retest as test failure isn't related to changes made --- .coveragerc | 1 + .../components/sensor/synologydsm.py | 252 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 256 insertions(+) create mode 100644 homeassistant/components/sensor/synologydsm.py diff --git a/.coveragerc b/.coveragerc index 88e446a30ea..294b6b1b747 100644 --- a/.coveragerc +++ b/.coveragerc @@ -285,6 +285,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py new file mode 100644 index 00000000000..31201879207 --- /dev/null +++ b/homeassistant/components/sensor/synologydsm.py @@ -0,0 +1,252 @@ +""" +Support for Synology NAS Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.synologydsm/ +""" + +import logging +from datetime import timedelta + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, EVENT_HOMEASSISTANT_START) +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +import voluptuous as vol + +REQUIREMENTS = ['python-synology==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DISKS = 'disks' +CONF_VOLUMES = 'volumes' +DEFAULT_NAME = 'Synology DSM' +DEFAULT_PORT = 5000 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + +_UTILISATION_MON_COND = { + 'cpu_other_load': ['CPU Load (Other)', '%', 'mdi:chip'], + 'cpu_user_load': ['CPU Load (User)', '%', 'mdi:chip'], + 'cpu_system_load': ['CPU Load (System)', '%', 'mdi:chip'], + 'cpu_total_load': ['CPU Load (Total)', '%', 'mdi:chip'], + 'cpu_1min_load': ['CPU Load (1 min)', '%', 'mdi:chip'], + 'cpu_5min_load': ['CPU Load (5 min)', '%', 'mdi:chip'], + 'cpu_15min_load': ['CPU Load (15 min)', '%', 'mdi:chip'], + 'memory_real_usage': ['Memory Usage (Real)', '%', 'mdi:memory'], + 'memory_size': ['Memory Size', 'Mb', 'mdi:memory'], + 'memory_cached': ['Memory Cached', 'Mb', 'mdi:memory'], + 'memory_available_swap': ['Memory Available (Swap)', 'Mb', 'mdi:memory'], + 'memory_available_real': ['Memory Available (Real)', 'Mb', 'mdi:memory'], + 'memory_total_swap': ['Memory Total (Swap)', 'Mb', 'mdi:memory'], + 'memory_total_real': ['Memory Total (Real)', 'Mb', 'mdi:memory'], + 'network_up': ['Network Up', 'Kbps', 'mdi:upload'], + 'network_down': ['Network Down', 'Kbps', 'mdi:download'], +} +_STORAGE_VOL_MON_COND = { + 'volume_status': ['Status', None, 'mdi:checkbox-marked-circle-outline'], + 'volume_device_type': ['Type', None, 'mdi:harddisk'], + 'volume_size_total': ['Total Size', None, 'mdi:chart-pie'], + 'volume_size_used': ['Used Space', None, 'mdi:chart-pie'], + 'volume_percentage_used': ['Volume Used', '%', 'mdi:chart-pie'], + 'volume_disk_temp_avg': ['Average Disk Temp', None, 'mdi:thermometer'], + 'volume_disk_temp_max': ['Maximum Disk Temp', None, 'mdi:thermometer'], +} +_STORAGE_DSK_MON_COND = { + 'disk_name': ['Name', None, 'mdi:harddisk'], + 'disk_device': ['Device', None, 'mdi:dots-horizontal'], + 'disk_smart_status': ['Status (Smart)', None, + 'mdi:checkbox-marked-circle-outline'], + 'disk_status': ['Status', None, 'mdi:checkbox-marked-circle-outline'], + 'disk_exceed_bad_sector_thr': ['Exceeded Max Bad Sectors', None, + 'mdi:test-tube'], + 'disk_below_remain_life_thr': ['Below Min Remaining Life', None, + 'mdi:test-tube'], + 'disk_temp': ['Temperature', None, 'mdi:thermometer'], +} + +_MONITORED_CONDITIONS = list(_UTILISATION_MON_COND.keys()) + \ + list(_STORAGE_VOL_MON_COND.keys()) + \ + list(_STORAGE_DSK_MON_COND.keys()) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]), + vol.Optional(CONF_DISKS, default=None): cv.ensure_list, + vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list, +}) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Synology NAS Sensor.""" + # pylint: disable=too-many-locals + def run_setup(event): + """Wait until HASS is fully initialized before creating. + + Delay the setup until Home Assistant is fully initialized. + This allows any entities to be created already + """ + # Setup API + api = SynoApi(config.get(CONF_HOST), config.get(CONF_PORT), + config.get(CONF_USERNAME), config.get(CONF_PASSWORD), + hass.config.units.temperature_unit) + + sensors = [SynoNasUtilSensor(api, variable, + _UTILISATION_MON_COND[variable]) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _UTILISATION_MON_COND] + + # Handle all Volumes + volumes = config['volumes'] + if volumes is None: + volumes = api.storage().volumes + + for volume in volumes: + sensors += [SynoNasStorageSensor(api, variable, + _STORAGE_VOL_MON_COND[variable], + volume) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _STORAGE_VOL_MON_COND] + + # Handle all Disks + disks = config['disks'] + if disks is None: + disks = api.storage().disks + + for disk in disks: + sensors += [SynoNasStorageSensor(api, variable, + _STORAGE_DSK_MON_COND[variable], + disk) + for variable in config[CONF_MONITORED_CONDITIONS] + if variable in _STORAGE_DSK_MON_COND] + + add_devices_callback(sensors) + + # Wait until start event is sent to load this component. + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) + + +class SynoApi(): + """Class to interface with API.""" + + # pylint: disable=too-many-arguments, bare-except + def __init__(self, host, port, username, password, temp_unit): + """Constructor of the API wrapper class.""" + from SynologyDSM import SynologyDSM + self.temp_unit = temp_unit + + try: + self._api = SynologyDSM(host, + port, + username, + password) + except: + _LOGGER.error("Error setting up Synology DSM") + + def utilisation(self): + """Return utilisation information from API.""" + if self._api is not None: + return self._api.utilisation + + def storage(self): + """Return storage information from API.""" + if self._api is not None: + return self._api.storage + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update function for updating api information.""" + self._api.update() + + +class SynoNasSensor(Entity): + """Representation of a Synology Nas Sensor.""" + + def __init__(self, api, variable, variableInfo, monitor_device=None): + """Initialize the sensor.""" + self.var_id = variable + self.var_name = variableInfo[0] + self.var_units = variableInfo[1] + self.var_icon = variableInfo[2] + self.monitor_device = monitor_device + self._api = api + + @property + def name(self): + """Return the name of the sensor, if any.""" + if self.monitor_device is not None: + return "{} ({})".format(self.var_name, self.monitor_device) + else: + return self.var_name + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.var_icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if self.var_id in ['volume_disk_temp_avg', 'volume_disk_temp_max', + 'disk_temp']: + return self._api.temp_unit + else: + return self.var_units + + def update(self): + """Get the latest data for the states.""" + if self._api is not None: + self._api.update() + + +class SynoNasUtilSensor(SynoNasSensor): + """Representation a Synology Utilisation Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + network_sensors = ['network_up', 'network_down'] + memory_sensors = ['memory_size', 'memory_cached', + 'memory_available_swap', 'memory_available_real', + 'memory_total_swap', 'memory_total_real'] + + if self.var_id in network_sensors or self.var_id in memory_sensors: + attr = getattr(self._api.utilisation(), self.var_id)(False) + + if self.var_id in network_sensors: + return round(attr / 1024.0, 1) + elif self.var_id in memory_sensors: + return round(attr / 1024.0 / 1024.0, 1) + else: + return getattr(self._api.utilisation(), self.var_id) + + +class SynoNasStorageSensor(SynoNasSensor): + """Representation a Synology Utilisation Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + temp_sensors = ['volume_disk_temp_avg', 'volume_disk_temp_max', + 'disk_temp'] + + if self.monitor_device is not None: + if self.var_id in temp_sensors: + attr = getattr(self._api.storage(), + self.var_id)(self.monitor_device) + + if self._api.temp_unit == TEMP_CELSIUS: + return attr + else: + return round(attr * 1.8 + 32.0, 1) + else: + return getattr(self._api.storage(), + self.var_id)(self.monitor_device) diff --git a/requirements_all.txt b/requirements_all.txt index 9ba9390c03c..121597b9be9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,6 +427,9 @@ python-nmap==0.6.1 # homeassistant.components.notify.pushover python-pushover==0.2 +# homeassistant.components.sensor.synologydsm +python-synology==0.1.0 + # homeassistant.components.notify.telegram python-telegram-bot==5.2.0 From 214a18f08cc45e46660f0e049ac3ee9302066394 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Thu, 3 Nov 2016 05:20:21 +0100 Subject: [PATCH 128/149] Support for Dovado routers (#4176) * Implemented support for the Dovado router * Update .coveragerc --- .coveragerc | 1 + homeassistant/components/sensor/dovado.py | 174 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 178 insertions(+) create mode 100644 homeassistant/components/sensor/dovado.py diff --git a/.coveragerc b/.coveragerc index 294b6b1b747..d1ead447856 100644 --- a/.coveragerc +++ b/.coveragerc @@ -244,6 +244,7 @@ omit = homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py + homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py diff --git a/homeassistant/components/sensor/dovado.py b/homeassistant/components/sensor/dovado.py new file mode 100644 index 00000000000..1e1bf785760 --- /dev/null +++ b/homeassistant/components/sensor/dovado.py @@ -0,0 +1,174 @@ +""" +Support for Dovado router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dovado/ +""" +import logging +import re +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.util import slugify +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_HOST, CONF_PORT, + CONF_SENSORS, STATE_UNKNOWN) +from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['dovado==0.1.15'] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +SENSOR_UPLOAD = "upload" +SENSOR_DOWNLOAD = "download" +SENSOR_SIGNAL = "signal" +SENSOR_NETWORK = "network" +SENSOR_SMS_UNREAD = "sms" + +SENSORS = { + SENSOR_NETWORK: ("signal strength", "Network", None, + "mdi:access-point-network"), + SENSOR_SIGNAL: ("signal strength", "Signal Strength", "%", + "mdi:signal"), + SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", + "mdi:message-text-outline"), + SENSOR_UPLOAD: ("traffic modem tx", "Sent", "GiB", + "mdi:cloud-upload"), + SENSOR_DOWNLOAD: ("traffic modem rx", "Received", "GiB", + "mdi:cloud-download"), +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the dovado platform for sensors.""" + return Dovado().setup(hass, config, add_devices) + + +class Dovado: + """A connection to the router.""" + + def __init__(self): + """Initialize.""" + self.state = {} + self._dovado = None + + def setup(self, hass, config, add_devices): + """Setup the connection.""" + import dovado + self._dovado = dovado.Dovado( + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + config.get(CONF_HOST), + config.get(CONF_PORT)) + + if not self.update(): + return False + + def send_sms(service): + """Send SMS through the router.""" + number = service.data.get("number"), + message = service.data.get("message") + _LOGGER.debug("message for %s: %s", + number, message) + self._dovado.send_sms(number, message) + + if self.state["sms"] == "enabled": + service_name = slugify("{} {}".format(self.name, + "send_sms")) + hass.services.register(DOMAIN, service_name, send_sms) + + for sensor in SENSORS: + if sensor in config.get(CONF_SENSORS, [sensor]): + add_devices([DovadoSensor(self, sensor)]) + + return True + + @property + def name(self): + """Name of the router.""" + return self.state["product name"] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update device state.""" + _LOGGER.info("Updating") + try: + self.state = self._dovado.query_state() + self.state.update( + connected=self.state["modem status"] == "CONNECTED") + _LOGGER.debug("Received: %s", self.state) + return True + except OSError as error: + _LOGGER.error("Could not contact the router: %s", error) + return False + + +class DovadoSensor(Entity): + """Representation of a Dovado sensor.""" + + def __init__(self, dovado, sensor): + """Initialize the sensor.""" + self._dovado = dovado + self._sensor = sensor + + def update(self): + """Update sensor values.""" + self._dovado.update() + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format(self._dovado.name, + SENSORS[self._sensor][1]) + + @property + def state(self): + """Return the sensor state.""" + key = SENSORS[self._sensor][0] + result = self._dovado.state[key] + if self._sensor == SENSOR_NETWORK: + match = re.search(r"\((.+)\)", result) + return match.group(1) if match else STATE_UNKNOWN + elif self._sensor == SENSOR_SIGNAL: + try: + return int(result.split()[0]) + except ValueError: + return 0 + elif self._sensor == SENSOR_SMS_UNREAD: + return int(result) + elif self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]: + gib = pow(2, 30) + return round(int(result) / gib, 1) + else: + return result + + @property + def icon(self): + """Return the icon for the sensor.""" + return SENSORS[self._sensor][3] + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return SENSORS[self._sensor][2] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {k: v for k, v in self._dovado.state.items() + if k not in ["date", "time"]} diff --git a/requirements_all.txt b/requirements_all.txt index 121597b9be9..6b02b09d95c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,6 +78,9 @@ distro==1.0.0 # homeassistant.components.notify.xmpp dnspython3==1.15.0 +# homeassistant.components.sensor.dovado +dovado==0.1.15 + # homeassistant.components.dweet # homeassistant.components.sensor.dweet dweepy==0.2.0 From 79fa2d41752444f86e40524f43e0dc0afe7dd4dc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 3 Nov 2016 09:31:50 +0100 Subject: [PATCH 129/149] CUPS sensor (#4142) * Add CUPS sensor * Use CupsData * Fix requirement --- .coveragerc | 1 + homeassistant/components/sensor/cups.py | 149 ++++++++++++++++++++++++ requirements_all.txt | 3 + script/gen_requirements_all.py | 1 + 4 files changed, 154 insertions(+) create mode 100644 homeassistant/components/sensor/cups.py diff --git a/.coveragerc b/.coveragerc index d1ead447856..cd86d001e37 100644 --- a/.coveragerc +++ b/.coveragerc @@ -240,6 +240,7 @@ omit = homeassistant/components/sensor/bom.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py + homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deutsche_bahn.py diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py new file mode 100644 index 00000000000..1ad26e85261 --- /dev/null +++ b/homeassistant/components/sensor/cups.py @@ -0,0 +1,149 @@ +""" +Details about printers which are connected to CUPS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.cups/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pycups==1.9.73'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEVICE_URI = 'device_uri' +ATTR_PRINTER_INFO = 'printer_info' +ATTR_PRINTER_IS_SHARED = 'printer_is_shared' +ATTR_PRINTER_LOCATION = 'printer_location' +ATTR_PRINTER_MODEL = 'printer_model' +ATTR_PRINTER_STATE_MESSAGE = 'printer_state_message' +ATTR_PRINTER_STATE_REASON = 'printer_state_reason' +ATTR_PRINTER_TYPE = 'printer_type' +ATTR_PRINTER_URI_SUPPORTED = 'printer_uri_supported' + +CONF_PRINTERS = 'printers' + +DEFAULT_HOST = '127.0.0.1' +DEFAULT_PORT = 631 + +ICON = 'mdi:printer' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +PRINTER_STATES = { + 3: 'idle', + 4: 'printing', + 5: 'stopped', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the CUPS sensor.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + printers = config.get(CONF_PRINTERS) + + try: + data = CupsData(host, port) + data.update() + except RuntimeError: + _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) + return False + + dev = [] + for printer in printers: + if printer in data.printers: + dev.append(CupsSensor(data, printer)) + else: + _LOGGER.error("Printer is not present: %s", printer) + continue + + add_devices(dev) + + +class CupsSensor(Entity): + """Representation of a CUPS sensor.""" + + def __init__(self, data, printer): + """Initialize the CUPS sensor.""" + self.data = data + self._name = printer + self._printer = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + if self._printer is not None: + try: + return next(v for k, v in PRINTER_STATES.items() + if self._printer['printer-state'] == k) + except StopIteration: + return self._printer['printer-state'] + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + if self._printer is not None: + return { + ATTR_DEVICE_URI: self._printer['device-uri'], + ATTR_PRINTER_INFO: self._printer['printer-info'], + ATTR_PRINTER_IS_SHARED: self._printer['printer-is-shared'], + ATTR_PRINTER_LOCATION: self._printer['printer-location'], + ATTR_PRINTER_MODEL: self._printer['printer-make-and-model'], + ATTR_PRINTER_STATE_MESSAGE: + self._printer['printer-state-message'], + ATTR_PRINTER_STATE_REASON: + self._printer['printer-state-reasons'], + ATTR_PRINTER_TYPE: self._printer['printer-type'], + ATTR_PRINTER_URI_SUPPORTED: + self._printer['printer-uri-supported'], + } + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + self._printer = self.data.printers.get(self._name) + + +# pylint: disable=import-error +class CupsData(object): + """Get the latest data from CUPS and update the state.""" + + def __init__(self, host, port): + """Initialize the data object.""" + self._host = host + self._port = port + self.printers = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from CUPS.""" + from cups import Connection + + conn = Connection(host=self._host, port=self._port) + self.printers = conn.getPrinters() diff --git a/requirements_all.txt b/requirements_all.txt index 6b02b09d95c..af5519f1d15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -351,6 +351,9 @@ pychromecast==0.7.6 # homeassistant.components.media_player.cmus pycmus==0.1.0 +# homeassistant.components.sensor.cups +# pycups==1.9.73 + # homeassistant.components.envisalink # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a5a25e7bab4..e16ee5996de 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -17,6 +17,7 @@ COMMENT_REQUIREMENTS = ( 'gattlib', 'pyuserinput', 'evdev', + 'pycups', ) IGNORE_PACKAGES = ( From c2a5f63b1f3a9fe833b7816286b3df72c264b721 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Nov 2016 10:09:03 +0100 Subject: [PATCH 130/149] Bugfix async Yr.no (#4190) --- homeassistant/components/sensor/yr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 05412131679..51616062475 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -57,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +@asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the Yr.no sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) From fcf318cf53de7efe38ab15607a2affefa9eb3eb0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Nov 2016 11:07:47 +0100 Subject: [PATCH 131/149] Bugfix windows have a other default loop now (#4195) * Bugfix windows have a other default loop now * fix handling with 3.4.2 that not support ensure_future * make the same as ensure_future does * fix spell * fix lazy test --- homeassistant/bootstrap.py | 8 ++++---- tests/components/test_group.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fdcdb5d4fe2..31e404ad87a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -352,8 +352,8 @@ def from_config_dict(config: Dict[str, Any], future.set_exception(exc) # run task - future = asyncio.Future() - asyncio.Task(_async_init_from_config_dict(future), loop=hass.loop) + future = asyncio.Future(loop=hass.loop) + hass.loop.create_task(_async_init_from_config_dict(future)) hass.loop.run_until_complete(future) return future.result() @@ -452,8 +452,8 @@ def from_config_file(config_path: str, future.set_exception(exc) # run task - future = asyncio.Future() - asyncio.Task(_async_init_from_config_file(future), loop=hass.loop) + future = asyncio.Future(loop=hass.loop) + hass.loop.create_task(_async_init_from_config_file(future)) hass.loop.run_until_complete(future) return future.result() diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 9b6d96d898f..786fee16624 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -368,9 +368,11 @@ class TestComponentsGroup(unittest.TestCase): # Hide the group group.set_visibility(self.hass, group_entity_id, False) group_state = self.hass.states.get(group_entity_id) + self.hass.block_till_done() self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) # Show it again group.set_visibility(self.hass, group_entity_id, True) group_state = self.hass.states.get(group_entity_id) + self.hass.block_till_done() self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) From 15dde7925af3394726b8e5009f2ea111e769cba6 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Thu, 3 Nov 2016 13:08:23 +0100 Subject: [PATCH 132/149] Prevent multiple instances of device initialzed (#4179) --- homeassistant/components/cover/zwave.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 9caac4522ff..2794995abb1 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -32,17 +32,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \ - and value.index == 0: + if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL + and value.index == 0): value.set_change_verified(False) add_devices([ZwaveRollershutter(value)]) - elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \ - node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): - if value.type != zwave.const.TYPE_BOOL and \ - value.genre != zwave.const.GENRE_USER: - return - value.set_change_verified(False) - add_devices([ZwaveGarageDoor(value)]) + elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL: + if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or + value.command_class == + zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): + if (value.type != zwave.const.TYPE_BOOL and + value.genre != zwave.const.GENRE_USER): + return + value.set_change_verified(False) + add_devices([ZwaveGarageDoor(value)]) else: return From ee5f2283092c3ba0c5bb1292cff58883472059d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2016 18:32:14 -0700 Subject: [PATCH 133/149] Make services yield (#4187) * Make services yield * Disable pylint abstract-method check * add input_select * add input_slider * change to async vers. * fix lint * yield on add_entities as other components does --- .../alarm_control_panel/alarmdotcom.py | 1 - .../components/alarm_control_panel/manual.py | 1 - .../components/alarm_control_panel/mqtt.py | 1 - .../alarm_control_panel/simplisafe.py | 1 - .../alarm_control_panel/verisure.py | 1 - .../components/automation/__init__.py | 11 +++---- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/camera/ffmpeg.py | 2 +- homeassistant/components/camera/generic.py | 2 +- homeassistant/components/camera/mjpeg.py | 2 +- homeassistant/components/camera/synology.py | 2 +- homeassistant/components/climate/ecobee.py | 1 - .../components/climate/eq3btsmart.py | 2 +- .../components/climate/generic_thermostat.py | 1 - homeassistant/components/climate/heatmiser.py | 1 - homeassistant/components/climate/homematic.py | 1 - homeassistant/components/climate/honeywell.py | 2 -- homeassistant/components/climate/nest.py | 1 - homeassistant/components/climate/netatmo.py | 1 - homeassistant/components/climate/proliphix.py | 1 - .../components/climate/radiotherm.py | 1 - homeassistant/components/climate/vera.py | 1 - homeassistant/components/climate/zwave.py | 1 - homeassistant/components/cover/homematic.py | 1 - homeassistant/components/cover/rfxtrx.py | 1 - homeassistant/components/cover/rpi_gpio.py | 1 - homeassistant/components/cover/vera.py | 1 - homeassistant/components/emulated_hue.py | 8 ++--- homeassistant/components/fan/__init__.py | 2 +- homeassistant/components/group.py | 15 +++++---- homeassistant/components/http.py | 10 +++--- homeassistant/components/input_boolean.py | 29 +++++++++-------- homeassistant/components/input_select.py | 32 +++++++++++-------- homeassistant/components/input_slider.py | 13 ++++---- homeassistant/components/light/__init__.py | 2 +- .../components/light/limitlessled.py | 2 +- homeassistant/components/light/mysensors.py | 2 +- homeassistant/components/lock/verisure.py | 1 - .../components/media_player/__init__.py | 2 +- .../components/media_player/braviatv.py | 1 - homeassistant/components/media_player/cast.py | 1 - homeassistant/components/media_player/cmus.py | 2 +- homeassistant/components/media_player/demo.py | 8 ++--- .../components/media_player/denon.py | 1 - .../components/media_player/directv.py | 1 - homeassistant/components/media_player/emby.py | 2 +- .../components/media_player/firetv.py | 1 - .../components/media_player/gpmdp.py | 1 - .../components/media_player/itunes.py | 1 - homeassistant/components/media_player/kodi.py | 1 - .../components/media_player/lg_netcast.py | 1 - .../components/media_player/mpchc.py | 1 - homeassistant/components/media_player/mpd.py | 2 +- .../components/media_player/onkyo.py | 1 - .../media_player/panasonic_viera.py | 1 - .../components/media_player/pandora.py | 1 - .../components/media_player/philips_js.py | 1 - .../components/media_player/pioneer.py | 1 - homeassistant/components/media_player/plex.py | 1 - homeassistant/components/media_player/roku.py | 2 -- .../components/media_player/russound_rnet.py | 1 - .../components/media_player/samsungtv.py | 1 - .../components/media_player/snapcast.py | 1 - .../components/media_player/sonos.py | 1 - .../components/media_player/squeezebox.py | 1 - .../components/media_player/webostv.py | 1 - .../components/media_player/yamaha.py | 1 - homeassistant/components/sensor/lastfm.py | 1 - .../components/sensor/steam_online.py | 1 - .../components/sensor/supervisord.py | 1 - homeassistant/components/sensor/twitch.py | 1 - homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/switch/arest.py | 2 +- homeassistant/helpers/entity.py | 28 +++++++++++++--- pylintrc | 2 ++ 75 files changed, 108 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 714741d7e1e..8bf36e176e5 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -42,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([AlarmDotCom(hass, name, code, username, password)]) -# pylint: disable=abstract-method class AlarmDotCom(alarm.AlarmControlPanel): """Represent an Alarm.com status.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 2af0c1499f6..9a7efbeaf5a 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=abstract-method class ManualAlarm(alarm.AlarmControlPanel): """ Represents an alarm status. diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 558653aa6a6..26e2a2f1f77 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -55,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_CODE))]) -# pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 38128489ba0..40ebfb2f39f 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([SimpliSafeAlarm(name, username, password, code)]) -# pylint: disable=abstract-method class SimpliSafeAlarm(alarm.AlarmControlPanel): """Representation a SimpliSafe alarm.""" diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 248d575baf7..4ef07c68f59 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(alarms) -# pylint: disable=abstract-method class VerisureAlarm(alarm.AlarmControlPanel): """Represent a Verisure alarm status.""" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 27b1fa9cd13..e88caab6824 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -218,7 +218,6 @@ def async_setup(hass, config): class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" - # pylint: disable=abstract-method def __init__(self, name, async_attach_triggers, cond_func, async_action, hidden): """Initialize an automation entity.""" @@ -265,7 +264,7 @@ class AutomationEntity(ToggleEntity): return yield from self.async_enable() - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() @asyncio.coroutine def async_turn_off(self, **kwargs) -> None: @@ -276,8 +275,6 @@ class AutomationEntity(ToggleEntity): self._async_detach_triggers() self._async_detach_triggers = None self._enabled = False - # It's important that the update is finished before this method - # ends because async_remove depends on it. yield from self.async_update_ha_state() @asyncio.coroutine @@ -289,7 +286,7 @@ class AutomationEntity(ToggleEntity): if skip_condition or self._cond_func(variables): yield from self._async_action(self.entity_id, variables) self._last_triggered = utcnow() - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() @asyncio.coroutine def async_remove(self): @@ -352,7 +349,7 @@ def _async_process_config(hass, config, component): entities.append(entity) yield from asyncio.gather(*tasks, loop=hass.loop) - hass.loop.create_task(component.async_add_entities(entities)) + yield from component.async_add_entities(entities) return len(entities) > 0 @@ -367,7 +364,7 @@ def _async_get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.async_log_entry( hass, name, 'has been triggered', DOMAIN, entity_id) - hass.loop.create_task(script_obj.async_run(variables)) + yield from script_obj.async_run(variables) return action diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d2ca0b50801..d02e7954349 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -133,7 +133,7 @@ class Camera(Entity): yield from asyncio.sleep(.5) finally: - self.hass.loop.create_task(response.write_eof()) + yield from response.write_eof() @property def state(self): diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 9bcb0c735a9..8e238bfdea7 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -87,7 +87,7 @@ class FFmpegCamera(Camera): response.write(data) finally: self.hass.loop.create_task(stream.close()) - self.hass.loop.create_task(response.write_eof()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index b1502778878..c6664ed70b2 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -114,7 +114,7 @@ class GenericCamera(Camera): auth=self._auth ) self._last_image = yield from respone.read() - self.hass.loop.create_task(respone.release()) + yield from respone.release() except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') return self._last_image diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index ea83ded4371..81759fa86df 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -124,7 +124,7 @@ class MjpegCamera(Camera): response.write(data) finally: self.hass.loop.create_task(stream.release()) - self.hass.loop.create_task(response.write_eof()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 77e1b3ee4d0..4d5020ec075 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -284,7 +284,7 @@ class SynologyCamera(Camera): response.write(data) finally: self.hass.loop.create_task(stream.release()) - self.hass.loop.create_task(response.write_eof()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 6193b955a61..c98ac6d0106 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=SET_FAN_MIN_ON_TIME_SCHEMA) -# pylint: disable=abstract-method class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index f6f0497c4af..72bd0b22522 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=import-error, abstract-method +# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of a eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 64448e9677c..3030ea9090e 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -62,7 +62,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): target_temp, ac_mode, min_cycle_duration)]) -# pylint: disable=abstract-method class GenericThermostat(ClimateDevice): """Representation of a GenericThermostat device.""" diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index 9f589b4c015..28d419597d3 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - # pylint: disable=abstract-method def __init__(self, heatmiser, device, name, serport): """Initialize the thermostat.""" self.heatmiser = heatmiser diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 7113779eb57..9be4e7a4886 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): ) -# pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ClimateDevice): """Representation of a Homematic thermostat.""" diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 09b5d92b9b6..540a9a941d1 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices): class RoundThermostat(ClimateDevice): """Representation of a Honeywell Round Connected thermostat.""" - # pylint: disable=abstract-method def __init__(self, device, zone_id, master, away_temp): """Initialize the thermostat.""" self.device = device @@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice): self._is_dhw = False -# pylint: disable=abstract-method class HoneywellUSThermostat(ClimateDevice): """Representation of a Honeywell US Thermostat.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index f9ac15e7d80..5020bb441d5 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -30,7 +30,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for structure, device in nest.devices()]) -# pylint: disable=abstract-method class NestThermostat(ClimateDevice): """Representation of a Nest thermostat.""" diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index b0a5059ef44..163054cd121 100755 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): return None -# pylint: disable=abstract-method class NetatmoThermostat(ClimateDevice): """Representation a Netatmo thermostat.""" diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 6aeee6e537c..515c43f7eba 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ProliphixThermostat(pdp)]) -# pylint: disable=abstract-method class ProliphixThermostat(ClimateDevice): """Representation a Proliphix thermostat.""" diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 90a1701536c..c2d712e19bd 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -58,7 +58,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(tstats) -# pylint: disable=abstract-method class RadioThermostat(ClimateDevice): """Representation of a Radio Thermostat.""" diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index b8b03f8dda9..fa4244497e6 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -31,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device in VERA_DEVICES['climate']) -# pylint: disable=abstract-method class VeraThermostat(VeraDevice, ClimateDevice): """Representation of a Vera Thermostat.""" diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 7dcd72cd37c..d94c4f1b94b 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -69,7 +69,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info, zwave.NETWORK) -# pylint: disable=abstract-method class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Represents a ZWave Climate device.""" diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index aea05a9160a..189c501aad5 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): ) -# pylint: disable=abstract-method class HMCover(homematic.HMDevice, CoverDevice): """Represents a Homematic Cover in Home Assistant.""" diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index d7ca03f5762..a016103a8fd 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -# pylint: disable=abstract-method class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): """Representation of an rfxtrx cover.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 39a82b5b3fc..4cd4e74be06 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=abstract-method class RPiGPIOCover(CoverDevice): """Representation of a Raspberry GPIO cover.""" diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index 57b85eca981..2c26fbf1723 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -22,7 +22,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device in VERA_DEVICES['cover']) -# pylint: disable=abstract-method class VeraCover(VeraDevice, CoverDevice): """Represents a Vera Cover in Home Assistant.""" diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 6aebb91f72f..187ee0de603 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -86,19 +86,19 @@ def setup(hass, yaml_config): upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port) - @core.callback + @asyncio.coroutine def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" upnp_listener.stop() - hass.loop.create_task(server.stop()) + yield from server.stop() - @core.callback + @asyncio.coroutine def start_emulated_hue_bridge(event): """Start the emulated hue bridge.""" - hass.loop.create_task(server.start()) upnp_listener.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + yield from server.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index b3c210285e8..79793435625 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -196,7 +196,7 @@ def setup(hass, config: dict) -> None: class FanEntity(ToggleEntity): """Representation of a fan.""" - # pylint: disable=no-self-use, abstract-method + # pylint: disable=no-self-use def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan.""" diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 3843c1b4854..f57c56f17db 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -175,15 +175,16 @@ def async_setup(hass, config): conf = yield from component.async_prepare_reload() if conf is None: return - hass.loop.create_task(_async_process_config(hass, conf, component)) + yield from _async_process_config(hass, conf, component) - @callback + @asyncio.coroutine def visibility_service_handler(service): """Change visibility of a group.""" visible = service.data.get(ATTR_VISIBLE) - for group in component.async_extract_from_service( - service, expand_group=False): - group.async_set_visible(visible) + tasks = [group.async_set_visible(visible) for group + in component.async_extract_from_service(service, + expand_group=False)] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -291,12 +292,12 @@ class Group(Entity): """Return the icon of the group.""" return self._icon - @callback + @asyncio.coroutine def async_set_visible(self, visible): """Change visibility of the group.""" if self._visible != visible: self._visible = visible - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() @property def hidden(self): diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index da2f0ac06f0..89e15d50a5b 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -22,7 +22,7 @@ from aiohttp.web_exceptions import ( HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified) from aiohttp.web_urldispatcher import StaticRoute -from homeassistant.core import callback, is_callback +from homeassistant.core import is_callback import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( @@ -141,16 +141,16 @@ def setup(hass, config): trusted_networks=trusted_networks ) - @callback + @asyncio.coroutine def stop_server(event): """Callback to stop the server.""" - hass.loop.create_task(server.stop()) + yield from server.stop() - @callback + @asyncio.coroutine def start_server(event): """Callback to start the server.""" - hass.loop.create_task(server.start()) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) + yield from server.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index fdc514f957f..1a510fbf6ec 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -9,7 +9,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, STATE_ON) @@ -77,18 +76,20 @@ def async_setup(hass, config): if not entities: return False - @callback + @asyncio.coroutine def async_handler_service(service): """Handle a calls to the input boolean services.""" target_inputs = component.async_extract_from_service(service) - for input_b in target_inputs: - if service.service == SERVICE_TURN_ON: - input_b.turn_on() - elif service.service == SERVICE_TURN_OFF: - input_b.turn_off() - else: - input_b.toggle() + if service.service == SERVICE_TURN_ON: + attr = 'async_turn_on' + elif service.service == SERVICE_TURN_OFF: + attr = 'async_turn_off' + else: + attr = 'async_toggle' + + tasks = [getattr(input_b, attr)() for input_b in target_inputs] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA) @@ -131,12 +132,14 @@ class InputBoolean(ToggleEntity): """Return true if entity is on.""" return self._state - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index d725a1129cf..61385c46cd6 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -9,7 +9,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -105,37 +104,40 @@ def async_setup(hass, config): if not entities: return False - @callback + @asyncio.coroutine def async_select_option_service(call): """Handle a calls to the input select option service.""" target_inputs = component.async_extract_from_service(call) - for input_select in target_inputs: - input_select.select_option(call.data[ATTR_OPTION]) + tasks = [input_select.async_select_option(call.data[ATTR_OPTION]) + for input_select in target_inputs] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service, schema=SERVICE_SELECT_OPTION_SCHEMA) - @callback + @asyncio.coroutine def async_select_next_service(call): """Handle a calls to the input select next service.""" target_inputs = component.async_extract_from_service(call) - for input_select in target_inputs: - input_select.offset_index(1) + tasks = [input_select.async_offset_index(1) + for input_select in target_inputs] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service, schema=SERVICE_SELECT_NEXT_SCHEMA) - @callback + @asyncio.coroutine def async_select_previous_service(call): """Handle a calls to the input select previous service.""" target_inputs = component.async_extract_from_service(call) - for input_select in target_inputs: - input_select.offset_index(-1) + tasks = [input_select.async_offset_index(-1) + for input_select in target_inputs] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, @@ -183,18 +185,20 @@ class InputSelect(Entity): ATTR_OPTIONS: self._options, } - def select_option(self, option): + @asyncio.coroutine + def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning('Invalid option: %s (possible options: %s)', option, ', '.join(self._options)) return self._current_option = option - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() - def offset_index(self, offset): + @asyncio.coroutine + def async_offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index f5ac8ead91c..2a942829517 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -9,7 +9,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -95,13 +94,14 @@ def async_setup(hass, config): if not entities: return False - @callback + @asyncio.coroutine def async_select_value_service(call): """Handle a calls to the input slider services.""" target_inputs = component.async_extract_from_service(call) - for input_slider in target_inputs: - input_slider.select_value(call.data[ATTR_VALUE]) + tasks = [input_slider.async_select_value(call.data[ATTR_VALUE]) + for input_slider in target_inputs] + yield from asyncio.gather(*tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service, @@ -160,7 +160,8 @@ class InputSlider(Entity): ATTR_STEP: self._step } - def select_value(self, value): + @asyncio.coroutine + def async_select_value(self, value): """Select new value.""" num_value = float(value) if num_value < self._minimum or num_value > self._maximum: @@ -168,4 +169,4 @@ class InputSlider(Entity): num_value, self._minimum, self._maximum) return self._current_value = num_value - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8cd4292908a..e3437d89e72 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -265,7 +265,7 @@ def setup(hass, config): class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use, abstract-method + # pylint: disable=no-self-use @property def brightness(self): diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 421696d22ba..8e0ea5cee83 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,7 +4,7 @@ Support for LimitlessLED bulbs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ -# pylint: disable=abstract-method + import logging import voluptuous as vol diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 479fb717213..3bd53ff9064 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,7 +4,7 @@ Support for MySensors lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mysensors/ """ -# pylint: disable=abstract-method + import logging from homeassistant.components import mysensors diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py index d758f4dc91d..7e73ceb680e 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/lock/verisure.py @@ -27,7 +27,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(locks) -# pylint: disable=abstract-method class VerisureDoorlock(LockDevice): """Representation of a Verisure doorlock.""" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7988064183a..c689cdbccc4 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -710,7 +710,7 @@ def _async_fetch_image(hass, url): if response.status == 200: content = yield from response.read() content_type = response.headers.get(CONTENT_TYPE_HEADER) - hass.loop.create_task(response.release()) + yield from response.release() except asyncio.TimeoutError: pass diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index f55f1e6021c..d1c60bf2ec1 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -180,7 +180,6 @@ def request_configuration(config, hass, add_devices): ) -# pylint: disable=abstract-method class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 9ee259a54ab..5d6289587be 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -88,7 +88,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CastDevice(MediaPlayerDevice): """Representation of a Cast device on the network.""" - # pylint: disable=abstract-method def __init__(self, chromecast): """Initialize the Cast device.""" self.cast = chromecast diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index dc623a274b5..16f3360a2ad 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): class CmusDevice(MediaPlayerDevice): """Representation of a running cmus.""" - # pylint: disable=no-member, abstract-method + # pylint: disable=no-member def __init__(self, server, password, port, name): """Initialize the CMUS device.""" from pycmus import remote diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index d59e6ef77d8..1c1687de319 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -42,7 +42,7 @@ class AbstractDemoPlayer(MediaPlayerDevice): """A demo media players.""" # We only implement the methods that we support - # pylint: disable=abstract-method + def __init__(self, name): """Initialize the demo device.""" self._name = name @@ -110,7 +110,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): """A Demo media player that only supports YouTube.""" # We only implement the methods that we support - # pylint: disable=abstract-method + def __init__(self, name, youtube_id=None, media_title=None): """Initialize the demo device.""" super().__init__(name) @@ -162,7 +162,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): """A Demo media player that only supports YouTube.""" # We only implement the methods that we support - # pylint: disable=abstract-method + tracks = [ ('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'), ('Paul Elstak', 'Luv U More'), @@ -269,7 +269,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """A Demo media player that only supports YouTube.""" # We only implement the methods that we support - # pylint: disable=abstract-method + def __init__(self): """Initialize the demo device.""" super().__init__('Lounge room') diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index e04b9ee3931..96cf0b99462 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DenonDevice(MediaPlayerDevice): """Representation of a Denon device.""" - # pylint: disable=abstract-method def __init__(self, name, host): """Initialize the Denon device.""" self._name = name diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index f1f22693e6b..397014992ea 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -65,7 +65,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DirecTvDevice(MediaPlayerDevice): """Representation of a DirecTV reciever on the network.""" - # pylint: disable=abstract-method def __init__(self, name, host, port): """Initialize the device.""" from DirectPy import DIRECTV diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 3422fadbc10..5349e74ed40 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -110,7 +110,7 @@ class EmbyClient(MediaPlayerDevice): """Representation of a Emby device.""" # pylint: disable=too-many-arguments, too-many-public-methods, - # pylint: disable=abstract-method + def __init__(self, client, device, emby_sessions, update_devices, update_sessions): """Initialize the Emby device.""" diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 518982a7038..c8cdc9e7422 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -104,7 +104,6 @@ class FireTV(object): class FireTVDevice(MediaPlayerDevice): """Representation of an Amazon Fire TV device on the network.""" - # pylint: disable=abstract-method def __init__(self, host, port, device, name): """Initialize the FireTV device.""" self._firetv = FireTV(host, port, device) diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index db1732f4288..f81c63e71a1 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -170,7 +170,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class GPMDP(MediaPlayerDevice): """Representation of a GPMDP.""" - # pylint: disable=abstract-method def __init__(self, name, url, code): """Initialize the media player.""" from websocket import create_connection diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 2ccc95c3243..7b869e14267 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -153,7 +153,6 @@ class Itunes(object): return self._request('PUT', path, {'level': level}) -# pylint: disable=unused-argument, abstract-method def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the iTunes platform.""" add_devices([ diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index e88770a22e7..1b2bc4f7fc7 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class KodiDevice(MediaPlayerDevice): """Representation of a XBMC/Kodi device.""" - # pylint: disable=abstract-method def __init__(self, name, url, auth=None, turn_off_action=None): """Initialize the Kodi device.""" import jsonrpc_requests diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 0def17a7dca..1f15153dbe8 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -52,7 +52,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([LgTVDevice(client, config[CONF_NAME])]) -# pylint: disable=abstract-method class LgTVDevice(MediaPlayerDevice): """Representation of a LG TV.""" diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index 8563b551a09..0cbd548f23f 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([MpcHcDevice(name, url)]) -# pylint: disable=abstract-method class MpcHcDevice(MediaPlayerDevice): """Representation of a MPC-HC server.""" diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 844be4a7a08..9967a26a3a0 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MpdDevice(MediaPlayerDevice): """Representation of a MPD server.""" - # pylint: disable=no-member, abstract-method + # pylint: disable=no-member def __init__(self, server, port, location, password): """Initialize the MPD device.""" import mpd diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 44afc0f8ad2..5829d119f95 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - # pylint: disable=abstract-method def __init__(self, receiver, sources, name=None): """Initialize the Onkyo Receiver.""" self._receiver = receiver diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index a98e54fd6c9..d1a971eb91e 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=abstract-method class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index d10b9f685b5..c97b20ee4bd 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PandoraMediaPlayer(MediaPlayerDevice): """A media player that uses the Pianobar interface to Pandora.""" - # pylint: disable=abstract-method def __init__(self, name): """Initialize the demo device.""" MediaPlayerDevice.__init__(self) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index af438d7dbec..02e520bb549 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([PhilipsTV(tvapi, name)]) -# pylint: disable=abstract-method class PhilipsTV(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 524c2c4520e..14e4c753765 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PioneerDevice(MediaPlayerDevice): """Representation of a Pioneer device.""" - # pylint: disable=abstract-method def __init__(self, name, host, port, timeout): """Initialize the Pioneer device.""" self._name = name diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 827665929c5..76278722291 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -60,7 +60,6 @@ def config_from_file(filename, config=None): return {} -# pylint: disable=abstract-method def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the Plex platform.""" config = config_from_file(hass.config.path(PLEX_CONFIG_FILE)) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index aff49d0a5be..dfeb6196750 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=abstract-method def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Roku platform.""" hosts = [] @@ -65,7 +64,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RokuDevice(MediaPlayerDevice): """Representation of a Roku device on the network.""" - # pylint: disable=abstract-method def __init__(self, host): """Initialize the Roku device.""" from roku import Roku diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index df8e66457c5..3128aebe04f 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('Not connected to %s:%s', host, port) -# pylint: disable=abstract-method class RussoundRNETDevice(MediaPlayerDevice): """Representation of a Russound RNET device.""" diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index a680b32b9b0..e384fd4bd3f 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([SamsungTVDevice(name, remote_config)]) -# pylint: disable=abstract-method class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 2be3c36816c..37e9c4d3109 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -52,7 +52,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SnapcastDevice(MediaPlayerDevice): """Representation of a Snapcast client device.""" - # pylint: disable=abstract-method def __init__(self, client): """Initialize the Snapcast device.""" self._client = client diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 1b8a0160e56..f40058ef883 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -248,7 +248,6 @@ class _ProcessSonosEventQueue(): self._sonos_device.process_sonos_event(item) -# pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """Representation of a Sonos device.""" diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 2e09087f012..ee21e67bf49 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -174,7 +174,6 @@ class LogitechMediaServer(object): class SqueezeBoxDevice(MediaPlayerDevice): """Representation of a SqueezeBox device.""" - # pylint: disable=abstract-method def __init__(self, lms, player_id): """Initialize the SqeezeBox device.""" super(SqueezeBoxDevice, self).__init__() diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index bc3133d0564..e131cad97cd 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -141,7 +141,6 @@ def request_configuration(host, name, customize, hass, add_devices): ) -# pylint: disable=abstract-method class LgWebOSDevice(MediaPlayerDevice): """Representation of a LG WebOS TV.""" diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index c3b5b83ec9a..94191862f44 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -85,7 +85,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class YamahaDevice(MediaPlayerDevice): """Representation of a Yamaha device.""" - # pylint: disable=abstract-method def __init__(self, name, receiver, source_ignore, source_names): """Initialize the Yamaha Receiver.""" self._receiver = receiver diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 038e8389d47..5d660f20217 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LastfmSensor(Entity): """A class for the Last.fm account.""" - # pylint: disable=abstract-method def __init__(self, user, lastfm): """Initialize the sensor.""" self._user = lastfm.get_user(user) diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index ed12d4f7844..c5427e7b8ba 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SteamSensor(Entity): """A class for the Steam account.""" - # pylint: disable=abstract-method def __init__(self, account, steamod): """Initialize the sensor.""" self._steamod = steamod diff --git a/homeassistant/components/sensor/supervisord.py b/homeassistant/components/sensor/supervisord.py index 22c1285a547..fae7032ea58 100644 --- a/homeassistant/components/sensor/supervisord.py +++ b/homeassistant/components/sensor/supervisord.py @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SupervisorProcessSensor(Entity): """Representation of a supervisor-monitored process.""" - # pylint: disable=abstract-method def __init__(self, info, server): """Initialize the sensor.""" self._info = info diff --git a/homeassistant/components/sensor/twitch.py b/homeassistant/components/sensor/twitch.py index 73e2d221cb1..249d18ce6cb 100644 --- a/homeassistant/components/sensor/twitch.py +++ b/homeassistant/components/sensor/twitch.py @@ -42,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TwitchSensor(Entity): """Representation of an Twitch channel.""" - # pylint: disable=abstract-method def __init__(self, channel): """Initialize the sensor.""" self._channel = channel diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 60b9c9fdcd8..1f92b458d53 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -108,7 +108,7 @@ def setup(hass, config): class SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use, abstract-method + # pylint: disable=no-self-use @property def current_power_mwh(self): """Return the current power usage in mWh.""" diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 76f5bc7b580..9ae33698fa4 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -4,7 +4,7 @@ Support for an exposed aREST RESTful API of a device. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.arest/ """ -# pylint: disable=abstract-method + import logging import requests diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 27f180a72ca..95ac50770cf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -306,16 +306,36 @@ class ToggleEntity(Entity): raise NotImplementedError() def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + run_coroutine_threadsafe(self.async_turn_on(**kwargs), + self.hass.loop).result() + + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Turn the entity on.""" raise NotImplementedError() def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + run_coroutine_threadsafe(self.async_turn_off(**kwargs), + self.hass.loop).result() + + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Turn the entity off.""" raise NotImplementedError() - def toggle(self, **kwargs) -> None: - """Toggle the entity off.""" + def toggle(self) -> None: + """Toggle the entity.""" if self.is_on: - self.turn_off(**kwargs) + self.turn_off() else: - self.turn_on(**kwargs) + self.turn_on() + + @asyncio.coroutine + def async_toggle(self): + """Toggle the entity.""" + if self.is_on: + yield from self.async_turn_off() + else: + yield from self.async_turn_on() diff --git a/pylintrc b/pylintrc index 710f392e95f..9a46acc6a56 100644 --- a/pylintrc +++ b/pylintrc @@ -12,6 +12,7 @@ reports=no # redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing disable= locally-disabled, @@ -30,6 +31,7 @@ disable= too-many-return-statements, too-many-statements, too-few-public-methods, + abstract-method [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError From e5d69feb93c6e67cf45072383b830406e33dafb7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Nov 2016 02:33:18 +0100 Subject: [PATCH 134/149] Fix blocking/stack trace with empty list (#4191) --- homeassistant/helpers/entity_component.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 30d62608f9b..a648de1d650 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -296,6 +296,10 @@ class EntityPlatform(object): tasks = [self._async_process_entity(entity, update_before_add) for entity in new_entities] + # handle empty list from component/platform + if not tasks: + return + yield from asyncio.gather(*tasks, loop=self.component.hass.loop) yield from self.component.async_update_group() From c128919b5fb002cab0831181f89f19377ad75a8e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 Nov 2016 02:40:43 +0100 Subject: [PATCH 135/149] Remove globally disabled pylint warnings (#4204) --- homeassistant/__main__.py | 3 +-- homeassistant/components/http.py | 2 +- homeassistant/components/logger.py | 2 +- homeassistant/components/media_player/sonos.py | 1 - homeassistant/components/pilight.py | 1 - homeassistant/components/sensor/glances.py | 1 - homeassistant/components/sensor/hddtemp.py | 1 - homeassistant/util/__init__.py | 2 +- 8 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e5305245b18..510b98b6bdd 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -45,8 +45,7 @@ def monkey_patch_asyncio(): See https://bugs.python.org/issue26617 for details of the Python bug. """ - # pylint: disable=no-self-use, too-few-public-methods, protected-access - # pylint: disable=bare-except + # pylint: disable=no-self-use, protected-access, bare-except import asyncio.tasks class IgnoreCalls: diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 89e15d50a5b..5886693c64f 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -166,7 +166,7 @@ def setup(hass, config): class GzipFileSender(FileSender): """FileSender class capable of sending gzip version if available.""" - # pylint: disable=invalid-name, too-few-public-methods + # pylint: disable=invalid-name development = False diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 2e772376ae0..4bf163ff9eb 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -40,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema({ class HomeAssistantLogFilter(logging.Filter): """A log filter.""" - # pylint: disable=no-init,too-few-public-methods + # pylint: disable=no-init def __init__(self, logfilter): """Initialize the filter.""" super().__init__() diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index f40058ef883..39b9559aa59 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -234,7 +234,6 @@ def _parse_timespan(timespan): reversed(timespan.split(':')))) -# pylint: disable=too-few-public-methods class _ProcessSonosEventQueue(): """Queue like object for dispatching sonos events.""" diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 2c92fec3513..e160d074bbe 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -120,7 +120,6 @@ def setup(hass, config): return True -# pylint: disable=too-few-public-methods class CallRateDelayThrottle(object): """Helper class to provide service call rate throttling. diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 30af601f63b..cadafb8e784 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -149,7 +149,6 @@ class GlancesSensor(Entity): self.rest.update() -# pylint: disable=too-few-public-methods class GlancesData(object): """The class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/hddtemp.py b/homeassistant/components/sensor/hddtemp.py index a4308535fe2..1a964a458e2 100644 --- a/homeassistant/components/sensor/hddtemp.py +++ b/homeassistant/components/sensor/hddtemp.py @@ -101,7 +101,6 @@ class HddTempSensor(Entity): self._state = STATE_UNKNOWN -# pylint: disable=too-few-public-methods class HddTempData(object): """Get the latest data from HDDTemp and update the states.""" diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 69ff5d7a61f..fe769f51129 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -109,7 +109,7 @@ def get_random_string(length=10): class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - # pylint: disable=no-init, too-few-public-methods + # pylint: disable=no-init def __ge__(self, other): """Return the greater than element.""" if self.__class__ is other.__class__: From a01939c6e9e6d5a1cc051f27a639d960ff85db4c Mon Sep 17 00:00:00 2001 From: jgriff2 Date: Thu, 3 Nov 2016 18:41:32 -0700 Subject: [PATCH 136/149] Fix Synology Camera SSL certificate option (#4201) [BREAKING CHANGE] * Fix Synology SSL config * Revert "Fix Synology SSL config" This reverts commit b8dc2a92abee6249b3dd42c99d0786820ebbeb72. * Revert "Fix Synology SSL config" This reverts commit 805e87f3af300a1b7627bb5df0792285fcf38901. * Fix Synology SSL config --- homeassistant/components/camera/synology.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 4d5020ec075..4abdf8d22dd 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -15,7 +15,7 @@ import async_timeout from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST) + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv @@ -28,7 +28,6 @@ DEFAULT_STREAM_ID = '0' TIMEOUT = 5 CONF_CAMERA_NAME = 'camera_name' CONF_STREAM_ID = 'stream_id' -CONF_VALID_CERT = 'valid_cert' QUERY_CGI = 'query.cgi' QUERY_API = 'SYNO.API.Info' @@ -51,7 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_URL): cv.string, vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, - vol.Optional(CONF_VALID_CERT, default=True): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, }) @@ -73,7 +72,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): query_req = yield from hass.websession.get( syno_api_url, params=query_payload, - verify=config.get(CONF_VALID_CERT) + verify_ssl=config.get(CONF_VERIFY_SSL) ) except asyncio.TimeoutError: _LOGGER.error("Timeout on %s", syno_api_url) @@ -97,7 +96,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_USERNAME), config.get(CONF_PASSWORD), syno_auth_url, - config.get(CONF_VALID_CERT) + config.get(CONF_VERIFY_SSL) ) # Use SessionID to get cameras in system @@ -114,7 +113,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera_req = yield from hass.websession.get( syno_camera_url, params=camera_payload, - verify_ssl=config.get(CONF_VALID_CERT), + verify_ssl=config.get(CONF_VERIFY_SSL), cookies={'id': session_id} ) except asyncio.TimeoutError: @@ -193,7 +192,7 @@ class SynologyCamera(Camera): self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi' self._camera_name = config.get(CONF_CAMERA_NAME) self._stream_id = config.get(CONF_STREAM_ID) - self._valid_cert = config.get(CONF_VALID_CERT) + self._valid_cert = config.get(CONF_VERIFY_SSL) self._camera_id = camera_id self._snapshot_path = snapshot_path self._streaming_path = streaming_path From d7b3c9c38ebaed8b45083c6cd24858bf5d2362d1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Nov 2016 02:42:22 +0100 Subject: [PATCH 137/149] Fix log owntrack log flooting (#4198) --- homeassistant/components/device_tracker/owntracks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 566b62fb171..d73cbfdb2ac 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -142,9 +142,9 @@ def setup_scanner(hass, config, see): return data if max_gps_accuracy is not None and \ convert(data.get('acc'), float, 0.0) > max_gps_accuracy: - _LOGGER.warning('Ignoring %s update because expected GPS ' - 'accuracy %s is not met: %s', - data_type, max_gps_accuracy, payload) + _LOGGER.info('Ignoring %s update because expected GPS ' + 'accuracy %s is not met: %s', + data_type, max_gps_accuracy, payload) return None if convert(data.get('acc'), float, 1.0) == 0.0: _LOGGER.warning('Ignoring %s update because GPS accuracy' @@ -247,7 +247,7 @@ def setup_scanner(hass, config, see): if (max_gps_accuracy is not None and data['acc'] > max_gps_accuracy): valid_gps = False - _LOGGER.warning( + _LOGGER.info( 'Ignoring GPS in region exit because expected ' 'GPS accuracy %s is not met: %s', max_gps_accuracy, payload) From 61a0976752946d7e8fb4c530fa7d5698b0853867 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 Nov 2016 02:43:42 +0100 Subject: [PATCH 138/149] Use port instead of url and fix PEP257 issues (#4192) --- homeassistant/components/light/litejet.py | 5 ++--- homeassistant/components/litejet.py | 13 +++++++------ homeassistant/components/scene/litejet.py | 5 +++-- homeassistant/components/switch/litejet.py | 5 +++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py index c278cdc1332..3ff8067ec8c 100644 --- a/homeassistant/components/light/litejet.py +++ b/homeassistant/components/light/litejet.py @@ -4,7 +4,6 @@ Support for LiteJet lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.litejet/ """ - import logging import homeassistant.components.litejet as litejet @@ -18,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup lights for the LiteJet platform.""" + """Set up lights for the LiteJet platform.""" litejet_ = hass.data['litejet_system'] devices = [] @@ -30,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LiteJetLight(Light): - """Represents a single LiteJet light.""" + """Representation of a single LiteJet light.""" def __init__(self, hass, lj, i, name): """Initialize a LiteJet light.""" diff --git a/homeassistant/components/litejet.py b/homeassistant/components/litejet.py index 70c3755144b..aa4488c1277 100644 --- a/homeassistant/components/litejet.py +++ b/homeassistant/components/litejet.py @@ -4,24 +4,25 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/litejet/ """ import logging + import voluptuous as vol from homeassistant.helpers import discovery -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv -DOMAIN = 'litejet' - REQUIREMENTS = ['pylitejet==0.1'] +_LOGGER = logging.getLogger(__name__) + CONF_EXCLUDE_NAMES = 'exclude_names' CONF_INCLUDE_SWITCHES = 'include_switches' -_LOGGER = logging.getLogger(__name__) +DOMAIN = 'litejet' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_URL): cv.string, + vol.Required(CONF_PORT): cv.string, vol.Optional(CONF_EXCLUDE_NAMES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_INCLUDE_SWITCHES, default=False): cv.boolean }) @@ -32,7 +33,7 @@ def setup(hass, config): """Initialize the LiteJet component.""" from pylitejet import LiteJet - url = config[DOMAIN].get(CONF_URL) + url = config[DOMAIN].get(CONF_PORT) hass.data['litejet_system'] = LiteJet(url) hass.data['litejet_config'] = config[DOMAIN] diff --git a/homeassistant/components/scene/litejet.py b/homeassistant/components/scene/litejet.py index 6e08ebfbee9..432ce060774 100644 --- a/homeassistant/components/scene/litejet.py +++ b/homeassistant/components/scene/litejet.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.litejet/ """ import logging + import homeassistant.components.litejet as litejet from homeassistant.components.scene import Scene @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup scenes for the LiteJet platform.""" + """Set up scenes for the LiteJet platform.""" litejet_ = hass.data['litejet_system'] devices = [] @@ -28,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LiteJetScene(Scene): - """Represents a single LiteJet scene.""" + """Representation of a single LiteJet scene.""" def __init__(self, lj, i, name): """Initialize the scene.""" diff --git a/homeassistant/components/switch/litejet.py b/homeassistant/components/switch/litejet.py index d058d648540..f51984b411b 100644 --- a/homeassistant/components/switch/litejet.py +++ b/homeassistant/components/switch/litejet.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.litejet/ """ import logging + import homeassistant.components.litejet as litejet from homeassistant.components.switch import SwitchDevice @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the LiteJet switch platform.""" + """Set up the LiteJet switch platform.""" litejet_ = hass.data['litejet_system'] devices = [] @@ -28,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LiteJetSwitch(SwitchDevice): - """Represents a single LiteJet switch.""" + """Representation of a single LiteJet switch.""" def __init__(self, hass, lj, i, name): """Initialize a LiteJet switch.""" From 6f68752d1e3330e9a65750abaae6cdf4d81ed719 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Fri, 4 Nov 2016 04:23:37 +0000 Subject: [PATCH 139/149] Speed up Sonos tests (#4196) --- .../www_static/home-assistant-polymer | 2 +- tests/components/media_player/test_sonos.py | 28 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 896e0427675..898a8acdc0d 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f +Subproject commit 898a8acdc0d61a609536774fcd5e20522bb58b82 diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index c8950030d72..dfd308d5459 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -1,4 +1,5 @@ """The tests for the Demo Media player platform.""" +import socket import unittest import soco.snapshot from unittest import mock @@ -113,7 +114,8 @@ class TestSonosMediaPlayer(unittest.TestCase): self.hass.stop() @mock.patch('soco.SoCo', new=SoCoMock) - def test_ensure_setup_discovery(self): + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_discovery(self, *args): """Test a single device using the autodiscovery provided by HASS.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') @@ -122,7 +124,8 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @mock.patch('soco.SoCo', new=SoCoMock) - def test_ensure_setup_config(self): + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_config(self, *args): """Test a single address config'd by the HASS config file.""" sonos.setup_platform(self.hass, {'hosts': '192.0.2.1'}, @@ -134,15 +137,17 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch.object(soco, 'discover', new=socoDiscoverMock.discover) - def test_ensure_setup_sonos_discovery(self): + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, mock.MagicMock()) self.assertEqual(len(sonos.DEVICES), 1) self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'partymode') - def test_sonos_group_players(self, partymodeMock): + def test_sonos_group_players(self, partymodeMock, *args): """Ensuring soco methods called for sonos_group_players service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] @@ -152,8 +157,9 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(partymodeMock.call_args, mock.call()) @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'unjoin') - def test_sonos_unjoin(self, unjoinMock): + def test_sonos_unjoin(self, unjoinMock, *args): """Ensuring soco methods called for sonos_unjoin service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] @@ -163,8 +169,9 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(unjoinMock.call_args, mock.call()) @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'set_sleep_timer') - def test_sonos_set_sleep_timer(self, set_sleep_timerMock): + def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] @@ -172,8 +179,9 @@ class TestSonosMediaPlayer(unittest.TestCase): set_sleep_timerMock.assert_called_once_with(30) @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(SoCoMock, 'set_sleep_timer') - def test_sonos_clear_sleep_timer(self, set_sleep_timerMock): + def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_clear_sleep_timer service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] @@ -181,8 +189,9 @@ class TestSonosMediaPlayer(unittest.TestCase): set_sleep_timerMock.assert_called_once_with(None) @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') - def test_sonos_snapshot(self, snapshotMock): + def test_sonos_snapshot(self, snapshotMock, *args): """Ensuring soco methods called for sonos_snapshot service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] @@ -192,8 +201,9 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(snapshotMock.call_args, mock.call()) @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(soco.snapshot.Snapshot, 'restore') - def test_sonos_restore(self, restoreMock): + def test_sonos_restore(self, restoreMock, *args): """Ensuring soco methods called for sonos_restor service.""" sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') device = sonos.DEVICES[-1] From e88b98f5fa6f60ce88c40ed1c4275dae58210ea8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2016 21:58:18 -0700 Subject: [PATCH 140/149] Clean up tests (#4209) --- tests/components/device_tracker/test_init.py | 2 ++ tests/components/mqtt/test_init.py | 4 ++++ tests/components/notify/test_demo.py | 3 +++ tests/components/notify/test_group.py | 2 ++ tests/components/recorder/test_init.py | 2 ++ tests/components/test_group.py | 6 +++--- tests/components/test_logbook.py | 2 ++ tests/components/test_mqtt_eventstream.py | 9 +++++++-- tests/components/test_rfxtrx.py | 3 +++ tests/components/test_script.py | 3 +++ tests/helpers/test_script.py | 11 +++++++++++ 11 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index e9045ecd02e..1f95c38cd7f 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -6,6 +6,7 @@ from unittest.mock import call, patch from datetime import datetime, timedelta import os +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.loader import get_component import homeassistant.util.dt as dt_util @@ -312,6 +313,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): TEST_PLATFORM)) test_events = [] + @callback def listener(event): """Helper method that will verify our event got called.""" test_events.append(event) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9626f1a878b..1f46ef01391 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -6,6 +6,7 @@ import socket import voluptuous as vol +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.mqtt as mqtt from homeassistant.const import ( @@ -29,6 +30,7 @@ class TestMQTT(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() + @callback def record_calls(self, *args): """Helper for recording calls.""" self.calls.append(args) @@ -236,6 +238,7 @@ class TestMQTTCallbacks(unittest.TestCase): """Test if receiving triggers an event.""" calls = [] + @callback def record(event): """Helper to record calls.""" calls.append(event) @@ -321,6 +324,7 @@ class TestMQTTCallbacks(unittest.TestCase): """Test receiving a non utf8 encoded message.""" calls = [] + @callback def record(event): """Helper to record calls.""" calls.append(event) diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 61baabed69f..ddf08d91127 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -1,6 +1,7 @@ """The tests for the notify demo platform.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import demo @@ -23,6 +24,7 @@ class TestNotifyDemo(unittest.TestCase): self.events = [] self.calls = [] + @callback def record_event(event): """Record event to send notification.""" self.events.append(event) @@ -33,6 +35,7 @@ class TestNotifyDemo(unittest.TestCase): """"Stop down everything that was started.""" self.hass.stop() + @callback def record_calls(self, *args): """Helper for recording calls.""" self.calls.append(args) diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index 4a318a2d3b8..98c6ab6e5cf 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -1,6 +1,7 @@ """The tests for the notify.group platform.""" import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import group @@ -34,6 +35,7 @@ class TestNotifyGroup(unittest.TestCase): assert self.service is not None + @callback def record_event(event): """Record event to send notification.""" self.events.append(event) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 2df88b7a6e4..03e782841a2 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -4,6 +4,7 @@ import json from datetime import datetime, timedelta import unittest +from homeassistant.core import callback from homeassistant.const import MATCH_ALL from homeassistant.components import recorder from homeassistant.bootstrap import setup_component @@ -110,6 +111,7 @@ class TestRecorder(unittest.TestCase): events = [] + @callback def event_listener(event): """Record events from eventbus.""" if event.event_type == event_type: diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 786fee16624..c5b705cbc43 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -357,7 +357,7 @@ class TestComponentsGroup(unittest.TestCase): def test_changing_group_visibility(self): """Test that a group can be hidden and shown.""" - setup_component(self.hass, 'group', { + assert setup_component(self.hass, 'group', { 'group': { 'test_group': 'hello.world,sensor.happy' } @@ -367,12 +367,12 @@ class TestComponentsGroup(unittest.TestCase): # Hide the group group.set_visibility(self.hass, group_entity_id, False) - group_state = self.hass.states.get(group_entity_id) self.hass.block_till_done() + group_state = self.hass.states.get(group_entity_id) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) # Show it again group.set_visibility(self.hass, group_entity_id, True) - group_state = self.hass.states.get(group_entity_id) self.hass.block_till_done() + group_state = self.hass.states.get(group_entity_id) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 8ffb2146319..dcb675e00e5 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -39,6 +39,7 @@ class TestComponentLogbook(unittest.TestCase): """Test if service call create log book entry.""" calls = [] + @ha.callback def event_listener(event): calls.append(event) @@ -69,6 +70,7 @@ class TestComponentLogbook(unittest.TestCase): """Test if service call create log book entry without message.""" calls = [] + @ha.callback def event_listener(event): calls.append(event) diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index 3cc57ef8a0a..a60e54df016 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -6,7 +6,7 @@ from unittest.mock import ANY, patch from homeassistant.bootstrap import setup_component import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED -from homeassistant.core import State +from homeassistant.core import State, callback from homeassistant.remote import JSONEncoder import homeassistant.util.dt as dt_util @@ -130,7 +130,12 @@ class TestMqttEventStream(unittest.TestCase): self.hass.block_till_done() calls = [] - self.hass.bus.listen_once('test_event', lambda _: calls.append(1)) + + @callback + def listener(_): + calls.append(1) + + self.hass.bus.listen_once('test_event', listener) self.hass.block_till_done() payload = json.dumps( diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index 95eaf54cd6b..7e47dfb6a50 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -4,6 +4,7 @@ import unittest import pytest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.components import rfxtrx as rfxtrx from tests.common import get_test_home_assistant @@ -89,6 +90,7 @@ class TestRFXTRX(unittest.TestCase): calls = [] + @callback def record_event(event): """Add recorded event to set.""" calls.append(event) @@ -133,6 +135,7 @@ class TestRFXTRX(unittest.TestCase): calls = [] + @callback def record_event(event): """Add recorded event to set.""" calls.append(event) diff --git a/tests/components/test_script.py b/tests/components/test_script.py index de13d43fe82..979e435456c 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import unittest +from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.components import script @@ -54,6 +55,7 @@ class TestScriptComponent(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -93,6 +95,7 @@ class TestScriptComponent(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8744170fc40..8787ff7b514 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest import mock import unittest +from homeassistant.core import callback # Otherwise can't test just this file (import order issue) import homeassistant.components # noqa import homeassistant.util.dt as dt_util @@ -33,6 +34,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' calls = [] + @callback def record_event(event): """Add recorded event to set.""" calls.append(event) @@ -58,6 +60,7 @@ class TestScriptHelper(unittest.TestCase): """Test the calling of a service.""" calls = [] + @callback def record_call(service): """Add recorded event to set.""" calls.append(service) @@ -80,6 +83,7 @@ class TestScriptHelper(unittest.TestCase): """Test the calling of a service.""" calls = [] + @callback def record_call(service): """Add recorded event to set.""" calls.append(service) @@ -114,6 +118,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -146,6 +151,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_evnt' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -178,6 +184,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -211,6 +218,7 @@ class TestScriptHelper(unittest.TestCase): """Test if we can pass variables to script.""" calls = [] + @callback def record_call(service): """Add recorded event to set.""" calls.append(service) @@ -257,6 +265,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -290,6 +299,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) @@ -318,6 +328,7 @@ class TestScriptHelper(unittest.TestCase): event = 'test_event' events = [] + @callback def record_event(event): """Add recorded event to set.""" events.append(event) From 525d735f2163aec55a56db883b770997e10dd33f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2016 21:58:25 -0700 Subject: [PATCH 141/149] Warn if fetching properties takes too long (#4208) * Warn if fetching properties takes too long * Update entity.py --- homeassistant/helpers/entity.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 95ac50770cf..560e944abe6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,6 +1,7 @@ """An abstract class for entities.""" import asyncio import logging +from timeit import default_timer as timer from typing import Any, Optional, List, Dict @@ -210,7 +211,15 @@ class Entity(object): # future support? yield from self.hass.loop.run_in_executor(None, self.update) - state = STATE_UNKNOWN if self.state is None else str(self.state) + start = timer() + + state = self.state + + if state is None: + state = STATE_UNKNOWN + else: + state = str(state) + attr = self.state_attributes or {} device_attr = self.device_state_attributes @@ -231,6 +240,13 @@ class Entity(object): self._attr_setter('hidden', bool, ATTR_HIDDEN, attr) self._attr_setter('assumed_state', bool, ATTR_ASSUMED_STATE, attr) + end = timer() + + if end - start > 0.2: + _LOGGER.warning('Updating state for %s took %.3f seconds. ' + 'Please report to the developers.', self.entity_id, + end - start) + # Overwrite properties that have been set in the config file. attr.update(_OVERWRITE.get(self.entity_id, {})) From 4cc417677e26ad24599a6be5e37089b86e09b9ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2016 22:45:01 -0700 Subject: [PATCH 142/149] Add link to issue in warning slow entity update (#4211) --- homeassistant/helpers/entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 560e944abe6..ecb04aca9d9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -244,7 +244,8 @@ class Entity(object): if end - start > 0.2: _LOGGER.warning('Updating state for %s took %.3f seconds. ' - 'Please report to the developers.', self.entity_id, + 'Please report platform to the developers at ' + 'https://goo.gl/Nvioub', self.entity_id, end - start) # Overwrite properties that have been set in the config file. From 18e965c3cdaa73d652dc51e815dfe015f070f455 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2016 22:56:55 -0700 Subject: [PATCH 143/149] Fix flaky group notify test (#4212) --- homeassistant/components/notify/group.py | 3 +- tests/components/notify/test_group.py | 57 +++++++++--------------- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 9a7d8b69681..d4c10ac884d 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.group/ """ import collections +from copy import deepcopy import logging import voluptuous as vol @@ -56,7 +57,7 @@ class GroupNotifyPlatform(BaseNotificationService): payload.update({key: val for key, val in kwargs.items() if val}) for entity in self.entities: - sending_payload = payload.copy() + sending_payload = deepcopy(payload.copy()) if entity.get(ATTR_DATA) is not None: update(sending_payload, entity.get(ATTR_DATA)) self.hass.services.call(DOMAIN, entity.get(ATTR_SERVICE), diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index 98c6ab6e5cf..f0871c78322 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -1,10 +1,10 @@ """The tests for the notify.group platform.""" import unittest +from unittest.mock import MagicMock, patch -from homeassistant.core import callback from homeassistant.bootstrap import setup_component import homeassistant.components.notify as notify -from homeassistant.components.notify import group +from homeassistant.components.notify import group, demo from tests.common import assert_setup_component, get_test_home_assistant @@ -16,7 +16,17 @@ class TestNotifyGroup(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.events = [] - with assert_setup_component(2): + self.service1 = MagicMock() + self.service2 = MagicMock() + + def mock_get_service(hass, config): + if config['name'] == 'demo1': + return self.service1 + else: + return self.service2 + + with assert_setup_component(2), \ + patch.object(demo, 'get_service', mock_get_service): setup_component(self.hass, notify.DOMAIN, { 'notify': [{ 'name': 'demo1', @@ -35,49 +45,24 @@ class TestNotifyGroup(unittest.TestCase): assert self.service is not None - @callback - def record_event(event): - """Record event to send notification.""" - self.events.append(event) - - self.hass.bus.listen("notify", record_event) - def tearDown(self): # pylint: disable=invalid-name """"Stop everything that was started.""" self.hass.stop() - def test_send_message_to_group(self): - """Test sending a message to a notify group.""" - self.service.send_message('Hello', title='Test notification') - self.hass.block_till_done() - self.assertTrue(len(self.events) == 2) - last_event = self.events[-1] - self.assertEqual(last_event.data[notify.ATTR_TITLE], - 'Test notification') - self.assertEqual(last_event.data[notify.ATTR_MESSAGE], 'Hello') - def test_send_message_with_data(self): """Test sending a message with to a notify group.""" - notify_data = {'hello': 'world'} self.service.send_message('Hello', title='Test notification', - data=notify_data) + data={'hello': 'world'}) self.hass.block_till_done() - last_event = self.events[-1] - self.assertEqual(last_event.data[notify.ATTR_TITLE], - 'Test notification') - self.assertEqual(last_event.data[notify.ATTR_MESSAGE], 'Hello') - self.assertEqual(last_event.data[notify.ATTR_DATA], notify_data) - def test_entity_data_passes_through(self): - """Test sending a message with data to merge to a notify group.""" - notify_data = {'hello': 'world'} - self.service.send_message('Hello', title='Test notification', - data=notify_data) - self.hass.block_till_done() - data = self.events[-1].data - assert { + assert self.service1.send_message.mock_calls[0][2] == { + 'message': 'Hello', + 'title': 'Test notification', + 'data': {'hello': 'world'} + } + assert self.service2.send_message.mock_calls[0][2] == { 'message': 'Hello', 'target': ['unnamed device'], 'title': 'Test notification', 'data': {'hello': 'world', 'test': 'message'} - } == data + } From a3db0ec231f467391438bf01c8cf466f4d5ec631 Mon Sep 17 00:00:00 2001 From: jbcodemonkey Date: Fri, 4 Nov 2016 17:28:22 -0400 Subject: [PATCH 144/149] add dimmer slide control to imported isy lights (#4152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supported attribute added and checks appear to pass. 🐬 --- homeassistant/components/light/isy994.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index ea6babad319..d8a6f558865 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/light.isy994/ import logging from typing import Callable -from homeassistant.components.light import Light, SUPPORT_BRIGHTNESS +from homeassistant.components.light import ( + Light, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS) import homeassistant.components.isy994 as isy from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN from homeassistant.helpers.typing import ConfigType @@ -68,6 +69,11 @@ class ISYLightDevice(isy.ISYDevice, Light): if not self._node.on(val=brightness): _LOGGER.debug('Unable to turn on light.') + @property + def state_attributes(self): + """Flag supported attributes.""" + return {ATTR_BRIGHTNESS: self.value} + @property def supported_features(self): """Flag supported features.""" From 91227d9a2e6ff012b2dec4a4f2d843c03fa7bc33 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 5 Nov 2016 01:22:47 +0100 Subject: [PATCH 145/149] Refactory nest component/platforms (#4219) * Refactory nest component/platforms --- .../components/binary_sensor/nest.py | 18 +++- homeassistant/components/climate/nest.py | 101 +++++++++++------- homeassistant/components/nest.py | 89 +++++++-------- homeassistant/components/sensor/nest.py | 78 +++++++++----- 4 files changed, 178 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 4dfe4d58b99..65fe6041f34 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.sensor.nest import NestSensor from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) -import homeassistant.components.nest as nest +from homeassistant.components.nest import DATA_NEST import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['nest'] @@ -35,9 +35,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Nest binary sensors.""" + nest = hass.data[DATA_NEST] + + all_sensors = [] for structure, device in nest.devices(): - add_devices([NestBinarySensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS]]) + all_sensors.extend( + [NestBinarySensor(structure, device, variable) + for variable in config[CONF_MONITORED_CONDITIONS]]) + + add_devices(all_sensors, True) class NestBinarySensor(NestSensor, BinarySensorDevice): @@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): @property def is_on(self): """True if the binary sensor is on.""" - return bool(getattr(self.device, self.variable)) + return self._state + + def update(self): + """Retrieve latest state.""" + self._state = bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 5020bb441d5..402cc2b2498 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.nest/ """ import logging + import voluptuous as vol -import homeassistant.components.nest as nest + +from homeassistant.components.nest import DATA_NEST from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -26,8 +28,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest thermostat.""" temp_unit = hass.config.units.temperature_unit - add_devices([NestThermostat(structure, device, temp_unit) - for structure, device in nest.devices()]) + add_devices( + [NestThermostat(structure, device, temp_unit) + for structure, device in hass.data[DATA_NEST].devices()], + True + ) class NestThermostat(ClimateDevice): @@ -53,18 +58,33 @@ class NestThermostat(ClimateDevice): if self.device.can_heat and self.device.can_cool: self._operation_list.append(STATE_AUTO) + # feature of device + self._has_humidifier = self.device.has_humidifier + self._has_dehumidifier = self.device.has_dehumidifier + self._has_fan = self.device.has_fan + + # data attributes + self._away = None + self._location = None + self._name = None + self._humidity = None + self._target_humidity = None + self._target_temperature = None + self._temperature = None + self._mode = None + self._fan = None + self._away_temperature = None + @property def name(self): """Return the name of the nest, if any.""" - location = self.device.where - name = self.device.name - if location is None: - return name + if self._location is None: + return self._name else: - if name == '': - return location.capitalize() + if self._name == '': + return self._location.capitalize() else: - return location.capitalize() + '(' + name + ')' + return self._location.capitalize() + '(' + self._name + ')' @property def temperature_unit(self): @@ -74,11 +94,11 @@ class NestThermostat(ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" - if self.device.has_humidifier or self.device.has_dehumidifier: + if self._has_humidifier or self._has_dehumidifier: # Move these to Thermostat Device and make them global return { - "humidity": self.device.humidity, - "target_humidity": self.device.target_humidity, + "humidity": self._humidity, + "target_humidity": self._target_humidity, } else: # No way to control humidity not show setting @@ -87,18 +107,18 @@ class NestThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.device.temperature + return self._temperature @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - if self.device.mode == 'cool': + if self._mode == 'cool': return STATE_COOL - elif self.device.mode == 'heat': + elif self._mode == 'heat': return STATE_HEAT - elif self.device.mode == 'range': + elif self._mode == 'range': return STATE_AUTO - elif self.device.mode == 'off': + elif self._mode == 'off': return STATE_OFF else: return STATE_UNKNOWN @@ -106,37 +126,37 @@ class NestThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self.device.mode != 'range' and not self.is_away_mode_on: - return self.device.target + if self._mode != 'range' and not self.is_away_mode_on: + return self._target_temperature else: return None @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[0]: + if self.is_away_mode_on and self._away_temperature[0]: # away_temperature is always a low, high tuple - return self.device.away_temperature[0] - if self.device.mode == 'range': - return self.device.target[0] + return self._away_temperature[0] + if self._mode == 'range': + return self._target_temperature[0] else: return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[1]: + if self.is_away_mode_on and self._away_temperature[1]: # away_temperature is always a low, high tuple - return self.device.away_temperature[1] - if self.device.mode == 'range': - return self.device.target[1] + return self._away_temperature[1] + if self._mode == 'range': + return self._target_temperature[1] else: return None @property def is_away_mode_on(self): """Return if away mode is on.""" - return self.structure.away + return self._away def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -144,7 +164,7 @@ class NestThermostat(ClimateDevice): target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if target_temp_low is not None and target_temp_high is not None: - if self.device.mode == 'range': + if self._mode == 'range': temp = (target_temp_low, target_temp_high) else: temp = kwargs.get(ATTR_TEMPERATURE) @@ -178,9 +198,9 @@ class NestThermostat(ClimateDevice): @property def current_fan_mode(self): """Return whether the fan is on.""" - if self.device.has_fan: + if self._has_fan: # Return whether the fan is on - return STATE_ON if self.device.fan else STATE_AUTO + return STATE_ON if self._fan else STATE_AUTO else: # No Fan available so disable slider return None @@ -197,7 +217,7 @@ class NestThermostat(ClimateDevice): @property def min_temp(self): """Identify min_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.low + temp = self._away_temperature[0] if temp is None: return super().min_temp else: @@ -206,12 +226,21 @@ class NestThermostat(ClimateDevice): @property def max_temp(self): """Identify max_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.high + temp = self._away_temperature[1] if temp is None: return super().max_temp else: return temp def update(self): - """Python-nest has its own mechanism for staying up to date.""" - pass + """Cache value from Python-nest.""" + self._location = self.device.where + self._name = self.device.name + self._humidity = self.device.humidity, + self._target_humidity = self.device.target_humidity, + self._temperature = self.device.temperature + self._mode = self.device.mode + self._target_temperature = self.device.target + self._fan = self.device.fan + self._away = self.structure.away + self._away_temperature = self.device.away_temperature diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index b8aa1d1c70a..9f766efe693 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -18,9 +18,8 @@ REQUIREMENTS = ['python-nest==2.11.0'] DOMAIN = 'nest' -NEST = None +DATA_NEST = 'nest' -STRUCTURES_TO_INCLUDE = None CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -31,52 +30,58 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def devices(): - """Generator returning list of devices and their location.""" - try: - for structure in NEST.structures: - if structure.name in STRUCTURES_TO_INCLUDE: - for device in structure.devices: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, STRUCTURES_TO_INCLUDE) - except socket.error: - _LOGGER.error("Connection error logging into the nest web service.") - - -def protect_devices(): - """Generator returning list of protect devices.""" - try: - for structure in NEST.structures: - if structure.name in STRUCTURES_TO_INCLUDE: - for device in structure.protectdevices: - yield(structure, device) - else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, STRUCTURES_TO_INCLUDE) - except socket.error: - _LOGGER.error("Connection error logging into the nest web service.") - - -# pylint: disable=unused-argument def setup(hass, config): """Setup the Nest thermostat component.""" - global NEST - global STRUCTURES_TO_INCLUDE + import nest conf = config[DOMAIN] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] - import nest + nest = nest.Nest(username, password) + hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - NEST = nest.Nest(username, password) - - if CONF_STRUCTURE not in conf: - STRUCTURES_TO_INCLUDE = [s.name for s in NEST.structures] - else: - STRUCTURES_TO_INCLUDE = conf[CONF_STRUCTURE] - - _LOGGER.debug("Structures to include: %s", STRUCTURES_TO_INCLUDE) return True + + +class NestDevice(object): + """Structure Nest functions for hass.""" + + def __init__(self, hass, conf, nest): + """Init Nest Devices.""" + self.hass = hass + self.nest = nest + + if CONF_STRUCTURE not in conf: + self._structure = [s.name for s in nest.structures] + else: + self._structure = conf[CONF_STRUCTURE] + _LOGGER.debug("Structures to include: %s", self._structure) + + def devices(self): + """Generator returning list of devices and their location.""" + try: + for structure in self.nest.structures: + if structure.name in self._structure: + for device in structure.devices: + yield (structure, device) + else: + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self._structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") + + def protect_devices(self): + """Generator returning list of protect devices.""" + try: + for structure in self.nest.structures: + if structure.name in self._structure: + for device in structure.protectdevices: + yield(structure, device) + else: + _LOGGER.info("Ignoring structure %s, not in %s", + structure.name, self._structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 98d018a7c0b..ccf8be84adc 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -8,7 +8,7 @@ from itertools import chain import voluptuous as vol -import homeassistant.components.nest as nest +from homeassistant.components.nest import DATA_NEST, DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.const import ( TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS @@ -41,7 +41,7 @@ _VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS + \ list(WEATHER_VARS.keys()) PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): nest.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(_VALID_SENSOR_TYPES)], @@ -50,6 +50,9 @@ PLATFORM_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest Sensor.""" + nest = hass.data[DATA_NEST] + + all_sensors = [] for structure, device in chain(nest.devices(), nest.protect_devices()): sensors = [NestBasicSensor(structure, device, variable) for variable in config[CONF_MONITORED_CONDITIONS] @@ -64,8 +67,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors += [NestProtectSensor(structure, device, variable) for variable in config[CONF_MONITORED_CONDITIONS] if variable in PROTECT_VARS and is_protect(device)] + all_sensors.extend(sensors) - add_devices(sensors) + add_devices(all_sensors, True) def is_thermostat(device): @@ -87,19 +91,23 @@ class NestSensor(Entity): self.device = device self.variable = variable + # device specific + self._location = self.device.where + self._name = self.device.name + self._state = None + @property def name(self): """Return the name of the nest, if any.""" - location = self.device.where - name = self.device.name - if location is None: - return "{} {}".format(name, self.variable) + if self._location is None: + return "{} {}".format(self._name, self.variable) else: - if name == '': - return "{} {}".format(location.capitalize(), self.variable) + if self._name == '': + return "{} {}".format(self._location.capitalize(), + self.variable) else: - return "{}({}){}".format(location.capitalize(), - name, + return "{}({}){}".format(self._location.capitalize(), + self._name, self.variable) @@ -109,16 +117,20 @@ class NestBasicSensor(NestSensor): @property def state(self): """Return the state of the sensor.""" - if self.variable == 'operation_mode': - return getattr(self.device, "mode") - else: - return getattr(self.device, self.variable) + return self._state @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_UNITS.get(self.variable, None) + def update(self): + """Retrieve latest state.""" + if self.variable == 'operation_mode': + self._state = getattr(self.device, "mode") + else: + self._state = getattr(self.device, self.variable) + class NestTempSensor(NestSensor): """Representation of a Nest Temperature sensor.""" @@ -131,15 +143,19 @@ class NestTempSensor(NestSensor): @property def state(self): """Return the state of the sensor.""" + return self._state + + def update(self): + """Retrieve latest state.""" temp = getattr(self.device, self.variable) if temp is None: - return None + self._state = None if isinstance(temp, tuple): low, high = temp - return "%s-%s" % (int(low), int(high)) + self._state = "%s-%s" % (int(low), int(high)) else: - return round(temp, 1) + self._state = round(temp, 1) class NestWeatherSensor(NestSensor): @@ -148,10 +164,16 @@ class NestWeatherSensor(NestSensor): @property def state(self): """Return the state of the sensor.""" + return self._state + + def update(self): + """Retrieve latest state.""" if self.variable == 'kph' or self.variable == 'direction': - return getattr(self.structure.weather.current.wind, self.variable) + self._state = getattr(self.structure.weather.current.wind, + self.variable) else: - return getattr(self.structure.weather.current, self.variable) + self._state = getattr(self.structure.weather.current, + self.variable) @property def unit_of_measurement(self): @@ -165,20 +187,24 @@ class NestProtectSensor(NestSensor): @property def state(self): """Return the state of the sensor.""" + return self._state + + def update(self): + """Retrieve latest state.""" state = getattr(self.device, self.variable) if self.variable == 'battery_level': - return getattr(self.device, self.variable) + self._state = getattr(self.device, self.variable) else: if state == 0: - return 'Ok' + self._state = 'Ok' if state == 1 or state == 2: - return 'Warning' + self._state = 'Warning' if state == 3: - return 'Emergency' + self._state = 'Emergency' - return 'Unknown' + self._state = 'Unknown' @property def name(self): """Return the name of the nest, if any.""" - return "{} {}".format(self.device.where.capitalize(), self.variable) + return "{} {}".format(self._location.capitalize(), self.variable) From c15fd4323eeea98c1fc99f16f458046d7b602688 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Nov 2016 23:38:27 -0700 Subject: [PATCH 146/149] Disable insteon hub (#4221) --- homeassistant/components/insteon_hub.py | 4 ++++ homeassistant/components/light/insteon_hub.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/insteon_hub.py b/homeassistant/components/insteon_hub.py index 906f15b6c3f..9d77a0fc5c3 100644 --- a/homeassistant/components/insteon_hub.py +++ b/homeassistant/components/insteon_hub.py @@ -33,6 +33,10 @@ def setup(hass, config): This will automatically import associated lights. """ + _LOGGER.warning('Component disabled at request from Insteon. ' + 'For more information: https://goo.gl/zLJaic') + return False + # pylint: disable=unreachable import insteon username = config[DOMAIN][CONF_USERNAME] diff --git a/homeassistant/components/light/insteon_hub.py b/homeassistant/components/light/insteon_hub.py index 70beadb6c1d..6f547b5f92a 100644 --- a/homeassistant/components/light/insteon_hub.py +++ b/homeassistant/components/light/insteon_hub.py @@ -8,6 +8,8 @@ from homeassistant.components.insteon_hub import INSTEON from homeassistant.components.light import (ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) +DEPENDENCIES = ['insteon_hub'] + SUPPORT_INSTEON_HUB = SUPPORT_BRIGHTNESS From d7d71c97e2c167471bb4e70f5b4f209af29399af Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 5 Nov 2016 09:15:59 +0100 Subject: [PATCH 147/149] Make the wind details more robust (weather.openweathermap) (#4215) * Make the wind details more robust * Return None if values is not available --- homeassistant/components/weather/openweathermap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index f8f7e2f3747..b029b4d44bb 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -98,7 +98,7 @@ class OpenWeatherMapWeather(WeatherEntity): @property def temperature(self): """Return the temperature.""" - return self.data.get_temperature('celsius')['temp'] + return self.data.get_temperature('celsius').get('temp') @property def temperature_unit(self): @@ -108,7 +108,7 @@ class OpenWeatherMapWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self.data.get_pressure()['press'] + return self.data.get_pressure().get('press') @property def humidity(self): @@ -118,12 +118,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def wind_speed(self): """Return the wind speed.""" - return self.data.get_wind()['speed'] + return self.data.get_wind().get('speed') @property def wind_bearing(self): """Return the wind bearing.""" - return self.data.get_wind()['deg'] + return self.data.get_wind().get('deg') @property def attribution(self): From 53d1a040d46c1ea88ea7272d4c3fd01f74fca11e Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 5 Nov 2016 10:55:59 -0400 Subject: [PATCH 148/149] Stop Octoprint from logging errors during startup (#4220) * Fix log errors * Remove discovery code --- homeassistant/components/octoprint.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 871f81759e0..24f7039a41c 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -11,13 +11,10 @@ import requests import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DISCOVER_BINARY_SENSORS = 'octoprint.binary_sensor' -DISCOVER_SENSORS = 'octoprint.sensors' DOMAIN = 'octoprint' OCTOPRINT = None @@ -44,12 +41,6 @@ def setup(hass, config): _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) return False - for component, discovery_service in ( - ('sensor', DISCOVER_SENSORS), - ('binary_sensor', DISCOVER_BINARY_SENSORS)): - discovery.discover(hass, discovery_service, component=component, - hass_config=config) - return True From 1d0f3b930f27508e1fcafefa1904887c2109641c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 08:40:32 -0700 Subject: [PATCH 149/149] Version bump to 0.32.0 --- .../components/frontend/www_static/home-assistant-polymer | 2 +- homeassistant/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 898a8acdc0d..896e0427675 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 898a8acdc0d61a609536774fcd5e20522bb58b82 +Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f diff --git a/homeassistant/const.py b/homeassistant/const.py index abe932fad15..ec05a7e24ac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)