From 3a00c3de64b6578684b02b2ca0efbb8b34cd192f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Jan 2018 11:28:39 -0800 Subject: [PATCH 01/15] Version bump to 0.60.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd15e1fb75d..b4fc8061d87 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 60 -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 5820bab9b1e9cc532d84dbd277effd328b70cc23 Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Fri, 22 Dec 2017 01:28:51 -0800 Subject: [PATCH 02/15] Fix async IO in Sesame lock component. (#11054) * Call update on Sesame devices to cache initial state * Switch to using async_add_devices * Fix line length * Fix Lint errors * Fix more Lint errors * Cache pysesame properties * Updates from CR feedback --- homeassistant/components/lock/sesame.py | 36 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 02b049618d2..5bc40435486 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -25,46 +25,53 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument -def setup_platform(hass, config: ConfigType, - add_devices: Callable[[list], None], discovery_info=None): +def setup_platform( + hass, config: ConfigType, + add_devices: Callable[[list], None], discovery_info=None): """Set up the Sesame platform.""" import pysesame email = config.get(CONF_EMAIL) password = config.get(CONF_PASSWORD) - add_devices([SesameDevice(sesame) for - sesame in pysesame.get_sesames(email, password)]) + add_devices([SesameDevice(sesame) for sesame in + pysesame.get_sesames(email, password)], + update_before_add=True) class SesameDevice(LockDevice): """Representation of a Sesame device.""" - _sesame = None - def __init__(self, sesame: object) -> None: """Initialize the Sesame device.""" self._sesame = sesame + # Cached properties from pysesame object. + self._device_id = None + self._nickname = None + self._is_unlocked = False + self._api_enabled = False + self._battery = -1 + @property def name(self) -> str: """Return the name of the device.""" - return self._sesame.nickname + return self._nickname @property def available(self) -> bool: """Return True if entity is available.""" - return self._sesame.api_enabled + return self._api_enabled @property def is_locked(self) -> bool: """Return True if the device is currently locked, else False.""" - return not self._sesame.is_unlocked + return not self._is_unlocked @property def state(self) -> str: """Get the state of the device.""" - if self._sesame.is_unlocked: + if self._is_unlocked: return STATE_UNLOCKED return STATE_LOCKED @@ -79,11 +86,16 @@ class SesameDevice(LockDevice): def update(self) -> None: """Update the internal state of the device.""" self._sesame.update_state() + self._nickname = self._sesame.nickname + self._api_enabled = self._sesame.api_enabled + self._is_unlocked = self._sesame.is_unlocked + self._device_id = self._sesame.device_id + self._battery = self._sesame.battery @property def device_state_attributes(self) -> dict: """Return the state attributes.""" attributes = {} - attributes[ATTR_DEVICE_ID] = self._sesame.device_id - attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery + attributes[ATTR_DEVICE_ID] = self._device_id + attributes[ATTR_BATTERY_LEVEL] = self._battery return attributes From e81d17dae079b4408665ec7ae4409a1cae96a02e Mon Sep 17 00:00:00 2001 From: maxlaverse Date: Sun, 17 Dec 2017 20:53:40 +0100 Subject: [PATCH 03/15] Fix webdav calendar schema (#11185) --- homeassistant/components/calendar/caldav.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 1647b9522b8..f1cc0f12bd8 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -29,7 +29,8 @@ CONF_ALL_DAY = 'all_day' CONF_SEARCH = 'search' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_URL): vol.Url, + # pylint: disable=no-value-for-parameter + vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, vol.Schema([ cv.string From 233026884278aaf2b7ac9a5314d7b463192d8530 Mon Sep 17 00:00:00 2001 From: Janne Grunau Date: Wed, 20 Dec 2017 00:38:59 +0100 Subject: [PATCH 04/15] homematic: add username and password to interface config schema (#11214) Fixes #11191, the json-rpc name resolving method requires user account and password. --- homeassistant/components/homematic/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index a11c8c0f22c..409f2a76fe8 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -169,6 +169,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), + 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, vol.Optional(CONF_CALLBACK_PORT): cv.port, }}, From 9055922153eaca74ac3e524fab197bd73d6078f5 Mon Sep 17 00:00:00 2001 From: Daniel Watkins Date: Fri, 22 Dec 2017 12:38:00 -0500 Subject: [PATCH 05/15] Fix webostv select source (#11227) * Fix reuse of variable name This should fix #11224. * Add tests for LgWebOSDevice.select_source --- .../components/media_player/webostv.py | 18 +++--- tests/components/media_player/test_webostv.py | 60 +++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 tests/components/media_player/test_webostv.py diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 0abdb90e67a..9d3e0b90fa4 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -322,17 +322,17 @@ class LgWebOSDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - source = self._source_list.get(source) - if source is None: + source_dict = self._source_list.get(source) + if source_dict is None: _LOGGER.warning("Source %s not found for %s", source, self.name) return - self._current_source_id = self._source_list[source]['id'] - if source.get('title'): - self._current_source = self._source_list[source]['title'] - self._client.launch_app(self._source_list[source]['id']) - elif source.get('label'): - self._current_source = self._source_list[source]['label'] - self._client.set_input(self._source_list[source]['id']) + self._current_source_id = source_dict['id'] + if source_dict.get('title'): + self._current_source = source_dict['title'] + self._client.launch_app(source_dict['id']) + elif source_dict.get('label'): + self._current_source = source_dict['label'] + self._client.set_input(source_dict['id']) def media_play(self): """Send play command.""" diff --git a/tests/components/media_player/test_webostv.py b/tests/components/media_player/test_webostv.py new file mode 100644 index 00000000000..8017ad6cd54 --- /dev/null +++ b/tests/components/media_player/test_webostv.py @@ -0,0 +1,60 @@ +"""The tests for the LG webOS media player platform.""" +import unittest +from unittest import mock + +from homeassistant.components.media_player import webostv + + +class FakeLgWebOSDevice(webostv.LgWebOSDevice): + """A fake device without the client setup required for the real one.""" + + def __init__(self, *args, **kwargs): + """Initialise parameters needed for tests with fake values.""" + self._source_list = {} + self._client = mock.MagicMock() + self._name = 'fake_device' + self._current_source = None + + +class TestLgWebOSDevice(unittest.TestCase): + """Test the LgWebOSDevice class.""" + + def setUp(self): + """Configure a fake device for each test.""" + self.device = FakeLgWebOSDevice() + + def test_select_source_with_empty_source_list(self): + """Ensure we don't call client methods when we don't have sources.""" + self.device.select_source('nonexistent') + assert 0 == self.device._client.launch_app.call_count + assert 0 == self.device._client.set_input.call_count + + def test_select_source_with_titled_entry(self): + """Test that a titled source is treated as an app.""" + self.device._source_list = { + 'existent': { + 'id': 'existent_id', + 'title': 'existent_title', + }, + } + + self.device.select_source('existent') + + assert 'existent_title' == self.device._current_source + assert [mock.call('existent_id')] == ( + self.device._client.launch_app.call_args_list) + + def test_select_source_with_labelled_entry(self): + """Test that a labelled source is treated as an input source.""" + self.device._source_list = { + 'existent': { + 'id': 'existent_id', + 'label': 'existent_label', + }, + } + + self.device.select_source('existent') + + assert 'existent_label' == self.device._current_source + assert [mock.call('existent_id')] == ( + self.device._client.set_input.call_args_list) From 794cfb79769d0645fa000547cca964a8be7d589f Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 20 Dec 2017 14:58:22 -0800 Subject: [PATCH 06/15] Fix detection of if a negative node is in use (#11255) * Fix detection of if a negative node is in use Fix a problem where every negative node gets detected as in-use. Code was not checking the correct property. * Allow protected access --- homeassistant/components/binary_sensor/isy994.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a5b61c9ffed..247ea0b231a 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -165,7 +165,8 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): """ self._negative_node = child - if not _is_val_unknown(self._negative_node): + # pylint: disable=protected-access + if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is # in use for this device. Therefore, we cannot determine the state # of the sensor until we receive our first ON event. From 889eef78e4000eaafae552f7035a1188b0cd22e3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 20 Dec 2017 23:59:11 +0100 Subject: [PATCH 07/15] Bugfix homematic available modus (#11256) --- homeassistant/components/homematic/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 409f2a76fe8..46f25e4e05f 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -749,10 +749,6 @@ class HMDevice(Entity): """Return device specific state attributes.""" attr = {} - # No data available - if not self.available: - return attr - # Generate a dictionary with attributes for node, data in HM_ATTRIBUTE_SUPPORT.items(): # Is an attribute and exists for this object @@ -808,6 +804,9 @@ class HMDevice(Entity): if attribute == 'UNREACH': self._available = bool(value) has_changed = True + elif not self.available: + self._available = False + has_changed = True # If it has changed data point, update HASS if has_changed: From 5a469f4d4bc059d8f0b750fa58239f51ec271873 Mon Sep 17 00:00:00 2001 From: Andrea Campi Date: Sun, 24 Dec 2017 00:12:54 +0000 Subject: [PATCH 08/15] Support multiple Hue bridges with lights of the same id (#11259) * Improve support for multiple Hue bridges with lights that have the same id. The old code pre-refactoring kept a per-bridge list of lights in a closure; my refactoring moved that to hass.data, which is convenient but caused them to conflict with each other. Fixes #11183 * Update test_hue.py --- homeassistant/components/hue.py | 2 + homeassistant/components/light/hue.py | 34 ++----- tests/components/light/test_hue.py | 140 +++++++++++++++++++------- 3 files changed, 113 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index 3dad4429b53..6147f706658 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -160,6 +160,8 @@ class HueBridge(object): self.allow_hue_groups = allow_hue_groups self.bridge = None + self.lights = {} + self.lightgroups = {} self.configured = False self.config_request_id = None diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index a454143bcd2..f5c910ea116 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -31,10 +31,6 @@ DEPENDENCIES = ['hue'] _LOGGER = logging.getLogger(__name__) -DATA_KEY = 'hue_lights' -DATA_LIGHTS = 'lights' -DATA_LIGHTGROUPS = 'lightgroups' - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -93,8 +89,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None or 'bridge_id' not in discovery_info: return - setup_data(hass) - if config is not None and len(config) > 0: # Legacy configuration, will be removed in 0.60 config_str = yaml.dump([config]) @@ -110,12 +104,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): unthrottled_update_lights(hass, bridge, add_devices) -def setup_data(hass): - """Initialize internal data. Useful from tests.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {DATA_LIGHTS: {}, DATA_LIGHTGROUPS: {}} - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_lights(hass, bridge, add_devices): """Update the Hue light objects with latest info from the bridge.""" @@ -176,18 +164,17 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb): new_lights = [] - lights = hass.data[DATA_KEY][DATA_LIGHTS] for light_id, info in api_lights.items(): - if light_id not in lights: - lights[light_id] = HueLight( + if light_id not in bridge.lights: + bridge.lights[light_id] = HueLight( int(light_id), info, bridge, update_lights_cb, bridge_type, bridge.allow_unreachable, bridge.allow_in_emulated_hue) - new_lights.append(lights[light_id]) + new_lights.append(bridge.lights[light_id]) else: - lights[light_id].info = info - lights[light_id].schedule_update_ha_state() + bridge.lights[light_id].info = info + bridge.lights[light_id].schedule_update_ha_state() return new_lights @@ -202,23 +189,22 @@ def process_groups(hass, api, bridge, bridge_type, update_lights_cb): new_lights = [] - groups = hass.data[DATA_KEY][DATA_LIGHTGROUPS] for lightgroup_id, info in api_groups.items(): if 'state' not in info: _LOGGER.warning('Group info does not contain state. ' 'Please update your hub.') return [] - if lightgroup_id not in groups: - groups[lightgroup_id] = HueLight( + if lightgroup_id not in bridge.lightgroups: + bridge.lightgroups[lightgroup_id] = HueLight( int(lightgroup_id), info, bridge, update_lights_cb, bridge_type, bridge.allow_unreachable, bridge.allow_in_emulated_hue, True) - new_lights.append(groups[lightgroup_id]) + new_lights.append(bridge.lightgroups[lightgroup_id]) else: - groups[lightgroup_id].info = info - groups[lightgroup_id].schedule_update_ha_state() + bridge.lightgroups[lightgroup_id].info = info + bridge.lightgroups[lightgroup_id].schedule_update_ha_state() return new_lights diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 5e5bd4f6c7f..7955cecba04 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -36,27 +36,45 @@ class TestSetup(unittest.TestCase): self.mock_lights = [] self.mock_groups = [] self.mock_add_devices = MagicMock() - hue_light.setup_data(self.hass) def setup_mocks_for_process_lights(self): """Set up all mocks for process_lights tests.""" - self.mock_bridge = MagicMock() + self.mock_bridge = self.create_mock_bridge('host') self.mock_api = MagicMock() self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api self.mock_bridge_type = MagicMock() - hue_light.setup_data(self.hass) def setup_mocks_for_process_groups(self): """Set up all mocks for process_groups tests.""" - self.mock_bridge = MagicMock() + self.mock_bridge = self.create_mock_bridge('host') self.mock_bridge.get_group.return_value = { 'name': 'Group 0', 'state': {'any_on': True}} + self.mock_api = MagicMock() self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api + self.mock_bridge_type = MagicMock() - hue_light.setup_data(self.hass) + + def create_mock_bridge(self, host, allow_hue_groups=True): + """Return a mock HueBridge with reasonable defaults.""" + mock_bridge = MagicMock() + mock_bridge.host = host + mock_bridge.allow_hue_groups = allow_hue_groups + mock_bridge.lights = {} + mock_bridge.lightgroups = {} + return mock_bridge + + def create_mock_lights(self, lights): + """Return a dict suitable for mocking api.get('lights').""" + mock_bridge_lights = lights + + for light_id, info in mock_bridge_lights.items(): + if 'state' not in info: + info['state'] = {'on': False} + + return mock_bridge_lights def test_setup_platform_no_discovery_info(self): """Test setup_platform without discovery info.""" @@ -211,6 +229,70 @@ class TestSetup(unittest.TestCase): self.mock_add_devices.assert_called_once_with( self.mock_lights) + @MockDependency('phue') + def test_update_lights_with_two_bridges(self, mock_phue): + """Test the update_lights function with two bridges.""" + self.setup_mocks_for_update_lights() + + mock_bridge_one = self.create_mock_bridge('one', False) + mock_bridge_one_lights = self.create_mock_lights( + {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}}) + + mock_bridge_two = self.create_mock_bridge('two', False) + mock_bridge_two_lights = self.create_mock_lights( + {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) + + with patch('homeassistant.components.light.hue.get_bridge_type', + return_value=self.mock_bridge_type): + with patch('homeassistant.components.light.hue.HueLight.' + 'schedule_update_ha_state'): + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_one_lights + with patch.object(mock_bridge_one, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_one, self.mock_add_devices) + + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_two_lights + with patch.object(mock_bridge_two, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_two, self.mock_add_devices) + + self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2]) + self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3]) + + self.assertEquals(len(self.mock_add_devices.mock_calls), 2) + + # first call + name, args, kwargs = self.mock_add_devices.mock_calls[0] + self.assertEquals(len(args), 1) + self.assertEquals(len(kwargs), 0) + + # one argument, a list of lights in bridge one; each of them is an + # object of type HueLight so we can't straight up compare them + lights = args[0] + self.assertEquals( + lights[0].unique_id, + '{}.b1l1.Light.1'.format(hue_light.HueLight)) + self.assertEquals( + lights[1].unique_id, + '{}.b1l2.Light.2'.format(hue_light.HueLight)) + + # second call works the same + name, args, kwargs = self.mock_add_devices.mock_calls[1] + self.assertEquals(len(args), 1) + self.assertEquals(len(kwargs), 0) + + lights = args[0] + self.assertEquals( + lights[0].unique_id, + '{}.b2l1.Light.1'.format(hue_light.HueLight)) + self.assertEquals( + lights[1].unique_id, + '{}.b2l3.Light.3'.format(hue_light.HueLight)) + def test_process_lights_api_error(self): """Test the process_lights function when the bridge errors out.""" self.setup_mocks_for_process_lights() @@ -221,9 +303,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]) + self.assertEquals(self.mock_bridge.lights, {}) def test_process_lights_no_lights(self): """Test the process_lights function when bridge returns no lights.""" @@ -234,9 +314,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]) + self.assertEquals(self.mock_bridge.lights, {}) @patch('homeassistant.components.light.hue.HueLight') def test_process_lights_some_lights(self, mock_hue_light): @@ -260,9 +338,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue), ]) - self.assertEquals( - len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]), - 2) + self.assertEquals(len(self.mock_bridge.lights), 2) @patch('homeassistant.components.light.hue.HueLight') def test_process_lights_new_light(self, mock_hue_light): @@ -274,8 +350,7 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_lights() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTS][1] = MagicMock() + self.mock_bridge.lights = {1: MagicMock()} ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, @@ -288,11 +363,9 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue), ]) - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS][ - 1].schedule_update_ha_state.assert_called_once_with() - self.assertEquals( - len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]), - 2) + self.assertEquals(len(self.mock_bridge.lights), 2) + self.mock_bridge.lights[1]\ + .schedule_update_ha_state.assert_called_once_with() def test_process_groups_api_error(self): """Test the process_groups function when the bridge errors out.""" @@ -304,9 +377,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]) + self.assertEquals(self.mock_bridge.lightgroups, {}) def test_process_groups_no_state(self): """Test the process_groups function when bridge returns no status.""" @@ -318,9 +389,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]) + self.assertEquals(self.mock_bridge.lightgroups, {}) @patch('homeassistant.components.light.hue.HueLight') def test_process_groups_some_groups(self, mock_hue_light): @@ -344,10 +413,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue, True), ]) - self.assertEquals( - len(self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]), - 2) + self.assertEquals(len(self.mock_bridge.lightgroups), 2) @patch('homeassistant.components.light.hue.HueLight') def test_process_groups_new_group(self, mock_hue_light): @@ -359,8 +425,7 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_groups() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][1] = MagicMock() + self.mock_bridge.lightgroups = {1: MagicMock()} ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, @@ -373,12 +438,9 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue, True), ]) - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][ - 1].schedule_update_ha_state.assert_called_once_with() - self.assertEquals( - len(self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]), - 2) + self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.mock_bridge.lightgroups[1]\ + .schedule_update_ha_state.assert_called_once_with() class TestHueLight(unittest.TestCase): From 486263fff771a5f647d70d062e67022ae5031378 Mon Sep 17 00:00:00 2001 From: CTLS Date: Wed, 20 Dec 2017 23:29:42 -0600 Subject: [PATCH 09/15] Fix inverted sensors on the concord232 binary sensor component (#11261) * Fix inverted sensors on the concord232 binary sensor component * Changed from == Tripped to != Normal --- homeassistant/components/binary_sensor/concord232.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index 7ba88f76611..d689f030d8a 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -118,7 +118,7 @@ class Concord232ZoneSensor(BinarySensorDevice): def is_on(self): """Return true if the binary sensor is on.""" # True means "faulted" or "open" or "abnormal state" - return bool(self._zone['state'] == 'Normal') + return bool(self._zone['state'] != 'Normal') def update(self): """Get updated stats from API.""" From f6a67d99e4b3b81b63944c97823ceb5d7fb34006 Mon Sep 17 00:00:00 2001 From: Zio Tibia <4745882+ziotibia81@users.noreply.github.com> Date: Thu, 21 Dec 2017 14:24:19 +0100 Subject: [PATCH 10/15] Fix handling zero values for state_on/state_off (#11264) --- homeassistant/components/switch/modbus.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index c731b336dfb..211ff54d5a4 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -141,10 +141,17 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._verify_register = ( verify_register if verify_register else self._register) self._register_type = register_type - self._state_on = ( - state_on if state_on else self._command_on) - self._state_off = ( - state_off if state_off else self._command_off) + + if state_on is not None: + self._state_on = state_on + else: + self._state_on = self._command_on + + if state_off is not None: + self._state_off = state_off + else: + self._state_off = self._command_off + self._is_on = None def turn_on(self, **kwargs): From a27b37be590cc40d194195615783282abc1056f3 Mon Sep 17 00:00:00 2001 From: maxlaverse Date: Fri, 22 Dec 2017 14:08:34 +0100 Subject: [PATCH 11/15] Fix allday events in custom_calendars (#11272) --- homeassistant/components/calendar/caldav.py | 3 +-- tests/components/calendar/test_caldav.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index f1cc0f12bd8..36894dcab61 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -25,7 +25,6 @@ CONF_DEVICE_ID = 'device_id' CONF_CALENDARS = 'calendars' CONF_CUSTOM_CALENDARS = 'custom_calendars' CONF_CALENDAR = 'calendar' -CONF_ALL_DAY = 'all_day' CONF_SEARCH = 'search' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -89,7 +88,7 @@ def setup_platform(hass, config, add_devices, disc_info=None): WebDavCalendarEventDevice(hass, device_data, calendar, - cust_calendar.get(CONF_ALL_DAY), + True, cust_calendar.get(CONF_SEARCH)) ) diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index 8a44f96fe87..7234d40c410 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -121,8 +121,10 @@ class TestComponentsWebDavCalendar(unittest.TestCase): assert len(devices) == 2 assert devices[0].name == "First" assert devices[0].dev_id == "First" + self.assertFalse(devices[0].data.include_all_day) assert devices[1].name == "Second" assert devices[1].dev_id == "Second" + self.assertFalse(devices[1].data.include_all_day) caldav.setup_platform(self.hass, { @@ -167,6 +169,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): assert len(devices) == 1 assert devices[0].name == "HomeOffice" assert devices[0].dev_id == "Second HomeOffice" + self.assertTrue(devices[0].data.include_all_day) caldav.setup_platform(self.hass, { From 5d6455415e57ddd4447727712972347754e544bd Mon Sep 17 00:00:00 2001 From: Bob Anderson Date: Mon, 25 Dec 2017 04:26:22 -0800 Subject: [PATCH 12/15] Fix unpredictable entity names in concord232 binary_sensor (#11292) --- homeassistant/components/binary_sensor/concord232.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index d689f030d8a..73cf77f2b93 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False + # The order of zones returned by client.list_zones() can vary. + # When the zones are not named, this can result in the same entity + # name mapping to different sensors in an unpredictable way. Sort + # the zones by zone number to prevent this. + + client.zones.sort(key=lambda zone: zone['number']) + for zone in client.zones: _LOGGER.info("Loading Zone found: %s", zone['name']) if zone['number'] not in exclude: From d3ed6d52422754450f186d4081b5d0c966967192 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 27 Dec 2017 00:23:21 -0800 Subject: [PATCH 13/15] Fix leak sensors always showing Unknown until Wet (#11313) Leak sensors were using the "wet" node as a negative node, which prevented them from ever gettng a Dry status unless the user pressed the button on the hardware after every Hass reboot. This change ignores the Wet node, as it is just always the exact inverse of the Dry node. We don't need to watch both. --- homeassistant/components/binary_sensor/isy994.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 247ea0b231a..e3b5d7b4369 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -62,10 +62,9 @@ def setup_platform(hass, config: ConfigType, node.nid, node.parent_nid) else: device_type = _detect_device_type(node) - if device_type in ['moisture', 'opening']: - subnode_id = int(node.nid[-1]) - # Leak and door/window sensors work the same way with negative - # nodes and heartbeat nodes + subnode_id = int(node.nid[-1]) + if device_type == 'opening': + # Door/window sensors use an optional "negative" node if subnode_id == 4: # Subnode 4 is the heartbeat node, which we will represent # as a separate binary_sensor @@ -74,6 +73,14 @@ def setup_platform(hass, config: ConfigType, devices.append(device) elif subnode_id == 2: parent_device.add_negative_node(node) + elif device_type == 'moisture': + # Moisure nodes have a subnode 2, but we ignore it because it's + # just the inverse of the primary node. + if subnode_id == 4: + # Heartbeat node + device = ISYBinarySensorHeartbeat(node, parent_device) + parent_device.add_heartbeat_device(device) + devices.append(device) else: # We don't yet have any special logic for other sensor types, # so add the nodes as individual devices From e0a1b872967476b6836ac88052e3a6a5e258a733 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 29 Dec 2017 18:44:06 +0100 Subject: [PATCH 14/15] Don't block on sevice call for alexa (#11358) * Don't block on sevice call for alexa * fix tests --- homeassistant/components/alexa/smart_home.py | 76 ++++++++++---------- tests/components/alexa/test_smart_home.py | 26 +++++++ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 3c8e9f5d21c..58888b19af7 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -245,7 +245,7 @@ def async_api_turn_on(hass, config, request, entity): yield from hass.services.async_call(domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -261,7 +261,7 @@ def async_api_turn_off(hass, config, request, entity): yield from hass.services.async_call(domain, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -276,7 +276,7 @@ def async_api_set_brightness(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -300,7 +300,7 @@ def async_api_adjust_brightness(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -321,14 +321,14 @@ def async_api_set_color(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb, - }, blocking=True) + }, blocking=False) else: xyz = color_util.color_RGB_to_xy(*rgb) yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_XY_COLOR: (xyz[0], xyz[1]), light.ATTR_BRIGHTNESS: xyz[2], - }, blocking=True) + }, blocking=False) return api_message(request) @@ -343,7 +343,7 @@ def async_api_set_color_temperature(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -361,7 +361,7 @@ def async_api_decrease_color_temp(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -379,7 +379,7 @@ def async_api_increase_color_temp(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -391,7 +391,7 @@ def async_api_activate(hass, config, request, entity): """Process a activate request.""" yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -421,8 +421,8 @@ def async_api_set_percentage(hass, config, request, entity): service = SERVICE_SET_COVER_POSITION data[cover.ATTR_POSITION] = percentage - yield from hass.services.async_call(entity.domain, service, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, service, data, blocking=False) return api_message(request) @@ -469,8 +469,8 @@ def async_api_adjust_percentage(hass, config, request, entity): data[cover.ATTR_POSITION] = max(0, percentage_delta + current) - yield from hass.services.async_call(entity.domain, service, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, service, data, blocking=False) return api_message(request) @@ -482,7 +482,7 @@ def async_api_lock(hass, config, request, entity): """Process a lock request.""" yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -495,7 +495,7 @@ def async_api_unlock(hass, config, request, entity): """Process a unlock request.""" yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -512,8 +512,9 @@ def async_api_set_volume(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, } - yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_VOLUME_SET, + data, blocking=False) return api_message(request) @@ -540,9 +541,9 @@ def async_api_adjust_volume(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, } - yield from hass.services.async_call(entity.domain, - media_player.SERVICE_VOLUME_SET, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_SET, + data, blocking=False) return api_message(request) @@ -559,9 +560,9 @@ def async_api_set_mute(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_MUTED: mute, } - yield from hass.services.async_call(entity.domain, - media_player.SERVICE_VOLUME_MUTE, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_MUTE, + data, blocking=False) return api_message(request) @@ -575,8 +576,9 @@ def async_api_play(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PLAY, + data, blocking=False) return api_message(request) @@ -590,8 +592,9 @@ def async_api_pause(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PAUSE, + data, blocking=False) return api_message(request) @@ -605,8 +608,9 @@ def async_api_stop(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_STOP, + data, blocking=False) return api_message(request) @@ -620,9 +624,9 @@ def async_api_next(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, - SERVICE_MEDIA_NEXT_TRACK, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_NEXT_TRACK, + data, blocking=False) return api_message(request) @@ -636,8 +640,8 @@ def async_api_previous(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, - SERVICE_MEDIA_PREVIOUS_TRACK, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK, + data, blocking=False) return api_message(request) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 55a412af1fd..a0876dea5df 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -346,6 +346,7 @@ def test_exclude_filters(hass): )) msg = yield from smart_home.async_handle_message(hass, config, request) + yield from hass.async_block_till_done() msg = msg['event'] @@ -378,6 +379,7 @@ def test_include_filters(hass): )) msg = yield from smart_home.async_handle_message(hass, config, request) + yield from hass.async_block_till_done() msg = msg['event'] @@ -393,6 +395,7 @@ def test_api_entity_not_exists(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -442,6 +445,7 @@ def test_api_turn_on(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -475,6 +479,7 @@ def test_api_turn_off(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -501,6 +506,7 @@ def test_api_set_brightness(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -532,6 +538,7 @@ def test_api_adjust_brightness(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -566,6 +573,7 @@ def test_api_set_color_rgb(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -600,6 +608,7 @@ def test_api_set_color_xy(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -629,6 +638,7 @@ def test_api_set_color_temperature(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -658,6 +668,7 @@ def test_api_decrease_color_temp(hass, result, initial): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -687,6 +698,7 @@ def test_api_increase_color_temp(hass, result, initial): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -714,6 +726,7 @@ def test_api_activate(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -740,6 +753,7 @@ def test_api_set_percentage_fan(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -769,6 +783,7 @@ def test_api_set_percentage_cover(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -800,6 +815,7 @@ def test_api_adjust_percentage_fan(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -832,6 +848,7 @@ def test_api_adjust_percentage_cover(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -859,6 +876,7 @@ def test_api_lock(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -885,6 +903,7 @@ def test_api_play(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -911,6 +930,7 @@ def test_api_pause(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -937,6 +957,7 @@ def test_api_stop(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -963,6 +984,7 @@ def test_api_next(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -989,6 +1011,7 @@ def test_api_previous(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1017,6 +1040,7 @@ def test_api_set_volume(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1048,6 +1072,7 @@ def test_api_adjust_volume(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1077,6 +1102,7 @@ def test_api_mute(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] From b61197196e39454db4c0fd627d15e5bff4ae93f8 Mon Sep 17 00:00:00 2001 From: Matt N Date: Fri, 5 Jan 2018 14:28:03 -0500 Subject: [PATCH 15/15] iOS 10 should be served javascript_version:es5 (#11387) * iOS 10 should be served javascript_version:es5 Fixes #11234 * Update min Safari version to 12 --- homeassistant/components/frontend/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index cd206135dde..2bbb4dbe405 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -582,9 +582,10 @@ def _is_latest(js_option, request): from user_agents import parse useragent = parse(request.headers.get('User-Agent')) - # on iOS every browser is a Safari which we support from version 10. + # on iOS every browser is a Safari which we support from version 11. if useragent.os.family == 'iOS': - return useragent.os.version[0] >= 10 + # Was >= 10, temp setting it to 12 to work around issue #11387 + return useragent.os.version[0] >= 12 family_min_version = { 'Chrome': 50, # Probably can reduce this