mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add DataUpdateCoordinator to bluesound integration (#135125)
This commit is contained in:
parent
d06cd1ad3b
commit
acbd501ede
@ -14,10 +14,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BluesoundCoordinator
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [
|
||||
Platform.MEDIA_PLAYER,
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -26,6 +29,7 @@ class BluesoundRuntimeData:
|
||||
|
||||
player: Player
|
||||
sync_status: SyncStatus
|
||||
coordinator: BluesoundCoordinator
|
||||
|
||||
|
||||
type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
|
||||
@ -33,9 +37,6 @@ type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bluesound."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = []
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -46,13 +47,16 @@ async def async_setup_entry(
|
||||
host = config_entry.data[CONF_HOST]
|
||||
port = config_entry.data[CONF_PORT]
|
||||
session = async_get_clientsession(hass)
|
||||
async with Player(host, port, session=session, default_timeout=10) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except PlayerUnreachableError as ex:
|
||||
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
||||
player = Player(host, port, session=session, default_timeout=10)
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except PlayerUnreachableError as ex:
|
||||
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
||||
|
||||
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status)
|
||||
coordinator = BluesoundCoordinator(hass, player, sync_status)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status, coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
|
160
homeassistant/components/bluesound/coordinator.py
Normal file
160
homeassistant/components/bluesound/coordinator.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""Define a base coordinator for Bluesound entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NODE_OFFLINE_CHECK_TIMEOUT = timedelta(minutes=3)
|
||||
PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BluesoundData:
|
||||
"""Define a class to hold Bluesound data."""
|
||||
|
||||
sync_status: SyncStatus
|
||||
status: Status
|
||||
presets: list[Preset]
|
||||
inputs: list[Input]
|
||||
|
||||
|
||||
def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]:
|
||||
"""Cancel a task."""
|
||||
|
||||
async def _cancel_task() -> None:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
return _cancel_task
|
||||
|
||||
|
||||
class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
|
||||
"""Define an object to hold Bluesound data."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, player: Player, sync_status: SyncStatus
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.player = player
|
||||
self._inital_sync_status = sync_status
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=sync_status.name,
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
assert self.config_entry is not None
|
||||
|
||||
preset = await self.player.presets()
|
||||
inputs = await self.player.inputs()
|
||||
status = await self.player.status()
|
||||
|
||||
self.async_set_updated_data(
|
||||
BluesoundData(
|
||||
sync_status=self._inital_sync_status,
|
||||
status=status,
|
||||
presets=preset,
|
||||
inputs=inputs,
|
||||
)
|
||||
)
|
||||
|
||||
status_loop_task = self.hass.async_create_background_task(
|
||||
self._poll_status_loop(),
|
||||
name=f"bluesound.poll_status_loop_{self.data.sync_status.id}",
|
||||
)
|
||||
self.config_entry.async_on_unload(cancel_task(status_loop_task))
|
||||
|
||||
sync_status_loop_task = self.hass.async_create_background_task(
|
||||
self._poll_sync_status_loop(),
|
||||
name=f"bluesound.poll_sync_status_loop_{self.data.sync_status.id}",
|
||||
)
|
||||
self.config_entry.async_on_unload(cancel_task(sync_status_loop_task))
|
||||
|
||||
presets_and_inputs_loop_task = self.hass.async_create_background_task(
|
||||
self._poll_presets_and_inputs_loop(),
|
||||
name=f"bluesound.poll_presets_and_inputs_loop_{self.data.sync_status.id}",
|
||||
)
|
||||
self.config_entry.async_on_unload(cancel_task(presets_and_inputs_loop_task))
|
||||
|
||||
async def _async_update_data(self) -> BluesoundData:
|
||||
return self.data
|
||||
|
||||
async def _poll_presets_and_inputs_loop(self) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(PRESET_AND_INPUTS_INTERVAL.total_seconds())
|
||||
try:
|
||||
preset = await self.player.presets()
|
||||
inputs = await self.player.inputs()
|
||||
self.async_set_updated_data(
|
||||
replace(
|
||||
self.data,
|
||||
presets=preset,
|
||||
inputs=inputs,
|
||||
)
|
||||
)
|
||||
except PlayerUnreachableError as ex:
|
||||
self.async_set_update_error(ex)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as ex: # noqa: BLE001 - this loop should never stop
|
||||
self.async_set_update_error(ex)
|
||||
|
||||
async def _poll_status_loop(self) -> None:
|
||||
"""Loop which polls the status of the player."""
|
||||
while True:
|
||||
try:
|
||||
status = await self.player.status(
|
||||
etag=self.data.status.etag, poll_timeout=120, timeout=125
|
||||
)
|
||||
self.async_set_updated_data(
|
||||
replace(
|
||||
self.data,
|
||||
status=status,
|
||||
)
|
||||
)
|
||||
except PlayerUnreachableError as ex:
|
||||
self.async_set_update_error(ex)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as ex: # noqa: BLE001 - this loop should never stop
|
||||
self.async_set_update_error(ex)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
|
||||
|
||||
async def _poll_sync_status_loop(self) -> None:
|
||||
"""Loop which polls the sync status of the player."""
|
||||
while True:
|
||||
try:
|
||||
sync_status = await self.player.sync_status(
|
||||
etag=self.data.sync_status.etag, poll_timeout=120, timeout=125
|
||||
)
|
||||
self.async_set_updated_data(
|
||||
replace(
|
||||
self.data,
|
||||
sync_status=sync_status,
|
||||
)
|
||||
)
|
||||
except PlayerUnreachableError as ex:
|
||||
self.async_set_update_error(ex)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as ex: # noqa: BLE001 - this loop should never stop
|
||||
self.async_set_update_error(ex)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
|
@ -2,15 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import CancelledError, Task
|
||||
from contextlib import suppress
|
||||
from asyncio import Task
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
@ -23,7 +20,7 @@ from homeassistant.components.media_player import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import (
|
||||
@ -36,9 +33,11 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
||||
from .coordinator import BluesoundCoordinator
|
||||
from .utils import dispatcher_join_signal, dispatcher_unjoin_signal, format_unique_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -56,11 +55,6 @@ SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
NODE_OFFLINE_CHECK_TIMEOUT = 180
|
||||
NODE_RETRY_INITIATION = timedelta(minutes=3)
|
||||
|
||||
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
POLL_TIMEOUT = 120
|
||||
|
||||
|
||||
@ -71,10 +65,10 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Bluesound entry."""
|
||||
bluesound_player = BluesoundPlayer(
|
||||
config_entry.runtime_data.coordinator,
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.data[CONF_PORT],
|
||||
config_entry.runtime_data.player,
|
||||
config_entry.runtime_data.sync_status,
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
@ -89,11 +83,10 @@ async def async_setup_entry(
|
||||
)
|
||||
platform.async_register_entity_service(SERVICE_UNJOIN, None, "async_unjoin")
|
||||
|
||||
hass.data[DATA_BLUESOUND].append(bluesound_player)
|
||||
async_add_entities([bluesound_player], update_before_add=True)
|
||||
|
||||
|
||||
class BluesoundPlayer(MediaPlayerEntity):
|
||||
class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity):
|
||||
"""Representation of a Bluesound Player."""
|
||||
|
||||
_attr_media_content_type = MediaType.MUSIC
|
||||
@ -102,12 +95,15 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BluesoundCoordinator,
|
||||
host: str,
|
||||
port: int,
|
||||
player: Player,
|
||||
sync_status: SyncStatus,
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
super().__init__(coordinator)
|
||||
sync_status = coordinator.data.sync_status
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._poll_status_loop_task: Task[None] | None = None
|
||||
@ -115,15 +111,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
self._id = sync_status.id
|
||||
self._last_status_update: datetime | None = None
|
||||
self._sync_status = sync_status
|
||||
self._status: Status | None = None
|
||||
self._inputs: list[Input] = []
|
||||
self._presets: list[Preset] = []
|
||||
self._status: Status = coordinator.data.status
|
||||
self._inputs: list[Input] = coordinator.data.inputs
|
||||
self._presets: list[Preset] = coordinator.data.presets
|
||||
self._group_name: str | None = None
|
||||
self._group_list: list[str] = []
|
||||
self._bluesound_device_name = sync_status.name
|
||||
self._player = player
|
||||
self._is_leader = False
|
||||
self._leader: BluesoundPlayer | None = None
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
|
||||
self._attr_unique_id = format_unique_id(sync_status.mac, port)
|
||||
# there should always be one player with the default port per mac
|
||||
@ -146,52 +141,10 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
via_device=(DOMAIN, format_mac(sync_status.mac)),
|
||||
)
|
||||
|
||||
async def _poll_status_loop(self) -> None:
|
||||
"""Loop which polls the status of the player."""
|
||||
while True:
|
||||
try:
|
||||
await self.async_update_status()
|
||||
except PlayerUnreachableError:
|
||||
_LOGGER.error(
|
||||
"Node %s:%s is offline, retrying later", self.host, self.port
|
||||
)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
except CancelledError:
|
||||
_LOGGER.debug(
|
||||
"Stopping the polling of node %s:%s", self.host, self.port
|
||||
)
|
||||
return
|
||||
except: # noqa: E722 - this loop should never stop
|
||||
_LOGGER.exception(
|
||||
"Unexpected error for %s:%s, retrying later", self.host, self.port
|
||||
)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
|
||||
async def _poll_sync_status_loop(self) -> None:
|
||||
"""Loop which polls the sync status of the player."""
|
||||
while True:
|
||||
try:
|
||||
await self.update_sync_status()
|
||||
except PlayerUnreachableError:
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
except CancelledError:
|
||||
raise
|
||||
except: # noqa: E722 - all errors must be caught for this loop
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Start the polling task."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._poll_status_loop_task = self.hass.async_create_background_task(
|
||||
self._poll_status_loop(),
|
||||
name=f"bluesound.poll_status_loop_{self.host}:{self.port}",
|
||||
)
|
||||
self._poll_sync_status_loop_task = self.hass.async_create_background_task(
|
||||
self._poll_sync_status_loop(),
|
||||
name=f"bluesound.poll_sync_status_loop_{self.host}:{self.port}",
|
||||
)
|
||||
|
||||
assert self._sync_status.id is not None
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
@ -212,105 +165,24 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
"""Stop the polling task."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
assert self._poll_status_loop_task is not None
|
||||
if self._poll_status_loop_task.cancel():
|
||||
# the sleeps in _poll_loop will raise CancelledError
|
||||
with suppress(CancelledError):
|
||||
await self._poll_status_loop_task
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._sync_status = self.coordinator.data.sync_status
|
||||
self._status = self.coordinator.data.status
|
||||
self._inputs = self.coordinator.data.inputs
|
||||
self._presets = self.coordinator.data.presets
|
||||
|
||||
assert self._poll_sync_status_loop_task is not None
|
||||
if self._poll_sync_status_loop_task.cancel():
|
||||
# the sleeps in _poll_sync_status_loop will raise CancelledError
|
||||
with suppress(CancelledError):
|
||||
await self._poll_sync_status_loop_task
|
||||
|
||||
self.hass.data[DATA_BLUESOUND].remove(self)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update internal status of the entity."""
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
with suppress(PlayerUnreachableError):
|
||||
await self.async_update_presets()
|
||||
await self.async_update_captures()
|
||||
|
||||
async def async_update_status(self) -> None:
|
||||
"""Use the poll session to always get the status of the player."""
|
||||
etag = None
|
||||
if self._status is not None:
|
||||
etag = self._status.etag
|
||||
|
||||
try:
|
||||
status = await self._player.status(
|
||||
etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
|
||||
)
|
||||
|
||||
self._attr_available = True
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
self._status = status
|
||||
|
||||
self.async_write_ha_state()
|
||||
except PlayerUnreachableError:
|
||||
self._attr_available = False
|
||||
self._last_status_update = None
|
||||
self._status = None
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.error(
|
||||
"Client connection error, marking %s as offline",
|
||||
self._bluesound_device_name,
|
||||
)
|
||||
raise
|
||||
|
||||
async def update_sync_status(self) -> None:
|
||||
"""Update the internal status."""
|
||||
etag = None
|
||||
if self._sync_status:
|
||||
etag = self._sync_status.etag
|
||||
sync_status = await self._player.sync_status(
|
||||
etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
|
||||
)
|
||||
|
||||
self._sync_status = sync_status
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
|
||||
self._group_list = self.rebuild_bluesound_group()
|
||||
|
||||
if sync_status.leader is not None:
|
||||
self._is_leader = False
|
||||
leader_id = f"{sync_status.leader.ip}:{sync_status.leader.port}"
|
||||
leader_device = [
|
||||
device
|
||||
for device in self.hass.data[DATA_BLUESOUND]
|
||||
if device.id == leader_id
|
||||
]
|
||||
|
||||
if leader_device and leader_id != self.id:
|
||||
self._leader = leader_device[0]
|
||||
else:
|
||||
self._leader = None
|
||||
_LOGGER.error("Leader not found %s", leader_id)
|
||||
else:
|
||||
if self._leader is not None:
|
||||
self._leader = None
|
||||
followers = self._sync_status.followers
|
||||
self._is_leader = followers is not None
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update_captures(self) -> None:
|
||||
"""Update Capture sources."""
|
||||
inputs = await self._player.inputs()
|
||||
self._inputs = inputs
|
||||
|
||||
async def async_update_presets(self) -> None:
|
||||
"""Update Presets."""
|
||||
presets = await self._player.presets()
|
||||
self._presets = presets
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the state of the device."""
|
||||
if self._status is None:
|
||||
if self.available is False:
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
if self.is_grouped and not self.is_leader:
|
||||
@ -327,7 +199,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_title(self) -> str | None:
|
||||
"""Title of current playing media."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
return self._status.name
|
||||
@ -335,7 +207,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
if self._status is None:
|
||||
if self.available is False:
|
||||
return None
|
||||
|
||||
if self.is_grouped and not self.is_leader:
|
||||
@ -346,7 +218,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_album_name(self) -> str | None:
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
return self._status.album
|
||||
@ -354,7 +226,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_image_url(self) -> str | None:
|
||||
"""Image url of current playing media."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
url = self._status.image
|
||||
@ -369,7 +241,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of current playing media in seconds."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
mediastate = self.state
|
||||
@ -388,7 +260,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_duration(self) -> int | None:
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
duration = self._status.total_seconds
|
||||
@ -405,16 +277,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def volume_level(self) -> float | None:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
volume = None
|
||||
volume = self._status.volume
|
||||
|
||||
if self._status is not None:
|
||||
volume = self._status.volume
|
||||
if self.is_grouped:
|
||||
volume = self._sync_status.volume
|
||||
|
||||
if volume is None:
|
||||
return None
|
||||
|
||||
return volume / 100
|
||||
|
||||
@property
|
||||
@ -447,7 +314,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def source_list(self) -> list[str] | None:
|
||||
"""List of available input sources."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
sources = [x.text for x in self._inputs]
|
||||
@ -458,7 +325,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Name of the current input source."""
|
||||
if self._status is None or (self.is_grouped and not self.is_leader):
|
||||
if self.available is False or (self.is_grouped and not self.is_leader):
|
||||
return None
|
||||
|
||||
if self._status.input_id is not None:
|
||||
@ -475,7 +342,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag of media commands that are supported."""
|
||||
if self._status is None:
|
||||
if self.available is False:
|
||||
return MediaPlayerEntityFeature(0)
|
||||
|
||||
if self.is_grouped and not self.is_leader:
|
||||
@ -577,16 +444,21 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if self.sync_status.leader is None and self.sync_status.followers is None:
|
||||
return []
|
||||
|
||||
player_entities: list[BluesoundPlayer] = self.hass.data[DATA_BLUESOUND]
|
||||
config_entries: list[BluesoundConfigEntry] = (
|
||||
self.hass.config_entries.async_entries(DOMAIN)
|
||||
)
|
||||
sync_status_list = [
|
||||
x.runtime_data.coordinator.data.sync_status for x in config_entries
|
||||
]
|
||||
|
||||
leader_sync_status: SyncStatus | None = None
|
||||
if self.sync_status.leader is None:
|
||||
leader_sync_status = self.sync_status
|
||||
else:
|
||||
required_id = f"{self.sync_status.leader.ip}:{self.sync_status.leader.port}"
|
||||
for x in player_entities:
|
||||
if x.sync_status.id == required_id:
|
||||
leader_sync_status = x.sync_status
|
||||
for sync_status in sync_status_list:
|
||||
if sync_status.id == required_id:
|
||||
leader_sync_status = sync_status
|
||||
break
|
||||
|
||||
if leader_sync_status is None or leader_sync_status.followers is None:
|
||||
@ -594,9 +466,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
follower_ids = [f"{x.ip}:{x.port}" for x in leader_sync_status.followers]
|
||||
follower_names = [
|
||||
x.sync_status.name
|
||||
for x in player_entities
|
||||
if x.sync_status.id in follower_ids
|
||||
sync_status.name
|
||||
for sync_status in sync_status_list
|
||||
if sync_status.id in follower_ids
|
||||
]
|
||||
follower_names.insert(0, leader_sync_status.name)
|
||||
return follower_names
|
||||
|
@ -9,7 +9,6 @@
|
||||
'media_artist': 'artist',
|
||||
'media_content_type': <MediaType.MUSIC: 'music'>,
|
||||
'media_duration': 123,
|
||||
'media_position': 2,
|
||||
'media_title': 'song',
|
||||
'shuffle': False,
|
||||
'source_list': list([
|
||||
|
@ -127,7 +127,9 @@ async def test_attributes_set(
|
||||
) -> None:
|
||||
"""Test the media player attributes set."""
|
||||
state = hass.states.get("media_player.player_name1111")
|
||||
assert state == snapshot(exclude=props("media_position_updated_at"))
|
||||
assert state == snapshot(
|
||||
exclude=props("media_position_updated_at", "media_position")
|
||||
)
|
||||
|
||||
|
||||
async def test_stop_maps_to_idle(
|
||||
|
Loading…
x
Reference in New Issue
Block a user