Refactor squeezebox integration media_player to use coordinator (#127695)

This commit is contained in:
Raj Laud 2024-10-29 09:21:28 -04:00 committed by GitHub
parent 9bda3bd477
commit 07c070e253
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 210 additions and 118 deletions

View File

@ -2,9 +2,10 @@
from asyncio import timeout from asyncio import timeout
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
import logging import logging
from pysqueezebox import Server from pysqueezebox import Player, Server
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -23,20 +24,30 @@ from homeassistant.helpers.device_registry import (
DeviceEntryType, DeviceEntryType,
format_mac, format_mac,
) )
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from .const import ( from .const import (
CONF_HTTPS, CONF_HTTPS,
DISCOVERY_INTERVAL,
DISCOVERY_TASK, DISCOVERY_TASK,
DOMAIN, DOMAIN,
KNOWN_PLAYERS,
KNOWN_SERVERS,
MANUFACTURER, MANUFACTURER,
SERVER_MODEL, SERVER_MODEL,
SIGNAL_PLAYER_DISCOVERED,
SIGNAL_PLAYER_REDISCOVERED,
STATUS_API_TIMEOUT, STATUS_API_TIMEOUT,
STATUS_QUERY_LIBRARYNAME, STATUS_QUERY_LIBRARYNAME,
STATUS_QUERY_MAC, STATUS_QUERY_MAC,
STATUS_QUERY_UUID, STATUS_QUERY_UUID,
STATUS_QUERY_VERSION, STATUS_QUERY_VERSION,
) )
from .coordinator import LMSStatusDataUpdateCoordinator from .coordinator import (
LMSStatusDataUpdateCoordinator,
SqueezeBoxPlayerUpdateCoordinator,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -117,15 +128,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
) )
_LOGGER.debug("LMS Device %s", device) _LOGGER.debug("LMS Device %s", device)
coordinator = LMSStatusDataUpdateCoordinator(hass, lms) server_coordinator = LMSStatusDataUpdateCoordinator(hass, lms)
entry.runtime_data = SqueezeboxData( entry.runtime_data = SqueezeboxData(
coordinator=coordinator, coordinator=server_coordinator,
server=lms, server=lms,
) )
await coordinator.async_config_entry_first_refresh() # set up player discovery
known_servers = hass.data.setdefault(DOMAIN, {}).setdefault(KNOWN_SERVERS, {})
known_players = known_servers.setdefault(lms.uuid, {}).setdefault(KNOWN_PLAYERS, [])
async def _player_discovery(now: datetime | None = None) -> None:
"""Discover squeezebox players by polling server."""
async def _discovered_player(player: Player) -> None:
"""Handle a (re)discovered player."""
if player.player_id in known_players:
await player.async_update()
async_dispatcher_send(
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
)
else:
_LOGGER.debug("Adding new entity: %s", player)
player_coordinator = SqueezeBoxPlayerUpdateCoordinator(
hass, player, lms.uuid
)
known_players.append(player.player_id)
async_dispatcher_send(
hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator
)
if players := await lms.async_get_players():
for player in players:
hass.async_create_task(_discovered_player(player))
entry.async_on_unload(
async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
)
await server_coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_LOGGER.debug(
"Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
)
entry.async_create_background_task(
hass, _player_discovery(), "squeezebox.media_player.player_discovery"
)
return True return True

View File

@ -5,6 +5,7 @@ DISCOVERY_TASK = "discovery_task"
DOMAIN = "squeezebox" DOMAIN = "squeezebox"
DEFAULT_PORT = 9000 DEFAULT_PORT = 9000
KNOWN_PLAYERS = "known_players" KNOWN_PLAYERS = "known_players"
KNOWN_SERVERS = "known_servers"
MANUFACTURER = "https://lyrion.org/" MANUFACTURER = "https://lyrion.org/"
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub" PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
SENSOR_UPDATE_INTERVAL = 60 SENSOR_UPDATE_INTERVAL = 60
@ -27,3 +28,7 @@ STATUS_QUERY_MAC = "mac"
STATUS_QUERY_UUID = "uuid" STATUS_QUERY_UUID = "uuid"
STATUS_QUERY_VERSION = "version" STATUS_QUERY_VERSION = "version"
SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:")
SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered"
SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
DISCOVERY_INTERVAL = 60
PLAYER_UPDATE_INTERVAL = 5

View File

