mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Remove entities from Alexa when disabling Alexa (#73999)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
824de2ef4c
commit
6eeb1855ff
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user