CONF_GROUP_MEMBERS,
CONF_HIDE_PLAYER,
CONF_PLAYERS,
+ CONF_PREVENT_SYNC_LEADER_OFF,
CONF_SYNC_LEADER,
+ CONF_SYNCGROUP_DEFAULT_ON,
CONF_TTS_PRE_ANNOUNCE,
SYNCGROUP_PREFIX,
)
if active_group_player_id := self._get_active_player_group(player):
active_group_player = self.get(active_group_player_id)
group_player_state = active_group_player.state
+ if not powered and active_group_player.type == PlayerType.SYNC_GROUP:
+ # handle 'prevent sync leader off' feature
+ powered_members = list(self.iter_group_members(active_group_player, True))
+ sync_leader = self.get_sync_leader(active_group_player)
+ if (
+ len(powered_members) > 1
+ and (sync_leader == player)
+ and self.mass.config.get_raw_player_config_value(
+ active_group_player_id, CONF_PREVENT_SYNC_LEADER_OFF, False
+ )
+ ):
+ raise PlayerCommandFailed(
+ f"{player.display_name} is the sync "
+ "leader of a syncgroup and cannot be turned off"
+ )
else:
active_group_player = None
if not power and group_player.state in (PlayerState.PLAYING, PlayerState.PAUSED):
await self.cmd_stop(player_id)
+ default_on_pref = self.mass.config.get_raw_player_config_value(
+ group_player.player_id, CONF_SYNCGROUP_DEFAULT_ON, "powered_only"
+ )
+
# handle syncgroup - this will also work for temporary syncgroups
# where players are manually synced against a group leader
any_member_powered = False
async with TaskManager(self.mass) as tg:
- for member in self.iter_group_members(group_player, only_powered=True):
+ for member in self.iter_group_members(
+ group_player, only_powered=(default_on_pref != "always_all")
+ ):
any_member_powered = True
if power:
if member.state in (PlayerState.PLAYING, PlayerState.PAUSED):
member.active_source = None
member.active_group = None
self.update(member.player_id, skip_forward=True)
- # edge case: group turned on but no members are powered, power them all!
- if not any_member_powered and power:
+ # handle default power ON
+ if power:
+ sync_leader = self.get_sync_leader(group_player)
for member in self.iter_group_members(group_player, only_powered=False):
- tg.create_task(self.cmd_power(member.player_id, True))
- member.active_group = group_player.player_id
- member.active_source = group_player.active_source
+ if default_on_pref == "always_all" or (
+ sync_leader
+ and default_on_pref == "always_leader"
+ and member.player_id == sync_leader.player_id
+ ):
+ tg.create_task(self.cmd_power(member.player_id, True))
+ member.active_group = group_player.player_id
+ member.active_source = group_player.active_source
+ any_member_powered = True
+ if not any_member_powered:
+ return
if power and group_player.player_id.startswith(SYNCGROUP_PREFIX):
await self.sync_syncgroup(group_player.player_id)
from abc import abstractmethod
from music_assistant.common.models.config_entries import (
+ BASE_PLAYER_CONFIG_ENTRIES,
CONF_ENTRY_ANNOUNCE_VOLUME,
CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
- CONF_ENTRY_AUTO_PLAY,
- CONF_ENTRY_FLOW_MODE,
- CONF_ENTRY_HIDE_PLAYER,
- CONF_ENTRY_HTTP_PROFILE_FORCED_1,
- CONF_ENTRY_PLAYER_ICON,
CONF_ENTRY_PLAYER_ICON_GROUP,
- CONF_ENTRY_SAMPLE_RATES,
- CONF_ENTRY_TTS_PRE_ANNOUNCE,
- CONF_ENTRY_VOLUME_NORMALIZATION,
- CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
ConfigEntry,
ConfigValueOption,
PlayerConfig,
)
from music_assistant.common.models.enums import ConfigEntryType, PlayerState
from music_assistant.common.models.player import Player, PlayerMedia
-from music_assistant.constants import CONF_GROUP_MEMBERS, CONF_SYNC_LEADER, SYNCGROUP_PREFIX
+from music_assistant.constants import (
+ CONF_GROUP_MEMBERS,
+ CONF_PREVENT_SYNC_LEADER_OFF,
+ CONF_SYNC_LEADER,
+ CONF_SYNCGROUP_DEFAULT_ON,
+ SYNCGROUP_PREFIX,
+)
from .provider import Provider
async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]:
"""Return all (provider/player specific) Config Entries for the given player (if any)."""
- entries = (
- CONF_ENTRY_PLAYER_ICON,
- CONF_ENTRY_FLOW_MODE,
- CONF_ENTRY_VOLUME_NORMALIZATION,
- CONF_ENTRY_AUTO_PLAY,
- CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
- CONF_ENTRY_HIDE_PLAYER,
- CONF_ENTRY_TTS_PRE_ANNOUNCE,
- CONF_ENTRY_SAMPLE_RATES,
- CONF_ENTRY_HTTP_PROFILE_FORCED_1,
- )
if player_id.startswith(SYNCGROUP_PREFIX):
- # add default entries for syncgroups
- entries = (
- *entries,
+ # default entries for syncgroups
+ return (
+ *BASE_PLAYER_CONFIG_ENTRIES,
+ CONF_ENTRY_PLAYER_ICON_GROUP,
ConfigEntry(
key=CONF_GROUP_MEMBERS,
type=ConfigEntryType.STRING,
"the sync leader, select it here.",
required=True,
),
- CONF_ENTRY_PLAYER_ICON_GROUP,
+ ConfigEntry(
+ key=CONF_PREVENT_SYNC_LEADER_OFF,
+ type=ConfigEntryType.BOOLEAN,
+ label="Prevent sync leader power off",
+ default_value=False,
+ description="With this setting enabled, Music Assistant will disallow powering "
+ "off the sync leader player if other players are still "
+ "active in the sync group. This is useful if you want to prevent "
+ "a short drop in the music while the music is transferred to another player.",
+ required=True,
+ ),
+ ConfigEntry(
+ key=CONF_SYNCGROUP_DEFAULT_ON,
+ type=ConfigEntryType.STRING,
+ label="Default power ON behavior",
+ default_value="powered_only",
+ options=(
+ ConfigValueOption("Always power ON all child devices", "always_all"),
+ ConfigValueOption("Always power ON sync leader", "always_leader"),
+ ConfigValueOption("Start with powered players", "powered_only"),
+ ConfigValueOption("Ignore", "ignore"),
+ ),
+ description="What should happen if you power ON a sync group "
+ "(or you start playback to it), while no (or not all) players "
+ "are powered ON ?\n\nShould Music Assistant power ON all players, or only the "
+ "sync leader, or should it ignore the command if no players are powered ON ?",
+ required=False,
+ ),
)
- if not player_id.startswith(SYNCGROUP_PREFIX):
+
+ return (
+ *BASE_PLAYER_CONFIG_ENTRIES,
# add default entries for announce feature
- entries = (
- *entries,
- CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
- CONF_ENTRY_ANNOUNCE_VOLUME,
- CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
- CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
- )
- return entries
+ CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+ CONF_ENTRY_ANNOUNCE_VOLUME,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+ )
def on_player_config_changed(self, config: PlayerConfig, changed_keys: set[str]) -> None:
"""Call (by config manager) when the configuration of a player changes."""