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/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) 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 == []