diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index b04be74029e62a..3173b9cf6657c4 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,8 +1,20 @@ """Alexa Devices integration.""" +import asyncio + +from homeassistant.components.labs import ( + EventLabsUpdatedData, + async_is_preview_feature_enabled, + async_subscribe_preview_feature, +) from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client, config_validation as cv, httpx_client +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + entity_registry as er, + httpx_client, +) from homeassistant.helpers.typing import ConfigType from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2 @@ -32,13 +44,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Set up Alexa Devices platform.""" + non_labs_platforms = [p for p in PLATFORMS if p != Platform.MEDIA_PLAYER] + session = aiohttp_client.async_create_clientsession(hass) coordinator = AmazonDevicesCoordinator(hass, entry, session) await coordinator.async_config_entry_first_refresh() await coordinator.sync_history_state() - await coordinator.sync_media_state() async def _on_http2_reauth_required() -> None: entry.async_start_reauth(hass) @@ -55,9 +68,73 @@ async def _on_http2_reauth_required() -> None: entry.async_on_unload(coordinator.api.stop_http2_processing) + media_player_loaded = False + _update_lock = asyncio.Lock() + + def _async_set_media_player_registry(*, enabled: bool) -> None: + """Sync media player registry entry disabled state with labs status.""" + ent_reg = er.async_get(hass) + entities = er.async_entries_for_config_entry(ent_reg, entry.entry_id) + + for entity in entities: + if entity.domain != Platform.MEDIA_PLAYER: + continue + + if enabled and entity.disabled_by is er.RegistryEntryDisabler.INTEGRATION: + ent_reg.async_update_entity(entity.entity_id, disabled_by=None) + elif not enabled and entity.disabled_by is None: + ent_reg.async_update_entity( + entity.entity_id, + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + + async def _async_update_alexa_media( + event_data: EventLabsUpdatedData | None = None, + ) -> None: + nonlocal media_player_loaded + + async with _update_lock: + enabled = ( + event_data["enabled"] + if event_data is not None + else async_is_preview_feature_enabled(hass, DOMAIN, "alexa_media") + ) + + if enabled: + _async_set_media_player_registry(enabled=True) + await coordinator.sync_media_state() + + if not media_player_loaded: + await hass.config_entries.async_forward_entry_setups( + entry, [Platform.MEDIA_PLAYER] + ) + media_player_loaded = True + else: + _async_set_media_player_registry(enabled=False) + if media_player_loaded: + if await hass.config_entries.async_unload_platforms( + entry, [Platform.MEDIA_PLAYER] + ): + media_player_loaded = False + else: + _LOGGER.warning( + "Failed to unload media player platform for %s", + entry.entry_id, + ) + + entry.async_on_unload( + async_subscribe_preview_feature( + hass, + DOMAIN, + "alexa_media", + _async_update_alexa_media, + ) + ) + entry.runtime_data = coordinator - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, non_labs_platforms) + await _async_update_alexa_media() return True diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 1c7842f6cac5c7..9286f0130a168e 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -7,6 +7,13 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], + "preview_features": { + "alexa_media": { + "feedback_url": "https://discord.gg/9P75ptv8WT", + "learn_more_url": "https://github.com/chemelli74/aioamazondevices/wiki/Media-Player-Testing", + "report_issue_url": "https://github.com/chemelli74/aioamazondevices/issues/new" + } + }, "quality_scale": "platinum", "requirements": ["aioamazondevices==13.8.1"] } diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 0ec611a8d62c9c..0baef99575b58f 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -127,6 +127,14 @@ "message": "Invalid sound {sound} specified" } }, + "preview_features": { + "alexa_media": { + "description": "This will enable functionality to view and control music playback and volume on your Alexa devices.\n\nThis is a preview feature and liable to change.\n\nPlease provide feedback or report issues using the buttons below or join us on [Discord](https://discord.gg/9P75ptv8WT).", + "disable_confirmation": "Disabling this feature will remove media players for your Alexa devices. You can re-enable this at any time to set up media players again.", + "enable_confirmation": "This feature is to gather feedback on the media player platform for Alexa Devices. Enabling this will set up media players for your Alexa devices.", + "name": "Media player platform" + } + }, "selector": { "info_skill": { "options": { diff --git a/homeassistant/generated/labs.py b/homeassistant/generated/labs.py index fb4abef4625476..5b7be2b1bd7929 100644 --- a/homeassistant/generated/labs.py +++ b/homeassistant/generated/labs.py @@ -4,6 +4,13 @@ """ LABS_PREVIEW_FEATURES = { + "alexa_devices": { + "alexa_media": { + "feedback_url": "https://discord.gg/9P75ptv8WT", + "learn_more_url": "https://github.com/chemelli74/aioamazondevices/wiki/Media-Player-Testing", + "report_issue_url": "https://github.com/chemelli74/aioamazondevices/issues/new", + }, + }, "analytics": { "snapshots": { "feedback_url": "https://forms.gle/GqvRmgmghSDco8M46", diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index dff813529d26e5..7c2a2f52ed3627 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -11,10 +11,12 @@ CONF_SITE, DOMAIN, ) +from homeassistant.components.labs import async_update_preview_feature from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component from . import setup_integration from .const import TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USER_ID, TEST_USERNAME @@ -205,3 +207,112 @@ async def test_http2_stop_processing_called_on_unload( await hass.async_block_till_done() mock_amazon_devices_client.stop_http2_processing.assert_awaited_once() + + +async def test_labs_disable_unloads_media_player_without_removing_registry_entry( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test media player entities are preserved in the registry when labs is disabled.""" + assert await async_setup_component(hass, "labs", {}) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("media_player.echo_test") is None + assert entity_registry.async_get("media_player.echo_test") is None + + await async_update_preview_feature(hass, DOMAIN, "alexa_media", True) + await hass.async_block_till_done() + + media_player_entry = entity_registry.async_get("media_player.echo_test") + assert media_player_entry is not None + assert hass.states.get("media_player.echo_test") is not None + assert media_player_entry.disabled_by is None + + entity_registry.async_update_entity("media_player.echo_test", name="Kitchen Echo") + media_player_entry = entity_registry.async_get("media_player.echo_test") + assert media_player_entry is not None + assert media_player_entry.name == "Kitchen Echo" + + await async_update_preview_feature(hass, DOMAIN, "alexa_media", False) + await hass.async_block_till_done() + + assert hass.states.get("media_player.echo_test") is None + media_player_entry = entity_registry.async_get("media_player.echo_test") + assert media_player_entry is not None + assert media_player_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert media_player_entry.name == "Kitchen Echo" + + await async_update_preview_feature(hass, DOMAIN, "alexa_media", True) + await hass.async_block_till_done() + + assert hass.states.get("media_player.echo_test") is not None + media_player_entry = entity_registry.async_get("media_player.echo_test") + assert media_player_entry is not None + assert media_player_entry.disabled_by is None + assert media_player_entry.name == "Kitchen Echo" + + +async def test_labs_enabled_before_setup_loads_media_player_platform( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test media player platform is loaded on setup when labs feature is already enabled.""" + assert await async_setup_component(hass, "labs", {}) + await async_update_preview_feature(hass, DOMAIN, "alexa_media", True) + await hass.async_block_till_done() + + await setup_integration(hass, mock_config_entry) + + media_player_entry = entity_registry.async_get("media_player.echo_test") + assert media_player_entry is not None + assert media_player_entry.disabled_by is None + assert hass.states.get("media_player.echo_test") is not None + + +async def test_labs_disable_logs_warning_when_unload_fails( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a warning is logged and media_player_loaded stays True when platform unload fails.""" + assert await async_setup_component(hass, "labs", {}) + + await async_update_preview_feature(hass, DOMAIN, "alexa_media", True) + await hass.async_block_till_done() + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("media_player.echo_test") is not None + + with ( + patch.object( + hass.config_entries, + "async_unload_platforms", + return_value=False, + ) as mock_unload, + patch("homeassistant.components.alexa_devices._LOGGER") as mock_logger, + ): + await async_update_preview_feature(hass, DOMAIN, "alexa_media", False) + await hass.async_block_till_done() + + mock_unload.assert_awaited_once() + mock_logger.warning.assert_called_once_with( + "Failed to unload media player platform for %s", + mock_config_entry.entry_id, + ) + + # Confirm media_player_loaded stayed True — unload is attempted again on next disable event + with patch.object( + hass.config_entries, + "async_unload_platforms", + return_value=False, + ) as mock_unload2: + await async_update_preview_feature(hass, DOMAIN, "alexa_media", False) + await hass.async_block_till_done() + + mock_unload2.assert_awaited_once() diff --git a/tests/components/alexa_devices/test_media_player.py b/tests/components/alexa_devices/test_media_player.py index 8d1399514e7337..af79427a483711 100644 --- a/tests/components/alexa_devices/test_media_player.py +++ b/tests/components/alexa_devices/test_media_player.py @@ -18,7 +18,9 @@ import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.labs import async_update_preview_feature from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, @@ -38,6 +40,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from . import setup_integration from .const import TEST_DEVICE_1_SN @@ -147,6 +150,10 @@ async def _setup_media_player_platform( mock_config_entry: MockConfigEntry, ) -> None: """Set up integration with only the media player platform enabled.""" + assert await async_setup_component(hass, "labs", {}) + await async_update_preview_feature(hass, DOMAIN, "alexa_media", True) + await hass.async_block_till_done() + with patch( "homeassistant.components.alexa_devices.PLATFORMS", [Platform.MEDIA_PLAYER] ):