Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions homeassistant/components/alexa_devices/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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,
)
)
Comment thread
chemelli74 marked this conversation as resolved.

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

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/alexa_devices/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
8 changes: 8 additions & 0 deletions homeassistant/components/alexa_devices/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/generated/labs.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 112 additions & 1 deletion tests/components/alexa_devices/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Comment thread
jamesonuk marked this conversation as resolved.
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()
7 changes: 7 additions & 0 deletions tests/components/alexa_devices/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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()

Comment thread
jamesonuk marked this conversation as resolved.
with patch(
"homeassistant.components.alexa_devices.PLATFORMS", [Platform.MEDIA_PLAYER]
):
Expand Down
Loading