@ -1,18 +1,23 @@
"""DataUpdateCoordinator for the Squeezebox integration.""" """DataUpdateCoordinator for the Squeezebox integration."""
from asyncio import timeout from asyncio import timeout
from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
import logging import logging
import re import re
from typing import Any
from pysqueezebox import Server from pysqueezebox import Player, Server
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
PLAYER_UPDATE_INTERVAL,
SENSOR_UPDATE_INTERVAL, SENSOR_UPDATE_INTERVAL,
SIGNAL_PLAYER_REDISCOVERED,
STATUS_API_TIMEOUT, STATUS_API_TIMEOUT,
STATUS_SENSOR_LASTSCAN, STATUS_SENSOR_LASTSCAN,
STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_NEEDSRESTART,
@ -38,7 +43,7 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator):
self.newversion_regex = re.compile("<.*$") self.newversion_regex = re.compile("<.*$")
async def _async_update_data(self) -> dict: async def _async_update_data(self) -> dict:
"""Fetch data fromn LMS status call. """Fetch data from LMS status call.
Then we process only a subset to make then nice for HA Then we process only a subset to make then nice for HA
""" """
@ -70,3 +75,46 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data) _LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data)
return data return data
class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for Squeezebox players."""
def __init__(self, hass: HomeAssistant, player: Player, server_uuid: str) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=player.name,
update_interval=timedelta(seconds=PLAYER_UPDATE_INTERVAL),
always_update=True,
)
self.player = player
self.available = True
self._remove_dispatcher: Callable | None = None
self.server_uuid = server_uuid
async def _async_update_data(self) -> dict[str, Any]:
"""Update Player if available, or listen for rediscovery if not."""
if self.available:
# Only update players available at last update, unavailable players are rediscovered instead
await self.player.async_update()
if self.player.connected is False:
_LOGGER.debug("Player %s is not available", self.name)
self.available = False
# start listening for restored players
self._remove_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
)
return {}
@callback
def rediscovered(self, unique_id: str, connected: bool) -> None:
"""Make a player available again."""
if unique_id == self.player.player_id and connected:
self.available = True
_LOGGER.debug("Player %s is available again", self.name)
if self._remove_dispatcher:
self._remove_dispatcher()

View File

@ -6,9 +6,9 @@ from collections.abc import Callable
from datetime import datetime from datetime import datetime
import json import json
import logging import logging
from typing import Any from typing import TYPE_CHECKING, Any
from pysqueezebox import Player, Server, async_discover from pysqueezebox import Server, async_discover
import voluptuous as vol import voluptuous as vol
from homeassistant.components import media_source from homeassistant.components import media_source
@ -25,50 +25,53 @@ from homeassistant.components.media_player import (
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY
from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import ( from homeassistant.helpers import (
config_validation as cv, config_validation as cv,
discovery_flow, discovery_flow,
entity_platform, entity_platform,
entity_registry as er,
) )
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC, CONNECTION_NETWORK_MAC,
DeviceInfo, DeviceInfo,
format_mac, format_mac,
) )
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import SqueezeboxConfigEntry
from .browse_media import ( from .browse_media import (
build_item_response, build_item_response,
generate_playlist, generate_playlist,
library_payload, library_payload,
media_source_content_filter, media_source_content_filter,
) )
from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, SQUEEZEBOX_SOURCE_STRINGS from .const import (
DISCOVERY_TASK,
DOMAIN,
KNOWN_PLAYERS,
KNOWN_SERVERS,
SIGNAL_PLAYER_DISCOVERED,
SQUEEZEBOX_SOURCE_STRINGS,
)
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
if TYPE_CHECKING:
from . import SqueezeboxConfigEntry
SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_METHOD = "call_method"
SERVICE_CALL_QUERY = "call_query" SERVICE_CALL_QUERY = "call_query"
ATTR_QUERY_RESULT = "query_result" ATTR_QUERY_RESULT = "query_result"
SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DISCOVERY_INTERVAL = 60
KNOWN_SERVERS = "known_servers"
ATTR_PARAMETERS = "parameters" ATTR_PARAMETERS = "parameters"
ATTR_OTHER_PLAYER = "other_player" ATTR_OTHER_PLAYER = "other_player"
@ -112,49 +115,15 @@ async def async_setup_entry(
entry: SqueezeboxConfigEntry, entry: SqueezeboxConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up an player discovery from a config entry.""" """Set up the Squeezebox media_player platform from a server config entry."""
hass.data.setdefault(DOMAIN, {})
known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, [])
lms = entry.runtime_data.server
async def _player_discovery(now: datetime | None = None) -> None: # Add media player entities when discovered
"""Discover squeezebox players by polling server.""" async def _player_discovered(player: SqueezeBoxPlayerUpdateCoordinator) -> None:
_LOGGER.debug("Setting up media_player entity for player %s", player)
async def _discovered_player(player: Player) -> None: async_add_entities([SqueezeBoxMediaPlayerEntity(player)])
"""Handle a (re)discovered player."""
entity = next(
(
known
for known in known_players
if known.unique_id == player.player_id
),
None,
)
if entity:
await player.async_update()
async_dispatcher_send(
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
)
if not entity:
_LOGGER.debug("Adding new entity: %s", player)
entity = SqueezeBoxEntity(player, lms)
known_players.append(entity)
async_add_entities([entity], True)
if players := await lms.async_get_players():
for player in players:
hass.async_create_task(_discovered_player(player))
entry.async_on_unload( entry.async_on_unload(
async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery) async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
)
_LOGGER.debug(
"Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
)
entry.async_create_background_task(
hass, _player_discovery(), "squeezebox.media_player.player_discovery"
) )
# Register entity services # Register entity services
@ -184,8 +153,10 @@ async def async_setup_entry(
entry.async_on_unload(async_at_start(hass, start_server_discovery)) entry.async_on_unload(async_at_start(hass, start_server_discovery))
class SqueezeBoxEntity(MediaPlayerEntity): class SqueezeBoxMediaPlayerEntity(
"""Representation of a SqueezeBox device. CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
):
"""Representation of the media player features of a SqueezeBox device.
Wraps a pysqueezebox.Player() object. Wraps a pysqueezebox.Player() object.
""" """
@ -212,13 +183,18 @@ class SqueezeBoxEntity(MediaPlayerEntity):
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None
_last_update: datetime | None = None _last_update: datetime | None = None
_attr_available = True
def __init__(self, player: Player, server: Server) -> None: def __init__(
self,
coordinator: SqueezeBoxPlayerUpdateCoordinator,
) -> None:
"""Initialize the SqueezeBox device.""" """Initialize the SqueezeBox device."""
super().__init__(coordinator)
player = coordinator.player
self._player = player self._player = player
self._query_result: bool | dict = {} self._query_result: bool | dict = {}
self._remove_dispatcher: Callable | None = None self._remove_dispatcher: Callable | None = None
self._previous_media_position = 0
self._attr_unique_id = format_mac(player.player_id) self._attr_unique_id = format_mac(player.player_id)
_manufacturer = None _manufacturer = None
if player.model == "SqueezeLite" or "SqueezePlay" in player.model: if player.model == "SqueezeLite" or "SqueezePlay" in player.model:
@ -234,11 +210,24 @@ class SqueezeBoxEntity(MediaPlayerEntity):
identifiers={(DOMAIN, self._attr_unique_id)}, identifiers={(DOMAIN, self._attr_unique_id)},
name=player.name, name=player.name,
connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)}, connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)},
via_device=(DOMAIN, server.uuid), via_device=(DOMAIN, coordinator.server_uuid),
model=player.model, model=player.model,
manufacturer=_manufacturer, manufacturer=_manufacturer,
) )
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if self._previous_media_position != self.media_position:
self._previous_media_position = self.media_position
self._last_update = utcnow()
self.async_write_ha_state()
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.coordinator.available and super().available
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return device-specific attributes.""" """Return device-specific attributes."""
@ -248,15 +237,6 @@ class SqueezeBoxEntity(MediaPlayerEntity):
if getattr(self, attr) is not None if getattr(self, attr) is not None
} }
@callback
def rediscovered(self, unique_id: str, connected: bool) -> None:
"""Make a player available again."""
if unique_id == self.unique_id and connected:
self._attr_available = True
_LOGGER.debug("Player %s is available again", self.name)
if self._remove_dispatcher:
self._remove_dispatcher()
@property @property
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
"""Return the state of the device.""" """Return the state of the device."""
@ -269,26 +249,11 @@ class SqueezeBoxEntity(MediaPlayerEntity):
) )
return None return None
async def async_update(self) -> None:
"""Update the Player() object."""
# only update available players, newly available players will be rediscovered and marked available
if self._attr_available:
last_media_position = self.media_position
await self._player.async_update()
if self.media_position != last_media_position:
self._last_update = utcnow()
if self._player.connected is False:
_LOGGER.debug("Player %s is not available", self.name)
self._attr_available = False
# start listening for restored players
self._remove_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Remove from list of known players when removed from hass.""" """Remove from list of known players when removed from hass."""
self.hass.data[DOMAIN][KNOWN_PLAYERS].remove(self) known_servers = self.hass.data[DOMAIN][KNOWN_SERVERS]
known_players = known_servers[self.coordinator.server_uuid][KNOWN_PLAYERS]
known_players.remove(self.coordinator.player.player_id)
@property @property
def volume_level(self) -> float | None: def volume_level(self) -> float | None:
@ -380,13 +345,15 @@ class SqueezeBoxEntity(MediaPlayerEntity):
@property @property
def group_members(self) -> list[str]: def group_members(self) -> list[str]:
"""List players we are synced with.""" """List players we are synced with."""
player_ids = { ent_reg = er.async_get(self.hass)
p.unique_id: p.entity_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS]
}
return [ return [
player_ids[player] entity_id
for player in self._player.sync_group for player in self._player.sync_group
if player in player_ids if (
entity_id := ent_reg.async_get_entity_id(
Platform.MEDIA_PLAYER, DOMAIN, player
)
)
] ]
@property @property
@ -397,55 +364,68 @@ class SqueezeBoxEntity(MediaPlayerEntity):
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn off media player.""" """Turn off media player."""
await self._player.async_set_power(False) await self._player.async_set_power(False)
await self.coordinator.async_refresh()
async def async_volume_up(self) -> None: async def async_volume_up(self) -> None:
"""Volume up media player.""" """Volume up media player."""
await self._player.async_set_volume("+5") await self._player.async_set_volume("+5")
await self.coordinator.async_refresh()
async def async_volume_down(self) -> None: async def async_volume_down(self) -> None:
"""Volume down media player.""" """Volume down media player."""
await self._player.async_set_volume("-5") await self._player.async_set_volume("-5")
await self.coordinator.async_refresh()
async def async_set_volume_level(self, volume: float) -> None: async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
volume_percent = str(int(volume * 100)) volume_percent = str(int(volume * 100))
await self._player.async_set_volume(volume_percent) await self._player.async_set_volume(volume_percent)
await self.coordinator.async_refresh()
async def async_mute_volume(self, mute: bool) -> None: async def async_mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
await self._player.async_set_muting(mute) await self._player.async_set_muting(mute)
await self.coordinator.async_refresh()
async def async_media_stop(self) -> None: async def async_media_stop(self) -> None:
"""Send stop command to media player.""" """Send stop command to media player."""
await self._player.async_stop() await self._player.async_stop()
await self.coordinator.async_refresh()
async def async_media_play_pause(self) -> None: async def async_media_play_pause(self) -> None:
"""Send pause command to media player.""" """Send pause command to media player."""
await self._player.async_toggle_pause() await self._player.async_toggle_pause()
await self.coordinator.async_refresh()
async def async_media_play(self) -> None: async def async_media_play(self) -> None:
"""Send play command to media player.""" """Send play command to media player."""
await self._player.async_play() await self._player.async_play()
await self.coordinator.async_refresh()
async def async_media_pause(self) -> None: async def async_media_pause(self) -> None:
"""Send pause command to media player.""" """Send pause command to media player."""
await self._player.async_pause() await self._player.async_pause()
await self.coordinator.async_refresh()
async def async_media_next_track(self) -> None: async def async_media_next_track(self) -> None:
"""Send next track command.""" """Send next track command."""
await self._player.async_index("+1") await self._player.async_index("+1")
await self.coordinator.async_refresh()
async def async_media_previous_track(self) -> None: async def async_media_previous_track(self) -> None:
"""Send next track command.""" """Send next track command."""
await self._player.async_index("-1") await self._player.async_index("-1")
await self.coordinator.async_refresh()
async def async_media_seek(self, position: float) -> None: async def async_media_seek(self, position: float) -> None:
"""Send seek command.""" """Send seek command."""
await self._player.async_time(position) await self._player.async_time(position)
await self.coordinator.async_refresh()
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the media player on.""" """Turn the media player on."""
await self._player.async_set_power(True) await self._player.async_set_power(True)
await self.coordinator.async_refresh()
async def async_play_media( async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any self, media_type: MediaType | str, media_id: str, **kwargs: Any
@ -504,6 +484,7 @@ class SqueezeBoxEntity(MediaPlayerEntity):
await self._player.async_load_playlist(playlist, cmd) await self._player.async_load_playlist(playlist, cmd)
if index is not None: if index is not None:
await self._player.async_index(index) await self._player.async_index(index)
await self.coordinator.async_refresh()
async def async_set_repeat(self, repeat: RepeatMode) -> None: async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set the repeat mode.""" """Set the repeat mode."""
@ -515,15 +496,18 @@ class SqueezeBoxEntity(MediaPlayerEntity):
repeat_mode = "none" repeat_mode = "none"
await self._player.async_set_repeat(repeat_mode) await self._player.async_set_repeat(repeat_mode)
await self.coordinator.async_refresh()
async def async_set_shuffle(self, shuffle: bool) -> None: async def async_set_shuffle(self, shuffle: bool) -> None:
"""Enable/disable shuffle mode.""" """Enable/disable shuffle mode."""
shuffle_mode = "song" if shuffle else "none" shuffle_mode = "song" if shuffle else "none"
await self._player.async_set_shuffle(shuffle_mode) await self._player.async_set_shuffle(shuffle_mode)
await self.coordinator.async_refresh()
async def async_clear_playlist(self) -> None: async def async_clear_playlist(self) -> None:
"""Send the media player the command for clear playlist.""" """Send the media player the command for clear playlist."""
await self._player.async_clear_playlist() await self._player.async_clear_playlist()
await self.coordinator.async_refresh()
async def async_call_method( async def async_call_method(
self, command: str, parameters: list[str] | None = None self, command: str, parameters: list[str] | None = None
@ -558,21 +542,24 @@ class SqueezeBoxEntity(MediaPlayerEntity):
If the other player is a member of a sync group, it will leave the current sync group If the other player is a member of a sync group, it will leave the current sync group
without asking. without asking.
""" """
player_ids = { ent_reg = er.async_get(self.hass)
p.entity_id: p.unique_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS] for other_player_entity_id in group_members:
} other_player = ent_reg.async_get(other_player_entity_id)
if other_player is None:
for other_player in group_members: raise ServiceValidationError(
if other_player_id := player_ids.get(other_player): f"Could not find player with entity_id {other_player_entity_id}"
)
if other_player_id := other_player.unique_id:
await self._player.async_sync(other_player_id) await self._player.async_sync(other_player_id)
else: else:
raise ServiceValidationError( raise ServiceValidationError(
f"Could not join unknown player {other_player}" f"Could not join unknown player {other_player_entity_id}"
) )
async def async_unjoin_player(self) -> None: async def async_unjoin_player(self) -> None:
"""Unsync this Squeezebox player.""" """Unsync this Squeezebox player."""
await self._player.async_unsync() await self._player.async_unsync()
await self.coordinator.async_refresh()
async def async_browse_media( async def async_browse_media(
self, self,

View File

@ -207,7 +207,7 @@ def player_factory() -> MagicMock:
def mock_pysqueezebox_player(uuid: str) -> MagicMock: def mock_pysqueezebox_player(uuid: str) -> MagicMock:
"""Mock a Lyrion Media Server player.""" """Mock a Lyrion Media Server player."""
with patch( with patch(
"homeassistant.components.squeezebox.media_player.Player", autospec=True "homeassistant.components.squeezebox.Player", autospec=True
) as mock_player: ) as mock_player:
mock_player.async_browse = AsyncMock(side_effect=mock_async_browse) mock_player.async_browse = AsyncMock(side_effect=mock_async_browse)
mock_player.generate_image_url_from_track_id = MagicMock( mock_player.generate_image_url_from_track_id = MagicMock(

View File

@ -30,10 +30,14 @@ from homeassistant.components.media_player import (
MediaType, MediaType,
RepeatMode, RepeatMode,
) )
from homeassistant.components.squeezebox.const import DOMAIN, SENSOR_UPDATE_INTERVAL from homeassistant.components.squeezebox.const import (
DISCOVERY_INTERVAL,
DOMAIN,
PLAYER_UPDATE_INTERVAL,
SENSOR_UPDATE_INTERVAL,
)
from homeassistant.components.squeezebox.media_player import ( from homeassistant.components.squeezebox.media_player import (
ATTR_PARAMETERS, ATTR_PARAMETERS,
DISCOVERY_INTERVAL,
SERVICE_CALL_METHOD, SERVICE_CALL_METHOD,
SERVICE_CALL_QUERY, SERVICE_CALL_QUERY,
) )
@ -101,12 +105,9 @@ async def test_squeezebox_player_rediscovery(
# Make the player appear unavailable # Make the player appear unavailable
configured_player.connected = False configured_player.connected = False
await hass.services.async_call( freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
MEDIA_PLAYER_DOMAIN, async_fire_time_changed(hass)
SERVICE_TURN_ON, await hass.async_block_till_done()
{ATTR_ENTITY_ID: "media_player.test_player"},
blocking=True,
)
assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE
# Make the player available again # Make the player available again
@ -115,7 +116,7 @@ async def test_squeezebox_player_rediscovery(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
freezer.tick(timedelta(seconds=SENSOR_UPDATE_INTERVAL)) freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("media_player.test_player").state == MediaPlayerState.IDLE assert hass.states.get("media_player.test_player").state == MediaPlayerState.IDLE