Skip to content

Commit

Permalink
Fix: announcements on HA players
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Nov 21, 2024
1 parent 790c6aa commit ec9c476
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 59 deletions.
13 changes: 4 additions & 9 deletions music_assistant/controllers/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from music_assistant.helpers.api import api_command
from music_assistant.helpers.tags import parse_tags
from music_assistant.helpers.throttle_retry import Throttler
from music_assistant.helpers.util import TaskManager, get_changed_values
from music_assistant.helpers.util import TaskManager, get_changed_values, lock
from music_assistant.models.core_controller import CoreController
from music_assistant.models.player_provider import PlayerProvider

Expand Down Expand Up @@ -214,9 +214,6 @@ async def cmd_pause(self, player_id: str) -> None:
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id)
if player.announcement_in_progress:
self.logger.warning("Ignore command: An announcement is in progress")
return
if PlayerFeature.PAUSE not in player.supported_features:
# if player does not support pause, we need to send stop
self.logger.info(
Expand Down Expand Up @@ -526,6 +523,7 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
await player_provider.cmd_volume_mute(player_id, muted)

@api_command("players/cmd/play_announcement")
@lock
async def play_announcement(
self,
player_id: str,
Expand All @@ -537,10 +535,6 @@ async def play_announcement(
player = self.get(player_id, True)
if not url.startswith("http"):
raise PlayerCommandFailed("Only URLs are supported for announcements")
if player.announcement_in_progress:
raise PlayerCommandFailed(
f"An announcement is already in progress to player {player.display_name}"
)
try:
# mark announcement_in_progress on player
player.announcement_in_progress = True
Expand Down Expand Up @@ -589,7 +583,8 @@ async def play_announcement(
# handle native announce support
if native_announce_support:
if prov := self.mass.get_provider(player.provider):
await prov.play_announcement(player_id, announcement, volume_level)
announcement_volume = self.get_announcement_volume(player_id, volume_level)
await prov.play_announcement(player_id, announcement, announcement_volume)
return
# use fallback/default implementation
await self._play_announcement(player, announcement, volume_level)
Expand Down
123 changes: 73 additions & 50 deletions music_assistant/providers/hass_players/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import asyncio
import time
from enum import IntFlag
from typing import TYPE_CHECKING, Any
Expand All @@ -26,6 +27,7 @@
CONF_ENTRY_HTTP_PROFILE,
)
from music_assistant.helpers.datetime import from_iso_string
from music_assistant.helpers.tags import parse_tags
from music_assistant.models.player_provider import PlayerProvider
from music_assistant.providers.hass import DOMAIN as HASS_DOMAIN

Expand Down Expand Up @@ -93,6 +95,7 @@ class MediaPlayerEntityFeature(IntFlag):
CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
CONF_ENTRY_HTTP_PROFILE,
CONF_ENTRY_ENABLE_ICY_METADATA,
CONF_ENTRY_FLOW_MODE_ENFORCED,
)


Expand All @@ -103,7 +106,7 @@ async def _get_hass_media_players(
for state in await hass_prov.hass.get_states():
if not state["entity_id"].startswith("media_player"):
continue
if "mass_player_id" in state["attributes"]:
if "mass_player_type" in state["attributes"]:
# filter out mass players
continue
if "friendly_name" not in state["attributes"]:
Expand All @@ -115,16 +118,6 @@ async def _get_hass_media_players(
yield state


async def _get_hass_media_player(
hass_prov: HomeAssistantProvider, entity_id: str
) -> HassState | None:
"""Return Hass state object for a single media_player entity."""
for state in await hass_prov.hass.get_states():
if state["entity_id"] == entity_id:
return state
return None


async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
) -> ProviderInstanceType:
Expand Down Expand Up @@ -202,16 +195,8 @@ async def get_player_config_entries(
player_id: str,
) -> tuple[ConfigEntry, ...]:
"""Return all (provider/player specific) Config Entries for the given player (if any)."""
entries = await super().get_player_config_entries(player_id)
entries = entries + PLAYER_CONFIG_ENTRIES
if hass_state := await _get_hass_media_player(self.hass_prov, player_id):
hass_supported_features = MediaPlayerEntityFeature(
hass_state["attributes"]["supported_features"]
)
if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in hass_supported_features:
entries += (CONF_ENTRY_FLOW_MODE_ENFORCED,)

return entries
base_entries = await super().get_player_config_entries(player_id)
return base_entries + PLAYER_CONFIG_ENTRIES

async def cmd_stop(self, player_id: str) -> None:
"""Send STOP command to given player.
Expand Down Expand Up @@ -281,20 +266,39 @@ async def play_media(self, player_id: str, media: PlayerMedia) -> None:
player.elapsed_time = 0
player.elapsed_time_last_updated = time.time()

async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
"""Handle enqueuing of the next queue item on the player."""
if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True):
media.uri = media.uri.replace(".flac", ".mp3")
async def play_announcement(
self, player_id: str, announcement: PlayerMedia, volume_level: int | None = None
) -> None:
"""Handle (provider native) playback of an announcement on given player."""
player = self.mass.players.get(player_id, True)
self.logger.info(
"Playing announcement %s on %s",
announcement.uri,
player.display_name,
)
if volume_level is not None:
self.logger.warning(
"Announcement volume level is not supported for player %s", player.display_name
)
await self.hass_prov.hass.call_service(
domain="media_player",
service="play_media",
service_data={
"media_content_id": media.uri,
"media_content_id": announcement.uri,
"media_content_type": "music",
"enqueue": "next",
"announce": True,
},
target={"entity_id": player_id},
)
# Wait until the announcement is finished playing
# This is helpful for people who want to play announcements in a sequence
media_info = await parse_tags(announcement.uri)
duration = media_info.duration or 5
await asyncio.sleep(duration)
self.logger.debug(
"Playing announcement on %s completed",
player.display_name,
)

async def cmd_power(self, player_id: str, powered: bool) -> None:
"""Send POWER command to given player.
Expand Down Expand Up @@ -372,41 +376,60 @@ async def _setup_player(
) -> None:
"""Handle setup of a Player from an hass entity."""
hass_device: HassDevice | None = None
hass_domain: str | None = None
if entity_registry_entry := entity_registry.get(state["entity_id"]):
hass_device = device_registry.get(entity_registry_entry["device_id"])
hass_domain = entity_registry_entry["platform"]

dev_info: dict[str, Any] = {}
if hass_device and (model := hass_device.get("model")):
dev_info["model"] = model
if hass_device and (manufacturer := hass_device.get("manufacturer")):
dev_info["manufacturer"] = manufacturer
if hass_device and (model_id := hass_device.get("model_id")):
dev_info["model_id"] = model_id
if hass_device and (sw_version := hass_device.get("sw_version")):
dev_info["software_version"] = sw_version
if hass_device and (connections := hass_device.get("connections")):
for key, value in connections:
if key == "mac":
dev_info["mac_address"] = value

player = Player(
player_id=state["entity_id"],
provider=self.instance_id,
type=PlayerType.PLAYER,
name=state["attributes"]["friendly_name"],
available=state["state"] not in ("unavailable", "unknown"),
powered=state["state"] not in ("unavailable", "unknown", "standby", "off"),
device_info=DeviceInfo.from_dict(dev_info),
state=StateMap.get(state["state"], PlayerState.IDLE),
)
# work out supported features
hass_supported_features = MediaPlayerEntityFeature(
state["attributes"]["supported_features"]
)
supported_features: set[PlayerFeature] = set()
if MediaPlayerEntityFeature.PAUSE in hass_supported_features:
supported_features.add(PlayerFeature.PAUSE)
player.supported_features.add(PlayerFeature.PAUSE)
if MediaPlayerEntityFeature.VOLUME_SET in hass_supported_features:
supported_features.add(PlayerFeature.VOLUME_SET)
player.supported_features.add(PlayerFeature.VOLUME_SET)
if MediaPlayerEntityFeature.VOLUME_MUTE in hass_supported_features:
supported_features.add(PlayerFeature.VOLUME_MUTE)
if MediaPlayerEntityFeature.MEDIA_ENQUEUE in hass_supported_features:
supported_features.add(PlayerFeature.ENQUEUE)
player.supported_features.add(PlayerFeature.VOLUME_MUTE)
if MediaPlayerEntityFeature.MEDIA_ANNOUNCE in hass_supported_features:
player.supported_features.add(PlayerFeature.PLAY_ANNOUNCEMENT)
if hass_domain and MediaPlayerEntityFeature.GROUPING in hass_supported_features:
player.supported_features.add(PlayerFeature.SET_MEMBERS)
player.can_group_with = {
x["entity_id"]
for x in entity_registry.values()
if x["entity_id"].startswith("media_player") and x["platform"] == hass_domain
}
if (
MediaPlayerEntityFeature.TURN_ON in hass_supported_features
and MediaPlayerEntityFeature.TURN_OFF in hass_supported_features
):
supported_features.add(PlayerFeature.POWER)
player = Player(
player_id=state["entity_id"],
provider=self.instance_id,
type=PlayerType.PLAYER,
name=state["attributes"]["friendly_name"],
available=state["state"] not in ("unavailable", "unknown"),
powered=state["state"] not in ("unavailable", "unknown", "standby", "off"),
device_info=DeviceInfo(
model=hass_device["model"] if hass_device else "Unknown model",
manufacturer=(
hass_device["manufacturer"] if hass_device else "Unknown Manufacturer"
),
),
supported_features=supported_features,
state=StateMap.get(state["state"], PlayerState.IDLE),
)
player.supported_features.add(PlayerFeature.POWER)

self._update_player_attributes(player, state["attributes"])
await self.mass.players.register_or_update(player)

Expand Down

0 comments on commit ec9c476

Please sign in to comment.