all_entries: list[ConfigEntry] = []
output_protocols = player.output_protocols
- if not player.available:
- # if player is not available, we cannot reliably determine the available protocols
- # so we return no options to avoid confusion
- return all_entries
-
# Build options from available output protocols, sorted by priority
options: list[ConfigValueOption] = []
default_value: str | None = None
for player in list(self._players.values())
if (player.state.available or return_unavailable)
and (player.state.enabled or return_disabled)
+ and player.initialized.is_set()
and (provider_filter is None or player.provider.instance_id == provider_filter)
and (
not user_filter
player.extra_data[ATTR_FAKE_POWER] = cached_value
# finally actually register it
- self._players[player_id] = player
- # update state without signaling event first (ensure all attributes are set)
- player.update_state(signal_event=False)
+ # Despite the fact that the player is not fully ready yet
+ # (config not loaded, protocol links not evaluated),
+ # we already add it to the _players dict here because we
+ # want to make sure the player is available in the controller
+ # during the rest of the registration process
+ # (such as when fetching config or evaluating protocol links).
+ # We use the 'initialized' attribute to indicate that the player
+ # is still in the process of being registered so we can filter it out where needed.
+ self._players[player_id] = player
# ensure we fetch and set the latest/full config for the player
player_config = await self.mass.config.get_player_config(player_id)
player.set_config(player_config)
+ # update state without signaling event first (ensures all attributes are set)
+ player.update_state(signal_event=False)
# call hook after the player is registered and config is set
await player.on_config_updated()
await self._enrich_player_identifiers(player)
self._evaluate_protocol_links(player)
+ # now we're ready to signal the player is added and available
+ player.set_initialized()
self.logger.info(
"Player (type %s) registered: %s/%s",
player.state.type.value,
player.state.name,
)
# signal event that a player was added
-
if player.state.type != PlayerType.PROTOCOL:
self.mass.signal_event(
EventType.PLAYER_ADDED, object_id=player.player_id, data=player
)
-
- # register playerqueue for this player
- # Skip if this is a protocol player pending evaluation (queue created when promoted)
- if (
- player.state.type != PlayerType.PROTOCOL
- and player.player_id not in self._pending_protocol_evaluations
- ):
+ # register playerqueue for this player (if not a protocol player)
+ if player.state.type != PlayerType.PROTOCOL:
await self.mass.player_queues.on_player_register(player)
- # always call update to fix special attributes like display name, group volume etc.
- player.update_state()
-
# Schedule debounced update of all players since can_group_with values may change
# when a new player is added (provider IDs expand to include the new player)
- self._schedule_update_all_players()
+ self._schedule_update_all_players(5)
async def register_or_update(self, player: Player) -> None:
"""Register a new player on the controller or update existing one."""
from __future__ import annotations
+import asyncio
import time
from abc import ABC
from collections.abc import Callable
self._extra_attributes: dict[str, Any] = {}
self._on_unload_callbacks: list[Callable[[], None]] = []
self.__active_mass_source: str | None = None
+ self.__initialized = asyncio.Event()
# The PlayerState is the (snapshotted) final state of the player
# after applying any config overrides and other transformations,
# such as the display name and player controls.
"""Return if the player is enabled."""
return self._config.enabled
+ @property
+ @final
+ def initialized(self) -> asyncio.Event:
+ """
+ Return if the player is initialized.
+
+ Used by player controller to indicate initial registration completed.
+ """
+ return self.__initialized
+
@property
def corrected_elapsed_time(self) -> float | None:
"""Return the corrected/realtime elapsed time."""
"""
# TODO: validate that caller is the PlayerController ?
self._config = config
- self.mass.players.trigger_player_update(self.player_id)
+
+ @final
+ def set_initialized(self) -> None:
+ """Set the player as initialized."""
+ self.__initialized.set()
@final
def to_dict(self) -> dict[str, Any]:
self._set_attributes()
async def on_config_updated(self) -> None:
- """Handle logic when the player is loaded or updated."""
+ """Handle logic when the PlayerConfig is first loaded or updated."""
# OPTIONAL
# This method is optional and should be implemented if you need to handle
# any initialization logic after the config was initially loaded or updated.
]
async def on_config_updated(self) -> None:
- """Handle config updates - resend Sendspin config if needed."""
+ """Handle config load/update - resend Sendspin config if needed."""
if not self.sendspin_mode_enabled:
return
self.update_state()
async def on_config_updated(self) -> None:
- """Apply preferred format when config changes."""
+ """Handle logic when the PlayerConfig is first loaded or updated."""
await self._apply_preferred_format()
async def _apply_preferred_format(self) -> None:
)
async def on_config_updated(self) -> None:
- """Handle logic when the player is registered or the config was updated."""
+ """Handle logic when the PlayerConfig is first loaded or updated."""
# set presets and display
await self._set_preset_items()
await self._set_display()
return None
async def on_config_updated(self) -> None:
- """Handle logic when the player is loaded or updated."""
+ """Handle logic when the PlayerConfig is first loaded or updated."""
# Config is only available after the player was registered
self._cache.clear() # clear to prevent loading old is_dynamic
default_members = cast("list[str]", self.config.get_value(CONF_GROUP_MEMBERS, []))
}
async def on_config_updated(self) -> None:
- """Handle logic when the player is loaded or updated."""
+ """Handle logic when the PlayerConfig is first loaded or updated."""
static_members = cast("list[str]", self.config.get_value(CONF_GROUP_MEMBERS, []))
self._attr_static_group_members = static_members.copy()
if not self.powered: