mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 07:05:48 +01:00
Handle toggling of the 'expose_to_ha' setting in Music Assistant integration (#152779)
This commit is contained in:
committed by
GitHub
parent
86db60c442
commit
72e608918b
@@ -9,8 +9,10 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion
|
||||
from music_assistant_models.config_entries import PlayerConfig
|
||||
from music_assistant_models.enums import EventType
|
||||
from music_assistant_models.errors import ActionUnavailable, MusicAssistantError
|
||||
from music_assistant_models.player import Player
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
@@ -25,7 +27,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
|
||||
from .actions import get_music_assistant_client, register_actions
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from music_assistant_models.event import MassEvent
|
||||
@@ -59,7 +61,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
async def async_setup_entry( # noqa: C901
|
||||
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Music Assistant from a config entry."""
|
||||
@@ -126,8 +128,25 @@ async def async_setup_entry(
|
||||
# initialize platforms
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
def add_player(player: Player) -> None:
|
||||
"""Handle adding Player from MA as HA device + entities."""
|
||||
entry.runtime_data.discovered_players.add(player.player_id)
|
||||
# run callback for each platform
|
||||
for callback in entry.runtime_data.platform_handlers.values():
|
||||
callback(player.player_id)
|
||||
|
||||
def remove_player(player_id: str) -> None:
|
||||
"""Handle removing Player from MA as HA device + entities."""
|
||||
if player_id in entry.runtime_data.discovered_players:
|
||||
entry.runtime_data.discovered_players.remove(player_id)
|
||||
dev_reg = dr.async_get(hass)
|
||||
if hass_device := dev_reg.async_get_device({(DOMAIN, player_id)}):
|
||||
dev_reg.async_update_device(
|
||||
hass_device.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
# register listener for new players
|
||||
async def handle_player_added(event: MassEvent) -> None:
|
||||
def handle_player_added(event: MassEvent) -> None:
|
||||
"""Handle Mass Player Added event."""
|
||||
if TYPE_CHECKING:
|
||||
assert event.object_id is not None
|
||||
@@ -138,10 +157,7 @@ async def async_setup_entry(
|
||||
assert player is not None
|
||||
if not player.expose_to_ha:
|
||||
return
|
||||
entry.runtime_data.discovered_players.add(event.object_id)
|
||||
# run callback for each platform
|
||||
for callback in entry.runtime_data.platform_handlers.values():
|
||||
callback(event.object_id)
|
||||
add_player(player)
|
||||
|
||||
entry.async_on_unload(mass.subscribe(handle_player_added, EventType.PLAYER_ADDED))
|
||||
|
||||
@@ -149,25 +165,40 @@ async def async_setup_entry(
|
||||
for player in mass.players:
|
||||
if not player.expose_to_ha:
|
||||
continue
|
||||
entry.runtime_data.discovered_players.add(player.player_id)
|
||||
for callback in entry.runtime_data.platform_handlers.values():
|
||||
callback(player.player_id)
|
||||
add_player(player)
|
||||
|
||||
# register listener for removed players
|
||||
async def handle_player_removed(event: MassEvent) -> None:
|
||||
def handle_player_removed(event: MassEvent) -> None:
|
||||
"""Handle Mass Player Removed event."""
|
||||
if event.object_id is None:
|
||||
return
|
||||
dev_reg = dr.async_get(hass)
|
||||
if hass_device := dev_reg.async_get_device({(DOMAIN, event.object_id)}):
|
||||
dev_reg.async_update_device(
|
||||
hass_device.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
remove_player(event.object_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
mass.subscribe(handle_player_removed, EventType.PLAYER_REMOVED)
|
||||
)
|
||||
|
||||
# register listener for player configs (to handle toggling of the 'expose_to_ha' setting)
|
||||
def handle_player_config_updated(event: MassEvent) -> None:
|
||||
"""Handle Mass Player Config Updated event."""
|
||||
if event.object_id is None or not event.data:
|
||||
return
|
||||
player_id = event.object_id
|
||||
player_config = PlayerConfig.from_dict(event.data)
|
||||
expose_to_ha = player_config.get_value(ATTR_CONF_EXPOSE_PLAYER_TO_HA, True)
|
||||
if not expose_to_ha and player_id in entry.runtime_data.discovered_players:
|
||||
# player is no longer exposed to Home Assistant
|
||||
remove_player(player_id)
|
||||
elif expose_to_ha and player_id not in entry.runtime_data.discovered_players:
|
||||
# player is now exposed to Home Assistant
|
||||
if not (player := mass.players.get(player_id)):
|
||||
return # guard
|
||||
add_player(player)
|
||||
|
||||
entry.async_on_unload(
|
||||
mass.subscribe(handle_player_config_updated, EventType.PLAYER_CONFIG_UPDATED)
|
||||
)
|
||||
|
||||
# check if any playerconfigs have been removed while we were disconnected
|
||||
all_player_configs = await mass.config.get_player_configs()
|
||||
player_ids = {player.player_id for player in all_player_configs}
|
||||
|
||||
@@ -65,5 +65,6 @@ ATTR_STREAM_TITLE = "stream_title"
|
||||
ATTR_PROVIDER = "provider"
|
||||
ATTR_ITEM_ID = "item_id"
|
||||
|
||||
ATTR_CONF_EXPOSE_PLAYER_TO_HA = "expose_player_to_ha"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
@@ -186,15 +186,15 @@ async def trigger_subscription_callback(
|
||||
):
|
||||
continue
|
||||
|
||||
event = MassEvent(
|
||||
mass_event = MassEvent(
|
||||
event=event,
|
||||
object_id=object_id,
|
||||
data=data,
|
||||
)
|
||||
if inspect.iscoroutinefunction(cb_func):
|
||||
await cb_func(event)
|
||||
await cb_func(mass_event)
|
||||
else:
|
||||
cb_func(event)
|
||||
cb_func(mass_event)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from music_assistant_models.enums import EventType
|
||||
from music_assistant_models.errors import ActionUnavailable
|
||||
|
||||
from homeassistant.components.music_assistant.const import DOMAIN
|
||||
from homeassistant.components.music_assistant.const import (
|
||||
ATTR_CONF_EXPOSE_PLAYER_TO_HA,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import setup_integration_from_fixtures
|
||||
from .common import setup_integration_from_fixtures, trigger_subscription_callback
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
@@ -68,3 +72,82 @@ async def test_remove_config_entry_device(
|
||||
response = await client.remove_device(device_entry.id, config_entry.entry_id)
|
||||
assert music_assistant_client.config.remove_player_config.call_count == 0
|
||||
assert response["success"] is True
|
||||
|
||||
|
||||
async def test_player_config_expose_to_ha_toggle(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test player exposure toggle via config update."""
|
||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
await hass.async_block_till_done()
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
# Initial state: player should be exposed (from fixture)
|
||||
entity_id = "media_player.test_player_1"
|
||||
player_id = "00:00:00:00:00:01"
|
||||
assert hass.states.get(entity_id)
|
||||
assert entity_registry.async_get(entity_id)
|
||||
device_entry = device_registry.async_get_device({(DOMAIN, player_id)})
|
||||
assert device_entry
|
||||
assert player_id in config_entry.runtime_data.discovered_players
|
||||
|
||||
# Simulate player config update: expose_to_ha = False
|
||||
# Trigger the subscription callback
|
||||
event_data = {
|
||||
"player_id": player_id,
|
||||
"provider": "test",
|
||||
"values": {
|
||||
ATTR_CONF_EXPOSE_PLAYER_TO_HA: {
|
||||
"key": ATTR_CONF_EXPOSE_PLAYER_TO_HA,
|
||||
"type": "boolean",
|
||||
"value": False,
|
||||
"label": ATTR_CONF_EXPOSE_PLAYER_TO_HA,
|
||||
"default_value": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
await trigger_subscription_callback(
|
||||
hass,
|
||||
music_assistant_client,
|
||||
EventType.PLAYER_CONFIG_UPDATED,
|
||||
player_id,
|
||||
event_data,
|
||||
)
|
||||
|
||||
# Verify player was removed from HA
|
||||
assert player_id not in config_entry.runtime_data.discovered_players
|
||||
assert not hass.states.get(entity_id)
|
||||
assert not entity_registry.async_get(entity_id)
|
||||
device_entry = device_registry.async_get_device({(DOMAIN, player_id)})
|
||||
assert not device_entry
|
||||
|
||||
# Now test re-adding the player: expose_to_ha = True
|
||||
await trigger_subscription_callback(
|
||||
hass,
|
||||
music_assistant_client,
|
||||
EventType.PLAYER_CONFIG_UPDATED,
|
||||
player_id,
|
||||
{
|
||||
"player_id": player_id,
|
||||
"provider": "test",
|
||||
"values": {
|
||||
ATTR_CONF_EXPOSE_PLAYER_TO_HA: {
|
||||
"key": ATTR_CONF_EXPOSE_PLAYER_TO_HA,
|
||||
"type": "boolean",
|
||||
"value": True,
|
||||
"label": ATTR_CONF_EXPOSE_PLAYER_TO_HA,
|
||||
"default_value": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Verify player was re-added to HA
|
||||
assert player_id in config_entry.runtime_data.discovered_players
|
||||
assert hass.states.get(entity_id)
|
||||
assert entity_registry.async_get(entity_id)
|
||||
device_entry = device_registry.async_get_device({(DOMAIN, player_id)})
|
||||
assert device_entry
|
||||
|
||||
Reference in New Issue
Block a user