Files
musicmouse/python-backend/musicmouse/services/mqtt/player.py

172 lines
5.6 KiB
Python

"""The audio player, exposed to Home Assistant.
Home Assistant has no MQTT ``media_player`` platform, so the player is published as
the pieces that do exist: a sensor for what is going on, a number for the volume, and
buttons for the transport. Commands come back in as intents, which is the same path
the physical buttons take.
"""
from __future__ import annotations
import logging
from typing import Any, ClassVar
from musicmouse.bus import EventBus
from musicmouse.config import MqttConfig
from musicmouse.devices.mouse import MusicMouseDevice
from musicmouse.devices.player import Player
from musicmouse.events import (
ActiveFigureChanged,
Event,
IntentEvent,
NextTrackRequested,
PauseRequested,
PlaybackChanged,
PlayRequested,
PreviousTrackRequested,
SetVolumeRequested,
TrackChanged,
VolumeChanged,
)
from musicmouse.services.mqtt.entity import Entity
_log = logging.getLogger(__name__)
__all__ = ["PlayerSensor", "TransportButton", "VolumeNumber", "player_entities"]
class PlayerSensor(Entity):
component = "sensor"
def __init__(
self, bus: EventBus, config: MqttConfig, mouse: MusicMouseDevice, player: Player
) -> None:
self.mouse = mouse
self.player = player
super().__init__(bus, config, object_id="player", name="Music Mouse Player")
def discovery_payload(self) -> dict[str, Any]:
return {
"name": self.name,
"unique_id": self.unique_id,
"state_topic": self.state_topic,
"json_attributes_topic": f"{self.base_topic}/attributes",
"icon": "mdi:music-circle",
"device": self.device_block(),
}
def subscribe(self) -> None:
for event_type in (PlaybackChanged, TrackChanged, VolumeChanged, ActiveFigureChanged):
self.bus.subscribe(event_type, self._on_change)
async def _on_change(self, _event: Event) -> None:
await self.publish_state()
async def publish_state(self) -> None:
track = self.player.current_track
playlist = self.player.playlist
await self.publish(self.state_topic, "playing" if self.player.is_playing else "paused")
await self.publish(
f"{self.base_topic}/attributes",
{
"figure": self.mouse.active_figure,
"playlist": playlist.name if playlist else None,
"track_index": self.player.track_index,
"track_count": len(playlist) if playlist else 0,
"title": track.title if track else None,
"volume": self.player.volume,
},
)
class VolumeNumber(Entity):
component = "number"
def __init__(self, bus: EventBus, config: MqttConfig, player: Player) -> None:
self.player = player
self._min = 0
self._max = 100
super().__init__(bus, config, object_id="volume", name="Music Mouse Volume")
def discovery_payload(self) -> dict[str, Any]:
return {
"name": self.name,
"unique_id": self.unique_id,
"command_topic": self.command_topic,
"state_topic": self.state_topic,
"min": self._min,
"max": self._max,
"step": 1,
"mode": "slider",
"icon": "mdi:volume-high",
"device": self.device_block(),
}
def command_topics(self) -> tuple[str, ...]:
return (self.command_topic,)
def subscribe(self) -> None:
self.bus.subscribe(VolumeChanged, self._on_volume)
async def _on_volume(self, _event: VolumeChanged) -> None:
await self.publish_state()
async def handle(self, topic: str, payload: str) -> None:
try:
volume = int(float(payload))
except ValueError:
_log.warning("Ignoring non-numeric volume on %s: %r", topic, payload[:40])
return
self.bus.emit(SetVolumeRequested(volume=volume, source="mqtt"))
async def publish_state(self) -> None:
await self.publish(self.state_topic, str(self.player.volume))
class TransportButton(Entity):
component = "button"
#: Button object id -> the intent pressing it emits.
INTENTS: ClassVar[dict[str, type[IntentEvent]]] = {
"next": NextTrackRequested,
"previous": PreviousTrackRequested,
"play": PlayRequested,
"pause": PauseRequested,
}
def __init__(self, bus: EventBus, config: MqttConfig, action: str, name: str) -> None:
self.action = action
super().__init__(bus, config, object_id=f"button_{action}", name=name)
def discovery_payload(self) -> dict[str, Any]:
return {
"name": self.name,
"unique_id": self.unique_id,
"command_topic": self.command_topic,
"device": self.device_block(),
}
def command_topics(self) -> tuple[str, ...]:
return (self.command_topic,)
async def handle(self, topic: str, payload: str) -> None:
# HA publishes "PRESS"; the payload carries no information beyond "it happened".
_log.debug("Transport button %s pressed via %s (%r)", self.action, topic, payload[:20])
self.bus.emit(self.INTENTS[self.action](source="mqtt"))
def player_entities(
bus: EventBus, config: MqttConfig, mouse: MusicMouseDevice, player: Player
) -> list[Entity]:
names = {
"next": "Music Mouse Next",
"previous": "Music Mouse Previous",
"play": "Music Mouse Play",
"pause": "Music Mouse Pause",
}
return [
PlayerSensor(bus, config, mouse, player),
VolumeNumber(bus, config, player),
*(TransportButton(bus, config, action, name) for action, name in names.items()),
]