172 lines
5.6 KiB
Python
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()),
|
|
]
|