From bc70619b17e877125887c12a75973e620640d41c Mon Sep 17 00:00:00 2001 From: nielstron Date: Thu, 22 Mar 2018 14:37:18 +0100 Subject: [PATCH 001/169] Added bandpass filter Allows values in a given range --- homeassistant/components/sensor/filter.py | 62 ++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 9c05028b3944ff..79a91c18cd433d 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -11,6 +11,7 @@ from functools import partial from copy import copy from datetime import timedelta +import math import voluptuous as vol @@ -28,6 +29,7 @@ _LOGGER = logging.getLogger(__name__) +FILTER_NAME_BANDPASS = 'bandpass' FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' @@ -40,6 +42,8 @@ CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_FILTER_LOWER_BOUND = 'lower_bound' +CONF_FILTER_UPPER_BOUND = 'upper_bound' CONF_TIME_SMA_TYPE = 'type' TIME_SMA_LAST = 'last' @@ -51,6 +55,8 @@ DEFAULT_PRECISION = 2 DEFAULT_FILTER_RADIUS = 2.0 DEFAULT_FILTER_TIME_CONSTANT = 10 +DEFAULT_LOWER_BOUND = -math.inf +DEFAULT_UPPER_BOUND = math.inf NAME_TEMPLATE = "{} filter" ICON = 'mdi:chart-line-variant' @@ -77,6 +83,14 @@ default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_BANDPASS_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_BANDPASS, + vol.Optional(CONF_FILTER_LOWER_BOUND, + default=DEFAULT_LOWER_BOUND): vol.Coerce(float), + vol.Optional(CONF_FILTER_UPPER_BOUND, + default=DEFAULT_UPPER_BOUND): vol.Coerce(float), +}) + FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, @@ -100,7 +114,8 @@ [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, FILTER_TIME_SMA_SCHEMA, - FILTER_THROTTLE_SCHEMA)]) + FILTER_THROTTLE_SCHEMA, + FILTER_BANDPASS_SCHEMA)]) }) @@ -325,6 +340,51 @@ def filter_state(self, new_state): return new_state +@FILTERS.register(FILTER_NAME_BANDPASS) +class BandPassFilter(Filter): + """Band pass filter. + + Determines if new state is in a band between upper_bound and lower_bound. + If not inside, lower or upper bound is returned instead. + + Args: + upper_bound (float): band upper bound + lower_bound (float): band lower bound + """ + + def __init__(self, window_size=1, precision=None, entity, + lower_bound=math.inf, upper_bound=-math.inf): + """Initialize Filter.""" + super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) + self._lower_bound = lower_bound + self._upper_bound = upper_bound + self._stats_internal = Counter() + + def _filter_state(self, new_state): + """Implement the outlier filter.""" + new_state = float(new_state) + + if new_state > self._upper_bound: + + self._stats_internal['erasures_up'] += 1 + + _LOGGER.debug("Upper outlier nr. %s in %s: %s", + self._stats_internal['erasures_up'], + self._entity, new_state) + return self._upper_bound + + if new_state < self._lower_bound: + + self._stats_internal['erasures_low'] += 1 + + _LOGGER.debug("Lower outlier nr. %s in %s: %s", + self._stats_internal['erasures_low'], + self._entity, new_state) + return self._lower_bound + + return new_state + + @FILTERS.register(FILTER_NAME_OUTLIER) class OutlierFilter(Filter): """BASIC outlier filter. From 3faed2edc14463e1257c55149d8826d600d017f3 Mon Sep 17 00:00:00 2001 From: nielstron Date: Thu, 22 Mar 2018 14:37:39 +0100 Subject: [PATCH 002/169] Add test for new band_pass filter --- tests/components/sensor/test_filter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 8e79306fe136a2..09b56ed6be4a0d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -131,6 +131,22 @@ def test_lowpass(self): filtered = filt.filter_state(state) self.assertEqual(18.05, filtered.state) + def test_bandpass(self): + """Test if bandpass filter works.""" + lower = 10 + upper = 20 + filt = LowPassFilter(entity=None, + lower_bound=lower, + upper_bound=upper) + for state in self.values: + filtered = filt.filter_state(state) + if state < lower: + self.assertEqual(lower, filtered) + elif state > upper: + self.assertEqual(upper, filtered) + else: + self.assertEqual(state, filtered) + def test_throttle(self): """Test if lowpass filter works.""" filt = ThrottleFilter(window_size=3, From 850131229220275b6b2a45dff0cd990b868fc103 Mon Sep 17 00:00:00 2001 From: nielstron Date: Thu, 22 Mar 2018 14:46:51 +0100 Subject: [PATCH 003/169] Reordered attribute order --- homeassistant/components/sensor/filter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 79a91c18cd433d..4505a794883418 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -85,6 +85,8 @@ FILTER_BANDPASS_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_BANDPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_LOWER_BOUND, default=DEFAULT_LOWER_BOUND): vol.Coerce(float), vol.Optional(CONF_FILTER_UPPER_BOUND, @@ -352,7 +354,7 @@ class BandPassFilter(Filter): lower_bound (float): band lower bound """ - def __init__(self, window_size=1, precision=None, entity, + def __init__(self, window_size, precision, entity, lower_bound=math.inf, upper_bound=-math.inf): """Initialize Filter.""" super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) From b42f4012d1df3ab2ecf8963772b98e0b0e8a1bb6 Mon Sep 17 00:00:00 2001 From: nielstron Date: Thu, 22 Mar 2018 15:29:56 +0100 Subject: [PATCH 004/169] Fixed test --- tests/components/sensor/test_filter.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 09b56ed6be4a0d..e43df67c84f13c 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -4,7 +4,8 @@ from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter, + BandPassFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component import homeassistant.core as ha @@ -135,9 +136,10 @@ def test_bandpass(self): """Test if bandpass filter works.""" lower = 10 upper = 20 - filt = LowPassFilter(entity=None, - lower_bound=lower, - upper_bound=upper) + filt = BandPassFilter(1, None, + entity=None, + lower_bound=lower, + upper_bound=upper) for state in self.values: filtered = filt.filter_state(state) if state < lower: From 734a83c65708f39d4629cb52df306c6a2746df79 Mon Sep 17 00:00:00 2001 From: nielstron Date: Thu, 22 Mar 2018 22:22:53 +0100 Subject: [PATCH 005/169] Removed default values and fixed description in sensor.filter --- homeassistant/components/sensor/filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 4505a794883418..9346480b4b39c6 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -355,7 +355,7 @@ class BandPassFilter(Filter): """ def __init__(self, window_size, precision, entity, - lower_bound=math.inf, upper_bound=-math.inf): + lower_bound, upper_bound): """Initialize Filter.""" super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._lower_bound = lower_bound @@ -363,7 +363,7 @@ def __init__(self, window_size, precision, entity, self._stats_internal = Counter() def _filter_state(self, new_state): - """Implement the outlier filter.""" + """Implement the band-pass filter.""" new_state = float(new_state) if new_state > self._upper_bound: From a0ab35693647b216eb2fd74e2b19a06abe47b978 Mon Sep 17 00:00:00 2001 From: nielstron Date: Fri, 30 Mar 2018 14:03:38 +0200 Subject: [PATCH 006/169] Renamed to range filter --- homeassistant/components/sensor/filter.py | 20 ++++++++++---------- tests/components/sensor/test_filter.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 9346480b4b39c6..f1fb1fca6f3f67 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -FILTER_NAME_BANDPASS = 'bandpass' +FILTER_NAME_RANGE = 'range' FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' @@ -83,8 +83,8 @@ default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) -FILTER_BANDPASS_SCHEMA = FILTER_SCHEMA.extend({ - vol.Required(CONF_FILTER_NAME): FILTER_NAME_BANDPASS, +FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_LOWER_BOUND, @@ -117,7 +117,7 @@ FILTER_LOWPASS_SCHEMA, FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA, - FILTER_BANDPASS_SCHEMA)]) + FILTER_RANGE_SCHEMA)]) }) @@ -342,11 +342,11 @@ def filter_state(self, new_state): return new_state -@FILTERS.register(FILTER_NAME_BANDPASS) -class BandPassFilter(Filter): - """Band pass filter. +@FILTERS.register(FILTER_NAME_RANGE) +class RangeFilter(Filter): + """Range filter. - Determines if new state is in a band between upper_bound and lower_bound. + Determines if new state is in the range of upper_bound and lower_bound. If not inside, lower or upper bound is returned instead. Args: @@ -357,13 +357,13 @@ class BandPassFilter(Filter): def __init__(self, window_size, precision, entity, lower_bound, upper_bound): """Initialize Filter.""" - super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) + super().__init__(FILTER_NAME_RANGE, window_size, precision, entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal = Counter() def _filter_state(self, new_state): - """Implement the band-pass filter.""" + """Implement the range filter.""" new_state = float(new_state) if new_state > self._upper_bound: diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index e43df67c84f13c..7b23210128912d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor.filter import ( LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter, - BandPassFilter) + RangeFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component import homeassistant.core as ha @@ -132,11 +132,11 @@ def test_lowpass(self): filtered = filt.filter_state(state) self.assertEqual(18.05, filtered.state) - def test_bandpass(self): - """Test if bandpass filter works.""" + def test_range(self): + """Test if range filter works.""" lower = 10 upper = 20 - filt = BandPassFilter(1, None, + filt = RangeFilter(1, None, entity=None, lower_bound=lower, upper_bound=upper) From ba836c2e3629257b4461202da5f6f5db7ed667e2 Mon Sep 17 00:00:00 2001 From: nielstron Date: Fri, 30 Mar 2018 14:10:22 +0200 Subject: [PATCH 007/169] Fix indent --- tests/components/sensor/test_filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 7b23210128912d..4d52648582f379 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -137,9 +137,9 @@ def test_range(self): lower = 10 upper = 20 filt = RangeFilter(1, None, - entity=None, - lower_bound=lower, - upper_bound=upper) + entity=None, + lower_bound=lower, + upper_bound=upper) for state in self.values: filtered = filt.filter_state(state) if state < lower: From f4ef8fd1bc95ced0352ef22486e16fcc477306bb Mon Sep 17 00:00:00 2001 From: nielstron Date: Sat, 28 Apr 2018 17:42:32 +0200 Subject: [PATCH 008/169] Changes for new FilterState construct --- homeassistant/components/sensor/filter.py | 9 ++++----- tests/components/sensor/test_filter.py | 14 +++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index f1fb1fca6f3f67..88868ddcc42720 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -364,25 +364,24 @@ def __init__(self, window_size, precision, entity, def _filter_state(self, new_state): """Implement the range filter.""" - new_state = float(new_state) - if new_state > self._upper_bound: + if new_state.state > self._upper_bound: self._stats_internal['erasures_up'] += 1 _LOGGER.debug("Upper outlier nr. %s in %s: %s", self._stats_internal['erasures_up'], self._entity, new_state) - return self._upper_bound + new_state.state = self._upper_bound - if new_state < self._lower_bound: + elif new_state < self._lower_bound: self._stats_internal['erasures_low'] += 1 _LOGGER.debug("Lower outlier nr. %s in %s: %s", self._stats_internal['erasures_low'], self._entity, new_state) - return self._lower_bound + new_state.state = self._upper_bound return new_state diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 4d52648582f379..0f5b581d075c91 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -140,14 +140,14 @@ def test_range(self): entity=None, lower_bound=lower, upper_bound=upper) - for state in self.values: - filtered = filt.filter_state(state) - if state < lower: - self.assertEqual(lower, filtered) - elif state > upper: - self.assertEqual(upper, filtered) + for unf_state in self.values: + filtered = filt.filter_state(unf_state) + if unf_state.state < lower: + self.assertEqual(lower, filtered.state) + elif unf_state.state > upper: + self.assertEqual(upper, filtered.state) else: - self.assertEqual(state, filtered) + self.assertEqual(unf_state.state, filtered.state) def test_throttle(self): """Test if lowpass filter works.""" From 07d139b3a82446ad8eaee304e59c947c762fd285 Mon Sep 17 00:00:00 2001 From: nielstron Date: Sat, 28 Apr 2018 17:51:04 +0200 Subject: [PATCH 009/169] Fix wrong comparison --- homeassistant/components/sensor/filter.py | 2 +- tests/components/sensor/test_filter.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 88868ddcc42720..21fd09248ff015 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -374,7 +374,7 @@ def _filter_state(self, new_state): self._entity, new_state) new_state.state = self._upper_bound - elif new_state < self._lower_bound: + elif new_state.state < self._lower_bound: self._stats_internal['erasures_low'] += 1 diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 0f5b581d075c91..e449f239c0bc4d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -141,13 +141,14 @@ def test_range(self): lower_bound=lower, upper_bound=upper) for unf_state in self.values: + prev = unf_state.state filtered = filt.filter_state(unf_state) - if unf_state.state < lower: + if prev < lower: self.assertEqual(lower, filtered.state) - elif unf_state.state > upper: + elif prev > upper: self.assertEqual(upper, filtered.state) else: - self.assertEqual(unf_state.state, filtered.state) + self.assertEqual(prev, filtered.state) def test_throttle(self): """Test if lowpass filter works.""" From bb98331ba45f9c2a4845316d21659ceabbf7ebd3 Mon Sep 17 00:00:00 2001 From: nielstron Date: Sat, 28 Apr 2018 18:09:37 +0200 Subject: [PATCH 010/169] Fix doctring newline and handle ha.state string-being --- homeassistant/components/sensor/filter.py | 1 - tests/components/sensor/test_filter.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 21fd09248ff015..93380291a3eb97 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -364,7 +364,6 @@ def __init__(self, window_size, precision, entity, def _filter_state(self, new_state): """Implement the range filter.""" - if new_state.state > self._upper_bound: self._stats_internal['erasures_up'] += 1 diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index e449f239c0bc4d..6fc732b28f5e39 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -141,7 +141,7 @@ def test_range(self): lower_bound=lower, upper_bound=upper) for unf_state in self.values: - prev = unf_state.state + prev = float(unf_state.state) filtered = filt.filter_state(unf_state) if prev < lower: self.assertEqual(lower, filtered.state) From 25f7c31911ffb6c1a646468b507a93c84831ecb9 Mon Sep 17 00:00:00 2001 From: nielstron Date: Sat, 28 Apr 2018 18:29:55 +0200 Subject: [PATCH 011/169] Fixed wrong bound assignment on values below the lower bound --- homeassistant/components/sensor/filter.py | 2 +- tests/components/sensor/test_filter.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 93380291a3eb97..49e9189a84d7dc 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -380,7 +380,7 @@ def _filter_state(self, new_state): _LOGGER.debug("Lower outlier nr. %s in %s: %s", self._stats_internal['erasures_low'], self._entity, new_state) - new_state.state = self._upper_bound + new_state.state = self._lower_bound return new_state diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 6fc732b28f5e39..718c39764d0e6a 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -141,14 +141,14 @@ def test_range(self): lower_bound=lower, upper_bound=upper) for unf_state in self.values: - prev = float(unf_state.state) + unf = float(unf_state.state) filtered = filt.filter_state(unf_state) - if prev < lower: + if unf < lower: self.assertEqual(lower, filtered.state) - elif prev > upper: + elif unf > upper: self.assertEqual(upper, filtered.state) else: - self.assertEqual(prev, filtered.state) + self.assertEqual(unf, filtered.state) def test_throttle(self): """Test if lowpass filter works.""" From 8061f15aec88332a8296c0f4c9ea3e278c4b0ea0 Mon Sep 17 00:00:00 2001 From: nielstron Date: Fri, 4 May 2018 00:51:03 +0200 Subject: [PATCH 012/169] Removal of windows size and precision for range filter --- homeassistant/components/sensor/filter.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 49e9189a84d7dc..770287228a2ed3 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -85,8 +85,6 @@ FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_LOWER_BOUND, default=DEFAULT_LOWER_BOUND): vol.Coerce(float), vol.Optional(CONF_FILTER_UPPER_BOUND, @@ -354,10 +352,10 @@ class RangeFilter(Filter): lower_bound (float): band lower bound """ - def __init__(self, window_size, precision, entity, + def __init__(self, entity, lower_bound, upper_bound): """Initialize Filter.""" - super().__init__(FILTER_NAME_RANGE, window_size, precision, entity) + super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal = Counter() From 33990badcd3955de50a408bb848d24b42f863241 Mon Sep 17 00:00:00 2001 From: nielstron Date: Fri, 4 May 2018 10:30:44 +0200 Subject: [PATCH 013/169] Fixed Rangefilter constructor call --- tests/components/sensor/test_filter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 718c39764d0e6a..cf2cc9c42054db 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -136,8 +136,7 @@ def test_range(self): """Test if range filter works.""" lower = 10 upper = 20 - filt = RangeFilter(1, None, - entity=None, + filt = RangeFilter(entity=None, lower_bound=lower, upper_bound=upper) for unf_state in self.values: From 87f9f1733570d2325ad478c0abc970cc216be644 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 10:51:07 -0400 Subject: [PATCH 014/169] Version bump to 0.72.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5644c3d0a1f2f6..d9446952f002c7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1f50e335fa26ad396d5537b0cd63f23b92036ff1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:32:49 -0400 Subject: [PATCH 015/169] Bump frontend to 20180616.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0c425ccd3b1e8d..af3459d0b19878 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180615.0'] +REQUIREMENTS = ['home-assistant-frontend==20180616.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index fdf4dc56f98415..55038299bc01a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b32efe9577b8c..03023966d958a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bdf625764043d8b5156621dbed97e44e8ad0a626 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 16 Jun 2018 21:53:25 +0200 Subject: [PATCH 016/169] Remove load power attribute for channel USB (#14996) * Remove load power attribute for channel USB * Fix format --- homeassistant/components/switch/xiaomi_miio.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 1e11b844fdf56c..37b16f44ea8eec 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -421,8 +421,11 @@ def __init__(self, name, plug, model, unique_id, channel_usb): self._device_features = FEATURE_FLAGS_PLUG_V3 self._state_attrs.update({ ATTR_WIFI_LED: None, - ATTR_LOAD_POWER: None, }) + if self._channel_usb is False: + self._state_attrs.update({ + ATTR_LOAD_POWER: None, + }) async def async_turn_on(self, **kwargs): """Turn a channel on.""" @@ -476,7 +479,7 @@ async def async_update(self): if state.wifi_led: self._state_attrs[ATTR_WIFI_LED] = state.wifi_led - if state.load_power: + if self._channel_usb is False and state.load_power: self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: From a0139081159fa697479d997d4d6f55f1b5521f84 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 16 Jun 2018 22:52:23 +0300 Subject: [PATCH 017/169] Switch to own packaged version of spotipy (#14997) --- homeassistant/components/media_player/spotify.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 963258f1861df6..73ec8a175b1f17 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -20,9 +20,7 @@ CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2' -REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/' - 'archive/%s.zip#spotipy==2.4.4' % COMMIT] +REQUIREMENTS = ['spotipy-homeassistant==2.4.4.dev1'] DEPENDENCIES = ['http'] diff --git a/requirements_all.txt b/requirements_all.txt index 55038299bc01a4..69a6f01dbd5e1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,9 +424,6 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 -# homeassistant.components.media_player.spotify -https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 @@ -1281,6 +1278,9 @@ speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 +# homeassistant.components.media_player.spotify +spotipy-homeassistant==2.4.4.dev1 + # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql From 8e185bc300ef5c92bea70c0967c2715fda982a77 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 16 Jun 2018 21:52:03 +0200 Subject: [PATCH 018/169] Bump pyhs100 version (#15001) Fixes #13925 --- homeassistant/components/light/tplink.py | 2 +- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 4101eab2150298..d7544cb6c5a2ee 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index cd2a0f189fc625..46682d87356c6f 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 69a6f01dbd5e1d..af5e9c6c787eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -719,7 +719,7 @@ pyCEC==0.4.13 # homeassistant.components.light.tplink # homeassistant.components.switch.tplink -pyHS100==0.3.0 +pyHS100==0.3.1 # homeassistant.components.rfxtrx pyRFXtrx==0.22.1 From 5d82f48c020f104a7848e1e1ca7969aa442b0469 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:12:03 -0400 Subject: [PATCH 019/169] Add experimental UI backend (#15002) * Add experimental UI * Add test * Lint --- homeassistant/components/frontend/__init__.py | 31 ++++++++++++++++--- tests/components/test_frontend.py | 19 ++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index af3459d0b19878..0f77b9e0adcf49 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,6 +23,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass +from homeassistant.util.yaml import load_yaml REQUIREMENTS = ['home-assistant-frontend==20180616.0'] @@ -105,6 +106,10 @@ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) +WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' +SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +}) class Panel: @@ -210,6 +215,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, + SCHEMA_GET_EXPERIMENTAL_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -254,10 +262,11 @@ def async_finalize_panel(panel): """Finalize setup of a panel.""" panel.async_register_index_routes(hass.http.app.router, index_view) - await asyncio.wait([ - async_register_built_in_panel(hass, panel) - for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop) + await asyncio.wait( + [async_register_built_in_panel(hass, panel) for panel in ( + 'dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -488,3 +497,17 @@ async def send_translations(): )) hass.async_add_job(send_translations()) + + +def websocket_experimental_config(hass, connection, msg): + """Send experimental UI config over websocket config.""" + async def send_exp_config(): + """Send experimental frontend config.""" + config = await hass.async_add_job( + load_yaml, hass.config.path('experimental-ui.yaml')) + + connection.send_message_outside(websocket_api.result_message( + msg['id'], config + )) + + hass.async_add_job(send_exp_config()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 2f118f24ef0933..cb0c72e9edd1a9 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -278,3 +278,22 @@ async def test_get_translations(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'resources': {'lang': 'nl'}} + + +async def test_experimental_ui(hass, hass_ws_client): + """Test experimental_ui command.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'frontend/experimental_ui', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} From 65970a22480f4154726ea8db39363ffa7273376e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:36:35 -0400 Subject: [PATCH 020/169] Version bump to 0.72.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d9446952f002c7..dd32c0e5be7d81 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 7238205adb144cd042a9081f8c7d487c13aa3150 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 22:35:19 -0400 Subject: [PATCH 021/169] Frontend bump to 20180617.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0f77b9e0adcf49..25aa0da0a3e27f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180616.0'] +REQUIREMENTS = ['home-assistant-frontend==20180617.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index af5e9c6c787eac..d860112c7f8f7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03023966d958a6..a2245c02cf1434 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 471d6e45eba1e08ecc3d876e6291a77868b8b2e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 22:37:13 -0400 Subject: [PATCH 022/169] Version bump to 0.72.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd32c0e5be7d81..562247a14c06ea 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From da3695dccc99f3cb18d47d11f5eefe2ea833618c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 17 Jun 2018 19:33:04 +0200 Subject: [PATCH 023/169] Update test_http.py --- tests/components/hassio/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ac90deb9f737d1..5f2c9c009c33d4 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -61,7 +61,7 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type): '_create_response') as mresp: mresp.return_value = 'response' resp = yield from hassio_client.get( - '/api/hassio/app-{}'.format(build_type)) + '/api/hassio/{}'.format(build_type)) # Check we got right response assert resp.status == 200 From 1642502a706042d7bc350d40eb8ba04b40a25890 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:03:29 -0400 Subject: [PATCH 024/169] Update translations --- .../components/cast/.translations/ca.json | 15 +++++++++ .../components/cast/.translations/ko.json | 15 +++++++++ .../components/cast/.translations/no.json | 15 +++++++++ .../components/cast/.translations/pl.json | 15 +++++++++ .../components/cast/.translations/ru.json | 15 +++++++++ .../components/cast/.translations/sv.json | 15 +++++++++ .../components/cast/.translations/vi.json | 15 +++++++++ .../cast/.translations/zh-Hans.json | 15 +++++++++ .../components/deconz/.translations/bg.json | 1 + .../components/deconz/.translations/ca.json | 33 +++++++++++++++++++ .../components/deconz/.translations/cs.json | 32 ++++++++++++++++++ .../components/deconz/.translations/en.json | 4 +-- .../components/deconz/.translations/fr.json | 32 ++++++++++++++++++ .../components/deconz/.translations/hu.json | 6 +++- .../components/deconz/.translations/it.json | 26 +++++++++++++++ .../components/deconz/.translations/ko.json | 11 +++++-- .../components/deconz/.translations/lb.json | 6 ++++ .../components/deconz/.translations/no.json | 7 ++++ .../components/deconz/.translations/pl.json | 6 ++++ .../deconz/.translations/pt-BR.json | 32 ++++++++++++++++++ .../components/deconz/.translations/pt.json | 29 ++++++++++++++-- .../components/deconz/.translations/ru.json | 7 ++++ .../components/deconz/.translations/sl.json | 6 ++++ .../components/deconz/.translations/sv.json | 33 +++++++++++++++++++ .../components/deconz/.translations/vi.json | 26 +++++++++++++++ .../deconz/.translations/zh-Hans.json | 7 ++++ .../deconz/.translations/zh-Hant.json | 7 ++++ .../components/hue/.translations/ca.json | 29 ++++++++++++++++ .../components/hue/.translations/cs.json | 29 ++++++++++++++++ .../components/hue/.translations/en.json | 2 +- .../components/hue/.translations/fr.json | 29 ++++++++++++++++ .../components/hue/.translations/hu.json | 3 +- .../components/hue/.translations/it.json | 21 +++++++++++- .../components/hue/.translations/pt-BR.json | 29 ++++++++++++++++ .../components/hue/.translations/pt.json | 24 ++++++++++++++ .../components/hue/.translations/sv.json | 29 ++++++++++++++++ .../components/hue/.translations/vi.json | 17 ++++++++++ .../components/nest/.translations/ca.json | 33 +++++++++++++++++++ .../components/nest/.translations/ko.json | 33 +++++++++++++++++++ .../components/nest/.translations/no.json | 33 +++++++++++++++++++ .../components/nest/.translations/pl.json | 33 +++++++++++++++++++ .../components/nest/.translations/ru.json | 33 +++++++++++++++++++ .../components/nest/.translations/sv.json | 33 +++++++++++++++++++ .../components/nest/.translations/vi.json | 22 +++++++++++++ .../nest/.translations/zh-Hans.json | 33 +++++++++++++++++++ .../sensor/.translations/season.ca.json | 8 +++++ .../sensor/.translations/season.fr.json | 8 +++++ .../sensor/.translations/season.pt-BR.json | 8 +++++ .../components/sonos/.translations/ca.json | 15 +++++++++ .../components/sonos/.translations/ko.json | 15 +++++++++ .../components/sonos/.translations/no.json | 15 +++++++++ .../components/sonos/.translations/pl.json | 15 +++++++++ .../components/sonos/.translations/ru.json | 15 +++++++++ .../components/sonos/.translations/sv.json | 15 +++++++++ .../components/sonos/.translations/vi.json | 15 +++++++++ .../sonos/.translations/zh-Hans.json | 15 +++++++++ .../components/zone/.translations/bg.json | 21 ++++++++++++ .../components/zone/.translations/ca.json | 21 ++++++++++++ .../components/zone/.translations/cs.json | 21 ++++++++++++ .../components/zone/.translations/fr.json | 21 ++++++++++++ .../components/zone/.translations/hu.json | 21 ++++++++++++ .../components/zone/.translations/it.json | 21 ++++++++++++ .../components/zone/.translations/ko.json | 2 +- .../components/zone/.translations/pt-BR.json | 21 ++++++++++++ .../components/zone/.translations/pt.json | 3 +- .../components/zone/.translations/sl.json | 21 ++++++++++++ .../components/zone/.translations/sv.json | 21 ++++++++++++ .../components/zone/.translations/vi.json | 21 ++++++++++++ .../zone/.translations/zh-Hant.json | 21 ++++++++++++ homeassistant/config_entries.py | 3 ++ 70 files changed, 1267 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/cast/.translations/ca.json create mode 100644 homeassistant/components/cast/.translations/ko.json create mode 100644 homeassistant/components/cast/.translations/no.json create mode 100644 homeassistant/components/cast/.translations/pl.json create mode 100644 homeassistant/components/cast/.translations/ru.json create mode 100644 homeassistant/components/cast/.translations/sv.json create mode 100644 homeassistant/components/cast/.translations/vi.json create mode 100644 homeassistant/components/cast/.translations/zh-Hans.json create mode 100644 homeassistant/components/deconz/.translations/ca.json create mode 100644 homeassistant/components/deconz/.translations/cs.json create mode 100644 homeassistant/components/deconz/.translations/fr.json create mode 100644 homeassistant/components/deconz/.translations/it.json create mode 100644 homeassistant/components/deconz/.translations/pt-BR.json create mode 100644 homeassistant/components/deconz/.translations/sv.json create mode 100644 homeassistant/components/deconz/.translations/vi.json create mode 100644 homeassistant/components/hue/.translations/ca.json create mode 100644 homeassistant/components/hue/.translations/cs.json create mode 100644 homeassistant/components/hue/.translations/fr.json create mode 100644 homeassistant/components/hue/.translations/pt-BR.json create mode 100644 homeassistant/components/hue/.translations/sv.json create mode 100644 homeassistant/components/hue/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/ca.json create mode 100644 homeassistant/components/nest/.translations/ko.json create mode 100644 homeassistant/components/nest/.translations/no.json create mode 100644 homeassistant/components/nest/.translations/pl.json create mode 100644 homeassistant/components/nest/.translations/ru.json create mode 100644 homeassistant/components/nest/.translations/sv.json create mode 100644 homeassistant/components/nest/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.ca.json create mode 100644 homeassistant/components/sensor/.translations/season.fr.json create mode 100644 homeassistant/components/sensor/.translations/season.pt-BR.json create mode 100644 homeassistant/components/sonos/.translations/ca.json create mode 100644 homeassistant/components/sonos/.translations/ko.json create mode 100644 homeassistant/components/sonos/.translations/no.json create mode 100644 homeassistant/components/sonos/.translations/pl.json create mode 100644 homeassistant/components/sonos/.translations/ru.json create mode 100644 homeassistant/components/sonos/.translations/sv.json create mode 100644 homeassistant/components/sonos/.translations/vi.json create mode 100644 homeassistant/components/sonos/.translations/zh-Hans.json create mode 100644 homeassistant/components/zone/.translations/bg.json create mode 100644 homeassistant/components/zone/.translations/ca.json create mode 100644 homeassistant/components/zone/.translations/cs.json create mode 100644 homeassistant/components/zone/.translations/fr.json create mode 100644 homeassistant/components/zone/.translations/hu.json create mode 100644 homeassistant/components/zone/.translations/it.json create mode 100644 homeassistant/components/zone/.translations/pt-BR.json create mode 100644 homeassistant/components/zone/.translations/sl.json create mode 100644 homeassistant/components/zone/.translations/sv.json create mode 100644 homeassistant/components/zone/.translations/vi.json create mode 100644 homeassistant/components/zone/.translations/zh-Hant.json diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json new file mode 100644 index 00000000000000..e65e00f8624b69 --- /dev/null +++ b/homeassistant/components/cast/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast." + }, + "step": { + "confirm": { + "description": "Voleu configurar Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json new file mode 100644 index 00000000000000..2be2a69c171327 --- /dev/null +++ b/homeassistant/components/cast/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json new file mode 100644 index 00000000000000..d36c929e7211b5 --- /dev/null +++ b/homeassistant/components/cast/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/pl.json b/homeassistant/components/cast/.translations/pl.json new file mode 100644 index 00000000000000..c4399f95defe81 --- /dev/null +++ b/homeassistant/components/cast/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Google Cast.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Google Cast." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json new file mode 100644 index 00000000000000..9c9353da37e3da --- /dev/null +++ b/homeassistant/components/cast/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sv.json b/homeassistant/components/cast/.translations/sv.json new file mode 100644 index 00000000000000..aea55058d108f7 --- /dev/null +++ b/homeassistant/components/cast/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/vi.json b/homeassistant/components/cast/.translations/vi.json new file mode 100644 index 00000000000000..2f2982293cfdac --- /dev/null +++ b/homeassistant/components/cast/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hans.json b/homeassistant/components/cast/.translations/zh-Hans.json new file mode 100644 index 00000000000000..4a844d3d4dd84a --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index 91727cae257009..2ea6576206375d 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" }, diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json new file mode 100644 index 00000000000000..0a9e6fdee3f68e --- /dev/null +++ b/homeassistant/components/deconz/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ" + }, + "error": { + "no_key": "No s'ha pogut obtenir una clau API" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port (predeterminat: '80')" + }, + "title": "Definiu la passarel\u00b7la deCONZ" + }, + "link": { + "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", + "title": "Vincular amb deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", + "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" + }, + "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json new file mode 100644 index 00000000000000..0721cac3321bfc --- /dev/null +++ b/homeassistant/components/deconz/.translations/cs.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny", + "one_instance_only": "Komponent podporuje pouze jednu instanci deCONZ" + }, + "error": { + "no_key": "Nelze z\u00edskat kl\u00ed\u010d API" + }, + "step": { + "init": { + "data": { + "host": "Hostitel", + "port": "Port (v\u00fdchoz\u00ed hodnota: '80')" + }, + "title": "Definujte br\u00e1nu deCONZ" + }, + "link": { + "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", + "title": "Propojit s deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + }, + "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" + } + }, + "title": "Br\u00e1na deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 465c6c1e0e86d1..f55f64ca43094a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -21,11 +21,11 @@ "title": "Link with deCONZ" }, "options": { - "title": "Extra configuration options for deCONZ", "data": { "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" - } + }, + "title": "Extra configuration options for deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json new file mode 100644 index 00000000000000..02f174cd59f746 --- /dev/null +++ b/homeassistant/components/deconz/.translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", + "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ" + }, + "error": { + "no_key": "Impossible d'obtenir une cl\u00e9 d'API" + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te", + "port": "Port (valeur par d\u00e9faut : 80)" + }, + "title": "Initialiser la passerelle deCONZ" + }, + "link": { + "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", + "title": "Lien vers deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels" + }, + "title": "Options de configuration suppl\u00e9mentaires pour deCONZ" + } + }, + "title": "Passerelle deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 42aab9c6d7e56f..c1fd76c5035fc2 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", + "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" }, "error": { @@ -11,9 +13,11 @@ "data": { "host": "H\u00e1zigazda (Host)", "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" - } + }, + "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" }, "link": { + "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" } }, diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json new file mode 100644 index 00000000000000..6fc7158b88269c --- /dev/null +++ b/homeassistant/components/deconz/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "no_bridges": "Nessun bridge deCONZ rilevato", + "one_instance_only": "Il componente supporto solo un'istanza di deCONZ" + }, + "error": { + "no_key": "Impossibile ottenere una API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Porta (valore di default: '80')" + }, + "title": "Definisci il gateway deCONZ" + }, + "link": { + "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", + "title": "Collega con deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index d6de1028218dee..9c5ffa19257f3b 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -18,9 +18,16 @@ }, "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", - "title": "deCONZ \uc640 \uc5f0\uacb0" + "title": "deCONZ\uc640 \uc5f0\uacb0" + }, + "options": { + "data": { + "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub0b4\uc6a9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" + }, + "title": "deCONZ\ub97c \uc704\ud55c \ucd94\uac00 \uad6c\uc131 \uc635\uc158" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 2a9dfc5e5438dd..46190d23926b8c 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -19,6 +19,12 @@ "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", "title": "Link mat deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + }, + "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 25e3b0b7d68c40..55518b7da532ae 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -19,6 +19,13 @@ "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", "title": "Koble til deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Tillat import av virtuelle sensorer", + "allow_deconz_groups": "Tillat import av deCONZ grupper" + }, + "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index bb7488fcbec1e9..461e8b185eebeb 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -19,6 +19,12 @@ "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w" + }, + "title": "Dodatkowe opcje konfiguracji dla deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json new file mode 100644 index 00000000000000..065c51aee21cdc --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas", + "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro", + "port": "Porta (valor padr\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Linkar com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "Gateway deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 2a00c69869140e..6ccbfe9f217d56 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -1,7 +1,32 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurada" - } + "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "no_bridges": "Nenhum deCONZ descoberto", + "one_instance_only": "Componente suporta apenas uma conex\u00e3o deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Servidor", + "port": "Porta (por omiss\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Link com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index b0dc6a8a4a85f4..56490f67cb3dc6 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -19,6 +19,13 @@ "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" + }, + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index b738002b273d64..59c5577c96b5a8 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -19,6 +19,12 @@ "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", "title": "Povezava z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + }, + "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json new file mode 100644 index 00000000000000..88cf8742acde8c --- /dev/null +++ b/homeassistant/components/deconz/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", + "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans" + }, + "error": { + "no_key": "Det gick inte att ta emot en API-nyckel" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd", + "port": "Port (standardv\u00e4rde: '80')" + }, + "title": "Definiera deCONZ-gatewaye" + }, + "link": { + "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", + "title": "L\u00e4nka med deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", + "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" + }, + "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/vi.json b/homeassistant/components/deconz/.translations/vi.json new file mode 100644 index 00000000000000..00f1d9be57f07e --- /dev/null +++ b/homeassistant/components/deconz/.translations/vi.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "C\u1ea7u \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "no_bridges": "Kh\u00f4ng t\u00ecm th\u1ea5y c\u1ea7u deCONZ n\u00e0o", + "one_instance_only": "Th\u00e0nh ph\u1ea7n ch\u1ec9 h\u1ed7 tr\u1ee3 m\u1ed9t c\u00e1 th\u1ec3 deCONZ" + }, + "error": { + "no_key": "Kh\u00f4ng th\u1ec3 l\u1ea5y kh\u00f3a API" + }, + "step": { + "init": { + "data": { + "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')" + } + }, + "options": { + "data": { + "allow_clip_sensor": "Cho ph\u00e9p nh\u1eadp c\u1ea3m bi\u1ebfn \u1ea3o", + "allow_deconz_groups": "Cho ph\u00e9p nh\u1eadp c\u00e1c nh\u00f3m deCONZ" + }, + "title": "T\u00f9y ch\u1ecdn c\u1ea5u h\u00ecnh b\u1ed5 sung cho deCONZ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index f41b5b5111c2be..2e5a216c77ddec 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -19,6 +19,13 @@ "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8bb8\u5bfc\u5165\u865a\u62df\u4f20\u611f\u5668", + "allow_deconz_groups": "\u5141\u8bb8\u5bfc\u5165 deCONZ \u7fa4\u7ec4" + }, + "title": "deCONZ \u7684\u9644\u52a0\u914d\u7f6e\u9879" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 33be3846eb8290..17cbe87f1e8f9b 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" }, @@ -18,6 +19,12 @@ "link": { "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + }, + "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, "title": "deCONZ" diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json new file mode 100644 index 00000000000000..6c41eed5467ac9 --- /dev/null +++ b/homeassistant/components/hue/.translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats", + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "cannot_connect": "No es pot connectar amb l'enlla\u00e7", + "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", + "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "error": { + "linking": "S'ha produ\u00eft un error desconegut al vincular.", + "register_failed": "No s'ha pogut registrar, torneu-ho a provar" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Tria l'enlla\u00e7 Hue" + }, + "link": { + "description": "Premeu el bot\u00f3 de l'ella\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", + "title": "Vincular concentrador" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cs.json b/homeassistant/components/hue/.translations/cs.json new file mode 100644 index 00000000000000..35c423b1a03420 --- /dev/null +++ b/homeassistant/components/hue/.translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "V\u0161echny Philips Hue p\u0159emost\u011bn\u00ed jsou ji\u017e nakonfigurov\u00e1ny", + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nelze se p\u0159ipojit k p\u0159emost\u011bn\u00ed", + "discover_timeout": "Nelze nal\u00e9zt p\u0159emost\u011bn\u00ed Hue", + "no_bridges": "Nebyly nalezeny \u017e\u00e1dn\u00e9 p\u0159emost\u011bn\u00ed Philips Hue", + "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" + }, + "error": { + "linking": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b propojen\u00ed.", + "register_failed": "Registrace se nezda\u0159ila, zkuste to pros\u00edm znovu" + }, + "step": { + "init": { + "data": { + "host": "Hostitel" + }, + "title": "Vybrat Hue p\u0159emost\u011bn\u00ed" + }, + "link": { + "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed] (/ static/images/config_philips_hue.jpg)", + "title": "P\u0159ipojit Hub" + } + }, + "title": "Philips Hue p\u0159emost\u011bn\u00ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index b0459ec39163ab..cea8d8be10af34 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json new file mode 100644 index 00000000000000..73613f237dac3b --- /dev/null +++ b/homeassistant/components/hue/.translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "Connexion au pont impossible", + "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", + "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", + "unknown": "Une erreur inconnue s'est produite" + }, + "error": { + "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant", + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te" + }, + "title": "Choisissez le pont Philips Hue" + }, + "link": { + "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont] (/static/images/config_philips_hue.jpg)", + "title": "Hub de liaison" + } + }, + "title": "Pont Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json index a4032dcbcfc215..be6548f59a0e91 100644 --- a/homeassistant/components/hue/.translations/hu.json +++ b/homeassistant/components/hue/.translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", - "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", @@ -20,6 +20,7 @@ "title": "V\u00e1lassz Hue bridge-t" }, "link": { + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" } }, diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json index 2c7a8c1924d6db..a9f2a732127a23 100644 --- a/homeassistant/components/hue/.translations/it.json +++ b/homeassistant/components/hue/.translations/it.json @@ -2,8 +2,27 @@ "config": { "abort": { "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi al bridge", "discover_timeout": "Impossibile trovare i bridge Hue", - "no_bridges": "Nessun bridge Hue di Philips trovato" + "no_bridges": "Nessun bridge Hue di Philips trovato", + "unknown": "Si \u00e8 verificato un errore" + }, + "error": { + "linking": "Si \u00e8 verificato un errore sconosciuto in fase di collegamento.", + "register_failed": "Errore in fase di registrazione, riprova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Selezione il bridge Hue" + }, + "link": { + "description": "Premi il pulsante sul bridge per registrare Philips Hue con Home Assistant\n\n![Posizione del pulsante sul bridge](/static/images/config_philips_hue.jpg)", + "title": "Collega Hub" + } }, "title": "Philips Hue Bridge" } diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json new file mode 100644 index 00000000000000..5c6e409245c7e9 --- /dev/null +++ b/homeassistant/components/hue/.translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Todas as pontes Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte", + "discover_timeout": "Incapaz de descobrir pontes Hue", + "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falhou ao registrar, por favor tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro" + }, + "title": "Escolha a ponte Hue" + }, + "link": { + "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/static/images/config_philips_hue.jpg)", + "title": "Hub de links" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index 8c4c45f9c897c8..f7988d82d8ce3b 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -1,5 +1,29 @@ { "config": { + "abort": { + "all_configured": "Todas os Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "cannot_connect": "N\u00e3o foi poss\u00edvel se conectar", + "discover_timeout": "Nenhum Hue bridge descoberto", + "no_bridges": "Nenhum Philips Hue descoberto", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falha ao registrar, por favor, tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Servidor" + }, + "title": "Hue bridge" + }, + "link": { + "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n ! [Localiza\u00e7\u00e3o do bot\u00e3o] (/ static / images / config_philips_hue.jpg)", + "title": "Link Hub" + } + }, "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json new file mode 100644 index 00000000000000..efbcfa544f5d81 --- /dev/null +++ b/homeassistant/components/hue/.translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade", + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta till bryggan", + "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor", + "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes", + "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "linking": "Ett ok\u00e4nt l\u00e4nkningsfel intr\u00e4ffade.", + "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd" + }, + "title": "V\u00e4lj Hue-brygga" + }, + "link": { + "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n ! [Placering av knapp p\u00e5 brygga] (/ static / images / config_philips_hue.jpg)", + "title": "L\u00e4nka hub" + } + }, + "title": "Philips Hue Brygga" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/vi.json b/homeassistant/components/hue/.translations/vi.json new file mode 100644 index 00000000000000..5cbd0c4aebfbd0 --- /dev/null +++ b/homeassistant/components/hue/.translations/vi.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "all_configured": "T\u1ea5t c\u1ea3 c\u00e1c c\u1ea7u Philips Hue \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "unknown": "X\u1ea3y ra l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c" + }, + "error": { + "linking": "\u0110\u00e3 x\u1ea3y ra l\u1ed7i li\u00ean k\u1ebft kh\u00f4ng x\u00e1c \u0111\u1ecbnh.", + "register_failed": "Kh\u00f4ng th\u1ec3 \u0111\u0103ng k\u00fd, vui l\u00f2ng th\u1eed l\u1ea1i" + }, + "step": { + "link": { + "title": "Li\u00ean k\u1ebft Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json new file mode 100644 index 00000000000000..2fb17916aee81b --- /dev/null +++ b/homeassistant/components/nest/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s podeu configurar un \u00fanic compte Nest.", + "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "Temps d'espera generant l'URL d'autoritzaci\u00f3 esgotat.", + "no_flows": "Necessiteu configurar Nest abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Error intern al validar el codi", + "invalid_code": "Codi inv\u00e0lid", + "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat", + "unknown": "Error desconegut al validar el codi" + }, + "step": { + "init": { + "data": { + "flow_impl": "Prove\u00efdor" + }, + "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Nest.", + "title": "Prove\u00efdor d'autenticaci\u00f3" + }, + "link": { + "data": { + "code": "Codi pin" + }, + "description": "Per enlla\u00e7ar el vostre compte de Nest, [autoritzeu el vostre compte] ({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copieu i enganxeu el codi pin que es mostra a sota.", + "title": "Enlla\u00e7ar compte de Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json new file mode 100644 index 00000000000000..0caa70aeff2853 --- /dev/null +++ b/homeassistant/components/nest/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, + "error": { + "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc", + "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc", + "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd" + }, + "step": { + "init": { + "data": { + "flow_impl": "\uacf5\uae09\uc790" + }, + "description": "Nest\ub85c \uc778\uc99d\ud558\ub824\ub294 \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\uc778\uc99d \uacf5\uae09\uc790" + }, + "link": { + "data": { + "code": "\ud540 \ucf54\ub4dc" + }, + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "title": "Nest \uacc4\uc815 \uc5f0\uacb0" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json new file mode 100644 index 00000000000000..03cf1a82b813bf --- /dev/null +++ b/homeassistant/components/nest/.translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern feil ved validering av kode", + "invalid_code": "Ugyldig kode", + "timeout": "Tidsavbrudd ved validering av kode", + "unknown": "Ukjent feil ved validering av kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Tilbyder" + }, + "description": "Velg via hvilken autentiseringstilbyder du vil godkjenne med Nest.", + "title": "Autentiseringstilbyder" + }, + "link": { + "data": { + "code": "PIN kode" + }, + "description": "For \u00e5 koble din Nest-konto, [autoriser kontoen din]({url}). \n\n Etter godkjenning, kopier og lim inn den oppgitte PIN koden nedenfor.", + "title": "Koble til Nest konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json new file mode 100644 index 00000000000000..c03b2eff0fabd0 --- /dev/null +++ b/homeassistant/components/nest/.translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcje](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", + "invalid_code": "Nieprawid\u0142owy kod", + "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu", + "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu" + }, + "step": { + "init": { + "data": { + "flow_impl": "Dostawca" + }, + "description": "Wybierz, kt\u00f3rego dostawc\u0119 uwierzytelnienia chcesz u\u017cywa\u0107 z Nest.", + "title": "Dostawca uwierzytelnienia" + }, + "link": { + "data": { + "code": "Kod PIN" + }, + "description": "Aby po\u0142\u0105czy\u0107 z kontem Nest, [wykonaj autoryzacj\u0119]({url}). \n\n Po autoryzacji skopiuj i wklej podany kod PIN poni\u017cej.", + "title": "Po\u0142\u0105cz z kontem Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json new file mode 100644 index 00000000000000..0f7b9b8dd719c2 --- /dev/null +++ b/homeassistant/components/nest/.translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest.", + "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "link": { + "data": { + "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" + }, + "description": " [\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sv.json b/homeassistant/components/nest/.translations/sv.json new file mode 100644 index 00000000000000..721f891219daa5 --- /dev/null +++ b/homeassistant/components/nest/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Nest-konto.", + "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.", + "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Internt fel vid validering av kod", + "invalid_code": "Ogiltig kod", + "timeout": "Timeout vid valididering av kod", + "unknown": "Ok\u00e4nt fel vid validering av kod" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverant\u00f6r" + }, + "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Nest.", + "title": "Autentiseringsleverant\u00f6r" + }, + "link": { + "data": { + "code": "Pin-kod" + }, + "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", + "title": "L\u00e4nka Nest-konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/vi.json b/homeassistant/components/nest/.translations/vi.json new file mode 100644 index 00000000000000..996c6c68eae9e3 --- /dev/null +++ b/homeassistant/components/nest/.translations/vi.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "internal_error": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i n\u1ed9i b\u1ed9", + "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7", + "timeout": "M\u00e3 x\u00e1c th\u1ef1c h\u1ebft th\u1eddi gian ch\u1edd", + "unknown": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh" + }, + "step": { + "init": { + "data": { + "flow_impl": "Nh\u00e0 cung c\u1ea5p" + }, + "title": "Nh\u00e0 cung c\u1ea5p x\u00e1c th\u1ef1c" + }, + "link": { + "title": "Li\u00ean k\u1ebft t\u00e0i kho\u1ea3n Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json new file mode 100644 index 00000000000000..05ba5bdf15525a --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hans.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Nest \u5e10\u6237\u3002", + "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", + "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", + "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Nest\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/nest/)\u3002" + }, + "error": { + "internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef", + "invalid_code": "\u65e0\u6548\u4ee3\u7801", + "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6", + "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u63d0\u4f9b\u8005" + }, + "description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002", + "title": "\u6388\u6743\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u7801" + }, + "description": "\u8981\u5173\u8054 Nest \u5e10\u6237\uff0c\u8bf7[\u6388\u6743\u5e10\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002", + "title": "\u5173\u8054 Nest \u5e10\u6237" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ca.json b/homeassistant/components/sensor/.translations/season.ca.json new file mode 100644 index 00000000000000..9bce187ec65d91 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Tardor", + "spring": "Primavera", + "summer": "Estiu", + "winter": "Hivern" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.fr.json b/homeassistant/components/sensor/.translations/season.fr.json new file mode 100644 index 00000000000000..ec9f9657428917 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Automne", + "spring": "Printemps", + "summer": "\u00c9t\u00e9", + "winter": "Hiver" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt-BR.json b/homeassistant/components/sensor/.translations/season.pt-BR.json new file mode 100644 index 00000000000000..fde45ad6c8efa0 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json new file mode 100644 index 00000000000000..9a745784b25fd2 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Sonos." + }, + "step": { + "confirm": { + "description": "Voleu configurar Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json new file mode 100644 index 00000000000000..5453e4322cd094 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Sonos \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Sonos\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json new file mode 100644 index 00000000000000..c837abad499db4 --- /dev/null +++ b/homeassistant/components/sonos/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json new file mode 100644 index 00000000000000..2a0c526b9a64ac --- /dev/null +++ b/homeassistant/components/sonos/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Sonos.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Sonos." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json new file mode 100644 index 00000000000000..63b6bd87c20b47 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Sonos." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sv.json b/homeassistant/components/sonos/.translations/sv.json new file mode 100644 index 00000000000000..756fe8a74832d2 --- /dev/null +++ b/homeassistant/components/sonos/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/vi.json b/homeassistant/components/sonos/.translations/vi.json new file mode 100644 index 00000000000000..ebeb1a8b07ce31 --- /dev/null +++ b/homeassistant/components/sonos/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Sonos n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Sonos l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json new file mode 100644 index 00000000000000..17c1e78d3e8922 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json new file mode 100644 index 00000000000000..5770058c5ebc4f --- /dev/null +++ b/homeassistant/components/zone/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, + "step": { + "init": { + "data": { + "icon": "\u0418\u043a\u043e\u043d\u0430", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json new file mode 100644 index 00000000000000..1676c8f390627a --- /dev/null +++ b/homeassistant/components/zone/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom", + "passive": "Passiu", + "radius": "Radi" + }, + "title": "Defineix els par\u00e0metres de la zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json new file mode 100644 index 00000000000000..a521377e5e0a59 --- /dev/null +++ b/homeassistant/components/zone/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "N\u00e1zev ji\u017e existuje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev", + "passive": "Pasivn\u00ed", + "radius": "Polom\u011br" + }, + "title": "Definujte parametry z\u00f3ny" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json new file mode 100644 index 00000000000000..eb02aba7b50c05 --- /dev/null +++ b/homeassistant/components/zone/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "init": { + "data": { + "icon": "Ic\u00f4ne", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom", + "passive": "Passif", + "radius": "Rayon" + }, + "title": "D\u00e9finir les param\u00e8tres de la zone" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json new file mode 100644 index 00000000000000..0181f688c27d0d --- /dev/null +++ b/homeassistant/components/zone/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v", + "passive": "Passz\u00edv", + "radius": "Sug\u00e1r" + }, + "title": "Z\u00f3na param\u00e9terek megad\u00e1sa" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json new file mode 100644 index 00000000000000..4490124510fa4d --- /dev/null +++ b/homeassistant/components/zone/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome", + "passive": "Passiva", + "radius": "Raggio" + }, + "title": "Imposta i parametri della zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json index 364f8f3cc77f3a..421f079a67ea48 100644 --- a/homeassistant/components/zone/.translations/ko.json +++ b/homeassistant/components/zone/.translations/ko.json @@ -13,7 +13,7 @@ "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", "radius": "\ubc18\uacbd" }, - "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + "title": "\uad6c\uc5ed \uc124\uc815" } }, "title": "\uad6c\uc5ed" diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json new file mode 100644 index 00000000000000..f2a41b0b26785c --- /dev/null +++ b/homeassistant/components/zone/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "O nome j\u00e1 existe" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + }, + "title": "Definir par\u00e2metros da zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json index a4ced557805661..2c3292e58c192d 100644 --- a/homeassistant/components/zone/.translations/pt.json +++ b/homeassistant/components/zone/.translations/pt.json @@ -12,7 +12,8 @@ "name": "Nome", "passive": "Passivo", "radius": "Raio" - } + }, + "title": "Definir os par\u00e2metros da zona" } }, "title": "Zona" diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json new file mode 100644 index 00000000000000..1885cb5d2c86bd --- /dev/null +++ b/homeassistant/components/zone/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radij" + }, + "title": "Dolo\u010dite parametre obmo\u010dja" + } + }, + "title": "Obmo\u010dje" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json new file mode 100644 index 00000000000000..55c5bcf712721c --- /dev/null +++ b/homeassistant/components/zone/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn", + "passive": "Passiv", + "radius": "Radie" + }, + "title": "Definiera zonparametrar" + } + }, + "title": "Zon" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json new file mode 100644 index 00000000000000..7217944bd6b631 --- /dev/null +++ b/homeassistant/components/zone/.translations/vi.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i" + }, + "step": { + "init": { + "data": { + "icon": "Bi\u1ec3u t\u01b0\u1ee3ng", + "latitude": "V\u0129 \u0111\u1ed9", + "longitude": "Kinh \u0111\u1ed9", + "name": "T\u00ean", + "passive": "Th\u1ee5 \u0111\u1ed9ng", + "radius": "B\u00e1n k\u00ednh" + }, + "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng" + } + }, + "title": "V\u00f9ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json new file mode 100644 index 00000000000000..12c1141397d7ef --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u5716\u793a", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31", + "passive": "\u88ab\u52d5", + "radius": "\u534a\u5f91" + }, + "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578" + } + }, + "title": "\u5340\u57df" + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4fbbbb77b794b4..db2912d7b42297 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,8 @@ async def async_step_discovery(info): data_entry_flow.SOURCE_IMPORT, ) +EVENT_FLOW_DISCOVERED = 'config_entry_discovered' + class ConfigEntry: """Hold a configuration entry.""" @@ -404,6 +406,7 @@ async def _async_create_flow(self, handler, *, source, data): # Create notification. if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) self.hass.components.persistent_notification.async_create( title='New devices discovered', message=("We have discovered new devices on your network. " From 86c6b4d8e3bfb728f4917e17fcb2f06f1faa3efd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 17 Jun 2018 20:34:47 +0200 Subject: [PATCH 025/169] Fix panel URL authentication for Hass.io (#15024) * Update http.py * Update http.py * fix tests * Update test_http.py --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index bb4f8219a333bd..c51d45cc3396eb 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ } NO_AUTH = { - re.compile(r'^app-(es5|latest)/.+$'), + re.compile(r'^app/.*$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 5f2c9c009c33d4..ce260225097a12 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -47,8 +47,8 @@ def test_auth_required_forward_request(hassio_client): @asyncio.coroutine @pytest.mark.parametrize( 'build_type', [ - 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', + 'app/index.html', 'app/hassio-app.html', 'app/index.html', + 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From 5a3ea74a2614bf1b9203e864d34d33d291af138e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 09:58:16 -0400 Subject: [PATCH 026/169] Bump frontend to 20180618.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 25aa0da0a3e27f..2c9b68bf079bd9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180617.0'] +REQUIREMENTS = ['home-assistant-frontend==20180618.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d860112c7f8f7a..e2507fa8cbec4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2245c02cf1434..d4a24fbc0846d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From ef39bca52eb24622d79bed0e3bdc09b6b14eebe4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 18 Jun 2018 15:21:41 +0200 Subject: [PATCH 027/169] Fix linode I/O in state property (#15010) * Fix linode I/O in state property * Move update of all attrs to update --- .../components/binary_sensor/linode.py | 33 ++++++++--------- homeassistant/components/switch/linode.py | 35 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/binary_sensor/linode.py index 8af0318373d5a0..d4fc60696cdafd 100644 --- a/homeassistant/components/binary_sensor/linode.py +++ b/homeassistant/components/binary_sensor/linode.py @@ -52,19 +52,18 @@ def __init__(self, li, node_id): self._node_id = node_id self._state = None self.data = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the sensor.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if the binary sensor is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_class(self): @@ -74,8 +73,18 @@ def device_class(self): @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { + return self._attrs + + def update(self): + """Update state of sensor.""" + self._linode.update() + if self._linode.data is not None: + for node in self._linode.data: + if node.id == self._node_id: + self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { ATTR_CREATED: self.data.created, ATTR_NODE_ID: self.data.id, ATTR_NODE_NAME: self.data.label, @@ -85,12 +94,4 @@ def device_state_attributes(self): ATTR_REGION: self.data.region.country, ATTR_VCPUS: self.data.specs.vcpus, } - return {} - - def update(self): - """Update state of sensor.""" - self._linode.update() - if self._linode.data is not None: - for node in self._linode.data: - if node.id == self._node_id: - self.data = node + self._name = self.data.label diff --git a/homeassistant/components/switch/linode.py b/homeassistant/components/switch/linode.py index 91177e321169ab..43f4bdc31b4b72 100644 --- a/homeassistant/components/switch/linode.py +++ b/homeassistant/components/switch/linode.py @@ -51,35 +51,23 @@ def __init__(self, li, node_id): self._node_id = node_id self.data = None self._state = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the switch.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if switch is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { - ATTR_CREATED: self.data.created, - ATTR_NODE_ID: self.data.id, - ATTR_NODE_NAME: self.data.label, - ATTR_IPV4_ADDRESS: self.data.ipv4, - ATTR_IPV6_ADDRESS: self.data.ipv6, - ATTR_MEMORY: self.data.specs.memory, - ATTR_REGION: self.data.region.country, - ATTR_VCPUS: self.data.specs.vcpus, - } - return {} + return self._attrs def turn_on(self, **kwargs): """Boot-up the Node.""" @@ -98,3 +86,16 @@ def update(self): for node in self._linode.data: if node.id == self._node_id: self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { + ATTR_CREATED: self.data.created, + ATTR_NODE_ID: self.data.id, + ATTR_NODE_NAME: self.data.label, + ATTR_IPV4_ADDRESS: self.data.ipv4, + ATTR_IPV6_ADDRESS: self.data.ipv6, + ATTR_MEMORY: self.data.specs.memory, + ATTR_REGION: self.data.region.country, + ATTR_VCPUS: self.data.specs.vcpus, + } + self._name = self.data.label From e29dfa8609d555b8e69c74ebf7f829f6ff431d53 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Jun 2018 02:24:11 +0200 Subject: [PATCH 028/169] Upgrade aiohttp to 3.3.2 (#15025) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c69e9eb4af41fa..5e7386242baa1c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index e2507fa8cbec4a..bbf74004be4a04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/setup.py b/setup.py index a4d15feb7fc324..f914e032fd7325 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'aiohttp==3.3.0', + 'aiohttp==3.3.2', 'astral==1.6.1', 'async_timeout==3.0.0', 'attrs==18.1.0', From e0cea2d18d25ed6dda440a0c185af6aabf5c3ed3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:55:35 -0400 Subject: [PATCH 029/169] Make zone entries work without radius (#15032) --- homeassistant/components/zone/__init__.py | 4 ++-- tests/components/zone/test_init.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index c33a16c632e629..ee19e00266c7fc 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -73,8 +73,8 @@ async def async_setup_entry(hass, config_entry): entry = config_entry.data name = entry[CONF_NAME] zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), entry.get(CONF_ICON), - entry.get(CONF_PASSIVE)) + entry.get(CONF_RADIUS, DEFAULT_RADIUS), entry.get(CONF_ICON), + entry.get(CONF_PASSIVE, DEFAULT_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, name, None, hass) hass.async_add_job(zone.async_update_ha_state()) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index c26b3375f3ace4..92dee05818dedb 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -17,7 +17,6 @@ async def test_setup_entry_successful(hass): zone.CONF_NAME: 'Test Zone', zone.CONF_LATITUDE: 1.1, zone.CONF_LONGITUDE: -2.2, - zone.CONF_RADIUS: 250, zone.CONF_RADIUS: True } hass.data[zone.DOMAIN] = {} From 60179a1cbb4597a9a37a2a801dbc69f5c991ee8a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 18 Jun 2018 15:22:52 +0200 Subject: [PATCH 030/169] Bugfix empty entity lists (#15035) * Bugfix empty entity lists * Add tests * Update test_entity_platform.py * Update entity_platform.py --- homeassistant/helpers/entity_platform.py | 4 ++++ tests/helpers/test_entity_platform.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ab6c3a084c07c2..472a88888d88f5 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -216,6 +216,10 @@ async def async_add_entities(self, new_entities, update_before_add=False): component_entities, registry) for entity in new_entities] + # No entities for processing + if not tasks: + return + await asyncio.wait(tasks, loop=self.hass.loop) self.async_entities_added_callback() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9fa178022dc43c..2d2f148189f683 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -592,3 +592,13 @@ async def test_reset_cancels_retry_setup(hass): assert len(mock_call_later.return_value.mock_calls) == 1 assert ent_platform._async_cancel_retry_setup is None + + +@asyncio.coroutine +def test_not_fails_with_adding_empty_entities_(hass): + """Test for not fails on empty entities list.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([]) + + assert len(hass.states.async_entity_ids()) == 0 From ef5b2a2492439969a167782f4280e0ca24f632bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 10:00:24 -0400 Subject: [PATCH 031/169] Version bump to 0.72.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 562247a14c06ea..72f018ad366809 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9800b74a6de576390f6c6818b5e9ef588671587d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 10:00:47 -0400 Subject: [PATCH 032/169] Version bump to 0.72.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 72f018ad366809..7682234233edbb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3b4f7b4f5de5be5dbca49014fcc07cd310f1e56c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jun 2018 10:56:33 -0400 Subject: [PATCH 033/169] Update frontend to 20180619.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2c9b68bf079bd9..9af1a7af3bedda 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180618.0'] +REQUIREMENTS = ['home-assistant-frontend==20180619.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index bbf74004be4a04..766ab10671c130 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4a24fbc0846d0..b32da6fc9f28a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 659616a4eb285d089a41632bb65abeec66eb506d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jun 2018 10:58:57 -0400 Subject: [PATCH 034/169] Version bump to 0.72.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7682234233edbb..98179b8502e71c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 49845d9398f642ec20e77c62a7487724b050a9bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Jun 2018 15:13:08 -0400 Subject: [PATCH 035/169] Rename experimental UI to lovelace (#15065) * Rename experimental UI to lovelace * Bump frontend to 20180620.0 --- homeassistant/components/frontend/__init__.py | 44 +++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/frontend/__init__.py | 1 + .../test_init.py} | 45 +++++++++++++++++-- 5 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 tests/components/frontend/__init__.py rename tests/components/{test_frontend.py => frontend/test_init.py} (86%) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9af1a7af3bedda..b2cac55bd77421 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,11 +21,12 @@ from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180619.0'] +REQUIREMENTS = ['home-assistant-frontend==20180620.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -106,9 +107,9 @@ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) -WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' -SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, }) @@ -216,8 +217,8 @@ async def async_setup(hass, config): WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, - SCHEMA_GET_EXPERIMENTAL_UI) + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -265,7 +266,7 @@ def async_finalize_panel(panel): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -499,15 +500,26 @@ async def send_translations(): hass.async_add_job(send_translations()) -def websocket_experimental_config(hass, connection, msg): - """Send experimental UI config over websocket config.""" +def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" async def send_exp_config(): - """Send experimental frontend config.""" - config = await hass.async_add_job( - load_yaml, hass.config.path('experimental-ui.yaml')) - - connection.send_message_outside(websocket_api.result_message( - msg['id'], config - )) + """Send lovelace frontend config.""" + error = None + try: + config = await hass.async_add_job( + load_yaml, hass.config.path('ui-lovelace.yaml')) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message_outside(message) hass.async_add_job(send_exp_config()) diff --git a/requirements_all.txt b/requirements_all.txt index 766ab10671c130..56896c9b6da0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b32da6fc9f28a3..177796961a5bac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/frontend/__init__.py b/tests/components/frontend/__init__.py new file mode 100644 index 00000000000000..991a74dee7a1c7 --- /dev/null +++ b/tests/components/frontend/__init__.py @@ -0,0 +1 @@ +"""Tests for the frontend component.""" diff --git a/tests/components/test_frontend.py b/tests/components/frontend/test_init.py similarity index 86% rename from tests/components/test_frontend.py rename to tests/components/frontend/test_init.py index cb0c72e9edd1a9..2125668facb8a9 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/frontend/test_init.py @@ -5,6 +5,7 @@ import pytest +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, @@ -280,8 +281,8 @@ async def test_get_translations(hass, hass_ws_client): assert msg['result'] == {'resources': {'lang': 'nl'}} -async def test_experimental_ui(hass, hass_ws_client): - """Test experimental_ui command.""" +async def test_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" await async_setup_component(hass, 'frontend') client = await hass_ws_client(hass) @@ -289,7 +290,7 @@ async def test_experimental_ui(hass, hass_ws_client): return_value={'hello': 'world'}): await client.send_json({ 'id': 5, - 'type': 'frontend/experimental_ui', + 'type': 'frontend/lovelace_config', }) msg = await client.receive_json() @@ -297,3 +298,41 @@ async def test_experimental_ui(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'hello': 'world'} + + +async def test_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' From c84f1d7d33b72f11fd765369b5ae67384e8863f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Jun 2018 15:13:33 -0400 Subject: [PATCH 036/169] Version bump to 0.72.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 98179b8502e71c..091bd907b93dc1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 8c2f0e3b3071ead3a7237e50b40661b530007de5 Mon Sep 17 00:00:00 2001 From: hanzoh Date: Thu, 21 Jun 2018 14:52:02 +0200 Subject: [PATCH 037/169] Homematic: Add optional port for resolvenames via JSON (#15029) * Add optional JSON port --- homeassistant/components/homematic/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 2e05f638afc37c..1428bbd3e563f7 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -148,6 +148,7 @@ CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_PORT = 'callback_port' CONF_RESOLVENAMES = 'resolvenames' +CONF_JSONPORT = 'jsonport' CONF_VARIABLES = 'variables' CONF_DEVICES = 'devices' CONF_PRIMARY = 'primary' @@ -155,6 +156,7 @@ DEFAULT_LOCAL_IP = '0.0.0.0' DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False +DEFAULT_JSONPORT = 80 DEFAULT_PORT = 2001 DEFAULT_PATH = '' DEFAULT_USERNAME = 'Admin' @@ -178,6 +180,7 @@ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string, @@ -299,6 +302,7 @@ def setup(hass, config): 'port': rconfig.get(CONF_PORT), 'path': rconfig.get(CONF_PATH), 'resolvenames': rconfig.get(CONF_RESOLVENAMES), + 'jsonport': rconfig.get(CONF_JSONPORT), 'username': rconfig.get(CONF_USERNAME), 'password': rconfig.get(CONF_PASSWORD), 'callbackip': rconfig.get(CONF_CALLBACK_IP), From 4048ad36a85adebc543d5328dc9bb4e49f0309c9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 21 Jun 2018 15:06:05 +0200 Subject: [PATCH 038/169] Add script to run monkeytype typing on test suite (#14440) * The monkeytype script takes an optional argument to specify a test module or directory to run. Otherwise the whole test suite will run. * Add monkeytype sqlite db to gitignore. --- .gitignore | 3 +++ script/monkeytype | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100755 script/monkeytype diff --git a/.gitignore b/.gitignore index bf49a1b61c1fea..c2b0d964a6225a 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,6 @@ desktop.ini # Secrets .lokalise_token + +# monkeytype +monkeytype.sqlite3 diff --git a/script/monkeytype b/script/monkeytype new file mode 100755 index 00000000000000..dc1894c91edea2 --- /dev/null +++ b/script/monkeytype @@ -0,0 +1,25 @@ +#!/bin/sh +# Run monkeytype on test suite or optionally on a test module or directory. + +# Stop on errors +set -e + +cd "$(dirname "$0")/.." + +command -v pytest >/dev/null 2>&1 || { + echo >&2 "This script requires pytest but it's not installed." \ + "Aborting. Try: pip install pytest"; exit 1; } + +command -v monkeytype >/dev/null 2>&1 || { + echo >&2 "This script requires monkeytype but it's not installed." \ + "Aborting. Try: pip install monkeytype"; exit 1; } + +if [ $# -eq 0 ] + then + echo "Run monkeytype on test suite" + monkeytype run "`command -v pytest`" + exit +fi + +echo "Run monkeytype on tests in $1" +monkeytype run "`command -v pytest`" "$1" From b687de879c25c4101e3da12a624ce65923de9f61 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 14:57:08 -0400 Subject: [PATCH 039/169] Update frontend to 20180621.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b2cac55bd77421..9200f4d78f65e7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180620.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d47496fea595c8..62a73303899a04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e12ef3910a543..69435adc83fc71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e98e7e2751510fa9b2ea6da7b0cad7f1afdc76a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 14:57:08 -0400 Subject: [PATCH 040/169] Update frontend to 20180621.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b2cac55bd77421..9200f4d78f65e7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180620.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 56896c9b6da0a4..c0c85012526e14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 177796961a5bac..8f09c4d7195d96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bfc55137ea06a1a534088b566fb50c0abda871a0 Mon Sep 17 00:00:00 2001 From: Bob Clough Date: Thu, 21 Jun 2018 19:59:03 +0100 Subject: [PATCH 041/169] Fix MQTT Light with RGB and Brightness (#15053) * Fix MQTT Light with RGB and Brightness When an MQTT light is given an RGB and Brightness topic, the RGB is scaled by the brightness *as well* as the brightness being set This causes 255,0,0 at 50% brightness to be sent as 127,0,0 at 50% brightness, which ends up as 63,0,0 after the RGB bulb has applied its brightness scaling. Fixes the same issue in mqtt, mqtt-json and mqtt-template. Related Issue: #13725 * Add comment to mqtt_json as well --- homeassistant/components/light/mqtt.py | 11 +++++++++-- homeassistant/components/light/mqtt_json.py | 11 ++++++++--- homeassistant/components/light/mqtt_template.py | 11 +++++++++-- tests/components/light/test_mqtt.py | 14 +++++++------- tests/components/light/test_mqtt_json.py | 4 ++-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 97a4cc8c137ea4..c0e363f85d6d40 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ async def async_turn_on(self, **kwargs): self._topic[CONF_RGB_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 14f5ee7a9b9142..705e106fdff8be 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ async def async_turn_on(self, **kwargs): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: - brightness = kwargs.get( - ATTR_BRIGHTNESS, - self._brightness if self._brightness else 255) + # If there's a brightness topic set, we don't want to scale the + # RGB values given using the brightness. + if self._brightness is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) message['color']['r'] = rgb[0] diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index e32c13fc5b6eff..f6b3fbe8b70799 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ async def async_turn_on(self, **kwargs): if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) values['red'] = rgb[0] diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 8b51adb2187399..49bcd8a73ecc0c 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.mock_publish.reset_mock() light.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), + mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), + mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.323, 0.329), state.attributes['xy_color']) + self.assertEqual((0.611, 0.375), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -808,11 +808,11 @@ def test_on_command_brightness(self): # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '50,50,50', 0, False), + mock.call('test_light/rgb', '255,128,0', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 275fb42ede917c..af560bff9c3224 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.assertEqual(50, message_json["brightness"]) self.assertEqual({ 'r': 0, - 'g': 50, - 'b': 4, + 'g': 255, + 'b': 21, }, message_json["color"]) self.assertEqual("ON", message_json["state"]) From 4b5d578c08d9fd69e8f1455152a4483a98e551f2 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 20 Jun 2018 20:44:05 -0500 Subject: [PATCH 042/169] X10 (#14741) * Implement X10 * Add X10 after add_device_callback * Ref device by id not hex and add x10OnOffSwitch name * X10 services and add sensor device * Correctly reference X10_HOUSECODE_SCHEMA * Log adding of X10 devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct ref to X10 states vs devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct X10 config * Debug x10 device additions * Config x10 from bool to housecode char * Pass PLM to X10 device create * Remove PLM to call to add_x10_device * Unconfuse x10 config and method names * Correct spelling of x10_all_lights_off_housecode * Bump insteonplm to 0.10.0 to support X10 --- .../components/insteon_plm/__init__.py | 111 +++++++++++++++++- .../components/insteon_plm/services.yaml | 18 +++ .../components/switch/insteon_plm.py | 3 +- requirements_all.txt | 2 +- 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index b86f80cbee788d..b2f7c8b66551bd 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.2'] +REQUIREMENTS = ['insteonplm==0.10.0'] _LOGGER = logging.getLogger(__name__) @@ -29,17 +29,31 @@ CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +CONF_X10 = 'x10_devices' +CONF_HOUSECODE = 'housecode' +CONF_UNITCODE = 'unitcode' +CONF_DIM_STEPS = 'dim_steps' +CONF_X10_ALL_UNITS_OFF = 'x10_all_units_off' +CONF_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' +CONF_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' SRV_ADD_ALL_LINK = 'add_all_link' SRV_DEL_ALL_LINK = 'delete_all_link' SRV_LOAD_ALDB = 'load_all_link_database' SRV_PRINT_ALDB = 'print_all_link_database' SRV_PRINT_IM_ALDB = 'print_im_all_link_database' +SRV_X10_ALL_UNITS_OFF = 'x10_all_units_off' +SRV_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' +SRV_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' SRV_ALL_LINK_GROUP = 'group' SRV_ALL_LINK_MODE = 'mode' SRV_LOAD_DB_RELOAD = 'reload' SRV_CONTROLLER = 'controller' SRV_RESPONDER = 'responder' +SRV_HOUSECODE = 'housecode' + +HOUSECODES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'] CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ @@ -51,11 +65,24 @@ vol.Optional(CONF_PLATFORM): cv.string, })) +CONF_X10_SCHEMA = vol.All( + vol.Schema({ + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255) + })) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PORT): cv.string, vol.Optional(CONF_OVERRIDE): vol.All( - cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]) + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]), + vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10): vol.All( + cv.ensure_list_csv, [CONF_X10_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -77,6 +104,10 @@ vol.Required(CONF_ENTITY_ID): cv.entity_id, }) +X10_HOUSECODE_SCHEMA = vol.Schema({ + vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES), + }) + @asyncio.coroutine def async_setup(hass, config): @@ -89,6 +120,10 @@ def async_setup(hass, config): conf = config[DOMAIN] port = conf.get(CONF_PORT) overrides = conf.get(CONF_OVERRIDE, []) + x10_devices = conf.get(CONF_X10, []) + x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) + x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) + x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) @callback def async_plm_new_device(device): @@ -106,7 +141,7 @@ def async_plm_new_device(device): hass.async_add_job( discovery.async_load_platform( hass, platform, DOMAIN, - discovered={'address': device.address.hex, + discovered={'address': device.address.id, 'state_key': state_key}, hass_config=config)) @@ -151,6 +186,21 @@ def print_im_aldb(service): # Furture direction is to create an INSTEON control panel. print_aldb_to_log(plm.aldb) + def x10_all_units_off(service): + """Send the X10 All Units Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_units_off(housecode) + + def x10_all_lights_off(service): + """Send the X10 All Lights Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_off(housecode) + + def x10_all_lights_on(service): + """Send the X10 All Lights On command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_on(housecode) + def _register_services(): hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA) @@ -162,6 +212,15 @@ def _register_services(): schema=PRINT_ALDB_SCHEMA) hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) + hass.services.register(DOMAIN, SRV_X10_ALL_UNITS_OFF, + x10_all_units_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_OFF, + x10_all_lights_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_ON, + x10_all_lights_on, + schema=X10_HOUSECODE_SCHEMA) _LOGGER.debug("Insteon_plm Services registered") _LOGGER.info("Looking for PLM on %s", port) @@ -192,6 +251,36 @@ def _register_services(): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + + if x10_all_units_off_housecode: + device = plm.add_x10_device(x10_all_units_off_housecode, + 20, + 'allunitsoff') + if x10_all_lights_on_housecode: + device = plm.add_x10_device(x10_all_lights_on_housecode, + 21, + 'alllightson') + if x10_all_lights_off_housecode: + device = plm.add_x10_device(x10_all_lights_off_housecode, + 22, + 'alllightsoff') + for device in x10_devices: + housecode = device.get(CONF_HOUSECODE) + unitcode = device.get(CONF_UNITCODE) + x10_type = 'onoff' + steps = device.get(CONF_DIM_STEPS, 22) + if device.get(CONF_PLATFORM) == 'light': + x10_type = 'dimmable' + elif device.get(CONF_PLATFORM) == 'binary_sensor': + x10_type = 'sensor' + _LOGGER.debug("Adding X10 device to insteonplm: %s %d %s", + housecode, unitcode, x10_type) + device = plm.add_x10_device(housecode, + unitcode, + x10_type) + if device and hasattr(device.states[0x01], 'steps'): + device.states[0x01].steps = steps + hass.async_add_job(_register_services) return True @@ -219,6 +308,13 @@ def __init__(self): IoLincSensor, LeakSensorDryWet) + from insteonplm.states.x10 import (X10DimmableSwitch, + X10OnOffSwitch, + X10OnOffSensor, + X10AllUnitsOffSensor, + X10AllLightsOnSensor, + X10AllLightsOffSensor) + self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), @@ -231,7 +327,14 @@ def __init__(self): State(VariableSensor, 'sensor'), State(DimmableSwitch_Fan, 'fan'), - State(DimmableSwitch, 'light')] + State(DimmableSwitch, 'light'), + + State(X10DimmableSwitch, 'light'), + State(X10OnOffSwitch, 'switch'), + State(X10OnOffSensor, 'binary_sensor'), + State(X10AllUnitsOffSensor, 'binary_sensor'), + State(X10AllLightsOnSensor, 'binary_sensor'), + State(X10AllLightsOffSensor, 'binary_sensor')] def __len__(self): """Return the number of INSTEON state types mapped to HA platforms.""" diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index 9ea53c10fbf1af..4d87d7881bf666 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -30,3 +30,21 @@ print_all_link_database: example: 'light.1a2b3c' print_im_all_link_database: description: Print the All-Link Database for the INSTEON Modem (IM). +x10_all_units_off: + description: Send X10 All Units Off command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_on: + description: Send X10 All Lights On command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_off: + description: Send X10 All Lights Off command + fields: + housecode: + description: X10 house code + example: c diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index be562e9d909d67..42b4829f64ecd0 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -30,7 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device.address.hex, device.states[state_key].name) new_entity = None - if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff']: + if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff', + 'x10OnOffSwitch']: new_entity = InsteonPLMSwitchDevice(device, state_key) elif state_name == 'openClosedRelay': new_entity = InsteonPLMOpenClosedDevice(device, state_key) diff --git a/requirements_all.txt b/requirements_all.txt index c0c85012526e14..35f45bbd5b0cd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.2 +insteonplm==0.10.0 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 617647c5fd1705848dd2bf425de9d7ace3a42103 Mon Sep 17 00:00:00 2001 From: Bob Clough Date: Thu, 21 Jun 2018 19:59:03 +0100 Subject: [PATCH 043/169] Fix MQTT Light with RGB and Brightness (#15053) * Fix MQTT Light with RGB and Brightness When an MQTT light is given an RGB and Brightness topic, the RGB is scaled by the brightness *as well* as the brightness being set This causes 255,0,0 at 50% brightness to be sent as 127,0,0 at 50% brightness, which ends up as 63,0,0 after the RGB bulb has applied its brightness scaling. Fixes the same issue in mqtt, mqtt-json and mqtt-template. Related Issue: #13725 * Add comment to mqtt_json as well --- homeassistant/components/light/mqtt.py | 11 +++++++++-- homeassistant/components/light/mqtt_json.py | 11 ++++++++--- homeassistant/components/light/mqtt_template.py | 11 +++++++++-- tests/components/light/test_mqtt.py | 14 +++++++------- tests/components/light/test_mqtt_json.py | 4 ++-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 97a4cc8c137ea4..c0e363f85d6d40 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ async def async_turn_on(self, **kwargs): self._topic[CONF_RGB_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 14f5ee7a9b9142..705e106fdff8be 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ async def async_turn_on(self, **kwargs): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: - brightness = kwargs.get( - ATTR_BRIGHTNESS, - self._brightness if self._brightness else 255) + # If there's a brightness topic set, we don't want to scale the + # RGB values given using the brightness. + if self._brightness is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) message['color']['r'] = rgb[0] diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index e32c13fc5b6eff..f6b3fbe8b70799 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ async def async_turn_on(self, **kwargs): if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) values['red'] = rgb[0] diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 8b51adb2187399..49bcd8a73ecc0c 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.mock_publish.reset_mock() light.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), + mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), + mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.323, 0.329), state.attributes['xy_color']) + self.assertEqual((0.611, 0.375), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -808,11 +808,11 @@ def test_on_command_brightness(self): # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '50,50,50', 0, False), + mock.call('test_light/rgb', '255,128,0', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 275fb42ede917c..af560bff9c3224 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ def test_sending_mqtt_commands_and_optimistic(self): \ self.assertEqual(50, message_json["brightness"]) self.assertEqual({ 'r': 0, - 'g': 50, - 'b': 4, + 'g': 255, + 'b': 21, }, message_json["color"]) self.assertEqual("ON", message_json["state"]) From 302717e8a1b6d6e0fdce24258303573a50c4195a Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 20 Jun 2018 18:46:15 -0700 Subject: [PATCH 044/169] Update Neato Library And Reduce Cloud Calls (#15072) * Update Neato library to 0.0.6 and reduce the amount of calls to the cloud * Remove file commited in error * Lint --- homeassistant/components/camera/neato.py | 2 +- homeassistant/components/neato.py | 6 +++--- homeassistant/components/switch/neato.py | 3 +++ homeassistant/components/vacuum/neato.py | 4 +++- requirements_all.txt | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 33bd00caa6bf68..689129e1067ff9 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -45,7 +45,7 @@ def camera_image(self): self.update() return self._image - @Throttle(timedelta(seconds=10)) + @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 7402bb18843ad2..c6a3dcf9c9a605 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,8 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.5.zip' - '#pybotvac==0.0.5'] +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' + '#pybotvac==0.0.6'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' @@ -122,7 +122,7 @@ def login(self): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=1)) + @Throttle(timedelta(seconds=60)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index a797abb47fcf9d..1d149383f6fad6 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -5,10 +5,12 @@ https://home-assistant.io/components/switch.neato/ """ import logging +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -50,6 +52,7 @@ def __init__(self, hass, robot, switch_type): self._schedule_state = None self._clean_state = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 9eba34cea321b3..128bece8494274 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/vacuum.neato/ """ import logging - +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON @@ -15,6 +15,7 @@ SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,6 +63,7 @@ def __init__(self, hass, robot): self.clean_suspension_charge_count = None self.clean_suspension_time = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") diff --git a/requirements_all.txt b/requirements_all.txt index 35f45bbd5b0cd1..fb365b26651889 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ httplib2==0.10.3 https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 +https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 # homeassistant.components.switch.anel_pwrctrl https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1 From a4b843eb2d053c92a1cdc9000f64b54e93b0e517 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 15:02:29 -0400 Subject: [PATCH 045/169] Version bump to 0.72.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 091bd907b93dc1..efed01d409e2fe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6781ecf159e35e91e8c2d12888a40da8386b4223 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:15:16 -0400 Subject: [PATCH 046/169] Bump frontend to 20180621.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9200f4d78f65e7..d8497f9c7900df 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 62a73303899a04..6246ea9913e177 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69435adc83fc71..938fd4976e8ee0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0df99f8762a03f51402ba4dd45b7d26d4a3ef15b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:15:16 -0400 Subject: [PATCH 047/169] Bump frontend to 20180621.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9200f4d78f65e7..d8497f9c7900df 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index fb365b26651889..7c03f3465c3636 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f09c4d7195d96..f6762e1faabf48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 35b609dd8b75f8b443b5ce17f1cec18162d8ab42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:27:01 -0400 Subject: [PATCH 048/169] Allow writing commit with version bump --- script/version_bump.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/script/version_bump.py b/script/version_bump.py index 59060a7075b024..eb61420a600839 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -2,6 +2,7 @@ """Helper script to bump the current version.""" import argparse import re +import subprocess from packaging.version import Version @@ -117,12 +118,20 @@ def main(): help="The type of the bump the version to.", choices=['beta', 'dev', 'patch', 'minor'], ) + parser.add_argument( + '--commit', action='store_true', + help='Create a version bump commit.') arguments = parser.parse_args() current = Version(const.__version__) bumped = bump_version(current, arguments.type) assert bumped > current, 'BUG! New version is not newer than old version' write_version(bumped) + if not arguments.commit: + return + + subprocess.run(['git', 'commit', '-am', f'Bumped version to {bumped}']) + def test_bump_version(): """Make sure it all works.""" From 6e5a2a77ab27ef605c81f6071edb237bcdfb7d67 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:27:08 -0400 Subject: [PATCH 049/169] Bumped version to 0.72.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index efed01d409e2fe..e9b72a70f1b137 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0b8' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 94eee6d0693538dea76d158c3f70aeddb9e8f4d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:38:44 -0400 Subject: [PATCH 050/169] Frontend bump to 20180621.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d8497f9c7900df..89353b56098b7f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.1'] +REQUIREMENTS = ['home-assistant-frontend==20180621.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6246ea9913e177..14c882f32afa2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 938fd4976e8ee0..807509833ff540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6456f66b476ba3bf3c333b0cd7c1b2599eb7ed46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:38:44 -0400 Subject: [PATCH 051/169] Frontend bump to 20180621.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d8497f9c7900df..89353b56098b7f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.1'] +REQUIREMENTS = ['home-assistant-frontend==20180621.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 7c03f3465c3636..83b8052f78f9fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6762e1faabf48..a3925262572400 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0ea2d99910b6defba4ce1ec53cf89cdd49efcf7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:39:02 -0400 Subject: [PATCH 052/169] Bumped version to 0.72.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e9b72a70f1b137..7feb5d8bdac2c1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b8' +PATCH_VERSION = '0b9' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 0c01f3a0fe551dc5a4a2158f601eecdc2a50a63b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:04 -0400 Subject: [PATCH 053/169] Update frontend to 20180622.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 89353b56098b7f..9c9fdd137e2e4c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.2'] +REQUIREMENTS = ['home-assistant-frontend==20180622.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 14c882f32afa2c..27ecc784f59c82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 807509833ff540..52e7e3e07b115e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 124495dd84f5c41284647308f115cad075098c7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:04 -0400 Subject: [PATCH 054/169] Update frontend to 20180622.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 89353b56098b7f..9c9fdd137e2e4c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.2'] +REQUIREMENTS = ['home-assistant-frontend==20180622.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 83b8052f78f9fb..54f3a89e089221 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3925262572400..c8194a8382f4a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 7325847fa951b729a5145e3d0762322d0d035273 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:45 -0400 Subject: [PATCH 055/169] Bumped version to 0.72.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7feb5d8bdac2c1..a22605c37f49a4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0b9' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a02d7989d5dc6c69ed80ab2282797be9f8a3acc5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 11:07:26 -0400 Subject: [PATCH 056/169] Use older syntax for version bump --- script/version_bump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/version_bump.py b/script/version_bump.py index eb61420a600839..e324b231d0667d 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -130,7 +130,8 @@ def main(): if not arguments.commit: return - subprocess.run(['git', 'commit', '-am', f'Bumped version to {bumped}']) + subprocess.run([ + 'git', 'commit', '-am', 'Bumped version to {}'.format(bumped)]) def test_bump_version(): From c419cbb46f2af51bb83043ada4e9b25b64d95942 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 12:46:45 -0400 Subject: [PATCH 057/169] Bump frontend to 20180622.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9c9fdd137e2e4c..3d2231ab43b440 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.0'] +REQUIREMENTS = ['home-assistant-frontend==20180622.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 27ecc784f59c82..008ed05143f83d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52e7e3e07b115e..45f47bbe5141fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 66110a7d57ffa52a303980595a34a4199f4a2b66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 12:46:45 -0400 Subject: [PATCH 058/169] Bump frontend to 20180622.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9c9fdd137e2e4c..3d2231ab43b440 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.0'] +REQUIREMENTS = ['home-assistant-frontend==20180622.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 54f3a89e089221..52a5e0525604de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8194a8382f4a7..a38c7f259b4782 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 91962e2681dde1b23612df06633b16aa0867c950 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jun 2018 13:22:48 -0600 Subject: [PATCH 059/169] Fix socket bug with Yi in 0.72 (#15109) * Fixes BrokenPipeError exceptions with Yi (#15108) * Make sure to close the socket --- homeassistant/components/camera/yi.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 868c5afb4473c5..93f526c2b9627c 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -53,7 +53,6 @@ def __init__(self, hass, config): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] @@ -64,8 +63,6 @@ def __init__(self, hass, config): self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - hass.async_add_job(self._connect_to_client) - @property def brand(self): """Camera brand.""" @@ -76,38 +73,35 @@ def name(self): """Return the name of this camera.""" return self._name - async def _connect_to_client(self): - """Attempt to establish a connection via FTP.""" + async def _get_latest_video_url(self): + """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - self._ftp = ftp except StatusCodeError as err: raise PlatformNotReady(err) - async def _get_latest_video_url(self): - """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import StatusCodeError - try: - await self._ftp.change_directory(self.path) + await ftp.change_directory(self.path) dirs = [] - for path, attrs in await self._ftp.list(): + for path, attrs in await ftp.list(): if attrs['type'] == 'dir' and '.' not in str(path): dirs.append(path) latest_dir = dirs[-1] - await self._ftp.change_directory(latest_dir) + await ftp.change_directory(latest_dir) videos = [] - for path, _ in await self._ftp.list(): + for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None + await ftp.quit() + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) From 96d5684a89f1b6fbf313de3da5f1e50107bb2d53 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 24 Jun 2018 12:06:25 +0300 Subject: [PATCH 060/169] Switch to pypi version of pybotvac (#15115) --- homeassistant/components/neato.py | 3 +-- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index c6a3dcf9c9a605..6d14a6f3c4db17 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' - '#pybotvac==0.0.6'] +REQUIREMENTS = ['pybotvac==0.0.7'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' diff --git a/requirements_all.txt b/requirements_all.txt index 008ed05143f83d..6dd581fc299f09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -429,9 +429,6 @@ http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b89974819 # homeassistant.components.remember_the_milk httplib2==0.10.3 -# homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 - # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 @@ -763,6 +760,9 @@ pyblackbird==0.5 # homeassistant.components.device_tracker.bluetooth_tracker # pybluez==0.22 +# homeassistant.components.neato +pybotvac==0.0.7 + # homeassistant.components.media_player.channels pychannels==1.0.0 From 9de7034d0e4e4f6cb6a2e8f20c9d0577aed319f6 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sun, 24 Jun 2018 07:36:27 -0400 Subject: [PATCH 061/169] Added attribute attribution to Digital Ocean component (#15114) --- homeassistant/components/binary_sensor/digital_ocean.py | 4 +++- homeassistant/components/digital_ocean.py | 1 + homeassistant/components/switch/digital_ocean.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/digital_ocean.py b/homeassistant/components/binary_sensor/digital_ocean.py index 140c84358c79d3..1eb86d4eb82bad 100644 --- a/homeassistant/components/binary_sensor/digital_ocean.py +++ b/homeassistant/components/binary_sensor/digital_ocean.py @@ -14,7 +14,8 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -75,6 +76,7 @@ def device_class(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index bd03fb019759f9..a0f50842649446 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -27,6 +27,7 @@ ATTR_REGION = 'region' ATTR_VCPUS = 'vcpus' +CONF_ATTRIBUTION = 'Data provided by Digital Ocean' CONF_DROPLETS = 'droplets' DATA_DIGITAL_OCEAN = 'data_do' diff --git a/homeassistant/components/switch/digital_ocean.py b/homeassistant/components/switch/digital_ocean.py index 081eea80e2dcbd..12a6aabb170079 100644 --- a/homeassistant/components/switch/digital_ocean.py +++ b/homeassistant/components/switch/digital_ocean.py @@ -13,7 +13,8 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -69,6 +70,7 @@ def is_on(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, From 6064932e2e5f4351fe091cd1140c4ca0aeb746ad Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jun 2018 11:04:31 -0600 Subject: [PATCH 062/169] Make Pollen.com platform async (#14963) * Most of the work in place * Final touches * Small style updates * Owner-requested changes * Member-requested changes --- homeassistant/components/sensor/pollen.py | 447 ++++++++++------------ requirements_all.txt | 2 +- 2 files changed, 200 insertions(+), 249 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 1ef5a27cf3dffc..838358fcfca8f0 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -13,17 +13,17 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS -) + ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify +from homeassistant.util import Throttle -REQUIREMENTS = ['pypollencom==1.1.2'] +REQUIREMENTS = ['pypollencom==2.1.0'] _LOGGER = logging.getLogger(__name__) -ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' -ATTR_ALLERGEN_NAME = 'primary_allergen_name' -ATTR_ALLERGEN_TYPE = 'primary_allergen_type' +ATTR_ALLERGEN_GENUS = 'allergen_genus' +ATTR_ALLERGEN_NAME = 'allergen_name' +ATTR_ALLERGEN_TYPE = 'allergen_type' ATTR_CITY = 'city' ATTR_OUTLOOK = 'outlook' ATTR_RATING = 'rating' @@ -34,53 +34,30 @@ CONF_ZIP_CODE = 'zip_code' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' - -MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12) -MIN_TIME_UPDATE_INDICES = timedelta(minutes=10) - -CONDITIONS = { - 'allergy_average_forecasted': ( - 'Allergy Index: Forecasted Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'extended_data'}, - 'mdi:flower' - ), - 'allergy_average_historical': ( - 'Allergy Index: Historical Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'historic_data'}, - 'mdi:flower' - ), - 'allergy_index_today': ( - 'Allergy Index: Today', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Today'}, - 'mdi:flower' - ), - 'allergy_index_tomorrow': ( - 'Allergy Index: Tomorrow', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Tomorrow'}, - 'mdi:flower' - ), - 'allergy_index_yesterday': ( - 'Allergy Index: Yesterday', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Yesterday'}, - 'mdi:flower' - ), - 'disease_average_forecasted': ( - 'Cold & Flu: Forecasted Average', - 'AllergyAverageSensor', - 'disease_average_data', - {'data_attr': 'extended_data'}, - 'mdi:snowflake' - ) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' +TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' +TYPE_ALLERGY_INDEX = 'allergy_index' +TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' +TYPE_ALLERGY_TODAY = 'allergy_index_today' +TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' +TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' +TYPE_DISEASE_FORECAST = 'disease_average_forecasted' + +SENSORS = { + TYPE_ALLERGY_FORECAST: ( + 'Allergy Index: Forecasted Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_HISTORIC: ( + 'Allergy Index: Historical Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_TODAY: ( + 'Allergy Index: Today', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_TOMORROW: ( + 'Allergy Index: Tomorrow', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_YESTERDAY: ( + 'Allergy Index: Yesterday', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_DISEASE_FORECAST: ( + 'Cold & Flu: Forecasted Average', None, 'mdi:snowflake', 'index') } RATING_MAPPING = [{ @@ -105,69 +82,69 @@ 'maximum': 12 }] +TREND_FLAT = 'Flat' +TREND_INCREASING = 'Increasing' +TREND_SUBSIDING = 'Subsiding' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ZIP_CODE): str, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Configure the platform and add the sensors.""" from pypollencom import Client - _LOGGER.debug('Configuration data: %s', config) + websession = aiohttp_client.async_get_clientsession(hass) - client = Client(config[CONF_ZIP_CODE]) - datas = { - 'allergy_average_data': AllergyAveragesData(client), - 'allergy_index_data': AllergyIndexData(client), - 'disease_average_data': DiseaseData(client) - } - classes = { - 'AllergyAverageSensor': AllergyAverageSensor, - 'AllergyIndexSensor': AllergyIndexSensor - } + data = PollenComData( + Client(config[CONF_ZIP_CODE], websession), + config[CONF_MONITORED_CONDITIONS]) - for data in datas.values(): - data.update() + await data.async_update() sensors = [] - for condition in config[CONF_MONITORED_CONDITIONS]: - name, sensor_class, data_key, params, icon = CONDITIONS[condition] - sensors.append(classes[sensor_class]( - datas[data_key], - params, - name, - icon, - config[CONF_ZIP_CODE] - )) + for kind in config[CONF_MONITORED_CONDITIONS]: + name, category, icon, unit = SENSORS[kind] + sensors.append( + PollencomSensor( + data, config[CONF_ZIP_CODE], kind, category, name, icon, unit)) - add_devices(sensors, True) + async_add_devices(sensors, True) -def calculate_trend(list_of_nums): - """Calculate the most common rating as a trend.""" +def calculate_average_rating(indices): + """Calculate the human-friendly historical allergy average.""" ratings = list( - r['label'] for n in list_of_nums - for r in RATING_MAPPING + r['label'] for n in indices for r in RATING_MAPPING if r['minimum'] <= n <= r['maximum']) return max(set(ratings), key=ratings.count) -class BaseSensor(Entity): - """Define a base class for all of our sensors.""" +class PollencomSensor(Entity): + """Define a Pollen.com sensor.""" - def __init__(self, data, data_params, name, icon, unique_id): + def __init__(self, pollencom, zip_code, kind, category, name, icon, unit): """Initialize the sensor.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._category = category self._icon = icon self._name = name - self._data_params = data_params self._state = None - self._unit = None - self._unique_id = unique_id - self.data = data + self._type = kind + self._unit = unit + self._zip_code = zip_code + self.pollencom = pollencom + + @property + def available(self): + """Return True if entity is available.""" + return bool( + self.pollencom.data.get(self._type) + or self.pollencom.data.get(self._category)) @property def device_state_attributes(self): @@ -192,187 +169,161 @@ def state(self): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}'.format(self._unique_id, slugify(self._name)) + return '{0}_{1}'.format(self._zip_code, self._type) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit - -class AllergyAverageSensor(BaseSensor): - """Define a sensor to show allergy average information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - try: - data_attr = getattr(self.data, self._data_params['data_attr']) - indices = [p['Index'] for p in data_attr['Location']['periods']] - self._attrs[ATTR_TREND] = calculate_trend(indices) - except KeyError: - _LOGGER.error("Pollen.com API didn't return any data") + async def async_update(self): + """Update the sensor.""" + await self.pollencom.async_update() + if not self.pollencom.data: return - try: - self._attrs[ATTR_CITY] = data_attr['Location']['City'].title() - self._attrs[ATTR_STATE] = data_attr['Location']['State'] - self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None + if self._category: + data = self.pollencom.data[self._category]['Location'] + else: + data = self.pollencom.data[self._type]['Location'] + indices = [p['Index'] for p in data['periods']] average = round(mean(indices), 1) [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= average <= i['maximum'] ] - self._attrs[ATTR_RATING] = rating - - self._state = average - self._unit = 'index' - - -class AllergyIndexSensor(BaseSensor): - """Define a sensor to show allergy index information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - try: - location_data = self.data.current_data['Location'] - [period] = [ - p for p in location_data['periods'] - if p['Type'] == self._data_params['key'] - ] + slope = (data['periods'][-1]['Index'] - data['periods'][-2]['Index']) + trend = TREND_FLAT + if slope > 0: + trend = TREND_INCREASING + elif slope < 0: + trend = TREND_SUBSIDING + + if self._type == TYPE_ALLERGY_FORECAST: + outlook = self.pollencom.data[TYPE_ALLERGY_OUTLOOK] + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_OUTLOOK: outlook['Outlook'], + ATTR_RATING: rating, + ATTR_SEASON: outlook['Season'].title(), + ATTR_STATE: data['State'], + ATTR_TREND: outlook['Trend'].title(), + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type == TYPE_ALLERGY_HISTORIC: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: calculate_average_rating(indices), + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + key = self._type.split('_')[-1].title() + [period] = [p for p in data['periods'] if p['Type'] == key] [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - for i in range(3): - index = i + 1 - try: - data = period['Triggers'][i] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_GENUS, index)] = data['Genus'] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_NAME, index)] = data['Name'] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_TYPE, index)] = data['PlantType'] - except IndexError: - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_GENUS, index)] = None - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_NAME, index)] = None - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_TYPE, index)] = None - - self._attrs[ATTR_RATING] = rating - - except KeyError: - _LOGGER.error("Pollen.com API didn't return any data") - return - - try: - self._attrs[ATTR_CITY] = location_data['City'].title() - self._attrs[ATTR_STATE] = location_data['State'] - self._attrs[ATTR_ZIP_CODE] = location_data['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None - - try: - self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook'] - except KeyError: - _LOGGER.debug('Outlook data not included in API response') - self._attrs[ATTR_OUTLOOK] = None - - try: - self._attrs[ATTR_SEASON] = self.data.outlook_data['Season'] - except KeyError: - _LOGGER.debug('Season data not included in API response') - self._attrs[ATTR_SEASON] = None - - try: - self._attrs[ATTR_TREND] = self.data.outlook_data['Trend'].title() - except KeyError: - _LOGGER.debug('Trend data not included in API response') - self._attrs[ATTR_TREND] = None - - self._state = period['Index'] - self._unit = 'index' - - -class DataBase(object): - """Define a generic data object.""" - - def __init__(self, client): + for idx, attrs in enumerate(period['Triggers']): + index = idx + 1 + self._attrs.update({ + '{0}_{1}'.format(ATTR_ALLERGEN_GENUS, index): + attrs['Genus'], + '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): + attrs['Name'], + '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): + attrs['PlantType'], + }) + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = period['Index'] + elif self._type == TYPE_DISEASE_FORECAST: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + + +class PollenComData(object): + """Define a data object to retrieve info from Pollen.com.""" + + def __init__(self, client, sensor_types): """Initialize.""" self._client = client + self._sensor_types = sensor_types + self.data = {} - def _get_client_data(self, module, operation): - """Get data from a particular point in the API.""" - from pypollencom.exceptions import HTTPError - - data = {} - try: - data = getattr(getattr(self._client, module), operation)() - _LOGGER.debug('Received "%s_%s" data: %s', module, operation, data) - except HTTPError as exc: - _LOGGER.error('An error occurred while retrieving data') - _LOGGER.debug(exc) - - return data - - -class AllergyAveragesData(DataBase): - """Define an object to averages on future and historical allergy data.""" - - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None - self.historic_data = None - - @Throttle(MIN_TIME_UPDATE_AVERAGES) - def update(self): - """Update with new data.""" - self.extended_data = self._get_client_data('allergens', 'extended') - self.historic_data = self._get_client_data('allergens', 'historic') - - -class AllergyIndexData(DataBase): - """Define an object to retrieve current allergy index info.""" + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Pollen.com data.""" + from pypollencom.errors import InvalidZipError, PollenComError - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.current_data = None - self.outlook_data = None - - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new index data.""" - self.current_data = self._get_client_data('allergens', 'current') - self.outlook_data = self._get_client_data('allergens', 'outlook') + # Pollen.com requires a bit more complicated error handling, given that + # it sometimes has parts (but not the whole thing) go down: + # + # 1. If `InvalidZipError` is thrown, quit everything immediately. + # 2. If an individual request throws any other error, try the others. + try: + if TYPE_ALLERGY_FORECAST in self._sensor_types: + try: + data = await self._client.allergens.extended() + self.data[TYPE_ALLERGY_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy forecast: %s', err) + self.data[TYPE_ALLERGY_FORECAST] = {} -class DiseaseData(DataBase): - """Define an object to retrieve current disease index info.""" + try: + data = await self._client.allergens.outlook() + self.data[TYPE_ALLERGY_OUTLOOK] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy outlook: %s', err) + self.data[TYPE_ALLERGY_OUTLOOK] = {} - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None + if TYPE_ALLERGY_HISTORIC in self._sensor_types: + try: + data = await self._client.allergens.historic() + self.data[TYPE_ALLERGY_HISTORIC] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy history: %s', err) + self.data[TYPE_ALLERGY_HISTORIC] = {} + + if all(s in self._sensor_types + for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY]): + try: + data = await self._client.allergens.current() + self.data[TYPE_ALLERGY_INDEX] = data + except PollenComError as err: + _LOGGER.error('Unable to get current allergies: %s', err) + self.data[TYPE_ALLERGY_TODAY] = {} - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new cold/flu data.""" - self.extended_data = self._get_client_data('disease', 'extended') + if TYPE_DISEASE_FORECAST in self._sensor_types: + try: + data = await self._client.disease.extended() + self.data[TYPE_DISEASE_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get disease forecast: %s', err) + self.data[TYPE_DISEASE_FORECAST] = {} + + _LOGGER.debug('New data retrieved: %s', self.data) + except InvalidZipError: + _LOGGER.error( + 'Cannot retrieve data for ZIP code: %s', self._client.zip_code) + self.data = {} diff --git a/requirements_all.txt b/requirements_all.txt index 6dd581fc299f09..6117119b9498f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -954,7 +954,7 @@ pyotp==2.2.6 pyowm==2.8.0 # homeassistant.components.sensor.pollen -pypollencom==1.1.2 +pypollencom==2.1.0 # homeassistant.components.qwikswitch pyqwikswitch==0.8 From 5a71a22fb90f5320630ba2bed14a0a10f8cbe1c0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 24 Jun 2018 23:48:59 +0200 Subject: [PATCH 063/169] deCONZ small improvements (#15128) * Make sure that bridge id is available for config entry * Fix so deconz reports proper color values * Bump dependency to v39 --- homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/deconz/config_flow.py | 9 +++------ homeassistant/components/light/deconz.py | 8 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 850645225d0018..4fa89f8cfd3b52 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==38'] +REQUIREMENTS = ['pydeconz==39'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 27fb6987f8c24c..b67d32508be9e9 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -163,9 +163,6 @@ async def async_step_import(self, import_config): if CONF_API_KEY not in import_config: return await self.async_step_link() - self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True - self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True - return self.async_create_entry( - title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], - data=self.deconz_config - ) + user_input = {CONF_ALLOW_CLIP_SENSOR: True, + CONF_ALLOW_DECONZ_GROUPS: True} + return await self.async_step_options(user_input=user_input) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a4593a72617bbf..05907ea86ee275 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -101,9 +101,11 @@ def color_temp(self): return self._light.ct @property - def xy_color(self): - """Return the XY color value.""" - return self._light.xy + def hs_color(self): + """Return the hs color value.""" + if self._light.colormode in ('xy', 'hs') and self._light.xy: + return color_util.color_xy_to_hs(*self._light.xy) + return None @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6117119b9498f2..777309442f32dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==38 +pydeconz==39 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f47bbe5141fb..6813378b12f0dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==38 +pydeconz==39 # homeassistant.components.zwave pydispatcher==2.0.5 From 021d08a9c46bf3ff32f4c6fbc632757006611a33 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jun 2018 19:09:08 -0600 Subject: [PATCH 064/169] Make sure Yi utilizes existing event loop (#15131) --- homeassistant/components/camera/yi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 93f526c2b9627c..b575a705f98bdf 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -77,7 +77,7 @@ async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError - ftp = Client() + ftp = Client(loop=self.hass.loop) try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) From 05924a286812ff7d7ab111e432412b95dbc3ad19 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 25 Jun 2018 06:46:55 -0500 Subject: [PATCH 065/169] Bump insteonplm version to 0.11.2 (#15133) * Bump insteonplm version to 0.11.2 * Gratuitous commit to force travis again. * Reverse change made to force Travis CI --- homeassistant/components/insteon_plm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index b2f7c8b66551bd..8197b45c28d963 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.10.0'] +REQUIREMENTS = ['insteonplm==0.11.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 777309442f32dd..74ff928680392d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.10.0 +insteonplm==0.11.2 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 3893d8a87612ed807f77d52db1d863eb7cefad83 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jun 2018 13:58:16 +0200 Subject: [PATCH 066/169] Reorganize mysensors (#15123) * Move mysensors.py to package * Move mysensors component to package * Split code into multiple modules. * Update coveragerc --- .coveragerc | 2 +- .../components/binary_sensor/mysensors.py | 3 +- homeassistant/components/climate/mysensors.py | 2 +- homeassistant/components/cover/mysensors.py | 2 +- .../components/device_tracker/mysensors.py | 4 +- homeassistant/components/light/mysensors.py | 2 +- homeassistant/components/mysensors.py | 705 ------------------ .../components/mysensors/__init__.py | 167 +++++ homeassistant/components/mysensors/const.py | 138 ++++ homeassistant/components/mysensors/device.py | 109 +++ homeassistant/components/mysensors/gateway.py | 328 ++++++++ homeassistant/components/notify/mysensors.py | 2 +- homeassistant/components/sensor/mysensors.py | 2 +- homeassistant/components/switch/mysensors.py | 2 +- 14 files changed, 753 insertions(+), 715 deletions(-) delete mode 100644 homeassistant/components/mysensors.py create mode 100644 homeassistant/components/mysensors/__init__.py create mode 100644 homeassistant/components/mysensors/const.py create mode 100644 homeassistant/components/mysensors/device.py create mode 100644 homeassistant/components/mysensors/gateway.py diff --git a/.coveragerc b/.coveragerc index d059d62b5f31a9..90b0a7f475d8b5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -192,7 +192,7 @@ omit = homeassistant/components/mychevy.py homeassistant/components/*/mychevy.py - homeassistant/components/mysensors.py + homeassistant/components/mysensors/* homeassistant/components/*/mysensors.py homeassistant/components/neato.py diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 214430211932e5..abb19129d5205d 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -29,7 +29,8 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): +class MySensorsBinarySensor( + mysensors.device.MySensorsEntity, BinarySensorDevice): """Representation of a MySensors Binary Sensor child node.""" @property diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 9fab56c61ac56c..37ae29fdf81258 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -39,7 +39,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): +class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): """Representation of a MySensors HVAC.""" @property diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 3f8eb054710377..c815cf44df2d02 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -17,7 +17,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): +class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index b0d29bf0566757..49d3f3207ba5ca 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -23,13 +23,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): id(device.gateway), device.node_id, device.child_id, device.value_type) async_dispatcher_connect( - hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), + hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id), device.async_update_callback) return True -class MySensorsDeviceScanner(mysensors.MySensorsDevice): +class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" def __init__(self, async_see, *args): diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 55387288d7f233..4139abd40fa287 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -28,7 +28,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsLight(mysensors.MySensorsEntity, Light): +class MySensorsLight(mysensors.device.MySensorsEntity, Light): """Representation of a MySensors Light child node.""" def __init__(self, *args): diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py deleted file mode 100644 index 1e7e252bd9db28..00000000000000 --- a/homeassistant/components/mysensors.py +++ /dev/null @@ -1,705 +0,0 @@ -""" -Connect to a MySensors gateway via pymysensors API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mysensors/ -""" -import asyncio -from collections import defaultdict -import logging -import os -import socket -import sys -from timeit import default_timer as timer - -import async_timeout -import voluptuous as vol - -from homeassistant.components.mqtt import ( - valid_publish_topic, valid_subscribe_topic) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, STATE_ON) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.setup import async_setup_component - -REQUIREMENTS = ['pymysensors==0.14.0'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CHILD_ID = 'child_id' -ATTR_DESCRIPTION = 'description' -ATTR_DEVICE = 'device' -ATTR_DEVICES = 'devices' -ATTR_NODE_ID = 'node_id' - -CONF_BAUD_RATE = 'baud_rate' -CONF_DEBUG = 'debug' -CONF_DEVICE = 'device' -CONF_GATEWAYS = 'gateways' -CONF_PERSISTENCE = 'persistence' -CONF_PERSISTENCE_FILE = 'persistence_file' -CONF_RETAIN = 'retain' -CONF_TCP_PORT = 'tcp_port' -CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' -CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' -CONF_VERSION = 'version' - -CONF_NODES = 'nodes' -CONF_NODE_NAME = 'name' - -DEFAULT_BAUD_RATE = 115200 -DEFAULT_TCP_PORT = 5003 -DEFAULT_VERSION = '1.4' -DOMAIN = 'mysensors' - -GATEWAY_READY_TIMEOUT = 15.0 -MQTT_COMPONENT = 'mqtt' -MYSENSORS_GATEWAYS = 'mysensors_gateways' -MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' -MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' -PLATFORM = 'platform' -SCHEMA = 'schema' -SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' -TYPE = 'type' - - -def is_socket_address(value): - """Validate that value is a valid address.""" - try: - socket.getaddrinfo(value, None) - return value - except OSError: - raise vol.Invalid('Device is not a valid domain name or ip address') - - -def has_parent_dir(value): - """Validate that value is in an existing directory which is writeable.""" - parent = os.path.dirname(os.path.realpath(value)) - is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK) - if not is_dir_writable: - raise vol.Invalid( - '{} directory does not exist or is not writeable'.format(parent)) - return value - - -def has_all_unique_files(value): - """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [ - gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files): - raise vol.Invalid( - 'persistence file name of all devices must be set if any is set') - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) - return value - - -def is_persistence_file(value): - """Validate that persistence file path ends in either .pickle or .json.""" - if value.endswith(('.json', '.pickle')): - return value - else: - raise vol.Invalid( - '{} does not end in either `.json` or `.pickle`'.format(value)) - - -def is_serial_port(value): - """Validate that value is a windows serial port or a unix device.""" - if sys.platform.startswith('win'): - ports = ('COM{}'.format(idx + 1) for idx in range(256)) - if value in ports: - return value - else: - raise vol.Invalid('{} is not a serial port'.format(value)) - else: - return cv.isdevice(value) - - -def deprecated(key): - """Mark key as deprecated in configuration.""" - def validator(config): - """Check if key is in config, log warning and remove key.""" - if key not in config: - return config - _LOGGER.warning( - '%s option for %s is deprecated. Please remove %s from your ' - 'configuration file', key, DOMAIN, key) - config.pop(key) - return config - return validator - - -NODE_SCHEMA = vol.Schema({ - cv.positive_int: { - vol.Required(CONF_NODE_NAME): cv.string - } -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, - [{ - vol.Required(CONF_DEVICE): - vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port), - vol.Optional(CONF_PERSISTENCE_FILE): - vol.All(cv.string, is_persistence_file, has_parent_dir), - vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): - cv.positive_int, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, - vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, - }] - ), - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - })) -}, extra=vol.ALLOW_EXTRA) - - -# MySensors const schemas -BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} -CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} -LIGHT_DIMMER_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_DIMMER', - SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} -LIGHT_PERCENTAGE_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_PERCENTAGE', - SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGB_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { - 'V_RGB': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGBW_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { - 'V_RGBW': cv.string, 'V_STATUS': cv.string}} -NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} -DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} -DUST_SCHEMA = [ - {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] -SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} -SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} -MYSENSORS_CONST_SCHEMA = { - 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SPRINKLER': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_WATER_LEAK': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SOUND': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_VIBRATION': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOISTURE': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_HVAC': [CLIMATE_SCHEMA], - 'S_COVER': [ - {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, - {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, - {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, - {PLATFORM: 'cover', TYPE: 'V_STATUS'}], - 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], - 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], - 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], - 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], - 'S_GPS': [ - DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], - 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], - 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], - 'S_BARO': [ - {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, - {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], - 'S_WIND': [ - {PLATFORM: 'sensor', TYPE: 'V_WIND'}, - {PLATFORM: 'sensor', TYPE: 'V_GUST'}, - {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], - 'S_RAIN': [ - {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, - {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], - 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], - 'S_WEIGHT': [ - {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_POWER': [ - {PLATFORM: 'sensor', TYPE: 'V_WATT'}, - {PLATFORM: 'sensor', TYPE: 'V_KWH'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR'}, - {PLATFORM: 'sensor', TYPE: 'V_VA'}, - {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], - 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], - 'S_LIGHT_LEVEL': [ - {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], - 'S_IR': [ - {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, - {PLATFORM: 'switch', TYPE: 'V_IR_SEND', - SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], - 'S_WATER': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_CUSTOM': [ - {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, - {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], - 'S_SCENE_CONTROLLER': [ - {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, - {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], - 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], - 'S_MULTIMETER': [ - {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, - {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_GAS': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_WATER_QUALITY': [ - {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, - {PLATFORM: 'sensor', TYPE: 'V_PH'}, - {PLATFORM: 'sensor', TYPE: 'V_ORP'}, - {PLATFORM: 'sensor', TYPE: 'V_EC'}, - {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_AIR_QUALITY': DUST_SCHEMA, - 'S_DUST': DUST_SCHEMA, - 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], - 'S_BINARY': [SWITCH_STATUS_SCHEMA], - 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], -} - - -async def async_setup(hass, config): - """Set up the MySensors component.""" - import mysensors.mysensors as mysensors - - version = config[DOMAIN].get(CONF_VERSION) - persistence = config[DOMAIN].get(CONF_PERSISTENCE) - - async def setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): - """Return gateway after setup of the gateway.""" - if device == MQTT_COMPONENT: - if not await async_setup_component(hass, MQTT_COMPONENT, config): - return None - mqtt = hass.components.mqtt - retain = config[DOMAIN].get(CONF_RETAIN) - - def pub_callback(topic, payload, qos, retain): - """Call MQTT publish function.""" - mqtt.async_publish(topic, payload, qos, retain) - - def sub_callback(topic, sub_cb, qos): - """Call MQTT subscribe function.""" - @callback - def internal_callback(*args): - """Call callback.""" - sub_cb(*args) - - hass.async_add_job( - mqtt.async_subscribe(topic, internal_callback, qos)) - - gateway = mysensors.AsyncMQTTGateway( - pub_callback, sub_callback, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - else: - try: - await hass.async_add_job(is_serial_port, device) - gateway = mysensors.AsyncSerialGateway( - device, baud=baud_rate, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - except vol.Invalid: - gateway = mysensors.AsyncTCPGateway( - device, port=tcp_port, loop=hass.loop, event_callback=None, - persistence=persistence, persistence_file=persistence_file, - protocol_version=version) - gateway.metric = hass.config.units.is_metric - gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) - gateway.device = device - gateway.event_callback = gw_callback_factory(hass) - if persistence: - await gateway.start_persistence() - - return gateway - - # Setup all devices from config - gateways = {} - conf_gateways = config[DOMAIN][CONF_GATEWAYS] - - for index, gway in enumerate(conf_gateways): - device = gway[CONF_DEVICE] - persistence_file = gway.get( - CONF_PERSISTENCE_FILE, - hass.config.path('mysensors{}.pickle'.format(index + 1))) - baud_rate = gway.get(CONF_BAUD_RATE) - tcp_port = gway.get(CONF_TCP_PORT) - in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') - out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - gateway = await setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix) - if gateway is not None: - gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(gateway)] = gateway - - if not gateways: - _LOGGER.error( - "No devices could be setup as gateways, check your configuration") - return False - - hass.data[MYSENSORS_GATEWAYS] = gateways - - hass.async_add_job(finish_setup(hass, gateways)) - - return True - - -async def finish_setup(hass, gateways): - """Load any persistent devices and platforms and start gateway.""" - discover_tasks = [] - start_tasks = [] - for gateway in gateways.values(): - discover_tasks.append(discover_persistent_devices(hass, gateway)) - start_tasks.append(gw_start(hass, gateway)) - if discover_tasks: - # Make sure all devices and platforms are loaded before gateway start. - await asyncio.wait(discover_tasks, loop=hass.loop) - if start_tasks: - await asyncio.wait(start_tasks, loop=hass.loop) - - -async def gw_start(hass, gateway): - """Start the gateway.""" - @callback - def gw_stop(event): - """Trigger to stop the gateway.""" - hass.async_add_job(gateway.stop()) - - await gateway.start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) - if gateway.device == 'mqtt': - # Gatways connected via mqtt doesn't send gateway ready message. - return - gateway_ready = asyncio.Future() - gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) - hass.data[gateway_ready_key] = gateway_ready - - try: - with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): - await gateway_ready - except asyncio.TimeoutError: - _LOGGER.warning( - "Gateway %s not ready after %s secs so continuing with setup", - gateway.device, GATEWAY_READY_TIMEOUT) - finally: - hass.data.pop(gateway_ready_key, None) - - -@callback -def set_gateway_ready(hass, msg): - """Set asyncio future result if gateway is ready.""" - if (msg.type != msg.gateway.const.MessageType.internal or - msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): - return - gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( - id(msg.gateway))) - if gateway_ready is None or gateway_ready.cancelled(): - return - gateway_ready.set_result(True) - - -def validate_child(gateway, node_id, child): - """Validate that a child has the correct values according to schema. - - Return a dict of platform with a list of device ids for validated devices. - """ - validated = defaultdict(list) - - if not child.values: - _LOGGER.debug( - "No child values for node %s child %s", node_id, child.id) - return validated - if gateway.sensors[node_id].sketch_name is None: - _LOGGER.debug("Node %s is missing sketch name", node_id) - return validated - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - s_name = next( - (member.name for member in pres if member.value == child.type), None) - if s_name not in MYSENSORS_CONST_SCHEMA: - _LOGGER.warning("Child type %s is not supported", s_name) - return validated - child_schemas = MYSENSORS_CONST_SCHEMA[s_name] - - def msg(name): - """Return a message for an invalid schema.""" - return "{} requires value_type {}".format( - pres(child.type).name, set_req[name].name) - - for schema in child_schemas: - platform = schema[PLATFORM] - v_name = schema[TYPE] - value_type = next( - (member.value for member in set_req if member.name == v_name), - None) - if value_type is None: - continue - _child_schema = child.get_schema(gateway.protocol_version) - vol_schema = _child_schema.extend( - {vol.Required(set_req[key].value, msg=msg(key)): - _child_schema.schema.get(set_req[key].value, val) - for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, - extra=vol.ALLOW_EXTRA) - try: - vol_schema(child.values) - except vol.Invalid as exc: - level = (logging.WARNING if value_type in child.values - else logging.DEBUG) - _LOGGER.log( - level, - "Invalid values: %s: %s platform: node %s child %s: %s", - child.values, platform, node_id, child.id, exc) - continue - dev_id = id(gateway), node_id, child.id, value_type - validated[platform].append(dev_id) - return validated - - -@callback -def discover_mysensors_platform(hass, platform, new_devices): - """Discover a MySensors platform.""" - task = hass.async_add_job(discovery.async_load_platform( - hass, platform, DOMAIN, - {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) - return task - - -async def discover_persistent_devices(hass, gateway): - """Discover platforms for devices loaded via persistence file.""" - tasks = [] - new_devices = defaultdict(list) - for node_id in gateway.sensors: - node = gateway.sensors[node_id] - for child in node.children.values(): - validated = validate_child(gateway, node_id, child) - for platform, dev_ids in validated.items(): - new_devices[platform].extend(dev_ids) - for platform, dev_ids in new_devices.items(): - tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - -def get_mysensors_devices(hass, domain): - """Return MySensors devices for a platform.""" - if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: - hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} - return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] - - -def gw_callback_factory(hass): - """Return a new callback for the gateway.""" - @callback - def mysensors_callback(msg): - """Handle messages from a MySensors gateway.""" - start = timer() - _LOGGER.debug( - "Node update: node %s child %s", msg.node_id, msg.child_id) - - set_gateway_ready(hass, msg) - - try: - child = msg.gateway.sensors[msg.node_id].children[msg.child_id] - except KeyError: - _LOGGER.debug("Not a child update for node %s", msg.node_id) - return - - signals = [] - - # Update all platforms for the device via dispatcher. - # Add/update entity if schema validates to true. - validated = validate_child(msg.gateway, msg.node_id, child) - for platform, dev_ids in validated.items(): - devices = get_mysensors_devices(hass, platform) - new_dev_ids = [] - for dev_id in dev_ids: - if dev_id in devices: - signals.append(SIGNAL_CALLBACK.format(*dev_id)) - else: - new_dev_ids.append(dev_id) - if new_dev_ids: - discover_mysensors_platform(hass, platform, new_dev_ids) - for signal in set(signals): - # Only one signal per device is needed. - # A device can have multiple platforms, ie multiple schemas. - # FOR LATER: Add timer to not signal if another update comes in. - async_dispatcher_send(hass, signal) - end = timer() - if end - start > 0.1: - _LOGGER.debug( - "Callback for node %s child %s took %.3f seconds", - msg.node_id, msg.child_id, end - start) - return mysensors_callback - - -def get_mysensors_name(gateway, node_id, child_id): - """Return a name for a node child.""" - node_name = '{} {}'.format( - gateway.sensors[node_id].sketch_name, node_id) - node_name = next( - (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() - if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), - node_name) - return '{} {}'.format(node_name, child_id) - - -def get_mysensors_gateway(hass, gateway_id): - """Return MySensors gateway.""" - if MYSENSORS_GATEWAYS not in hass.data: - hass.data[MYSENSORS_GATEWAYS] = {} - gateways = hass.data.get(MYSENSORS_GATEWAYS) - return gateways.get(gateway_id) - - -@callback -def setup_mysensors_platform( - hass, domain, discovery_info, device_class, device_args=None, - async_add_devices=None): - """Set up a MySensors platform.""" - # Only act if called via mysensors by discovery event. - # Otherwise gateway is not setup. - if not discovery_info: - return - if device_args is None: - device_args = () - new_devices = [] - new_dev_ids = discovery_info[ATTR_DEVICES] - for dev_id in new_dev_ids: - devices = get_mysensors_devices(hass, domain) - if dev_id in devices: - continue - gateway_id, node_id, child_id, value_type = dev_id - gateway = get_mysensors_gateway(hass, gateway_id) - if not gateway: - continue - device_class_copy = device_class - if isinstance(device_class, dict): - child = gateway.sensors[node_id].children[child_id] - s_type = gateway.const.Presentation(child.type).name - device_class_copy = device_class[s_type] - name = get_mysensors_name(gateway, node_id, child_id) - - args_copy = (*device_args, gateway, node_id, child_id, name, - value_type) - devices[dev_id] = device_class_copy(*args_copy) - new_devices.append(devices[dev_id]) - if new_devices: - _LOGGER.info("Adding new devices: %s", new_devices) - if async_add_devices is not None: - async_add_devices(new_devices, True) - return new_devices - - -class MySensorsDevice(object): - """Representation of a MySensors device.""" - - def __init__(self, gateway, node_id, child_id, name, value_type): - """Set up the MySensors device.""" - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - child = gateway.sensors[node_id].children[child_id] - self.child_type = child.type - self._values = {} - - @property - def name(self): - """Return the name of this entity.""" - return self._name - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - attr = { - ATTR_BATTERY_LEVEL: node.battery_level, - ATTR_CHILD_ID: self.child_id, - ATTR_DESCRIPTION: child.description, - ATTR_DEVICE: self.gateway.device, - ATTR_NODE_ID: self.node_id, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - attr[set_req(value_type).name] = value - - return attr - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - set_req = self.gateway.const.SetReq - for value_type, value in child.values.items(): - _LOGGER.debug( - "Entity update: %s: value_type %s, value = %s", - self._name, value_type, value) - if value_type in (set_req.V_ARMED, set_req.V_LIGHT, - set_req.V_LOCK_STATUS, set_req.V_TRIPPED): - self._values[value_type] = ( - STATE_ON if int(value) == 1 else STATE_OFF) - elif value_type == set_req.V_DIMMER: - self._values[value_type] = int(value) - else: - self._values[value_type] = value - - -class MySensorsEntity(MySensorsDevice, Entity): - """Representation of a MySensors entity.""" - - @property - def should_poll(self): - """Return the polling state. The gateway pushes its states.""" - return False - - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values - - @callback - def async_update_callback(self): - """Update the entity.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Register update callback.""" - dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type - async_dispatcher_connect( - self.hass, SIGNAL_CALLBACK.format(*dev_id), - self.async_update_callback) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py new file mode 100644 index 00000000000000..3aa8e82911eff5 --- /dev/null +++ b/homeassistant/components/mysensors/__init__.py @@ -0,0 +1,167 @@ +""" +Connect to a MySensors gateway via pymysensors API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mysensors/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.mqtt import ( + valid_publish_topic, valid_subscribe_topic) +from homeassistant.const import CONF_OPTIMISTIC +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, + CONF_NODES, CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, + CONF_TCP_PORT, CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, + DOMAIN, MYSENSORS_GATEWAYS) +from .device import get_mysensors_devices +from .gateway import get_mysensors_gateway, setup_gateways, finish_setup + +REQUIREMENTS = ['pymysensors==0.14.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEBUG = 'debug' +CONF_NODE_NAME = 'name' + +DEFAULT_BAUD_RATE = 115200 +DEFAULT_TCP_PORT = 5003 +DEFAULT_VERSION = '1.4' + + +def has_all_unique_files(value): + """Validate that all persistence files are unique and set if any is set.""" + persistence_files = [ + gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] + if None in persistence_files and any( + name is not None for name in persistence_files): + raise vol.Invalid( + 'persistence file name of all devices must be set if any is set') + if not all(name is None for name in persistence_files): + schema = vol.Schema(vol.Unique()) + schema(persistence_files) + return value + + +def is_persistence_file(value): + """Validate that persistence file path ends in either .pickle or .json.""" + if value.endswith(('.json', '.pickle')): + return value + else: + raise vol.Invalid( + '{} does not end in either `.json` or `.pickle`'.format(value)) + + +def deprecated(key): + """Mark key as deprecated in configuration.""" + def validator(config): + """Check if key is in config, log warning and remove key.""" + if key not in config: + return config + _LOGGER.warning( + '%s option for %s is deprecated. Please remove %s from your ' + 'configuration file', key, DOMAIN, key) + config.pop(key) + return config + return validator + + +NODE_SCHEMA = vol.Schema({ + cv.positive_int: { + vol.Required(CONF_NODE_NAME): cv.string + } +}) + +GATEWAY_SCHEMA = { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_PERSISTENCE_FILE): + vol.All(cv.string, is_persistence_file), + vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): + cv.positive_int, + vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, + vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, + vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { + vol.Required(CONF_GATEWAYS): vol.All( + cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA]), + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, + vol.Optional(CONF_RETAIN, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + })) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the MySensors component.""" + gateways = await setup_gateways(hass, config) + + if not gateways: + _LOGGER.error( + "No devices could be setup as gateways, check your configuration") + return False + + hass.data[MYSENSORS_GATEWAYS] = gateways + + hass.async_add_job(finish_setup(hass, gateways)) + + return True + + +def _get_mysensors_name(gateway, node_id, child_id): + """Return a name for a node child.""" + node_name = '{} {}'.format( + gateway.sensors[node_id].sketch_name, node_id) + node_name = next( + (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() + if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), + node_name) + return '{} {}'.format(node_name, child_id) + + +@callback +def setup_mysensors_platform( + hass, domain, discovery_info, device_class, device_args=None, + async_add_devices=None): + """Set up a MySensors platform.""" + # Only act if called via MySensors by discovery event. + # Otherwise gateway is not setup. + if not discovery_info: + return + if device_args is None: + device_args = () + new_devices = [] + new_dev_ids = discovery_info[ATTR_DEVICES] + for dev_id in new_dev_ids: + devices = get_mysensors_devices(hass, domain) + if dev_id in devices: + continue + gateway_id, node_id, child_id, value_type = dev_id + gateway = get_mysensors_gateway(hass, gateway_id) + if not gateway: + continue + device_class_copy = device_class + if isinstance(device_class, dict): + child = gateway.sensors[node_id].children[child_id] + s_type = gateway.const.Presentation(child.type).name + device_class_copy = device_class[s_type] + name = _get_mysensors_name(gateway, node_id, child_id) + + args_copy = (*device_args, gateway, node_id, child_id, name, + value_type) + devices[dev_id] = device_class_copy(*args_copy) + new_devices.append(devices[dev_id]) + if new_devices: + _LOGGER.info("Adding new devices: %s", new_devices) + if async_add_devices is not None: + async_add_devices(new_devices, True) + return new_devices diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py new file mode 100644 index 00000000000000..4f9718a39dbf2d --- /dev/null +++ b/homeassistant/components/mysensors/const.py @@ -0,0 +1,138 @@ +"""MySensors constants.""" +import homeassistant.helpers.config_validation as cv + +ATTR_DEVICES = 'devices' + +CONF_BAUD_RATE = 'baud_rate' +CONF_DEVICE = 'device' +CONF_GATEWAYS = 'gateways' +CONF_NODES = 'nodes' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_RETAIN = 'retain' +CONF_TCP_PORT = 'tcp_port' +CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' +CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' +CONF_VERSION = 'version' + +DOMAIN = 'mysensors' +MYSENSORS_GATEWAYS = 'mysensors_gateways' +PLATFORM = 'platform' +SCHEMA = 'schema' +SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' +TYPE = 'type' + +# MySensors const schemas +BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} +CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} +LIGHT_DIMMER_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_DIMMER', + SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} +LIGHT_PERCENTAGE_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_PERCENTAGE', + SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGB_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { + 'V_RGB': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGBW_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { + 'V_RGBW': cv.string, 'V_STATUS': cv.string}} +NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} +DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} +DUST_SCHEMA = [ + {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] +SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} +SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} +MYSENSORS_CONST_SCHEMA = { + 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SPRINKLER': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_WATER_LEAK': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SOUND': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_VIBRATION': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOISTURE': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_HVAC': [CLIMATE_SCHEMA], + 'S_COVER': [ + {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, + {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, + {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, + {PLATFORM: 'cover', TYPE: 'V_STATUS'}], + 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], + 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], + 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], + 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], + 'S_GPS': [ + DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], + 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], + 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], + 'S_BARO': [ + {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, + {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], + 'S_WIND': [ + {PLATFORM: 'sensor', TYPE: 'V_WIND'}, + {PLATFORM: 'sensor', TYPE: 'V_GUST'}, + {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], + 'S_RAIN': [ + {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, + {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], + 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], + 'S_WEIGHT': [ + {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_POWER': [ + {PLATFORM: 'sensor', TYPE: 'V_WATT'}, + {PLATFORM: 'sensor', TYPE: 'V_KWH'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR'}, + {PLATFORM: 'sensor', TYPE: 'V_VA'}, + {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], + 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], + 'S_LIGHT_LEVEL': [ + {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], + 'S_IR': [ + {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, + {PLATFORM: 'switch', TYPE: 'V_IR_SEND', + SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], + 'S_WATER': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_CUSTOM': [ + {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, + {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], + 'S_SCENE_CONTROLLER': [ + {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, + {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], + 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], + 'S_MULTIMETER': [ + {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, + {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_GAS': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_WATER_QUALITY': [ + {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, + {PLATFORM: 'sensor', TYPE: 'V_PH'}, + {PLATFORM: 'sensor', TYPE: 'V_ORP'}, + {PLATFORM: 'sensor', TYPE: 'V_EC'}, + {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_AIR_QUALITY': DUST_SCHEMA, + 'S_DUST': DUST_SCHEMA, + 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], + 'S_BINARY': [SWITCH_STATUS_SCHEMA], + 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], +} diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py new file mode 100644 index 00000000000000..b0770f90c1db76 --- /dev/null +++ b/homeassistant/components/mysensors/device.py @@ -0,0 +1,109 @@ +"""Handle MySensors devices.""" +import logging + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import SIGNAL_CALLBACK + +_LOGGER = logging.getLogger(__name__) + +ATTR_CHILD_ID = 'child_id' +ATTR_DESCRIPTION = 'description' +ATTR_DEVICE = 'device' +ATTR_NODE_ID = 'node_id' +MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' + + +def get_mysensors_devices(hass, domain): + """Return MySensors devices for a platform.""" + if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: + hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} + return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] + + +class MySensorsDevice(object): + """Representation of a MySensors device.""" + + def __init__(self, gateway, node_id, child_id, name, value_type): + """Set up the MySensors device.""" + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + child = gateway.sensors[node_id].children[child_id] + self.child_type = child.type + self._values = {} + + @property + def name(self): + """Return the name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + attr = { + ATTR_BATTERY_LEVEL: node.battery_level, + ATTR_CHILD_ID: self.child_id, + ATTR_DESCRIPTION: child.description, + ATTR_DEVICE: self.gateway.device, + ATTR_NODE_ID: self.node_id, + } + + set_req = self.gateway.const.SetReq + + for value_type, value in self._values.items(): + attr[set_req(value_type).name] = value + + return attr + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + set_req = self.gateway.const.SetReq + for value_type, value in child.values.items(): + _LOGGER.debug( + "Entity update: %s: value_type %s, value = %s", + self._name, value_type, value) + if value_type in (set_req.V_ARMED, set_req.V_LIGHT, + set_req.V_LOCK_STATUS, set_req.V_TRIPPED): + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + elif value_type == set_req.V_DIMMER: + self._values[value_type] = int(value) + else: + self._values[value_type] = value + + +class MySensorsEntity(MySensorsDevice, Entity): + """Representation of a MySensors entity.""" + + @property + def should_poll(self): + """Return the polling state. The gateway pushes its states.""" + return False + + @property + def available(self): + """Return true if entity is available.""" + return self.value_type in self._values + + @callback + def async_update_callback(self): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register update callback.""" + dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type + async_dispatcher_connect( + self.hass, SIGNAL_CALLBACK.format(*dev_id), + self.async_update_callback) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py new file mode 100644 index 00000000000000..a7719a80d99536 --- /dev/null +++ b/homeassistant/components/mysensors/gateway.py @@ -0,0 +1,328 @@ +"""Handle MySensors gateways.""" +import asyncio +from collections import defaultdict +import logging +import socket +import sys +from timeit import default_timer as timer + +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, CONF_NODES, + CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, + CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, DOMAIN, + MYSENSORS_CONST_SCHEMA, MYSENSORS_GATEWAYS, PLATFORM, SCHEMA, + SIGNAL_CALLBACK, TYPE) +from .device import get_mysensors_devices + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_READY_TIMEOUT = 15.0 +MQTT_COMPONENT = 'mqtt' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' + + +def is_serial_port(value): + """Validate that value is a windows serial port or a unix device.""" + if sys.platform.startswith('win'): + ports = ('COM{}'.format(idx + 1) for idx in range(256)) + if value in ports: + return value + else: + raise vol.Invalid('{} is not a serial port'.format(value)) + else: + return cv.isdevice(value) + + +def is_socket_address(value): + """Validate that value is a valid address.""" + try: + socket.getaddrinfo(value, None) + return value + except OSError: + raise vol.Invalid('Device is not a valid domain name or ip address') + + +def get_mysensors_gateway(hass, gateway_id): + """Return MySensors gateway.""" + if MYSENSORS_GATEWAYS not in hass.data: + hass.data[MYSENSORS_GATEWAYS] = {} + gateways = hass.data.get(MYSENSORS_GATEWAYS) + return gateways.get(gateway_id) + + +async def setup_gateways(hass, config): + """Set up all gateways.""" + conf = config[DOMAIN] + gateways = {} + + for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]): + persistence_file = gateway_conf.get( + CONF_PERSISTENCE_FILE, + hass.config.path('mysensors{}.pickle'.format(index + 1))) + ready_gateway = await _get_gateway( + hass, config, gateway_conf, persistence_file) + if ready_gateway is not None: + gateways[id(ready_gateway)] = ready_gateway + + return gateways + + +async def _get_gateway(hass, config, gateway_conf, persistence_file): + """Return gateway after setup of the gateway.""" + import mysensors.mysensors as mysensors + + conf = config[DOMAIN] + persistence = conf[CONF_PERSISTENCE] + version = conf[CONF_VERSION] + device = gateway_conf[CONF_DEVICE] + baud_rate = gateway_conf[CONF_BAUD_RATE] + tcp_port = gateway_conf[CONF_TCP_PORT] + in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, '') + out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, '') + + if device == MQTT_COMPONENT: + if not await async_setup_component(hass, MQTT_COMPONENT, config): + return None + mqtt = hass.components.mqtt + retain = conf[CONF_RETAIN] + + def pub_callback(topic, payload, qos, retain): + """Call MQTT publish function.""" + mqtt.async_publish(topic, payload, qos, retain) + + def sub_callback(topic, sub_cb, qos): + """Call MQTT subscribe function.""" + @callback + def internal_callback(*args): + """Call callback.""" + sub_cb(*args) + + hass.async_add_job( + mqtt.async_subscribe(topic, internal_callback, qos)) + + gateway = mysensors.AsyncMQTTGateway( + pub_callback, sub_callback, in_prefix=in_prefix, + out_prefix=out_prefix, retain=retain, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + else: + try: + await hass.async_add_job(is_serial_port, device) + gateway = mysensors.AsyncSerialGateway( + device, baud=baud_rate, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + try: + await hass.async_add_job(is_socket_address, device) + # valid ip address + gateway = mysensors.AsyncTCPGateway( + device, port=tcp_port, loop=hass.loop, event_callback=None, + persistence=persistence, persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + # invalid ip address + return None + gateway.metric = hass.config.units.is_metric + gateway.optimistic = conf[CONF_OPTIMISTIC] + gateway.device = device + gateway.event_callback = _gw_callback_factory(hass) + gateway.nodes_config = gateway_conf[CONF_NODES] + if persistence: + await gateway.start_persistence() + + return gateway + + +async def finish_setup(hass, gateways): + """Load any persistent devices and platforms and start gateway.""" + discover_tasks = [] + start_tasks = [] + for gateway in gateways.values(): + discover_tasks.append(_discover_persistent_devices(hass, gateway)) + start_tasks.append(_gw_start(hass, gateway)) + if discover_tasks: + # Make sure all devices and platforms are loaded before gateway start. + await asyncio.wait(discover_tasks, loop=hass.loop) + if start_tasks: + await asyncio.wait(start_tasks, loop=hass.loop) + + +async def _discover_persistent_devices(hass, gateway): + """Discover platforms for devices loaded via persistence file.""" + tasks = [] + new_devices = defaultdict(list) + for node_id in gateway.sensors: + node = gateway.sensors[node_id] + for child in node.children.values(): + validated = _validate_child(gateway, node_id, child) + for platform, dev_ids in validated.items(): + new_devices[platform].extend(dev_ids) + for platform, dev_ids in new_devices.items(): + tasks.append(_discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + +@callback +def _discover_mysensors_platform(hass, platform, new_devices): + """Discover a MySensors platform.""" + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task + + +async def _gw_start(hass, gateway): + """Start the gateway.""" + @callback + def gw_stop(event): + """Trigger to stop the gateway.""" + hass.async_add_job(gateway.stop()) + + await gateway.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +def _gw_callback_factory(hass): + """Return a new callback for the gateway.""" + @callback + def mysensors_callback(msg): + """Handle messages from a MySensors gateway.""" + start = timer() + _LOGGER.debug( + "Node update: node %s child %s", msg.node_id, msg.child_id) + + _set_gateway_ready(hass, msg) + + try: + child = msg.gateway.sensors[msg.node_id].children[msg.child_id] + except KeyError: + _LOGGER.debug("Not a child update for node %s", msg.node_id) + return + + signals = [] + + # Update all platforms for the device via dispatcher. + # Add/update entity if schema validates to true. + validated = _validate_child(msg.gateway, msg.node_id, child) + for platform, dev_ids in validated.items(): + devices = get_mysensors_devices(hass, platform) + new_dev_ids = [] + for dev_id in dev_ids: + if dev_id in devices: + signals.append(SIGNAL_CALLBACK.format(*dev_id)) + else: + new_dev_ids.append(dev_id) + if new_dev_ids: + _discover_mysensors_platform(hass, platform, new_dev_ids) + for signal in set(signals): + # Only one signal per device is needed. + # A device can have multiple platforms, ie multiple schemas. + # FOR LATER: Add timer to not signal if another update comes in. + async_dispatcher_send(hass, signal) + end = timer() + if end - start > 0.1: + _LOGGER.debug( + "Callback for node %s child %s took %.3f seconds", + msg.node_id, msg.child_id, end - start) + return mysensors_callback + + +@callback +def _set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) + + +def _validate_child(gateway, node_id, child): + """Validate that a child has the correct values according to schema. + + Return a dict of platform with a list of device ids for validated devices. + """ + validated = defaultdict(list) + + if not child.values: + _LOGGER.debug( + "No child values for node %s child %s", node_id, child.id) + return validated + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.debug("Node %s is missing sketch name", node_id) + return validated + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + s_name = next( + (member.name for member in pres if member.value == child.type), None) + if s_name not in MYSENSORS_CONST_SCHEMA: + _LOGGER.warning("Child type %s is not supported", s_name) + return validated + child_schemas = MYSENSORS_CONST_SCHEMA[s_name] + + def msg(name): + """Return a message for an invalid schema.""" + return "{} requires value_type {}".format( + pres(child.type).name, set_req[name].name) + + for schema in child_schemas: + platform = schema[PLATFORM] + v_name = schema[TYPE] + value_type = next( + (member.value for member in set_req if member.name == v_name), + None) + if value_type is None: + continue + _child_schema = child.get_schema(gateway.protocol_version) + vol_schema = _child_schema.extend( + {vol.Required(set_req[key].value, msg=msg(key)): + _child_schema.schema.get(set_req[key].value, val) + for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, + extra=vol.ALLOW_EXTRA) + try: + vol_schema(child.values) + except vol.Invalid as exc: + level = (logging.WARNING if value_type in child.values + else logging.DEBUG) + _LOGGER.log( + level, + "Invalid values: %s: %s platform: node %s child %s: %s", + child.values, platform, node_id, child.id, exc) + continue + dev_id = id(gateway), node_id, child.id, value_type + validated[platform].append(dev_id) + return validated diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index db568514dea25a..71ce7fb0b74eec 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -18,7 +18,7 @@ async def async_get_service(hass, config, discovery_info=None): return MySensorsNotificationService(hass) -class MySensorsNotificationDevice(mysensors.MySensorsDevice): +class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): """Represent a MySensors Notification device.""" def send_msg(self, msg): diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 1add4157f0e952..2fbfc0e97a4169 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsSensor(mysensors.MySensorsEntity): +class MySensorsSensor(mysensors.device.MySensorsEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index a91ca6d11e74f9..340eed83b567e8 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -65,7 +65,7 @@ async def async_send_ir_code_service(service): schema=SEND_IR_CODE_SERVICE_SCHEMA) -class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): +class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): """Representation of the value of a MySensors Switch child node.""" @property From d3ceb9080c4363db24e7fcff37b771afae483578 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Mon, 25 Jun 2018 05:04:16 -0700 Subject: [PATCH 067/169] MQTT Alarm Control Panel: add retain option for publishing for cases... (#15134) * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep --- .../components/alarm_control_panel/mqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 8a0dfefdc70849..9f2a4176ed8862 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -20,7 +20,7 @@ from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability) + CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -54,6 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_QOS), + config.get(CONF_RETAIN), config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_AWAY), @@ -66,9 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" - def __init__(self, name, state_topic, command_topic, qos, payload_disarm, - payload_arm_home, payload_arm_away, code, availability_topic, - payload_available, payload_not_available): + def __init__(self, name, state_topic, command_topic, qos, retain, + payload_disarm, payload_arm_home, payload_arm_away, code, + availability_topic, payload_available, payload_not_available): """Init the MQTT Alarm Control Panel.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -77,6 +78,7 @@ def __init__(self, name, state_topic, command_topic, qos, payload_disarm, self._state_topic = state_topic self._command_topic = command_topic self._qos = qos + self._retain = retain self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away @@ -134,7 +136,8 @@ def async_alarm_disarm(self, code=None): if not self._validate_code(code, 'disarming'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_disarm, self._qos) + self.hass, self._command_topic, self._payload_disarm, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_home(self, code=None): @@ -145,7 +148,8 @@ def async_alarm_arm_home(self, code=None): if not self._validate_code(code, 'arming home'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_home, self._qos) + self.hass, self._command_topic, self._payload_arm_home, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_away(self, code=None): @@ -156,7 +160,8 @@ def async_alarm_arm_away(self, code=None): if not self._validate_code(code, 'arming away'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_away, self._qos) + self.hass, self._command_topic, self._payload_arm_away, self._qos, + self._retain) def _validate_code(self, code, state): """Validate given code.""" From 73034c933ed6cc856a7d656c3c41e474fd7d33e0 Mon Sep 17 00:00:00 2001 From: dreizehnelf Date: Mon, 25 Jun 2018 15:13:19 +0200 Subject: [PATCH 068/169] Add discovery support to mqtt climate component. (#15085) * Add discovery support to mqtt climate component. * - Fix flake8 error (./homeassistant/components/climate/mqtt.py:130:1: D202 No blank lines allowed after function docstring) - Fix test error (since climate component was expected not to work - changed it to "lock" component, which also does not have MQTT discovery support yet) * Fix old assert statement to reflect new lock component usage * Change invalid MQTT discovery component type from 'lock' to 'timer', since contrary to the documentation the lock component is properly supported when using MQTT discovery. * Make configuration of invalid MQTT config component a single point of entry to prevent missing the assertion later in the code when changing. * Add new testcases to cover not-yet-covered code paths in https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/climate/mqtt.py --- homeassistant/components/climate/mqtt.py | 3 + homeassistant/components/mqtt/discovery.py | 3 +- tests/components/climate/test_mqtt.py | 82 +++++++++++++++++++++- tests/components/mqtt/test_discovery.py | 34 ++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 5397daeb784cfd..2878717d91b2cf 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -129,6 +129,9 @@ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT climate devices.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d5a3b4a2efb7e7..3916714b8d1626 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -21,7 +21,7 @@ SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', - 'light', 'sensor', 'switch', 'lock'] + 'light', 'sensor', 'switch', 'lock', 'climate'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], @@ -32,6 +32,7 @@ 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], + 'climate': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 255d482d584394..5db77331cd4038 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -137,6 +137,37 @@ def test_set_operation_pessimistic(self): self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) + def test_set_operation_with_power_command(self): + """Test setting of new operation mode with power command enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['power_command_topic'] = 'power-command' + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("on", state.attributes.get('operation_mode')) + self.assertEqual("on", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'ON', 0, False), + unittest.mock.call('mode-topic', 'on', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + + climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'OFF', 0, False), + unittest.mock.call('mode-topic', 'off', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_fan_mode_bad_attr(self): """Test setting fan mode without required attribute.""" assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) @@ -241,6 +272,8 @@ def test_set_target_temperature(self): self.assertEqual(21, state.attributes.get('temperature')) climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('heat', state.attributes.get('operation_mode')) self.mock_publish.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() @@ -252,6 +285,21 @@ def test_set_target_temperature(self): self.mock_publish.async_publish.assert_called_once_with( 'temperature-topic', 47, 0, False) + # also test directly supplying the operation mode to set_temperature + self.mock_publish.async_publish.reset_mock() + climate.set_temperature(self.hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('cool', state.attributes.get('operation_mode')) + self.assertEqual(21, state.attributes.get('temperature')) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('mode-topic', 'cool', 0, False), + unittest.mock.call('temperature-topic', 21, 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_target_temperature_pessimistic(self): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -508,13 +556,28 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('swing_mode')) - # Temperature + # Temperature - with valid value self.assertEqual(21, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '"1031"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(1031, state.attributes.get('temperature')) + # Temperature - with invalid value + with self.assertLogs(level='ERROR') as log: + fire_mqtt_message(self.hass, 'temperature-state', '"-INVALID-"') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + self.assertEqual(len(log.output), 1) + self.assertEqual(len(log.records), 1) + self.assertIn( + "Could not parse temperature from -INVALID-", + log.output[0] + ) + # ... but the actual value stays unchanged. + self.assertEqual(1031, state.attributes.get('temperature')) + # Away Mode self.assertEqual('off', state.attributes.get('away_mode')) fire_mqtt_message(self.hass, 'away-state', '"ON"') @@ -522,6 +585,17 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) + # Away Mode with JSON values + fire_mqtt_message(self.hass, 'away-state', 'false') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('away_mode')) + + fire_mqtt_message(self.hass, 'away-state', 'true') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('on', state.attributes.get('away_mode')) + # Hold Mode self.assertEqual(None, state.attributes.get('hold_mode')) fire_mqtt_message(self.hass, 'hold-state', """ @@ -538,6 +612,12 @@ def test_set_with_templates(self): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) + # anything other than 'switchmeon' should turn Aux mode off + fire_mqtt_message(self.hass, 'aux-state', 'somerandomstring') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('aux_heat')) + # Current temperature fire_mqtt_message(self.hass, 'current-temperature', '"74656"') self.hass.block_till_done() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1dd29909ffdfb4..ed6c77f676ce16 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -52,12 +52,21 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + invalid_component = "timer" + mock_load_platform.return_value = mock_coro() yield from async_start(hass, 'homeassistant', {}) - async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '{}') + async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format( + invalid_component + ), '{}') + yield from hass.async_block_till_done() - assert 'Component climate is not supported' in caplog.text + + assert 'Component {} is not supported'.format( + invalid_component + ) in caplog.text + assert not mock_load_platform.called @@ -94,6 +103,27 @@ def test_discover_fan(hass, mqtt_mock, caplog): assert ('fan', 'bla') in hass.data[ALREADY_DISCOVERED] +@asyncio.coroutine +def test_discover_climate(hass, mqtt_mock, caplog): + """Test discovering an MQTT climate component.""" + yield from async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "ClimateTest",' + ' "current_temperature_topic": "climate/bla/current_temp",' + ' "temperature_command_topic": "climate/bla/target_temp" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('climate.ClimateTest') + + assert state is not None + assert state.name == 'ClimateTest' + assert ('climate', 'bla') in hass.data[ALREADY_DISCOVERED] + + @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" From 038168c417a5e43f4cb0dfd868a84616e71c2e22 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 25 Jun 2018 09:45:26 -0400 Subject: [PATCH 069/169] Support for Homekit Controller climate devices (#15057) * Support for Homekit Controller climate devices * Handle stale state when operating mode off --- .../components/climate/homekit_controller.py | 130 ++++++++++++++++++ .../components/homekit_controller/__init__.py | 6 + .../components/light/homekit_controller.py | 7 +- .../components/switch/homekit_controller.py | 7 +- 4 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/climate/homekit_controller.py diff --git a/homeassistant/components/climate/homekit_controller.py b/homeassistant/components/climate/homekit_controller.py new file mode 100644 index 00000000000000..f9178c2e0d55af --- /dev/null +++ b/homeassistant/components/climate/homekit_controller.py @@ -0,0 +1,130 @@ +""" +Support for Homekit climate devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import ( + HomeKitEntity, KNOWN_ACCESSORIES) +from homeassistant.components.climate import ( + ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + +# Map of Homekit operation modes to hass modes +MODE_HOMEKIT_TO_HASS = { + 0: STATE_OFF, + 1: STATE_HEAT, + 2: STATE_COOL, +} + +# Map of hass operation modes to homekit modes +MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Homekit climate.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_devices([HomeKitClimateDevice(accessory, discovery_info)], True) + + +class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): + """Representation of a Homekit climate device.""" + + def __init__(self, *args): + """Initialise the device.""" + super().__init__(*args) + self._state = None + self._current_mode = None + self._valid_modes = [] + self._current_temp = None + self._target_temp = None + + def update_characteristics(self, characteristics): + """Synchronise device state with Home Assistant.""" + # pylint: disable=import-error + from homekit import CharacteristicsTypes as ctypes + + for characteristic in characteristics: + ctype = characteristic['type'] + if ctype == ctypes.HEATING_COOLING_CURRENT: + self._state = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + if ctype == ctypes.HEATING_COOLING_TARGET: + self._chars['target_mode'] = characteristic['iid'] + self._features |= SUPPORT_OPERATION_MODE + self._current_mode = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( + mode) for mode in characteristic['valid-values']] + elif ctype == ctypes.TEMPERATURE_CURRENT: + self._current_temp = characteristic['value'] + elif ctype == ctypes.TEMPERATURE_TARGET: + self._chars['target_temp'] = characteristic['iid'] + self._features |= SUPPORT_TARGET_TEMPERATURE + self._target_temp = characteristic['value'] + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_temp'], + 'value': temp}] + self.put_characteristics(characteristics) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_mode'], + 'value': MODE_HASS_TO_HOMEKIT[operation_mode]}] + self.put_characteristics(characteristics) + + @property + def state(self): + """Return the current state.""" + # If the device reports its operating mode as off, it sometimes doesn't + # report a new state. + if self._current_mode == STATE_OFF: + return STATE_OFF + + if self._state == STATE_OFF and self._current_mode != STATE_OFF: + return STATE_IDLE + return self._state + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_mode + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._valid_modes + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._features + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 0883c5a3cc85b0..ff981c1607a8fe 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -23,6 +23,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { 'lightbulb': 'light', 'outlet': 'switch', + 'thermostat': 'climate', } KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) @@ -219,6 +220,11 @@ def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError + def put_characteristics(self, characteristics): + """Control a HomeKit device state from Home Assistant.""" + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) + # pylint: too-many-function-args def setup(hass, config): diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py index e6dc09e455cb27..8d77cb0523668e 100644 --- a/homeassistant/components/light/homekit_controller.py +++ b/homeassistant/components/light/homekit_controller.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import ( @@ -122,13 +121,11 @@ def turn_on(self, **kwargs): characteristics.append({'aid': self._aid, 'iid': self._chars['on'], 'value': True}) - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified light off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py index 6b97200ba499a3..3293c8fe1953bb 100644 --- a/homeassistant/components/switch/homekit_controller.py +++ b/homeassistant/components/switch/homekit_controller.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import (HomeKitEntity, @@ -56,13 +55,11 @@ def turn_on(self, **kwargs): characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': True}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified switch off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) From f8bc3411adac137b134aef6e60f57b0f490783c9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Jun 2018 15:57:26 +0200 Subject: [PATCH 070/169] PyPi: Fix description and setup.cfg (#15107) * Fix description and extend use of setup.cfg * Fix lint --- setup.cfg | 31 +++++++++++++++++++++++++++++++ setup.py | 44 ++++++++++---------------------------------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8b17da455dc949..2abd445bb855f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,34 @@ +[metadata] +license = Apache License 2.0 +license_file = LICENSE.md +platforms = any +description = Open-source home automation platform running on Python 3. +long_description = file: README.rst +keywords = home, automation +classifier = + Development Status :: 4 - Beta + Intended Audience :: End Users/Desktop + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Topic :: Home Automation + +[options] +packages = find: +include_package_data = true +zip_safe = false + +[options.entry_points] +console_scripts = + hass = homeassistant.__main__:main + +[options.packages.find] +exclude = + tests + tests.* + [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 69929285f78dff..3833f90f2d1055 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" -from setuptools import setup, find_packages +from datetime import datetime as dt +from setuptools import setup import homeassistant.const as hass_const @@ -8,26 +9,9 @@ PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) PROJECT_URL = 'https://home-assistant.io/' PROJECT_EMAIL = 'hello@home-assistant.io' -PROJECT_DESCRIPTION = ('Open-source home automation platform ' - 'running on Python 3.') -PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source ' - 'home automation platform running on Python 3. ' - 'Track and control all devices at home and ' - 'automate control. ' - 'Installation in less than a minute.') -PROJECT_CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Home Automation' -] PROJECT_GITHUB_USERNAME = 'home-assistant' PROJECT_GITHUB_REPOSITORY = 'home-assistant' @@ -38,8 +22,12 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) - -PACKAGES = find_packages(exclude=['tests', 'tests.*']) +PROJECT_URLS = { + 'Bug Reports': '{}/issues'.format(GITHUB_URL), + 'Dev Docs': 'https://developers.home-assistant.io/', + 'Discord': 'https://discordapp.com/invite/c5DvZ4e', + 'Forum': 'https://community.home-assistant.io/', +} REQUIRES = [ 'aiohttp==3.3.2', @@ -60,24 +48,12 @@ setup( name=PROJECT_PACKAGE_NAME, version=hass_const.__version__, - license=PROJECT_LICENSE, url=PROJECT_URL, download_url=DOWNLOAD_URL, + project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, - description=PROJECT_DESCRIPTION, - packages=PACKAGES, - include_package_data=True, - zip_safe=False, - platforms='any', install_requires=REQUIRES, python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', - keywords=['home', 'automation'], - entry_points={ - 'console_scripts': [ - 'hass = homeassistant.__main__:main' - ] - }, - classifiers=PROJECT_CLASSIFIERS, ) From 672a3c7178afc91ac0da015f098dd5f96672e4f2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Jun 2018 16:35:44 +0200 Subject: [PATCH 071/169] Add language to dark sky weather component (#15130) * Add language to dark sky weather component * Update darksky.py --- homeassistant/components/weather/darksky.py | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index f0712542ea544b..86cc740edbc6bf 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,9 +25,22 @@ ATTRIBUTION = "Powered by Dark Sky" +# Language Supported Codes +LANGUAGE_CODES = [ + 'ar', 'az', 'be', 'bg', 'bs', 'ca', + 'cs', 'da', 'de', 'el', 'en', 'es', + 'et', 'fi', 'fr', 'hr', 'hu', 'id', + 'is', 'it', 'ja', 'ka', 'kw', 'nb', + 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', + 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', + 'x-pig-latin', 'zh', 'zh-tw', +] + CONF_UNITS = 'units' +CONF_LANGUAGE = 'language' DEFAULT_NAME = 'Dark Sky' +DEFAULT_LANGUAGE = 'en' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -35,6 +48,8 @@ vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), }) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -44,15 +59,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky weather.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config.get(CONF_NAME) + name = config[CONF_NAME] + lang = config[CONF_LANGUAGE] + api_key = config[CONF_API_KEY] units = config.get(CONF_UNITS) if not units: units = 'si' if hass.config.units.is_metric else 'us' - dark_sky = DarkSkyData( - config.get(CONF_API_KEY), latitude, longitude, units) - + dark_sky = DarkSkyData(api_key, latitude, longitude, units, lang) add_devices([DarkSkyWeather(name, dark_sky)], True) @@ -132,12 +147,13 @@ def update(self): class DarkSkyData(object): """Get the latest data from Dark Sky.""" - def __init__(self, api_key, latitude, longitude, units): + def __init__(self, api_key, latitude, longitude, units, lang): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.requested_units = units + self.language = lang self.data = None self.currently = None @@ -152,7 +168,7 @@ def update(self): try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, - units=self.requested_units) + units=self.requested_units, lang=self.language) self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() From ae51dc08bf5cb947d14b0e55fb64387b31924ac5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 12:53:49 -0400 Subject: [PATCH 072/169] Add storage helper and migrate config entries (#15045) * Add storage helper * Migrate config entries to use the storage helper * Make sure tests do not do I/O * Lint * Add versions to stored data * Add more instance variables * Make migrator load config if nothing to migrate * Address comments --- homeassistant/components/sensor/fitbit.py | 2 +- homeassistant/config_entries.py | 60 ++++---- homeassistant/core.py | 14 ++ homeassistant/helpers/storage.py | 157 +++++++++++++++++++++ homeassistant/util/json.py | 14 +- tests/common.py | 8 +- tests/helpers/test_storage.py | 158 ++++++++++++++++++++++ tests/test_config_entries.py | 17 ++- 8 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 homeassistant/helpers/storage.py create mode 100644 tests/helpers/test_storage.py diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index f312d1f22cc1e1..87bd735a03df1d 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -225,7 +225,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config, add_devices, config_path, discovery_info=None) return False else: - config_file = save_json(config_path, DEFAULT_CONFIG) + save_json(config_path, DEFAULT_CONFIG) request_app_setup( hass, config, add_devices, config_path, discovery_info=None) return False diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index db2912d7b42297..13cb7de62ef9a6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -112,15 +112,13 @@ async def async_step_discovery(info): """ import logging -import os import uuid -from . import data_entry_flow -from .core import callback -from .exceptions import HomeAssistantError -from .setup import async_setup_component, async_process_deps_reqs -from .util.json import load_json, save_json -from .util.decorator import Registry +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component, async_process_deps_reqs +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) @@ -136,6 +134,10 @@ async def async_step_discovery(info): ] +STORAGE_KEY = 'core.config_entries' +STORAGE_VERSION = 1 + +# Deprecated since 0.73 PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 @@ -271,7 +273,7 @@ def __init__(self, hass, hass_config): hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None - self._sched_save = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback def async_domains(self): @@ -305,7 +307,7 @@ async def async_remove(self, entry_id): raise UnknownEntry entry = self._entries.pop(found) - self._async_schedule_save() + await self._async_schedule_save() unloaded = await entry.async_unload(self.hass) @@ -314,14 +316,14 @@ async def async_remove(self, entry_id): } async def async_load(self): - """Load the config.""" - path = self.hass.config.path(PATH_CONFIG) - if not os.path.isfile(path): - self._entries = [] - return + """Handle loading the config.""" + # Migrating for config entries stored before 0.73 + config = await self.hass.helpers.storage.async_migrator( + self.hass.config.path(PATH_CONFIG), self._store, + old_conf_migrate_func=_old_conf_migrator + ) - entries = await self.hass.async_add_job(load_json, path) - self._entries = [ConfigEntry(**entry) for entry in entries] + self._entries = [ConfigEntry(**entry) for entry in config['entries']] async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. @@ -372,7 +374,7 @@ async def _async_finish_flow(self, result): source=result['source'], ) self._entries.append(entry) - self._async_schedule_save() + await self._async_schedule_save() # Setup entry if entry.domain in self.hass.config.components: @@ -416,20 +418,14 @@ async def _async_create_flow(self, handler, *, source, data): return handler() - @callback - def _async_schedule_save(self): - """Schedule saving the entity registry.""" - if self._sched_save is not None: - self._sched_save.cancel() - - self._sched_save = self.hass.loop.call_later( - SAVE_DELAY, self.hass.async_add_job, self._async_save - ) - - async def _async_save(self): + async def _async_schedule_save(self): """Save the entity registry to a file.""" - self._sched_save = None - data = [entry.as_dict() for entry in self._entries] + data = { + 'entries': [entry.as_dict() for entry in self._entries] + } + await self._store.async_save(data, delay=SAVE_DELAY) + - await self.hass.async_add_job( - save_json, self.hass.config.path(PATH_CONFIG), data) +async def _old_conf_migrator(old_config): + """Migrate the pre-0.73 config format to the latest version.""" + return {'entries': old_config} diff --git a/homeassistant/core.py b/homeassistant/core.py index 5e6dcd81310b0d..e09501729139bb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -230,6 +230,20 @@ def async_add_job( return task + @callback + def async_add_executor_job( + self, + target: Callable[..., Any], + *args: Any) -> asyncio.tasks.Task: + """Add an executor job from within the event loop.""" + task = self.loop.run_in_executor(None, target, *args) + + # If a task is scheduled + if self._track_task: + self._pending_tasks.append(task) + + return task + @callback def async_track_tasks(self): """Track tasks so you can wait for all tasks to be done.""" diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py new file mode 100644 index 00000000000000..4b0c576f129d96 --- /dev/null +++ b/homeassistant/helpers/storage.py @@ -0,0 +1,157 @@ +"""Helper to help store data.""" +import asyncio +import logging +import os +from typing import Dict, Optional + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.util import json +from homeassistant.helpers.event import async_call_later + +STORAGE_DIR = '.storage' +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_migrator(hass, old_path, store, *, old_conf_migrate_func=None): + """Helper function to migrate old data to a store and then load data. + + async def old_conf_migrate_func(old_data) + """ + def load_old_config(): + """Helper to load old config.""" + if not os.path.isfile(old_path): + return None + + return json.load_json(old_path) + + config = await hass.async_add_executor_job(load_old_config) + + if config is None: + return await store.async_load() + + if old_conf_migrate_func is not None: + config = await old_conf_migrate_func(config) + + await store.async_save(config) + await hass.async_add_executor_job(os.remove, old_path) + return config + + +@bind_hass +class Store: + """Class to help storing data.""" + + def __init__(self, hass, version: int, key: str): + """Initialize storage class.""" + self.version = version + self.key = key + self.hass = hass + self._data = None + self._unsub_delay_listener = None + self._unsub_stop_listener = None + self._write_lock = asyncio.Lock() + + @property + def path(self): + """Return the config path.""" + return self.hass.config.path(STORAGE_DIR, self.key) + + async def async_load(self): + """Load data. + + If the expected version does not match the given version, the migrate + function will be invoked with await migrate_func(version, config). + """ + if self._data is not None: + data = self._data + else: + data = await self.hass.async_add_executor_job( + json.load_json, self.path, None) + + if data is None: + return {} + + if data['version'] == self.version: + return data['data'] + + return await self._async_migrate_func(data['version'], data['data']) + + async def async_save(self, data: Dict, *, delay: Optional[int] = None): + """Save data with an optional delay.""" + self._data = { + 'version': self.version, + 'key': self.key, + 'data': data, + } + + self._async_cleanup_delay_listener() + + if delay is None: + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + return + + self._unsub_delay_listener = async_call_later( + self.hass, delay, self._async_callback_delayed_write) + + self._async_ensure_stop_listener() + + @callback + def _async_ensure_stop_listener(self): + """Ensure that we write if we quit before delay has passed.""" + if self._unsub_stop_listener is None: + self._unsub_stop_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_callback_stop_write) + + @callback + def _async_cleanup_stop_listener(self): + """Clean up a stop listener.""" + if self._unsub_stop_listener is not None: + self._unsub_stop_listener() + self._unsub_stop_listener = None + + @callback + def _async_cleanup_delay_listener(self): + """Clean up a delay listener.""" + if self._unsub_delay_listener is not None: + self._unsub_delay_listener() + self._unsub_delay_listener = None + + async def _async_callback_delayed_write(self, _now): + """Handle a delayed write callback.""" + self._unsub_delay_listener = None + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + + async def _async_callback_stop_write(self, _event): + """Handle a write because Home Assistant is stopping.""" + self._unsub_stop_listener = None + self._async_cleanup_delay_listener() + await self._async_handle_write_data() + + async def _async_handle_write_data(self, *_args): + """Handler to handle writing the config.""" + data = self._data + self._data = None + + async with self._write_lock: + try: + await self.hass.async_add_executor_job( + self._write_data, self.path, data) + except (json.SerializationError, json.WriteError) as err: + _LOGGER.error('Error writing config for %s: %s', self.key, err) + + def _write_data(self, path: str, data: Dict): + """Write the data.""" + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + _LOGGER.debug('Writing data for %s', self.key) + json.save_json(path, data) + + async def _async_migrate_func(self, old_version, old_data): + """Migrate to the new version.""" + raise NotImplementedError diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index b2577ff6be6da6..0e53342b0cafb8 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,14 @@ _UNDEFINED = object() +class SerializationError(HomeAssistantError): + """Error serializing the data to JSON.""" + + +class WriteError(HomeAssistantError): + """Error writing the data.""" + + def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \ -> Union[List, Dict]: """Load JSON data from a file and return as dict or list. @@ -41,13 +49,11 @@ def save_json(filename: str, data: Union[List, Dict]): data = json.dumps(data, sort_keys=True, indent=4) with open(filename, 'w', encoding='utf-8') as fdesc: fdesc.write(data) - return True except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) - raise HomeAssistantError(error) + raise SerializationError(error) except OSError as error: _LOGGER.exception('Saving JSON file failed: %s', filename) - raise HomeAssistantError(error) - return False + raise WriteError(error) diff --git a/tests/common.py b/tests/common.py index 556935a6ac173a..56575bdb1e9f2d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,7 +14,7 @@ from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( - intent, entity, restore_state, entity_registry, + intent, entity, restore_state, entity_registry, entity_platform) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util @@ -110,8 +110,6 @@ def stop_hass(): def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant(loop) - hass.config_entries = config_entries.ConfigEntries(hass, {}) - hass.config_entries._entries = [] hass.config.async_load = Mock() store = auth.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}) @@ -137,6 +135,10 @@ def async_add_job(target, *args): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True + hass.config_entries = config_entries.ConfigEntries(hass, {}) + hass.config_entries._entries = [] + hass.config_entries._store._async_ensure_stop_listener = lambda: None + hass.state = ha.CoreState.running # Mock async_start diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py new file mode 100644 index 00000000000000..289d07edab242f --- /dev/null +++ b/tests/helpers/test_storage.py @@ -0,0 +1,158 @@ +"""Tests for the storage helper.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import storage +from homeassistant.util import dt + +from tests.common import async_fire_time_changed, mock_coro + + +MOCK_VERSION = 1 +MOCK_KEY = 'storage-test' +MOCK_DATA = {'hello': 'world'} + + +@pytest.fixture +def mock_save(): + """Fixture to mock JSON save.""" + written = [] + with patch('homeassistant.util.json.save_json', + side_effect=lambda *args: written.append(args)): + yield written + + +@pytest.fixture +def mock_load(mock_save): + """Fixture to mock JSON read.""" + with patch('homeassistant.util.json.load_json', + side_effect=lambda *args: mock_save[-1][1]): + yield + + +@pytest.fixture +def store(hass): + """Fixture of a store that prevents writing on HASS stop.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + store._async_ensure_stop_listener = lambda: None + yield store + + +async def test_loading(hass, store, mock_save, mock_load): + """Test we can save and load data.""" + await store.async_save(MOCK_DATA) + data = await store.async_load() + assert data == MOCK_DATA + + +async def test_loading_non_existing(hass, store): + """Test we can save and load data.""" + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + data = await store.async_load() + assert data == {} + + +async def test_saving_with_delay(hass, store, mock_save): + """Test saving data after a delay.""" + await store.async_save(MOCK_DATA, delay=1) + assert len(mock_save) == 0 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + +async def test_saving_on_stop(hass, mock_save): + """Test delayed saves trigger when we quit Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + await store.async_save(MOCK_DATA, delay=1) + assert len(mock_save) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + +async def test_loading_while_delay(hass, store, mock_save, mock_load): + """Test we load new data even if not written yet.""" + await store.async_save({'delay': 'no'}) + assert len(mock_save) == 1 + + await store.async_save({'delay': 'yes'}, delay=1) + assert len(mock_save) == 1 + + data = await store.async_load() + assert data == {'delay': 'yes'} + + +async def test_writing_while_writing_delay(hass, store, mock_save, mock_load): + """Test a write while a write with delay is active.""" + await store.async_save({'delay': 'yes'}, delay=1) + assert len(mock_save) == 0 + await store.async_save({'delay': 'no'}) + assert len(mock_save) == 1 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + data = await store.async_load() + assert data == {'delay': 'no'} + + +async def test_migrator_no_existing_config(hass, store, mock_save): + """Test migrator with no existing config.""" + with patch('os.path.isfile', return_value=False), \ + patch.object(store, 'async_load', + return_value=mock_coro({'cur': 'config'})): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert data == {'cur': 'config'} + assert len(mock_save) == 0 + + +async def test_migrator_existing_config(hass, store, mock_save): + """Test migrating existing config.""" + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'old': 'config'} + assert len(mock_save) == 1 + assert mock_save[0][1] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } + + +async def test_migrator_transforming_config(hass, store, mock_save): + """Test migrating config to new format.""" + async def old_conf_migrate_func(old_config): + """Migrate old config to new format.""" + return {'new': old_config['old']} + + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store, + old_conf_migrate_func=old_conf_migrate_func) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'new': 'config'} + assert len(mock_save) == 1 + assert mock_save[0][1] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 84bd077154253d..fc0a549f1aebf9 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,13 +1,16 @@ """Test the config manager.""" import asyncio +from datetime import timedelta from unittest.mock import MagicMock, patch, mock_open import pytest from homeassistant import config_entries, loader, data_entry_flow from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from tests.common import MockModule, mock_coro, MockConfigEntry +from tests.common import ( + MockModule, mock_coro, MockConfigEntry, async_fire_time_changed) @pytest.fixture @@ -15,6 +18,7 @@ def manager(hass): """Fixture of a loaded config manager.""" manager = config_entries.ConfigEntries(hass, {}) manager._entries = [] + manager._store._async_ensure_stop_listener = lambda: None hass.config_entries = manager return manager @@ -151,7 +155,9 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" - loader.set_component(hass, 'test', MockModule('test')) + loader.set_component( + hass, 'test', + MockModule('test', async_setup_entry=lambda *args: mock_coro(True))) class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -183,13 +189,12 @@ def async_step_init(self, user_input=None): json_path = 'homeassistant.util.json.open' with patch('homeassistant.config_entries.HANDLERS.get', - return_value=Test2Flow), \ - patch.object(config_entries, 'SAVE_DELAY', 0): + return_value=Test2Flow): yield from hass.config_entries.flow.async_init('test') with patch(json_path, mock_open(), create=True) as mock_write: # To trigger the call_later - yield from asyncio.sleep(0, loop=hass.loop) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) # To execute the save yield from hass.async_block_till_done() @@ -199,7 +204,7 @@ def async_step_init(self, user_input=None): # Now load written data in new config manager manager = config_entries.ConfigEntries(hass, {}) - with patch('os.path.isfile', return_value=True), \ + with patch('os.path.isfile', return_value=False), \ patch(json_path, mock_open(read_data=written), create=True): yield from manager.async_load() From dbae410cf40f3a684ee5500b0691bed4c5c65204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Jun 2018 19:55:03 +0300 Subject: [PATCH 073/169] Fix pylintrc section order and option placements (#15120) --- pylintrc | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pylintrc b/pylintrc index df839b379b549b..d47437cb121823 100644 --- a/pylintrc +++ b/pylintrc @@ -1,6 +1,4 @@ -[MASTER] -reports=no - +[MESSAGES CONTROL] # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - unavoidable @@ -14,9 +12,6 @@ reports=no # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise - -generated-members=botocore.errorfactory - disable= abstract-class-little-used, abstract-class-not-used, @@ -39,9 +34,13 @@ disable= too-many-statements, unused-argument -[EXCEPTIONS] -overgeneral-exceptions=Exception,HomeAssistantError +[REPORTS] +reports=no +[TYPECHECK] # For attrs -[typecheck] ignored-classes=_CountingAttr +generated-members=botocore.errorfactory + +[EXCEPTIONS] +overgeneral-exceptions=Exception,HomeAssistantError From 508d0459a7c188e87176e0ad57bed5d168e11050 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:03:39 -0700 Subject: [PATCH 074/169] Fix #14919. Should throw exception when camera stream closed by frontend (#15028) * Fix #14919. Should throw exception when camera stream closed by frontend * Re-trigger CI * pythonic re-raise --- homeassistant/components/camera/__init__.py | 1 + homeassistant/components/camera/proxy.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ebda09de20cd35..14550dab899d34 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -322,6 +322,7 @@ async def write_to_mjpeg_stream(img_bytes): except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") response = None + raise finally: if response is not None: diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 1984c21fadbb77..447f4e1e56a7b0 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -233,6 +233,7 @@ async def write(img_bytes): _LOGGER.debug("Stream closed by frontend.") req.close() response = None + raise finally: if response is not None: From 42ba2a68ce47b4890328b83cf2ddcaf907380611 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Jun 2018 19:04:07 +0200 Subject: [PATCH 075/169] Revert "Add language to dark sky weather component" (#15142) * Revert "Fix #14919. Should throw exception when camera stream closed by frontend (#15028)" This reverts commit 508d0459a7c188e87176e0ad57bed5d168e11050. * Revert "Fix pylintrc section order and option placements (#15120)" This reverts commit dbae410cf40f3a684ee5500b0691bed4c5c65204. * Revert "Add storage helper and migrate config entries (#15045)" This reverts commit ae51dc08bf5cb947d14b0e55fb64387b31924ac5. * Revert "Add language to dark sky weather component (#15130)" This reverts commit 672a3c7178afc91ac0da015f098dd5f96672e4f2. --- homeassistant/components/weather/darksky.py | 28 +++++---------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 86cc740edbc6bf..f0712542ea544b 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,22 +25,9 @@ ATTRIBUTION = "Powered by Dark Sky" -# Language Supported Codes -LANGUAGE_CODES = [ - 'ar', 'az', 'be', 'bg', 'bs', 'ca', - 'cs', 'da', 'de', 'el', 'en', 'es', - 'et', 'fi', 'fr', 'hr', 'hu', 'id', - 'is', 'it', 'ja', 'ka', 'kw', 'nb', - 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', - 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', - 'x-pig-latin', 'zh', 'zh-tw', -] - CONF_UNITS = 'units' -CONF_LANGUAGE = 'language' DEFAULT_NAME = 'Dark Sky' -DEFAULT_LANGUAGE = 'en' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -48,8 +35,6 @@ vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_LANGUAGE, - default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), }) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -59,15 +44,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky weather.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config[CONF_NAME] - lang = config[CONF_LANGUAGE] - api_key = config[CONF_API_KEY] + name = config.get(CONF_NAME) units = config.get(CONF_UNITS) if not units: units = 'si' if hass.config.units.is_metric else 'us' - dark_sky = DarkSkyData(api_key, latitude, longitude, units, lang) + dark_sky = DarkSkyData( + config.get(CONF_API_KEY), latitude, longitude, units) + add_devices([DarkSkyWeather(name, dark_sky)], True) @@ -147,13 +132,12 @@ def update(self): class DarkSkyData(object): """Get the latest data from Dark Sky.""" - def __init__(self, api_key, latitude, longitude, units, lang): + def __init__(self, api_key, latitude, longitude, units): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.requested_units = units - self.language = lang self.data = None self.currently = None @@ -168,7 +152,7 @@ def update(self): try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, - units=self.requested_units, lang=self.language) + units=self.requested_units) self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() From 6c0fc65eaf66cea8f697e40823a39ae556aa639e Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:04:32 -0700 Subject: [PATCH 076/169] Bump python-nest to 4.0.3 (#15098) Resolve network reconnect issue --- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/sensor/nest.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bd74897371ad05..f9507b6ec7b1be 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -23,7 +23,7 @@ from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.2'] +REQUIREMENTS = ['python-nest==4.0.3'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest): _LOGGER.debug("dispatching nest data update") async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) else: + _LOGGER.debug("stop listening nest.update_event") return diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index bf1b3f65c4a9fc..7afd3b762b3a50 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -133,7 +133,8 @@ def update(self): elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility - self._state = getattr(self.device, self.variable).capitalize() + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None else: self._state = getattr(self.device, self.variable) diff --git a/requirements_all.txt b/requirements_all.txt index 74ff928680392d..78fda284b17775 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6813378b12f0dd..04952b75b816ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -156,7 +156,7 @@ pyqwikswitch==0.8 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From b92350fb5588a05a870495753a0c0b3c69d12bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Jun 2018 20:05:07 +0300 Subject: [PATCH 077/169] Lint cleanup (#15103) * Remove unneeded inline pylint disables * Remove unneeded noqa's * Use symbol names instead of message ids in inline pylint disables --- homeassistant/bootstrap.py | 1 - .../alarm_control_panel/__init__.py | 1 - homeassistant/components/alexa/smart_home.py | 4 ---- homeassistant/components/api.py | 1 - homeassistant/components/bbb_gpio.py | 19 ++++++++----------- .../components/binary_sensor/__init__.py | 1 - .../binary_sensor/bmw_connected_drive.py | 6 +++--- .../components/binary_sensor/gc100.py | 1 - .../components/binary_sensor/isy994.py | 2 +- .../components/binary_sensor/rpi_gpio.py | 1 - .../components/binary_sensor/wemo.py | 1 - homeassistant/components/calendar/google.py | 4 +--- homeassistant/components/camera/xeoma.py | 2 -- homeassistant/components/climate/__init__.py | 1 - .../components/climate/eq3btsmart.py | 2 +- .../components/climate/generic_thermostat.py | 2 -- homeassistant/components/climate/heatmiser.py | 1 - homeassistant/components/climate/mqtt.py | 2 -- homeassistant/components/climate/zwave.py | 1 - homeassistant/components/cover/__init__.py | 1 - homeassistant/components/cover/demo.py | 1 - homeassistant/components/cover/garadget.py | 1 - homeassistant/components/cover/isy994.py | 2 +- homeassistant/components/cover/opengarage.py | 1 - homeassistant/components/cover/zwave.py | 1 - .../components/device_tracker/cisco_ios.py | 1 - .../components/device_tracker/gpslogger.py | 2 +- .../components/device_tracker/linksys_ap.py | 1 - .../components/device_tracker/snmp.py | 3 --- .../components/device_tracker/tplink.py | 5 ----- homeassistant/components/ecobee.py | 1 - homeassistant/components/gc100.py | 2 +- homeassistant/components/google.py | 2 +- .../components/google_assistant/__init__.py | 5 ++--- .../components/google_assistant/auth.py | 7 +++---- .../components/google_assistant/http.py | 4 ++-- .../components/google_assistant/smart_home.py | 2 +- .../components/homekit_controller/__init__.py | 1 - homeassistant/components/http/static.py | 1 - .../components/image_processing/opencv.py | 2 -- homeassistant/components/ios.py | 2 -- homeassistant/components/isy994.py | 5 ++--- homeassistant/components/keyboard_remote.py | 1 - homeassistant/components/konnected.py | 2 +- homeassistant/components/lametric.py | 1 - homeassistant/components/light/__init__.py | 2 -- homeassistant/components/light/avion.py | 6 +++--- homeassistant/components/light/blinkt.py | 2 +- homeassistant/components/light/decora.py | 2 +- homeassistant/components/light/decora_wifi.py | 2 +- .../components/light/limitlessled.py | 2 +- homeassistant/components/light/zwave.py | 2 -- homeassistant/components/lirc.py | 2 +- homeassistant/components/lock/__init__.py | 1 - homeassistant/components/lock/isy994.py | 2 +- homeassistant/components/lock/sesame.py | 2 +- homeassistant/components/lock/zwave.py | 2 -- homeassistant/components/logger.py | 1 - .../components/media_player/__init__.py | 1 - homeassistant/components/media_player/cast.py | 1 - homeassistant/components/media_player/demo.py | 1 - .../components/media_player/denonavr.py | 1 - homeassistant/components/media_player/plex.py | 1 - .../components/media_player/universal.py | 1 - homeassistant/components/modbus.py | 1 - homeassistant/components/notify/aws_lambda.py | 1 - homeassistant/components/notify/aws_sns.py | 1 - homeassistant/components/notify/aws_sqs.py | 1 - homeassistant/components/notify/ciscospark.py | 1 - homeassistant/components/notify/ecobee.py | 2 +- homeassistant/components/notify/html5.py | 1 - .../components/notify/joaoapps_join.py | 1 - homeassistant/components/notify/lametric.py | 2 -- homeassistant/components/notify/pushover.py | 1 - homeassistant/components/notify/slack.py | 1 - homeassistant/components/octoprint.py | 1 - .../components/remote/xiaomi_miio.py | 1 - homeassistant/components/rfxtrx.py | 2 -- homeassistant/components/rpi_gpio.py | 1 - homeassistant/components/satel_integra.py | 1 - homeassistant/components/sensor/bitcoin.py | 1 - homeassistant/components/sensor/buienradar.py | 5 +---- homeassistant/components/sensor/cpuspeed.py | 1 - homeassistant/components/sensor/cups.py | 2 +- .../components/sensor/dwd_weather_warnings.py | 2 -- homeassistant/components/sensor/dweet.py | 1 - homeassistant/components/sensor/envirophat.py | 2 -- homeassistant/components/sensor/glances.py | 1 - homeassistant/components/sensor/gpsd.py | 1 - homeassistant/components/sensor/isy994.py | 2 +- homeassistant/components/sensor/kira.py | 1 - homeassistant/components/sensor/lastfm.py | 1 - homeassistant/components/sensor/loopenergy.py | 1 - homeassistant/components/sensor/mfi.py | 1 - homeassistant/components/sensor/pvoutput.py | 1 - homeassistant/components/sensor/skybeacon.py | 3 +-- homeassistant/components/sensor/sma.py | 3 +-- .../components/sensor/steam_online.py | 1 - homeassistant/components/sensor/tado.py | 1 - homeassistant/components/sensor/ted5000.py | 1 - .../components/sensor/wirelesstag.py | 2 +- homeassistant/components/sensor/yr.py | 2 +- homeassistant/components/sensor/zwave.py | 2 -- homeassistant/components/switch/__init__.py | 1 - .../components/switch/anel_pwrctrl.py | 1 - homeassistant/components/switch/flux.py | 1 - homeassistant/components/switch/fritzdect.py | 2 +- homeassistant/components/switch/gc100.py | 1 - homeassistant/components/switch/isy994.py | 4 ++-- homeassistant/components/switch/mfi.py | 1 - homeassistant/components/switch/rpi_rf.py | 2 +- homeassistant/components/switch/wemo.py | 1 - homeassistant/components/switch/zwave.py | 2 -- homeassistant/components/vacuum/demo.py | 1 - homeassistant/components/vacuum/mqtt.py | 1 - homeassistant/components/vera.py | 1 - homeassistant/components/weather/__init__.py | 1 - homeassistant/components/websocket_api.py | 1 - homeassistant/components/wemo.py | 1 - homeassistant/components/wirelesstag.py | 2 +- homeassistant/components/zoneminder.py | 3 --- homeassistant/components/zwave/__init__.py | 1 - homeassistant/components/zwave/const.py | 1 - homeassistant/helpers/__init__.py | 3 +-- homeassistant/helpers/aiohttp_client.py | 1 - homeassistant/helpers/entity.py | 2 -- homeassistant/helpers/entity_registry.py | 2 -- homeassistant/helpers/event.py | 1 - homeassistant/remote.py | 1 - homeassistant/scripts/check_config.py | 3 +-- homeassistant/util/__init__.py | 1 - homeassistant/util/async_.py | 2 -- homeassistant/util/color.py | 13 ++----------- homeassistant/util/dt.py | 1 - homeassistant/util/location.py | 2 +- homeassistant/util/yaml.py | 3 +-- script/lazytox.py | 2 -- 137 files changed, 58 insertions(+), 209 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b108ac805e9f67..0a71c2887b13d8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any], components.update(hass.config_entries.async_domains()) # setup components - # pylint: disable=not-an-iterable res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 25e303cbe853c3..f81d2ef1037cd6 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -154,7 +154,6 @@ def async_alarm_service_handler(service): return True -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index c5c68f1af40fa7..ff2d4adf30dc38 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -107,7 +107,6 @@ class _DisplayCategory(object): THERMOSTAT = "THERMOSTAT" # Indicates the endpoint is a television. - # pylint: disable=invalid-name TV = "TV" @@ -1474,9 +1473,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity): mode = mode if isinstance(mode, str) else mode['value'] operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) - # Work around a pylint false positive due to - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return ha_mode = next( (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ae89e2fc3b62c1..b80a571606161e 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView): async def get(self, request): """Provide a streaming interface for the event bus.""" - # pylint: disable=no-self-use hass = request.app['hass'] stop_obj = object() to_write = asyncio.Queue(loop=hass.loop) diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py index 5d3954b4c87e21..f932f239969167 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio.py @@ -16,7 +16,6 @@ DOMAIN = 'bbb_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error @@ -34,41 +33,39 @@ def prepare_gpio(event): return True -# noqa: F821 - def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO - GPIO.setup(pin, GPIO.IN, # noqa: F821 - GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821 - else GPIO.PUD_UP) # noqa: F821 + GPIO.setup(pin, GPIO.IN, + GPIO.PUD_DOWN if pull_mode == 'DOWN' + else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.add_event_detect( pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d72211d5ad1e1e..26878044fe28ad 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -# pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index e214610f46dfe7..308298d1bcd103 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -124,11 +124,11 @@ def device_state_attributes(self): result['check_control_messages'] = check_control_messages elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value - # pylint: disable=W0212 + # pylint: disable=protected-access result['last_charging_end_result'] = \ vehicle_state._attributes['lastChargingEndResult'] if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access result['connection_status'] = \ vehicle_state._attributes['connectionStatus'] @@ -166,7 +166,7 @@ def update(self): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/binary_sensor/gc100.py index 767be2874e6ab8..515d7e7123d4ad 100644 --- a/homeassistant/components/binary_sensor/gc100.py +++ b/homeassistant/components/binary_sensor/gc100.py @@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice): def __init__(self, name, port_addr, gc100): """Initialize the GC100 binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a80e4db747d505..deaa118f51cf43 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -8,7 +8,7 @@ import asyncio import logging from datetime import timedelta -from typing import Callable # noqa +from typing import Callable from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index e1e06ce57b9612..4072f4ae23490e 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice): def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port = port self._pull_mode = pull_mode diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index d3c78597c70bc7..e6eff0d9bb5bca 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -13,7 +13,6 @@ _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Register discovered WeMo binary sensors.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index da76530a36d634..87893125e6f74a 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -89,9 +89,7 @@ async def async_get_events(self, hass, start_date, end_date): params['timeMin'] = start_date.isoformat('T') params['timeMax'] = end_date.isoformat('T') - # pylint: disable=no-member events = await hass.async_add_job(service.events) - # pylint: enable=no-member result = await hass.async_add_job(events.list(**params).execute) items = result.get('items', []) @@ -111,7 +109,7 @@ def update(self): service, params = self._prepare_query() params['timeMin'] = dt.now().isoformat('T') - events = service.events() # pylint: disable=no-member + events = service.events() result = events.list(**params).execute() items = result.get('items', []) diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index cec04b52047b8e..2a4d15268180c5 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices, ] for cam in config.get(CONF_CAMERAS, []): - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return camera = next( (dc for dc in discovered_cameras if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a47edc5af42632..9584422e2b41c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry): class ClimateDevice(Entity): """Representation of a climate device.""" - # pylint: disable=no-self-use @property def state(self): """Return the current state.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 820e715b00d118..10fd879e386296 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of an eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 030a76626c6e2a..3f1d9a208ac5fd 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -263,7 +263,6 @@ def async_set_temperature(self, **kwargs): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member if self._min_temp: return self._min_temp @@ -273,7 +272,6 @@ def min_temp(self): @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member if self._max_temp: return self._max_temp diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index 19c033a319f5bf..92e363228a8b84 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -34,7 +34,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the heatmiser thermostat.""" from heatmiserV3 import heatmiser, connection diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 2878717d91b2cf..fbe5460979b099 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -638,11 +638,9 @@ def supported_features(self): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member return self._max_temp diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 1eec9c82f3ca9a..c87d1507e921ec 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/climate.zwave/ """ # Because we do not compile openzwave on CI -# pylint: disable=import-error import logging from homeassistant.components.climate import ( DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e4c8f5634cf4a9..f5d3d798e2eb6b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -198,7 +198,6 @@ async def async_handle_cover_service(service): class CoverDevice(Entity): """Representation a cover.""" - # pylint: disable=no-self-use @property def current_cover_position(self): """Return current position of cover. diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 70e681f11207fb..b1533bd68c8a09 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoCover(CoverDevice): """Representation of a demo cover.""" - # pylint: disable=no-self-use def __init__(self, hass, name, position=None, tilt_position=None, device_class=None, supported_features=None): """Initialize the cover.""" diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index c19aa69c8f04bb..70f6956810984f 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class GaradgetCover(CoverDevice): """Representation of a Garadget cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.particle_url = 'https://api.particle.io' diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 743a36d41d5084..0ccfe267989e71 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/cover.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 028a7a0c9fc8ad..fe6c7763cc7779 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenGarageCover(CoverDevice): """Representation of a OpenGarage cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.opengarage_url = 'http://{}:{}'.format( diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 6f4a11684bde61..c29c11c5b6bffb 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def __init__(self, hass, values, invert_buttons): """Initialize the Z-Wave rollershutter.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) - # pylint: disable=no-member self._network = hass.data[zwave.const.DATA_NETWORK] self._open_id = None self._close_id = None diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py index 0978ba99593e65..c13f622c5bf101 100644 --- a/homeassistant/components/device_tracker/cisco_ios.py +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -50,7 +50,6 @@ def __init__(self, config): self.success_init = self._update_info() _LOGGER.info('cisco_ios scanner initialized') - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 68ea9ac88ae819..6336ba51d23dd8 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -7,7 +7,7 @@ import logging from hmac import compare_digest -from aiohttp.web import Request, HTTPUnauthorized # NOQA +from aiohttp.web import Request, HTTPUnauthorized import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 8837b628b32650..bf3916f3abe0ed 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -61,7 +61,6 @@ def scan_devices(self): return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """ Return the name (if known) of the device. diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 3d57cb108e243c..6a849d0b05abfd 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -74,8 +74,6 @@ def scan_devices(self): return [client['mac'] for client in self.last_results if client.get('mac')] - # Suppressing no-self-use warning - # pylint: disable=R0201 def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # We have no names @@ -106,7 +104,6 @@ def get_snmp_data(self): if errindication: _LOGGER.error("SNMPLIB error: %s", errindication) return - # pylint: disable=no-member if errstatus: _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[int(errindex) - 1][0] or '?') diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 6c5fb697c072da..5266b9c6f574b3 100644 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -68,7 +68,6 @@ def scan_devices(self): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None @@ -103,7 +102,6 @@ def scan_devices(self): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return self.last_results.get(device) @@ -164,7 +162,6 @@ def scan_devices(self): self._log_out() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device. @@ -273,7 +270,6 @@ def scan_devices(self): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get the name of the wireless device.""" return None @@ -349,7 +345,6 @@ def scan_devices(self): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 22348dcc297abb..96f094b527dfdf 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -105,7 +105,6 @@ def setup(hass, config): Will automatically load thermostat and sensor components to support devices discovered on the network. """ - # pylint: disable=import-error global NETWORK if 'ecobee' in _CONFIGURING: diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100.py index bc627d4441796d..25bcb5b0f79f55 100644 --- a/homeassistant/components/gc100.py +++ b/homeassistant/components/gc100.py @@ -31,7 +31,7 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=no-member, import-self +# pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" import gc100 diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index b41d4ea33a20b1..203b1a94b7f9e8 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -197,7 +197,7 @@ def _found_calendar(call): def _scan_for_calendars(service): """Scan for new calendars.""" service = calendar_service.get() - cal_list = service.calendarList() # pylint: disable=no-member + cal_list = service.calendarList() calendars = cal_list.list().execute()['items'] for calendar in calendars: calendar['track'] = track_new_found_calendars diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 1c6d11a7c99216..567a6d842339ca 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -13,9 +13,8 @@ import voluptuous as vol # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports -from homeassistant.core import HomeAssistant # NOQA -from typing import Dict, Any # NOQA +from homeassistant.core import HomeAssistant +from typing import Dict, Any from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index a21dd0e673859d..e80b2282066b73 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -3,12 +3,11 @@ import logging # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports # if False: -from aiohttp.web import Request, Response # NOQA -from typing import Dict, Any # NOQA +from aiohttp.web import Request, Response +from typing import Dict, Any -from homeassistant.core import HomeAssistant # NOQA +from homeassistant.core import HomeAssistant from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( HTTP_BAD_REQUEST, diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0ea5f7d9fa4379..65079a1a26e754 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -7,10 +7,10 @@ import logging from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports +# pylint: disable=unused-import from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback # NOQA from homeassistant.helpers.entity import Entity # NOQA diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 27d993aee76abd..f20d4f747cceb8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -4,7 +4,7 @@ import logging # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports +# pylint: disable=unused-import # if False: from aiohttp.web import Request, Response # NOQA from typing import Dict, Tuple, Any, Optional # NOQA diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ff981c1607a8fe..34fdcb2c035e86 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -226,7 +226,6 @@ def put_characteristics(self, characteristics): self._securecon.put('/characteristics', body) -# pylint: too-many-function-args def setup(hass, config): """Set up for Homekit devices.""" def discovery_dispatch(service, discovery_info): diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 3fbaf703d06798..cd07ab6df6916c 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -18,7 +18,6 @@ async def _handle(self, request): filename = URL(request.match_info['filename']).path try: # PyLint is wrong about resolve not being a member. - # pylint: disable=no-member filepath = self._directory.joinpath(filename).resolve() if not self._follow_symlinks: filepath.relative_to(self._directory) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e01131c7d1b3f7..ca0f3527f73491 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -152,7 +152,6 @@ def process_image(self, image): import cv2 # pylint: disable=import-error import numpy - # pylint: disable=no-member cv_image = cv2.imdecode( numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) @@ -168,7 +167,6 @@ def process_image(self, image): else: path = classifier - # pylint: disable=no-member cascade = cv2.CascadeClassifier(path) detections = cascade.detectMultiScale( diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 249f147847c08b..7f7377469fd3cb 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -181,7 +181,6 @@ def devices_with_push(): def enabled_push_ids(): """Return a list of push enabled target push IDs.""" push_ids = list() - # pylint: disable=unused-variable for device in CONFIG_FILE[ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) @@ -203,7 +202,6 @@ def device_name_for_push_id(push_id): def setup(hass, config): """Set up the iOS component.""" - # pylint: disable=import-error global CONFIG_FILE global CONFIG_FILE_PATH diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 90ab41cf98b7e0..d8afb7be5dae13 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -11,12 +11,12 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant # noqa +from homeassistant.core import HomeAssistant from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict # noqa +from homeassistant.helpers.typing import ConfigType, Dict REQUIREMENTS = ['PyISY==1.1.0'] @@ -268,7 +268,6 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str)-> None: """Sort the nodes to their proper domains.""" - # pylint: disable=no-member for (path, node) in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index af45bd3d4f983d..bbd7bc4408235f 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -151,7 +151,6 @@ def run(self): if not event: continue - # pylint: disable=no-member if event.type is ecodes.EV_KEY and event.value is self.key_value: _LOGGER.debug(categorize(event)) self.hass.bus.fire( diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 5b28b7b0999740..26fe356d77247f 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -10,7 +10,7 @@ import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py index 49b4f73ea17e3f..96ea3781566cd1 100644 --- a/homeassistant/components/lametric.py +++ b/homeassistant/components/lametric.py @@ -31,7 +31,6 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=broad-except def setup(hass, config): """Set up the LaMetricManager.""" _LOGGER.debug("Setting up LaMetric platform") diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 30a1a800a44988..b8a97607215566 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -446,8 +446,6 @@ def get(cls, name): class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use - @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index b4b9f4e777567c..be608ea477668b 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Avion switch.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion lights = [] @@ -70,7 +70,7 @@ class AvionLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion self._name = device['name'] @@ -117,7 +117,7 @@ def assumed_state(self): def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion # Bluetooth LE is unreliable, and the connection may drop at any diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index 97edd7c54d254e..7035320945a0ef 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Blinkt Light platform.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import blinkt # ensure that the lights are off when exiting diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index c7478b435ee3e0..85d9180c59bcae 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -75,7 +75,7 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import decora self._name = device['name'] diff --git a/homeassistant/components/light/decora_wifi.py b/homeassistant/components/light/decora_wifi.py index 111d39f20190ac..17003d51610c13 100644 --- a/homeassistant/components/light/decora_wifi.py +++ b/homeassistant/components/light/decora_wifi.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-member, no-name-in-module + # pylint: disable=import-error, no-name-in-module from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residential_account import ResidentialAccount diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bd4fece89e339c..71d3f9d95d7177 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -136,7 +136,7 @@ def state(new_state): """ def decorator(function): """Set up the decorator function.""" - # pylint: disable=no-member,protected-access + # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" from limitlessled.pipeline import Pipeline diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 04216780c80252..3bfa167f8eca20 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -6,8 +6,6 @@ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from threading import Timer from homeassistant.components.light import ( ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py index 0cd49ab6c9a322..d7ec49e00968fb 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc.py @@ -4,7 +4,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/lirc/ """ -# pylint: disable=import-error,no-member +# pylint: disable=no-member import threading import time import logging diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b3e4ac8f0ff6a7..f03d028a38f1ee 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -145,7 +145,6 @@ def changed_by(self): """Last change triggered by.""" return None - # pylint: disable=no-self-use @property def code_format(self): """Regex for code format or None if no code is required.""" diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 79e4308dbda114..9bcf5a86d08ecc 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/lock.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.lock import LockDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 09f7266d15c6a1..8d9c05e3f26d71 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -4,7 +4,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/lock.sesame/ """ -from typing import Callable # noqa +from typing import Callable import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 8f39d440caed8e..b7bc9f15e19953 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -4,8 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.zwave/ """ -# Because we do not compile openzwave on CI -# pylint: disable=import-error import asyncio import logging diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index daaffd0174c7ac..0baca2f341c8c0 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -55,7 +55,6 @@ def set_level(hass, logs): class HomeAssistantLogFilter(logging.Filter): """A log filter.""" - # pylint: disable=no-init def __init__(self, logfilter): """Initialize the filter.""" super().__init__() diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d963deba7b55e7..d314dec65ea967 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -471,7 +471,6 @@ class MediaPlayerDevice(Entity): _access_token = None - # pylint: disable=no-self-use # Implement these for your media player @property def state(self): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index eced0dbbe25bb2..be7b635f8635d7 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ -# pylint: disable=import-error import logging import threading from typing import Optional, Tuple diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 405c220c8770a3..9edf69cd9c69ef 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -295,7 +295,6 @@ def media_artist(self): @property def media_album_name(self): """Return the album of current playing media (Music track only).""" - # pylint: disable=no-self-use return "Bounzz" @property diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 8cd47476058f35..ff0e4d907b11e5 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -61,7 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Denon platform.""" - # pylint: disable=import-error import denonavr # Initialize list with receivers to be started diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 6690382846fd15..ca6b9722a496a0 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -747,7 +747,6 @@ def media_previous_track(self): if self.device and 'playback' in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - # pylint: disable=W0613 def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if not (self.device and diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 03f847ae40c19e..66d12190320f4b 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/media_player.universal/ """ import logging -# pylint: disable=import-error from copy import copy import voluptuous as vol diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index fe46c858b5119f..fc6db96e029b03 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -75,7 +75,6 @@ def setup(hass, config): """Set up Modbus component.""" # Modbus connection type - # pylint: disable=import-error client_type = config[DOMAIN][CONF_TYPE] # Connect to Modbus network diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index b0cc4a0121d5f9..46ac2f89d33cbf 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -44,7 +44,6 @@ def get_service(hass, config, discovery_info=None): context_b64 = base64.b64encode(context_str.encode('utf-8')) context = context_b64.decode('utf-8') - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index c94e3abaa96fca..7ecf5a7cc7f88c 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -35,7 +35,6 @@ def get_service(hass, config, discovery_info=None): """Get the AWS SNS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index 43c04ed16d055b..30b673846e7f96 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -34,7 +34,6 @@ def get_service(hass, config, discovery_info=None): """Get the AWS SQS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/ciscospark.py b/homeassistant/components/notify/ciscospark.py index 0bf184023d7af6..e83e0e9024fff8 100644 --- a/homeassistant/components/notify/ciscospark.py +++ b/homeassistant/components/notify/ciscospark.py @@ -25,7 +25,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the CiscoSpark notification service.""" return CiscoSparkNotificationService( diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py index c718149b4b553b..31e4c4751c8000 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/notify/ecobee.py @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import ecobee from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) # NOQA + BaseNotificationService, PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 7ccf4f8db9066f..7529608387d86b 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -413,7 +413,6 @@ def send_message(self, message="", **kwargs): json.dumps(payload), gcm_key=gcm_key, ttl='86400' ) - # pylint: disable=no-member if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py index e391d6559e5537..a75ff9cd165b7c 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/notify/joaoapps_join.py @@ -28,7 +28,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Join notification service.""" api_key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f6c3e152b0a3fd..0cc3a0213b3f00 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -36,7 +36,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the LaMetric notification service.""" hlmn = hass.data.get(LAMETRIC_DOMAIN) @@ -59,7 +58,6 @@ def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): self._priority = priority self._devices = [] - # pylint: disable=broad-except def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" from lmnotify import SimpleFrame, Sound, Model diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index cd73bbba4bfe8f..3ec0b27e7c4e09 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -26,7 +26,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" from pushover import InitError diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index b50260e4c613b4..d4c5a196a3fbbe 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -44,7 +44,6 @@ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" import slacker diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 5caaa1b372d701..c1059227f7a722 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -144,7 +144,6 @@ def update(self, sensor_type, end_point, group, tool=None): return response -# pylint: disable=unused-variable def get_value_from_json(json_dict, sensor_type, group, tool): """Return the value for sensor_type from the JSON.""" if group not in json_dict: diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 8a3e51b55b32b7..59a2dc861a62ab 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -229,7 +229,6 @@ def device_state_attributes(self): return {'hidden': 'true'} return - # pylint: disable=R0201 @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2f170a206461fc..afe777ff7ccfdf 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -162,7 +162,6 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -# pylint: disable=unused-variable def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for device in RFX_DEVICES.values(): @@ -176,7 +175,6 @@ def get_pt2262_device(device_id): return None -# pylint: disable=unused-variable def find_possible_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for dev_id, device in RFX_DEVICES.items(): diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index dfc60b5e45ee7a..5cb7bb337ce9ab 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -17,7 +17,6 @@ DOMAIN = 'rpi_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the Raspberry PI GPIO component.""" import RPi.GPIO as GPIO diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 4b61ff15c08579..4247855da39d60 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/satel_integra/ """ -# pylint: disable=invalid-name import asyncio import logging diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 38d2226012c8ae..bd23b9850f773f 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -121,7 +121,6 @@ def update(self): stats = self.data.stats ticker = self.data.ticker - # pylint: disable=no-member if self.type == 'exchangerate': self._state = ticker[self._currency].p15min self._unit_of_measurement = self._currency diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 590d5a8f1ceb46..10a96ded43739e 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -287,7 +287,6 @@ def load_data(self, data): img = condition.get(IMAGE, None) - # pylint: disable=protected-access if new_state != self._state or img != self._entity_picture: self._state = new_state self._entity_picture = img @@ -299,12 +298,10 @@ def load_data(self, data): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - # pylint: disable=protected-access self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) return True # update all other sensors - # pylint: disable=protected-access self._state = data.get(self.type) return True @@ -329,7 +326,7 @@ def state(self): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index c39ae43aef0ad5..c6a7106663f699 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -30,7 +30,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CPU speed sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py index 7c1d9fc3d49cd8..6d55853d724221 100644 --- a/homeassistant/components/sensor/cups.py +++ b/homeassistant/components/sensor/cups.py @@ -128,7 +128,7 @@ def update(self): self._printer = self.data.printers.get(self._name) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=no-name-in-module class CupsData(object): """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/sensor/dwd_weather_warnings.py b/homeassistant/components/sensor/dwd_weather_warnings.py index 9105e30eb422d1..e023dfcc49ff09 100644 --- a/homeassistant/components/sensor/dwd_weather_warnings.py +++ b/homeassistant/components/sensor/dwd_weather_warnings.py @@ -95,7 +95,6 @@ def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._var_units - # pylint: disable=no-member @property def state(self): """Return the state of the device.""" @@ -104,7 +103,6 @@ def state(self): except TypeError: return self._api.data[self._var_id] - # pylint: disable=no-member @property def device_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index 157f366c0c40c1..cca06bd9782d33 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -34,7 +34,6 @@ }) -# pylint: disable=unused-variable, too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dweet sensor.""" import dweepy diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index b11dae8e1682e7..265350f3e95104 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -55,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sense HAT sensor platform.""" try: - # pylint: disable=import-error import envirophat except OSError: _LOGGER.error("No Enviro pHAT was found.") @@ -175,7 +174,6 @@ def update(self): self.light_red, self.light_green, self.light_blue = \ self.envirophat.light.rgb() if self.use_leds: - # pylint: disable=no-value-for-parameter self.envirophat.leds.off() # accelerometer readings in G diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 4fed3793c50c00..bd6e91c7b531b0 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -56,7 +56,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Glances sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 472dd1d70f6cb8..1d270419933a18 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -86,7 +86,6 @@ def name(self): """Return the name.""" return self._name - # pylint: disable=no-member @property def state(self): """Return the state of GPSD.""" diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index ca8c19bbc7a193..1048c04d43dc13 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/sensor.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.sensor import DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py index 74a1bd19d34428..19566100f99534 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/sensor/kira.py @@ -18,7 +18,6 @@ CONF_SENSOR = 'sensor' -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Kira sensor.""" if discovery_info is not None: diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index ee9ab146c87fcb..6ee3f7d16d0835 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -68,7 +68,6 @@ def state(self): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" self._cover = self._user.get_image() diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index 09ed4ab3d4988b..d888a6c634d655 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) - # pylint: disable=too-many-function-args controller = pyloopenergy.LoopEnergy( elec_config.get(CONF_ELEC_SERIAL), elec_config.get(CONF_ELEC_SECRET), diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index f6bec3284c341f..ab6bd8270ce18c 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -49,7 +49,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index 26c3e27bba51ba..d4307d50228691 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -64,7 +64,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([PvoutputSensor(rest, name)], True) -# pylint: disable=no-member class PvoutputSensor(Entity): """Representation of a PVOutput sensor.""" diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 53cbaab19a58eb..2731587ed710e3 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) _LOGGER.debug("Setting up...") @@ -139,7 +138,7 @@ def __init__(self, hass, mac, name): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error, no-name-in-module, no-member + # pylint: disable=import-error import pygatt from pygatt.backends import Characteristic from pygatt.exceptions import ( diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 3451789424b08c..2be46da0bdb01c 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -198,5 +198,4 @@ def async_update_values(self, key_values): update = True self._state = new_state - return self.async_update_ha_state() if update else None \ - # pylint: disable=protected-access + return self.async_update_ha_state() if update else None diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index e22e1594b55479..7521b74cd28e43 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -76,7 +76,6 @@ def state(self): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" try: diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index ff8ad7fe8496f5..737b3d08368922 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -147,7 +147,6 @@ def update(self): unit = TEMP_CELSIUS - # pylint: disable=R0912 if self.zone_variable == 'temperature': if 'sensorDataPoints' in data: sensor_data = data['sensorDataPoints'] diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 55d520cf6ca67f..c2ef1d4c6b90b3 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -32,7 +32,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Ted5000 sensor.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py index c93da3c791f107..ad2115e9bd30c5 100755 --- a/homeassistant/components/sensor/wirelesstag.py +++ b/homeassistant/components/sensor/wirelesstag.py @@ -168,7 +168,7 @@ def _update_tag_info_callback(self, event): new_value = event.data.get('cap') elif self._sensor_type == SENSOR_LIGHT: new_value = event.data.get('lux') - except Exception as error: # pylint: disable=W0703 + except Exception as error: # pylint: disable=broad-except _LOGGER.info("Unable to update value of entity: \ %s error: %s event: %s", self, error, event) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 88c23771bd4c39..c7ff967723b68f 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -117,7 +117,7 @@ def state(self): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index fe295d84d4991b..b2a913c2af8b0c 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -5,8 +5,6 @@ at https://home-assistant.io/components/sensor.zwave/ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.sensor import DOMAIN from homeassistant.components import zwave from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 9a35198628a649..bab2abbad0d83c 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -135,7 +135,6 @@ async def async_handle_switch_service(service): class SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use @property def current_power_w(self): """Return the current power usage in W.""" diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 30739676f17950..4e62b711979558 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -33,7 +33,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up PwrCtrl devices/switches.""" host = config.get(CONF_HOST, None) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index f57843cdaa0f7f..7df8f0e1aa620c 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -218,7 +218,6 @@ def flux_update(self, now=None): else: sunset_time = sunset - # pylint: disable=no-member night_length = int(stop_time.timestamp() - sunset_time.timestamp()) seconds_from_sunset = int(now.timestamp() - diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py index 58ad745a2d2dd8..9968f631260d97 100644 --- a/homeassistant/components/switch/fritzdect.py +++ b/homeassistant/components/switch/fritzdect.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fritz = FritzBox(host, username, password) try: fritz.login() - except Exception: # pylint: disable=W0703 + except Exception: # pylint: disable=broad-except _LOGGER.error("Login to Fritz!Box failed") return diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py index 54c3b5e942aeae..34a29483d3cceb 100644 --- a/homeassistant/components/switch/gc100.py +++ b/homeassistant/components/switch/gc100.py @@ -39,7 +39,6 @@ class GC100Switch(ToggleEntity): def __init__(self, name, port_addr, gc100): """Initialize the GC100 switch.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 3d29c53bd7cb08..2a7dee87747db7 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -5,12 +5,12 @@ https://home-assistant.io/components/switch.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.switch import SwitchDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, ISYDevice) -from homeassistant.helpers.typing import ConfigType # noqa +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/mfi.py b/homeassistant/components/switch/mfi.py index c0dc72440d3308..2c547fa210f1b2 100644 --- a/homeassistant/components/switch/mfi.py +++ b/homeassistant/components/switch/mfi.py @@ -39,7 +39,6 @@ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 62c92ad2d968c6..03f11de21f708c 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -44,7 +44,7 @@ }) -# pylint: disable=import-error, no-member +# pylint: disable=no-member def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" import rpi_rf diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 569566bcbfb6f4..c18ad492d40ae3 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -33,7 +33,6 @@ WEMO_STANDBY = 8 -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up discovered WeMo switches.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 3b82d87d7e75a3..8a0a1683aa41aa 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -6,8 +6,6 @@ """ import logging import time -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components import zwave from homeassistant.components.zwave import workaround, async_setup_platform # noqa # pylint: disable=unused-import diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index bd501167ffa8d7..45fd8de269612e 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoVacuum(VacuumDevice): """Representation of a demo vacuum.""" - # pylint: disable=no-self-use def __init__(self, name, supported_features): """Initialize the vacuum.""" self._name = name diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index ef3bb0f636b7ef..8c2f110257f7b0 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -210,7 +210,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" - # pylint: disable=no-self-use def __init__( self, name, supported_features, qos, retain, command_topic, payload_turn_on, payload_turn_off, payload_return_to_base, diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index cbbf279bb8c8d8..0ab5e7ce39aafe 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -53,7 +53,6 @@ ] -# pylint: disable=too-many-function-args def setup(hass, base_config): """Set up for Vera devices.""" import pyvera as veraApi diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c36c960c4fcf4f..a43999f2276255 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -46,7 +46,6 @@ def async_setup(hass, config): return True -# pylint: disable=no-member, no-self-use class WeatherEntity(Entity): """ABC for weather data.""" diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index e16e5524f95456..aacef4547b7670 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -228,7 +228,6 @@ class WebsocketAPIView(HomeAssistantView): async def get(self, request): """Handle an incoming websocket connection.""" - # pylint: disable=no-self-use return await ActiveConnection(request.app['hass'], request).handle() diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index d38a42e2cbf891..e8c7db5efe106d 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -44,7 +44,6 @@ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-function-args def setup(hass, config): """Set up for WeMo devices.""" import pywemo diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py index 9fabcb1cd5aefb..0f8f47f5100c6d 100644 --- a/homeassistant/components/wirelesstag.py +++ b/homeassistant/components/wirelesstag.py @@ -146,7 +146,7 @@ def handle_binary_event(self, event): self.hass, SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), event) - except Exception as ex: # pylint: disable=W0703 + except Exception as ex: # pylint: disable=broad-except _LOGGER.error("Unable to handle binary event:\ %s error: %s", str(event), str(ex)) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 86531401774198..471c1c6e82cad4 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -67,7 +67,6 @@ def setup(hass, config): return login() -# pylint: disable=no-member def login(): """Login to the ZoneMinder API.""" _LOGGER.debug("Attempting to login to ZoneMinder") @@ -118,13 +117,11 @@ def _zm_request(method, api_url, data=None): 'decode "%s"', req.text) -# pylint: disable=no-member def get_state(api_url): """Get a state from the ZoneMinder API service.""" return _zm_request('get', api_url) -# pylint: disable=no-member def change_state(api_url, post_data): """Update a state using the Zoneminder API.""" return _zm_request('post', api_url, data=post_data) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a8ba5e4a6d3242..e540259edd5544 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -218,7 +218,6 @@ async def async_setup_platform(hass, config, async_add_devices, return True -# pylint: disable=R0914 async def async_setup(hass, config): """Set up Z-Wave. diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 3e503e4d9a4d7e..0228e64cf6ef41 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -345,7 +345,6 @@ DISC_TYPE = "type" DISC_VALUES = "values" -# noqa # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L49 # See also: # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L275 diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 91ec50515524bf..54cd569acebc53 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_PLATFORM # Typing Imports and TypeAlias -# pylint: disable=using-constant-test,unused-import,wrong-import-order +# pylint: disable=using-constant-test,unused-import if False: from logging import Logger # NOQA @@ -14,7 +14,6 @@ ConfigType = Dict[str, Any] -# pylint: disable=invalid-sequence-index def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: """Break a component config into different platforms. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index bb34942ad795dd..5ee2cd560819d8 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -128,7 +128,6 @@ async def async_aiohttp_proxy_stream(hass, request, stream, content_type, @callback -# pylint: disable=invalid-name def _async_register_clientsession_shutdown(hass, clientsession): """Register ClientSession close on Home Assistant shutdown. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 85050b5736f4ec..7dc5d2524eccbc 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -59,7 +59,6 @@ def async_generate_entity_id(entity_id_format: str, name: Optional[str], class Entity(object): """An abstract class for Home Assistant entities.""" - # pylint: disable=no-self-use # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. @@ -365,7 +364,6 @@ def __repr__(self): class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" - # pylint: disable=no-self-use @property def state(self) -> str: """Return the state.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4a2cd5fa50c31d..04d9cc450ba0da 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -37,8 +37,6 @@ class RegistryEntry: """Entity Registry Entry.""" - # pylint: disable=no-member - entity_id = attr.ib(type=str) unique_id = attr.ib(type=str) platform = attr.ib(type=str) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d69a556b0cc37c..712b48da0d76df 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -133,7 +133,6 @@ def clear_listener(): """Clear all unsub listener.""" nonlocal async_remove_state_for_cancel, async_remove_state_for_listener - # pylint: disable=not-callable if async_remove_state_for_listener is not None: async_remove_state_for_listener() async_remove_state_for_listener = None diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 5a33bd58641a8b..b3e5f417618497 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -31,7 +31,6 @@ class APIStatus(enum.Enum): """Representation of an API status.""" - # pylint: disable=no-init, invalid-name OK = "ok" INVALID_PASSWORD = "invalid_password" CANNOT_CONNECT = "cannot_connect" diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 3a1ffa82d47e88..69b1bf21c088fc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -267,7 +267,7 @@ def sort_dict_key(val): print(' ', indent_str, i) -CheckConfigError = namedtuple( # pylint: disable=invalid-name +CheckConfigError = namedtuple( 'CheckConfigError', "message domain config") @@ -378,7 +378,6 @@ def _comp_error(ex, domain, config): # Validate platform specific schema if hasattr(platform, 'PLATFORM_SCHEMA'): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA(p_validated) except vol.Invalid as ex: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a8a84c6c880730..bbf0f7e11e23d8 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -120,7 +120,6 @@ def get_random_string(length=10): class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - # pylint: disable=no-init def __ge__(self, other): """Return the greater than element.""" if self.__class__ is other.__class__: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 5676a1d08440a9..b3aa370da2ede8 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -107,7 +107,6 @@ def run_coroutine_threadsafe(coro, loop): def callback(): """Handle the call to the coroutine.""" try: - # pylint: disable=deprecated-method _chain_future(ensure_future(coro, loop=loop), future) # pylint: disable=broad-except except Exception as exc: @@ -136,7 +135,6 @@ def fire_coroutine_threadsafe(coro, loop): def callback(): """Handle the firing of a coroutine.""" - # pylint: disable=deprecated-method ensure_future(coro, loop=loop) loop.call_soon_threadsafe(callback) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 32e9df70a03e1c..d2138f4293c59c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,7 +173,7 @@ def color_name_to_rgb(color_name): return hex_value -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert from RGB color to XY color.""" return color_RGB_to_xy_brightness(iR, iG, iB)[:2] @@ -182,7 +182,7 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy_brightness( iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" @@ -224,7 +224,6 @@ def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy -# pylint: disable=invalid-sequence-index def color_xy_brightness_to_RGB(vX: float, vY: float, ibrightness: int) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" @@ -265,7 +264,6 @@ def color_xy_brightness_to_RGB(vX: float, vY: float, return (ir, ig, ib) -# pylint: disable=invalid-sequence-index def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: """Convert a hsb into its rgb representation.""" if fS == 0: @@ -307,7 +305,6 @@ def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: return (r, g, b) -# pylint: disable=invalid-sequence-index def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: """Convert an rgb color to its hsv representation. @@ -319,13 +316,11 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) -# pylint: disable=invalid-sequence-index def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert an rgb color to its hs representation.""" return color_RGB_to_hsv(iR, iG, iB)[:2] -# pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -337,26 +332,22 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: return (int(fRGB[0]*255), int(fRGB[1]*255), int(fRGB[2]*255)) -# pylint: disable=invalid-sequence-index def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation.""" return color_hsv_to_RGB(iH, iS, 100) -# pylint: disable=invalid-sequence-index def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) -# pylint: disable=invalid-sequence-index def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: """Convert an hs color to its xy representation.""" return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) -# pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: """Match the maximum value of the output to the input.""" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index cd440783cc3e01..37b917baa2efa7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -184,7 +184,6 @@ def formatn(number: int, unit: str) -> str: elif number > 1: return "%d %ss" % (number, unit) - # pylint: disable=invalid-sequence-index def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" return first // second, first % second diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index dae8ed17dc95ad..e390b537d34784 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -82,7 +82,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=invalid-name, unused-variable, invalid-sequence-index +# pylint: disable=invalid-name def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], miles: bool = False) -> Optional[float]: """ diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 66d673987a3789..0e7befd5e9ebb9 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -13,7 +13,7 @@ keyring = None try: - import credstash # pylint: disable=import-error, no-member + import credstash except ImportError: credstash = None @@ -246,7 +246,6 @@ def _load_secret_yaml(secret_path: str) -> Dict: return secrets -# pylint: disable=protected-access def _secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node): """Load secrets and embed it into the configuration YAML.""" diff --git a/script/lazytox.py b/script/lazytox.py index 19af5560dfb132..f0388a0fdcbb47 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -39,7 +39,6 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable=E0402 from gen_requirements_all import main as req_main return req_main(True) == 0 @@ -70,7 +69,6 @@ async def async_exec(*args, display=False): 'stderr': asyncio.subprocess.STDOUT} if display: kwargs['stderr'] = asyncio.subprocess.PIPE - # pylint: disable=E1120 proc = await asyncio.create_subprocess_exec(*args, **kwargs) except FileNotFoundError as err: printc(FAIL, "Could not execute {}. Did you install test requirements?" From 9dd2c36de4563fe6b7929611b7ab523d04516006 Mon Sep 17 00:00:00 2001 From: Luc Touraille Date: Mon, 25 Jun 2018 19:05:33 +0200 Subject: [PATCH 078/169] Update aiofreepybox to fix HTTPS connection issues (#15104) The previous version of aiofreepybox was not working with custom domain names, which uses a Let's Encrypt certificates. Also, it was not working with the default domain name when connecting to Freebox v6. This should be fixed in aiofreepybox 0.0.4. See https://github.com/stilllman/freepybox/pull/1, https://github.com/stilllman/freepybox/pull/3 and https://github.com/stilllman/freepybox/issues/2 for more info. --- homeassistant/components/device_tracker/freebox.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index 67957ca99b9f68..b278c4219254f7 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT) -REQUIREMENTS = ['aiofreepybox==0.0.3'] +REQUIREMENTS = ['aiofreepybox==0.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 78fda284b17775..985bd8a1d24ad0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.device_tracker.freebox -aiofreepybox==0.0.3 +aiofreepybox==0.0.4 # homeassistant.components.camera.yi aioftp==0.10.1 From b2d37ccef673178df6bdd72e827d03719c2348a9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jun 2018 19:06:12 +0200 Subject: [PATCH 079/169] Fix mysensors climate supported features (#15110) --- homeassistant/components/climate/mysensors.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 37ae29fdf81258..a2043c2434bfbb 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -26,9 +26,8 @@ 'Off': STATE_OFF, } -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) +FAN_LIST = ['Auto', 'Min', 'Normal', 'Max'] +OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] async def async_setup_platform( @@ -45,7 +44,18 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + features = SUPPORT_OPERATION_MODE + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SPEED in self._values: + features = features | SUPPORT_FAN_MODE + if (set_req.V_HVAC_SETPOINT_COOL in self._values and + set_req.V_HVAC_SETPOINT_HEAT in self._values): + features = ( + features | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + else: + features = features | SUPPORT_TARGET_TEMPERATURE + return features @property def assumed_state(self): @@ -103,7 +113,7 @@ def current_operation(self): @property def operation_list(self): """List of available operation modes.""" - return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] + return OPERATION_LIST @property def current_fan_mode(self): @@ -113,7 +123,7 @@ def current_fan_mode(self): @property def fan_list(self): """List of available fan modes.""" - return ['Auto', 'Min', 'Normal', 'Max'] + return FAN_LIST async def async_set_temperature(self, **kwargs): """Set new target temperature.""" From e681a7929c8b51abb0f6a6435f0bc437fdc37548 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:13:41 -0700 Subject: [PATCH 080/169] Skip nest security state sensor if no Nest Cam exists (#15112) --- homeassistant/components/sensor/nest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 7afd3b762b3a50..d2e1501ad7e961 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -24,10 +24,14 @@ # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ - + STRUCTURE_SENSOR_TYPES + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES SENSOR_UNITS = {'humidity': '%'} @@ -105,6 +109,14 @@ def get_sensors(): for variable in conditions if variable in PROTECT_SENSOR_TYPES] + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + return all_sensors async_add_devices(await hass.async_add_job(get_sensors), True) From c8458fd7c5364d71e849c505ee27f7cc335cbed8 Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Mon, 25 Jun 2018 22:44:36 +0530 Subject: [PATCH 081/169] Update xiaomi.py (#15136) * Update xiaomi.py Minor logic fix for Xiaofang cameras. * Removed whitespace * Removed whitespace --- homeassistant/components/camera/xiaomi.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index c18a3649e7bbaa..f0e66dbd20e462 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -104,20 +104,16 @@ def get_latest_video_url(self): dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: - if self._model == MODEL_YI: - _LOGGER.warning("There don't appear to be any uploaded videos") - return False - elif self._model == MODEL_XIAOFANG: - _LOGGER.warning("There don't appear to be any folders") - return False - - first_dir = dirs[-1] - try: - ftp.cwd(first_dir) - except error_perm as exc: - _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) - return False + _LOGGER.warning("There don't appear to be any folders") + return False + first_dir = dirs[-1] + try: + ftp.cwd(first_dir) + except error_perm as exc: + _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) + return False + if self._model == MODEL_XIAOFANG: dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: _LOGGER.warning("There don't appear to be any uploaded videos") From 46ea28a4f81b121967a4041a5baa5d34e0827ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 15:59:05 -0400 Subject: [PATCH 082/169] Fix cast config (#15143) --- homeassistant/components/media_player/cast.py | 11 +++- tests/components/media_player/test_cast.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index be7b635f8635d7..4e24d5f2f713b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ +import asyncio import logging import threading from typing import Optional, Tuple @@ -199,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up Cast from a config entry.""" - await _async_setup_platform( - hass, hass.data[CAST_DOMAIN].get('media_player', {}), - async_add_devices, None) + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_devices, None) + for cfg in config]) async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 41cf6749b7158b..47be39c68e5836 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -17,6 +17,8 @@ from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + @pytest.fixture(autouse=True) def cast_mock(): @@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert chromecast.disconnect.call_count == 1 + + +async def test_entry_setup_no_config(hass: HomeAssistantType): + """Test setting up entry with no config..""" + await async_setup_component(hass, 'cast', {}) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {} + + +async def test_entry_setup_single_config(hass: HomeAssistantType): + """Test setting up entry and having a single config option.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': { + 'host': 'bla' + } + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + + +async def test_entry_setup_list_config(hass: HomeAssistantType): + """Test setting up entry and having multiple config options.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': [ + {'host': 'bla'}, + {'host': 'blu'}, + ] + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 2 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'} From 15507df407d9e3db5fff06bc7dbbc54e6af8e10b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 16:04:17 -0400 Subject: [PATCH 083/169] Bump frontend to 20180625.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3d2231ab43b440..54a77af5cfb9ab 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.1'] +REQUIREMENTS = ['home-assistant-frontend==20180625.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 985bd8a1d24ad0..6b9da71bd1fab8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04952b75b816ad..e9226b30498b58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From ab1939f56f4dbcfc3c954059410f2f854c5fe9ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 16:04:17 -0400 Subject: [PATCH 084/169] Bump frontend to 20180625.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3d2231ab43b440..54a77af5cfb9ab 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.1'] +REQUIREMENTS = ['home-assistant-frontend==20180625.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 52a5e0525604de..65f6485f176b94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a38c7f259b4782..e1fdc2cb3d4572 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6e4fb7a937fa7f64bc233183c6ee1697a46f5a81 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 13:06:00 -0700 Subject: [PATCH 085/169] Prevent Nest component setup crash due insufficient permission. (#14966) * Prevent Nest component setup crash due insufficient permission. * Trigger CI * Better error handle and address code review comments * Lint * Tiny wording adjust * Notify user if async_setup_entry failed * Return False if exception occurred in NestDevice.initialize --- homeassistant/components/nest/__init__.py | 85 +++++++++++++---------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index f9507b6ec7b1be..58fa1953ef0d4c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -123,7 +123,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("proceeding with setup") conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - await hass.async_add_job(hass.data[DATA_NEST].initialize) + if not await hass.async_add_job(hass.data[DATA_NEST].initialize): + return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': hass.async_add_job(hass.config_entries.async_forward_entry_setup( @@ -193,63 +194,73 @@ def __init__(self, hass, conf, nest): def initialize(self): """Initialize Nest.""" - if self.local_structure is None: - self.local_structure = [s.name for s in self.nest.structures] + from nest.nest import AuthorizationError, APIError + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, socket.error) as err: + _LOGGER.error( + "Connection error while access Nest web service: %s", err) + return False + return True def structures(self): """Generate a list of structures.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - yield structure - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + yield structure + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) def thermostats(self): - """Generate a list of thermostats and their location.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.thermostats: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + """Generate a list of thermostats.""" + return self._devices('thermostats') def smoke_co_alarms(self): """Generate a list of smoke co alarms.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.smoke_co_alarms: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + return self._devices('smoke_co_alarms') def cameras(self): """Generate a list of cameras.""" + return self._devices('cameras') + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.cameras: - yield (structure, device) - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning("Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings.", + device.serial) + continue + yield (structure, device) + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) class NestSensorDevice(Entity): From 1c8b52f63073ca2f955e4acf1ebba40ee3193262 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 13:06:00 -0700 Subject: [PATCH 086/169] Prevent Nest component setup crash due insufficient permission. (#14966) * Prevent Nest component setup crash due insufficient permission. * Trigger CI * Better error handle and address code review comments * Lint * Tiny wording adjust * Notify user if async_setup_entry failed * Return False if exception occurred in NestDevice.initialize --- homeassistant/components/nest/__init__.py | 85 +++++++++++++---------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bd74897371ad05..bf99fadc1d7818 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -122,7 +122,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("proceeding with setup") conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - await hass.async_add_job(hass.data[DATA_NEST].initialize) + if not await hass.async_add_job(hass.data[DATA_NEST].initialize): + return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': hass.async_add_job(hass.config_entries.async_forward_entry_setup( @@ -192,63 +193,73 @@ def __init__(self, hass, conf, nest): def initialize(self): """Initialize Nest.""" - if self.local_structure is None: - self.local_structure = [s.name for s in self.nest.structures] + from nest.nest import AuthorizationError, APIError + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, socket.error) as err: + _LOGGER.error( + "Connection error while access Nest web service: %s", err) + return False + return True def structures(self): """Generate a list of structures.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - yield structure - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + yield structure + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) def thermostats(self): - """Generate a list of thermostats and their location.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.thermostats: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + """Generate a list of thermostats.""" + return self._devices('thermostats') def smoke_co_alarms(self): """Generate a list of smoke co alarms.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.smoke_co_alarms: - yield (structure, device) - else: - _LOGGER.debug("Ignoring structure %s, not in %s", - structure.name, self.local_structure) - except socket.error: - _LOGGER.error( - "Connection error logging into the nest web service.") + return self._devices('smoke_co_alarms') def cameras(self): """Generate a list of cameras.""" + return self._devices('cameras') + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.cameras: - yield (structure, device) - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning("Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings.", + device.serial) + continue + yield (structure, device) + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) class NestSensorDevice(Entity): From 893e0f8db630f2c6dbc35b6ec9daa6a4118ed526 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jun 2018 13:22:48 -0600 Subject: [PATCH 087/169] Fix socket bug with Yi in 0.72 (#15109) * Fixes BrokenPipeError exceptions with Yi (#15108) * Make sure to close the socket --- homeassistant/components/camera/yi.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 868c5afb4473c5..93f526c2b9627c 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -53,7 +53,6 @@ def __init__(self, hass, config): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] @@ -64,8 +63,6 @@ def __init__(self, hass, config): self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - hass.async_add_job(self._connect_to_client) - @property def brand(self): """Camera brand.""" @@ -76,38 +73,35 @@ def name(self): """Return the name of this camera.""" return self._name - async def _connect_to_client(self): - """Attempt to establish a connection via FTP.""" + async def _get_latest_video_url(self): + """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - self._ftp = ftp except StatusCodeError as err: raise PlatformNotReady(err) - async def _get_latest_video_url(self): - """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import StatusCodeError - try: - await self._ftp.change_directory(self.path) + await ftp.change_directory(self.path) dirs = [] - for path, attrs in await self._ftp.list(): + for path, attrs in await ftp.list(): if attrs['type'] == 'dir' and '.' not in str(path): dirs.append(path) latest_dir = dirs[-1] - await self._ftp.change_directory(latest_dir) + await ftp.change_directory(latest_dir) videos = [] - for path, _ in await self._ftp.list(): + for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None + await ftp.quit() + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) From 69502163bd298ed2eff13d76893e2129d477ec07 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:13:41 -0700 Subject: [PATCH 088/169] Skip nest security state sensor if no Nest Cam exists (#15112) --- homeassistant/components/sensor/nest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index bf1b3f65c4a9fc..75c25f25baaab9 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -24,10 +24,14 @@ # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ - + STRUCTURE_SENSOR_TYPES + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES SENSOR_UNITS = {'humidity': '%'} @@ -105,6 +109,14 @@ def get_sensors(): for variable in conditions if variable in PROTECT_SENSOR_TYPES] + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + return all_sensors async_add_devices(await hass.async_add_job(get_sensors), True) From 3f21966ec92446571f095f158c848b56c9350e10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 15:59:05 -0400 Subject: [PATCH 089/169] Fix cast config (#15143) --- homeassistant/components/media_player/cast.py | 12 ++-- tests/components/media_player/test_cast.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index eced0dbbe25bb2..4e24d5f2f713b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,7 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ -# pylint: disable=import-error +import asyncio import logging import threading from typing import Optional, Tuple @@ -200,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up Cast from a config entry.""" - await _async_setup_platform( - hass, hass.data[CAST_DOMAIN].get('media_player', {}), - async_add_devices, None) + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_devices, None) + for cfg in config]) async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 41cf6749b7158b..47be39c68e5836 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -17,6 +17,8 @@ from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + @pytest.fixture(autouse=True) def cast_mock(): @@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert chromecast.disconnect.call_count == 1 + + +async def test_entry_setup_no_config(hass: HomeAssistantType): + """Test setting up entry with no config..""" + await async_setup_component(hass, 'cast', {}) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {} + + +async def test_entry_setup_single_config(hass: HomeAssistantType): + """Test setting up entry and having a single config option.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': { + 'host': 'bla' + } + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + + +async def test_entry_setup_list_config(hass: HomeAssistantType): + """Test setting up entry and having multiple config options.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': [ + {'host': 'bla'}, + {'host': 'blu'}, + ] + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 2 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'} From 2520fddbdf5ee8680eaa4fbb2d56075746c47c7e Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:04:32 -0700 Subject: [PATCH 090/169] Bump python-nest to 4.0.3 (#15098) Resolve network reconnect issue --- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/sensor/nest.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bf99fadc1d7818..58fa1953ef0d4c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -23,7 +23,7 @@ from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.2'] +REQUIREMENTS = ['python-nest==4.0.3'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest): _LOGGER.debug("dispatching nest data update") async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) else: + _LOGGER.debug("stop listening nest.update_event") return diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 75c25f25baaab9..d2e1501ad7e961 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -145,7 +145,8 @@ def update(self): elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility - self._state = getattr(self.device, self.variable).capitalize() + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None else: self._state = getattr(self.device, self.variable) diff --git a/requirements_all.txt b/requirements_all.txt index 65f6485f176b94..cd9dfb5194a4f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1060,7 +1060,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1fdc2cb3d4572..5f7967761de17a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -156,7 +156,7 @@ pyqwikswitch==0.8 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From 9b950f51928d66293eb942bdf6a7fd4a8eaad12b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 16:59:14 -0400 Subject: [PATCH 091/169] Bumped version to 0.72.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a22605c37f49a4..f1a4e55d662bb2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 72 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c79c94550faf75d8833b11bfe655914ae5a2ac80 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 17:21:38 -0400 Subject: [PATCH 092/169] Return None to indicate no config found (#15147) * Return None to indicate no config found * Fix tests --- homeassistant/config_entries.py | 4 ++++ homeassistant/helpers/storage.py | 2 +- tests/helpers/test_storage.py | 2 +- tests/test_config_entries.py | 10 ++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 13cb7de62ef9a6..be67ebd9cc3a2c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -323,6 +323,10 @@ async def async_load(self): old_conf_migrate_func=_old_conf_migrator ) + if config is None: + self._entries = [] + return + self._entries = [ConfigEntry(**entry) for entry in config['entries']] async def async_forward_entry_setup(self, entry, component): diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 4b0c576f129d96..18c3ddf7fcd520 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -72,7 +72,7 @@ async def async_load(self): json.load_json, self.path, None) if data is None: - return {} + return None if data['version'] == self.version: return data['data'] diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 289d07edab242f..04de920b03602a 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -52,7 +52,7 @@ async def test_loading_non_existing(hass, store): """Test we can save and load data.""" with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): data = await store.async_load() - assert data == {} + assert data is None async def test_saving_with_delay(hass, store, mock_save): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index fc0a549f1aebf9..b65e0dd62e753e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -309,3 +309,13 @@ async def async_step_discovery(self, user_input=None): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_loading_default_config(hass): + """Test loading the default config.""" + manager = config_entries.ConfigEntries(hass, {}) + + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + await manager.async_load() + + assert len(manager.async_entries()) == 0 From 0094fd5c34c7292491bba06188c5f8f1860a03bb Mon Sep 17 00:00:00 2001 From: Matt LeBrun Date: Tue, 26 Jun 2018 10:22:10 -0400 Subject: [PATCH 093/169] Add channel changing support to SamsungTV component (#14451) Add channel changing support to SamsungTV component --- .../components/media_player/samsungtv.py | 26 +++++++- .../components/media_player/test_samsungtv.py | 63 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 15a2b41795e8c7..c3de341d607fd0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ +import asyncio import logging import socket from datetime import timedelta @@ -15,8 +16,9 @@ from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY_MEDIA, + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT, CONF_MAC) @@ -32,12 +34,13 @@ DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 0 +KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -256,6 +259,23 @@ def media_previous_track(self): """Send the previous track command.""" self.send_key('KEY_REWIND') + async def async_play_media(self, media_type, media_id, **kwargs): + """Support changing a channel.""" + if media_type != MEDIA_TYPE_CHANNEL: + _LOGGER.error('Unsupported media type') + return + + # media_id should only be a channel number + try: + cv.positive_int(media_id) + except vol.Invalid: + _LOGGER.error('Media ID must be positive integer') + return + + for digit in media_id: + await self.hass.async_add_job(self.send_key, 'KEY_' + digit) + await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) + def turn_on(self): """Turn the media player on.""" if self._mac: diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index b5baf8b078b6ff..349067f7cd30c8 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -1,11 +1,16 @@ """Tests for samsungtv Components.""" +import asyncio import unittest +from unittest.mock import call, patch, MagicMock from subprocess import CalledProcessError from asynctest import mock +import pytest + import tests.common -from homeassistant.components.media_player import SUPPORT_TURN_ON +from homeassistant.components.media_player import SUPPORT_TURN_ON, \ + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL from homeassistant.components.media_player.samsungtv import setup_platform, \ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ @@ -301,3 +306,59 @@ def test_turn_on(self): self.device._mac = "fake" self.device.turn_on() self.device._wol.send_magic_packet.assert_called_once_with("fake") + + +@pytest.fixture +def samsung_mock(): + """Mock samsungctl.""" + with patch.dict('sys.modules', { + 'samsungctl': MagicMock(), + }): + yield + + +async def test_play_media(hass, samsung_mock): + """Test for play_media.""" + asyncio_sleep = asyncio.sleep + sleeps = [] + + async def sleep(duration, loop): + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('asyncio.sleep', new=sleep): + device = SamsungTVDevice(**WORKING_CONFIG) + device.hass = hass + + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "576") + + exp = [call("KEY_5"), call("KEY_7"), call("KEY_6")] + assert device.send_key.call_args_list == exp + assert len(sleeps) == 3 + + +async def test_play_media_invalid_type(hass, samsung_mock): + """Test for play_media with invalid media type.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_URL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_string(hass, samsung_mock): + """Test for play_media with invalid channel as string.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_non_positive(hass, samsung_mock): + """Test for play_media with invalid channel as non positive integer.""" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "-4") + assert device.send_key.call_count == 0 From 3921dc77a6e36aec2148fb28d06de77fdfd2a184 Mon Sep 17 00:00:00 2001 From: Robert Kiss Date: Tue, 26 Jun 2018 17:44:08 +0200 Subject: [PATCH 094/169] Add SSL peer certificate support to HTTP server (#15043) * adding SSL peer certificate support to HTTP server * remove unnecessary exception block --- homeassistant/components/emulated_hue/__init__.py | 1 + homeassistant/components/http/__init__.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fd7f7147fdba01..708b3db83cd7d9 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -91,6 +91,7 @@ def setup(hass, yaml_config): server_port=config.listen_port, api_password=None, ssl_certificate=None, + ssl_peer_certificate=None, ssl_key=None, cors_origins=None, use_x_forwarded_for=False, diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 17906157a6e797..d8c877e83a2051 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -40,6 +40,7 @@ CONF_SERVER_PORT = 'server_port' CONF_BASE_URL = 'base_url' CONF_SSL_CERTIFICATE = 'ssl_certificate' +CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' @@ -80,6 +81,7 @@ vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), @@ -108,6 +110,7 @@ async def async_setup(hass, config): server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) + ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] @@ -125,6 +128,7 @@ async def async_setup(hass, config): server_port=server_port, api_password=api_password, ssl_certificate=ssl_certificate, + ssl_peer_certificate=ssl_peer_certificate, ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, @@ -166,7 +170,8 @@ async def start_server(event): class HomeAssistantHTTP(object): """HTTP server for Home Assistant.""" - def __init__(self, hass, api_password, ssl_certificate, + def __init__(self, hass, api_password, + ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_networks, login_threshold, is_ban_enabled): @@ -190,6 +195,7 @@ def __init__(self, hass, api_password, ssl_certificate, self.hass = hass self.api_password = api_password self.ssl_certificate = ssl_certificate + self.ssl_peer_certificate = ssl_peer_certificate self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port @@ -287,8 +293,12 @@ async def start(self): except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", self.ssl_certificate, error) - context = None return + + if self.ssl_peer_certificate: + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(cafile=self.ssl_peer_certificate) + else: context = None From 15af6b1ad905cd0d279665c6bd5178efad4823a8 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Tue, 26 Jun 2018 14:23:57 -0500 Subject: [PATCH 095/169] Address inconsistent behavior on flux_led component (#14713) * Address inconsistent behavior between different controllers. Correct issue with comparison that was preventing white value slider from being shown. * Add white mode for Flux LED * Call _bulb.turnOn() after bulb properties have been set to prevent immediate on action * Only use existing brightness if rgb is None to prevent unexpected recalculation of passed rgb values. * Remove blank line * Undo change so current brightness is used in all cases. --- homeassistant/components/light/flux_led.py | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index fc85e05238f9b6..b9db9d4f99b64d 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -33,6 +33,10 @@ MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' +# This mode enables white value to be controlled by brightness. +# RGB value is ignored when this mode is specified. +MODE_WHITE = 'w' + # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = 'red_fade' EFFECT_GREEN_FADE = 'green_fade' @@ -84,7 +88,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(ATTR_MODE, default=MODE_RGBW): - vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])), + vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE])), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(['ledenet'])), }) @@ -181,6 +185,9 @@ def is_on(self): @property def brightness(self): """Return the brightness of this light between 0..255.""" + if self._mode == MODE_WHITE: + return self.white_value + return self._bulb.brightness @property @@ -191,9 +198,12 @@ def hs_color(self): @property def supported_features(self): """Flag supported features.""" - if self._mode is MODE_RGBW: + if self._mode == MODE_RGBW: return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + if self._mode == MODE_WHITE: + return SUPPORT_BRIGHTNESS + return SUPPORT_FLUX_LED @property @@ -208,9 +218,6 @@ def effect_list(self): def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - if not self.is_on: - self._bulb.turnOn() - hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color: @@ -247,10 +254,23 @@ def turn_on(self, **kwargs): if rgb is None: rgb = self._bulb.getRgb() - self._bulb.setRgb(*tuple(rgb), brightness=brightness) + if white is None and self._mode == MODE_RGBW: + white = self.white_value - if white is not None: - self._bulb.setWarmWhite255(white) + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + + # handle RGBW mode + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + + # handle RGB mode + else: + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if not self.is_on: + self._bulb.turnOn() def turn_off(self, **kwargs): """Turn the specified or all lights off.""" From 4208bb457d96e92dc952e69465ac6d2fc7e4fd1b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:11:26 +0200 Subject: [PATCH 096/169] Upgrade youtube_dl to 2018.06.25 (#15168) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 497b6f995bd18b..85895fdd7516e9 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.06.14'] +REQUIREMENTS = ['youtube_dl==2018.06.25'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6b9da71bd1fab8..84fa16a41cb349 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.06.14 +youtube_dl==2018.06.25 # homeassistant.components.light.zengge zengge==0.2 From ba50a5c329e037062adfdd8071eb96d9c6a0d921 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:11:41 +0200 Subject: [PATCH 097/169] Upgrade keyring to 13.0.0 (#15167) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index e02305b5fbbec5..51d70d1f3b2cec 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.2.1', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==13.0.0', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 84fa16a41cb349..13b4364fe806e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -476,7 +476,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.1 +keyring==13.0.0 # homeassistant.scripts.keyring keyrings.alt==3.1 From 41017f10a3cc08f10372643a61e5b4697531dd7f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:12:02 +0200 Subject: [PATCH 098/169] Upgrade sendgrid to 5.4.1 (#15166) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index b73f3a17ee74d9..92b709af8ad10c 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -14,7 +14,7 @@ CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==5.4.0'] +REQUIREMENTS = ['sendgrid==5.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 13b4364fe806e8..24b1de60e1a1a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ schiene==0.22 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==5.4.0 +sendgrid==5.4.1 # homeassistant.components.light.sensehat # homeassistant.components.sensor.sensehat From d6dee62c927d1e6c52c48fee35fff00731a39f12 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jun 2018 06:19:56 -0400 Subject: [PATCH 099/169] Add Mini remote support to insteon_plm (#15152) * Add mini-remote * Bump insteonplm version to 0.11.3 to support mini-remotes --- homeassistant/components/insteon_plm/__init__.py | 6 ++++-- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 8197b45c28d963..82fc6b0226621a 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.11.2'] +REQUIREMENTS = ['insteonplm==0.11.3'] _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,8 @@ def __init__(self): OpenClosedRelay) from insteonplm.states.dimmable import (DimmableSwitch, - DimmableSwitch_Fan) + DimmableSwitch_Fan, + DimmableRemote) from insteonplm.states.sensor import (VariableSensor, OnOffSensor, @@ -328,6 +329,7 @@ def __init__(self): State(DimmableSwitch_Fan, 'fan'), State(DimmableSwitch, 'light'), + State(DimmableRemote, 'binary_sensor'), State(X10DimmableSwitch, 'light'), State(X10OnOffSwitch, 'switch'), diff --git a/requirements_all.txt b/requirements_all.txt index 24b1de60e1a1a1..3d2d8ad2590477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.11.2 +insteonplm==0.11.3 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From c0b6a857f7e6345817f82799a027268a85a075d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jun 2018 14:20:24 -0400 Subject: [PATCH 100/169] Version bump to 20180627.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 54a77af5cfb9ab..ffdd3160b2e9c3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180625.0'] +REQUIREMENTS = ['home-assistant-frontend==20180627.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3d2d8ad2590477..40b77e986130e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180625.0 +home-assistant-frontend==20180627.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9226b30498b58..e2cfced7d6123e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180625.0 +home-assistant-frontend==20180627.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 742144f401075ce054c0b8b005e9d965b8af2a71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jun 2018 15:21:32 -0400 Subject: [PATCH 101/169] Warn when using custom components (#15172) * Warn when using custom components * Update text --- homeassistant/loader.py | 10 +++++++++- tests/test_loader.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ce93c8705b5984..e3e41e09db23d3 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -81,7 +81,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: potential_paths = ['custom_components.{}'.format(comp_or_platform), 'homeassistant.components.{}'.format(comp_or_platform)] - for path in potential_paths: + for index, path in enumerate(potential_paths): try: module = importlib.import_module(path) @@ -100,6 +100,14 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: cache[comp_or_platform] = module + if index == 0: + _LOGGER.warning( + 'You are using a custom component for %s which has not ' + 'been tested by Home Assistant. This component might ' + 'cause stability problems, be sure to disable it if you ' + 'do experience issues with Home Assistant.', + comp_or_platform) + return module except ImportError as err: diff --git a/tests/test_loader.py b/tests/test_loader.py index c97e94a7ce10f1..d87201fb61bdce 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -124,3 +124,13 @@ async def test_custom_component_name(hass): # Test custom components is mounted from custom_components.test_package import TEST assert TEST == 5 + + +async def test_log_warning_custom_component(hass, caplog): + """Test that we log a warning when loading a custom component.""" + loader.get_component(hass, 'test_standalone') + assert \ + 'You are using a custom component for test_standalone' in caplog.text + + loader.get_component(hass, 'light.test') + assert 'You are using a custom component for light.test' in caplog.text From 9066ac44fe89607e1d02ec333605519b016b5eac Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Wed, 27 Jun 2018 15:22:29 -0400 Subject: [PATCH 102/169] Philips Hue Scene Activation: Simplified scene lookup logic, improved error handling (#15175) * Simplified scene lookup logic, improved error handling * Lint --- homeassistant/components/hue/bridge.py | 28 +++++++++----------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d7a8dc7f7300b3..8710b2561b0580 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,24 +124,16 @@ async def hue_activate_scene(self, call, updated=False): (group for group in self.api.groups.values() if group.name == group_name), None) - # The same scene name can exist in multiple groups. - # In this case, activate first scene that contains the - # the exact same light IDs as the group - scenes = [] - for scene in self.api.scenes.values(): - if scene.name == scene_name: - scenes.append(scene) - if len(scenes) == 1: - scene_id = scenes[0].id - else: - group_lights = sorted(group.lights) - for scene in scenes: - if group_lights == scene.lights: - scene_id = scene.id - break + # Additional scene logic to handle duplicate scene names across groups + scene = next( + (scene for scene in self.api.scenes.values() + if scene.name == scene_name + and group is not None + and sorted(scene.lights) == sorted(group.lights)), + None) # If we can't find it, fetch latest info. - if not updated and (group is None or scene_id is None): + if not updated and (group is None or scene is None): await self.api.groups.update() await self.api.scenes.update() await self.hue_activate_scene(call, updated=True) @@ -151,11 +143,11 @@ async def hue_activate_scene(self, call, updated=False): LOGGER.warning('Unable to find group %s', group_name) return - if scene_id is None: + if scene is None: LOGGER.warning('Unable to find scene %s', scene_name) return - await group.set_action(scene=scene_id) + await group.set_action(scene=scene.id) async def get_bridge(hass, host, username=None): From 4fbe3bb07062b81ac4562d4080550d92cbd47828 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 27 Jun 2018 13:55:27 -0700 Subject: [PATCH 103/169] Finalize BotVac D7 Support And Further Reduce Cloud Calls (#15161) * Finalize BotVac D7 Support And Further Reduce Cloud Calls * Lint * Lint Again * Implement requested changes * Hound * Lint --- homeassistant/components/camera/neato.py | 4 +-- homeassistant/components/neato.py | 31 +++++++++++++++++++++--- homeassistant/components/switch/neato.py | 4 +-- homeassistant/components/vacuum/neato.py | 8 ++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 689129e1067ff9..3a8a137c1fe28e 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -10,12 +10,13 @@ from homeassistant.components.camera import Camera from homeassistant.components.neato import ( NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Neato Camera.""" @@ -45,7 +46,6 @@ def camera_image(self): self.update() return self._image - @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 6d14a6f3c4db17..fc407de0a6b79d 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -54,7 +54,12 @@ 7: 'Updating...', 8: 'Copying logs...', 9: 'Calculating position...', - 10: 'IEC test' + 10: 'IEC test', + 11: 'Map cleaning', + 12: 'Exploring map (creating a persistent map)', + 13: 'Acquiring Persistent Map IDs', + 14: 'Creating & Uploading Map', + 15: 'Suspended Exploration' } ERRORS = { @@ -70,12 +75,30 @@ 'ui_error_navigation_pathproblems_returninghome': 'Cannot return to base', 'ui_error_navigation_falling': 'Clear my path', 'ui_error_picked_up': 'Picked up', - 'ui_error_stuck': 'Stuck!' + 'ui_error_stuck': 'Stuck!', + 'dustbin_full': 'Dust bin full', + 'dustbin_missing': 'Dust bin missing', + 'maint_brush_stuck': 'Brush stuck', + 'maint_brush_overload': 'Brush overloaded', + 'maint_bumper_stuck': 'Bumper stuck', + 'maint_vacuum_stuck': 'Vacuum is stuck', + 'maint_left_drop_stuck': 'Vacuum is stuck', + 'maint_left_wheel_stuck': 'Vacuum is stuck', + 'maint_right_drop_stuck': 'Vacuum is stuck', + 'maint_right_wheel_stuck': 'Vacuum is stuck', + 'not_on_charge_base': 'Not on the charge base', + 'nav_robot_falling': 'Clear my path', + 'nav_no_path': 'Clear my path', + 'nav_path_problem': 'Clear my path' } ALERTS = { 'ui_alert_dust_bin_full': 'Please empty dust bin', - 'ui_alert_recovering_location': 'Returning to start' + 'ui_alert_recovering_location': 'Returning to start', + 'dustbin_full': 'Please empty dust bin', + 'maint_brush_change': 'Change the brush', + 'maint_filter_change': 'Change the filter', + 'clean_completed_to_start': 'Cleaning completed' } @@ -121,7 +144,7 @@ def login(self): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=60)) + @Throttle(timedelta(seconds=300)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 1d149383f6fad6..dca5d63b43dd30 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -10,12 +10,13 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + SWITCH_TYPE_SCHEDULE = 'schedule' SWITCH_TYPES = { @@ -52,7 +53,6 @@ def __init__(self, hass, robot, switch_type): self._schedule_state = None self._clean_state = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 128bece8494274..1b32fff9e5b8f7 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -15,12 +15,13 @@ SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=5) + SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ SUPPORT_STATUS | SUPPORT_MAP @@ -63,7 +64,6 @@ def __init__(self, hass, robot): self.clean_suspension_charge_count = None self.clean_suspension_time = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") @@ -101,6 +101,10 @@ def update(self): self.robot.state['action'] == 3 and self.robot.state['state'] == 2): self._clean_state = STATE_ON + elif (self.robot.state['action'] == 11 or + self.robot.state['action'] == 12 and + self.robot.state['state'] == 2): + self._clean_state = STATE_ON else: self._clean_state = STATE_OFF From dbb786c548bb21a0c2b5adfc1519f28155085466 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 28 Jun 2018 12:23:32 +0200 Subject: [PATCH 104/169] DarkSky weather / Fix states (#15174) * DarkSky weather / Fix states * fix lint * fix tests --- homeassistant/components/weather/darksky.py | 28 ++++++++++++++++++--- tests/components/weather/test_darksky.py | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index f0712542ea544b..7afa97fd4f61db 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, + PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -25,6 +26,22 @@ ATTRIBUTION = "Powered by Dark Sky" +MAP_CONDITION = { + 'clear-day': 'sunny', + 'clear-night': 'clear-night', + 'rain': 'rainy', + 'snow': 'snowy', + 'sleet': 'snowy-rainy', + 'wind': 'windy', + 'fog': 'fog', + 'cloudy': 'cloudy', + 'partly-cloudy-day': 'partlycloudy', + 'partly-cloudy-night': 'partlycloudy', + 'hail': 'hail', + 'thunderstorm': 'lightning', + 'tornado': None, +} + CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -108,7 +125,7 @@ def pressure(self): @property def condition(self): """Return the weather condition.""" - return self._ds_currently.get('summary') + return MAP_CONDITION.get(self._ds_currently.get('icon')) @property def forecast(self): @@ -116,8 +133,11 @@ def forecast(self): return [{ ATTR_FORECAST_TIME: datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: entry.d.get('temperature')} - for entry in self._ds_hourly.data] + ATTR_FORECAST_TEMP: + entry.d.get('temperature'), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_hourly.data] def update(self): """Get the latest data from Dark Sky.""" diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 7faa033e0a86c4..41687451cd6493 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -48,4 +48,4 @@ def test_setup(self, mock_req, mock_get_forecast): self.assertEqual(mock_get_forecast.call_count, 1) state = self.hass.states.get('weather.test') - self.assertEqual(state.state, 'Clear') + self.assertEqual(state.state, 'sunny') From 19f2bbf52f2ff947699ebd8e513ba2da6ebc6241 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Thu, 28 Jun 2018 09:16:11 -0400 Subject: [PATCH 105/169] Only use the X-Forwarded-For header if connection is from a trusted network (#15182) See https://github.com/home-assistant/home-assistant/issues/14345#issuecomment-400854569 --- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/http/real_ip.py | 14 +++++++++----- tests/components/http/test_auth.py | 2 +- tests/components/http/test_real_ip.py | 23 ++++++++++++++++++++--- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d8c877e83a2051..f769d2bc4ffba1 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -180,7 +180,7 @@ def __init__(self, hass, api_password, middlewares=[staticresource_middleware]) # This order matters - setup_real_ip(app, use_x_forwarded_for) + setup_real_ip(app, use_x_forwarded_for, trusted_networks) if is_ban_enabled: setup_bans(hass, app, login_threshold) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index c394016a683c43..401a09dc3066fa 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -11,18 +11,22 @@ @callback -def setup_real_ip(app, use_x_forwarded_for): +def setup_real_ip(app, use_x_forwarded_for, trusted_networks): """Create IP Ban middleware for the app.""" @middleware async def real_ip_middleware(request, handler): """Real IP middleware.""" + connected_ip = ip_address( + request.transport.get_extra_info('peername')[0]) + request[KEY_REAL_IP] = connected_ip + + # Only use the XFF header if enabled, present, and from a trusted proxy if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers): + X_FORWARDED_FOR in request.headers and + any(connected_ip in trusted_network + for trusted_network in trusted_networks)): request[KEY_REAL_IP] = ip_address( request.headers.get(X_FORWARDED_FOR).split(',')[0]) - else: - request[KEY_REAL_IP] = \ - ip_address(request.transport.get_extra_info('peername')[0]) return await handler(request) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index a44d17d513db98..dd8b2cd35c46c2 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -41,7 +41,7 @@ def app(): """Fixture to setup a web.Application.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, False) + setup_real_ip(app, False, []) return app diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 61846eb94c242f..b6af81592078fe 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -1,6 +1,7 @@ """Test real IP middleware.""" from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR +from ipaddress import ip_network from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_REAL_IP @@ -15,7 +16,7 @@ async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, False) + setup_real_ip(app, False, []) mock_api_client = await aiohttp_client(app) @@ -27,11 +28,27 @@ async def test_ignore_x_forwarded_for(aiohttp_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(aiohttp_client): +async def test_use_x_forwarded_for_without_trusted_proxy(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, True) + setup_real_ip(app, True, []) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) mock_api_client = await aiohttp_client(app) From a277470363c0758bb305410aad49c257ff8bac40 Mon Sep 17 00:00:00 2001 From: Alex Barcelo Date: Thu, 28 Jun 2018 16:49:33 +0200 Subject: [PATCH 106/169] Adding 'namespace' for prometheus metrics (#13738) * Updating prometheus client version * Using `entity_filter` as filter mechanism * New optional `namespace` configuration --- homeassistant/components/prometheus.py | 49 +++++++++++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/test_prometheus.py | 2 +- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 96ed098567d1d0..6f233dafe0832a 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -11,16 +11,15 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView -from homeassistant.components import recorder from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) from homeassistant import core as hacore -from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entityfilter, state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius -REQUIREMENTS = ['prometheus_client==0.1.0'] +REQUIREMENTS = ['prometheus_client==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -29,8 +28,14 @@ DOMAIN = 'prometheus' DEPENDENCIES = ['http'] +CONF_FILTER = 'filter' +CONF_PROM_NAMESPACE = 'namespace' + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: recorder.FILTER_SCHEMA, + DOMAIN: vol.All({ + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_PROM_NAMESPACE): cv.string, + }) }, extra=vol.ALLOW_EXTRA) @@ -40,25 +45,26 @@ def setup(hass, config): hass.http.register_view(PrometheusView(prometheus_client)) - conf = config.get(DOMAIN, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - include = conf.get(CONF_INCLUDE, {}) - metrics = Metrics(prometheus_client, exclude, include) + conf = config[DOMAIN] + entity_filter = conf[CONF_FILTER] + namespace = conf.get(CONF_PROM_NAMESPACE) + metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) return True -class Metrics(object): +class PrometheusMetrics(object): """Model all of the metrics which should be exposed to Prometheus.""" - def __init__(self, prometheus_client, exclude, include): + def __init__(self, prometheus_client, entity_filter, namespace): """Initialize Prometheus Metrics.""" self.prometheus_client = prometheus_client - self.exclude = exclude.get(CONF_ENTITIES, []) + \ - exclude.get(CONF_DOMAINS, []) - self.include_domains = include.get(CONF_DOMAINS, []) - self.include_entities = include.get(CONF_ENTITIES, []) + self._filter = entity_filter + if namespace: + self.metrics_prefix = "{}_".format(namespace) + else: + self.metrics_prefix = "" self._metrics = {} def handle_event(self, event): @@ -71,14 +77,7 @@ def handle_event(self, event): _LOGGER.debug("Handling state update for %s", entity_id) domain, _ = hacore.split_entity_id(entity_id) - if entity_id in self.exclude: - return - if domain in self.exclude and entity_id not in self.include_entities: - return - if self.include_domains and domain not in self.include_domains: - return - if not self.exclude and (self.include_entities and - entity_id not in self.include_entities): + if not self._filter(state.entity_id): return handler = '_handle_{}'.format(domain) @@ -100,7 +99,9 @@ def _metric(self, metric, factory, documentation, labels=None): try: return self._metrics[metric] except KeyError: - self._metrics[metric] = factory(metric, documentation, labels) + full_metric_name = "{}{}".format(self.metrics_prefix, metric) + self._metrics[metric] = factory( + full_metric_name, documentation, labels) return self._metrics[metric] @staticmethod diff --git a/requirements_all.txt b/requirements_all.txt index 40b77e986130e0..1a3b6cacf9e6ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -674,7 +674,7 @@ postnl_api==1.0.2 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.sensor.systemmonitor psutil==5.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2cfced7d6123e..1ae0a5db6c336d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.notify.pushbullet # homeassistant.components.sensor.pushbullet diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index e336a28eb0399e..49744421c726ec 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -12,7 +12,7 @@ def prometheus_client(loop, hass, aiohttp_client): assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, - {}, + {prometheus.DOMAIN: {}}, )) return loop.run_until_complete(aiohttp_client(hass.http.app)) From 2205090795c7a67b1d764a59ec5cfcb20956c0c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jun 2018 22:14:26 -0400 Subject: [PATCH 107/169] Storage auth (#15192) * Support parallel loading * Add storage mock * Store auth * Fix tests --- homeassistant/auth.py | 143 +++++++++++++++--- homeassistant/helpers/storage.py | 21 ++- tests/auth_providers/test_insecure_example.py | 8 +- tests/common.py | 51 ++++++- tests/conftest.py | 12 +- tests/helpers/test_storage.py | 105 ++++++++----- tests/test_auth.py | 48 +++++- tests/test_config_entries.py | 28 ++-- 8 files changed, 324 insertions(+), 92 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 5e434b74ca82db..0c8346607ca0e9 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -21,6 +21,8 @@ _LOGGER = logging.getLogger(__name__) +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth' AUTH_PROVIDERS = Registry() @@ -121,23 +123,12 @@ class User: is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) name = attr.ib(type=str, default=None) - # For persisting and see if saved? - # store = attr.ib(type=AuthStore, default=None) # List of credentials of a user. - credentials = attr.ib(type=list, default=attr.Factory(list)) + credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False) # Tokens associated with a user. - refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) - - def as_dict(self): - """Convert user object to a dictionary.""" - return { - 'id': self.id, - 'is_owner': self.is_owner, - 'is_active': self.is_active, - 'name': self.name, - } + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False) @attr.s(slots=True) @@ -152,7 +143,7 @@ class RefreshToken: default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) - access_tokens = attr.ib(type=list, default=attr.Factory(list)) + access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False) @attr.s(slots=True) @@ -376,7 +367,7 @@ def __init__(self, hass): self.hass = hass self.users = None self.clients = None - self._load_lock = asyncio.Lock(loop=hass.loop) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) async def credentials_for_provider(self, provider_type, provider_id): """Return credentials for specific auth provider type and id.""" @@ -494,10 +485,128 @@ async def async_get_client(self, client_id): async def async_load(self): """Load the users.""" - async with self._load_lock: + data = await self._store.async_load() + + # Make sure that we're not overriding data if 2 loads happened at the + # same time + if self.users is not None: + return + + if data is None: self.users = {} self.clients = {} + return + + users = { + user_dict['id']: User(**user_dict) for user_dict in data['users'] + } + + for cred_dict in data['credentials']: + users[cred_dict['user_id']].credentials.append(Credentials( + id=cred_dict['id'], + is_new=False, + auth_provider_type=cred_dict['auth_provider_type'], + auth_provider_id=cred_dict['auth_provider_id'], + data=cred_dict['data'], + )) + + refresh_tokens = {} + + for rt_dict in data['refresh_tokens']: + token = RefreshToken( + id=rt_dict['id'], + user=users[rt_dict['user_id']], + client_id=rt_dict['client_id'], + created_at=dt_util.parse_datetime(rt_dict['created_at']), + access_token_expiration=timedelta( + rt_dict['access_token_expiration']), + token=rt_dict['token'], + ) + refresh_tokens[token.id] = token + users[rt_dict['user_id']].refresh_tokens[token.token] = token + + for ac_dict in data['access_tokens']: + refresh_token = refresh_tokens[ac_dict['refresh_token_id']] + token = AccessToken( + refresh_token=refresh_token, + created_at=dt_util.parse_datetime(ac_dict['created_at']), + token=ac_dict['token'], + ) + refresh_token.access_tokens.append(token) + + clients = { + cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients'] + } + + self.users = users + self.clients = clients async def async_save(self): """Save users.""" - pass + users = [ + { + 'id': user.id, + 'is_owner': user.is_owner, + 'is_active': user.is_active, + 'name': user.name, + } + for user in self.users.values() + ] + + credentials = [ + { + 'id': credential.id, + 'user_id': user.id, + 'auth_provider_type': credential.auth_provider_type, + 'auth_provider_id': credential.auth_provider_id, + 'data': credential.data, + } + for user in self.users.values() + for credential in user.credentials + ] + + refresh_tokens = [ + { + 'id': refresh_token.id, + 'user_id': user.id, + 'client_id': refresh_token.client_id, + 'created_at': refresh_token.created_at.isoformat(), + 'access_token_expiration': + refresh_token.access_token_expiration.total_seconds(), + 'token': refresh_token.token, + } + for user in self.users.values() + for refresh_token in user.refresh_tokens.values() + ] + + access_tokens = [ + { + 'id': user.id, + 'refresh_token_id': refresh_token.id, + 'created_at': access_token.created_at.isoformat(), + 'token': access_token.token, + } + for user in self.users.values() + for refresh_token in user.refresh_tokens.values() + for access_token in refresh_token.access_tokens + ] + + clients = [ + { + 'id': client.id, + 'name': client.name, + 'secret': client.secret, + 'redirect_uris': client.redirect_uris, + } + for client in self.clients.values() + ] + + data = { + 'users': users, + 'clients': clients, + 'credentials': credentials, + 'access_tokens': access_tokens, + 'refresh_tokens': refresh_tokens, + } + + await self._store.async_save(data, delay=1) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 18c3ddf7fcd520..962074ec3affd0 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -53,6 +53,7 @@ def __init__(self, hass, version: int, key: str): self._unsub_delay_listener = None self._unsub_stop_listener = None self._write_lock = asyncio.Lock() + self._load_task = None @property def path(self): @@ -64,7 +65,17 @@ async def async_load(self): If the expected version does not match the given version, the migrate function will be invoked with await migrate_func(version, config). + + Will ensure that when a call comes in while another one is in progress, + the second call will wait and return the result of the first call. """ + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load()) + + return await self._load_task + + async def _async_load(self): + """Helper to load the data.""" if self._data is not None: data = self._data else: @@ -75,9 +86,15 @@ async def async_load(self): return None if data['version'] == self.version: - return data['data'] + stored = data['data'] + else: + _LOGGER.info('Migrating %s storage from %s to %s', + self.key, data['version'], self.version) + stored = await self._async_migrate_func( + data['version'], data['data']) - return await self._async_migrate_func(data['version'], data['data']) + self._load_task = None + return stored async def async_save(self, data: Dict, *, delay: Optional[int] = None): """Save data with an optional delay.""" diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 0b481f93099bcb..3377a60c45b010 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -11,15 +11,15 @@ @pytest.fixture -def store(): +def store(hass): """Mock store.""" - return auth.AuthStore(Mock()) + return auth.AuthStore(hass) @pytest.fixture -def provider(store): +def provider(hass, store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(None, store, { + return insecure_example.ExampleAuthProvider(hass, store, { 'type': 'insecure_example', 'users': [ { diff --git a/tests/common.py b/tests/common.py index 56575bdb1e9f2d..8eaee686b22275 100644 --- a/tests/common.py +++ b/tests/common.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import functools as ft +import json import os import sys from unittest.mock import patch, MagicMock, Mock @@ -15,7 +16,7 @@ from homeassistant.config import async_process_component_config from homeassistant.helpers import ( intent, entity, restore_state, entity_registry, - entity_platform) + entity_platform, storage) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util import homeassistant.util.yaml as yaml @@ -705,3 +706,51 @@ def _handle(self, attr): if attr in self._values: return self._values[attr] return getattr(super(), attr) + + +@contextmanager +def mock_storage(data=None): + """Mock storage. + + Data is a dict {'key': {'version': version, 'data': data}} + + Written data will be converted to JSON to ensure JSON parsing works. + """ + if data is None: + data = {} + + orig_load = storage.Store._async_load + + async def mock_async_load(store): + """Mock version of load.""" + if store._data is None: + # No data to load + if store.key not in data: + return None + + store._data = data.get(store.key) + + # Route through original load so that we trigger migration + loaded = await orig_load(store) + _LOGGER.info('Loading data for %s: %s', store.key, loaded) + return loaded + + def mock_write_data(store, path, data_to_write): + """Mock version of write data.""" + # To ensure that the data can be serialized + _LOGGER.info('Writing data to %s: %s', store.key, data_to_write) + data[store.key] = json.loads(json.dumps(data_to_write)) + + with patch('homeassistant.helpers.storage.Store._async_load', + side_effect=mock_async_load, autospec=True), \ + patch('homeassistant.helpers.storage.Store._write_data', + side_effect=mock_write_data, autospec=True): + yield data + + +async def flush_store(store): + """Make sure all delayed writes of a store are written.""" + if store._data is None: + return + + await store._async_handle_write_data() diff --git a/tests/conftest.py b/tests/conftest.py index 4d619c5ef61d02..0a350b62fc1cb9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,8 @@ from homeassistant.util import location from tests.common import ( - async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro) + async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro, + mock_storage as mock_storage) from tests.test_util.aiohttp import mock_aiohttp_client from tests.mock.zwave import MockNetwork, MockOption @@ -59,7 +60,14 @@ def verify_cleanup(): @pytest.fixture -def hass(loop): +def hass_storage(): + """Fixture to mock storage.""" + with mock_storage() as stored_data: + yield stored_data + + +@pytest.fixture +def hass(loop, hass_storage): """Fixture to provide a test instance of HASS.""" hass = loop.run_until_complete(async_test_home_assistant(loop)) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 04de920b03602a..f414eaec97c844 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -1,4 +1,5 @@ """Tests for the storage helper.""" +import asyncio from datetime import timedelta from unittest.mock import patch @@ -16,32 +17,13 @@ MOCK_DATA = {'hello': 'world'} -@pytest.fixture -def mock_save(): - """Fixture to mock JSON save.""" - written = [] - with patch('homeassistant.util.json.save_json', - side_effect=lambda *args: written.append(args)): - yield written - - -@pytest.fixture -def mock_load(mock_save): - """Fixture to mock JSON read.""" - with patch('homeassistant.util.json.load_json', - side_effect=lambda *args: mock_save[-1][1]): - yield - - @pytest.fixture def store(hass): """Fixture of a store that prevents writing on HASS stop.""" - store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) - store._async_ensure_stop_listener = lambda: None - yield store + yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) -async def test_loading(hass, store, mock_save, mock_load): +async def test_loading(hass, store): """Test we can save and load data.""" await store.async_save(MOCK_DATA) data = await store.async_load() @@ -55,55 +37,96 @@ async def test_loading_non_existing(hass, store): assert data is None -async def test_saving_with_delay(hass, store, mock_save): +async def test_loading_parallel(hass, store, hass_storage, caplog): + """Test we can save and load data.""" + hass_storage[store.key] = { + 'version': MOCK_VERSION, + 'data': MOCK_DATA, + } + + results = await asyncio.gather( + store.async_load(), + store.async_load() + ) + + assert results[0] is MOCK_DATA + assert results[1] is MOCK_DATA + assert caplog.text.count('Loading data for {}'.format(store.key)) + + +async def test_saving_with_delay(hass, store, hass_storage): """Test saving data after a delay.""" await store.async_save(MOCK_DATA, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } -async def test_saving_on_stop(hass, mock_save): +async def test_saving_on_stop(hass, hass_storage): """Test delayed saves trigger when we quit Home Assistant.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) await store.async_save(MOCK_DATA, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } -async def test_loading_while_delay(hass, store, mock_save, mock_load): +async def test_loading_while_delay(hass, store, hass_storage): """Test we load new data even if not written yet.""" await store.async_save({'delay': 'no'}) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } await store.async_save({'delay': 'yes'}, delay=1) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } data = await store.async_load() assert data == {'delay': 'yes'} -async def test_writing_while_writing_delay(hass, store, mock_save, mock_load): +async def test_writing_while_writing_delay(hass, store, hass_storage): """Test a write while a write with delay is active.""" await store.async_save({'delay': 'yes'}, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage await store.async_save({'delay': 'no'}) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } data = await store.async_load() assert data == {'delay': 'no'} -async def test_migrator_no_existing_config(hass, store, mock_save): +async def test_migrator_no_existing_config(hass, store, hass_storage): """Test migrator with no existing config.""" with patch('os.path.isfile', return_value=False), \ patch.object(store, 'async_load', @@ -112,10 +135,10 @@ async def test_migrator_no_existing_config(hass, store, mock_save): hass, 'old-path', store) assert data == {'cur': 'config'} - assert len(mock_save) == 0 + assert store.key not in hass_storage -async def test_migrator_existing_config(hass, store, mock_save): +async def test_migrator_existing_config(hass, store, hass_storage): """Test migrating existing config.""" with patch('os.path.isfile', return_value=True), \ patch('os.remove') as mock_remove, \ @@ -126,15 +149,14 @@ async def test_migrator_existing_config(hass, store, mock_save): assert len(mock_remove.mock_calls) == 1 assert data == {'old': 'config'} - assert len(mock_save) == 1 - assert mock_save[0][1] == { + assert hass_storage[store.key] == { 'key': MOCK_KEY, 'version': MOCK_VERSION, 'data': data, } -async def test_migrator_transforming_config(hass, store, mock_save): +async def test_migrator_transforming_config(hass, store, hass_storage): """Test migrating config to new format.""" async def old_conf_migrate_func(old_config): """Migrate old config to new format.""" @@ -150,8 +172,7 @@ async def old_conf_migrate_func(old_config): assert len(mock_remove.mock_calls) == 1 assert data == {'new': 'config'} - assert len(mock_save) == 1 - assert mock_save[0][1] == { + assert hass_storage[store.key] == { 'key': MOCK_KEY, 'version': MOCK_VERSION, 'data': data, diff --git a/tests/test_auth.py b/tests/test_auth.py index 4bbf218fd23ca7..116f92ca81740d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,7 +4,7 @@ import pytest from homeassistant import auth, data_entry_flow -from tests.common import MockUser, ensure_auth_manager_loaded +from tests.common import MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture @@ -53,9 +53,9 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass): }] -async def test_create_new_user(mock_hass): +async def test_create_new_user(hass, hass_storage): """Test creating new user.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -124,9 +124,9 @@ async def test_login_as_existing_user(mock_hass): assert user.name == 'Paulus' -async def test_linking_user_to_two_auth_providers(mock_hass): +async def test_linking_user_to_two_auth_providers(hass, hass_storage): """Test linking user to two auth providers.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -157,3 +157,41 @@ async def test_linking_user_to_two_auth_providers(mock_hass): }) await manager.async_link_user(user, step['result']) assert len(user.credentials) == 2 + + +async def test_saving_loading(hass, hass_storage): + """Test storing and saving data. + + Creates one of each type that we store to test we restore correctly. + """ + manager = await auth.auth_manager_from_config(hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + user = await manager.async_get_or_create_user(step['result']) + + client = await manager.async_create_client( + 'test', redirect_uris=['https://example.com']) + + refresh_token = await manager.async_create_refresh_token(user, client.id) + + manager.async_create_access_token(refresh_token) + + await flush_store(manager._store._store) + + store2 = auth.AuthStore(hass) + await store2.async_load() + assert len(store2.users) == 1 + assert store2.users[user.id] == user + + assert len(store2.clients) == 1 + assert store2.clients[client.id] == client diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b65e0dd62e753e..d7a7ec4b82bf90 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch, mock_open +from unittest.mock import MagicMock, patch import pytest @@ -152,8 +152,7 @@ def test_domains_gets_uniques(manager): assert manager.async_domains() == ['test', 'test2', 'test3'] -@asyncio.coroutine -def test_saving_and_loading(hass): +async def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" loader.set_component( hass, 'test', @@ -172,7 +171,7 @@ def async_step_init(self, user_input=None): ) with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - yield from hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init('test') class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @@ -186,27 +185,18 @@ def async_step_init(self, user_input=None): } ) - json_path = 'homeassistant.util.json.open' - with patch('homeassistant.config_entries.HANDLERS.get', return_value=Test2Flow): - yield from hass.config_entries.flow.async_init('test') - - with patch(json_path, mock_open(), create=True) as mock_write: - # To trigger the call_later - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) - # To execute the save - yield from hass.async_block_till_done() + await hass.config_entries.flow.async_init('test') - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] + # To trigger the call_later + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + # To execute the save + await hass.async_block_till_done() # Now load written data in new config manager manager = config_entries.ConfigEntries(hass, {}) - - with patch('os.path.isfile', return_value=False), \ - patch(json_path, mock_open(read_data=written), create=True): - yield from manager.async_load() + await manager.async_load() # Ensure same order for orig, loaded in zip(hass.config_entries.async_entries(), From 39971ee9190b616fc3149c53912f9f8b2976c46a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 00:02:33 -0400 Subject: [PATCH 108/169] Make sure we check access token expiration (#15207) * Make sure we check access token expiration * Use correct access token websocket --- homeassistant/auth.py | 27 +++++++--- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/websocket_api.py | 5 +- tests/common.py | 1 + tests/test_auth.py | 50 ++++++++++++++++++- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 0c8346607ca0e9..22abcdf213ccc2 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -159,9 +159,10 @@ class AccessToken: default=attr.Factory(generate_secret)) @property - def expires(self): - """Return datetime when this token expires.""" - return self.created_at + self.refresh_token.access_token_expiration + def expired(self): + """Return if this token has expired.""" + expires = self.created_at + self.refresh_token.access_token_expiration + return dt_util.utcnow() > expires @attr.s(slots=True) @@ -272,7 +273,12 @@ def __init__(self, hass, store, providers): self.login_flow = data_entry_flow.FlowManager( hass, self._async_create_login_flow, self._async_finish_login_flow) - self.access_tokens = {} + self._access_tokens = {} + + @property + def active(self): + """Return if any auth providers are registered.""" + return bool(self._providers) @property def async_auth_providers(self): @@ -308,13 +314,22 @@ async def async_get_refresh_token(self, token): def async_create_access_token(self, refresh_token): """Create a new access token.""" access_token = AccessToken(refresh_token) - self.access_tokens[access_token.token] = access_token + self._access_tokens[access_token.token] = access_token return access_token @callback def async_get_access_token(self, token): """Get an access token.""" - return self.access_tokens.get(token) + tkn = self._access_tokens.get(token) + + if tkn is None: + return None + + if tkn.expired: + self._access_tokens.pop(token) + return None + + return tkn async def async_create_client(self, name, *, redirect_uris=None, no_secret=False): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ffdd3160b2e9c3..0e9d7612669152 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -200,7 +200,7 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" - if list(hass.auth.async_auth_providers): + if hass.auth.active: client = await hass.auth.async_create_client( 'Home Assistant Frontend', redirect_uris=['/'], diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index aacef4547b7670..bf472348babf8c 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -324,8 +324,9 @@ def handle_hass_stop(event): request, msg['api_password']) elif 'access_token' in msg: - authenticated = \ - msg['access_token'] in self.hass.auth.access_tokens + token = self.hass.auth.async_get_access_token( + msg['access_token']) + authenticated = token is not None if not authenticated: self.debug("Invalid password") diff --git a/tests/common.py b/tests/common.py index 8eaee686b22275..1b8eabaa0db4bc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -320,6 +320,7 @@ def add_to_hass(self, hass): def add_to_auth_manager(self, auth_mgr): """Test helper to add entry to hass.""" + ensure_auth_manager_loaded(auth_mgr) auth_mgr._store.users[self.id] = self return self diff --git a/tests/test_auth.py b/tests/test_auth.py index 116f92ca81740d..4c0db71466e97d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,14 +1,16 @@ """Tests for the Home Assistant auth module.""" -from unittest.mock import Mock +from datetime import timedelta +from unittest.mock import Mock, patch import pytest from homeassistant import auth, data_entry_flow +from homeassistant.util import dt as dt_util from tests.common import MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture -def mock_hass(): +def mock_hass(loop): """Hass mock with minimum amount of data set to make it work with auth.""" hass = Mock() hass.config.skip_pip = True @@ -195,3 +197,47 @@ async def test_saving_loading(hass, hass_storage): assert len(store2.clients) == 1 assert store2.clients[client.id] == client + + +def test_access_token_expired(): + """Test that the expired property on access tokens work.""" + refresh_token = auth.RefreshToken( + user=None, + client_id='bla' + ) + + access_token = auth.AccessToken( + refresh_token=refresh_token + ) + + assert access_token.expired is False + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert access_token.expired is True + + almost_exp = dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION - timedelta(1) + with patch('homeassistant.auth.dt_util.utcnow', return_value=almost_exp): + assert access_token.expired is False + + +async def test_cannot_retrieve_expired_access_token(hass): + """Test that we cannot retrieve expired access tokens.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, 'bla') + access_token = manager.async_create_access_token(refresh_token) + + assert manager.async_get_access_token(access_token.token) is access_token + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert manager.async_get_access_token(access_token.token) is None + + # Even with unpatched time, it should have been removed from manager + assert manager.async_get_access_token(access_token.token) is None From 26590e244ced1b67792bac8fdd5fc81ac446dba1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 00:02:45 -0400 Subject: [PATCH 109/169] Migrate home assistant auth provider to use storage helper (#15200) --- homeassistant/auth_providers/homeassistant.py | 39 ++++--- homeassistant/scripts/auth.py | 25 +++-- tests/auth_providers/test_homeassistant.py | 101 +++++++----------- tests/scripts/test_auth.py | 72 ++++++++----- 4 files changed, 117 insertions(+), 120 deletions(-) diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py index c2db193ce1a1a2..c4d2021f6ce054 100644 --- a/homeassistant/auth_providers/homeassistant.py +++ b/homeassistant/auth_providers/homeassistant.py @@ -8,10 +8,10 @@ from homeassistant import auth, data_entry_flow from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import json -PATH_DATA = '.users.json' +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth_provider.homeassistant' CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) @@ -31,14 +31,22 @@ class InvalidUser(HomeAssistantError): class Data: """Hold the user data.""" - def __init__(self, path, data): + def __init__(self, hass): """Initialize the user data store.""" - self.path = path + self.hass = hass + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data = None + + async def async_load(self): + """Load stored data.""" + data = await self._store.async_load() + if data is None: data = { 'salt': auth.generate_secret(), 'users': [] } + self._data = data @property @@ -99,14 +107,9 @@ def change_password(self, username, new_password): else: raise InvalidUser - def save(self): + async def async_save(self): """Save data.""" - json.save_json(self.path, self._data) - - -def load_data(path): - """Load auth data.""" - return Data(path, json.load_json(path, None)) + await self._store.async_save(self._data) @auth.AUTH_PROVIDERS.register('homeassistant') @@ -121,12 +124,10 @@ async def async_credential_flow(self): async def async_validate_login(self, username, password): """Helper to validate a username and password.""" - def validate(): - """Validate creds.""" - data = self._auth_data() - data.validate_login(username, password) - - await self.hass.async_add_job(validate) + data = Data(self.hass) + await data.async_load() + await self.hass.async_add_executor_job( + data.validate_login, username, password) async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" @@ -141,10 +142,6 @@ async def async_get_or_create_credentials(self, flow_result): 'username': username }) - def _auth_data(self): - """Return the auth provider data.""" - return load_data(self.hass.config.path(PATH_DATA)) - class LoginFlow(data_entry_flow.FlowHandler): """Handler for the login flow.""" diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index b4f1ddd2f11be7..dacdc7b18e24ed 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -1,7 +1,9 @@ """Script to manage users for the Home Assistant auth provider.""" import argparse +import asyncio import os +from homeassistant.core import HomeAssistant from homeassistant.config import get_default_config_dir from homeassistant.auth_providers import homeassistant as hass_auth @@ -17,7 +19,8 @@ def run(args): default=get_default_config_dir(), help="Directory that contains the Home Assistant configuration") - subparsers = parser.add_subparsers() + subparsers = parser.add_subparsers(dest='func') + subparsers.required = True parser_list = subparsers.add_parser('list') parser_list.set_defaults(func=list_users) @@ -37,11 +40,15 @@ def run(args): parser_change_pw.set_defaults(func=change_password) args = parser.parse_args(args) - path = os.path.join(os.getcwd(), args.config, hass_auth.PATH_DATA) - args.func(hass_auth.load_data(path), args) + loop = asyncio.get_event_loop() + hass = HomeAssistant(loop=loop) + hass.config.config_dir = os.path.join(os.getcwd(), args.config) + data = hass_auth.Data(hass) + loop.run_until_complete(data.async_load()) + loop.run_until_complete(args.func(data, args)) -def list_users(data, args): +async def list_users(data, args): """List the users.""" count = 0 for user in data.users: @@ -52,14 +59,14 @@ def list_users(data, args): print("Total users:", count) -def add_user(data, args): +async def add_user(data, args): """Create a user.""" data.add_user(args.username, args.password) - data.save() + await data.async_save() print("User created") -def validate_login(data, args): +async def validate_login(data, args): """Validate a login.""" try: data.validate_login(args.username, args.password) @@ -68,11 +75,11 @@ def validate_login(data, args): print("Auth invalid") -def change_password(data, args): +async def change_password(data, args): """Change password.""" try: data.change_password(args.username, args.new_password) - data.save() + await data.async_save() print("Password changed") except hass_auth.InvalidUser: print("User not found") diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py index 8b12e682865746..1d9a29bf48b7be 100644 --- a/tests/auth_providers/test_homeassistant.py +++ b/tests/auth_providers/test_homeassistant.py @@ -1,60 +1,48 @@ """Test the Home Assistant local auth provider.""" -from unittest.mock import patch, mock_open - import pytest from homeassistant import data_entry_flow from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' -JSON__OPEN_PATH = 'homeassistant.util.json.open' - - -def test_initialize_empty_config_file_not_found(): - """Test that we initialize an empty config.""" - with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): - data = hass_auth.load_data(MOCK_PATH) - - assert data is not None +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_adding_user(): +async def test_adding_user(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.validate_login('test-user', 'test-pass') -def test_adding_user_duplicate_username(): +async def test_adding_user_duplicate_username(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): data.add_user('test-user', 'other-pass') -def test_validating_password_invalid_user(): +async def test_validating_password_invalid_user(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidAuth): data.validate_login('non-existing', 'pw') -def test_validating_password_invalid_password(): +async def test_validating_password_invalid_password(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('test-user', 'invalid-pass') -def test_changing_password(): +async def test_changing_password(data, hass): """Test adding a user.""" user = 'test-user' - data = hass_auth.Data(MOCK_PATH, None) data.add_user(user, 'test-pass') data.change_password(user, 'new-pass') @@ -64,61 +52,50 @@ def test_changing_password(): data.validate_login(user, 'new-pass') -def test_changing_password_raises_invalid_user(): +async def test_changing_password_raises_invalid_user(data, hass): """Test that we initialize an empty config.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidUser): data.change_password('non-existing', 'pw') -async def test_login_flow_validates(hass): +async def test_login_flow_validates(data, hass): """Test login flow.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') + await data.async_save() provider = hass_auth.HassAuthProvider(hass, None, {}) flow = hass_auth.LoginFlow(provider) result = await flow.async_step_init() assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - with patch.object(provider, '_auth_data', return_value=data): - result = await flow.async_step_init({ - 'username': 'incorrect-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' - - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'incorrect-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' - - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_saving_loading(hass): - """Test saving and loading JSON.""" - data = hass_auth.Data(MOCK_PATH, None) - data.add_user('test-user', 'test-pass') - data.add_user('second-user', 'second-pass') + result = await flow.async_step_init({ + 'username': 'incorrect-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'incorrect-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' - with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: - await hass.async_add_job(data.save) + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] - with patch('os.path.isfile', return_value=True), \ - patch(JSON__OPEN_PATH, mock_open(read_data=written), create=True): - await hass.async_add_job(hass_auth.load_data, MOCK_PATH) +async def test_saving_loading(data, hass): + """Test saving and loading JSON.""" + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + await data.async_save() + data = hass_auth.Data(hass) + await data.async_load() data.validate_login('test-user', 'test-pass') data.validate_login('second-user', 'second-pass') diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 2e837b06b58b81..e6aa7893f33b0a 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -6,16 +6,21 @@ from homeassistant.scripts import auth as script_auth from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_list_user(capsys): + +async def test_list_user(data, capsys): """Test we can list users.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.add_user('second-user', 'second-pass') - script_auth.list_users(data, None) + await script_auth.list_users(data, None) captured = capsys.readouterr() @@ -28,15 +33,12 @@ def test_list_user(capsys): ]) -def test_add_user(capsys): +async def test_add_user(data, capsys, hass_storage): """Test we can add a user.""" - data = hass_auth.Data(MOCK_PATH, None) - - with patch.object(data, 'save') as mock_save: - script_auth.add_user( - data, Mock(username='paulus', password='test-pass')) + await script_auth.add_user( + data, Mock(username='paulus', password='test-pass')) - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'User created\n' @@ -45,37 +47,34 @@ def test_add_user(capsys): data.validate_login('paulus', 'test-pass') -def test_validate_login(capsys): +async def test_validate_login(data, capsys): """Test we can validate a user login.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth valid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='invalid-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='invalid-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' -def test_change_password(capsys): +async def test_change_password(data, capsys, hass_storage): """Test we can change a password.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='test-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='test-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'Password changed\n' data.validate_login('test-user', 'new-pass') @@ -83,18 +82,35 @@ def test_change_password(capsys): data.validate_login('test-user', 'test-pass') -def test_change_password_invalid_user(capsys): +async def test_change_password_invalid_user(data, capsys, hass_storage): """Test changing password of non-existing user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='invalid-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='invalid-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 0 + assert hass_auth.STORAGE_KEY not in hass_storage captured = capsys.readouterr() assert captured.out == 'User not found\n' data.validate_login('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('invalid-user', 'new-pass') + + +def test_parsing_args(loop): + """Test we parse args correctly.""" + called = False + + async def mock_func(data, args2): + """Mock function to be called.""" + nonlocal called + called = True + assert data.hass.config.config_dir == '/somewhere/config' + assert args2 is args + + args = Mock(config='/somewhere/config', func=mock_func) + + with patch('argparse.ArgumentParser.parse_args', return_value=args): + script_auth.run(None) + + assert called, 'Mock function did not get called' From e3e014bccca3e3f505eabaf7574ee495d5ed05a7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jun 2018 16:09:46 +0200 Subject: [PATCH 110/169] Fix zwave climate operation mode mappings (#15162) --- homeassistant/components/climate/zwave.py | 33 ++++++++++++--- tests/components/climate/test_zwave.py | 49 ++++++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index c87d1507e921ec..52c544256b6768 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -7,12 +7,13 @@ # Because we do not compile openzwave on CI import logging from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, + DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,15 @@ REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } +STATE_MAPPINGS = { + 'Off': STATE_OFF, + 'Heat': STATE_HEAT, + 'Heat Mode': STATE_HEAT, + 'Heat (Default)': STATE_HEAT, + 'Cool': STATE_COOL, + 'Auto': STATE_AUTO, +} + def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" @@ -48,6 +58,7 @@ def __init__(self, values, temp_unit): self._current_temperature = None self._current_operation = None self._operation_list = None + self._operation_mapping = None self._operating_state = None self._current_fan_mode = None self._fan_list = None @@ -86,10 +97,21 @@ def update_properties(self): """Handle the data changes for node values.""" # Operation Mode if self.values.mode: - self._current_operation = self.values.mode.data + self._operation_list = [] + self._operation_mapping = {} operation_list = self.values.mode.data_items if operation_list: - self._operation_list = list(operation_list) + for mode in operation_list: + ha_mode = STATE_MAPPINGS.get(mode) + if ha_mode and ha_mode not in self._operation_mapping: + self._operation_mapping[ha_mode] = mode + self._operation_list.append(ha_mode) + continue + self._operation_list.append(mode) + current_mode = self.values.mode.data + self._current_operation = next( + (key for key, value in self._operation_mapping.items() + if value == current_mode), current_mode) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", self._current_operation) @@ -205,7 +227,8 @@ def set_fan_mode(self, fan_mode): def set_operation_mode(self, operation_mode): """Set new target operation mode.""" if self.values.mode: - self.values.mode.data = operation_mode + self.values.mode.data = self._operation_mapping.get( + operation_mode, operation_mode) def set_swing_mode(self, swing_mode): """Set new target swing mode.""" diff --git a/tests/components/climate/test_zwave.py b/tests/components/climate/test_zwave.py index fbd6ea7f798078..39a85ab493f545 100644 --- a/tests/components/climate/test_zwave.py +++ b/tests/components/climate/test_zwave.py @@ -1,9 +1,9 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import zwave +from homeassistant.components.climate import zwave, STATE_COOL, STATE_HEAT from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -46,6 +46,24 @@ def device_zxt_120(hass, mock_openzwave): yield device +@pytest.fixture +def device_mapping(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state mapping.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue(data='Off', data_items=['Off', 'Cool', 'Heat'], + node=node), + fan_mode=MockValue(data='test2', data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=6, node=node), + fan_state=MockValue(data=7, node=node), + ) + device = zwave.get_device(hass, node=node, values=values, node_config={}) + + yield device + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -109,6 +127,18 @@ def test_operation_value_set(device): assert device.values.mode.data == 'test_set' +def test_operation_value_set_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.values.mode.data == 'Off' + device.set_operation_mode(STATE_HEAT) + assert device.values.mode.data == 'Heat' + device.set_operation_mode(STATE_COOL) + assert device.values.mode.data == 'Cool' + device.set_operation_mode(STATE_OFF) + assert device.values.mode.data == 'Off' + + def test_fan_mode_value_set(device): """Test values changed for climate device.""" assert device.values.fan_mode.data == 'test2' @@ -140,6 +170,21 @@ def test_operation_value_changed(device): assert device.current_operation == 'test_updated' +def test_operation_value_changed_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.current_operation == 'off' + device.values.mode.data = 'Heat' + value_changed(device.values.mode) + assert device.current_operation == STATE_HEAT + device.values.mode.data = 'Cool' + value_changed(device.values.mode) + assert device.current_operation == STATE_COOL + device.values.mode.data = 'Off' + value_changed(device.values.mode) + assert device.current_operation == STATE_OFF + + def test_fan_mode_value_changed(device): """Test values changed for climate device.""" assert device.current_fan_mode == 'test2' From c61a652c9085efc0717682de432488fe0975c982 Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Fri, 29 Jun 2018 19:53:14 +0530 Subject: [PATCH 111/169] Fixed Indentation error (#15210) * Fixed Indentation error * Update xiaomi.py --- homeassistant/components/camera/xiaomi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index f0e66dbd20e462..e80f4b7532acff 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -113,14 +113,16 @@ def get_latest_video_url(self): except error_perm as exc: _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) return False + if self._model == MODEL_XIAOFANG: dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: _LOGGER.warning("There don't appear to be any uploaded videos") return False - latest_dir = dirs[-1] - ftp.cwd(latest_dir) + latest_dir = dirs[-1] + ftp.cwd(latest_dir) + videos = [v for v in ftp.nlst() if '.tmp' not in v] if not videos: _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) From fd38caa287b500da9e6efbd2e2034984356e99e6 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Fri, 29 Jun 2018 16:27:06 -0400 Subject: [PATCH 112/169] X-Forwarded-For improvements and bug fixes (#15204) * Use new trusted_proxies setting for X-Forwarded-For whitelist * Only use the last IP in the header Per Wikipedia (https://en.wikipedia.org/wiki/X-Forwarded-For#Format): > The last IP address is always the IP address that connects to the last proxy, > which means it is the most reliable source of information. * Add two additional tests * Ignore nonsense header values instead of failing --- .../components/emulated_hue/__init__.py | 1 + homeassistant/components/http/__init__.py | 9 +++- homeassistant/components/http/real_ip.py | 17 ++++--- tests/components/http/test_real_ip.py | 48 +++++++++++++++++++ tests/scripts/test_check_config.py | 1 + 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 708b3db83cd7d9..6988e20fb5f0dd 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -95,6 +95,7 @@ def setup(hass, yaml_config): ssl_key=None, cors_origins=None, use_x_forwarded_for=False, + trusted_proxies=[], trusted_networks=[], login_threshold=0, is_ban_enabled=False diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f769d2bc4ffba1..9d43a741ba524c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -44,6 +44,7 @@ CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' +CONF_TRUSTED_PROXIES = 'trusted_proxies' CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' @@ -86,6 +87,8 @@ vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, + vol.Optional(CONF_TRUSTED_PROXIES, default=[]): + vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, @@ -114,6 +117,7 @@ async def async_setup(hass, config): ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] + trusted_proxies = conf[CONF_TRUSTED_PROXIES] trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] @@ -132,6 +136,7 @@ async def async_setup(hass, config): ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, + trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, is_ban_enabled=is_ban_enabled @@ -173,14 +178,14 @@ class HomeAssistantHTTP(object): def __init__(self, hass, api_password, ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_networks, + use_x_forwarded_for, trusted_proxies, trusted_networks, login_threshold, is_ban_enabled): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) # This order matters - setup_real_ip(app, use_x_forwarded_for, trusted_networks) + setup_real_ip(app, use_x_forwarded_for, trusted_proxies) if is_ban_enabled: setup_bans(hass, app, login_threshold) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index 401a09dc3066fa..f8adc815fdef25 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -11,7 +11,7 @@ @callback -def setup_real_ip(app, use_x_forwarded_for, trusted_networks): +def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" @middleware async def real_ip_middleware(request, handler): @@ -21,12 +21,15 @@ async def real_ip_middleware(request, handler): request[KEY_REAL_IP] = connected_ip # Only use the XFF header if enabled, present, and from a trusted proxy - if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers and - any(connected_ip in trusted_network - for trusted_network in trusted_networks)): - request[KEY_REAL_IP] = ip_address( - request.headers.get(X_FORWARDED_FOR).split(',')[0]) + try: + if (use_x_forwarded_for and + X_FORWARDED_FOR in request.headers and + any(connected_ip in trusted_proxy + for trusted_proxy in trusted_proxies)): + request[KEY_REAL_IP] = ip_address( + request.headers.get(X_FORWARDED_FOR).split(', ')[-1]) + except ValueError: + pass return await handler(request) diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index b6af81592078fe..6cf6fec6bce994 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -58,3 +58,51 @@ async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): assert resp.status == 200 text = await resp.text() assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_untrusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('1.1.1.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_spoofed_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '222.222.222.222, 255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_nonsense_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: 'This value is invalid' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '127.0.0.1' diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8dfc5db90e0bd4..33154090286d76 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -160,6 +160,7 @@ def test_secrets(self, isfile_patch): 'server_host': '0.0.0.0', 'server_port': 8123, 'trusted_networks': [], + 'trusted_proxies': [], 'use_x_forwarded_for': False} assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} From 94b55efef3bf1728eb2dcdec9457d4d9beef7f4a Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Fri, 29 Jun 2018 23:18:44 +0200 Subject: [PATCH 113/169] Stop supporting deprecated TLS ciphers (#15217) * Stop supporting deprecated TLS ciphers * Lint --- homeassistant/components/http/__init__.py | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9d43a741ba524c..485433434fd0dd 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -51,24 +51,18 @@ # TLS configuration follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS -# Intermediate guidelines are followed. -SSL_VERSION = ssl.PROTOCOL_SSLv23 -SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 +# Modern guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | \ + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | \ + ssl.OP_CIPHER_SERVER_PREFERENCE if hasattr(ssl, 'OP_NO_COMPRESSION'): SSL_OPTS |= ssl.OP_NO_COMPRESSION -CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ +CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ - "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ - "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ - "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ - "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ - "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ - "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ - "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ - "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ - "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ - "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ - "AES256-SHA:DES-CBC3-SHA:!DSS" + "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" _LOGGER = logging.getLogger(__name__) From bbbec5a0565e9793d7880c2f30ec630176bcc6e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 17:21:50 -0400 Subject: [PATCH 114/169] Bump frontend to 20180629.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0e9d7612669152..84118e57c8f9be 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180627.0'] +REQUIREMENTS = ['home-assistant-frontend==20180629.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 1a3b6cacf9e6ff..862a6d3f8940b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180627.0 +home-assistant-frontend==20180629.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae0a5db6c336d..7bb05bbdd00ba4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180627.0 +home-assistant-frontend==20180629.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 66479dc2e55469db34bb0f918d0a815e53548afa Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 29 Jun 2018 23:25:49 +0200 Subject: [PATCH 115/169] Update eternalegypt (#15180) * Update eternalegypt to 0.0.2 * Share websession * Renames --- homeassistant/components/netgear_lte.py | 56 ++++++++++++------- .../components/notify/netgear_lte.py | 8 +-- .../components/sensor/netgear_lte.py | 14 ++--- requirements_all.txt | 2 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py index 4887ea1aa67596..23a01d37c2b81e 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte.py @@ -9,12 +9,15 @@ import voluptuous as vol import attr +import aiohttp -from homeassistant.const import CONF_HOST, CONF_PASSWORD -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util import Throttle -REQUIREMENTS = ['eternalegypt==0.0.1'] +REQUIREMENTS = ['eternalegypt==0.0.2'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -30,33 +33,34 @@ @attr.s -class LTEData: - """Class for LTE state.""" +class ModemData: + """Class for modem state.""" - eternalegypt = attr.ib() + modem = attr.ib() unread_count = attr.ib(init=False) usage = attr.ib(init=False) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Call the API to update the data.""" - information = await self.eternalegypt.information() + information = await self.modem.information() self.unread_count = sum(1 for x in information.sms if x.unread) self.usage = information.usage @attr.s -class LTEHostData: - """Container for LTE states.""" +class LTEData: + """Shared state.""" - hostdata = attr.ib(init=False, factory=dict) + websession = attr.ib() + modem_data = attr.ib(init=False, factory=dict) - def get(self, config): - """Get the requested or the only hostdata value.""" + def get_modem_data(self, config): + """Get the requested or the only modem_data value.""" if CONF_HOST in config: - return self.hostdata.get(config[CONF_HOST]) - elif len(self.hostdata) == 1: - return next(iter(self.hostdata.values())) + return self.modem_data.get(config[CONF_HOST]) + elif len(self.modem_data) == 1: + return next(iter(self.modem_data.values())) return None @@ -64,7 +68,9 @@ def get(self, config): async def async_setup(hass, config): """Set up Netgear LTE component.""" if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = LTEHostData() + websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + hass.data[DATA_KEY] = LTEData(websession) tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] if tasks: @@ -80,7 +86,17 @@ async def _setup_lte(hass, lte_config): host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] - eternalegypt = eternalegypt.LB2120(host, password) - lte_data = LTEData(eternalegypt) - await lte_data.async_update() - hass.data[DATA_KEY].hostdata[host] = lte_data + websession = hass.data[DATA_KEY].websession + + modem = eternalegypt.Modem(hostname=host, websession=websession) + await modem.login(password=password) + + modem_data = ModemData(modem) + await modem_data.async_update() + hass.data[DATA_KEY].modem_data[host] = modem_data + + async def cleanup(event): + """Clean up resources.""" + await modem.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py index b4ed53b828d6b3..97dfe504a51308 100644 --- a/homeassistant/components/notify/netgear_lte.py +++ b/homeassistant/components/notify/netgear_lte.py @@ -25,16 +25,16 @@ async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) phone = config.get(ATTR_TARGET) - return NetgearNotifyService(lte_data, phone) + return NetgearNotifyService(modem_data, phone) @attr.s class NetgearNotifyService(BaseNotificationService): """Implementation of a notification service.""" - lte_data = attr.ib() + modem_data = attr.ib() phone = attr.ib() async def async_send_message(self, message="", **kwargs): @@ -42,4 +42,4 @@ async def async_send_message(self, message="", **kwargs): targets = kwargs.get(ATTR_TARGET, self.phone) if targets and message: for target in targets: - await self.lte_data.eternalegypt.sms(target, message) + await self.modem_data.modem.sms(target, message) diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py index 859435edbc99ea..b4a3e2a1155981 100644 --- a/homeassistant/components/sensor/netgear_lte.py +++ b/homeassistant/components/sensor/netgear_lte.py @@ -29,14 +29,14 @@ async def async_setup_platform( hass, config, async_add_devices, discovery_info): """Set up Netgear LTE sensor devices.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) sensors = [] for sensortype in config[CONF_SENSORS]: if sensortype == SENSOR_SMS: - sensors.append(SMSSensor(lte_data)) + sensors.append(SMSSensor(modem_data)) elif sensortype == SENSOR_USAGE: - sensors.append(UsageSensor(lte_data)) + sensors.append(UsageSensor(modem_data)) async_add_devices(sensors, True) @@ -45,11 +45,11 @@ async def async_setup_platform( class LTESensor(Entity): """Data usage sensor entity.""" - lte_data = attr.ib() + modem_data = attr.ib() async def async_update(self): """Update state.""" - await self.lte_data.async_update() + await self.modem_data.async_update() class SMSSensor(LTESensor): @@ -63,7 +63,7 @@ def name(self): @property def state(self): """Return the state of the sensor.""" - return self.lte_data.unread_count + return self.modem_data.unread_count class UsageSensor(LTESensor): @@ -82,4 +82,4 @@ def name(self): @property def state(self): """Return the state of the sensor.""" - return round(self.lte_data.usage / 1024**2, 1) + return round(self.modem_data.usage / 1024**2, 1) diff --git a/requirements_all.txt b/requirements_all.txt index 862a6d3f8940b9..4b795a9b39b080 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ ephem==3.7.6.0 epson-projector==0.1.3 # homeassistant.components.netgear_lte -eternalegypt==0.0.1 +eternalegypt==0.0.2 # homeassistant.components.keyboard_remote # evdev==0.6.1 From 49623d2dadd05615696a3319b98ae9fae19143e0 Mon Sep 17 00:00:00 2001 From: Vignesh Venkat Date: Fri, 29 Jun 2018 14:26:06 -0700 Subject: [PATCH 116/169] Update python-wink to 1.9.1 (#15215) Fixes a bug for when GE Z-Wave fan speeds are adjusted using the wink app. --- homeassistant/components/wink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 7016250c6b184d..7c171d74967234 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.9.0', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.9.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4b795a9b39b080..1267a7028111b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1098,7 +1098,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.9.0 +python-wink==1.9.1 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.1.3 From 56f17b8651dc7867019de7295bd2ef47a6d555b6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 29 Jun 2018 23:26:48 +0200 Subject: [PATCH 117/169] =?UTF-8?q?Fix=20'AirQualityMonitorStatus'=20objec?= =?UTF-8?q?t=20has=20no=20attribute=20=E2=80=98time=5Fstate=E2=80=99=20(#1?= =?UTF-8?q?5216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/sensor/xiaomi_miio.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index a70d701fac639b..63d93d31cf37a4 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -30,7 +30,11 @@ ATTR_POWER = 'power' ATTR_CHARGING = 'charging' ATTR_BATTERY_LEVEL = 'battery_level' -ATTR_TIME_STATE = 'time_state' +ATTR_DISPLAY_CLOCK = 'display_clock' +ATTR_NIGHT_MODE = 'night_mode' +ATTR_NIGHT_TIME_BEGIN = 'night_time_begin' +ATTR_NIGHT_TIME_END = 'night_time_end' +ATTR_SENSOR_STATE = 'sensor_state' ATTR_MODEL = 'model' SUCCESS = ['ok'] @@ -85,7 +89,11 @@ def __init__(self, name, device, model, unique_id): ATTR_POWER: None, ATTR_BATTERY_LEVEL: None, ATTR_CHARGING: None, - ATTR_TIME_STATE: None, + ATTR_DISPLAY_CLOCK: None, + ATTR_NIGHT_MODE: None, + ATTR_NIGHT_TIME_BEGIN: None, + ATTR_NIGHT_TIME_END: None, + ATTR_SENSOR_STATE: None, ATTR_MODEL: self._model, } @@ -143,7 +151,11 @@ async def async_update(self): ATTR_POWER: state.power, ATTR_CHARGING: state.usb_power, ATTR_BATTERY_LEVEL: state.battery, - ATTR_TIME_STATE: state.time_state, + ATTR_DISPLAY_CLOCK: state.display_clock, + ATTR_NIGHT_MODE: state.night_mode, + ATTR_NIGHT_TIME_BEGIN: state.night_time_begin, + ATTR_NIGHT_TIME_END: state.night_time_end, + ATTR_SENSOR_STATE: state.sensor_state, }) except DeviceException as ex: From 2524dca7bfc006b9dd1b4aaaf9b90e2b2d7a1ad7 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 29 Jun 2018 14:27:18 -0700 Subject: [PATCH 118/169] Use cached states for neato when possible (#15218) --- homeassistant/components/switch/neato.py | 2 +- homeassistant/components/vacuum/neato.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index dca5d63b43dd30..34dad9bb5818ad 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -67,7 +67,7 @@ def update(self): _LOGGER.debug('self._state=%s', self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) - if self.robot.schedule_enabled: + if self._state['details']['isScheduleEnabled']: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 1b32fff9e5b8f7..6289fed265d065 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -96,14 +96,14 @@ def update(self): elif self._state['state'] == 4: self._status_state = ERRORS.get(self._state['error']) - if (self.robot.state['action'] == 1 or - self.robot.state['action'] == 2 or - self.robot.state['action'] == 3 and - self.robot.state['state'] == 2): + if (self._state['action'] == 1 or + self._state['action'] == 2 or + self._state['action'] == 3 and + self._state['state'] == 2): self._clean_state = STATE_ON - elif (self.robot.state['action'] == 11 or - self.robot.state['action'] == 12 and - self.robot.state['state'] == 2): + elif (self._state['action'] == 11 or + self._state['action'] == 12 and + self._state['state'] == 2): self._clean_state = STATE_ON else: self._clean_state = STATE_OFF From 10d1e81f10f061df995c3f4bdde3386069791e91 Mon Sep 17 00:00:00 2001 From: Leonardo Brondani Schenkel Date: Sat, 30 Jun 2018 00:59:10 +0200 Subject: [PATCH 119/169] deconz: fix light.turn_off with transition (#15222) When light.turn_off is invoked with a transition, the following payload was sent to deCONZ via PUT to /light/N/state: { "bri": 0, "transitiontime": transition } However, on recent versions of deCONZ (latest is 2.05.31 at the time of writing) this does not turn off the light, just sets it to minimum brightness level (brightness is clamped to minimum level the light supports without turning it off). This commit makes the code send this payload instead: { "on": false, "transitiontime": transition } This works as intended and the light does transition to the 'off' state. This change was tested with Philips Hue colored lights, IKEA colored lights and IKEA white spectrum lights: they were all able to be turned off successfully with the new payload, and none of them could be turned off with the old payload. --- homeassistant/components/light/deconz.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 05907ea86ee275..d6078490e7f960 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -174,7 +174,6 @@ async def async_turn_off(self, **kwargs): data = {'on': False} if ATTR_TRANSITION in kwargs: - data = {'bri': 0} data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10 if ATTR_FLASH in kwargs: From 27a37e2013bbdc46cf4d249c957325829d701c89 Mon Sep 17 00:00:00 2001 From: pepeEL Date: Sat, 30 Jun 2018 14:56:43 +0200 Subject: [PATCH 120/169] Add new RTS device (#15116) * Add new RTS device Add new RTS Somfy device as cover-ExteriorVenetianBlindRTSComponent * add next device add next device --- homeassistant/components/tahoma.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 84edd9afd40990..ba91dd7c1fc27d 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -40,6 +40,8 @@ 'rts:CurtainRTSComponent': 'cover', 'rts:BlindRTSComponent': 'cover', 'rts:VenetianBlindRTSComponent': 'cover', + 'rts:DualCurtainRTSComponent': 'cover', + 'rts:ExteriorVenetianBlindRTSComponent': 'cover', 'io:ExteriorVenetianBlindIOComponent': 'cover', 'io:RollerShutterUnoIOComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', From c5ceb40598f167d6726d5f6700dfb3365948dd36 Mon Sep 17 00:00:00 2001 From: Carl Chan Date: Sat, 30 Jun 2018 08:57:48 -0400 Subject: [PATCH 121/169] Add additional parameters to NUT UPS sensor (#15066) * Update nut.py Added input.frequency and a number of output parameters. * Update nut.py Fixed formatting issues Added "devices" fields * Separated "device.description" line to two lines. * Update nut.py Removed device.* sensors --- homeassistant/components/sensor/nut.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index bf440728a2ee55..7c7ff3480b00e0 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -107,6 +107,20 @@ ['Voltage Transfer Reason', '', 'mdi:information-outline'], 'input.voltage': ['Input Voltage', 'V', 'mdi:flash'], 'input.voltage.nominal': ['Nominal Input Voltage', 'V', 'mdi:flash'], + 'input.frequency': ['Input Line Frequency', 'hz', 'mdi:flash'], + 'input.frequency.nominal': + ['Nominal Input Line Frequency', 'hz', 'mdi:flash'], + 'input.frequency.status': + ['Input Frequency Status', '', 'mdi:information-outline'], + 'output.current': ['Output Current', 'A', 'mdi:flash'], + 'output.current.nominal': + ['Nominal Output Current', 'A', 'mdi:flash'], + 'output.voltage': ['Output Voltage', 'V', 'mdi:flash'], + 'output.voltage.nominal': + ['Nominal Output Voltage', 'V', 'mdi:flash'], + 'output.frequency': ['Output Frequency', 'hz', 'mdi:flash'], + 'output.frequency.nominal': + ['Nominal Output Frequency', 'hz', 'mdi:flash'], } STATE_TYPES = { From 0aad056ca75113c202841d1bee72f181dd857357 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 30 Jun 2018 17:12:00 +0200 Subject: [PATCH 122/169] Fix typos (#15233) --- homeassistant/components/watson_iot.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/watson_iot.py b/homeassistant/components/watson_iot.py index 246cf3a96c28ea..889984eb223bf8 100644 --- a/homeassistant/components/watson_iot.py +++ b/homeassistant/components/watson_iot.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/watson_iot/ """ - import logging import queue import threading @@ -13,8 +12,8 @@ import voluptuous as vol from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, - CONF_TOKEN, CONF_TYPE, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_ID, CONF_INCLUDE, + CONF_TOKEN, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) from homeassistant.helpers import state as state_helper import homeassistant.helpers.config_validation as cv @@ -24,13 +23,13 @@ _LOGGER = logging.getLogger(__name__) CONF_ORG = 'organization' -CONF_ID = 'id' DOMAIN = 'watson_iot' -RETRY_DELAY = 20 MAX_TRIES = 3 +RETRY_DELAY = 20 + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(vol.Schema({ vol.Required(CONF_ORG): cv.string, @@ -103,7 +102,7 @@ def event_to_json(event): }, 'time': event.time_fired.isoformat(), 'fields': { - 'state': state.state + 'state': state.state, } } if _state_as_value is not None: @@ -113,7 +112,7 @@ def event_to_json(event): if key != 'unit_of_measurement': # If the key is already in fields if key in out_event['fields']: - key = key + "_" + key = '{}_'.format(key) # For each value we try to cast it as float # But if we can not do it we store the value # as string @@ -153,7 +152,7 @@ def __init__(self, hass, gateway, event_to_json): hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) def _event_listener(self, event): - """Listen for new messages on the bus and queue them for Watson IOT.""" + """Listen for new messages on the bus and queue them for Watson IoT.""" item = (time.monotonic(), event) self.queue.put(item) @@ -191,7 +190,7 @@ def write_to_watson(self, events): field, 'json', value) if not device_success: _LOGGER.error( - "Failed to publish message to watson iot") + "Failed to publish message to Watson IoT") continue break except (ibmiotf.MissingMessageEncoderException, IOError): @@ -199,7 +198,7 @@ def write_to_watson(self, events): time.sleep(RETRY_DELAY) else: _LOGGER.exception( - "Failed to publish message to watson iot") + "Failed to publish message to Watson IoT") def run(self): """Process incoming events.""" From 3da46421949fde5fc0509565ce6296b15c9eac9a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Jun 2018 18:10:59 +0200 Subject: [PATCH 123/169] Use async syntax for cover platforms (#15230) --- .../components/cover/lutron_caseta.py | 17 +++---- homeassistant/components/cover/mqtt.py | 35 +++++-------- homeassistant/components/cover/rflink.py | 5 +- homeassistant/components/cover/template.py | 50 ++++++++----------- homeassistant/components/cover/velbus.py | 6 +-- homeassistant/components/cover/wink.py | 5 +- 6 files changed, 44 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py index 1ed502e0f7f8af..87821b802ba6b9 100644 --- a/homeassistant/components/cover/lutron_caseta.py +++ b/homeassistant/components/cover/lutron_caseta.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.cover import ( @@ -18,8 +17,8 @@ DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Lutron Caseta shades as a cover device.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -49,25 +48,21 @@ def current_cover_position(self): """Return the current position of cover.""" return self._state['current_state'] - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" self._smartbridge.set_value(self._device_id, 0) - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" self._smartbridge.set_value(self._device_id, 100) - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] self._smartbridge.set_value(self._device_id, position) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Call when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 235ff5799cc810..62e1069e18bb32 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -93,8 +92,8 @@ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the MQTT Cover.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -174,10 +173,9 @@ def __init__(self, name, state_topic, command_topic, availability_topic, self._position_topic = position_topic self._set_position_template = set_position_template - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def tilt_updated(topic, payload, qos): @@ -218,7 +216,7 @@ def state_message_received(topic, payload, qos): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) @@ -227,7 +225,7 @@ def state_message_received(topic, payload, qos): else: self._tilt_optimistic = False self._tilt_value = STATE_UNKNOWN - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._tilt_status_topic, tilt_updated, self._qos) @property @@ -278,8 +276,7 @@ def supported_features(self): return supported_features - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up. This method is a coroutine. @@ -292,8 +289,7 @@ def async_open_cover(self, **kwargs): self._state = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down. This method is a coroutine. @@ -306,8 +302,7 @@ def async_close_cover(self, **kwargs): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the device. This method is a coroutine. @@ -316,8 +311,7 @@ def async_stop_cover(self, **kwargs): self.hass, self._command_topic, self._payload_stop, self._qos, self._retain) - @asyncio.coroutine - def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" mqtt.async_publish(self.hass, self._tilt_command_topic, self._tilt_open_position, self._qos, @@ -326,8 +320,7 @@ def async_open_cover_tilt(self, **kwargs): self._tilt_value = self._tilt_open_position self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" mqtt.async_publish(self.hass, self._tilt_command_topic, self._tilt_closed_position, self._qos, @@ -336,8 +329,7 @@ def async_close_cover_tilt(self, **kwargs): self._tilt_value = self._tilt_closed_position self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" if ATTR_TILT_POSITION not in kwargs: return @@ -350,8 +342,7 @@ def async_set_cover_tilt_position(self, **kwargs): mqtt.async_publish(self.hass, self._tilt_command_topic, level, self._qos, self._retain) - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index a9b7598159f967..3357bf2d204fb0 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.rflink/ """ -import asyncio import logging import voluptuous as vol @@ -79,8 +78,8 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Rflink cover platform.""" async_add_devices(devices_from_config(config, hass)) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 4e197365a7098e..d9d0d61c77a8f0 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.template/ """ -import asyncio import logging import voluptuous as vol @@ -72,8 +71,8 @@ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Template cover.""" covers = [] @@ -199,8 +198,7 @@ def __init__(self, hass, device_id, friendly_name, state_template, if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_cover_state_listener(entity, old_state, new_state): @@ -277,70 +275,62 @@ def should_poll(self): """Return the polling state.""" return False - @asyncio.coroutine - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - yield from self._open_script.async_run() + await self._open_script.async_run() elif self._position_script: - yield from self._position_script.async_run({"position": 100}) + await self._position_script.async_run({"position": 100}) if self._optimistic: self._position = 100 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - yield from self._close_script.async_run() + await self._close_script.async_run() elif self._position_script: - yield from self._position_script.async_run({"position": 0}) + await self._position_script.async_run({"position": 0}) if self._optimistic: self._position = 0 self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Fire the stop action.""" if self._stop_script: - yield from self._stop_script.async_run() + await self._stop_script.async_run() - @asyncio.coroutine - def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] - yield from self._position_script.async_run( + await self._position_script.async_run( {"position": self._position}) if self._optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run({"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" self._tilt_value = 0 - yield from self._tilt_script.async_run( + await self._tilt_script.async_run( {"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + await self._tilt_script.async_run({"tilt": self._tilt_value}) if self._tilt_optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" if self._template is not None: try: diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py index ab5d6e8ef7947a..fd060e7a7e1c07 100644 --- a/homeassistant/components/cover/velbus.py +++ b/homeassistant/components/cover/velbus.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/cover.velbus/ """ import logging -import asyncio import time import voluptuous as vol @@ -70,15 +69,14 @@ def __init__(self, velbus, name, module, open_channel, close_channel): self._open_channel = open_channel self._close_channel = close_channel - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add listener for Velbus messages on bus.""" def _init_velbus(): """Initialize Velbus on startup.""" self._velbus.subscribe(self._on_message) self.get_status() - yield from self.hass.async_add_job(_init_velbus) + await self.hass.async_add_job(_init_velbus) def _on_message(self, message): import velbus diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 7f7a3a116443a7..2206de05041435 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -4,8 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.wink/ """ -import asyncio - from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \ ATTR_POSITION from homeassistant.components.wink import WinkDevice, DOMAIN @@ -34,8 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkCoverDevice(WinkDevice, CoverDevice): """Representation of a Wink cover device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['cover'].append(self) From f874efb224fd2133793234ff924d7e445f013cc9 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 30 Jun 2018 19:31:36 -0700 Subject: [PATCH 124/169] By default to use access_token if hass.auth.active (#15212) * Force to use access_token if hass.auth.active * Not allow Basic auth with api_password if hass.auth.active * Block websocket api_password auth when hass.auth.active * Add legacy_api_password auth provider * lint * lint --- homeassistant/auth.py | 14 +- .../auth_providers/legacy_api_password.py | 104 +++++++++++++ homeassistant/components/http/__init__.py | 17 +- homeassistant/components/http/auth.py | 66 ++++---- homeassistant/components/websocket_api.py | 24 +-- .../test_legacy_api_password.py | 67 ++++++++ tests/components/http/test_auth.py | 147 ++++++++++++++++-- tests/components/test_websocket_api.py | 108 +++++++++---- 8 files changed, 466 insertions(+), 81 deletions(-) create mode 100644 homeassistant/auth_providers/legacy_api_password.py create mode 100644 tests/auth_providers/test_legacy_api_password.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 22abcdf213ccc2..f56e00bf31e553 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -280,6 +280,18 @@ def active(self): """Return if any auth providers are registered.""" return bool(self._providers) + @property + def support_legacy(self): + """ + Return if legacy_api_password auth providers are registered. + + Should be removed when we removed legacy_api_password auth providers. + """ + for provider_type, _ in self._providers: + if provider_type == 'legacy_api_password': + return True + return False + @property def async_auth_providers(self): """Return a list of available auth providers.""" @@ -534,7 +546,7 @@ async def async_load(self): client_id=rt_dict['client_id'], created_at=dt_util.parse_datetime(rt_dict['created_at']), access_token_expiration=timedelta( - rt_dict['access_token_expiration']), + seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], ) refresh_tokens[token.id] = token diff --git a/homeassistant/auth_providers/legacy_api_password.py b/homeassistant/auth_providers/legacy_api_password.py new file mode 100644 index 00000000000000..510cc4d02792fe --- /dev/null +++ b/homeassistant/auth_providers/legacy_api_password.py @@ -0,0 +1,104 @@ +""" +Support Legacy API password auth provider. + +It will be removed when auth system production ready +""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant.exceptions import HomeAssistantError +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + +LEGACY_USER = 'homeassistant' + + +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + +@auth.AUTH_PROVIDERS.register('legacy_api_password') +class LegacyApiPasswordAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + DEFAULT_TITLE = 'Legacy API Password' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, password): + """Helper to validate a username and password.""" + if not hasattr(self.hass, 'http'): + raise ValueError('http component is not loaded') + + if self.hass.http.api_password is None: + raise ValueError('http component is not configured using' + ' api_password') + + if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'), + password.encode('utf-8')): + raise InvalidAuthError + + async def async_get_or_create_credentials(self, flow_result): + """Return LEGACY_USER always.""" + for credential in await self.async_credentials(): + if credential.data['username'] == LEGACY_USER: + return credential + + return self.async_create_credentials({ + 'username': LEGACY_USER + }) + + async def async_user_meta_for_credentials(self, credentials): + """ + Set name as LEGACY_USER always. + + Will be used to populate info when creating a new user. + """ + return {'name': LEGACY_USER} + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + self._auth_provider.async_validate_login( + user_input['password']) + except InvalidAuthError: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data={} + ) + + schema = OrderedDict() + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 485433434fd0dd..37a6805dfb58b8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -184,7 +184,22 @@ def __init__(self, hass, api_password, if is_ban_enabled: setup_bans(hass, app, login_threshold) - setup_auth(app, trusted_networks, api_password) + if hass.auth.active: + if hass.auth.support_legacy: + _LOGGER.warning("Experimental auth api enabled and " + "legacy_api_password support enabled. Please " + "use access_token instead api_password, " + "although you can still use legacy " + "api_password") + else: + _LOGGER.warning("Experimental auth api enabled. Please use " + "access_token instead api_password.") + elif api_password is None: + _LOGGER.warning("You have been advised to set http.api_password.") + + setup_auth(app, trusted_networks, hass.auth.active, + support_legacy=hass.auth.support_legacy, + api_password=api_password) if cors_origins: setup_cors(app, cors_origins) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index c4723abccee348..a232d9295a4d7f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -17,37 +17,44 @@ @callback -def setup_auth(app, trusted_networks, api_password): +def setup_auth(app, trusted_networks, use_auth, + support_legacy=False, api_password=None): """Create auth middleware for the app.""" @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" - # If no password set, just always set authenticated=True - if api_password is None: - request[KEY_AUTHENTICATED] = True - return await handler(request) - - # Check authentication authenticated = False - if (HTTP_HEADER_HA_AUTH in request.headers and - hmac.compare_digest( - api_password.encode('utf-8'), - request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): + if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or + DATA_API_PASSWORD in request.query): + _LOGGER.warning('Please use access_token instead api_password.') + + legacy_auth = (not use_auth or support_legacy) and api_password + if (hdrs.AUTHORIZATION in request.headers and + await async_validate_auth_header( + request, api_password if legacy_auth else None)): + # it included both use_auth and api_password Basic auth + authenticated = True + + elif (legacy_auth and HTTP_HEADER_HA_AUTH in request.headers and + hmac.compare_digest( + api_password.encode('utf-8'), + request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): # A valid auth header has been set authenticated = True - elif (DATA_API_PASSWORD in request.query and + elif (legacy_auth and DATA_API_PASSWORD in request.query and hmac.compare_digest( api_password.encode('utf-8'), request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True - elif (hdrs.AUTHORIZATION in request.headers and - await async_validate_auth_header(api_password, request)): + elif _is_trusted_ip(request, trusted_networks): authenticated = True - elif _is_trusted_ip(request, trusted_networks): + elif not use_auth and api_password is None: + # If neither password nor auth_providers set, + # just always set authenticated=True authenticated = True request[KEY_AUTHENTICATED] = authenticated @@ -76,8 +83,12 @@ def validate_password(request, api_password): request.app['hass'].http.api_password.encode('utf-8')) -async def async_validate_auth_header(api_password, request): - """Test an authorization header if valid password.""" +async def async_validate_auth_header(request, api_password=None): + """ + Test authorization header against access token. + + Basic auth_type is legacy code, should be removed with api_password. + """ if hdrs.AUTHORIZATION not in request.headers: return False @@ -88,7 +99,16 @@ async def async_validate_auth_header(api_password, request): # If no space in authorization header return False - if auth_type == 'Basic': + if auth_type == 'Bearer': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: + return False + + request['hass_user'] = access_token.refresh_token.user + return True + + elif auth_type == 'Basic' and api_password is not None: decoded = base64.b64decode(auth_val).decode('utf-8') try: username, password = decoded.split(':', 1) @@ -102,13 +122,5 @@ async def async_validate_auth_header(api_password, request): return hmac.compare_digest(api_password.encode('utf-8'), password.encode('utf-8')) - if auth_type != 'Bearer': + else: return False - - hass = request.app['hass'] - access_token = hass.auth.async_get_access_token(auth_val) - if access_token is None: - return False - - request['hass_user'] = access_token.refresh_token.user - return True diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index bf472348babf8c..c26f68a2c29f0f 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -315,26 +315,32 @@ def handle_hass_stop(event): authenticated = True else: + self.debug("Request auth") await self.wsock.send_json(auth_required_message()) msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if 'api_password' in msg: - authenticated = validate_password( - request, msg['api_password']) - - elif 'access_token' in msg: + if self.hass.auth.active and 'access_token' in msg: + self.debug("Received access_token") token = self.hass.auth.async_get_access_token( msg['access_token']) authenticated = token is not None + elif ((not self.hass.auth.active or + self.hass.auth.support_legacy) and + 'api_password' in msg): + self.debug("Received api_password") + authenticated = validate_password( + request, msg['api_password']) + if not authenticated: - self.debug("Invalid password") + self.debug("Authorization failed") await self.wsock.send_json( - auth_invalid_message('Invalid password')) + auth_invalid_message('Invalid access token or password')) await process_wrong_login(request) return wsock + self.debug("Auth OK") await self.wsock.send_json(auth_ok_message()) # ---------- AUTH PHASE OVER ---------- @@ -392,7 +398,7 @@ def handle_hass_stop(event): if wsock.closed: self.debug("Connection closed by client") else: - _LOGGER.exception("Unexpected TypeError: %s", msg) + _LOGGER.exception("Unexpected TypeError: %s", err) except ValueError as err: msg = "Received invalid JSON" @@ -403,7 +409,7 @@ def handle_hass_stop(event): self._writer_task.cancel() except CANCELLATION_ERRORS: - self.debug("Connection cancelled by server") + self.debug("Connection cancelled") except asyncio.QueueFull: self.log_error("Client exceeded max pending messages [1]:", diff --git a/tests/auth_providers/test_legacy_api_password.py b/tests/auth_providers/test_legacy_api_password.py new file mode 100644 index 00000000000000..7a8f17894aa1fc --- /dev/null +++ b/tests/auth_providers/test_legacy_api_password.py @@ -0,0 +1,67 @@ +"""Tests for the legacy_api_password auth provider.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import legacy_api_password + + +@pytest.fixture +def store(hass): + """Mock store.""" + return auth.AuthStore(hass) + + +@pytest.fixture +def provider(hass, store): + """Mock provider.""" + return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, { + 'type': 'legacy_api_password', + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({}) + assert credentials.data["username"] is legacy_api_password.LEGACY_USER + assert credentials.is_new is True + + +async def test_only_one_credentials(store, provider): + """Call create twice will return same credential.""" + credentials = await provider.async_get_or_create_credentials({}) + await store.async_get_or_create_user(credentials, provider) + credentials2 = await provider.async_get_or_create_credentials({}) + assert credentials2.data["username"] is legacy_api_password.LEGACY_USER + assert credentials2.id is credentials.id + assert credentials2.is_new is False + + +async def test_verify_not_load(hass, provider): + """Test we raise if http module not load.""" + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password=None) + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + + +async def test_verify_login(hass, provider): + """Test we raise if http module not load.""" + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + with pytest.raises(legacy_api_password.InvalidAuthError): + provider.async_validate_login('invalid-password') + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': '🎉', + 'password': '😎', + }) + assert credentials.is_new is True diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index dd8b2cd35c46c2..3e5eed4c924b55 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,20 +1,23 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access from ipaddress import ip_network -from unittest.mock import patch +from unittest.mock import patch, Mock +import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized -import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH -from homeassistant.setup import async_setup_component +from homeassistant.auth import AccessToken, RefreshToken from homeassistant.components.http.auth import setup_auth -from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_AUTHENTICATED - +from homeassistant.components.http.real_ip import setup_real_ip +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.setup import async_setup_component from . import mock_real_ip + +ACCESS_TOKEN = 'tk.1234' + API_PASSWORD = 'test1234' # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases @@ -36,15 +39,37 @@ async def mock_handler(request): return web.Response(status=200) +def mock_async_get_access_token(token): + """Return if token is valid.""" + if token == ACCESS_TOKEN: + return Mock(spec=AccessToken, + token=ACCESS_TOKEN, + refresh_token=Mock(spec=RefreshToken)) + else: + return None + + @pytest.fixture def app(): """Fixture to setup a web.Application.""" app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) app.router.add_get('/', mock_handler) setup_real_ip(app, False, []) return app +@pytest.fixture +def app2(): + """Fixture to setup a web.Application without real_ip middleware.""" + app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) + app.router.add_get('/', mock_handler) + return app + + async def test_auth_middleware_loaded_by_default(hass): """Test accessing to server from banned IP when feature is off.""" with patch('homeassistant.components.http.setup_auth') as mock_setup: @@ -57,7 +82,7 @@ async def test_auth_middleware_loaded_by_default(hass): async def test_access_without_password(app, aiohttp_client): """Test access without password.""" - setup_auth(app, [], None) + setup_auth(app, [], False, api_password=None) client = await aiohttp_client(app) resp = await client.get('/') @@ -65,8 +90,8 @@ async def test_access_without_password(app, aiohttp_client): async def test_access_with_password_in_header(app, aiohttp_client): - """Test access with password in URL.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in header.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -79,8 +104,8 @@ async def test_access_with_password_in_header(app, aiohttp_client): async def test_access_with_password_in_query(app, aiohttp_client): - """Test access without password.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in URL.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) resp = await client.get('/', params={ @@ -99,7 +124,7 @@ async def test_access_with_password_in_query(app, aiohttp_client): async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" - setup_auth(app, [], API_PASSWORD) + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -125,16 +150,64 @@ async def test_basic_auth_works(app, aiohttp_client): assert req.status == 401 -async def test_access_with_trusted_ip(aiohttp_client): +async def test_access_with_trusted_ip(app2, aiohttp_client): """Test access with an untrusted ip address.""" - app = web.Application() - app.router.add_get('/', mock_handler) + setup_auth(app2, TRUSTED_NETWORKS, False, api_password='some-pass') - setup_auth(app, TRUSTED_NETWORKS, 'some-pass') + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) - set_mock_ip = mock_real_ip(app) + for remote_addr in UNTRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 401, \ + "{} shouldn't be trusted".format(remote_addr) + + for remote_addr in TRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 200, \ + "{} should be trusted".format(remote_addr) + + +async def test_auth_active_access_with_access_token_in_header( + app, aiohttp_client): + """Test access with access token in header.""" + setup_auth(app, [], True, api_password=None) client = await aiohttp_client(app) + req = await client.get( + '/', headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'AUTHORIZATION': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'Authorization': ACCESS_TOKEN}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'BEARER {}'.format(ACCESS_TOKEN)}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'Bearer wrong-pass'}) + assert req.status == 401 + + +async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client): + """Test access with an untrusted ip address.""" + setup_auth(app2, TRUSTED_NETWORKS, True, api_password=None) + + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) + for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get('/') @@ -146,3 +219,43 @@ async def test_access_with_trusted_ip(aiohttp_client): resp = await client.get('/') assert resp.status == 200, \ "{} should be trusted".format(remote_addr) + + +async def test_auth_active_blocked_api_password_access(app, aiohttp_client): + """Test access using api_password should be blocked when auth.active.""" + setup_auth(app, [], True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 401 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 401 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 401 + + +async def test_auth_legacy_support_api_password_access(app, aiohttp_client): + """Test access using api_password if auth.support_legacy.""" + setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 200 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 200 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 200 diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index fbd8584a7d14ae..6ea90bcdb88f24 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -77,7 +77,7 @@ def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): assert mock_process_wrong_login.called assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'] == 'Invalid password' + assert msg['message'] == 'Invalid access token or password' @asyncio.coroutine @@ -316,47 +316,103 @@ def test_unknown_command(websocket_client): assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND -async def test_auth_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token - }) + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK -async def test_auth_with_invalid_token(hass, aiohttp_client): +async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, 'api_password': API_PASSWORD - } - }) + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_auth_legacy_support_with_password(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active', + return_value=True),\ + patch('homeassistant.auth.AuthManager.support_legacy', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': 'incorrect' - }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID From cfe7c0aa017c316a3fe97456e6eef44e1c41afa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 1 Jul 2018 11:40:23 +0300 Subject: [PATCH 125/169] Upgrade pytest to 3.6.2 (#15241) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 7ee0e166cf2839..d6e92d5b8ffe30 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.0 -pytest==3.6.1 +pytest==3.6.2 requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bb05bbdd00ba4..bdd96562206dc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.0 -pytest==3.6.1 +pytest==3.6.2 requests_mock==1.5 From c0229ebb77c0906b6f0258d6a44c37b8b07eea69 Mon Sep 17 00:00:00 2001 From: Yevgeniy <33804747+sgttrs@users.noreply.github.com> Date: Sun, 1 Jul 2018 15:54:24 +0600 Subject: [PATCH 126/169] Add precipitations to Openweathermap daily forecast mode (#15240) * Add precipitations to daily forecast mode * Remove line breaks --- 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 8354757ff33eed..65fa7c8cb0f9e8 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -156,6 +156,8 @@ def forecast(self): entry.get_temperature('celsius').get('day'), ATTR_FORECAST_TEMP_LOW: entry.get_temperature('celsius').get('night'), + ATTR_FORECAST_PRECIPITATION: + entry.get_rain().get('all'), ATTR_FORECAST_WIND_SPEED: entry.get_wind().get('speed'), ATTR_FORECAST_WIND_BEARING: @@ -223,12 +225,10 @@ def update_forecast(self): try: if self._mode == 'daily': fcd = self.owm.daily_forecast_at_coords( - self.latitude, self.longitude, 15 - ) + self.latitude, self.longitude, 15) else: fcd = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) + self.latitude, self.longitude) except APICallError: _LOGGER.error("Exception when calling OWM web API " "to update forecast") From 3c04b0756f5fb91ff54a9a88297fdb141d9b87ca Mon Sep 17 00:00:00 2001 From: Leonardo Brondani Schenkel Date: Sun, 1 Jul 2018 12:32:48 +0200 Subject: [PATCH 127/169] deconz: proper fix light.turn_off with transition (#15227) Previous commit d4f7dfa successfully fixed the bug in which lights would not turn off if a transition was specified, however if 'bri' is not present in the payload of the PUT request set to deCONZ, then any 'transitiontime' ends up being ignored. This commit addresses the unintended side effect by reintroducing 'bri', resulting in the following payload: { "on": false, "bri": 0, "transitiontime": ... } --- homeassistant/components/light/deconz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index d6078490e7f960..08d7f5773f7579 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -174,6 +174,7 @@ async def async_turn_off(self, **kwargs): data = {'on': False} if ATTR_TRANSITION in kwargs: + data['bri'] = 0 data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10 if ATTR_FLASH in kwargs: From 4c258ce08b6e53c491f948332397c128e0e83787 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Jul 2018 17:48:54 +0200 Subject: [PATCH 128/169] Revert some changes to setup.py (#15248) --- setup.cfg | 14 -------------- setup.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2abd445bb855f4..7813cc5c0472ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,20 +15,6 @@ classifier = Programming Language :: Python :: 3.6 Topic :: Home Automation -[options] -packages = find: -include_package_data = true -zip_safe = false - -[options.entry_points] -console_scripts = - hass = homeassistant.__main__:main - -[options.packages.find] -exclude = - tests - tests.* - [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 3833f90f2d1055..928d894c9d1a9f 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" from datetime import datetime as dt -from setuptools import setup +from setuptools import setup, find_packages import homeassistant.const as hass_const @@ -29,6 +29,8 @@ 'Forum': 'https://community.home-assistant.io/', } +PACKAGES = find_packages(exclude=['tests', 'tests.*']) + REQUIRES = [ 'aiohttp==3.3.2', 'astral==1.6.1', @@ -53,7 +55,15 @@ project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, + packages=PACKAGES, + include_package_data=True, + zip_safe=False, install_requires=REQUIRES, python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', + entry_points={ + 'console_scripts': [ + 'hass = homeassistant.__main__:main' + ] + }, ) From 136cc1d44decd8bcd3324254722e780f0dce26d1 Mon Sep 17 00:00:00 2001 From: David Thulke Date: Sun, 1 Jul 2018 17:51:40 +0200 Subject: [PATCH 129/169] allow extra slot values in intents (#15246) --- homeassistant/helpers/intent.py | 3 ++- tests/components/test_snips.py | 44 +++++++++++++++++++++++++++++++++ tests/helpers/test_intent.py | 36 ++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 5aa53f17e7bba0..4357c4109ebbf1 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -137,7 +137,8 @@ def async_validate_slots(self, slots): if self._slot_schema is None: self._slot_schema = vol.Schema({ key: SLOT_SCHEMA.extend({'value': validator}) - for key, validator in self.slot_schema.items()}) + for key, validator in self.slot_schema.items()}, + extra=vol.ALLOW_EXTRA) return self._slot_schema(slots) diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index d9238336768f9d..baeda2c49a839b 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -5,6 +5,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips +from homeassistant.helpers.intent import (ServiceIntentHandler, async_register) from tests.common import (async_fire_mqtt_message, async_mock_intent, async_mock_service) @@ -124,6 +125,49 @@ async def test_snips_intent(hass, mqtt_mock): assert intent.text_input == 'turn the lights green' +async def test_snips_service_intent(hass, mqtt_mock): + """Test ServiceIntentHandler via Snips.""" + hass.states.async_set('light.kitchen', 'off') + calls = async_mock_service(hass, 'light', 'turn_on') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + payload = """ + { + "input": "turn the light on", + "intent": { + "intentName": "Lights", + "probability": 0.85 + }, + "siteId": "default", + "slots": [ + { + "slotName": "name", + "value": { + "kind": "Custom", + "value": "kitchen" + } + } + ] + } + """ + + async_register(hass, ServiceIntentHandler( + "Lights", "light", 'turn_on', "Turned {} on")) + + async_fire_mqtt_message(hass, 'hermes/intent/Lights', + payload) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'light' + assert calls[0].service == 'turn_on' + assert calls[0].data['entity_id'] == 'light.kitchen' + assert 'probability' not in calls[0].data + assert 'site_id' not in calls[0].data + + async def test_snips_intent_with_duration(hass, mqtt_mock): """Test intent with Snips duration.""" result = await async_setup_component(hass, "snips", { diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index a8d37a249bc92c..707129ae531743 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,6 +1,18 @@ """Tests for the intent helpers.""" + +import unittest +import voluptuous as vol + from homeassistant.core import State -from homeassistant.helpers import intent +from homeassistant.helpers import (intent, config_validation as cv) + + +class MockIntentHandler(intent.IntentHandler): + """Provide a mock intent handler.""" + + def __init__(self, slot_schema): + """Initialize the mock handler.""" + self.slot_schema = slot_schema def test_async_match_state(): @@ -10,3 +22,25 @@ def test_async_match_state(): state = intent.async_match_state(None, 'kitch', [state1, state2]) assert state is state1 + + +class TestIntentHandler(unittest.TestCase): + """Test the Home Assistant event helpers.""" + + def test_async_validate_slots(self): + """Test async_validate_slots of IntentHandler.""" + handler1 = MockIntentHandler({ + vol.Required('name'): cv.string, + }) + + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {}) + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {'name': 1}) + self.assertRaises(vol.error.MultipleInvalid, + handler1.async_validate_slots, {'name': 'kitchen'}) + handler1.async_validate_slots({'name': {'value': 'kitchen'}}) + handler1.async_validate_slots({ + 'name': {'value': 'kitchen'}, + 'probability': {'value': '0.5'} + }) From 9db8759317c846998ecc820393b82c350791d683 Mon Sep 17 00:00:00 2001 From: Andy Castille Date: Sun, 1 Jul 2018 10:54:51 -0500 Subject: [PATCH 130/169] Rachio webhooks (#15111) * Make fewer requests to the Rachio API * BREAKING: Rewrite Rachio component --- .../components/binary_sensor/rachio.py | 127 +++++++ homeassistant/components/rachio.py | 289 ++++++++++++++++ homeassistant/components/switch/rachio.py | 312 +++++++++--------- requirements_all.txt | 4 +- 4 files changed, 580 insertions(+), 152 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rachio.py create mode 100644 homeassistant/components/rachio.py diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py new file mode 100644 index 00000000000000..cc3079c6e53025 --- /dev/null +++ b/homeassistant/components/binary_sensor/rachio.py @@ -0,0 +1,127 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rachio/ +""" +from abc import abstractmethod +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_STATUS, + KEY_SUBTYPE, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + STATUS_OFFLINE, + STATUS_ONLINE, + SUBTYPE_OFFLINE, + SUBTYPE_ONLINE,) +from homeassistant.helpers.dispatcher import dispatcher_connect + +DEPENDENCIES = ['rachio'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Rachio binary sensors.""" + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioControllerOnlineBinarySensor(hass, controller)) + + add_devices(devices) + _LOGGER.info("%d Rachio binary sensor(s) added", len(devices)) + + +class RachioControllerBinarySensor(BinarySensorDevice): + """Represent a binary sensor that reflects a Rachio state.""" + + def __init__(self, hass, controller, poll=True): + """Set up a new Rachio controller binary sensor.""" + self._controller = controller + + if poll: + self._state = self._poll_update() + else: + self._state = None + + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + + @property + def is_on(self) -> bool: + """Return whether the sensor has a 'true' value.""" + return self._state + + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + + # For this device + self._handle_update() + + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + pass + + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + pass + + +class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): + """Represent a binary sensor that reflects if the controller is online.""" + + def __init__(self, hass, controller): + """Set up a new Rachio controller online binary sensor.""" + super().__init__(hass, controller, poll=False) + self._state = self._poll_update(controller.init_data) + + @property + def name(self) -> str: + """Return the name of this sensor including the controller name.""" + return "{} online".format(self._controller.name) + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + @property + def icon(self) -> str: + """Return the name of an icon for this sensor.""" + return 'mdi:wifi-strength-4' if self.is_on\ + else 'mdi:wifi-strength-off-outline' + + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] + + if data[KEY_STATUS] == STATUS_ONLINE: + return True + elif data[KEY_STATUS] == STATUS_OFFLINE: + return False + else: + _LOGGER.warning('"%s" reported in unknown state "%s"', self.name, + data[KEY_STATUS]) + + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + self._state = False + + self.schedule_update_ha_state() diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py new file mode 100644 index 00000000000000..b3b2d05e933485 --- /dev/null +++ b/homeassistant/components/rachio.py @@ -0,0 +1,289 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rachio/ +""" +import asyncio +import logging + +from aiohttp import web +import voluptuous as vol + +from homeassistant.auth import generate_secret +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['rachiopy==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rachio' + +CONF_CUSTOM_URL = 'hass_url_override' +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_CUSTOM_URL): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +# Keys used in the API JSON +KEY_DEVICE_ID = 'deviceId' +KEY_DEVICES = 'devices' +KEY_ENABLED = 'enabled' +KEY_EXTERNAL_ID = 'externalId' +KEY_ID = 'id' +KEY_NAME = 'name' +KEY_ON = 'on' +KEY_STATUS = 'status' +KEY_SUBTYPE = 'subType' +KEY_SUMMARY = 'summary' +KEY_TYPE = 'type' +KEY_URL = 'url' +KEY_USERNAME = 'username' +KEY_ZONE_ID = 'zoneId' +KEY_ZONE_NUMBER = 'zoneNumber' +KEY_ZONES = 'zones' + +STATUS_ONLINE = 'ONLINE' +STATUS_OFFLINE = 'OFFLINE' + +# Device webhook values +TYPE_CONTROLLER_STATUS = 'DEVICE_STATUS' +SUBTYPE_OFFLINE = 'OFFLINE' +SUBTYPE_ONLINE = 'ONLINE' +SUBTYPE_OFFLINE_NOTIFICATION = 'OFFLINE_NOTIFICATION' +SUBTYPE_COLD_REBOOT = 'COLD_REBOOT' +SUBTYPE_SLEEP_MODE_ON = 'SLEEP_MODE_ON' +SUBTYPE_SLEEP_MODE_OFF = 'SLEEP_MODE_OFF' +SUBTYPE_BROWNOUT_VALVE = 'BROWNOUT_VALVE' +SUBTYPE_RAIN_SENSOR_DETECTION_ON = 'RAIN_SENSOR_DETECTION_ON' +SUBTYPE_RAIN_SENSOR_DETECTION_OFF = 'RAIN_SENSOR_DETECTION_OFF' +SUBTYPE_RAIN_DELAY_ON = 'RAIN_DELAY_ON' +SUBTYPE_RAIN_DELAY_OFF = 'RAIN_DELAY_OFF' + +# Schedule webhook values +TYPE_SCHEDULE_STATUS = 'SCHEDULE_STATUS' +SUBTYPE_SCHEDULE_STARTED = 'SCHEDULE_STARTED' +SUBTYPE_SCHEDULE_STOPPED = 'SCHEDULE_STOPPED' +SUBTYPE_SCHEDULE_COMPLETED = 'SCHEDULE_COMPLETED' +SUBTYPE_WEATHER_NO_SKIP = 'WEATHER_INTELLIGENCE_NO_SKIP' +SUBTYPE_WEATHER_SKIP = 'WEATHER_INTELLIGENCE_SKIP' +SUBTYPE_WEATHER_CLIMATE_SKIP = 'WEATHER_INTELLIGENCE_CLIMATE_SKIP' +SUBTYPE_WEATHER_FREEZE = 'WEATHER_INTELLIGENCE_FREEZE' + +# Zone webhook values +TYPE_ZONE_STATUS = 'ZONE_STATUS' +SUBTYPE_ZONE_STARTED = 'ZONE_STARTED' +SUBTYPE_ZONE_STOPPED = 'ZONE_STOPPED' +SUBTYPE_ZONE_COMPLETED = 'ZONE_COMPLETED' +SUBTYPE_ZONE_CYCLING = 'ZONE_CYCLING' +SUBTYPE_ZONE_CYCLING_COMPLETED = 'ZONE_CYCLING_COMPLETED' + +# Webhook callbacks +LISTEN_EVENT_TYPES = ['DEVICE_STATUS_EVENT', 'ZONE_STATUS_EVENT'] +WEBHOOK_CONST_ID = 'homeassistant.rachio:' +WEBHOOK_PATH = URL_API + DOMAIN +SIGNAL_RACHIO_UPDATE = DOMAIN + '_update' +SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + '_controller' +SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + '_zone' +SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + '_schedule' + + +def setup(hass, config) -> bool: + """Set up the Rachio component.""" + from rachiopy import Rachio + + # Listen for incoming webhook connections + hass.http.register_view(RachioWebhookView()) + + # Configure API + api_key = config[DOMAIN].get(CONF_API_KEY) + rachio = Rachio(api_key) + + # Get the URL of this server + custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) + hass_url = hass.config.api.base_url if custom_url is None else custom_url + rachio.webhook_auth = generate_secret() + rachio.webhook_url = hass_url + WEBHOOK_PATH + + # Get the API user + try: + person = RachioPerson(hass, rachio) + except AssertionError as error: + _LOGGER.error("Could not reach the Rachio API: %s", error) + return False + + # Check for Rachio controller devices + if not person.controllers: + _LOGGER.error("No Rachio devices found in account %s", + person.username) + return False + else: + _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) + + # Enable component + hass.data[DOMAIN] = person + return True + + +class RachioPerson(object): + """Represent a Rachio user.""" + + def __init__(self, hass, rachio): + """Create an object from the provided API instance.""" + # Use API token to get user ID + self._hass = hass + self.rachio = rachio + + response = rachio.person.getInfo() + assert int(response[0][KEY_STATUS]) == 200, "API key error" + self._id = response[1][KEY_ID] + + # Use user ID to get user data + data = rachio.person.get(self._id) + assert int(data[0][KEY_STATUS]) == 200, "User ID error" + self.username = data[1][KEY_USERNAME] + self._controllers = [RachioIro(self._hass, self.rachio, controller) + for controller in data[1][KEY_DEVICES]] + _LOGGER.info('Using Rachio API as user "%s"', self.username) + + @property + def user_id(self) -> str: + """Get the user ID as defined by the Rachio API.""" + return self._id + + @property + def controllers(self) -> list: + """Get a list of controllers managed by this account.""" + return self._controllers + + +class RachioIro(object): + """Represent a Rachio Iro.""" + + def __init__(self, hass, rachio, data): + """Initialize a Rachio device.""" + self.hass = hass + self.rachio = rachio + self._id = data[KEY_ID] + self._name = data[KEY_NAME] + self._zones = data[KEY_ZONES] + self._init_data = data + _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + + # Listen for all updates + self._init_webhooks() + + def _init_webhooks(self) -> None: + """Start getting updates from the Rachio API.""" + current_webhook_id = None + + # First delete any old webhooks that may have stuck around + def _deinit_webhooks(event) -> None: + """Stop getting updates from the Rachio API.""" + webhooks = self.rachio.notification.getDeviceWebhook( + self.controller_id)[1] + for webhook in webhooks: + if webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) or\ + webhook[KEY_ID] == current_webhook_id: + self.rachio.notification.deleteWebhook(webhook[KEY_ID]) + _deinit_webhooks(None) + + # Choose which events to listen for and get their IDs + event_types = [] + for event_type in self.rachio.notification.getWebhookEventType()[1]: + if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: + event_types.append({"id": event_type[KEY_ID]}) + + # Register to listen to these events from the device + url = self.rachio.webhook_url + auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth + new_webhook = self.rachio.notification.postWebhook(self.controller_id, + auth, url, + event_types) + # Save ID for deletion at shutdown + current_webhook_id = new_webhook[1][KEY_ID] + self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) + + def __str__(self) -> str: + """Display the controller as a string.""" + return 'Rachio controller "{}"'.format(self.name) + + @property + def controller_id(self) -> str: + """Return the Rachio API controller ID.""" + return self._id + + @property + def name(self) -> str: + """Return the user-defined name of the controller.""" + return self._name + + @property + def current_schedule(self) -> str: + """Return the schedule that the device is running right now.""" + return self.rachio.device.getCurrentSchedule(self.controller_id)[1] + + @property + def init_data(self) -> dict: + """Return the information used to set up the controller.""" + return self._init_data + + def list_zones(self, include_disabled=False) -> list: + """Return a list of the zone dicts connected to the device.""" + # All zones + if include_disabled: + return self._zones + + # Only enabled zones + return [z for z in self._zones if z[KEY_ENABLED]] + + def get_zone(self, zone_id) -> dict or None: + """Return the zone with the given ID.""" + for zone in self.list_zones(include_disabled=True): + if zone[KEY_ID] == zone_id: + return zone + + return None + + def stop_watering(self) -> None: + """Stop watering all zones connected to this controller.""" + self.rachio.device.stopWater(self.controller_id) + _LOGGER.info("Stopped watering of all zones on %s", str(self)) + + +class RachioWebhookView(HomeAssistantView): + """Provide a page for the server to call.""" + + SIGNALS = { + TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, + TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, + TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, + } + + requires_auth = False # Handled separately + url = WEBHOOK_PATH + name = url[1:].replace('/', ':') + + # pylint: disable=no-self-use + @asyncio.coroutine + async def post(self, request) -> web.Response: + """Handle webhook calls from the server.""" + hass = request.app['hass'] + data = await request.json() + + try: + auth = data.get(KEY_EXTERNAL_ID, str()).split(':')[1] + assert auth == hass.data[DOMAIN].rachio.webhook_auth + except (AssertionError, IndexError): + return web.Response(status=web.HTTPForbidden.status_code) + + update_type = data[KEY_TYPE] + if update_type in self.SIGNALS: + async_dispatcher_send(hass, self.SIGNALS[update_type], data) + + return web.Response(status=web.HTTPNoContent.status_code) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index dc661c3e5bfb10..5f0ca995c90f20 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -4,227 +4,239 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rachio/ """ +from abc import abstractmethod from datetime import timedelta import logging - import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_ENABLED, + KEY_ID, + KEY_NAME, + KEY_ON, + KEY_SUBTYPE, + KEY_SUMMARY, + KEY_ZONE_ID, + KEY_ZONE_NUMBER, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_ZONE_UPDATE, + SUBTYPE_ZONE_STARTED, + SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED, + SUBTYPE_SLEEP_MODE_ON, + SUBTYPE_SLEEP_MODE_OFF) import homeassistant.helpers.config_validation as cv -import homeassistant.util as util +from homeassistant.helpers.dispatcher import dispatcher_connect -REQUIREMENTS = ['rachiopy==0.1.2'] +DEPENDENCIES = ['rachio'] _LOGGER = logging.getLogger(__name__) +# Manual run length CONF_MANUAL_RUN_MINS = 'manual_run_mins' - -DATA_RACHIO = 'rachio' - DEFAULT_MANUAL_RUN_MINS = 10 -MIN_UPDATE_INTERVAL = timedelta(seconds=30) -MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): cv.positive_int, }) +ATTR_ZONE_SUMMARY = 'Summary' +ATTR_ZONE_NUMBER = 'Zone number' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Rachio switches.""" - from rachiopy import Rachio + manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS)) + _LOGGER.info("Rachio run time is %s", str(manual_run_time)) - # Get options - manual_run_mins = config.get(CONF_MANUAL_RUN_MINS) - _LOGGER.debug("Rachio run time is %d min", manual_run_mins) + # Add all zones from all controllers as switches + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioStandbySwitch(hass, controller)) - access_token = config.get(CONF_ACCESS_TOKEN) + for zone in controller.list_zones(): + devices.append(RachioZone(hass, controller, zone, manual_run_time)) - # Configure API - _LOGGER.debug("Configuring Rachio API") - rachio = Rachio(access_token) + add_devices(devices) + _LOGGER.info("%d Rachio switch(es) added", len(devices)) - person = None - try: - person = _get_person(rachio) - except KeyError: - _LOGGER.error( - "Could not reach the Rachio API. Is your access token valid?") - return - # Get and persist devices - devices = _list_devices(rachio, manual_run_mins) - if not devices: - _LOGGER.error( - "No Rachio devices found in account %s", person['username']) - return +class RachioSwitch(SwitchDevice): + """Represent a Rachio state that can be toggled.""" - hass.data[DATA_RACHIO] = devices[0] + def __init__(self, controller, poll=True): + """Initialize a new Rachio switch.""" + self._controller = controller - if len(devices) > 1: - _LOGGER.warning("Multiple Rachio devices found in account, " - "using %s", hass.data[DATA_RACHIO].device_id) - else: - _LOGGER.debug("Found Rachio device") + if poll: + self._state = self._poll_update() + else: + self._state = None - hass.data[DATA_RACHIO].update() - add_devices(hass.data[DATA_RACHIO].list_zones()) + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + @property + def name(self) -> str: + """Get a name for this switch.""" + return "Switch on {}".format(self._controller.name) -def _get_person(rachio): - """Pull the account info of the person whose access token was provided.""" - person_id = rachio.person.getInfo()[1]['id'] - return rachio.person.get(person_id)[1] + @property + def is_on(self) -> bool: + """Return whether the switch is currently on.""" + return self._state + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Poll the API.""" + pass -def _list_devices(rachio, manual_run_mins): - """Pull a list of devices on the account.""" - return [RachioIro(rachio, d['id'], manual_run_mins) - for d in _get_person(rachio)['devices']] + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + # For this device + self._handle_update(args, kwargs) -class RachioIro(object): - """Representation of a Rachio Iro.""" + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook data.""" + pass - def __init__(self, rachio, device_id, manual_run_mins): - """Initialize a Rachio device.""" - self.rachio = rachio - self._device_id = device_id - self.manual_run_mins = manual_run_mins - self._device = None - self._running = None - self._zones = None - def __str__(self): - """Display the device as a string.""" - return "Rachio Iro {}".format(self.serial_number) +class RachioStandbySwitch(RachioSwitch): + """Representation of a standby status/button.""" - @property - def device_id(self): - """Return the Rachio API device ID.""" - return self._device['id'] + def __init__(self, hass, controller): + """Instantiate a new Rachio standby mode switch.""" + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + super().__init__(controller, poll=False) + self._poll_update(controller.init_data) @property - def status(self): - """Return the current status of the device.""" - return self._device['status'] + def name(self) -> str: + """Return the name of the standby switch.""" + return "{} in standby mode".format(self._controller.name) @property - def serial_number(self): - """Return the serial number of the device.""" - return self._device['serialNumber'] + def icon(self) -> str: + """Return an icon for the standby switch.""" + return "mdi:power" - @property - def is_paused(self): - """Return whether the device is temporarily disabled.""" - return self._device['paused'] + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] - @property - def is_on(self): - """Return whether the device is powered on and connected.""" - return self._device['on'] + return not data[KEY_ON] - @property - def current_schedule(self): - """Return the schedule that the device is running right now.""" - return self._running + def _handle_update(self, *args, **kwargs) -> None: + """Update the state using webhook data.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + self._state = False - def list_zones(self, include_disabled=False): - """Return a list of the zones connected to the device, incl. data.""" - if not self._zones: - self._zones = [RachioZone(self.rachio, self, zone['id'], - self.manual_run_mins) - for zone in self._device['zones']] + self.schedule_update_ha_state() - if include_disabled: - return self._zones + def turn_on(self, **kwargs) -> None: + """Put the controller in standby mode.""" + self._controller.rachio.device.off(self._controller.controller_id) - self.update(no_throttle=True) - return [z for z in self._zones if z.is_enabled] + def turn_off(self, **kwargs) -> None: + """Resume controller functionality.""" + self._controller.rachio.device.on(self._controller.controller_id) - @util.Throttle(MIN_UPDATE_INTERVAL, MIN_FORCED_UPDATE_INTERVAL) - def update(self, **kwargs): - """Pull updated device info from the Rachio API.""" - self._device = self.rachio.device.get(self._device_id)[1] - self._running = self.rachio.device\ - .getCurrentSchedule(self._device_id)[1] - # Possibly update all zones - for zone in self.list_zones(include_disabled=True): - zone.update() - - _LOGGER.debug("Updated %s", str(self)) - - -class RachioZone(SwitchDevice): +class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" - def __init__(self, rachio, device, zone_id, manual_run_mins): + def __init__(self, hass, controller, data, manual_run_time): """Initialize a new Rachio Zone.""" - self.rachio = rachio - self._device = device - self._zone_id = zone_id - self._zone = None - self._manual_run_secs = manual_run_mins * 60 + self._id = data[KEY_ID] + self._zone_name = data[KEY_NAME] + self._zone_number = data[KEY_ZONE_NUMBER] + self._zone_enabled = data[KEY_ENABLED] + self._manual_run_time = manual_run_time + self._summary = str() + super().__init__(controller) + + # Listen for all zone updates + dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, + self._handle_update) def __str__(self): """Display the zone as a string.""" - return "Rachio Zone {}".format(self.name) + return 'Rachio Zone "{}" on {}'.format(self.name, + str(self._controller)) @property - def zone_id(self): + def zone_id(self) -> str: """How the Rachio API refers to the zone.""" - return self._zone['id'] + return self._id @property - def unique_id(self): - """Return the unique string ID for the zone.""" - return '{iro}-{zone}'.format( - iro=self._device.device_id, zone=self.zone_id) - - @property - def number(self): - """Return the physical connection of the zone pump.""" - return self._zone['zoneNumber'] + def name(self) -> str: + """Return the friendly name of the zone.""" + return self._zone_name @property - def name(self): - """Return the friendly name of the zone.""" - return self._zone['name'] + def icon(self) -> str: + """Return the icon to display.""" + return "mdi:water" @property - def is_enabled(self): + def zone_is_enabled(self) -> bool: """Return whether the zone is allowed to run.""" - return self._zone['enabled'] + return self._zone_enabled @property - def is_on(self): - """Return whether the zone is currently running.""" - schedule = self._device.current_schedule - return self.zone_id == schedule.get('zoneId') + def state_attributes(self) -> dict: + """Return the optional state attributes.""" + return { + ATTR_ZONE_NUMBER: self._zone_number, + ATTR_ZONE_SUMMARY: self._summary, + } + + def turn_on(self, **kwargs) -> None: + """Start watering this zone.""" + # Stop other zones first + self.turn_off() - def update(self): - """Pull updated zone info from the Rachio API.""" - self._zone = self.rachio.zone.get(self._zone_id)[1] + # Start this zone + self._controller.rachio.zone.start(self.zone_id, + self._manual_run_time.seconds) + _LOGGER.debug("Watering %s on %s", self.name, self._controller.name) - # Possibly update device - self._device.update() + def turn_off(self, **kwargs) -> None: + """Stop watering all zones.""" + self._controller.stop_watering() - _LOGGER.debug("Updated %s", str(self)) + def _poll_update(self, data=None) -> bool: + """Poll the API to check whether the zone is running.""" + schedule = self._controller.current_schedule + return self.zone_id == schedule.get(KEY_ZONE_ID) - def turn_on(self, **kwargs): - """Start the zone.""" - # Stop other zones first - self.turn_off() + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook zone data.""" + if args[0][KEY_ZONE_ID] != self.zone_id: + return + + self._summary = kwargs.get(KEY_SUMMARY, str()) - _LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs) - self.rachio.zone.start(self.zone_id, self._manual_run_secs) + if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED: + self._state = True + elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED]: + self._state = False - def turn_off(self, **kwargs): - """Stop all zones.""" - _LOGGER.info("Stopping watering of all zones") - self.rachio.device.stopWater(self._device.device_id) + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 1267a7028111b7..b173dc1e56b8cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1163,8 +1163,8 @@ pyzabbix==0.7.4 # homeassistant.components.sensor.qnap qnapstats==0.2.6 -# homeassistant.components.switch.rachio -rachiopy==0.1.2 +# homeassistant.components.rachio +rachiopy==0.1.3 # homeassistant.components.climate.radiotherm radiotherm==1.3 From 6f582dcf24978a5288bbf583450070bb63b7c234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 1 Jul 2018 18:57:01 +0300 Subject: [PATCH 131/169] Lint cleanups (#15243) * Remove some unused imports * Fix a flake8 E271 --- homeassistant/components/google_assistant/http.py | 4 +--- homeassistant/components/google_assistant/smart_home.py | 8 -------- homeassistant/components/sensor/tibber.py | 2 +- homeassistant/config.py | 2 +- homeassistant/helpers/service.py | 2 -- homeassistant/helpers/translation.py | 2 -- homeassistant/loader.py | 8 +------- 7 files changed, 4 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 65079a1a26e754..05bc3cbd01c0a4 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,10 +10,8 @@ from aiohttp.web import Request, Response # Typing imports -# pylint: disable=unused-import from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant, callback # NOQA -from homeassistant.helpers.entity import Entity # NOQA +from homeassistant.core import callback from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f20d4f747cceb8..927139a483e0a8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -3,14 +3,6 @@ from itertools import product import logging -# Typing imports -# pylint: disable=unused-import -# if False: -from aiohttp.web import Request, Response # NOQA -from typing import Dict, Tuple, Any, Optional # NOQA -from homeassistant.helpers.entity import Entity # NOQA -from homeassistant.core import HomeAssistant # NOQA -from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.util.decorator import Registry from homeassistant.core import callback diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 42568a6b9ada4b..c75c40dd929ca3 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -123,7 +123,7 @@ def unique_id(self): async def _fetch_data(self): try: await self._tibber_home.update_info() - await self._tibber_home.update_price_info() + await self._tibber_home.update_price_info() except (asyncio.TimeoutError, aiohttp.ClientError): return data = self._tibber_home.info['viewer']['home'] diff --git a/homeassistant/config.py b/homeassistant/config.py index 2906f07a307c0f..52ff0e19c598b2 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ import re import shutil # pylint: disable=unused-import -from typing import Any, List, Tuple, Optional # NOQA +from typing import Any, Tuple, Optional # noqa: F401 import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9114a4db941cfc..7ab90b7a048910 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,7 +1,5 @@ """Service calling related helpers.""" import logging -# pylint: disable=unused-import -from typing import Optional # NOQA from os import path import voluptuous as vol diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index f1335f733466e5..81ec046f2e9966 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -1,7 +1,5 @@ """Translation string lookup helpers.""" import logging -# pylint: disable=unused-import -from typing import Optional # NOQA from os import path from homeassistant import config_entries diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e3e41e09db23d3..9e5efffdccbab1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,17 +16,11 @@ import sys from types import ModuleType -# pylint: disable=unused-import -from typing import Dict, List, Optional, Sequence, Set # NOQA +from typing import Optional, Set from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet -# Typing imports -# pylint: disable=using-constant-test,unused-import -if False: - from homeassistant.core import HomeAssistant # NOQA - PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) From 235282e335bc039c83e9cc848a74f22d697243b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Jul 2018 13:00:34 -0400 Subject: [PATCH 132/169] Bump frontend to 20180701.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 84118e57c8f9be..7bad8ff727d937 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180629.1'] +REQUIREMENTS = ['home-assistant-frontend==20180701.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index b173dc1e56b8cb..b011bd6747ea19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180629.1 +home-assistant-frontend==20180701.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdd96562206dc1..a0a51fdc20321e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180629.1 +home-assistant-frontend==20180701.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 4a4b9180d81853af6fc8ef578199389e6bea6770 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 1 Jul 2018 19:01:48 +0200 Subject: [PATCH 133/169] Upgrade sqlalchemy to 1.2.9 (#15250) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 38ba593261f3dc..43c2aa5c7b11bb 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.8'] +REQUIREMENTS = ['sqlalchemy==1.2.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 7fefb0f450b54f..8574a7231da5b3 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.8'] +REQUIREMENTS = ['sqlalchemy==1.2.9'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/requirements_all.txt b/requirements_all.txt index b011bd6747ea19..6184bed0224c74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.8 +sqlalchemy==1.2.9 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0a51fdc20321e..6bfe170c67c0cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -194,7 +194,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.8 +sqlalchemy==1.2.9 # homeassistant.components.statsd statsd==3.2.1 From 6c77c9d372e2b33d7474ac6caa234ea4f3c44641 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 1 Jul 2018 19:02:02 +0200 Subject: [PATCH 134/169] Upgrade WazeRouteCalculator to 0.6 (#15251) --- .../components/sensor/waze_travel_time.py | 20 ++++++++----------- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index fc40d17d0afd29..0b059379c11805 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['WazeRouteCalculator==0.5'] +REQUIREMENTS = ['WazeRouteCalculator==0.6'] _LOGGER = logging.getLogger(__name__) @@ -40,6 +40,8 @@ SCAN_INTERVAL = timedelta(minutes=5) +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ORIGIN): cv.string, vol.Required(CONF_DESTINATION): cv.string, @@ -49,8 +51,6 @@ vol.Optional(CONF_EXCL_FILTER): cv.string, }) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Waze travel time sensor platform.""" @@ -72,10 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def _get_location_from_attributes(state): """Get the lat/long string from an states attributes.""" attr = state.attributes - return '{},{}'.format( - attr.get(ATTR_LATITUDE), - attr.get(ATTR_LONGITUDE) - ) + return '{},{}'.format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) class WazeTravelTime(Entity): @@ -186,13 +183,11 @@ def update(self): if self._origin_entity_id is not None: self._origin = self._get_location_from_entity( - self._origin_entity_id - ) + self._origin_entity_id) if self._destination_entity_id is not None: self._destination = self._get_location_from_entity( - self._destination_entity_id - ) + self._destination_entity_id) self._destination = self._resolve_zone(self._destination) self._origin = self._resolve_zone(self._origin) @@ -217,7 +212,8 @@ def update(self): self._state = { 'duration': duration, 'distance': distance, - 'route': route} + 'route': route, + } except WazeRouteCalculator.WRCError as exp: _LOGGER.error("Error on retrieving data: %s", exp) return diff --git a/requirements_all.txt b/requirements_all.txt index 6184bed0224c74..af71ea2ee54804 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,7 +66,7 @@ TravisPy==0.3.5 TwitterAPI==2.5.4 # homeassistant.components.sensor.waze_travel_time -WazeRouteCalculator==0.5 +WazeRouteCalculator==0.6 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 0a186650bf2ac83f2ebeb9e7c44bfa718bf1303d Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 1 Jul 2018 10:04:12 -0700 Subject: [PATCH 135/169] Fix an issue when user's nest developer account don't have permission (#15237) --- homeassistant/components/binary_sensor/nest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 9da352e1268bff..31460c1eedca0d 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -31,12 +31,10 @@ STRUCTURE_BINARY_TYPES = { 'away': None, - # 'security_state', # pending python-nest update } STRUCTURE_BINARY_STATE_MAP = { 'away': {'away': True, 'home': False}, - 'security_state': {'deter': True, 'ok': False}, } _BINARY_TYPES_DEPRECATED = [ @@ -135,7 +133,7 @@ def update(self): value = getattr(self.device, self.variable) if self.variable in STRUCTURE_BINARY_TYPES: self._state = bool(STRUCTURE_BINARY_STATE_MAP - [self.variable][value]) + [self.variable].get(value)) else: self._state = bool(value) From dffe36761db78510e3c7ed43b00e991b5174fdca Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 1 Jul 2018 19:06:30 +0200 Subject: [PATCH 136/169] Make LIFX color/temperature attributes mutually exclusive (#15234) --- homeassistant/components/light/lifx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 421356f07bc632..9b2c183c1d1a87 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -446,7 +446,9 @@ def brightness(self): @property def color_temp(self): """Return the color temperature.""" - kelvin = self.device.color[3] + _, sat, _, kelvin = self.device.color + if sat: + return None return color_util.color_temperature_kelvin_to_mired(kelvin) @property @@ -601,7 +603,7 @@ def hs_color(self): hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - return (hue, sat) + return (hue, sat) if sat else None class LIFXStrip(LIFXColor): From a64a66dd6216fa38360dcd2e0f590a7ded9a8514 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 1 Jul 2018 10:36:50 -0700 Subject: [PATCH 137/169] Only create front-end client_id once (#15214) * Only create frontend client_id once * Check user and client_id before create refresh token * Lint * Follow code review comment * Minor clenaup * Update doc string --- homeassistant/auth.py | 97 ++++++++++++------- homeassistant/components/frontend/__init__.py | 2 +- tests/common.py | 10 +- tests/components/auth/__init__.py | 2 +- tests/test_auth.py | 53 ++++++++-- 5 files changed, 117 insertions(+), 47 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index f56e00bf31e553..a4e8ee05943b4c 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -1,23 +1,22 @@ """Provide an authentication layer for Home Assistant.""" import asyncio import binascii -from collections import OrderedDict -from datetime import datetime, timedelta -import os import importlib import logging +import os import uuid +from collections import OrderedDict +from datetime import datetime, timedelta import attr import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements -from homeassistant.core import callback from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID -from homeassistant.util.decorator import Registry +from homeassistant.core import callback from homeassistant.util import dt as dt_util - +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) @@ -349,6 +348,16 @@ async def async_create_client(self, name, *, redirect_uris=None, return await self._store.async_create_client( name, redirect_uris, no_secret) + async def async_get_or_create_client(self, name, *, redirect_uris=None, + no_secret=False): + """Find a client, if not exists, create a new one.""" + for client in await self._store.async_get_clients(): + if client.name == name: + return client + + return await self._store.async_create_client( + name, redirect_uris, no_secret) + async def async_get_client(self, client_id): """Get a client.""" return await self._store.async_get_client(client_id) @@ -392,29 +401,36 @@ class AuthStore: def __init__(self, hass): """Initialize the auth store.""" self.hass = hass - self.users = None - self.clients = None + self._users = None + self._clients = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) async def credentials_for_provider(self, provider_type, provider_id): """Return credentials for specific auth provider type and id.""" - if self.users is None: + if self._users is None: await self.async_load() return [ credentials - for user in self.users.values() + for user in self._users.values() for credentials in user.credentials if (credentials.auth_provider_type == provider_type and credentials.auth_provider_id == provider_id) ] + async def async_get_users(self): + """Retrieve all users.""" + if self._users is None: + await self.async_load() + + return list(self._users.values()) + async def async_get_user(self, user_id): """Retrieve a user.""" - if self.users is None: + if self._users is None: await self.async_load() - return self.users.get(user_id) + return self._users.get(user_id) async def async_get_or_create_user(self, credentials, auth_provider): """Get or create a new user for given credentials. @@ -422,7 +438,7 @@ async def async_get_or_create_user(self, credentials, auth_provider): If link_user is passed in, the credentials will be linked to the passed in user if the credentials are new. """ - if self.users is None: + if self._users is None: await self.async_load() # New credentials, store in user @@ -430,7 +446,7 @@ async def async_get_or_create_user(self, credentials, auth_provider): info = await auth_provider.async_user_meta_for_credentials( credentials) # Make owner and activate user if it's the first user. - if self.users: + if self._users: is_owner = False is_active = False else: @@ -442,11 +458,11 @@ async def async_get_or_create_user(self, credentials, auth_provider): is_active=is_active, name=info.get('name'), ) - self.users[new_user.id] = new_user + self._users[new_user.id] = new_user await self.async_link_user(new_user, credentials) return new_user - for user in self.users.values(): + for user in self._users.values(): for creds in user.credentials: if (creds.auth_provider_type == credentials.auth_provider_type and creds.auth_provider_id == @@ -463,11 +479,19 @@ async def async_link_user(self, user, credentials): async def async_remove_user(self, user): """Remove a user.""" - self.users.pop(user.id) + self._users.pop(user.id) await self.async_save() async def async_create_refresh_token(self, user, client_id): """Create a new token for a user.""" + local_user = await self.async_get_user(user.id) + if local_user is None: + raise ValueError('Invalid user') + + local_client = await self.async_get_client(client_id) + if local_client is None: + raise ValueError('Invalid client_id') + refresh_token = RefreshToken(user, client_id) user.refresh_tokens[refresh_token.token] = refresh_token await self.async_save() @@ -475,10 +499,10 @@ async def async_create_refresh_token(self, user, client_id): async def async_get_refresh_token(self, token): """Get refresh token by token.""" - if self.users is None: + if self._users is None: await self.async_load() - for user in self.users.values(): + for user in self._users.values(): refresh_token = user.refresh_tokens.get(token) if refresh_token is not None: return refresh_token @@ -487,7 +511,7 @@ async def async_get_refresh_token(self, token): async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" - if self.clients is None: + if self._clients is None: await self.async_load() kwargs = { @@ -499,16 +523,23 @@ async def async_create_client(self, name, redirect_uris, no_secret): kwargs['secret'] = None client = Client(**kwargs) - self.clients[client.id] = client + self._clients[client.id] = client await self.async_save() return client + async def async_get_clients(self): + """Return all clients.""" + if self._clients is None: + await self.async_load() + + return list(self._clients.values()) + async def async_get_client(self, client_id): """Get a client.""" - if self.clients is None: + if self._clients is None: await self.async_load() - return self.clients.get(client_id) + return self._clients.get(client_id) async def async_load(self): """Load the users.""" @@ -516,12 +547,12 @@ async def async_load(self): # Make sure that we're not overriding data if 2 loads happened at the # same time - if self.users is not None: + if self._users is not None: return if data is None: - self.users = {} - self.clients = {} + self._users = {} + self._clients = {} return users = { @@ -565,8 +596,8 @@ async def async_load(self): cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients'] } - self.users = users - self.clients = clients + self._users = users + self._clients = clients async def async_save(self): """Save users.""" @@ -577,7 +608,7 @@ async def async_save(self): 'is_active': user.is_active, 'name': user.name, } - for user in self.users.values() + for user in self._users.values() ] credentials = [ @@ -588,7 +619,7 @@ async def async_save(self): 'auth_provider_id': credential.auth_provider_id, 'data': credential.data, } - for user in self.users.values() + for user in self._users.values() for credential in user.credentials ] @@ -602,7 +633,7 @@ async def async_save(self): refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, } - for user in self.users.values() + for user in self._users.values() for refresh_token in user.refresh_tokens.values() ] @@ -613,7 +644,7 @@ async def async_save(self): 'created_at': access_token.created_at.isoformat(), 'token': access_token.token, } - for user in self.users.values() + for user in self._users.values() for refresh_token in user.refresh_tokens.values() for access_token in refresh_token.access_tokens ] @@ -625,7 +656,7 @@ async def async_save(self): 'secret': client.secret, 'redirect_uris': client.redirect_uris, } - for client in self.clients.values() + for client in self._clients.values() ] data = { diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7bad8ff727d937..9a32626c66a0fe 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -201,7 +201,7 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" if hass.auth.active: - client = await hass.auth.async_create_client( + client = await hass.auth.async_get_or_create_client( 'Home Assistant Frontend', redirect_uris=['/'], no_secret=True, diff --git a/tests/common.py b/tests/common.py index 1b8eabaa0db4bc..3a51cd3e059847 100644 --- a/tests/common.py +++ b/tests/common.py @@ -321,7 +321,7 @@ def add_to_hass(self, hass): def add_to_auth_manager(self, auth_mgr): """Test helper to add entry to hass.""" ensure_auth_manager_loaded(auth_mgr) - auth_mgr._store.users[self.id] = self + auth_mgr._store._users[self.id] = self return self @@ -329,10 +329,10 @@ def add_to_auth_manager(self, auth_mgr): def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store - if store.clients is None: - store.clients = {} - if store.users is None: - store.users = {} + if store._clients is None: + store._clients = {} + if store._users is None: + store._users = {} class MockModule(object): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index f0b205ff5ce490..21719c12569b3b 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -34,7 +34,7 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, }) client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, redirect_uris=[CLIENT_REDIRECT_URI]) - hass.auth._store.clients[client.id] = client + hass.auth._store._clients[client.id] = client if setup_api: await async_setup_component(hass, 'api', {}) return await aiohttp_client(hass.http.app) diff --git a/tests/test_auth.py b/tests/test_auth.py index 4c0db71466e97d..5b545223c15a9c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -191,12 +191,13 @@ async def test_saving_loading(hass, hass_storage): await flush_store(manager._store._store) store2 = auth.AuthStore(hass) - await store2.async_load() - assert len(store2.users) == 1 - assert store2.users[user.id] == user + users = await store2.async_get_users() + assert len(users) == 1 + assert users[0] == user - assert len(store2.clients) == 1 - assert store2.clients[client.id] == client + clients = await store2.async_get_clients() + assert len(clients) == 1 + assert clients[0] == client def test_access_token_expired(): @@ -224,15 +225,18 @@ def test_access_token_expired(): async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) + client = await manager.async_create_client('test') user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(manager) - refresh_token = await manager.async_create_refresh_token(user, 'bla') - access_token = manager.async_create_access_token(refresh_token) + refresh_token = await manager.async_create_refresh_token(user, client.id) + assert refresh_token.user.id is user.id + assert refresh_token.client_id is client.id + access_token = manager.async_create_access_token(refresh_token) assert manager.async_get_access_token(access_token.token) is access_token with patch('homeassistant.auth.dt_util.utcnow', @@ -241,3 +245,38 @@ async def test_cannot_retrieve_expired_access_token(hass): # Even with unpatched time, it should have been removed from manager assert manager.async_get_access_token(access_token.token) is None + + +async def test_get_or_create_client(hass): + """Test that get_or_create_client works.""" + manager = await auth.auth_manager_from_config(hass, []) + + client1 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client1.name is 'Test Client' + + client2 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client2.id is client1.id + + +async def test_cannot_create_refresh_token_with_invalide_client_id(hass): + """Test that we cannot create refresh token with invalid client id.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user, 'bla') + + +async def test_cannot_create_refresh_token_with_invalide_user(hass): + """Test that we cannot create refresh token with invalid client id.""" + manager = await auth.auth_manager_from_config(hass, []) + client = await manager.async_create_client('test') + user = MockUser(id='invalid-user') + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user, client.id) From 86165750ff54e7b5302dfaa1175332f45fc7d6b6 Mon Sep 17 00:00:00 2001 From: Steven Conaway Date: Sun, 1 Jul 2018 22:02:09 -0700 Subject: [PATCH 138/169] Fix typo in Docker files (#15256) --- virtualization/Docker/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 0cb49fde54eb90..15504ea57aff2f 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -46,7 +46,7 @@ apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} # This is a list of scripts that install additional dependencies. If you only # need to install a package from the official debian repository, just add it # to the list above. Only create a script if you need compiling, manually -# downloading or a 3th party repository. +# downloading or a 3rd party repository. if [ "$INSTALL_TELLSTICK" == "yes" ]; then virtualization/Docker/scripts/tellstick fi From 6c77702dcc1f35c8454fb69c0f6bf65db2974d70 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 2 Jul 2018 10:57:26 +0300 Subject: [PATCH 139/169] Switch to own packaged version of pylgnetcast (#15042) ## Description: Switch to own packaged version of pylgnetcast Request to make a pypi package didn't get any response: https://github.com/wokar/pylgnetcast/issues/1 **Related issue (if applicable):** #7069 --- homeassistant/components/media_player/lg_netcast.py | 3 +-- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index 8c98844cf9358a..df1ee662124bd0 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -20,8 +20,7 @@ STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) import homeassistant.util as util -REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/' - 'v0.2.0.zip#pylgnetcast==0.2.0'] +REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index af71ea2ee54804..08985bff945e36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,9 +432,6 @@ httplib2==0.10.3 # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 -# homeassistant.components.media_player.lg_netcast -https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 - # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -881,6 +878,9 @@ pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm pylast==2.3.0 +# homeassistant.components.media_player.lg_netcast +pylgnetcast-homeassistant==0.2.0.dev0 + # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv pylgtv==0.1.7 From d3df96a8de474ff4a327f47be576645542274115 Mon Sep 17 00:00:00 2001 From: Klaudiusz Staniek Date: Mon, 2 Jul 2018 02:44:36 -0700 Subject: [PATCH 140/169] Added setting cover tilt position in scene (#15255) ## Description: This feature adds possibly of setting tilt_position in scene for covers. **Related issue (if applicable):** fixes # **Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io# ## Example entry for `configuration.yaml` (if applicable): ```yaml scene: - name: Close Cover Tilt entities: cover.c_office_north: tilt_position: 0 - name: Open Cover Tilt entities: cover.c_office_north: tilt_position: 100 ``` ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54 --- homeassistant/helpers/state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index f97d70514591c7..72deabaae2844d 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -27,14 +27,15 @@ ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) from homeassistant.components.cover import ( - ATTR_POSITION) + ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, STATE_ALARM_ARMED_AWAY, + SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, @@ -68,7 +69,8 @@ SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], SERVICE_SELECT_OPTION: [ATTR_OPTION], - SERVICE_SET_COVER_POSITION: [ATTR_POSITION] + SERVICE_SET_COVER_POSITION: [ATTR_POSITION], + SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION] } # Update this dict when new services are added to HA. From 4d93a9fd3804a496019cddbfc21b917853cbfbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 2 Jul 2018 12:47:20 +0300 Subject: [PATCH 141/169] Pass tox posargs to pylint (#15226) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8b034346475f54..ca82c83d0fcb8e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint homeassistant + pylint {posargs} homeassistant [testenv:lint] basepython = {env:PYTHON3_PATH:python3} From 36f566a5295b1bbe1dd4f5d13a1565f41853c651 Mon Sep 17 00:00:00 2001 From: David Worsham Date: Mon, 2 Jul 2018 05:12:25 -0700 Subject: [PATCH 142/169] Fix Roomba exception (#15262) * Fix Roomba exception * Switch to single quotes --- homeassistant/components/vacuum/roomba.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index 44d22e03f41624..750c2c0ae0abad 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -284,7 +284,9 @@ def async_update(self): software_version = state.get('softwareVer') # Error message in plain english - error_msg = self.vacuum.error_message + error_msg = 'None' + if hasattr(self.vacuum, 'error_message'): + error_msg = self.vacuum.error_message self._battery_level = state.get('batPct') self._status = self.vacuum.current_state From dd59054003e1202c68b8aa7aa1cf5c2f930dc555 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 08:53:33 -0400 Subject: [PATCH 143/169] Update translations --- .../components/cast/.translations/cs.json | 15 +++++++++ .../components/cast/.translations/de.json | 14 ++++++++ .../components/cast/.translations/hu.json | 14 ++++++++ .../components/cast/.translations/it.json | 15 +++++++++ .../components/cast/.translations/lb.json | 15 +++++++++ .../components/cast/.translations/nl.json | 15 +++++++++ .../components/cast/.translations/sl.json | 15 +++++++++ .../cast/.translations/zh-Hant.json | 15 +++++++++ .../components/deconz/.translations/cs.json | 3 +- .../components/deconz/.translations/de.json | 8 ++++- .../components/deconz/.translations/lb.json | 3 +- .../components/deconz/.translations/nl.json | 7 ++++ .../components/deconz/.translations/sl.json | 3 +- .../deconz/.translations/zh-Hant.json | 3 +- .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/ru.json | 2 +- .../components/nest/.translations/cs.json | 33 +++++++++++++++++++ .../components/nest/.translations/de.json | 21 ++++++++++++ .../components/nest/.translations/hu.json | 20 +++++++++++ .../components/nest/.translations/it.json | 17 ++++++++++ .../components/nest/.translations/lb.json | 33 +++++++++++++++++++ .../components/nest/.translations/nl.json | 33 +++++++++++++++++++ .../components/nest/.translations/sl.json | 33 +++++++++++++++++++ .../nest/.translations/zh-Hant.json | 33 +++++++++++++++++++ .../components/sonos/.translations/cs.json | 15 +++++++++ .../components/sonos/.translations/de.json | 14 ++++++++ .../components/sonos/.translations/hu.json | 14 ++++++++ .../components/sonos/.translations/it.json | 15 +++++++++ .../components/sonos/.translations/lb.json | 15 +++++++++ .../components/sonos/.translations/nl.json | 15 +++++++++ .../components/sonos/.translations/sl.json | 15 +++++++++ .../sonos/.translations/zh-Hant.json | 15 +++++++++ script/translations_download | 2 +- 33 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/cast/.translations/cs.json create mode 100644 homeassistant/components/cast/.translations/de.json create mode 100644 homeassistant/components/cast/.translations/hu.json create mode 100644 homeassistant/components/cast/.translations/it.json create mode 100644 homeassistant/components/cast/.translations/lb.json create mode 100644 homeassistant/components/cast/.translations/nl.json create mode 100644 homeassistant/components/cast/.translations/sl.json create mode 100644 homeassistant/components/cast/.translations/zh-Hant.json create mode 100644 homeassistant/components/nest/.translations/cs.json create mode 100644 homeassistant/components/nest/.translations/de.json create mode 100644 homeassistant/components/nest/.translations/hu.json create mode 100644 homeassistant/components/nest/.translations/it.json create mode 100644 homeassistant/components/nest/.translations/lb.json create mode 100644 homeassistant/components/nest/.translations/nl.json create mode 100644 homeassistant/components/nest/.translations/sl.json create mode 100644 homeassistant/components/nest/.translations/zh-Hant.json create mode 100644 homeassistant/components/sonos/.translations/cs.json create mode 100644 homeassistant/components/sonos/.translations/de.json create mode 100644 homeassistant/components/sonos/.translations/hu.json create mode 100644 homeassistant/components/sonos/.translations/it.json create mode 100644 homeassistant/components/sonos/.translations/lb.json create mode 100644 homeassistant/components/sonos/.translations/nl.json create mode 100644 homeassistant/components/sonos/.translations/sl.json create mode 100644 homeassistant/components/sonos/.translations/zh-Hant.json diff --git a/homeassistant/components/cast/.translations/cs.json b/homeassistant/components/cast/.translations/cs.json new file mode 100644 index 00000000000000..82f063b365f1ec --- /dev/null +++ b/homeassistant/components/cast/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Google Cast.", + "single_instance_allowed": "Pouze jedin\u00e1 konfigurace Google Cast je nezbytn\u00e1." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json new file mode 100644 index 00000000000000..2572c3344ebac2 --- /dev/null +++ b/homeassistant/components/cast/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Google Cast einrichten?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/hu.json b/homeassistant/components/cast/.translations/hu.json new file mode 100644 index 00000000000000..f59a1b43ef1b1f --- /dev/null +++ b/homeassistant/components/cast/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/it.json b/homeassistant/components/cast/.translations/it.json new file mode 100644 index 00000000000000..21c8e60518e2ad --- /dev/null +++ b/homeassistant/components/cast/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Google Cast trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/lb.json b/homeassistant/components/cast/.translations/lb.json new file mode 100644 index 00000000000000..f1daff8306955c --- /dev/null +++ b/homeassistant/components/cast/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Google Cast Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Google Cast ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Google Cast konfigur\u00e9iert ginn?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/nl.json b/homeassistant/components/cast/.translations/nl.json new file mode 100644 index 00000000000000..91c428770f5fc8 --- /dev/null +++ b/homeassistant/components/cast/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Google Cast instellen?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sl.json b/homeassistant/components/cast/.translations/sl.json new file mode 100644 index 00000000000000..24a7215574dbd9 --- /dev/null +++ b/homeassistant/components/cast/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json new file mode 100644 index 00000000000000..711ac3203978c6 --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0721cac3321bfc..1588766e406c78 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", + "allow_deconz_groups": "Povolit import skupin deCONZ " }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 9d3dc9e6e62f41..b09b7e15b31a61 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -19,8 +19,14 @@ "link": { "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" + }, + "options": { + "data": { + "allow_clip_sensor": "Import virtueller Sensoren zulassen", + "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 46190d23926b8c..3de7de9ddb3e6e 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren", + "allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen" }, "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 90d13bb39b470d..6f3fa2ec9a4c01 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -19,6 +19,13 @@ "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"", "title": "Koppel met deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Sta het importeren van virtuele sensoren toe", + "allow_deconz_groups": "Sta de import van deCONZ-groepen toe" + }, + "title": "Extra configuratieopties voor deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 59c5577c96b5a8..bc7a2cbd861583 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", + "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" }, "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 17cbe87f1e8f9b..5cd1a14d499bd3 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" }, "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index d466488e9fcd85..dc0968dc88acbb 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -24,6 +24,6 @@ "title": "Hub verbinden" } }, - "title": "Philips Hue Bridge" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index ea1e4fff1bf915..b471dd1a0cd59e 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -24,6 +24,6 @@ "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" } }, - "title": "\u0428\u043b\u044e\u0437 Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/cs.json b/homeassistant/components/nest/.translations/cs.json new file mode 100644 index 00000000000000..c884226174b00b --- /dev/null +++ b/homeassistant/components/nest/.translations/cs.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "M\u016f\u017eete nastavit pouze jeden Nest \u00fa\u010det.", + "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00ed URL vypr\u0161el", + "no_flows": "Pot\u0159ebujete nakonfigurovat Nest, abyste se s n\u00edm mohli autentizovat. [P\u0159e\u010dt\u011bte si pros\u00edm pokyny] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern\u00ed chyba ov\u011b\u0159en\u00ed k\u00f3du", + "invalid_code": "Neplatn\u00fd k\u00f3d", + "timeout": "\u010casov\u00fd limit ov\u011b\u0159ov\u00e1n\u00ed k\u00f3du vypr\u0161el", + "unknown": "Nezn\u00e1m\u00e1 chyba ov\u011b\u0159en\u00ed k\u00f3du" + }, + "step": { + "init": { + "data": { + "flow_impl": "Poskytovatel" + }, + "description": "Zvolte pomoc\u00ed kter\u00e9ho poskytovatele ov\u011b\u0159en\u00ed chcete ov\u011b\u0159it slu\u017ebu Nest.", + "title": "Poskytovatel ov\u011b\u0159en\u00ed" + }, + "link": { + "data": { + "code": "K\u00f3d PIN" + }, + "description": "Chcete-li propojit \u00fa\u010det Nest, [autorizujte sv\u016fj \u00fa\u010det]({url}). \n\n Po autorizaci zkop\u00edrujte n\u00ed\u017ee uveden\u00fd k\u00f3d PIN.", + "title": "Propojit s Nest \u00fa\u010dtem" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json new file mode 100644 index 00000000000000..721eafa807fb6d --- /dev/null +++ b/homeassistant/components/nest/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "init": { + "data": { + "flow_impl": "Anbieter" + }, + "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.", + "title": "Authentifizierungsanbieter" + }, + "link": { + "data": { + "code": "PIN Code" + }, + "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.", + "title": "Nest-Konto verkn\u00fcpfen" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json new file mode 100644 index 00000000000000..abf8f79599f505 --- /dev/null +++ b/homeassistant/components/nest/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d" + }, + "step": { + "init": { + "data": { + "flow_impl": "Szolg\u00e1ltat\u00f3" + } + }, + "link": { + "data": { + "code": "PIN-k\u00f3d" + } + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json new file mode 100644 index 00000000000000..ca34179cf5b191 --- /dev/null +++ b/homeassistant/components/nest/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "init": { + "title": "Fornitore di autenticazione" + }, + "link": { + "data": { + "code": "Codice PIN" + }, + "description": "Per collegare l'account Nido, [autorizzare l'account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito di seguito.", + "title": "Collega un account Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/lb.json b/homeassistant/components/nest/.translations/lb.json new file mode 100644 index 00000000000000..197cc8206d0510 --- /dev/null +++ b/homeassistant/components/nest/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen\u00a0Nest Kont\u00a0konfigur\u00e9ieren.", + "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung\u00a0beim gener\u00e9ieren\u00a0vun der Autorisatiouns\u00a0URL.", + "no_flows": "Dir musst Nest konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung\u00a0k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne Feeler beim valid\u00e9ieren vum Code", + "invalid_code": "Ong\u00ebltege Code", + "timeout": "Z\u00e4it Iwwerschreidung\u00a0beim valid\u00e9ieren vum Code", + "unknown": "Onbekannte Feeler beim valid\u00e9ieren vum Code" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ubidder" + }, + "description": "Wielt den Authentifikatioun Ubidder deen sech mat Nest verbanne soll.", + "title": "Authentifikatioun Ubidder" + }, + "link": { + "data": { + "code": "Pin code" + }, + "description": "Fir den Nest Kont ze verbannen, [autoris\u00e9iert \u00e4ren Kont]({url}).\nKop\u00e9iert no der Autorisatioun den Pin hei \u00ebnnendr\u00ebnner", + "title": "Nest Kont verbannen" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/nl.json b/homeassistant/components/nest/.translations/nl.json new file mode 100644 index 00000000000000..756eb07189a2be --- /dev/null +++ b/homeassistant/components/nest/.translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Je kunt slechts \u00e9\u00e9n Nest-account configureren.", + "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", + "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url.", + "no_flows": "U moet Nest configureren voordat u zich ermee kunt authenticeren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne foutvalidatiecode", + "invalid_code": "Ongeldige code", + "timeout": "Time-out validatie van code", + "unknown": "Onbekende foutvalidatiecode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverancier" + }, + "description": "Kies met welke authenticatieleverancier u wilt verifi\u00ebren met Nest.", + "title": "Authenticatieleverancier" + }, + "link": { + "data": { + "code": "Pincode" + }, + "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", + "title": "Koppel Nest-account" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sl.json b/homeassistant/components/nest/.translations/sl.json new file mode 100644 index 00000000000000..d038ed4157fab0 --- /dev/null +++ b/homeassistant/components/nest/.translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Nest.", + "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Nest. [Preberite navodila](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Notranja napaka pri preverjanju kode", + "invalid_code": "Neveljavna koda", + "timeout": "\u010casovna omejitev je potekla pri preverjanju kode", + "unknown": "Neznana napaka pri preverjanju kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ponudnik" + }, + "description": "Izberite prek katerega ponudnika overjanja \u017eelite overiti Nest.", + "title": "Ponudnik za preverjanje pristnosti" + }, + "link": { + "data": { + "code": "PIN koda" + }, + "description": "\u010ce \u017eelite povezati svoj ra\u010dun Nest, [pooblastite svoj ra\u010dun]({url}). \n\n Po odobritvi kopirajte in prilepite podano kodo PIN.", + "title": "Pove\u017eite Nest ra\u010dun" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hant.json b/homeassistant/components/nest/.translations/zh-Hant.json new file mode 100644 index 00000000000000..6b9dbdb19b1148 --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Nest \u5e33\u865f\u3002", + "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/nest/\uff09\u3002" + }, + "error": { + "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", + "invalid_code": "\u8a8d\u8b49\u78bc\u7121\u6548", + "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642", + "unknown": "\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "description": "\u65bc\u8a8d\u8b49\u63d0\u4f9b\u8005\u4e2d\u6311\u9078\u6240\u8981\u9032\u884c Nest \u8a8d\u8b49\u63d0\u4f9b\u8005\u3002", + "title": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u78bc" + }, + "description": "\u6b32\u9023\u7d50 Nest \u5e33\u865f\uff0c[\u8a8d\u8b49\u5e33\u865f]({url}).\n\n\u65bc\u8a8d\u8b49\u5f8c\uff0c\u8907\u88fd\u4e26\u8cbc\u4e0a\u4e0b\u65b9\u7684\u8a8d\u8b49\u78bc\u3002", + "title": "\u9023\u7d50 Nest \u5e33\u865f" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/cs.json b/homeassistant/components/sonos/.translations/cs.json new file mode 100644 index 00000000000000..c0b26284cdff39 --- /dev/null +++ b/homeassistant/components/sonos/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Sonos.", + "single_instance_allowed": "Je t\u0159eba jen jedna konfigurace Sonos." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json new file mode 100644 index 00000000000000..f1b76b0d155787 --- /dev/null +++ b/homeassistant/components/sonos/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Sonos konfigurieren?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/hu.json b/homeassistant/components/sonos/.translations/hu.json new file mode 100644 index 00000000000000..4726d57ad249a7 --- /dev/null +++ b/homeassistant/components/sonos/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Sonos eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/it.json b/homeassistant/components/sonos/.translations/it.json new file mode 100644 index 00000000000000..e32557f1d95566 --- /dev/null +++ b/homeassistant/components/sonos/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Non sono presenti dispositivi Sonos in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Sonos." + }, + "step": { + "confirm": { + "description": "Vuoi installare Sonos", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/lb.json b/homeassistant/components/sonos/.translations/lb.json new file mode 100644 index 00000000000000..26eaec4584d4dc --- /dev/null +++ b/homeassistant/components/sonos/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Sonos Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Sonos ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Sonos konfigur\u00e9iert ginn?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/nl.json b/homeassistant/components/sonos/.translations/nl.json new file mode 100644 index 00000000000000..de84482cc63c45 --- /dev/null +++ b/homeassistant/components/sonos/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Sonos-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Sonos nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Sonos instellen?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sl.json b/homeassistant/components/sonos/.translations/sl.json new file mode 100644 index 00000000000000..6773465bbbfd22 --- /dev/null +++ b/homeassistant/components/sonos/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav Sonos.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Sonosa." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c6fb13c3605d30 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Sonos\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/script/translations_download b/script/translations_download index 099e32c9d1b164..15b6a6810563d9 100755 --- a/script/translations_download +++ b/script/translations_download @@ -28,7 +28,7 @@ mkdir -p ${LOCAL_DIR} docker run \ -v ${LOCAL_DIR}:/opt/dest/locale \ - lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 lokalise \ + lokalise/lokalise-cli@sha256:ddf5677f58551261008342df5849731c88bcdc152ab645b133b21819aede8218 lokalise \ --token ${LOKALISE_TOKEN} \ export ${PROJECT_ID} \ --export_empty skip \ From 00c366d7ea0f292da30884fb5b3af6cba5045e45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 08:56:37 -0400 Subject: [PATCH 144/169] Update frontend to 20180702.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9a32626c66a0fe..b916b794936039 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180701.0'] +REQUIREMENTS = ['home-assistant-frontend==20180702.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 08985bff945e36..52e9c6a719ffb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180701.0 +home-assistant-frontend==20180702.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bfe170c67c0cf..ea62c0bd7e4fae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180701.0 +home-assistant-frontend==20180702.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2145ac5e4602cdd49eb1de40592db6058f49bb1d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 2 Jul 2018 10:55:34 -0400 Subject: [PATCH 145/169] Added support for Duke Energy smart meters (#15165) * Added support for Duke Energy smart meters * Fixed hound * Added function docstring * Moved strings to constants, implemented unique_id, and cleaned up setup. * Added doc string. * Fixed review issues. * Updated pydukenergy to 0.0.6 and set update interval to 2 hours * Updated requirements_all --- .coveragerc | 1 + .../components/sensor/duke_energy.py | 84 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 88 insertions(+) create mode 100644 homeassistant/components/sensor/duke_energy.py diff --git a/.coveragerc b/.coveragerc index 90b0a7f475d8b5..a100e2c0a4958d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -612,6 +612,7 @@ omit = homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dublin_bus_transport.py + homeassistant/components/sensor/duke_energy.py homeassistant/components/sensor/dwd_weather_warnings.py homeassistant/components/sensor/ebox.py homeassistant/components/sensor/eddystone_temperature.py diff --git a/homeassistant/components/sensor/duke_energy.py b/homeassistant/components/sensor/duke_energy.py new file mode 100644 index 00000000000000..458a2929d0b6b3 --- /dev/null +++ b/homeassistant/components/sensor/duke_energy.py @@ -0,0 +1,84 @@ +""" +Support for Duke Energy Gas and Electric meters. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.duke_energy/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pydukeenergy==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + +LAST_BILL_USAGE = "last_bills_usage" +LAST_BILL_AVERAGE_USAGE = "last_bills_average_usage" +LAST_BILL_DAYS_BILLED = "last_bills_days_billed" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup all Duke Energy meters.""" + from pydukeenergy.api import DukeEnergy, DukeEnergyException + + try: + duke = DukeEnergy(config[CONF_USERNAME], + config[CONF_PASSWORD], + update_interval=120) + except DukeEnergyException: + _LOGGER.error("Failed to setup Duke Energy") + return + + add_devices([DukeEnergyMeter(meter) for meter in duke.get_meters()]) + + +class DukeEnergyMeter(Entity): + """Representation of a Duke Energy meter.""" + + def __init__(self, meter): + """Initialize the meter.""" + self.duke_meter = meter + + @property + def name(self): + """Return the name.""" + return "duke_energy_{}".format(self.duke_meter.id) + + @property + def unique_id(self): + """Return the unique ID.""" + return self.duke_meter.id + + @property + def state(self): + """Return yesterdays usage.""" + return self.duke_meter.get_usage() + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self.duke_meter.get_unit() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + LAST_BILL_USAGE: self.duke_meter.get_total(), + LAST_BILL_AVERAGE_USAGE: self.duke_meter.get_average(), + LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed() + } + return attributes + + def update(self): + """Update meter.""" + self.duke_meter.update() diff --git a/requirements_all.txt b/requirements_all.txt index 52e9c6a719ffb2..3f58fefb389d6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -791,6 +791,9 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 +# homeassistant.components.sensor.duke_energy +pydukeenergy==0.0.6 + # homeassistant.components.sensor.ebox pyebox==1.1.4 From f3588a8782aa9297cfad34ff20bad81cf9c893e3 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 2 Jul 2018 15:57:52 +0100 Subject: [PATCH 146/169] Update image_processing async (#15082) * scan() -> async_job * added async_scan --- .../components/image_processing/__init__.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 29f26cc84e6c0e..480ec31da7d127 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -69,27 +69,32 @@ @bind_hass def scan(hass, entity_id=None): - """Force process an image.""" + """Force process of all cameras or given entity.""" + hass.add_job(async_scan, hass, entity_id) + + +@callback +@bind_hass +def async_scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_SCAN, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_setup(config) + await component.async_setup(config) - @asyncio.coroutine - def async_scan_service(service): + async def async_scan_service(service): """Service handler for scan.""" image_entities = component.async_extract_from_service(service) update_task = [entity.async_update_ha_state(True) for entity in image_entities] if update_task: - yield from asyncio.wait(update_task, loop=hass.loop) + await asyncio.wait(update_task, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_scan_service, @@ -124,8 +129,7 @@ def async_process_image(self, image): """ return self.hass.async_add_job(self.process_image, image) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update image and process it. This method is a coroutine. @@ -134,7 +138,7 @@ def async_update(self): image = None try: - image = yield from camera.async_get_image( + image = await camera.async_get_image( self.camera_entity, timeout=self.timeout) except HomeAssistantError as err: @@ -142,7 +146,7 @@ def async_update(self): return # process image data - yield from self.async_process_image(image.content) + await self.async_process_image(image.content) class ImageProcessingFaceEntity(ImageProcessingEntity): From 0feb4c5439daa5104f1315c5f5d6b3b5c89d309c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 14:43:31 -0400 Subject: [PATCH 147/169] Bump frontend to 20180702.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b916b794936039..25859020be46d7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180702.0'] +REQUIREMENTS = ['home-assistant-frontend==20180702.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3f58fefb389d6f..34e3283abb8a58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.0 +home-assistant-frontend==20180702.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea62c0bd7e4fae..a50e10a871ee15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.0 +home-assistant-frontend==20180702.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e6390b8e413b8573fcc83f09cc97a01b9dd530b9 Mon Sep 17 00:00:00 2001 From: shker Date: Tue, 3 Jul 2018 04:33:40 +0800 Subject: [PATCH 148/169] Fix python-miio 0.4 compatibility of the xiaomi miio device tracker (#15244) --- homeassistant/components/device_tracker/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index 5d6e1453124c09..074d6a1054ee51 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -64,7 +64,7 @@ async def async_scan_devices(self): station_info = await self.hass.async_add_job(self.device.status) _LOGGER.debug("Got new station info: %s", station_info) - for device in station_info['mat']: + for device in station_info.associated_stations: devices.append(device['mac']) except DeviceException as ex: From 120111ceeefc1ab1c864b05c44d45f6f6b3fed93 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 Jul 2018 23:03:56 +0200 Subject: [PATCH 149/169] Upgrade keyring to 13.1.0 (#15268) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 51d70d1f3b2cec..0ca60894f9b983 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==13.0.0', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==13.1.0', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 34e3283abb8a58..800b7e85c16acd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -473,7 +473,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==13.0.0 +keyring==13.1.0 # homeassistant.scripts.keyring keyrings.alt==3.1 From bedd2d7e41f66bbad4380fd9efd5fc351b939ea3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 2 Jul 2018 23:14:38 +0200 Subject: [PATCH 150/169] deCONZ - new sensor attribute 'on' and new sensor GenericFlag (#15247) * New sensor attribute 'on' * New sensor GenericFlag --- homeassistant/components/binary_sensor/deconz.py | 13 ++++++++----- homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/deconz/const.py | 3 +++ homeassistant/components/sensor/deconz.py | 13 ++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 6f59da0755ae37..0a370d754eea4d 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -5,9 +5,9 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.deconz import ( - CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB) +from homeassistant.components.deconz.const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -62,7 +62,8 @@ def async_update_callback(self, reason): """ if reason['state'] or \ 'reachable' in reason['attr'] or \ - 'battery' in reason['attr']: + 'battery' in reason['attr'] or \ + 'on' in reason['attr']: self.async_schedule_update_ha_state() @property @@ -107,6 +108,8 @@ def device_state_attributes(self): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.on is not None: + attr[ATTR_ON] = self._sensor.on if self._sensor.type in PRESENCE and self._sensor.dark is not None: - attr['dark'] = self._sensor.dark + attr[ATTR_DARK] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 4fa89f8cfd3b52..88174b9d61297b 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==39'] +REQUIREMENTS = ['pydeconz==42'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index f7aa4c7a43057d..6deee322a15e31 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -11,3 +11,6 @@ CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' + +ATTR_DARK = 'dark' +ATTR_ON = 'on' diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 0db06622ad8338..7c492fd496d263 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -4,9 +4,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ -from homeassistant.components.deconz import ( - CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB) +from homeassistant.components.deconz.const import ( + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -72,7 +72,8 @@ def async_update_callback(self, reason): """ if reason['state'] or \ 'reachable' in reason['attr'] or \ - 'battery' in reason['attr']: + 'battery' in reason['attr'] or \ + 'on' in reason['attr']: self.async_schedule_update_ha_state() @property @@ -122,8 +123,10 @@ def device_state_attributes(self): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.on is not None: + attr[ATTR_ON] = self._sensor.on if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: - attr['dark'] = self._sensor.dark + attr[ATTR_DARK] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/requirements_all.txt b/requirements_all.txt index 800b7e85c16acd..b3bb6261cabcec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==39 +pydeconz==42 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a50e10a871ee15..6942b16bc29f2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==39 +pydeconz==42 # homeassistant.components.zwave pydispatcher==2.0.5 From fb65276daf96b6e844bee1ed060b2c2fc3ba0449 Mon Sep 17 00:00:00 2001 From: nielstron Date: Mon, 2 Jul 2018 23:59:04 +0200 Subject: [PATCH 151/169] Remove math.inf as bounds --- homeassistant/components/sensor/filter.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 770287228a2ed3..bc0764f3a35477 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -11,7 +11,6 @@ from functools import partial from copy import copy from datetime import timedelta -import math import voluptuous as vol @@ -55,8 +54,6 @@ DEFAULT_PRECISION = 2 DEFAULT_FILTER_RADIUS = 2.0 DEFAULT_FILTER_TIME_CONSTANT = 10 -DEFAULT_LOWER_BOUND = -math.inf -DEFAULT_UPPER_BOUND = math.inf NAME_TEMPLATE = "{} filter" ICON = 'mdi:chart-line-variant' @@ -85,10 +82,8 @@ FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, - vol.Optional(CONF_FILTER_LOWER_BOUND, - default=DEFAULT_LOWER_BOUND): vol.Coerce(float), - vol.Optional(CONF_FILTER_UPPER_BOUND, - default=DEFAULT_UPPER_BOUND): vol.Coerce(float), + vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), + vol.Optional(CONF_FILTER_UPPER_BOUND): vol.Coerce(float), }) FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ @@ -353,7 +348,7 @@ class RangeFilter(Filter): """ def __init__(self, entity, - lower_bound, upper_bound): + lower_bound=None, upper_bound=None): """Initialize Filter.""" super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound @@ -362,7 +357,7 @@ def __init__(self, entity, def _filter_state(self, new_state): """Implement the range filter.""" - if new_state.state > self._upper_bound: + if self._upper_bound and new_state.state > self._upper_bound: self._stats_internal['erasures_up'] += 1 @@ -371,7 +366,7 @@ def _filter_state(self, new_state): self._entity, new_state) new_state.state = self._upper_bound - elif new_state.state < self._lower_bound: + elif self._lower_bound and new_state.state < self._lower_bound: self._stats_internal['erasures_low'] += 1 From 31e23ebae2f227968b9d184e6bb7f5ba9797dd88 Mon Sep 17 00:00:00 2001 From: Paul Stenius Date: Mon, 2 Jul 2018 17:03:46 -0500 Subject: [PATCH 152/169] expose climate current temperature in prometeus metrics (#15232) * expose climate current temperature in prometeus metrics * import ATTR_CURRENT_TEMPERATURE from climate instead of const * remove duplicated ATTR_CURRENT_TEMPERATURE from const * fix ATTR_CURRENT_TEMPERATURE import --- homeassistant/components/prometheus.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 6f233dafe0832a..0a6c959f243d06 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -10,6 +10,7 @@ import voluptuous as vol from aiohttp import web +from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, @@ -180,6 +181,15 @@ def _handle_climate(self, state): 'Temperature in degrees Celsius') metric.labels(**self._labels(state)).set(temp) + current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) + if current_temp: + if unit == TEMP_FAHRENHEIT: + current_temp = fahrenheit_to_celsius(current_temp) + metric = self._metric( + 'current_temperature_c', self.prometheus_client.Gauge, + 'Current Temperature in degrees Celsius') + metric.labels(**self._labels(state)).set(current_temp) + metric = self._metric( 'climate_state', self.prometheus_client.Gauge, 'State of the thermostat (0/1)') From cd1cfd7e8ee20dccd6273983df0c881b1bb3bc50 Mon Sep 17 00:00:00 2001 From: pepeEL Date: Tue, 3 Jul 2018 08:39:42 +0200 Subject: [PATCH 153/169] New device to support option MY in somfy (#15272) New device to support option MY in somfy --- homeassistant/components/cover/tahoma.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index cf8b7dfad48c0e..824e330d6a0712 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -81,7 +81,11 @@ def stop_cover(self, **kwargs): self.apply_action('setPosition', 'secured') elif self.tahoma_device.type in \ ('rts:BlindRTSComponent', - 'io:ExteriorVenetianBlindIOComponent'): + 'io:ExteriorVenetianBlindIOComponent', + 'rts:VenetianBlindRTSComponent', + 'rts:DualCurtainRTSComponent', + 'rts:ExteriorVenetianBlindRTSComponent', + 'rts:BlindRTSComponent'): self.apply_action('my') else: self.apply_action('stopIdentify') From ed3fe1cc6f6e8c4cb3bf5a5d1d9b8a0478e3f668 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Jul 2018 09:47:14 +0200 Subject: [PATCH 154/169] Add isort configuration (#15278) --- .isort.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000000000..79a65508287448 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +multi_line_output=4 From 6420ab5535a3edc38ee2e839b9bc4d295297d647 Mon Sep 17 00:00:00 2001 From: nielstron Date: Tue, 3 Jul 2018 11:06:10 +0200 Subject: [PATCH 155/169] Remove default none from filter sensor --- homeassistant/components/sensor/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index bc0764f3a35477..261f6e2b510a3f 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -348,7 +348,7 @@ class RangeFilter(Filter): """ def __init__(self, entity, - lower_bound=None, upper_bound=None): + lower_bound, upper_bound): """Initialize Filter.""" super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound From 232f56de6297ee1bb5c7dbcfbb587f5096720f40 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Jul 2018 12:30:56 +0200 Subject: [PATCH 156/169] Add support for new API (fixes #14911) (#15279) --- homeassistant/components/sensor/fixer.py | 27 +++++++++++------------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index 3e909b7b21de55..438366ae5558e0 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -10,15 +10,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_BASE, CONF_NAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['fixerio==0.1.1'] +REQUIREMENTS = ['fixerio==1.0.0a0'] _LOGGER = logging.getLogger(__name__) -ATTR_BASE = 'Base currency' ATTR_EXCHANGE_RATE = 'Exchange rate' ATTR_TARGET = 'Target currency' @@ -33,8 +32,8 @@ SCAN_INTERVAL = timedelta(days=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TARGET): cv.string, - vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -43,17 +42,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Fixer.io sensor.""" from fixerio import Fixerio, exceptions + api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) - base = config.get(CONF_BASE) target = config.get(CONF_TARGET) try: - Fixerio(base=base, symbols=[target], secure=True).latest() + Fixerio(symbols=[target], access_key=api_key).latest() except exceptions.FixerioException: _LOGGER.error("One of the given currencies is not supported") - return False + return - data = ExchangeData(base, target) + data = ExchangeData(target, api_key) add_devices([ExchangeRateSensor(data, name, target)], True) @@ -87,10 +86,9 @@ def device_state_attributes(self): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_BASE: self.data.rate['base'], - ATTR_TARGET: self._target, - ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], + ATTR_TARGET: self._target, } @property @@ -107,16 +105,15 @@ def update(self): class ExchangeData(object): """Get the latest data and update the states.""" - def __init__(self, base_currency, target_currency): + def __init__(self, target_currency, api_key): """Initialize the data object.""" from fixerio import Fixerio + self.api_key = api_key self.rate = None - self.base_currency = base_currency self.target_currency = target_currency self.exchange = Fixerio( - base=self.base_currency, symbols=[self.target_currency], - secure=True) + symbols=[self.target_currency], access_key=self.api_key) def update(self): """Get the latest data from Fixer.io.""" diff --git a/requirements_all.txt b/requirements_all.txt index b3bb6261cabcec..b6677698772544 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -338,7 +338,7 @@ fints==0.2.1 fitbit==0.3.0 # homeassistant.components.sensor.fixer -fixerio==0.1.1 +fixerio==1.0.0a0 # homeassistant.components.light.flux_led flux_led==0.21 From 184d0a99c076cadf0335d45035a111e561ae6695 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 3 Jul 2018 13:43:24 +0300 Subject: [PATCH 157/169] Switch to own packaged version of suds-passworddigest (#15261) --- homeassistant/components/camera/onvif.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 3ae47ba5dee9df..32f8e15748d7b1 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -25,9 +25,7 @@ REQUIREMENTS = ['onvif-py3==0.1.3', 'suds-py3==1.3.3.0', - 'http://github.com/tgaugry/suds-passworddigest-py3' - '/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip' - '#suds-passworddigest-py3==0.1.2a'] + 'suds-passworddigest-homeassistant==0.1.2a0.dev0'] DEPENDENCIES = ['ffmpeg'] DEFAULT_NAME = 'ONVIF Camera' DEFAULT_PORT = 5000 diff --git a/requirements_all.txt b/requirements_all.txt index b6677698772544..c170a477f0724d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -423,9 +423,6 @@ home-assistant-frontend==20180702.1 # homeassistant.components.homematicip_cloud homematicip==0.9.4 -# homeassistant.components.camera.onvif -http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a - # homeassistant.components.remember_the_milk httplib2==0.10.3 @@ -1294,6 +1291,9 @@ statsd==3.2.1 # homeassistant.components.sensor.steam_online steamodd==4.21 +# homeassistant.components.camera.onvif +suds-passworddigest-homeassistant==0.1.2a0.dev0 + # homeassistant.components.camera.onvif suds-py3==1.3.3.0 From 5ec61e4649b4dd94a80d695a34b254f2a2a0f1d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 11:03:23 -0400 Subject: [PATCH 158/169] Bump frontend to 20180703.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 25859020be46d7..cb5f06f12ed6c0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180702.1'] +REQUIREMENTS = ['home-assistant-frontend==20180703.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index c170a477f0724d..26a0745a37f3b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.1 +home-assistant-frontend==20180703.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6942b16bc29f2e..01ac2b301e11c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.1 +home-assistant-frontend==20180703.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 857c58c4b7d1bfe255240b6af61acb9854fc3117 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 13:20:42 -0400 Subject: [PATCH 159/169] Disable the calendar panel (#15282) --- homeassistant/components/calendar/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 9716e46bc032af..35566b0cbed9f6 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -41,8 +41,9 @@ async def async_setup(hass, config): hass.http.register_view(CalendarListView(component)) hass.http.register_view(CalendarEventView(component)) - await hass.components.frontend.async_register_built_in_panel( - 'calendar', 'calendar', 'hass:calendar') + # Doesn't work in prod builds of the frontend: home-assistant-polymer#1289 + # await hass.components.frontend.async_register_built_in_panel( + # 'calendar', 'calendar', 'hass:calendar') await component.async_setup(config) return True From b2df199674112010d880506a4fa9eed08c637c80 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 14:51:57 -0400 Subject: [PATCH 160/169] Bump frontend to 20180703.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index cb5f06f12ed6c0..d74aadd332303a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180703.0'] +REQUIREMENTS = ['home-assistant-frontend==20180703.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 26a0745a37f3b7..35e740082eab71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.0 +home-assistant-frontend==20180703.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01ac2b301e11c3..1715f95b5df45c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.0 +home-assistant-frontend==20180703.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2525fc52b35eda328bf63d13270ccb428dac9a08 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Jul 2018 20:41:54 -0600 Subject: [PATCH 161/169] Update Tile platform to be async (#15073) * Updated * Updated requirements * Added expired session handling * Changes * Member-requested changes * Bump to 2.0.2 * Bumping requirements * Better exception handling and tidying * Move asyncio stuff to HASS built-ins * Revising re-initi * Hound * Hound --- .../components/device_tracker/tile.py | 162 ++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 92 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index 377686b69054f3..6df9f3c9974caa 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -5,24 +5,22 @@ https://home-assistant.io/components/device_tracker.tile/ """ import logging +from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD) -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['pytile==1.1.0'] +REQUIREMENTS = ['pytile==2.0.2'] CLIENT_UUID_CONFIG_FILE = '.tile.conf' -DEFAULT_ICON = 'mdi:bluetooth' DEVICE_TYPES = ['PHONE', 'TILE'] ATTR_ALTITUDE = 'altitude' @@ -34,89 +32,111 @@ CONF_SHOW_INACTIVE = 'show_inactive' +DEFAULT_ICON = 'mdi:bluetooth' +DEFAULT_SCAN_INTERVAL = timedelta(minutes=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean, - vol.Optional(CONF_MONITORED_VARIABLES): + vol.Optional(CONF_MONITORED_VARIABLES, default=DEVICE_TYPES): vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), }) -def setup_scanner(hass, config: dict, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - TileDeviceScanner(hass, config, see) - return True - - -class TileDeviceScanner(DeviceScanner): - """Define a device scanner for Tiles.""" - - def __init__(self, hass, config, see): + from pytile import Client + + websession = aiohttp_client.async_get_clientsession(hass) + + config_data = await hass.async_add_job( + load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE)) + if config_data: + client = Client( + config[CONF_USERNAME], + config[CONF_PASSWORD], + websession, + client_uuid=config_data['client_uuid']) + else: + client = Client( + config[CONF_USERNAME], config[CONF_PASSWORD], websession) + + config_data = {'client_uuid': client.client_uuid} + config_saved = await hass.async_add_job( + save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data) + if not config_saved: + _LOGGER.error('Failed to save the client UUID') + + scanner = TileScanner( + client, hass, async_see, config[CONF_MONITORED_VARIABLES], + config[CONF_SHOW_INACTIVE]) + return await scanner.async_init() + + +class TileScanner(object): + """Define an object to retrieve Tile data.""" + + def __init__(self, client, hass, async_see, types, show_inactive): """Initialize.""" - from pytile import Client - - _LOGGER.debug('Received configuration data: %s', config) + self._async_see = async_see + self._client = client + self._hass = hass + self._show_inactive = show_inactive + self._types = types - # Load the client UUID (if it exists): - config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE)) - if config_data: - _LOGGER.debug('Using existing client UUID') - self._client = Client( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config_data['client_uuid']) - else: - _LOGGER.debug('Generating new client UUID') - self._client = Client( - config[CONF_USERNAME], - config[CONF_PASSWORD]) + async def async_init(self): + """Further initialize connection to the Tile servers.""" + from pytile.errors import TileError - if not save_json( - hass.config.path(CLIENT_UUID_CONFIG_FILE), - {'client_uuid': self._client.client_uuid}): - _LOGGER.error("Failed to save configuration file") + try: + await self._client.async_init() + except TileError as err: + _LOGGER.error('Unable to set up Tile scanner: %s', err) + return False - _LOGGER.debug('Client UUID: %s', self._client.client_uuid) - _LOGGER.debug('User UUID: %s', self._client.user_uuid) + await self._async_update() - self._show_inactive = config.get(CONF_SHOW_INACTIVE) - self._types = config.get(CONF_MONITORED_VARIABLES) + async_track_time_interval( + self._hass, self._async_update, DEFAULT_SCAN_INTERVAL) - self.devices = {} - self.see = see + return True - track_utc_time_change( - hass, self._update_info, second=range(0, 60, 30)) + async def _async_update(self, now=None): + """Update info from Tile.""" + from pytile.errors import SessionExpiredError, TileError - self._update_info() + _LOGGER.debug('Updating Tile data') - def _update_info(self, now=None) -> None: - """Update the device info.""" - self.devices = self._client.get_tiles( - type_whitelist=self._types, show_inactive=self._show_inactive) + try: + await self._client.asayn_init() + tiles = await self._client.tiles.all( + whitelist=self._types, show_inactive=self._show_inactive) + except SessionExpiredError: + _LOGGER.info('Session expired; trying again shortly') + return + except TileError as err: + _LOGGER.error('There was an error while updating: %s', err) + return - if not self.devices: + if not tiles: _LOGGER.warning('No Tiles found') return - for dev in self.devices: - dev_id = 'tile_{0}'.format(slugify(dev['name'])) - lat = dev['tileState']['latitude'] - lon = dev['tileState']['longitude'] - - attrs = { - ATTR_ALTITUDE: dev['tileState']['altitude'], - ATTR_CONNECTION_STATE: dev['tileState']['connection_state'], - ATTR_IS_DEAD: dev['is_dead'], - ATTR_IS_LOST: dev['tileState']['is_lost'], - ATTR_RING_STATE: dev['tileState']['ring_state'], - ATTR_VOIP_STATE: dev['tileState']['voip_state'], - } - - self.see( - dev_id=dev_id, - gps=(lat, lon), - attributes=attrs, - icon=DEFAULT_ICON - ) + for tile in tiles: + await self._async_see( + dev_id='tile_{0}'.format(slugify(tile['name'])), + gps=( + tile['tileState']['latitude'], + tile['tileState']['longitude'] + ), + attributes={ + ATTR_ALTITUDE: tile['tileState']['altitude'], + ATTR_CONNECTION_STATE: + tile['tileState']['connection_state'], + ATTR_IS_DEAD: tile['is_dead'], + ATTR_IS_LOST: tile['tileState']['is_lost'], + ATTR_RING_STATE: tile['tileState']['ring_state'], + ATTR_VOIP_STATE: tile['tileState']['voip_state'], + }, + icon=DEFAULT_ICON) diff --git a/requirements_all.txt b/requirements_all.txt index 35e740082eab71..9591925ac24775 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1113,7 +1113,7 @@ pythonegardia==1.0.39 pythonwhois==2.4.3 # homeassistant.components.device_tracker.tile -pytile==1.1.0 +pytile==2.0.2 # homeassistant.components.climate.touchline pytouchline==0.7 From 42775142f82a5aca4abd3ed08e317d22bf50d020 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 4 Jul 2018 04:50:13 +0200 Subject: [PATCH 162/169] Fix yeelight light brightness integer (#15290) --- homeassistant/components/light/yeelight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 202c6ac594d807..791de291b4803d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -310,7 +310,7 @@ def update(self) -> None: bright = self._properties.get('bright', None) if bright: - self._brightness = 255 * (int(bright) / 100) + self._brightness = round(255 * (int(bright) / 100)) temp_in_k = self._properties.get('ct', None) if temp_in_k: From 5f7ac09a74ea25842cb74689254c99bc67267ebd Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 4 Jul 2018 06:44:47 +0100 Subject: [PATCH 163/169] Added Push Camera (#15151) * Added push camera * add camera.push * Address comments and add tests * auff auff * trip time made no sense * travis lint * Mock dependency * hound * long line * long line * better mocking * remove blank image * no more need to mock dependency * remove import * cleanup * no longer needed * unused constant * address @pvizeli review * add force_update * Revert "add force_update" This reverts commit e203785ea8232722effeec4fc70358190ec9284a. * rename parameter --- homeassistant/components/camera/push.py | 162 ++++++++++++++++++++++++ tests/components/camera/test_push.py | 63 +++++++++ 2 files changed, 225 insertions(+) create mode 100644 homeassistant/components/camera/push.py create mode 100644 tests/components/camera/test_push.py diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py new file mode 100644 index 00000000000000..fc4b18e26e40d2 --- /dev/null +++ b/homeassistant/components/camera/push.py @@ -0,0 +1,162 @@ +""" +Camera platform that receives images through HTTP POST. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/camera.push/ +""" +import logging + +from collections import deque +from datetime import timedelta +import voluptuous as vol + +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ + STATE_IDLE, STATE_RECORDING +from homeassistant.core import callback +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +CONF_BUFFER_SIZE = 'buffer' +CONF_IMAGE_FIELD = 'field' + +DEFAULT_NAME = "Push Camera" + +ATTR_FILENAME = 'filename' +ATTR_LAST_TRIP = 'last_trip' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int, + vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( + cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Push Camera platform.""" + cameras = [PushCamera(config[CONF_NAME], + config[CONF_BUFFER_SIZE], + config[CONF_TIMEOUT])] + + hass.http.register_view(CameraPushReceiver(cameras, + config[CONF_IMAGE_FIELD])) + + async_add_devices(cameras) + + +class CameraPushReceiver(HomeAssistantView): + """Handle pushes from remote camera.""" + + url = "/api/camera_push/{entity_id}" + name = 'api:camera_push:camera_entity' + + def __init__(self, cameras, image_field): + """Initialize CameraPushReceiver with camera entity.""" + self._cameras = cameras + self._image = image_field + + async def post(self, request, entity_id): + """Accept the POST from Camera.""" + try: + (_camera,) = [camera for camera in self._cameras + if camera.entity_id == entity_id] + except ValueError: + _LOGGER.error("Unknown push camera %s", entity_id) + return self.json_message('Unknown Push Camera', + HTTP_BAD_REQUEST) + + try: + data = await request.post() + _LOGGER.debug("Received Camera push: %s", data[self._image]) + await _camera.update_image(data[self._image].file.read(), + data[self._image].filename) + except ValueError as value_error: + _LOGGER.error("Unknown value %s", value_error) + return self.json_message('Invalid POST', HTTP_BAD_REQUEST) + except KeyError as key_error: + _LOGGER.error('In your POST message %s', key_error) + return self.json_message('{} missing'.format(self._image), + HTTP_BAD_REQUEST) + + +class PushCamera(Camera): + """The representation of a Push camera.""" + + def __init__(self, name, buffer_size, timeout): + """Initialize push camera component.""" + super().__init__() + self._name = name + self._last_trip = None + self._filename = None + self._expired_listener = None + self._state = STATE_IDLE + self._timeout = timeout + self.queue = deque([], buffer_size) + self._current_image = None + + @property + def state(self): + """Current state of the camera.""" + return self._state + + async def update_image(self, image, filename): + """Update the camera image.""" + if self._state == STATE_IDLE: + self._state = STATE_RECORDING + self._last_trip = dt_util.utcnow() + self.queue.clear() + + self._filename = filename + self.queue.appendleft(image) + + @callback + def reset_state(now): + """Set state to idle after no new images for a period of time.""" + self._state = STATE_IDLE + self._expired_listener = None + _LOGGER.debug("Reset state") + self.async_schedule_update_ha_state() + + if self._expired_listener: + self._expired_listener() + + self._expired_listener = async_track_point_in_utc_time( + self.hass, reset_state, dt_util.utcnow() + self._timeout) + + self.async_schedule_update_ha_state() + + async def async_camera_image(self): + """Return a still image response.""" + if self.queue: + if self._state == STATE_IDLE: + self.queue.rotate(1) + self._current_image = self.queue[0] + + return self._current_image + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + name: value for name, value in ( + (ATTR_LAST_TRIP, self._last_trip), + (ATTR_FILENAME, self._filename), + ) if value is not None + } diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py new file mode 100644 index 00000000000000..78053e540f5cd8 --- /dev/null +++ b/tests/components/camera/test_push.py @@ -0,0 +1,63 @@ +"""The tests for generic camera component.""" +import io + +from datetime import timedelta + +from homeassistant import core as ha +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util +from tests.components.auth import async_setup_auth + + +async def test_bad_posting(aioclient_mock, hass, aiohttp_client): + """Test that posting to wrong api endpoint fails.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + }}) + + client = await async_setup_auth(hass, aiohttp_client) + + # missing file + resp = await client.post('/api/camera_push/camera.config_test') + assert resp.status == 400 + + files = {'image': io.BytesIO(b'fake')} + + # wrong entity + resp = await client.post('/api/camera_push/camera.wrong', data=files) + assert resp.status == 400 + + +async def test_posting_url(aioclient_mock, hass, aiohttp_client): + """Test that posting to api endpoint works.""" + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'push', + 'name': 'config_test', + }}) + + client = await async_setup_auth(hass, aiohttp_client) + files = {'image': io.BytesIO(b'fake')} + + # initial state + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' + + # post image + resp = await client.post('/api/camera_push/camera.config_test', data=files) + assert resp.status == 200 + + # state recording + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'recording' + + # await timeout + shifted_time = dt_util.utcnow() + timedelta(seconds=15) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time}) + await hass.async_block_till_done() + + # back to initial state + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' From a6e9dc81aa38775342e8d115840e4f4c1a0142e2 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Wed, 4 Jul 2018 01:46:01 -0400 Subject: [PATCH 164/169] Added support to HTTPS URLs on SynologyDSM (#15270) * Added support to HTTPS URLs on SynologyDSM * Bumped python-synology to 0.1.1 * Makes lint happy * Added attribution to Synology and fixed 3rd library version * Fixed requirements_all.txt * Makes SynologyDSM defaults to 5001 using SSL --- .../components/sensor/synologydsm.py | 26 ++++++++++++++----- requirements_all.txt | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index a0198169b6d3d4..e3c3a0cf5caaed 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -12,18 +12,20 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, CONF_DISKS) + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, + EVENT_HOMEASSISTANT_START, CONF_DISKS) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['python-synology==0.1.0'] +REQUIREMENTS = ['python-synology==0.2.0'] _LOGGER = logging.getLogger(__name__) +CONF_ATTRIBUTION = 'Data provided by Synology' CONF_VOLUMES = 'volumes' DEFAULT_NAME = 'Synology DSM' -DEFAULT_PORT = 5000 +DEFAULT_PORT = 5001 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -74,6 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=True): cv.boolean, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS): @@ -95,10 +98,11 @@ def run_setup(event): port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) + use_ssl = config.get(CONF_SSL) unit = hass.config.units.temperature_unit monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) - api = SynoApi(host, port, username, password, unit) + api = SynoApi(host, port, username, password, unit, use_ssl) sensors = [SynoNasUtilSensor( api, variable, _UTILISATION_MON_COND[variable]) @@ -128,13 +132,14 @@ def run_setup(event): class SynoApi(object): """Class to interface with Synology DSM API.""" - def __init__(self, host, port, username, password, temp_unit): + def __init__(self, host, port, username, password, temp_unit, use_ssl): """Initialize the API wrapper class.""" from SynologyDSM import SynologyDSM self.temp_unit = temp_unit try: - self._api = SynologyDSM(host, port, username, password) + self._api = SynologyDSM(host, port, username, password, + use_https=use_ssl) except: # noqa: E722 # pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") @@ -185,6 +190,13 @@ def update(self): if self._api is not None: self._api.update() + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + class SynoNasUtilSensor(SynoNasSensor): """Representation a Synology Utilisation Sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index 9591925ac24775..0e6e6e0fc21bcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1080,7 +1080,7 @@ python-sochain-api==0.0.2 python-songpal==0.0.7 # homeassistant.components.sensor.synologydsm -python-synology==0.1.0 +python-synology==0.2.0 # homeassistant.components.tado python-tado==0.2.3 From cb129bd207df23fe0b41be61963b54029ac0f03b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Jul 2018 11:50:08 -0400 Subject: [PATCH 165/169] Add system generated users (#15291) * Add system generated users * Fix typing --- homeassistant/auth.py | 155 ++++++++++-------- homeassistant/components/auth/__init__.py | 16 +- tests/auth_providers/test_insecure_example.py | 2 +- .../test_legacy_api_password.py | 16 +- tests/common.py | 3 +- tests/components/conftest.py | 2 +- tests/test_auth.py | 57 ++++--- 7 files changed, 149 insertions(+), 102 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index a4e8ee05943b4c..e6760cd9096bc2 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -79,7 +79,14 @@ def name(self): async def async_credentials(self): """Return all credentials of this provider.""" - return await self.store.credentials_for_provider(self.type, self.id) + users = await self.store.async_get_users() + return [ + credentials + for user in users + for credentials in user.credentials + if (credentials.auth_provider_type == self.type and + credentials.auth_provider_id == self.id) + ] @callback def async_create_credentials(self, data): @@ -118,10 +125,11 @@ async def async_user_meta_for_credentials(self, credentials): class User: """A user.""" + name = attr.ib(type=str) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) - name = attr.ib(type=str, default=None) + system_generated = attr.ib(type=bool, default=False) # List of credentials of a user. credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False) @@ -300,10 +308,45 @@ async def async_get_user(self, user_id): """Retrieve a user.""" return await self._store.async_get_user(user_id) + async def async_create_system_user(self, name): + """Create a system user.""" + return await self._store.async_create_user( + name=name, + system_generated=True, + is_active=True, + ) + async def async_get_or_create_user(self, credentials): """Get or create a user.""" - return await self._store.async_get_or_create_user( - credentials, self._async_get_auth_provider(credentials)) + if not credentials.is_new: + for user in await self._store.async_get_users(): + for creds in user.credentials: + if (creds.auth_provider_type == + credentials.auth_provider_type + and creds.auth_provider_id == + credentials.auth_provider_id): + return user + + raise ValueError('Unable to find the user.') + + auth_provider = self._async_get_auth_provider(credentials) + info = await auth_provider.async_user_meta_for_credentials( + credentials) + + kwargs = { + 'credentials': credentials, + 'name': info.get('name') + } + + # Make owner and activate user if it's the first user. + if await self._store.async_get_users(): + kwargs['is_owner'] = False + kwargs['is_active'] = False + else: + kwargs['is_owner'] = True + kwargs['is_active'] = True + + return await self._store.async_create_user(**kwargs) async def async_link_user(self, user, credentials): """Link credentials to an existing user.""" @@ -313,9 +356,20 @@ async def async_remove_user(self, user): """Remove a user.""" await self._store.async_remove_user(user) - async def async_create_refresh_token(self, user, client_id): + async def async_create_refresh_token(self, user, client=None): """Create a new refresh token for a user.""" - return await self._store.async_create_refresh_token(user, client_id) + if not user.is_active: + raise ValueError('User is not active') + + if user.system_generated and client is not None: + raise ValueError( + 'System generated users cannot have refresh tokens connected ' + 'to a client.') + + if not user.system_generated and client is None: + raise ValueError('Client is required to generate a refresh token.') + + return await self._store.async_create_refresh_token(user, client) async def async_get_refresh_token(self, token): """Get refresh token by token.""" @@ -324,7 +378,7 @@ async def async_get_refresh_token(self, token): @callback def async_create_access_token(self, refresh_token): """Create a new access token.""" - access_token = AccessToken(refresh_token) + access_token = AccessToken(refresh_token=refresh_token) self._access_tokens[access_token.token] = access_token return access_token @@ -405,19 +459,6 @@ def __init__(self, hass): self._clients = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - async def credentials_for_provider(self, provider_type, provider_id): - """Return credentials for specific auth provider type and id.""" - if self._users is None: - await self.async_load() - - return [ - credentials - for user in self._users.values() - for credentials in user.credentials - if (credentials.auth_provider_type == provider_type and - credentials.auth_provider_id == provider_id) - ] - async def async_get_users(self): """Retrieve all users.""" if self._users is None: @@ -426,50 +467,42 @@ async def async_get_users(self): return list(self._users.values()) async def async_get_user(self, user_id): - """Retrieve a user.""" + """Retrieve a user by id.""" if self._users is None: await self.async_load() return self._users.get(user_id) - async def async_get_or_create_user(self, credentials, auth_provider): - """Get or create a new user for given credentials. - - If link_user is passed in, the credentials will be linked to the passed - in user if the credentials are new. - """ + async def async_create_user(self, name, is_owner=None, is_active=None, + system_generated=None, credentials=None): + """Create a new user.""" if self._users is None: await self.async_load() - # New credentials, store in user - if credentials.is_new: - info = await auth_provider.async_user_meta_for_credentials( - credentials) - # Make owner and activate user if it's the first user. - if self._users: - is_owner = False - is_active = False - else: - is_owner = True - is_active = True - - new_user = User( - is_owner=is_owner, - is_active=is_active, - name=info.get('name'), - ) - self._users[new_user.id] = new_user - await self.async_link_user(new_user, credentials) - return new_user + kwargs = { + 'name': name + } - for user in self._users.values(): - for creds in user.credentials: - if (creds.auth_provider_type == credentials.auth_provider_type - and creds.auth_provider_id == - credentials.auth_provider_id): - return user + if is_owner is not None: + kwargs['is_owner'] = is_owner + + if is_active is not None: + kwargs['is_active'] = is_active + + if system_generated is not None: + kwargs['system_generated'] = system_generated + + new_user = User(**kwargs) - raise ValueError('We got credentials with ID but found no user') + self._users[new_user.id] = new_user + + if credentials is None: + await self.async_save() + return new_user + + # Saving is done inside the link. + await self.async_link_user(new_user, credentials) + return new_user async def async_link_user(self, user, credentials): """Add credentials to an existing user.""" @@ -482,17 +515,10 @@ async def async_remove_user(self, user): self._users.pop(user.id) await self.async_save() - async def async_create_refresh_token(self, user, client_id): + async def async_create_refresh_token(self, user, client=None): """Create a new token for a user.""" - local_user = await self.async_get_user(user.id) - if local_user is None: - raise ValueError('Invalid user') - - local_client = await self.async_get_client(client_id) - if local_client is None: - raise ValueError('Invalid client_id') - - refresh_token = RefreshToken(user, client_id) + client_id = client.id if client is not None else None + refresh_token = RefreshToken(user=user, client_id=client_id) user.refresh_tokens[refresh_token.token] = refresh_token await self.async_save() return refresh_token @@ -607,6 +633,7 @@ async def async_save(self): 'is_owner': user.is_owner, 'is_active': user.is_active, 'name': user.name, + 'system_generated': user.system_generated, } for user in self._users.values() ] diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 0f7295a41e0938..511999c52abaff 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -236,18 +236,16 @@ async def post(self, request, client): grant_type = data.get('grant_type') if grant_type == 'authorization_code': - return await self._async_handle_auth_code( - hass, client.id, data) + return await self._async_handle_auth_code(hass, client, data) elif grant_type == 'refresh_token': - return await self._async_handle_refresh_token( - hass, client.id, data) + return await self._async_handle_refresh_token(hass, client, data) return self.json({ 'error': 'unsupported_grant_type', }, status_code=400) - async def _async_handle_auth_code(self, hass, client_id, data): + async def _async_handle_auth_code(self, hass, client, data): """Handle authorization code request.""" code = data.get('code') @@ -256,7 +254,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): 'error': 'invalid_request', }, status_code=400) - credentials = self._retrieve_credentials(client_id, code) + credentials = self._retrieve_credentials(client.id, code) if credentials is None: return self.json({ @@ -265,7 +263,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): user = await hass.auth.async_get_or_create_user(credentials) refresh_token = await hass.auth.async_create_refresh_token(user, - client_id) + client) access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ @@ -276,7 +274,7 @@ async def _async_handle_auth_code(self, hass, client_id, data): int(refresh_token.access_token_expiration.total_seconds()), }) - async def _async_handle_refresh_token(self, hass, client_id, data): + async def _async_handle_refresh_token(self, hass, client, data): """Handle authorization code request.""" token = data.get('refresh_token') @@ -287,7 +285,7 @@ async def _async_handle_refresh_token(self, hass, client_id, data): refresh_token = await hass.auth.async_get_refresh_token(token) - if refresh_token is None or refresh_token.client_id != client_id: + if refresh_token is None or refresh_token.client_id != client.id: return self.json({ 'error': 'invalid_grant', }, status_code=400) diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 3377a60c45b010..cb0bab4afed04f 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -54,7 +54,7 @@ async def test_match_existing_credentials(store, provider): }, is_new=False, ) - store.credentials_for_provider = Mock(return_value=mock_coro([existing])) + provider.async_credentials = Mock(return_value=mock_coro([existing])) credentials = await provider.async_get_or_create_credentials({ 'username': 'user-test', 'password': 'password-test', diff --git a/tests/auth_providers/test_legacy_api_password.py b/tests/auth_providers/test_legacy_api_password.py index 7a8f17894aa1fc..3a186a0454c6db 100644 --- a/tests/auth_providers/test_legacy_api_password.py +++ b/tests/auth_providers/test_legacy_api_password.py @@ -21,6 +21,14 @@ def provider(hass, store): }) +@pytest.fixture +def manager(hass, store, provider): + """Mock manager.""" + return auth.AuthManager(hass, store, { + (provider.type, provider.id): provider + }) + + async def test_create_new_credential(provider): """Test that we create a new credential.""" credentials = await provider.async_get_or_create_credentials({}) @@ -28,13 +36,13 @@ async def test_create_new_credential(provider): assert credentials.is_new is True -async def test_only_one_credentials(store, provider): +async def test_only_one_credentials(manager, provider): """Call create twice will return same credential.""" credentials = await provider.async_get_or_create_credentials({}) - await store.async_get_or_create_user(credentials, provider) + await manager.async_get_or_create_user(credentials) credentials2 = await provider.async_get_or_create_credentials({}) - assert credentials2.data["username"] is legacy_api_password.LEGACY_USER - assert credentials2.id is credentials.id + assert credentials2.data["username"] == legacy_api_password.LEGACY_USER + assert credentials2.id == credentials.id assert credentials2.is_new is False diff --git a/tests/common.py b/tests/common.py index 3a51cd3e059847..ccb8f49ea97923 100644 --- a/tests/common.py +++ b/tests/common.py @@ -312,7 +312,8 @@ class MockUser(auth.User): def __init__(self, id='mock-id', is_owner=True, is_active=True, name='Mock User'): """Initialize mock user.""" - super().__init__(id, is_owner, is_active, name) + super().__init__( + id=id, is_owner=is_owner, is_active=is_active, name=name) def add_to_hass(self, hass): """Test helper to add entry to hass.""" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8a1b934ab76617..00e3ee88d1660b 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -34,5 +34,5 @@ def hass_access_token(hass): no_secret=True, )) refresh_token = hass.loop.run_until_complete( - hass.auth.async_create_refresh_token(user, client.id)) + hass.auth.async_create_refresh_token(user, client)) yield hass.auth.async_create_access_token(refresh_token) diff --git a/tests/test_auth.py b/tests/test_auth.py index 5b545223c15a9c..8096a081679232 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -184,7 +184,7 @@ async def test_saving_loading(hass, hass_storage): client = await manager.async_create_client( 'test', redirect_uris=['https://example.com']) - refresh_token = await manager.async_create_refresh_token(user, client.id) + refresh_token = await manager.async_create_refresh_token(user, client) manager.async_create_access_token(refresh_token) @@ -226,13 +226,8 @@ async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) client = await manager.async_create_client('test') - user = MockUser( - id='mock-user', - is_owner=False, - is_active=False, - name='Paulus', - ).add_to_auth_manager(manager) - refresh_token = await manager.async_create_refresh_token(user, client.id) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, client) assert refresh_token.user.id is user.id assert refresh_token.client_id is client.id @@ -260,23 +255,41 @@ async def test_get_or_create_client(hass): assert client2.id is client1.id -async def test_cannot_create_refresh_token_with_invalide_client_id(hass): - """Test that we cannot create refresh token with invalid client id.""" +async def test_generating_system_user(hass): + """Test that we can add a system user.""" manager = await auth.auth_manager_from_config(hass, []) - user = MockUser( - id='mock-user', - is_owner=False, - is_active=False, - name='Paulus', - ).add_to_auth_manager(manager) + user = await manager.async_create_system_user('Hass.io') + token = await manager.async_create_refresh_token(user) + assert user.system_generated + assert token is not None + assert token.client_id is None + + +async def test_refresh_token_requires_client_for_user(hass): + """Test that we can add a system user.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser().add_to_auth_manager(manager) + assert user.system_generated is False + with pytest.raises(ValueError): - await manager.async_create_refresh_token(user, 'bla') + await manager.async_create_refresh_token(user) + + client = await manager.async_get_or_create_client('Test client') + token = await manager.async_create_refresh_token(user, client) + assert token is not None + assert token.client_id == client.id -async def test_cannot_create_refresh_token_with_invalide_user(hass): - """Test that we cannot create refresh token with invalid client id.""" +async def test_refresh_token_not_requires_client_for_system_user(hass): + """Test that we can add a system user.""" manager = await auth.auth_manager_from_config(hass, []) - client = await manager.async_create_client('test') - user = MockUser(id='invalid-user') + user = await manager.async_create_system_user('Hass.io') + assert user.system_generated is True + client = await manager.async_get_or_create_client('Test client') + with pytest.raises(ValueError): - await manager.async_create_refresh_token(user, client.id) + await manager.async_create_refresh_token(user, client) + + token = await manager.async_create_refresh_token(user) + assert token is not None + assert token.client_id is None From 91d6d0df84c27136556e2ab5b460bfa2c931d0b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Jul 2018 12:11:18 -0400 Subject: [PATCH 166/169] Bump frontend to 20180704.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d74aadd332303a..0b9c8edd4117b5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180703.1'] +REQUIREMENTS = ['home-assistant-frontend==20180704.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0e6e6e0fc21bcf..b1863df1e4658e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.1 +home-assistant-frontend==20180704.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1715f95b5df45c..476b3d96c3d2e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.1 +home-assistant-frontend==20180704.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f65c3940aee624b27b5b60f1ded39367443d1f6f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 4 Jul 2018 17:30:15 -0600 Subject: [PATCH 167/169] Fix exception when parts of Pollen.com can't be reached (#15296) Fix exception when parts of Pollen.com can't be reached --- homeassistant/components/sensor/pollen.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 838358fcfca8f0..c11c83ab40e1c8 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -183,9 +183,12 @@ async def async_update(self): return if self._category: - data = self.pollencom.data[self._category]['Location'] + data = self.pollencom.data[self._category].get('Location') else: - data = self.pollencom.data[self._type]['Location'] + data = self.pollencom.data[self._type].get('Location') + + if not data: + return indices = [p['Index'] for p in data['periods']] average = round(mean(indices), 1) From 0f1bcfd63b81b86cb422f1ed374a49842baa9db0 Mon Sep 17 00:00:00 2001 From: Luke Fritz Date: Fri, 6 Jul 2018 03:26:03 -0500 Subject: [PATCH 168/169] Add additional sensors for Arlo Baby camera (#15074) * Add additional sensors for Arlo Baby camera * Fix linter errors * Fix linter error * Add tests for Arlo sensors * Fix linter errors * Bump pyarlo dependency to 0.1.9 * Remove unnecessary AttributeError except * Fix module reference error in py35 * Fix test * Address PR review concerns * Convert to standalone pytest methods * Fix linter errors * Fix linter errors * Fix linter errors * Fix test * Remove redundant check, fix async test * Fix linter error * Added check for total_cameras sensor, added additional attribute tests * Add missing docstring --- homeassistant/components/arlo.py | 2 +- homeassistant/components/sensor/arlo.py | 59 +++++- requirements_all.txt | 2 +- tests/components/sensor/test_arlo.py | 240 ++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 tests/components/sensor/test_arlo.py diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index fa58c9b0baa897..475e43e55a4023 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.8'] +REQUIREMENTS = ['pyarlo==0.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py index 18029691dc7bae..609887e9690256 100644 --- a/homeassistant/components/sensor/arlo.py +++ b/homeassistant/components/sensor/arlo.py @@ -13,7 +13,10 @@ from homeassistant.components.arlo import ( CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -28,7 +31,10 @@ 'total_cameras': ['Arlo Cameras', None, 'video'], 'captured_today': ['Captured Today', None, 'file-video'], 'battery_level': ['Battery Level', '%', 'battery-50'], - 'signal_strength': ['Signal Strength', None, 'signal'] + 'signal_strength': ['Signal Strength', None, 'signal'], + 'temperature': ['Temperature', TEMP_CELSIUS, 'thermometer'], + 'humidity': ['Humidity', '%', 'water-percent'], + 'air_quality': ['Air Quality', 'ppm', 'biohazard'] } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -41,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Arlo IP sensor.""" arlo = hass.data.get(DATA_ARLO) if not arlo: - return False + return sensors = [] for sensor_type in config.get(CONF_MONITORED_CONDITIONS): @@ -50,10 +56,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): SENSOR_TYPES[sensor_type][0], arlo, sensor_type)) else: for camera in arlo.cameras: + if sensor_type == 'temperature' or \ + sensor_type == 'humidity' or \ + sensor_type == 'air_quality': + continue + name = '{0} {1}'.format( SENSOR_TYPES[sensor_type][0], camera.name) sensors.append(ArloSensor(name, camera, sensor_type)) + for base_station in arlo.base_stations: + if ((sensor_type == 'temperature' or + sensor_type == 'humidity' or + sensor_type == 'air_quality') and + base_station.model_id == 'ABC1000'): + name = '{0} {1}'.format( + SENSOR_TYPES[sensor_type][0], base_station.name) + sensors.append(ArloSensor(name, base_station, sensor_type)) + add_devices(sensors, True) @@ -62,6 +82,7 @@ class ArloSensor(Entity): def __init__(self, name, device, sensor_type): """Initialize an Arlo sensor.""" + _LOGGER.debug('ArloSensor created for %s', name) self._name = name self._data = device self._sensor_type = sensor_type @@ -101,6 +122,15 @@ def unit_of_measurement(self): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[1] + @property + def device_class(self): + """Return the device class of the sensor.""" + if self._sensor_type == 'temperature': + return DEVICE_CLASS_TEMPERATURE + elif self._sensor_type == 'humidity': + return DEVICE_CLASS_HUMIDITY + return None + def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating Arlo sensor %s", self.name) @@ -133,6 +163,24 @@ def update(self): except TypeError: self._state = None + elif self._sensor_type == 'temperature': + try: + self._state = self._data.ambient_temperature + except TypeError: + self._state = None + + elif self._sensor_type == 'humidity': + try: + self._state = self._data.ambient_humidity + except TypeError: + self._state = None + + elif self._sensor_type == 'air_quality': + try: + self._state = self._data.ambient_air_quality + except TypeError: + self._state = None + @property def device_state_attributes(self): """Return the device state attributes.""" @@ -141,10 +189,7 @@ def device_state_attributes(self): attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION attrs['brand'] = DEFAULT_BRAND - if self._sensor_type == 'last_capture' or \ - self._sensor_type == 'captured_today' or \ - self._sensor_type == 'battery_level' or \ - self._sensor_type == 'signal_strength': + if self._sensor_type != 'total_cameras': attrs['model'] = self._data.model_id return attrs diff --git a/requirements_all.txt b/requirements_all.txt index b1863df1e4658e..1491e1dbff4122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -730,7 +730,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.8 +pyarlo==0.1.9 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 diff --git a/tests/components/sensor/test_arlo.py b/tests/components/sensor/test_arlo.py new file mode 100644 index 00000000000000..d31490ab2afa10 --- /dev/null +++ b/tests/components/sensor/test_arlo.py @@ -0,0 +1,240 @@ +"""The tests for the Netgear Arlo sensors.""" +from collections import namedtuple +from unittest.mock import patch, MagicMock +import pytest +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, ATTR_ATTRIBUTION) +from homeassistant.components.sensor import arlo +from homeassistant.components.arlo import DATA_ARLO + + +def _get_named_tuple(input_dict): + return namedtuple('Struct', input_dict.keys())(*input_dict.values()) + + +def _get_sensor(name='Last', sensor_type='last_capture', data=None): + if data is None: + data = {} + return arlo.ArloSensor(name, data, sensor_type) + + +@pytest.fixture() +def default_sensor(): + """Create an ArloSensor with default values.""" + return _get_sensor() + + +@pytest.fixture() +def battery_sensor(): + """Create an ArloSensor with battery data.""" + data = _get_named_tuple({ + 'battery_level': 50 + }) + return _get_sensor('Battery Level', 'battery_level', data) + + +@pytest.fixture() +def temperature_sensor(): + """Create a temperature ArloSensor.""" + return _get_sensor('Temperature', 'temperature') + + +@pytest.fixture() +def humidity_sensor(): + """Create a humidity ArloSensor.""" + return _get_sensor('Humidity', 'humidity') + + +@pytest.fixture() +def cameras_sensor(): + """Create a total cameras ArloSensor.""" + data = _get_named_tuple({ + 'cameras': [0, 0] + }) + return _get_sensor('Arlo Cameras', 'total_cameras', data) + + +@pytest.fixture() +def captured_sensor(): + """Create a captured today ArloSensor.""" + data = _get_named_tuple({ + 'captured_today': [0, 0, 0, 0, 0] + }) + return _get_sensor('Captured Today', 'captured_today', data) + + +class PlatformSetupFixture(): + """Fixture for testing platform setup call to add_devices().""" + + def __init__(self): + """Instantiate the platform setup fixture.""" + self.sensors = None + self.update = False + + def add_devices(self, sensors, update): + """Mock method for adding devices.""" + self.sensors = sensors + self.update = update + + +@pytest.fixture() +def platform_setup(): + """Create an instance of the PlatformSetupFixture class.""" + return PlatformSetupFixture() + + +@pytest.fixture() +def sensor_with_hass_data(default_sensor, hass): + """Create a sensor with async_dispatcher_connected mocked.""" + hass.data = {} + default_sensor.hass = hass + return default_sensor + + +@pytest.fixture() +def mock_dispatch(): + """Mock the dispatcher connect method.""" + target = 'homeassistant.components.sensor.arlo.async_dispatcher_connect' + with patch(target, MagicMock()) as _mock: + yield _mock + + +def test_setup_with_no_data(platform_setup, hass): + """Test setup_platform with no data.""" + arlo.setup_platform(hass, None, platform_setup.add_devices) + assert platform_setup.sensors is None + assert not platform_setup.update + + +def test_setup_with_valid_data(platform_setup, hass): + """Test setup_platform with valid data.""" + config = { + 'monitored_conditions': [ + 'last_capture', + 'total_cameras', + 'captured_today', + 'battery_level', + 'signal_strength', + 'temperature', + 'humidity', + 'air_quality' + ] + } + + hass.data[DATA_ARLO] = _get_named_tuple({ + 'cameras': [_get_named_tuple({ + 'name': 'Camera', + 'model_id': 'ABC1000' + })], + 'base_stations': [_get_named_tuple({ + 'name': 'Base Station', + 'model_id': 'ABC1000' + })] + }) + + arlo.setup_platform(hass, config, platform_setup.add_devices) + assert len(platform_setup.sensors) == 8 + assert platform_setup.update + + +def test_sensor_name(default_sensor): + """Test the name property.""" + assert default_sensor.name == 'Last' + + +async def test_async_added_to_hass(sensor_with_hass_data, mock_dispatch): + """Test dispatcher called when added.""" + await sensor_with_hass_data.async_added_to_hass() + assert len(mock_dispatch.mock_calls) == 1 + kall = mock_dispatch.call_args + args, kwargs = kall + assert len(args) == 3 + assert args[0] == sensor_with_hass_data.hass + assert args[1] == 'arlo_update' + assert not kwargs + + +def test_sensor_state_default(default_sensor): + """Test the state property.""" + assert default_sensor.state is None + + +def test_sensor_icon_battery(battery_sensor): + """Test the battery icon.""" + assert battery_sensor.icon == 'mdi:battery-50' + + +def test_sensor_icon(temperature_sensor): + """Test the icon property.""" + assert temperature_sensor.icon == 'mdi:thermometer' + + +def test_unit_of_measure(default_sensor, battery_sensor): + """Test the unit_of_measurement property.""" + assert default_sensor.unit_of_measurement is None + assert battery_sensor.unit_of_measurement == '%' + + +def test_device_class(default_sensor, temperature_sensor, humidity_sensor): + """Test the device_class property.""" + assert default_sensor.device_class is None + assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE + assert humidity_sensor.device_class == DEVICE_CLASS_HUMIDITY + + +def test_update_total_cameras(cameras_sensor): + """Test update method for total_cameras sensor type.""" + cameras_sensor.update() + assert cameras_sensor.state == 2 + + +def test_update_captured_today(captured_sensor): + """Test update method for captured_today sensor type.""" + captured_sensor.update() + assert captured_sensor.state == 5 + + +def _test_attributes(sensor_type): + data = _get_named_tuple({ + 'model_id': 'TEST123' + }) + sensor = _get_sensor('test', sensor_type, data) + attrs = sensor.device_state_attributes + assert attrs.get(ATTR_ATTRIBUTION) == 'Data provided by arlo.netgear.com' + assert attrs.get('brand') == 'Netgear Arlo' + assert attrs.get('model') == 'TEST123' + + +def test_state_attributes(): + """Test attributes for camera sensor types.""" + _test_attributes('battery_level') + _test_attributes('signal_strength') + _test_attributes('temperature') + _test_attributes('humidity') + _test_attributes('air_quality') + + +def test_attributes_total_cameras(cameras_sensor): + """Test attributes for total cameras sensor type.""" + attrs = cameras_sensor.device_state_attributes + assert attrs.get(ATTR_ATTRIBUTION) == 'Data provided by arlo.netgear.com' + assert attrs.get('brand') == 'Netgear Arlo' + assert attrs.get('model') is None + + +def _test_update(sensor_type, key, value): + data = _get_named_tuple({ + key: value + }) + sensor = _get_sensor('test', sensor_type, data) + sensor.update() + assert sensor.state == value + + +def test_update(): + """Test update method for direct transcription sensor types.""" + _test_update('battery_level', 'battery_level', 100) + _test_update('signal_strength', 'signal_strength', 100) + _test_update('temperature', 'ambient_temperature', 21.4) + _test_update('humidity', 'ambient_humidity', 45.1) + _test_update('air_quality', 'ambient_air_quality', 14.2) From 99709657187b4638c736b478598fff22eb3d6ffa Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Fri, 6 Jul 2018 23:05:34 +0200 Subject: [PATCH 169/169] Add HomematicIP Cloud Config Flow and Entries loading (#14861) * Add HomematicIP Cloud to config flow * Inititial trial for config_flow * Integrations text files * Load and write config_flow and init homematicip_cloud * Split into dedicated files * Ceanup of text messages * Working config_flow * Move imports inside a function * Enable laoding even no accesspoints are defined * Revert unnecassary changes in CONFIG_SCHEMA * Better error handling * fix flask8 * Migration to async for token generation * A few fixes * Simplify config_flow * Bump version to 9.6 with renamed package * Requirements file * First fixes after review * Implement async_step_import * Cleanup for Config Flow * First tests for homematicip_cloud setup * Remove config_flow tests * Really remove all things * Fix comment * Update picture * Add support for async_setup_entry to switch and climate platform * Update path of the config_flow picture * Refactoring for better tesability * Further tests implemented * Move 3th party lib inside function * Fix lint * Update requirments_test_all.txt file * UPdate of requirments_test_all.txt did not work * Furder cleanup in websocket connection * Remove a test for the hap * Revert "Remove a test for the hap" This reverts commit 968d58cba108e0f371022c7ab540374aa2ab13f4. * First tests implemented for config_flow * Fix lint * Rework of client registration process * Implemented tests for config_flow 100% coverage * Cleanup * Cleanup comments and code * Try to fix import problem * Add homematicip to the test env requirements --- .../binary_sensor/homematicip_cloud.py | 15 +- .../components/climate/homematicip_cloud.py | 14 +- homeassistant/components/homematicip_cloud.py | 262 ------------------ .../homematicip_cloud/.translations/en.json | 30 ++ .../components/homematicip_cloud/__init__.py | 65 +++++ .../homematicip_cloud/config_flow.py | 97 +++++++ .../components/homematicip_cloud/const.py | 23 ++ .../components/homematicip_cloud/device.py | 71 +++++ .../components/homematicip_cloud/errors.py | 22 ++ .../components/homematicip_cloud/hap.py | 256 +++++++++++++++++ .../components/homematicip_cloud/strings.json | 30 ++ .../components/light/homematicip_cloud.py | 15 +- .../components/sensor/homematicip_cloud.py | 14 +- homeassistant/components/switch/__init__.py | 12 +- .../components/switch/homematicip_cloud.py | 13 +- homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + .../components/homematicip_cloud/__init__.py | 1 + .../homematicip_cloud/test_config_flow.py | 150 ++++++++++ .../components/homematicip_cloud/test_hap.py | 113 ++++++++ .../components/homematicip_cloud/test_init.py | 103 +++++++ 23 files changed, 1020 insertions(+), 293 deletions(-) delete mode 100644 homeassistant/components/homematicip_cloud.py create mode 100644 homeassistant/components/homematicip_cloud/.translations/en.json create mode 100644 homeassistant/components/homematicip_cloud/__init__.py create mode 100644 homeassistant/components/homematicip_cloud/config_flow.py create mode 100644 homeassistant/components/homematicip_cloud/const.py create mode 100644 homeassistant/components/homematicip_cloud/device.py create mode 100644 homeassistant/components/homematicip_cloud/errors.py create mode 100644 homeassistant/components/homematicip_cloud/hap.py create mode 100644 homeassistant/components/homematicip_cloud/strings.json create mode 100644 tests/components/homematicip_cloud/__init__.py create mode 100644 tests/components/homematicip_cloud/test_config_flow.py create mode 100644 tests/components/homematicip_cloud/test_hap.py create mode 100644 tests/components/homematicip_cloud/test_init.py diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index 40ffe4984020c9..72a7db1ac7a8ec 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -26,12 +26,15 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the HomematicIP binary sensor devices.""" + """Set up the binary sensor devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP binary sensor from a config entry.""" from homematicip.device import (ShutterContact, MotionDetectorIndoor) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, ShutterContact): diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/climate/homematicip_cloud.py index bf96f1f746d8c2..8cf47159c103fd 100644 --- a/homeassistant/components/climate/homematicip_cloud.py +++ b/homeassistant/components/climate/homematicip_cloud.py @@ -12,8 +12,8 @@ STATE_AUTO, STATE_MANUAL) from homeassistant.const import TEMP_CELSIUS from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) _LOGGER = logging.getLogger(__name__) @@ -30,12 +30,14 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP climate devices.""" - from homematicip.group import HeatingGroup + pass + - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP climate from a config entry.""" + from homematicip.group import HeatingGroup + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.groups: if isinstance(device, HeatingGroup): diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py deleted file mode 100644 index 859841dfca64bc..00000000000000 --- a/homeassistant/components/homematicip_cloud.py +++ /dev/null @@ -1,262 +0,0 @@ -""" -Support for HomematicIP components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip_cloud/ -""" - -import asyncio -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.entity import Entity -from homeassistant.core import callback - -REQUIREMENTS = ['homematicip==0.9.4'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'homematicip_cloud' - -COMPONENTS = [ - 'sensor', - 'binary_sensor', - 'switch', - 'light', - 'climate', -] - -CONF_NAME = 'name' -CONF_ACCESSPOINT = 'accesspoint' -CONF_AUTHTOKEN = 'authtoken' - -CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ - vol.Optional(CONF_NAME): vol.Any(cv.string), - vol.Required(CONF_ACCESSPOINT): cv.string, - vol.Required(CONF_AUTHTOKEN): cv.string, - })]), -}, extra=vol.ALLOW_EXTRA) - -HMIP_ACCESS_POINT = 'Access Point' -HMIP_HUB = 'HmIP-HUB' - -ATTR_HOME_ID = 'home_id' -ATTR_HOME_NAME = 'home_name' -ATTR_DEVICE_ID = 'device_id' -ATTR_DEVICE_LABEL = 'device_label' -ATTR_STATUS_UPDATE = 'status_update' -ATTR_FIRMWARE_STATE = 'firmware_state' -ATTR_UNREACHABLE = 'unreachable' -ATTR_LOW_BATTERY = 'low_battery' -ATTR_MODEL_TYPE = 'model_type' -ATTR_GROUP_TYPE = 'group_type' -ATTR_DEVICE_RSSI = 'device_rssi' -ATTR_DUTY_CYCLE = 'duty_cycle' -ATTR_CONNECTED = 'connected' -ATTR_SABOTAGE = 'sabotage' -ATTR_OPERATION_LOCK = 'operation_lock' - - -async def async_setup(hass, config): - """Set up the HomematicIP component.""" - from homematicip.base.base_connection import HmipConnectionError - - hass.data.setdefault(DOMAIN, {}) - accesspoints = config.get(DOMAIN, []) - for conf in accesspoints: - _websession = async_get_clientsession(hass) - _hmip = HomematicipConnector(hass, conf, _websession) - try: - await _hmip.init() - except HmipConnectionError: - _LOGGER.error('Failed to connect to the HomematicIP server, %s.', - conf.get(CONF_ACCESSPOINT)) - return False - - home = _hmip.home - home.name = conf.get(CONF_NAME) - home.label = HMIP_ACCESS_POINT - home.modelType = HMIP_HUB - - hass.data[DOMAIN][home.id] = home - _LOGGER.info('Connected to the HomematicIP server, %s.', - conf.get(CONF_ACCESSPOINT)) - homeid = {ATTR_HOME_ID: home.id} - for component in COMPONENTS: - hass.async_add_job(async_load_platform(hass, component, DOMAIN, - homeid, config)) - - hass.loop.create_task(_hmip.connect()) - return True - - -class HomematicipConnector: - """Manages HomematicIP http and websocket connection.""" - - def __init__(self, hass, config, websession): - """Initialize HomematicIP cloud connection.""" - from homematicip.async.home import AsyncHome - - self._hass = hass - self._ws_close_requested = False - self._retry_task = None - self._tries = 0 - self._accesspoint = config.get(CONF_ACCESSPOINT) - _authtoken = config.get(CONF_AUTHTOKEN) - - self.home = AsyncHome(hass.loop, websession) - self.home.set_auth_token(_authtoken) - - self.home.on_update(self.async_update) - self._accesspoint_connected = True - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) - - async def init(self): - """Initialize connection.""" - await self.home.init(self._accesspoint) - await self.home.get_current_state() - - @callback - def async_update(self, *args, **kwargs): - """Async update the home device. - - Triggered when the hmip HOME_CHANGED event has fired. - There are several occasions for this event to happen. - We are only interested to check whether the access point - is still connected. If not, device state changes cannot - be forwarded to hass. So if access point is disconnected all devices - are set to unavailable. - """ - if not self.home.connected: - _LOGGER.error( - "HMIP access point has lost connection with the cloud") - self._accesspoint_connected = False - self.set_all_to_unavailable() - elif not self._accesspoint_connected: - # Explicitly getting an update as device states might have - # changed during access point disconnect.""" - - job = self._hass.async_add_job(self.get_state()) - job.add_done_callback(self.get_state_finished) - - async def get_state(self): - """Update hmip state and tell hass.""" - await self.home.get_current_state() - self.update_all() - - def get_state_finished(self, future): - """Execute when get_state coroutine has finished.""" - from homematicip.base.base_connection import HmipConnectionError - - try: - future.result() - except HmipConnectionError: - # Somehow connection could not recover. Will disconnect and - # so reconnect loop is taking over. - _LOGGER.error( - "updating state after himp access point reconnect failed.") - self._hass.async_add_job(self.home.disable_events()) - - def set_all_to_unavailable(self): - """Set all devices to unavailable and tell Hass.""" - for device in self.home.devices: - device.unreach = True - self.update_all() - - def update_all(self): - """Signal all devices to update their state.""" - for device in self.home.devices: - device.fire_update_event() - - async def _handle_connection(self): - """Handle websocket connection.""" - from homematicip.base.base_connection import HmipConnectionError - - await self.home.get_current_state() - hmip_events = await self.home.enable_events() - try: - await hmip_events - except HmipConnectionError: - return - - async def connect(self): - """Start websocket connection.""" - self._tries = 0 - while True: - await self._handle_connection() - if self._ws_close_requested: - break - self._ws_close_requested = False - self._tries += 1 - try: - self._retry_task = self._hass.async_add_job(asyncio.sleep( - 2 ** min(9, self._tries), loop=self._hass.loop)) - await self._retry_task - except asyncio.CancelledError: - break - _LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.', - self._tries) - - async def close(self): - """Close the websocket connection.""" - self._ws_close_requested = True - if self._retry_task is not None: - self._retry_task.cancel() - await self.home.disable_events() - _LOGGER.info("Closed connection to HomematicIP cloud server.") - - -class HomematicipGenericDevice(Entity): - """Representation of an HomematicIP generic device.""" - - def __init__(self, home, device, post=None): - """Initialize the generic device.""" - self._home = home - self._device = device - self.post = post - _LOGGER.info('Setting up %s (%s)', self.name, - self._device.modelType) - - async def async_added_to_hass(self): - """Register callbacks.""" - self._device.on_update(self._device_changed) - - def _device_changed(self, json, **kwargs): - """Handle device state changes.""" - _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the generic device.""" - name = self._device.label - if self._home.name is not None: - name = "{} {}".format(self._home.name, name) - if self.post is not None: - name = "{} {}".format(name, self.post) - return name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def available(self): - """Device available.""" - return not self._device.unreach - - @property - def device_state_attributes(self): - """Return the state attributes of the generic device.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_MODEL_TYPE: self._device.modelType - } diff --git a/homeassistant/components/homematicip_cloud/.translations/en.json b/homeassistant/components/homematicip_cloud/.translations/en.json new file mode 100644 index 00000000000000..887a3a5780b0eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "title": "HomematicIP Cloud", + "step": { + "init": { + "title": "Pick HomematicIP Accesspoint", + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "pin": "Pin Code (optional)", + "name": "Name (optional, used as name prefix for all devices)" + } + }, + "link": { + "title": "Link Accesspoint", + "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)" + } + }, + "error": { + "register_failed": "Failed to register, please try again.", + "invalid_pin": "Invalid PIN, please try again.", + "press_the_button": "Please press the blue button.", + "timeout_button": "Blue button press timeout, please try again." + }, + "abort": { + "unknown": "Unknown error occurred.", + "conection_aborted": "Could not connect to HMIP server", + "already_configured": "Accesspoint is already configured" + } + } +} diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py new file mode 100644 index 00000000000000..3ff4e438f53c7f --- /dev/null +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -0,0 +1,65 @@ +""" +Support for HomematicIP components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip_cloud/ +""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from .const import ( + DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME, + CONF_ACCESSPOINT, CONF_AUTHTOKEN, CONF_NAME) +# Loading the config flow file will register the flow +from .config_flow import configured_haps +from .hap import HomematicipHAP, HomematicipAuth # noqa: F401 +from .device import HomematicipGenericDevice # noqa: F401 + +REQUIREMENTS = ['homematicip==0.9.6'] + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME, default=''): vol.Any(cv.string), + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + })]), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the HomematicIP component.""" + hass.data[DOMAIN] = {} + + accesspoints = config.get(DOMAIN, []) + + for conf in accesspoints: + if conf[CONF_ACCESSPOINT] not in configured_haps(hass): + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + HMIPC_HAPID: conf[CONF_ACCESSPOINT], + HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN], + HMIPC_NAME: conf[CONF_NAME], + } + )) + + return True + + +async def async_setup_entry(hass, entry): + """Set up an accsspoint from a config entry.""" + hap = HomematicipHAP(hass, entry) + hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() + hass.data[DOMAIN][hapid] = hap + return await hap.async_setup() + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) + return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py new file mode 100644 index 00000000000000..9e5356d914a5a6 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -0,0 +1,97 @@ +"""Config flow to configure HomematicIP Cloud.""" +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback + +from .const import ( + DOMAIN as HMIPC_DOMAIN, _LOGGER, + HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME) +from .hap import HomematicipAuth + + +@callback +def configured_haps(hass): + """Return a set of the configured accesspoints.""" + return set(entry.data[HMIPC_HAPID] for entry + in hass.config_entries.async_entries(HMIPC_DOMAIN)) + + +@config_entries.HANDLERS.register(HMIPC_DOMAIN) +class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler): + """Config flow HomematicIP Cloud.""" + + VERSION = 1 + + def __init__(self): + """Initialize HomematicIP Cloud config flow.""" + self.auth = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + user_input[HMIPC_HAPID] = \ + user_input[HMIPC_HAPID].replace('-', '').upper() + if user_input[HMIPC_HAPID] in configured_haps(self.hass): + return self.async_abort(reason='already_configured') + + self.auth = HomematicipAuth(self.hass, user_input) + connected = await self.auth.async_setup() + if connected: + _LOGGER.info("Connection established") + return await self.async_step_link() + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(HMIPC_HAPID): str, + vol.Optional(HMIPC_PIN): str, + vol.Optional(HMIPC_NAME): str, + }), + errors=errors + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the HomematicIP Cloud accesspoint.""" + errors = {} + + pressed = await self.auth.async_checkbutton() + if pressed: + authtoken = await self.auth.async_register() + if authtoken: + _LOGGER.info("Write config entry") + return self.async_create_entry( + title=self.auth.config.get(HMIPC_HAPID), + data={ + HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID), + HMIPC_AUTHTOKEN: authtoken, + HMIPC_NAME: self.auth.config.get(HMIPC_NAME) + }) + return self.async_abort(reason='conection_aborted') + else: + errors['base'] = 'press_the_button' + + return self.async_show_form(step_id='link', errors=errors) + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry.""" + hapid = import_info[HMIPC_HAPID] + authtoken = import_info[HMIPC_AUTHTOKEN] + name = import_info[HMIPC_NAME] + + hapid = hapid.replace('-', '').upper() + if hapid in configured_haps(self.hass): + return self.async_abort(reason='already_configured') + + _LOGGER.info('Imported authentication for %s', hapid) + + return self.async_create_entry( + title=hapid, + data={ + HMIPC_HAPID: hapid, + HMIPC_AUTHTOKEN: authtoken, + HMIPC_NAME: name + } + ) diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py new file mode 100644 index 00000000000000..c40e577ae4a570 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/const.py @@ -0,0 +1,23 @@ +"""Constants for the HomematicIP Cloud component.""" +import logging + +_LOGGER = logging.getLogger('homeassistant.components.homematicip_cloud') + +DOMAIN = 'homematicip_cloud' + +COMPONENTS = [ + 'binary_sensor', + 'climate', + 'light', + 'sensor', + 'switch', +] + +CONF_NAME = 'name' +CONF_ACCESSPOINT = 'accesspoint' +CONF_AUTHTOKEN = 'authtoken' + +HMIPC_NAME = 'name' +HMIPC_HAPID = 'hapid' +HMIPC_AUTHTOKEN = 'authtoken' +HMIPC_PIN = 'pin' diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py new file mode 100644 index 00000000000000..94fe5f40be8db5 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/device.py @@ -0,0 +1,71 @@ +"""GenericDevice for the HomematicIP Cloud component.""" +import logging + +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +ATTR_HOME_ID = 'home_id' +ATTR_HOME_NAME = 'home_name' +ATTR_DEVICE_ID = 'device_id' +ATTR_DEVICE_LABEL = 'device_label' +ATTR_STATUS_UPDATE = 'status_update' +ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_UNREACHABLE = 'unreachable' +ATTR_LOW_BATTERY = 'low_battery' +ATTR_MODEL_TYPE = 'model_type' +ATTR_GROUP_TYPE = 'group_type' +ATTR_DEVICE_RSSI = 'device_rssi' +ATTR_DUTY_CYCLE = 'duty_cycle' +ATTR_CONNECTED = 'connected' +ATTR_SABOTAGE = 'sabotage' +ATTR_OPERATION_LOCK = 'operation_lock' + + +class HomematicipGenericDevice(Entity): + """Representation of an HomematicIP generic device.""" + + def __init__(self, home, device, post=None): + """Initialize the generic device.""" + self._home = home + self._device = device + self.post = post + _LOGGER.info('Setting up %s (%s)', self.name, + self._device.modelType) + + async def async_added_to_hass(self): + """Register callbacks.""" + self._device.on_update(self._device_changed) + + def _device_changed(self, json, **kwargs): + """Handle device state changes.""" + _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the name of the generic device.""" + name = self._device.label + if (self._home.name is not None and self._home.name != ''): + name = "{} {}".format(self._home.name, name) + if (self.post is not None and self.post != ''): + name = "{} {}".format(name, self.post) + return name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Device available.""" + return not self._device.unreach + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_MODEL_TYPE: self._device.modelType + } diff --git a/homeassistant/components/homematicip_cloud/errors.py b/homeassistant/components/homematicip_cloud/errors.py new file mode 100644 index 00000000000000..cb2925d1a70450 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/errors.py @@ -0,0 +1,22 @@ +"""Errors for the HomematicIP component.""" +from homeassistant.exceptions import HomeAssistantError + + +class HmipcException(HomeAssistantError): + """Base class for HomematicIP exceptions.""" + + +class HmipcConnectionError(HmipcException): + """Unable to connect to the HomematicIP cloud server.""" + + +class HmipcConnectionWait(HmipcException): + """Wait for registration to the HomematicIP cloud server.""" + + +class HmipcRegistrationFailed(HmipcException): + """Registration on HomematicIP cloud failed.""" + + +class HmipcPressButton(HmipcException): + """User needs to press the blue button.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py new file mode 100644 index 00000000000000..a4e3e78e860805 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -0,0 +1,256 @@ +"""Accesspoint for the HomematicIP Cloud component.""" +import asyncio +import logging + +from homeassistant import config_entries +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import callback + +from .const import ( + HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME, + COMPONENTS) +from .errors import HmipcConnectionError + +_LOGGER = logging.getLogger(__name__) + + +class HomematicipAuth(object): + """Manages HomematicIP client registration.""" + + def __init__(self, hass, config): + """Initialize HomematicIP Cloud client registration.""" + self.hass = hass + self.config = config + self.auth = None + + async def async_setup(self): + """Connect to HomematicIP for registration.""" + try: + self.auth = await self.get_auth( + self.hass, + self.config.get(HMIPC_HAPID), + self.config.get(HMIPC_PIN) + ) + return True + except HmipcConnectionError: + return False + + async def async_checkbutton(self): + """Check blue butten has been pressed.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + await self.auth.isRequestAcknowledged() + return True + except HmipConnectionError: + return False + + async def async_register(self): + """Register client at HomematicIP.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + authtoken = await self.auth.requestAuthToken() + await self.auth.confirmAuthToken(authtoken) + return authtoken + except HmipConnectionError: + return False + + async def get_auth(self, hass, hapid, pin): + """Create a HomematicIP access point object.""" + from homematicip.aio.auth import AsyncAuth + from homematicip.base.base_connection import HmipConnectionError + + auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) + print(auth) + try: + await auth.init(hapid) + if pin: + auth.pin = pin + await auth.connectionRequest('HomeAssistant') + except HmipConnectionError: + return False + return auth + + +class HomematicipHAP(object): + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config_entry): + """Initialize HomematicIP cloud connection.""" + self.hass = hass + self.config_entry = config_entry + self.home = None + + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint_connected = True + self._retry_setup = None + + async def async_setup(self, tries=0): + """Initialize connection.""" + try: + self.home = await self.get_hap( + self.hass, + self.config_entry.data.get(HMIPC_HAPID), + self.config_entry.data.get(HMIPC_AUTHTOKEN), + self.config_entry.data.get(HMIPC_NAME) + ) + except HmipcConnectionError: + retry_delay = 2 ** min(tries + 1, 6) + _LOGGER.error("Error connecting to HomematicIP with HAP %s. " + "Retrying in %d seconds.", + self.config_entry.data.get(HMIPC_HAPID), retry_delay) + + async def retry_setup(_now): + """Retry setup.""" + if await self.async_setup(tries + 1): + self.config_entry.state = config_entries.ENTRY_STATE_LOADED + + self._retry_setup = self.hass.helpers.event.async_call_later( + retry_delay, retry_setup) + + return False + + _LOGGER.info('Connected to HomematicIP with HAP %s.', + self.config_entry.data.get(HMIPC_HAPID)) + + for component in COMPONENTS: + self.hass.async_add_job( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, component) + ) + return True + + @callback + def async_update(self, *args, **kwargs): + """Async update the home device. + + Triggered when the hmip HOME_CHANGED event has fired. + There are several occasions for this event to happen. + We are only interested to check whether the access point + is still connected. If not, device state changes cannot + be forwarded to hass. So if access point is disconnected all devices + are set to unavailable. + """ + if not self.home.connected: + _LOGGER.error( + "HMIP access point has lost connection with the cloud") + self._accesspoint_connected = False + self.set_all_to_unavailable() + elif not self._accesspoint_connected: + # Explicitly getting an update as device states might have + # changed during access point disconnect.""" + + job = self.hass.async_add_job(self.get_state()) + job.add_done_callback(self.get_state_finished) + + async def get_state(self): + """Update hmip state and tell hass.""" + await self.home.get_current_state() + self.update_all() + + def get_state_finished(self, future): + """Execute when get_state coroutine has finished.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + future.result() + except HmipConnectionError: + # Somehow connection could not recover. Will disconnect and + # so reconnect loop is taking over. + _LOGGER.error( + "updating state after himp access point reconnect failed.") + self.hass.async_add_job(self.home.disable_events()) + + def set_all_to_unavailable(self): + """Set all devices to unavailable and tell Hass.""" + for device in self.home.devices: + device.unreach = True + self.update_all() + + def update_all(self): + """Signal all devices to update their state.""" + for device in self.home.devices: + device.fire_update_event() + + async def _handle_connection(self): + """Handle websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + await self.home.get_current_state() + except HmipConnectionError: + return + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def async_connect(self): + """Start websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + tries = 0 + while True: + try: + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + tries = 0 + await hmip_events + except HmipConnectionError: + pass + + if self._ws_close_requested: + break + self._ws_close_requested = False + + tries += 1 + retry_delay = 2 ** min(tries + 1, 6) + _LOGGER.error("Error connecting to HomematicIP with HAP %s. " + "Retrying in %d seconds.", + self.config_entry.data.get(HMIPC_HAPID), retry_delay) + try: + self._retry_task = self.hass.async_add_job(asyncio.sleep( + retry_delay, loop=self.hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + + async def async_reset(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_setup is not None: + self._retry_setup.cancel() + if self._retry_task is not None: + self._retry_task.cancel() + self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + for component in COMPONENTS: + await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, component) + return True + + async def get_hap(self, hass, hapid, authtoken, name): + """Create a HomematicIP access point object.""" + from homematicip.aio.home import AsyncHome + from homematicip.base.base_connection import HmipConnectionError + + home = AsyncHome(hass.loop, async_get_clientsession(hass)) + + home.name = name + home.label = 'Access Point' + home.modelType = 'HmIP-HAP' + + home.set_auth_token(authtoken) + try: + await home.init(hapid) + await home.get_current_state() + except HmipConnectionError: + raise HmipcConnectionError + home.on_update(self.async_update) + hass.loop.create_task(self.async_connect()) + + return home diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json new file mode 100644 index 00000000000000..887a3a5780b0eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "title": "HomematicIP Cloud", + "step": { + "init": { + "title": "Pick HomematicIP Accesspoint", + "data": { + "hapid": "Accesspoint ID (SGTIN)", + "pin": "Pin Code (optional)", + "name": "Name (optional, used as name prefix for all devices)" + } + }, + "link": { + "title": "Link Accesspoint", + "description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)" + } + }, + "error": { + "register_failed": "Failed to register, please try again.", + "invalid_pin": "Invalid PIN, please try again.", + "press_the_button": "Please press the blue button.", + "timeout_button": "Blue button press timeout, please try again." + }, + "abort": { + "unknown": "Unknown error occurred.", + "conection_aborted": "Could not connect to HMIP server", + "already_configured": "Accesspoint is already configured" + } + } +} diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py index e433da44ae768b..5984fb0365792e 100644 --- a/homeassistant/components/light/homematicip_cloud.py +++ b/homeassistant/components/light/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.light import Light from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -23,13 +23,16 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the HomematicIP light devices.""" + """Old way of setting up HomematicIP lights.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP lights from a config entry.""" from homematicip.device import ( BrandSwitchMeasuring) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, BrandSwitchMeasuring): diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index ccd1949cc3b2bd..0596bc0b6ccd3e 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -8,8 +8,8 @@ import logging from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) from homeassistant.const import ( TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE) @@ -36,15 +36,17 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP sensors devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP sensors from a config entry.""" from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay, MotionDetectorIndoor) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] - for device in home.devices: if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index bab2abbad0d83c..b9ee8126ed3c29 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -95,7 +95,7 @@ def toggle(hass, entity_id=None): async def async_setup(hass, config): """Track states and offer events for switches.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES) await component.async_setup(config) @@ -132,6 +132,16 @@ async def async_handle_switch_service(service): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class SwitchDevice(ToggleEntity): """Representation of a switch.""" diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/switch/homematicip_cloud.py index 9123d46c87b2ef..68884aaaa02963 100644 --- a/homeassistant/components/switch/homematicip_cloud.py +++ b/homeassistant/components/switch/homematicip_cloud.py @@ -9,8 +9,8 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, - ATTR_HOME_ID) + HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, + HMIPC_HAPID) DEPENDENCIES = ['homematicip_cloud'] @@ -24,13 +24,16 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the HomematicIP switch devices.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the HomematicIP switch from a config entry.""" from homematicip.device import ( PlugableSwitch, PlugableSwitchMeasuring, BrandSwitchMeasuring) - if discovery_info is None: - return - home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: if isinstance(device, BrandSwitchMeasuring): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index be67ebd9cc3a2c..2e5613057f148f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -127,6 +127,7 @@ async def async_step_discovery(info): FLOWS = [ 'cast', 'deconz', + 'homematicip_cloud', 'hue', 'nest', 'sonos', diff --git a/requirements_all.txt b/requirements_all.txt index 1491e1dbff4122..c72e56821d6f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -421,7 +421,7 @@ home-assistant-frontend==20180704.0 # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.9.4 +homematicip==0.9.6 # homeassistant.components.remember_the_milk httplib2==0.10.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 476b3d96c3d2e1..aabbdc44bea154 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -83,6 +83,9 @@ holidays==0.9.5 # homeassistant.components.frontend home-assistant-frontend==20180704.0 +# homeassistant.components.homematicip_cloud +homematicip==0.9.6 + # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb influxdb==5.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7bf87c74de7266..9a5b4dd1a43e15 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -56,6 +56,7 @@ 'hbmqtt', 'holidays', 'home-assistant-frontend', + 'homematicip', 'influxdb', 'libpurecoollink', 'libsoundtouch', diff --git a/tests/components/homematicip_cloud/__init__.py b/tests/components/homematicip_cloud/__init__.py new file mode 100644 index 00000000000000..1d89bd73183c91 --- /dev/null +++ b/tests/components/homematicip_cloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the HomematicIP Cloud component.""" diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py new file mode 100644 index 00000000000000..1c2e54a1a5dfb8 --- /dev/null +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -0,0 +1,150 @@ +"""Tests for HomematicIP Cloud config flow.""" +from unittest.mock import patch + +from homeassistant.components.homematicip_cloud import hap as hmipc +from homeassistant.components.homematicip_cloud import config_flow, const + +from tests.common import MockConfigEntry, mock_coro + + +async def test_flow_works(hass): + """Test config flow works.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', return_value=mock_coro()), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_register', + return_value=mock_coro(True)): + hap.authtoken = 'ABC' + result = await flow.async_step_init(user_input=config) + + assert hap.authtoken == 'ABC' + assert result['type'] == 'create_entry' + + +async def test_flow_init_connection_error(hass): + """Test config flow with accesspoint connection error.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'form' + + +async def test_flow_link_connection_error(hass): + """Test config flow client registration connection error.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_register', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'abort' + + +async def test_flow_link_press_button(hass): + """Test config flow ask for pressing the blue button.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + with patch.object(hmipc.HomematicipAuth, 'async_setup', + return_value=mock_coro(True)), \ + patch.object(hmipc.HomematicipAuth, 'async_checkbutton', + return_value=mock_coro(False)): + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'form' + assert result['errors'] == {'base': 'press_the_button'} + + +async def test_init_flow_show_form(hass): + """Test config flow shows up with a form.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input=None) + assert result['type'] == 'form' + + +async def test_init_already_configured(hass): + """Test accesspoint is already configured.""" + MockConfigEntry(domain=const.DOMAIN, data={ + const.HMIPC_HAPID: 'ABC123', + }).add_to_hass(hass) + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input=config) + assert result['type'] == 'abort' + + +async def test_import_config(hass): + """Test importing a host with an existing config file.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'ABC123' + assert result['data'] == { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + } + + +async def test_import_existing_config(hass): + """Test abort of an existing accesspoint from config.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + MockConfigEntry(domain=const.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + }).add_to_hass(hass) + + result = await flow.async_step_import({ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip' + }) + + assert result['type'] == 'abort' diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py new file mode 100644 index 00000000000000..5344773fde6595 --- /dev/null +++ b/tests/components/homematicip_cloud/test_hap.py @@ -0,0 +1,113 @@ +"""Test HomematicIP Cloud accesspoint.""" +from unittest.mock import Mock, patch + +from homeassistant.components.homematicip_cloud import hap as hmipc +from homeassistant.components.homematicip_cloud import const, errors +from tests.common import mock_coro + + +async def test_auth_setup(hass): + """Test auth setup for client registration.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', return_value=mock_coro()): + assert await hap.async_setup() is True + + +async def test_auth_setup_connection_error(hass): + """Test auth setup connection error behaviour.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + with patch.object(hap, 'get_auth', + side_effect=errors.HmipcConnectionError): + assert await hap.async_setup() is False + + +async def test_auth_auth_check_and_register(hass): + """Test auth client registration.""" + config = { + const.HMIPC_HAPID: 'ABC123', + const.HMIPC_PIN: '123', + const.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipAuth(hass, config) + hap.auth = Mock() + with patch.object(hap.auth, 'isRequestAcknowledged', + return_value=mock_coro()), \ + patch.object(hap.auth, 'requestAuthToken', + return_value=mock_coro('ABC')), \ + patch.object(hap.auth, 'confirmAuthToken', + return_value=mock_coro()): + assert await hap.async_checkbutton() is True + assert await hap.async_register() == 'ABC' + + +async def test_hap_setup_works(aioclient_mock): + """Test a successful setup of a accesspoint.""" + hass = Mock() + entry = Mock() + home = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', return_value=mock_coro(home)): + assert await hap.async_setup() is True + + assert hap.home is home + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5 + assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + + +async def test_hap_setup_connection_error(): + """Test a failed accesspoint setup.""" + hass = Mock() + entry = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', + side_effect=errors.HmipcConnectionError): + assert await hap.async_setup() is False + + assert len(hass.async_add_job.mock_calls) == 0 + assert len(hass.config_entries.flow.async_init.mock_calls) == 0 + + +async def test_hap_reset_unloads_entry_if_setup(): + """Test calling reset while the entry has been setup.""" + hass = Mock() + entry = Mock() + home = Mock() + entry.data = { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + hap = hmipc.HomematicipHAP(hass, entry) + with patch.object(hap, 'get_hap', return_value=mock_coro(home)): + assert await hap.async_setup() is True + + assert hap.home is home + assert len(hass.services.async_register.mock_calls) == 0 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5 + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + await hap.async_reset() + + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 5 diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py new file mode 100644 index 00000000000000..185372272471bd --- /dev/null +++ b/tests/components/homematicip_cloud/test_init.py @@ -0,0 +1,103 @@ +"""Test HomematicIP Cloud setup process.""" + +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import homematicip_cloud as hmipc + +from tests.common import mock_coro, MockConfigEntry + + +async def test_config_with_accesspoint_passed_to_config_entry(hass): + """Test that config for a accesspoint are loaded via config entry.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=[]): + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'name', + } + }) is True + + # Flow started for the access point + assert len(mock_config_entries.flow.mock_calls) == 2 + + +async def test_config_already_registered_not_passed_to_config_entry(hass): + """Test that an already registered accesspoint does not get imported.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=['ABC123']): + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'name', + } + }) is True + + # No flow started + assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = MockConfigEntry(domain=hmipc.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + }) + entry.add_to_hass(hass) + with patch.object(hmipc, 'HomematicipHAP') as mock_hap: + mock_hap.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'hmip', + } + }) is True + + assert len(mock_hap.mock_calls) == 2 + + +async def test_setup_defined_accesspoint(hass): + """Test we initiate config entry for the accesspoint.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hmipc, 'configured_haps', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hmipc.DOMAIN, { + hmipc.DOMAIN: { + hmipc.CONF_ACCESSPOINT: 'ABC123', + hmipc.CONF_AUTHTOKEN: '123', + hmipc.CONF_NAME: 'hmip', + } + }) is True + + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + } + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=hmipc.DOMAIN, data={ + hmipc.HMIPC_HAPID: 'ABC123', + hmipc.HMIPC_AUTHTOKEN: '123', + hmipc.HMIPC_NAME: 'hmip', + }) + entry.add_to_hass(hass) + + with patch.object(hmipc, 'HomematicipHAP') as mock_hap: + mock_hap.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True + + assert len(mock_hap.return_value.mock_calls) == 1 + + mock_hap.return_value.async_reset.return_value = mock_coro(True) + assert await hmipc.async_unload_entry(hass, entry) + assert len(mock_hap.return_value.async_reset.mock_calls) == 1 + assert hass.data[hmipc.DOMAIN] == {}