mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Refactor squeezebox integration media_player to use coordinator (#127695)
This commit is contained in:
parent
9bda3bd477
commit
07c070e253
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user