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."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from contextlib import suppress
from datetime import timedelta
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.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
_LOGGER = logging.getLogger(__name__)
@ -54,8 +65,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._token = None
self._token_valid = None
self._cur_entity_prefs = prefs.alexa_entity_configs
self._cur_default_expose = prefs.alexa_default_expose
self._alexa_sync_unsub = None
self._alexa_sync_unsub: Callable[[], None] | None = None
self._endpoint = None
@property
@ -75,7 +85,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
@property
def should_report_state(self):
"""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
def endpoint(self):
@ -179,7 +193,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._token_valid = utcnow() + timedelta(seconds=body["expires_in"])
return self._token
async def _async_prefs_updated(self, prefs):
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
"""Handle updated preferences."""
if not self._cloud.is_logged_in:
if self.is_reporting_states:
@ -190,6 +204,8 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._alexa_sync_unsub = None
return
updated_prefs = prefs.last_updated
if (
ALEXA_DOMAIN not in self.hass.config.components
and self.enabled
@ -211,28 +227,30 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
await self.async_sync_entities()
return
# If user has filter in config.yaml, don't sync.
if not self._config[CONF_FILTER].empty_filter:
return
# If entity prefs are the same, don't sync.
if (
self._cur_entity_prefs is prefs.alexa_entity_configs
and self._cur_default_expose is prefs.alexa_default_expose
# Nothing to do if no Alexa related things have changed
if not any(
key in updated_prefs
for key in (
PREF_ALEXA_DEFAULT_EXPOSE,
PREF_ALEXA_ENTITY_CONFIGS,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
)
):
return
if self._alexa_sync_unsub:
self._alexa_sync_unsub()
self._alexa_sync_unsub = None
# 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:
self._alexa_sync_unsub()
if self._cur_default_expose is not prefs.alexa_default_expose:
await self.async_sync_entities()
self._alexa_sync_unsub = async_call_later(
self.hass, SYNC_DELAY, self._sync_prefs
)
return
self._alexa_sync_unsub = async_call_later(
self.hass, SYNC_DELAY, self._sync_prefs
)
await self.async_sync_entities()
async def _sync_prefs(self, _now):
"""Sync the updated preferences to Alexa."""
@ -243,9 +261,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
seen = set()
to_update = []
to_remove = []
is_enabled = self.enabled
for entity_id, info in old_prefs.items():
seen.add(entity_id)
if not is_enabled:
to_remove.append(entity_id)
old_expose = info.get(PREF_SHOULD_EXPOSE)
if entity_id in new_prefs:
@ -291,8 +314,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
to_update = []
to_remove = []
is_enabled = self.enabled
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)
else:
to_remove.append(entity.entity_id)

View file

@ -50,6 +50,7 @@ class CloudPreferences:
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._prefs = None
self._listeners = []
self.last_updated: set[str] = set()
async def async_initialize(self):
"""Finish initializing the preferences."""
@ -308,6 +309,9 @@ class CloudPreferences:
async def _save_prefs(self, prefs):
"""Save preferences to disk."""
self.last_updated = {
key for key, value in prefs.items() if value != self._prefs.get(key)
}
self._prefs = 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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import EntityCategory
from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed, mock_registry
@ -270,10 +269,7 @@ async def test_alexa_config_fail_refresh_token(
@contextlib.contextmanager
def patch_sync_helper():
"""Patch sync helper.
In Py3.7 this would have been an async context manager.
"""
"""Patch sync helper."""
to_update = []
to_remove = []
@ -291,21 +287,32 @@ def patch_sync_helper():
async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub):
"""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(
alexa_enabled=True,
alexa_report_state=False,
)
await alexa_config.CloudAlexaConfig(
conf = alexa_config.CloudAlexaConfig(
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):
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())
async_fire_time_changed(hass, fire_all=True)
await hass.async_block_till_done()
assert conf._alexa_sync_unsub is None
assert to_update == ["light.kitchen"]
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
)
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()
assert conf._alexa_sync_unsub is None
assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"]
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):
"""Test Alexa config responds to entity registry."""