Remove entities from Alexa when disabling Alexa (#73999)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Paulus Schoutsen 2022-06-28 04:32:50 -04:00 committed by GitHub
parent 824de2ef4c
commit 6eeb1855ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 31 deletions

View File

@ -1,5 +1,8 @@
"""Alexa configuration for Home Assistant Cloud.""" """Alexa configuration for Home Assistant Cloud."""
from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus from http import HTTPStatus
@ -24,7 +27,15 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .const import CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
PREF_ALEXA_DEFAULT_EXPOSE,
PREF_ALEXA_ENTITY_CONFIGS,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_SHOULD_EXPOSE,
)
from .prefs import CloudPreferences from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -54,8 +65,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._token = None self._token = None
self._token_valid = None self._token_valid = None
self._cur_entity_prefs = prefs.alexa_entity_configs self._cur_entity_prefs = prefs.alexa_entity_configs
self._cur_default_expose = prefs.alexa_default_expose self._alexa_sync_unsub: Callable[[], None] | None = None
self._alexa_sync_unsub = None
self._endpoint = None self._endpoint = None
@property @property
@ -75,7 +85,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
@property @property
def should_report_state(self): def should_report_state(self):
"""Return if states should be proactively reported.""" """Return if states should be proactively reported."""
return self._prefs.alexa_report_state and self.authorized return (
self._prefs.alexa_enabled
and self._prefs.alexa_report_state
and self.authorized
)
@property @property
def endpoint(self): def endpoint(self):
@ -179,7 +193,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._token_valid = utcnow() + timedelta(seconds=body["expires_in"]) self._token_valid = utcnow() + timedelta(seconds=body["expires_in"])
return self._token return self._token
async def _async_prefs_updated(self, prefs): async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
"""Handle updated preferences.""" """Handle updated preferences."""
if not self._cloud.is_logged_in: if not self._cloud.is_logged_in:
if self.is_reporting_states: if self.is_reporting_states:
@ -190,6 +204,8 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._alexa_sync_unsub = None self._alexa_sync_unsub = None
return return
updated_prefs = prefs.last_updated
if ( if (
ALEXA_DOMAIN not in self.hass.config.components ALEXA_DOMAIN not in self.hass.config.components
and self.enabled and self.enabled
@ -211,28 +227,30 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
await self.async_sync_entities() await self.async_sync_entities()
return return
# If user has filter in config.yaml, don't sync. # Nothing to do if no Alexa related things have changed
if not self._config[CONF_FILTER].empty_filter: if not any(
return key in updated_prefs
for key in (
# If entity prefs are the same, don't sync. PREF_ALEXA_DEFAULT_EXPOSE,
if ( PREF_ALEXA_ENTITY_CONFIGS,
self._cur_entity_prefs is prefs.alexa_entity_configs PREF_ALEXA_REPORT_STATE,
and self._cur_default_expose is prefs.alexa_default_expose PREF_ENABLE_ALEXA,
)
): ):
return return
# If we update just entity preferences, delay updating
# as we might update more
if updated_prefs == {PREF_ALEXA_ENTITY_CONFIGS}:
if self._alexa_sync_unsub: if self._alexa_sync_unsub:
self._alexa_sync_unsub() self._alexa_sync_unsub()
self._alexa_sync_unsub = None
if self._cur_default_expose is not prefs.alexa_default_expose:
await self.async_sync_entities()
return
self._alexa_sync_unsub = async_call_later( self._alexa_sync_unsub = async_call_later(
self.hass, SYNC_DELAY, self._sync_prefs self.hass, SYNC_DELAY, self._sync_prefs
) )
return
await self.async_sync_entities()
async def _sync_prefs(self, _now): async def _sync_prefs(self, _now):
"""Sync the updated preferences to Alexa.""" """Sync the updated preferences to Alexa."""
@ -243,9 +261,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
seen = set() seen = set()
to_update = [] to_update = []
to_remove = [] to_remove = []
is_enabled = self.enabled
for entity_id, info in old_prefs.items(): for entity_id, info in old_prefs.items():
seen.add(entity_id) seen.add(entity_id)
if not is_enabled:
to_remove.append(entity_id)
old_expose = info.get(PREF_SHOULD_EXPOSE) old_expose = info.get(PREF_SHOULD_EXPOSE)
if entity_id in new_prefs: if entity_id in new_prefs:
@ -291,8 +314,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
to_update = [] to_update = []
to_remove = [] to_remove = []
is_enabled = self.enabled
for entity in alexa_entities.async_get_entities(self.hass, self): for entity in alexa_entities.async_get_entities(self.hass, self):
if self.should_expose(entity.entity_id): if is_enabled and self.should_expose(entity.entity_id):
to_update.append(entity.entity_id) to_update.append(entity.entity_id)
else: else:
to_remove.append(entity.entity_id) to_remove.append(entity.entity_id)

View File

@ -50,6 +50,7 @@ class CloudPreferences:
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._prefs = None self._prefs = None
self._listeners = [] self._listeners = []
self.last_updated: set[str] = set()
async def async_initialize(self): async def async_initialize(self):
"""Finish initializing the preferences.""" """Finish initializing the preferences."""
@ -308,6 +309,9 @@ class CloudPreferences:
async def _save_prefs(self, prefs): async def _save_prefs(self, prefs):
"""Save preferences to disk.""" """Save preferences to disk."""
self.last_updated = {
key for key, value in prefs.items() if value != self._prefs.get(key)
}
self._prefs = prefs self._prefs = prefs
await self._store.async_save(self._prefs) await self._store.async_save(self._prefs)

View File

@ -9,7 +9,6 @@ from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed, mock_registry from tests.common import async_fire_time_changed, mock_registry
@ -270,10 +269,7 @@ async def test_alexa_config_fail_refresh_token(
@contextlib.contextmanager @contextlib.contextmanager
def patch_sync_helper(): def patch_sync_helper():
"""Patch sync helper. """Patch sync helper."""
In Py3.7 this would have been an async context manager.
"""
to_update = [] to_update = []
to_remove = [] to_remove = []
@ -291,21 +287,32 @@ def patch_sync_helper():
async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub):
"""Test Alexa config responds to updating exposed entities.""" """Test Alexa config responds to updating exposed entities."""
hass.states.async_set("binary_sensor.door", "on")
hass.states.async_set(
"sensor.temp",
"23",
{"device_class": "temperature", "unit_of_measurement": "°C"},
)
hass.states.async_set("light.kitchen", "off")
await cloud_prefs.async_update( await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False, alexa_report_state=False,
) )
await alexa_config.CloudAlexaConfig( conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
).async_initialize() )
await conf.async_initialize()
with patch_sync_helper() as (to_update, to_remove): with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update_alexa_entity_config( await cloud_prefs.async_update_alexa_entity_config(
entity_id="light.kitchen", should_expose=True entity_id="light.kitchen", should_expose=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow()) async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done() await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert to_update == ["light.kitchen"] assert to_update == ["light.kitchen"]
assert to_remove == [] assert to_remove == []
@ -320,12 +327,23 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub):
entity_id="sensor.temp", should_expose=True entity_id="sensor.temp", should_expose=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow()) async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done() await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"] assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"]
assert to_remove == ["light.kitchen"] assert to_remove == ["light.kitchen"]
with patch_sync_helper() as (to_update, to_remove):
await cloud_prefs.async_update(
alexa_enabled=False,
)
await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert to_update == []
assert to_remove == ["binary_sensor.door", "sensor.temp", "light.kitchen"]
async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
"""Test Alexa config responds to entity registry.""" """Test Alexa config responds to entity registry."""