From f367c49fb922330b235f9e5c2b2997c851e35068 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 6 Nov 2016 00:58:29 +0100 Subject: [PATCH 01/30] Sonos fix for slow update (#4232) * Sonos fix for slow update * fix auto update on discovery * fix unittest --- .../components/media_player/sonos.py | 29 ++++++++++--------- tests/components/media_player/test_sonos.py | 23 ++++++++++----- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 39b9559aa59..89f5d7b07ed 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if player.is_visible: device = SonosDevice(hass, player) - add_devices([device]) + add_devices([device], True) if not DEVICES: register_services(hass) DEVICES.append(device) @@ -106,7 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False DEVICES = [SonosDevice(hass, p) for p in players] - add_devices(DEVICES) + add_devices(DEVICES, True) register_services(hass) _LOGGER.info('Added %s Sonos speakers', len(players)) return True @@ -256,6 +256,7 @@ class SonosDevice(MediaPlayerDevice): self.hass = hass self.volume_increment = 5 + self._unique_id = player.uid self._player = player self._player_volume = None self._player_volume_muted = None @@ -278,7 +279,8 @@ class SonosDevice(MediaPlayerDevice): self._current_track_is_radio_stream = False self._queue = None self._last_avtransport_event = None - self.update() + self._is_playing_line_in = None + self._is_playing_tv = None self.soco_snapshot = Snapshot(self._player) @property @@ -286,14 +288,10 @@ class SonosDevice(MediaPlayerDevice): """Polling needed.""" return True - def update_sonos(self, now): - """Update state, called by track_utc_time_change.""" - self.update_ha_state(True) - @property def unique_id(self): """Return an unique ID.""" - return self._player.uid + return self._unique_id @property def name(self): @@ -354,6 +352,9 @@ class SonosDevice(MediaPlayerDevice): if is_available: + self._is_playing_tv = self._player.is_playing_tv + self._is_playing_line_in = self._player.is_playing_line_in + track_info = None if self._last_avtransport_event: variables = self._last_avtransport_event.variables @@ -511,10 +512,10 @@ class SonosDevice(MediaPlayerDevice): # update state of the whole group # pylint: disable=protected-access for device in [x for x in DEVICES if x._coordinator == self]: - if device.entity_id: - device.update_ha_state(False) + if device.entity_id is not self.entity_id: + self.hass.add_job(device.async_update_ha_state) - if self._queue is None and self.entity_id: + if self._queue is None: self._subscribe_to_player_events() else: self._player_volume = None @@ -534,6 +535,8 @@ class SonosDevice(MediaPlayerDevice): self._support_previous_track = False self._support_next_track = False self._support_pause = False + self._is_playing_tv = False + self._is_playing_line_in = False self._last_avtransport_event = None @@ -713,9 +716,9 @@ class SonosDevice(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - if self._player.is_playing_line_in: + if self._is_playing_line_in: return SUPPORT_SOURCE_LINEIN - if self._player.is_playing_tv: + if self._is_playing_tv: return SUPPORT_SOURCE_TV return None diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index dfd308d5459..b170f14c372 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -92,6 +92,13 @@ class SoCoMock(): return "RINCON_XXXXXXXXXXXXXXXXX" +def fake_add_device(devices, update_befor_add=False): + """Fake add device / update.""" + if update_befor_add: + for speaker in devices: + speaker.update() + + class TestSonosMediaPlayer(unittest.TestCase): """Test the media_player module.""" @@ -117,7 +124,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_discovery(self, *args): """Test a single device using the autodiscovery provided by HASS.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) @@ -129,7 +136,7 @@ class TestSonosMediaPlayer(unittest.TestCase): """Test a single address config'd by the HASS config file.""" sonos.setup_platform(self.hass, {'hosts': '192.0.2.1'}, - mock.MagicMock()) + fake_add_device) # Ensure registration took place (#2558) self.assertEqual(len(sonos.DEVICES), 1) @@ -140,7 +147,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock()) + sonos.setup_platform(self.hass, {}, fake_add_device) self.assertEqual(len(sonos.DEVICES), 1) self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') @@ -149,7 +156,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'partymode') def test_sonos_group_players(self, partymodeMock, *args): """Ensuring soco methods called for sonos_group_players service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] partymodeMock.return_value = True device.group_players() @@ -161,7 +168,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'unjoin') def test_sonos_unjoin(self, unjoinMock, *args): """Ensuring soco methods called for sonos_unjoin service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] unjoinMock.return_value = True device.unjoin() @@ -173,7 +180,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] device.set_sleep_timer(30) set_sleep_timerMock.assert_called_once_with(30) @@ -193,7 +200,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') def test_sonos_snapshot(self, snapshotMock, *args): """Ensuring soco methods called for sonos_snapshot service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] snapshotMock.return_value = True device.snapshot() @@ -205,7 +212,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(soco.snapshot.Snapshot, 'restore') def test_sonos_restore(self, restoreMock, *args): """Ensuring soco methods called for sonos_restor service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') device = sonos.DEVICES[-1] restoreMock.return_value = True device.restore() From 28861221aea3b0d6deaf977c21eea7a7bd04a03a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 15:29:22 -0700 Subject: [PATCH 02/30] Remove chunked encoding (#4230) --- homeassistant/components/camera/__init__.py | 1 - homeassistant/components/camera/ffmpeg.py | 1 - homeassistant/components/camera/mjpeg.py | 1 - homeassistant/components/camera/synology.py | 1 - 4 files changed, 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d02e7954349..6724598419f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -101,7 +101,6 @@ class Camera(Entity): response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--jpegboundary') - response.enable_chunked_encoding() yield from response.prepare(request) def write(img_bytes): diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 8e238bfdea7..c3f0ffbfe0b 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -75,7 +75,6 @@ class FFmpegCamera(Camera): response = web.StreamResponse() response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' - response.enable_chunked_encoding() yield from response.prepare(request) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 81759fa86df..e8799d1be34 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -112,7 +112,6 @@ class MjpegCamera(Camera): response = web.StreamResponse() response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - response.enable_chunked_encoding() yield from response.prepare(request) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 4abdf8d22dd..4ca63c16d7d 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -271,7 +271,6 @@ class SynologyCamera(Camera): response = web.StreamResponse() response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) - response.enable_chunked_encoding() yield from response.prepare(request) From 20e1b3eae08b9052b44e9f54d5ed8c5d76e15033 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 13:28:11 -0700 Subject: [PATCH 03/30] Fix radiotherm I/O inside properties (#4227) --- homeassistant/components/climate/radiotherm.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index c2d712e19bd..5fa3f891aac 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -69,6 +69,8 @@ class RadioThermostat(ClimateDevice): self._current_temperature = None self._current_operation = STATE_IDLE self._name = None + self._fmode = None + self._tmode = None self.hold_temp = hold_temp self.update() self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF] @@ -87,8 +89,8 @@ class RadioThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" return { - ATTR_FAN: self.device.fmode['human'], - ATTR_MODE: self.device.tmode['human'] + ATTR_FAN: self._fmode, + ATTR_MODE: self._tmode, } @property @@ -115,10 +117,13 @@ class RadioThermostat(ClimateDevice): """Update the data from the thermostat.""" self._current_temperature = self.device.temp['raw'] self._name = self.device.name['raw'] - if self.device.tmode['human'] == 'Cool': + self._fmode = self.device.fmode['human'] + self._tmode = self.device.tmode['human'] + + if self._tmode == 'Cool': self._target_temperature = self.device.t_cool['raw'] self._current_operation = STATE_COOL - elif self.device.tmode['human'] == 'Heat': + elif self._tmode == 'Heat': self._target_temperature = self.device.t_heat['raw'] self._current_operation = STATE_HEAT else: From af297aa0dccd41fa7e5b35f86163a41bc7fcfa49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Nov 2016 17:00:06 -0700 Subject: [PATCH 04/30] Version bump to 0.32.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ec05a7e24ac..299e437be3c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 6d5f00098a0ef4a941542cb84873909f494009f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 07:53:54 -0800 Subject: [PATCH 05/30] Move Honeywell I/O out of event loop (#4244) --- homeassistant/components/climate/honeywell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 540a9a941d1..0d31cdd1387 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -223,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - self._device.refresh() return self._device.current_temperature @property @@ -274,3 +273,7 @@ class HoneywellUSThermostat(ClimateDevice): """Set the system mode (Cool, Heat, etc).""" if hasattr(self._device, ATTR_SYSTEM_MODE): self._device.system_mode = operation_mode + + def update(self): + """Update the state.""" + self._device.refresh() From faceb4c1dc0ac52212ef6fe9c9263cddc2fd9fa8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 23:12:20 -0800 Subject: [PATCH 06/30] Sequential updates for non-async entities --- homeassistant/helpers/entity_component.py | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a648de1d650..f4ca2f1274f 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -279,6 +279,7 @@ class EntityPlatform(object): self.entity_namespace = entity_namespace self.platform_entities = [] self._async_unsub_polling = None + self._process_updates = False def add_entities(self, new_entities, update_before_add=False): """Add entities for a single platform.""" @@ -335,14 +336,35 @@ class EntityPlatform(object): self._async_unsub_polling() self._async_unsub_polling = None - @callback + @asyncio.coroutine def _update_entity_states(self, now): """Update the states of all the polling entities. - + To protect from flooding the executor, we will update async entities + in parallel and other entities sequential. This method must be run in the event loop. """ - for entity in self.platform_entities: - if entity.should_poll: - self.component.hass.loop.create_task( - entity.async_update_ha_state(True) - ) + if self._process_updates: + return + self._process_updates = True + + try: + tasks = [] + to_update = [] + + for entity in self.platform_entities: + if not entity.should_poll: + continue + + update_coro = entity.async_update_ha_state(True) + if hasattr(entity, 'async_update'): + tasks.append(update_coro) + else: + to_update.append(update_coro) + + for update_coro in to_update: + yield from update_coro + + if tasks: + yield from asyncio.wait(tasks, loop=self.component.hass.loop) + finally: + self._process_updates = False From 6a92e27e2fc00ab31b5439e62fdfeba3208821c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 23:12:37 -0800 Subject: [PATCH 07/30] Version bump to 0.32.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 299e437be3c..0eb2384517d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 272899ec963ebef2f12ba2308f0e91bb06758383 Mon Sep 17 00:00:00 2001 From: andyat Date: Sun, 6 Nov 2016 23:18:06 -0800 Subject: [PATCH 08/30] Fix setting temperature in Celsius on radiotherm CT50 (#4270) --- homeassistant/components/climate/radiotherm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 5fa3f891aac..d06e148cfdd 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -135,9 +135,9 @@ class RadioThermostat(ClimateDevice): if temperature is None: return if self._current_operation == STATE_COOL: - self.device.t_cool = temperature + self.device.t_cool = round(temperature * 2.0) / 2.0 elif self._current_operation == STATE_HEAT: - self.device.t_heat = temperature + self.device.t_heat = round(temperature * 2.0) / 2.0 if self.hold_temp: self.device.hold = 1 else: @@ -159,6 +159,6 @@ class RadioThermostat(ClimateDevice): elif operation_mode == STATE_AUTO: self.device.tmode = 3 elif operation_mode == STATE_COOL: - self.device.t_cool = self._target_temperature + self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 elif operation_mode == STATE_HEAT: - self.device.t_heat = self._target_temperature + self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 From 0af1a96f14ef112acc9cf2f0212087aefd5353b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Nov 2016 23:24:25 -0800 Subject: [PATCH 09/30] Lint --- homeassistant/helpers/entity_component.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f4ca2f1274f..1144ee112e7 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -339,8 +339,10 @@ class EntityPlatform(object): @asyncio.coroutine def _update_entity_states(self, now): """Update the states of all the polling entities. + To protect from flooding the executor, we will update async entities in parallel and other entities sequential. + This method must be run in the event loop. """ if self._process_updates: From d129df93dd509d85798786f6cc01a6458435e538 Mon Sep 17 00:00:00 2001 From: David-Leon Pohl Date: Mon, 7 Nov 2016 08:34:32 +0100 Subject: [PATCH 10/30] Hotfix #4272 (#4273) --- homeassistant/components/switch/pilight.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 1818372f1dc..f07d91ca9fb 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -37,8 +37,10 @@ SWITCHES_SCHEMA = vol.Schema({ vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECIEVE): COMMAND_SCHEMA, - vol.Optional(CONF_ON_CODE_RECIEVE): COMMAND_SCHEMA, + vol.Optional(CONF_OFF_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + [COMMAND_SCHEMA]), + vol.Optional(CONF_ON_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list, + [COMMAND_SCHEMA]) }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From 58600f25b3d0f70e469ccf72b537de5cf6454009 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 9 Nov 2016 02:57:56 +0000 Subject: [PATCH 11/30] Fix OWM async I/O (#4298) --- homeassistant/components/weather/openweathermap.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index b029b4d44bb..a93b0142d90 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data = WeatherData(owm, latitude, longitude) add_devices([OpenWeatherMapWeather( - name, data, hass.config.units.temperature_unit)]) + name, data, hass.config.units.temperature_unit)], True) class OpenWeatherMapWeather(WeatherEntity): @@ -78,8 +78,7 @@ class OpenWeatherMapWeather(WeatherEntity): self._name = name self._owm = owm self._temperature_unit = temperature_unit - self.date = None - self.update() + self.data = None @property def name(self): From a18fdbfbb8590b12c732d62703ca978343e5337b Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Tue, 8 Nov 2016 19:57:46 -0800 Subject: [PATCH 12/30] Fix alarm.com I/O inside properties (#4307) * Fix alarm.com I/O inside properties * First line should end with a period * Not needed * Fetch state on init --- .../components/alarm_control_panel/alarmdotcom.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 8bf36e176e5..cd37fc6a828 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - add_devices([AlarmDotCom(hass, name, code, username, password)]) + add_devices([AlarmDotCom(hass, name, code, username, password)], True) class AlarmDotCom(alarm.AlarmControlPanel): @@ -54,12 +54,17 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._code = str(code) if code else None self._username = username self._password = password + self._state = STATE_UNKNOWN @property def should_poll(self): """No polling needed.""" return True + def update(self): + """Fetch the latest state.""" + self._state = self._alarm.state + @property def name(self): """Return the name of the alarm.""" @@ -73,11 +78,11 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def state(self): """Return the state of the device.""" - if self._alarm.state == 'Disarmed': + if self._state == 'Disarmed': return STATE_ALARM_DISARMED - elif self._alarm.state == 'Armed Stay': + elif self._state == 'Armed Stay': return STATE_ALARM_ARMED_HOME - elif self._alarm.state == 'Armed Away': + elif self._state == 'Armed Away': return STATE_ALARM_ARMED_AWAY else: return STATE_UNKNOWN From ffe4c425af25235ab3ee9338ceea1dcae331554b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Nov 2016 20:25:19 -0800 Subject: [PATCH 13/30] Fix Tellstick doing I/O inside event loop (#4268) --- homeassistant/components/sensor/tellstick.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 464e3554324..08e15cd332f 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -98,6 +98,7 @@ class TellstickSensor(Entity): self.datatype = datatype self.sensor = sensor self._unit_of_measurement = sensor_info.unit or None + self._value = None self._name = '{} {}'.format(name, sensor_info.name) @@ -109,9 +110,13 @@ class TellstickSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.sensor.value(self.datatype).value + return self._value @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + + def update(self): + """Update tellstick sensor.""" + self._value = self.sensor.value(self.datatype).value From eb17ba970cbd67e8b48b3bfbeb570b68df004fc9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Nov 2016 07:21:58 -0800 Subject: [PATCH 14/30] Increase update delay (#4321) --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ecb04aca9d9..ac058f89143 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -242,7 +242,7 @@ class Entity(object): end = timer() - if end - start > 0.2: + if end - start > 0.4: _LOGGER.warning('Updating state for %s took %.3f seconds. ' 'Please report platform to the developers at ' 'https://goo.gl/Nvioub', self.entity_id, From 200bdb30ff9cc2024118ecd6611f6d257ac75f92 Mon Sep 17 00:00:00 2001 From: Jan Losinski Date: Thu, 10 Nov 2016 22:14:40 +0100 Subject: [PATCH 15/30] Change pilight systemcode validation to integer (#4286) * Change pilight systemcode validation to integer According to the pilight code the systemcode should be an integer and not a string (it is an int in the pilight code). Passing this as a string caused errors from pilight: "ERROR: elro_800_switch: insufficient number of arguments" This fixes #4282 * Change pilight unit-id to positive integer According to the pilight code the unit of an entity is also evrywhere handled as an integer. So converting and passing this as string causes pilight not to work. This fixes #4282 Signed-off-by: Jan Losinski --- homeassistant/components/switch/pilight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index f07d91ca9fb..80a36756d79 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -27,10 +27,10 @@ DEPENDENCIES = ['pilight'] COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({ vol.Optional('on'): cv.positive_int, vol.Optional('off'): cv.positive_int, - vol.Optional(CONF_UNIT): cv.string, + vol.Optional(CONF_UNIT): cv.positive_int, vol.Optional(CONF_ID): cv.positive_int, vol.Optional(CONF_STATE): cv.string, - vol.Optional(CONF_SYSTEMCODE): cv.string, + vol.Optional(CONF_SYSTEMCODE): cv.positive_int, }) SWITCHES_SCHEMA = vol.Schema({ From 3e1cc4282ea39586950e51fd79bf3543b554589b Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Thu, 10 Nov 2016 23:44:38 -0500 Subject: [PATCH 16/30] Fix "argument of type 'NoneType' is not iterable" during discovery (#4279) * Fix "argument of type 'NoneType' is not iterable" during discovery When yamaha receivers are dynamically discovered, there config is empty, which means that we need to set zone_ignore to [] otherwise the iteration over receivers fails. * Bump rxv library version to fix play_status bug rxv version 0.3 will issue the play_status command even for sources that don't support it, causing stack traces during updates when receivers are on HDMI inputs. This was fixed in rxv 0.3.1. Bump to fix bug #4226. * Don't discovery receivers that we've already configured The discovery component doesn't know anything about already configured receivers. This means that specifying a receiver manually will make it show up twice if you have the discovery component enabled. This puts a platform specific work around here that ensures that if the media_player is found, we ignore the discovery system. --- homeassistant/components/media_player/yamaha.py | 14 +++++++++++++- requirements_all.txt | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 94191862f44..0e265199fce 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, STATE_PLAYING, STATE_IDLE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.3.0'] +REQUIREMENTS = ['rxv==0.3.1'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +35,7 @@ CONF_SOURCE_IGNORE = 'source_ignore' CONF_ZONE_IGNORE = 'zone_ignore' DEFAULT_NAME = 'Yamaha Receiver' +KNOWN = 'yamaha_known_receivers' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -50,6 +51,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yamaha platform.""" import rxv + # keep track of configured receivers so that we don't end up + # discovering a receiver dynamically that we have static config + # for. + if hass.data.get(KNOWN, None) is None: + hass.data[KNOWN] = set() name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -62,12 +68,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): model = discovery_info[1] ctrl_url = discovery_info[2] desc_url = discovery_info[3] + if ctrl_url in hass.data[KNOWN]: + _LOGGER.info("%s already manually configured", ctrl_url) + return receivers = rxv.RXV( ctrl_url, model_name=model, friendly_name=name, unit_desc_url=desc_url).zone_controllers() _LOGGER.info("Receivers: %s", receivers) + # when we are dynamically discovered config is empty + zone_ignore = [] elif host is None: receivers = [] for recv in rxv.find(): @@ -78,6 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for receiver in receivers: if receiver.zone not in zone_ignore: + hass.data[KNOWN].add(receiver.ctrl_url) add_devices([ YamahaDevice(name, receiver, source_ignore, source_names)]) diff --git a/requirements_all.txt b/requirements_all.txt index af5519f1d15..39e037cdf89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ radiotherm==1.2 # rpi-rf==0.9.5 # homeassistant.components.media_player.yamaha -rxv==0.3.0 +rxv==0.3.1 # homeassistant.components.media_player.samsungtv samsungctl==0.5.1 From 6860d9b096d2b260cdf3575e534fd06319f0f9f1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:01:42 +0100 Subject: [PATCH 17/30] Update SoCo to 0.12 (#4337) * Update SoCo to 0.12 * fix req --- homeassistant/components/media_player/sonos.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 89f5d7b07ed..ebc8d58874a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -21,9 +21,7 @@ from homeassistant.const import ( from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/' - 'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#' - 'SoCo==0.12'] +REQUIREMENTS = ['SoCo==0.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 39e037cdf89..e6ccf11cbed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,6 +24,9 @@ PyMata==2.13 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 +# homeassistant.components.media_player.sonos +SoCo==0.12 + # homeassistant.components.notify.twitter TwitterAPI==2.4.2 @@ -157,9 +160,6 @@ https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e4273 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5 -# homeassistant.components.media_player.sonos -https://github.com/SoCo/SoCo/archive/cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#SoCo==0.12 - # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 From 55ddaf1ee7b6d05145803f37f2c96174578f0445 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:04:47 +0100 Subject: [PATCH 18/30] Synology SSL fix & Error handling (#4325) * Synology SSL fix & Error handling * change handling for cookies/ssl * fix use not deprecated functions * fix lint * change verify * fix connector close to coro * fix force close * not needed since websession close connector too * fix params * fix lint --- homeassistant/components/camera/synology.py | 113 ++++++++++---------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 4ca63c16d7d..bbca25fd6b6 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -9,13 +9,14 @@ import logging import voluptuous as vol +import aiohttp from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL) + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv @@ -57,6 +58,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" + if not config.get(CONF_VERIFY_SSL): + connector = aiohttp.TCPConnector(verify_ssl=False) + else: + connector = None + + websession_init = aiohttp.ClientSession( + loop=hass.loop, + connector=connector + ) + # Determine API to use for authentication syno_api_url = SYNO_API_URL.format( config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) @@ -69,13 +80,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - query_req = yield from hass.websession.get( + query_req = yield from websession_init.get( syno_api_url, - params=query_payload, - verify_ssl=config.get(CONF_VERIFY_SSL) + params=query_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", syno_api_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", syno_api_url) return False query_resp = yield from query_req.json() @@ -93,12 +103,26 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): session_id = yield from get_session_id( hass, + websession_init, config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - syno_auth_url, - config.get(CONF_VERIFY_SSL) + syno_auth_url ) + websession_init.detach() + + # init websession + websession = aiohttp.ClientSession( + loop=hass.loop, connector=connector, cookies={'id': session_id}) + + @asyncio.coroutine + def _async_close_websession(event): + """Close webssesion on shutdown.""" + yield from websession.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_websession) + # Use SessionID to get cameras in system syno_camera_url = SYNO_API_URL.format( config.get(CONF_URL), WEBAPI_PATH, camera_api) @@ -110,14 +134,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - camera_req = yield from hass.websession.get( + camera_req = yield from websession.get( syno_camera_url, - params=camera_payload, - verify_ssl=config.get(CONF_VERIFY_SSL), - cookies={'id': session_id} + params=camera_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", syno_camera_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", syno_camera_url) return False camera_resp = yield from camera_req.json() @@ -126,13 +148,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # add cameras devices = [] - tasks = [] for camera in cameras: if not config.get(CONF_WHITELIST): camera_id = camera['id'] snapshot_path = camera['snapshot_path'] device = SynologyCamera( + hass, + websession, config, camera_id, camera['name'], @@ -141,15 +164,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera_path, auth_path ) - tasks.append(device.async_read_sid()) devices.append(device) - yield from asyncio.gather(*tasks, loop=hass.loop) - hass.loop.create_task(async_add_devices(devices)) + yield from async_add_devices(devices) @asyncio.coroutine -def get_session_id(hass, username, password, login_url, valid_cert): +def get_session_id(hass, websession, username, password, login_url): """Get a session id.""" auth_payload = { 'api': AUTH_API, @@ -162,13 +183,12 @@ def get_session_id(hass, username, password, login_url, valid_cert): } try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - auth_req = yield from hass.websession.get( + auth_req = yield from websession.get( login_url, - params=auth_payload, - verify_ssl=valid_cert + params=auth_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", login_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", login_url) return False auth_resp = yield from auth_req.json() @@ -180,36 +200,22 @@ def get_session_id(hass, username, password, login_url, valid_cert): class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" - def __init__(self, config, camera_id, camera_name, - snapshot_path, streaming_path, camera_path, auth_path): + def __init__(self, hass, websession, config, camera_id, + camera_name, snapshot_path, streaming_path, camera_path, + auth_path): """Initialize a Synology Surveillance Station camera.""" super().__init__() + self.hass = hass + self._websession = websession self._name = camera_name - self._username = config.get(CONF_USERNAME) - self._password = config.get(CONF_PASSWORD) self._synology_url = config.get(CONF_URL) - self._api_url = config.get(CONF_URL) + 'webapi/' - self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi' self._camera_name = config.get(CONF_CAMERA_NAME) self._stream_id = config.get(CONF_STREAM_ID) - self._valid_cert = config.get(CONF_VERIFY_SSL) self._camera_id = camera_id self._snapshot_path = snapshot_path self._streaming_path = streaming_path self._camera_path = camera_path self._auth_path = auth_path - self._session_id = None - - @asyncio.coroutine - def async_read_sid(self): - """Get a session id.""" - self._session_id = yield from get_session_id( - self.hass, - self._username, - self._password, - self._login_url, - self._valid_cert - ) def camera_image(self): """Return bytes of camera image.""" @@ -230,14 +236,12 @@ class SynologyCamera(Camera): } try: with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from self.hass.websession.get( + response = yield from self._websession.get( image_url, - params=image_payload, - verify_ssl=self._valid_cert, - cookies={'id': self._session_id} + params=image_payload ) - except asyncio.TimeoutError: - _LOGGER.error("Timeout on %s", image_url) + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", image_url) return None image = yield from response.read() @@ -260,13 +264,12 @@ class SynologyCamera(Camera): } try: with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - stream = yield from self.hass.websession.get( + stream = yield from self._websession.get( streaming_url, - payload=streaming_payload, - verify_ssl=self._valid_cert, - cookies={'id': self._session_id} + params=streaming_payload ) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.exception("Error on %s", streaming_url) raise HTTPGatewayTimeout() response = web.StreamResponse() @@ -281,7 +284,7 @@ class SynologyCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.release()) + self.hass.async_add_job(stream.release()) yield from response.write_eof() @property From 6e6b1ef7ab137f6fa9f4592e45075188e9c3c58c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Nov 2016 21:17:44 -0800 Subject: [PATCH 19/30] fix panasonic viera doing I/O in event loop (#4341) --- .../components/media_player/panasonic_viera.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index d1a971eb91e..987364bbf63 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -79,16 +79,16 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): self._playing = True self._state = STATE_UNKNOWN self._remote = remote + self._volume = 0 def update(self): """Retrieve the latest data.""" try: self._muted = self._remote.get_mute() + self._volume = self._remote.get_volume() / 100 self._state = STATE_ON except OSError: self._state = STATE_OFF - return False - return True def send_key(self, key): """Send a key to the tv and handles exceptions.""" @@ -113,13 +113,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - volume = 0 - try: - volume = self._remote.get_volume() / 100 - self._state = STATE_ON - except OSError: - self._state = STATE_OFF - return volume + return self._volume @property def is_volume_muted(self): From 2c39c39d5276f9fae3456c2e09e9145352fef6a3 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 11 Nov 2016 07:28:22 +0200 Subject: [PATCH 20/30] Improve async generic camera's error handling (#4316) * Handle errors * Feedback * DisconnectedError --- homeassistant/components/camera/generic.py | 16 +++++++++------- homeassistant/components/sensor/yr.py | 14 ++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index c6664ed70b2..e4d97987dbf 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -91,7 +91,7 @@ class GenericCamera(Camera): if url == self._last_url and self._limit_refetch: return self._last_image - # aiohttp don't support DigestAuth jet + # aiohttp don't support DigestAuth yet if self._authentication == HTTP_DIGEST_AUTHENTICATION: def fetch(): """Read image from a URL.""" @@ -109,15 +109,17 @@ class GenericCamera(Camera): else: try: with async_timeout.timeout(10, loop=self.hass.loop): - respone = yield from self.hass.websession.get( - url, - auth=self._auth - ) - self._last_image = yield from respone.read() - yield from respone.release() + response = yield from self.hass.websession.get( + url, auth=self._auth) + self._last_image = yield from response.read() + yield from response.release() except asyncio.TimeoutError: _LOGGER.error('Timeout getting camera image') return self._last_image + except (aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: + _LOGGER.error('Error getting new camera image: %s', err) + return self._last_image self._last_url = url return self._last_image diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 51616062475..3436288b627 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -10,7 +10,7 @@ import logging from xml.parsers.expat import ExpatError import async_timeout -from aiohttp.web import HTTPException +import aiohttp import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -154,12 +154,9 @@ class YrData(object): try_again('{} returned {}'.format(self._url, resp.status)) return text = yield from resp.text() - self.hass.loop.create_task(resp.release()) - except asyncio.TimeoutError as err: - try_again(err) - return - except HTTPException as err: - resp.close() + self.hass.async_add_job(resp.release()) + except (asyncio.TimeoutError, aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: try_again(err) return @@ -218,4 +215,5 @@ class YrData(object): dev._state = new_state tasks.append(dev.async_update_ha_state()) - yield from asyncio.gather(*tasks, loop=self.hass.loop) + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) From 2feea1d1eba4dada7d66ac3fcc21a29fc9fda790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 11 Nov 2016 06:30:52 +0100 Subject: [PATCH 21/30] Add support for rgb light in led flux, fixes issue #4303 (#4332) --- homeassistant/components/light/flux_led.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 095733afd8e..9de4aa6b0fc 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -23,6 +23,7 @@ REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip' _LOGGER = logging.getLogger(__name__) CONF_AUTOMATIC_ADD = 'automatic_add' +ATTR_MODE = 'mode' DOMAIN = 'flux_led' @@ -31,6 +32,8 @@ SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(ATTR_MODE, default='rgbw'): + vol.All(cv.string, vol.In(['rgbw', 'rgb'])), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -48,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device = {} device['name'] = device_config[CONF_NAME] device['ipaddr'] = ipaddr + device[ATTR_MODE] = device_config[ATTR_MODE] light = FluxLight(device) if light.is_valid: lights.append(light) @@ -65,6 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ipaddr in light_ips: continue device['name'] = device['id'] + " " + ipaddr + device[ATTR_MODE] = 'rgbw' light = FluxLight(device) if light.is_valid: lights.append(light) @@ -82,6 +87,7 @@ class FluxLight(Light): self._name = device['name'] self._ipaddr = device['ipaddr'] + self._mode = device[ATTR_MODE] self.is_valid = True self._bulb = None try: @@ -132,7 +138,11 @@ class FluxLight(Light): if rgb: self._bulb.setRgb(*tuple(rgb)) elif brightness: - self._bulb.setWarmWhite255(brightness) + if self._mode == 'rgbw': + self._bulb.setWarmWhite255(brightness) + elif self._mode == 'rgb': + (red, green, blue) = self._bulb.getRgb() + self._bulb.setRgb(red, green, blue, brightness=brightness) elif effect == EFFECT_RANDOM: self._bulb.setRgb(random.randrange(0, 255), random.randrange(0, 255), From cc5233103cf211a64d2d07c619e2fc4d7ad244af Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 11 Nov 2016 06:32:08 +0100 Subject: [PATCH 22/30] Fix rest switch default template (#4331) --- homeassistant/components/switch/rest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 056bcef0281..36674c16d16 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -12,11 +12,12 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template CONF_BODY_OFF = 'body_off' CONF_BODY_ON = 'body_on' -DEFAULT_BODY_OFF = 'OFF' -DEFAULT_BODY_ON = 'ON' +DEFAULT_BODY_OFF = Template('OFF') +DEFAULT_BODY_ON = Template('ON') DEFAULT_NAME = 'REST Switch' DEFAULT_TIMEOUT = 10 CONF_IS_ON_TEMPLATE = 'is_on_template' From 1b79722b6973babddd3b0d86d5028f5750da5cfa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Nov 2016 21:00:33 -0800 Subject: [PATCH 23/30] Fix KNX async I/O (#4267) --- homeassistant/components/climate/knx.py | 19 +++++++++++++------ homeassistant/components/knx.py | 2 -- homeassistant/components/sensor/knx.py | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index ef7445c35fd..888a217d90c 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius self._away = False # not yet supported self._is_fan_on = False # not yet supported + self._current_temp = None + self._target_temp = None @property def should_poll(self): @@ -70,16 +72,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value('temperature')) + return self._current_temp @property def target_temperature(self): """Return the temperature we try to reach.""" - from knxip.conversion import knx2_to_float - - return knx2_to_float(self.value('setpoint')) + return self._target_temp def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" raise NotImplementedError() + + def update(self): + """Update KNX climate.""" + from knxip.conversion import knx2_to_float + + super().update() + + self._current_temp = knx2_to_float(self.value('temperature')) + self._target_temp = knx2_to_float(self.value('setpoint')) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 5d096b30ee0..8653f33c663 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -161,8 +161,6 @@ class KNXGroupAddress(Entity): @property def is_on(self): """Return True if the value is not 0 is on, else False.""" - if self.should_poll: - self.update() return self._state != 0 @property diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index 007291f5fb1..1f5c9a76520 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -113,15 +113,24 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass): self._unit_of_measurement = unit_of_measurement self._minimum_value = minimum_sensor_value self._maximum_value = maximum_sensor_value + self._value = None KNXGroupAddress.__init__(self, hass, config) @property def state(self): """Return the Value of the KNX Sensor.""" + return self._value + + def update(self): + """Update KNX sensor.""" + from knxip.conversion import knx2_to_float + + super().update() + + self._value = None + if self._data: - from knxip.conversion import knx2_to_float value = knx2_to_float(self._data) if self._minimum_value <= value <= self._maximum_value: - return value - return None + self._value = value From 72407c2f95817e8e6a0139e923de0126726490bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Nov 2016 21:49:56 -0800 Subject: [PATCH 24/30] Make yr compatible with 0.32 --- homeassistant/components/sensor/yr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 3436288b627..ab1e2da5852 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -154,7 +154,7 @@ class YrData(object): try_again('{} returned {}'.format(self._url, resp.status)) return text = yield from resp.text() - self.hass.async_add_job(resp.release()) + self.hass.async_add_job(resp.release) except (asyncio.TimeoutError, aiohttp.errors.ClientError, aiohttp.errors.ClientDisconnectedError) as err: try_again(err) From 173e15e73360cf0661650fef017d7d62e1753c35 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Nov 2016 21:50:05 -0800 Subject: [PATCH 25/30] Version bump to 0.32.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0eb2384517d..cdc846c49d3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) From 4c01b47945e3be7275f30ecb64c10a554b215760 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 18:35:58 -0800 Subject: [PATCH 26/30] device_tracker.see should not call async methods (#4377) --- homeassistant/components/device_tracker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6c6ae3adea6..082602b09f8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -242,7 +242,7 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, device) # During init, we ignore the group if self.group is not None: From 09c29737de16e8e08113afeb9b731d525fda9388 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 20:59:29 -0800 Subject: [PATCH 27/30] Fix device tracker sending invalid event data --- homeassistant/components/device_tracker/__init__.py | 7 +++++-- tests/components/device_tracker/test_init.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 082602b09f8..13194f88894 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -31,7 +31,7 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) + DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID) DOMAIN = 'device_tracker' DEPENDENCIES = ['zone'] @@ -242,7 +242,10 @@ class DeviceTracker(object): if device.track: device.update_ha_state() - self.hass.bus.fire(EVENT_NEW_DEVICE, device) + self.hass.bus.fire(EVENT_NEW_DEVICE, { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + }) # During init, we ignore the group if self.group is not None: diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 1f95c38cd7f..e4576ec9830 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,6 @@ """The tests for the device tracker component.""" # pylint: disable=protected-access +import json import logging import unittest from unittest.mock import call, patch @@ -15,6 +16,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM) import homeassistant.components.device_tracker as device_tracker from homeassistant.exceptions import HomeAssistantError +from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, fire_service_discovered, @@ -324,7 +326,16 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.see(self.hass, 'mac_1', host_name='hello') self.hass.block_till_done() - self.assertEqual(1, len(test_events)) + + assert len(test_events) == 1 + + # Assert we can serialize the event + json.dumps(test_events[0].as_dict(), cls=JSONEncoder) + + assert test_events[0].data == { + 'entity_id': 'device_tracker.hello', + 'host_name': 'hello', + } # pylint: disable=invalid-name def test_not_write_duplicate_yaml_keys(self): From fc2df34206a2f6069bfcaa00653b6fa74653c05e Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Mon, 14 Nov 2016 12:09:16 -0500 Subject: [PATCH 28/30] Pin versions on linters for tests The linters really need to specify an exact version, because when either flake8 or pylint release a new version, a whole lot of new issues are caught, causing failures on the code unrelated to the patches being pushed. Pinning is a best practice for linters. This allows patches which move forward the linter version to happen with any code fixes required for it to pass. --- requirements_test.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 933bb8a7c7b..19a70665b56 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,10 @@ -flake8>=3.0.4 -pylint>=1.5.6 +# linters such as flake8 and pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version +flake8==3.0.4 +pylint==1.6.4 +mypy-lang==0.4.5 +pydocstyle==1.1.1 coveralls>=1.1 pytest>=2.9.2 pytest-aiohttp>=0.1.3 @@ -7,7 +12,5 @@ pytest-asyncio>=0.5.0 pytest-cov>=2.3.1 pytest-timeout>=1.0.0 pytest-catchlog>=1.2.2 -pydocstyle>=1.0.0 requests_mock>=1.0 -mypy-lang>=0.4 mock-open>=1.3.1 From 96b8d8fcfa4545b8c316a8c958ab18fd954a76dd Mon Sep 17 00:00:00 2001 From: hexa- Date: Sun, 13 Nov 2016 01:14:39 +0100 Subject: [PATCH 29/30] http: reimplement X-Forwarded-For parsing (#4355) This feature needs to be enabled through the `http.use_x_forwarded_for` option, satisfying security concerns of spoofed remote addresses in untrusted network environments. The testsuite was enhanced to explicitly test the functionality of the header. Fixes #4265. Signed-off-by: Martin Weinelt --- homeassistant/components/emulated_hue.py | 1 + homeassistant/components/http.py | 23 +++++++++++------ homeassistant/const.py | 1 + tests/components/test_http.py | 32 ++++++++++++++++++++---- tests/scripts/test_check_config.py | 3 ++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 187ee0de603..0f06ed631ca 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -76,6 +76,7 @@ def setup(hass, yaml_config): ssl_certificate=None, ssl_key=None, cors_origins=[], + use_x_forwarded_for=False, trusted_networks=[] ) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 5886693c64f..fabff7add53 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -28,7 +28,7 @@ from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START) + EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR) import homeassistant.helpers.config_validation as cv from homeassistant.components import persistent_notification @@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development' CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' +CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_TRUSTED_NETWORKS = 'trusted_networks' DATA_API_PASSWORD = 'api_password' @@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]) }), @@ -125,6 +127,7 @@ def setup(hass, config): ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf.get(CONF_CORS_ORIGINS, []) + use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) trusted_networks = [ ip_network(trusted_network) for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] @@ -138,6 +141,7 @@ def setup(hass, config): ssl_certificate=ssl_certificate, ssl_key=ssl_key, cors_origins=cors_origins, + use_x_forwarded_for=use_x_forwarded_for, trusted_networks=trusted_networks ) @@ -248,7 +252,7 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, - trusted_networks): + use_x_forwarded_for, trusted_networks): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors @@ -260,6 +264,7 @@ class HomeAssistantWSGI(object): self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_networks = trusted_networks self.event_forwarder = None self._handler = None @@ -366,11 +371,15 @@ class HomeAssistantWSGI(object): yield from self._handler.finish_connections(60.0) yield from self.app.cleanup() - @staticmethod - def get_real_ip(request): + def get_real_ip(self, request): """Return the clients correct ip address, even in proxied setups.""" - peername = request.transport.get_extra_info('peername') - return peername[0] if peername is not None else None + if self.use_x_forwarded_for \ + and HTTP_HEADER_X_FORWARDED_FOR in request.headers: + return request.headers.get( + HTTP_HEADER_X_FORWARDED_FOR).split(',')[0] + else: + peername = request.transport.get_extra_info('peername') + return peername[0] if peername is not None else None def is_trusted_ip(self, remote_addr): """Match an ip address against trusted CIDR networks.""" @@ -452,7 +461,7 @@ def request_handler_factory(view, handler): @asyncio.coroutine def handle(request): """Handle incoming request.""" - remote_addr = HomeAssistantWSGI.get_real_ip(request) + remote_addr = view.hass.http.get_real_ip(request) # Auth code verbose on purpose authenticated = False diff --git a/homeassistant/const.py b/homeassistant/const.py index cdc846c49d3..d0fc145edde 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -360,6 +360,7 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' HTTP_HEADER_CACHE_CONTROL = 'Cache-Control' HTTP_HEADER_EXPIRES = 'Expires' HTTP_HEADER_ORIGIN = 'Origin' +HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With' HTTP_HEADER_ACCEPT = 'Accept' HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' diff --git a/tests/components/test_http.py b/tests/components/test_http.py index 42a0498ae60..28ded4d6b44 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -22,6 +22,10 @@ HA_HEADERS = { # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1', 'FD01:DB8::1'] +TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', + '2001:DB8:ABCD::1'] +UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1'] + CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE] @@ -85,10 +89,19 @@ class TestHttp: assert req.status_code == 401 + def test_access_denied_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in UNTRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 401, \ + "{} shouldn't be trusted".format(remote_addr) + def test_access_denied_with_untrusted_ip(self, caplog): """Test access with an untrusted ip address.""" - for remote_addr in ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', - '::1']: + for remote_addr in UNTRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): @@ -138,10 +151,19 @@ class TestHttp: # assert const.URL_API in logs assert API_PASSWORD not in logs - def test_access_with_trusted_ip(self, caplog): + def test_access_granted_with_x_forwarded_for(self, caplog): + """Test access denied through the X-Forwarded-For http header.""" + hass.http.use_x_forwarded_for = True + for remote_addr in TRUSTED_ADDRESSES: + req = requests.get(_url(const.URL_API), headers={ + const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr}) + + assert req.status_code == 200, \ + "{} should be trusted".format(remote_addr) + + def test_access_granted_with_trusted_ip(self, caplog): """Test access with trusted addresses.""" - for remote_addr in ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1', - '2001:DB8:ABCD::1']: + for remote_addr in TRUSTED_ADDRESSES: with patch('homeassistant.components.http.' 'HomeAssistantWSGI.get_real_ip', return_value=remote_addr): diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index f0ef9efb2d1..e709d4693c7 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -165,7 +165,8 @@ class TestCheckConfig(unittest.TestCase): self.assertDictEqual({ 'components': {'http': {'api_password': 'abc123', - 'server_port': 8123}}, + 'server_port': 8123, + 'use_x_forwarded_for': False}}, 'except': {}, 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, 'secrets': {'http_pw': 'abc123'}, From 44bc057fdba7373360ed2867d805b9f03a018ca1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Nov 2016 21:34:40 -0800 Subject: [PATCH 30/30] Version bump to 0.32.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d0fc145edde..b9971f63750 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 32 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)