From a78361341ebe42e354c5e73d9a36c54ea174f7af Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 26 Jun 2019 18:03:11 -0500 Subject: [PATCH 01/16] Fix life360 exception when no location provided (#24777) --- homeassistant/components/life360/device_tracker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index cf69d8b656a..4329f2a162b 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -177,8 +177,11 @@ class Life360Scanner: return prev_seen def _update_member(self, member, dev_id): - loc = member.get('location', {}) - last_seen = _utc_from_ts(loc.get('timestamp')) + loc = member.get('location') + try: + last_seen = _utc_from_ts(loc.get('timestamp')) + except AttributeError: + last_seen = None prev_seen = self._prev_seen(dev_id, last_seen) if not loc: From dce667fa0781b96ffc26f626c5055e1c339c30da Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 26 Jun 2019 19:14:01 -0400 Subject: [PATCH 02/16] Pubnub to 1.0.8 (#24781) --- homeassistant/components/wink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index a878b084169..cddfdc5dc9c 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -3,7 +3,7 @@ "name": "Wink", "documentation": "https://www.home-assistant.io/components/wink", "requirements": [ - "pubnubsub-handler==1.0.7", + "pubnubsub-handler==1.0.8", "python-wink==1.10.5" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index fd6af461a0c..e763a02b754 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -942,7 +942,7 @@ psutil==5.6.2 ptvsd==4.2.8 # homeassistant.components.wink -pubnubsub-handler==1.0.7 +pubnubsub-handler==1.0.8 # homeassistant.components.pushbullet pushbullet.py==0.11.0 From c1d0ac7b9dd6e29576b54be4d97e9d219bc93531 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jun 2019 20:24:20 -0700 Subject: [PATCH 03/16] Catch uncaught Alexa error (#24785) --- homeassistant/components/cloud/http_api.py | 4 +-- tests/components/cloud/test_http_api.py | 40 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 0cd08dd3d5f..e5f00873aab 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -23,7 +23,7 @@ from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks, - InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE) + InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE, RequireRelink) _LOGGER = logging.getLogger(__name__) @@ -388,7 +388,7 @@ async def websocket_update_prefs(hass, connection, msg): connection.send_error(msg['id'], 'alexa_timeout', 'Timeout validating Alexa access token.') return - except alexa_errors.NoTokenAvailable: + except (alexa_errors.NoTokenAvailable, RequireRelink): connection.send_error( msg['id'], 'alexa_relink', 'Please go to the Alexa app and re-link the Home Assistant ' diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index bc60568f0d4..442643672eb 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -12,7 +12,7 @@ from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.cloud.const import ( PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, - DOMAIN) + DOMAIN, RequireRelink) from homeassistant.components.google_assistant.helpers import ( GoogleEntity) from homeassistant.components.alexa.entities import LightCapabilities @@ -527,6 +527,44 @@ async def test_websocket_update_preferences(hass, hass_ws_client, assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == '1234' +async def test_websocket_update_preferences_require_relink( + hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login): + """Test updating preference requires relink.""" + client = await hass_ws_client(hass) + + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_get_access_token', + side_effect=RequireRelink): + await client.send_json({ + 'id': 5, + 'type': 'cloud/update_prefs', + 'alexa_report_state': True, + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink' + + +async def test_websocket_update_preferences_no_token( + hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login): + """Test updating preference no token available.""" + client = await hass_ws_client(hass) + + with patch('homeassistant.components.cloud.alexa_config.AlexaConfig' + '.async_get_access_token', + side_effect=alexa_errors.NoTokenAvailable): + await client.send_json({ + 'id': 5, + 'type': 'cloud/update_prefs', + 'alexa_report_state': True, + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 'alexa_relink' + + async def test_enabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_login): """Test we call right code to enable webhooks.""" From 01b6830fd2e9680807297a02e722ce345bd42bd9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jun 2019 21:25:33 -0700 Subject: [PATCH 04/16] Bumped version to 0.95.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6cf77275f6e..de5dac85c8f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 204dd77404b0c301c9124fd79aca36cd564333d2 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 28 Jun 2019 05:16:46 +0200 Subject: [PATCH 05/16] Fix netatmo weatherstation setup error (#24788) * Check if station data exists and reduce calls * Fix module names list * Add warning * Remove dead code --- homeassistant/components/netatmo/sensor.py | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 48d82eca2f0..9902fedde8f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -99,6 +99,12 @@ MODULE_TYPE_RAIN = 'NAModule3' MODULE_TYPE_INDOOR = 'NAModule4' +NETATMO_DEVICE_TYPES = { + 'WeatherStationData': 'weather station', + 'HomeCoachData': 'home coach' +} + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" dev = [] @@ -132,7 +138,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import pyatmo for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: - data = NetatmoData(auth, data_class, config.get(CONF_STATION)) + try: + data = NetatmoData(auth, data_class, config.get(CONF_STATION)) + except pyatmo.NoDevice: + _LOGGER.warning( + "No %s devices found", + NETATMO_DEVICE_TYPES[data_class.__name__] + ) + continue # Test if manually configured if CONF_MODULES in config: module_items = config[CONF_MODULES].items() @@ -157,18 +170,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def find_devices(data): """Find all devices.""" dev = [] - not_handled = [] - for module_name in data.get_module_names(): - if (module_name not in data.get_module_names() - and module_name not in not_handled): - not_handled.append(not_handled) - continue + module_names = data.get_module_names() + for module_name in module_names: for condition in data.station_data.monitoredConditions(module_name): dev.append(NetatmoSensor( data, module_name, condition.lower(), data.station)) - - for module_name in not_handled: - _LOGGER.error('Module name: "%s" not found', module_name) return dev @@ -187,12 +193,11 @@ class NetatmoSensor(Entity): self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._module_type = self.netatmo_data. \ - station_data.moduleByName(module=module_name)['type'] - module_id = self.netatmo_data. \ - station_data.moduleByName(station=self.station_name, - module=module_name)['_id'] - self._unique_id = '{}-{}'.format(module_id, self.type) + module = self.netatmo_data.station_data.moduleByName( + station=self.station_name, module=module_name + ) + self._module_type = module['type'] + self._unique_id = '{}-{}'.format(module['_id'], self.type) @property def name(self): @@ -515,15 +520,14 @@ class NetatmoData: self.auth = auth self.data_class = data_class self.data = {} - self.station_data = None + self.station_data = self.data_class(self.auth) self.station = station self._next_update = time() self._update_in_progress = threading.Lock() def get_module_names(self): """Return all module available on the API as a list.""" - self.update() - return self.data.keys() + return self.station_data.modulesNamesList() def update(self): """Call the Netatmo API to update the data. From 1f5e2fa3ce50f156fd629b035f23a8dd3352f8e1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 27 Jun 2019 18:26:13 +0200 Subject: [PATCH 06/16] Update azure-pipelines-release.yml for Azure Pipelines (#24800) --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index d6395dad5ac..af737290143 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -8,7 +8,7 @@ trigger: pr: none variables: - name: versionBuilder - value: '4.2' + value: '4.5' - group: docker - group: github - group: twine From e39f0f3e259aa67f9031c4b19b7bd6a67e7216c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Jun 2019 12:17:42 -0700 Subject: [PATCH 07/16] Make sure entity config is never none (#24801) --- homeassistant/components/google_assistant/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 95528eea3ca..8f6d441d498 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -37,7 +37,7 @@ class GoogleConfig(AbstractConfig): @property def entity_config(self): """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG, {}) + return self._config.get(CONF_ENTITY_CONFIG) or {} @property def secure_devices_pin(self): From 1990df63aaa249dd3f427ab5ff6b43d488429b5a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 27 Jun 2019 15:28:56 -0400 Subject: [PATCH 08/16] Bump ZHA quirks module (#24802) * bump quirks version * bump version - mija magnet --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 15fcf38100f..7f067353b37 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.8.2", - "zha-quirks==0.0.15", + "zha-quirks==0.0.17", "zigpy-deconz==0.1.6", "zigpy-homeassistant==0.6.1", "zigpy-xbee-homeassistant==0.3.0" diff --git a/requirements_all.txt b/requirements_all.txt index e763a02b754..081f357b52c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1917,7 +1917,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.15 +zha-quirks==0.0.17 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From fafc68673a557d0cb75c47f928048ac99e27c2c3 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 27 Jun 2019 17:11:32 -0500 Subject: [PATCH 09/16] Fix another Life360 bug (#24805) --- homeassistant/components/life360/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 4329f2a162b..abf97ffa7b8 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -294,7 +294,6 @@ class Life360Scanner: member_id = member['id'] if member_id in members_updated: continue - members_updated.append(member_id) err_key = 'Member data' try: first = member.get('firstName') @@ -318,6 +317,7 @@ class Life360Scanner: self._ok(err_key) if include_member and sharing: + members_updated.append(member_id) self._update_member(member, dev_id) def _update_life360(self, now=None): From 2886b217ab97055edce8aa53185eb65ef66aeb5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 08:49:33 -0700 Subject: [PATCH 10/16] Fix calling empty script turn off (#24827) --- homeassistant/components/script/__init__.py | 7 ++++++- tests/components/script/test_init.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 36cb144fada..495df2c5e17 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -79,9 +79,14 @@ async def async_setup(hass, config): async def turn_off_service(service): """Cancel a script.""" # Stopping a script is ok to be done in parallel + scripts = await component.async_extract_from_service(service) + + if not scripts: + return + await asyncio.wait([ script.async_turn_off() for script - in await component.async_extract_from_service(service) + in scripts ]) async def toggle_service(service): diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index c2ff17d9444..9e0b751c430 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -322,3 +322,13 @@ async def test_logging_script_error(hass, caplog): assert err.value.domain == 'non' assert err.value.service == 'existing' assert 'Error executing script' in caplog.text + + +async def test_turning_no_scripts_off(hass): + """Test it is possible to turn two scripts off.""" + assert await async_setup_component(hass, 'script', {}) + + # Testing it doesn't raise + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {'entity_id': []}, blocking=True + ) From 6cbfc63311fb20d628ff32f11c2ce976abdcb50b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 08:50:23 -0700 Subject: [PATCH 11/16] Bumped version to 0.95.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de5dac85c8f..2345c5fbe63 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From cc75adfed6aae8d3915804ab90657d8efb61d892 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 20:43:57 -0700 Subject: [PATCH 12/16] Alexa sync state report (#24835) * Do a sync after changing state reporting * Fix entity config being None --- .../components/cloud/alexa_config.py | 12 +- tests/components/cloud/conftest.py | 23 ++- tests/components/cloud/test_alexa_config.py | 154 +++++++++++++++++ tests/components/cloud/test_client.py | 156 +----------------- 4 files changed, 187 insertions(+), 158 deletions(-) create mode 100644 tests/components/cloud/test_alexa_config.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index aae48df9884..a6aced474d6 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -78,7 +78,7 @@ class AlexaConfig(alexa_config.AbstractConfig): @property def entity_config(self): """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG, {}) + return self._config.get(CONF_ENTITY_CONFIG) or {} def should_expose(self, entity_id): """If an entity should be exposed.""" @@ -129,6 +129,11 @@ class AlexaConfig(alexa_config.AbstractConfig): else: await self.async_disable_proactive_mode() + # State reporting is reported as a property on entities. + # So when we change it, we need to sync all entities. + await self.async_sync_entities() + return + # If entity prefs are the same or we have filter in config.yaml, # don't sync. if (self._cur_entity_prefs is prefs.alexa_entity_configs or @@ -190,6 +195,11 @@ class AlexaConfig(alexa_config.AbstractConfig): async def async_sync_entities(self): """Sync all entities to Alexa.""" + # Remove any pending sync + if self._alexa_sync_unsub: + self._alexa_sync_unsub() + self._alexa_sync_unsub = None + to_update = [] to_remove = [] diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 87ef6809fdd..2c52c0a0a82 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,9 +1,10 @@ """Fixtures for cloud tests.""" -import pytest - from unittest.mock import patch -from homeassistant.components.cloud import prefs +import jwt +import pytest + +from homeassistant.components.cloud import const, prefs from . import mock_cloud, mock_cloud_prefs @@ -28,3 +29,19 @@ async def cloud_prefs(hass): cloud_prefs = prefs.CloudPreferences(hass) await cloud_prefs.async_initialize() return cloud_prefs + + +@pytest.fixture +async def mock_cloud_setup(hass): + """Set up the cloud.""" + await mock_cloud(hass) + + +@pytest.fixture +def mock_cloud_login(hass, mock_cloud_setup): + """Mock cloud is logged in.""" + hass.data[const.DOMAIN].id_token = jwt.encode({ + 'email': 'hello@home-assistant.io', + 'custom:sub-exp': '2018-01-03', + 'cognito:username': 'abcdefghjkl', + }, 'test') diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py new file mode 100644 index 00000000000..a51fc5b8594 --- /dev/null +++ b/tests/components/cloud/test_alexa_config.py @@ -0,0 +1,154 @@ +"""Test Alexa config.""" +import contextlib +from unittest.mock import patch + +from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config +from homeassistant.util.dt import utcnow +from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from tests.common import mock_coro, async_fire_time_changed + + +async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): + """Test Alexa config should expose using prefs.""" + entity_conf = { + 'should_expose': False + } + await cloud_prefs.async_update(alexa_entity_configs={ + 'light.kitchen': entity_conf + }) + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + + assert not conf.should_expose('light.kitchen') + entity_conf['should_expose'] = True + assert conf.should_expose('light.kitchen') + + +async def test_alexa_config_report_state(hass, cloud_prefs): + """Test Alexa config should expose using prefs.""" + conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + + assert cloud_prefs.alexa_report_state is False + assert conf.should_report_state is False + assert conf.is_reporting_states is False + + with patch.object(conf, 'async_get_access_token', + return_value=mock_coro("hello")): + await cloud_prefs.async_update(alexa_report_state=True) + await hass.async_block_till_done() + + assert cloud_prefs.alexa_report_state is True + assert conf.should_report_state is True + assert conf.is_reporting_states is True + + await cloud_prefs.async_update(alexa_report_state=False) + await hass.async_block_till_done() + + assert cloud_prefs.alexa_report_state is False + assert conf.should_report_state is False + assert conf.is_reporting_states is False + + +@contextlib.contextmanager +def patch_sync_helper(): + """Patch sync helper. + + In Py3.7 this would have been an async context manager. + """ + to_update = [] + to_remove = [] + + with patch( + 'homeassistant.components.cloud.alexa_config.SYNC_DELAY', 0 + ), patch( + 'homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper', + side_effect=mock_coro + ) as mock_helper: + yield to_update, to_remove + + actual_to_update, actual_to_remove = mock_helper.mock_calls[0][1] + to_update.extend(actual_to_update) + to_remove.extend(actual_to_remove) + + +async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): + """Test Alexa config responds to updating exposed entities.""" + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + + with patch_sync_helper() as (to_update, to_remove): + await cloud_prefs.async_update_alexa_entity_config( + entity_id='light.kitchen', should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert to_update == ['light.kitchen'] + assert to_remove == [] + + with patch_sync_helper() as (to_update, to_remove): + await cloud_prefs.async_update_alexa_entity_config( + entity_id='light.kitchen', should_expose=False + ) + await cloud_prefs.async_update_alexa_entity_config( + entity_id='binary_sensor.door', should_expose=True + ) + await cloud_prefs.async_update_alexa_entity_config( + entity_id='sensor.temp', should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert sorted(to_update) == ['binary_sensor.door', 'sensor.temp'] + assert to_remove == ['light.kitchen'] + + +async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): + """Test Alexa config responds to entity registry.""" + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) + + with patch_sync_helper() as (to_update, to_remove): + hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'create', + 'entity_id': 'light.kitchen', + }) + await hass.async_block_till_done() + + assert to_update == ['light.kitchen'] + assert to_remove == [] + + with patch_sync_helper() as (to_update, to_remove): + hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'remove', + 'entity_id': 'light.kitchen', + }) + await hass.async_block_till_done() + + assert to_update == [] + assert to_remove == ['light.kitchen'] + + with patch_sync_helper() as (to_update, to_remove): + hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { + 'action': 'update', + 'entity_id': 'light.kitchen', + }) + await hass.async_block_till_done() + + assert to_update == [] + assert to_remove == [] + + +async def test_alexa_update_report_state(hass, cloud_prefs): + """Test Alexa config responds to reporting state.""" + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + + with patch( + 'homeassistant.components.cloud.alexa_config.AlexaConfig.' + 'async_sync_entities', side_effect=mock_coro) as mock_sync, patch( + 'homeassistant.components.cloud.alexa_config.' + 'AlexaConfig.async_enable_proactive_mode', side_effect=mock_coro): + await cloud_prefs.async_update(alexa_report_state=True) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index fa42bda32db..ac3be538111 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,21 +1,16 @@ """Test the cloud.iot module.""" -import contextlib from unittest.mock import patch, MagicMock from aiohttp import web -import jwt import pytest from homeassistant.core import State from homeassistant.setup import async_setup_component -from homeassistant.components.cloud import ( - DOMAIN, ALEXA_SCHEMA, alexa_config) +from homeassistant.components.cloud import DOMAIN from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE) -from homeassistant.util.dt import utcnow -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro, async_fire_time_changed +from tests.common import mock_coro from . import mock_cloud_prefs, mock_cloud @@ -26,22 +21,6 @@ def mock_cloud_inst(): return MagicMock(subscription_expired=False) -@pytest.fixture -async def mock_cloud_setup(hass): - """Set up the cloud.""" - await mock_cloud(hass) - - -@pytest.fixture -def mock_cloud_login(hass, mock_cloud_setup): - """Mock cloud is logged in.""" - hass.data[DOMAIN].id_token = jwt.encode({ - 'email': 'hello@home-assistant.io', - 'custom:sub-exp': '2018-01-03', - 'cognito:username': 'abcdefghjkl', - }, 'test') - - async def test_handler_alexa(hass): """Test handler Alexa.""" hass.states.async_set( @@ -244,134 +223,3 @@ async def test_google_config_should_2fa( ) assert not cloud_client.google_config.should_2fa(state) - - -async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): - """Test Alexa config should expose using prefs.""" - entity_conf = { - 'should_expose': False - } - await cloud_prefs.async_update(alexa_entity_configs={ - 'light.kitchen': entity_conf - }) - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) - - assert not conf.should_expose('light.kitchen') - entity_conf['should_expose'] = True - assert conf.should_expose('light.kitchen') - - -async def test_alexa_config_report_state(hass, cloud_prefs): - """Test Alexa config should expose using prefs.""" - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) - - assert cloud_prefs.alexa_report_state is False - assert conf.should_report_state is False - assert conf.is_reporting_states is False - - with patch.object(conf, 'async_get_access_token', - return_value=mock_coro("hello")): - await cloud_prefs.async_update(alexa_report_state=True) - await hass.async_block_till_done() - - assert cloud_prefs.alexa_report_state is True - assert conf.should_report_state is True - assert conf.is_reporting_states is True - - await cloud_prefs.async_update(alexa_report_state=False) - await hass.async_block_till_done() - - assert cloud_prefs.alexa_report_state is False - assert conf.should_report_state is False - assert conf.is_reporting_states is False - - -@contextlib.contextmanager -def patch_sync_helper(): - """Patch sync helper. - - In Py3.7 this would have been an async context manager. - """ - to_update = [] - to_remove = [] - - with patch( - 'homeassistant.components.cloud.alexa_config.SYNC_DELAY', 0 - ), patch( - 'homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper', - side_effect=mock_coro - ) as mock_helper: - yield to_update, to_remove - - actual_to_update, actual_to_remove = mock_helper.mock_calls[0][1] - to_update.extend(actual_to_update) - to_remove.extend(actual_to_remove) - - -async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): - """Test Alexa config responds to updating exposed entities.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) - - with patch_sync_helper() as (to_update, to_remove): - await cloud_prefs.async_update_alexa_entity_config( - entity_id='light.kitchen', should_expose=True - ) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - await hass.async_block_till_done() - - assert to_update == ['light.kitchen'] - assert to_remove == [] - - with patch_sync_helper() as (to_update, to_remove): - await cloud_prefs.async_update_alexa_entity_config( - entity_id='light.kitchen', should_expose=False - ) - await cloud_prefs.async_update_alexa_entity_config( - entity_id='binary_sensor.door', should_expose=True - ) - await cloud_prefs.async_update_alexa_entity_config( - entity_id='sensor.temp', should_expose=True - ) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - await hass.async_block_till_done() - - assert sorted(to_update) == ['binary_sensor.door', 'sensor.temp'] - assert to_remove == ['light.kitchen'] - - -async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): - """Test Alexa config responds to entity registry.""" - alexa_config.AlexaConfig( - hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data['cloud']) - - with patch_sync_helper() as (to_update, to_remove): - hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { - 'action': 'create', - 'entity_id': 'light.kitchen', - }) - await hass.async_block_till_done() - - assert to_update == ['light.kitchen'] - assert to_remove == [] - - with patch_sync_helper() as (to_update, to_remove): - hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { - 'action': 'remove', - 'entity_id': 'light.kitchen', - }) - await hass.async_block_till_done() - - assert to_update == [] - assert to_remove == ['light.kitchen'] - - with patch_sync_helper() as (to_update, to_remove): - hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, { - 'action': 'update', - 'entity_id': 'light.kitchen', - }) - await hass.async_block_till_done() - - assert to_update == [] - assert to_remove == [] From 072879cc6e9ab0715c21840b88e1d22b552e3d85 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 20:44:23 -0700 Subject: [PATCH 13/16] Bumped version to 0.95.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2345c5fbe63..4d932481a4c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c6e8e2398c6e1bce1af66654a0b76f19b6459848 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 21 Jun 2019 21:50:25 +0200 Subject: [PATCH 14/16] Improve autodiscovered yeelights model detection (#24671) * Improve autodiscovered yeelights model detection * Lint fixes * Logger warn fix --- homeassistant/components/yeelight/__init__.py | 13 ++++--------- homeassistant/components/yeelight/light.py | 6 ++++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 39dc62eddb0..6978acdce8c 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -122,17 +122,12 @@ def setup(hass, config): def device_discovered(_, info): _LOGGER.debug("Adding autodetected %s", info['hostname']) - device_type = info['device_type'] - - name = "yeelight_%s_%s" % (device_type, + name = "yeelight_%s_%s" % (info['device_type'], info['properties']['mac']) - ipaddr = info[CONF_HOST] - device_config = DEVICE_SCHEMA({ - CONF_NAME: name, - CONF_MODEL: device_type - }) - _setup_device(hass, config, ipaddr, device_config) + device_config = DEVICE_SCHEMA({CONF_NAME: name}) + + _setup_device(hass, config, info[CONF_HOST], device_config) discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 1abb05e784f..88314773be0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -178,8 +178,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _lights_setup_helper(YeelightWithAmbientLight) _lights_setup_helper(YeelightAmbientLight) else: - _LOGGER.error("Cannot determine device type for %s, %s", - device.ipaddr, device.name) + _lights_setup_helper(YeelightGenericLight) + _LOGGER.warning("Cannot determine device type for %s, %s. " + "Falling back to white only", device.ipaddr, + device.name) hass.data[data_key] += lights add_entities(lights, True) From b3963e56ec149e4a709c35685cdd24ee88c86130 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 22:23:00 -0700 Subject: [PATCH 15/16] Guard for None entity config (#24838) --- homeassistant/components/alexa/smart_home_http.py | 2 +- homeassistant/components/cloud/google_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index e9437a411d6..4636ee10bb7 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -52,7 +52,7 @@ class AlexaConfig(AbstractConfig): @property def entity_config(self): """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG, {}) + return self._config.get(CONF_ENTITY_CONFIG) or {} def should_expose(self, entity_id): """If an entity should be exposed.""" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index b047d25ee49..5e95417cd33 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -24,7 +24,7 @@ class CloudGoogleConfig(AbstractConfig): @property def entity_config(self): """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG) + return self._config.get(CONF_ENTITY_CONFIG) or {} @property def secure_devices_pin(self): From 21c2e8da6e59283284bee898f82504040a30da2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Jun 2019 22:23:22 -0700 Subject: [PATCH 16/16] Bumped version to 0.95.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4d932481a4c..79aa735bfe2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 95 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)