Typing lessons like duolingo & musicmouse cleanup

This commit is contained in:
2026-09-12 18:58:02 +02:00
parent 498243af46
commit a5210fead2
50 changed files with 3074 additions and 710 deletions

View File

@@ -23,7 +23,7 @@ from musicmouse.events import (
PlayAlbumRequested,
PlayRequested,
PlaySeriesLatestRequested,
PrevTrackRequested,
PreviousTrackRequested,
SetVolumeRequested,
VolumeChangeRequested,
)
@@ -40,8 +40,8 @@ _TRANSPORT: dict[str, Callable[[], IntentEvent]] = {
# pauses, like its pause button.
"KEY_PAUSE": lambda: PauseRequested(source="lirc"),
"KEY_STOP": lambda: PauseRequested(source="lirc"),
"KEY_PREVIOUS": lambda: PrevTrackRequested(source="lirc"),
"KEY_REWIND": lambda: PrevTrackRequested(source="lirc"),
"KEY_PREVIOUS": lambda: PreviousTrackRequested(source="lirc"),
"KEY_REWIND": lambda: PreviousTrackRequested(source="lirc"),
"KEY_FORWARD": lambda: NextTrackRequested(source="lirc"),
}

View File

@@ -13,6 +13,7 @@ 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,
@@ -22,7 +23,7 @@ from musicmouse.events import (
PauseRequested,
PlaybackChanged,
PlayRequested,
PrevTrackRequested,
PreviousTrackRequested,
SetVolumeRequested,
TrackChanged,
VolumeChanged,
@@ -37,9 +38,11 @@ __all__ = ["PlayerSensor", "TransportButton", "VolumeNumber", "player_entities"]
class PlayerSensor(Entity):
component = "sensor"
def __init__(self, bus: EventBus, config: MqttConfig, player: Player) -> None:
def __init__(
self, bus: EventBus, config: MqttConfig, mouse: MusicMouseDevice, player: Player
) -> None:
self.mouse = mouse
self.player = player
self._figure: str | None = None
super().__init__(bus, config, object_id="player", name="Music Mouse Player")
def discovery_payload(self) -> dict[str, Any]:
@@ -56,9 +59,7 @@ class PlayerSensor(Entity):
for event_type in (PlaybackChanged, TrackChanged, VolumeChanged, ActiveFigureChanged):
self.bus.subscribe(event_type, self._on_change)
async def _on_change(self, event: Event) -> None:
if isinstance(event, ActiveFigureChanged):
self._figure = event.figure
async def _on_change(self, _event: Event) -> None:
await self.publish_state()
async def publish_state(self) -> None:
@@ -68,7 +69,7 @@ class PlayerSensor(Entity):
await self.publish(
f"{self.base_topic}/attributes",
{
"figure": self._figure,
"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,
@@ -128,7 +129,7 @@ class TransportButton(Entity):
#: Button object id -> the intent pressing it emits.
INTENTS: ClassVar[dict[str, type[IntentEvent]]] = {
"next": NextTrackRequested,
"previous": PrevTrackRequested,
"previous": PreviousTrackRequested,
"play": PlayRequested,
"pause": PauseRequested,
}
@@ -154,7 +155,9 @@ class TransportButton(Entity):
self.bus.emit(self.INTENTS[self.action](source="mqtt"))
def player_entities(bus: EventBus, config: MqttConfig, player: Player) -> list[Entity]:
def player_entities(
bus: EventBus, config: MqttConfig, mouse: MusicMouseDevice, player: Player
) -> list[Entity]:
names = {
"next": "Music Mouse Next",
"previous": "Music Mouse Previous",
@@ -162,7 +165,7 @@ def player_entities(bus: EventBus, config: MqttConfig, player: Player) -> list[E
"pause": "Music Mouse Pause",
}
return [
PlayerSensor(bus, config, player),
PlayerSensor(bus, config, mouse, player),
VolumeNumber(bus, config, player),
*(TransportButton(bus, config, action, name) for action, name in names.items()),
]

View File

@@ -41,7 +41,7 @@ def build_entities(
"""Everything this backend exposes to Home Assistant."""
return [
*(LightEntity(bus, config, mouse, zone, ZONE_NAMES[zone]) for zone in LedZone),
*player_entities(bus, config, player),
*player_entities(bus, config, mouse, player),
*trigger_entities(bus, config),
]

View File

@@ -27,7 +27,7 @@ from musicmouse.events import (
PauseRequested,
PlayAlbumRequested,
PlayRequested,
PrevTrackRequested,
PreviousTrackRequested,
SeekRequested,
SetVolumeRequested,
)
@@ -152,7 +152,7 @@ def build_router(
@router.post("/previous", status_code=204)
def previous_track() -> Response:
return emit(PrevTrackRequested(source="web"))
return emit(PreviousTrackRequested(source="web"))
@router.post("/seek", status_code=204)
def seek(body: SeekIn) -> Response:

View File

@@ -34,6 +34,8 @@ __all__ = [
def to_device_volume(percent: int, general: GeneralConfig) -> int:
"""Map 0..100 onto the configured range, so 100 % is exactly ``max_volume``."""
span = general.max_volume - general.min_volume
if span <= 0:
return general.min_volume
return general.min_volume + round(span * max(0, min(100, percent)) / 100)

View File

@@ -17,7 +17,7 @@ __all__ = ["snapshot"]
def snapshot(app: App) -> PlayerStateOut:
player = app.player
playlist = player.playlist
album = app.library.get(playlist.album_id if playlist else None)
album = app.album_for(playlist)
index = player.track_index
# The player only knows a file path, and a tagged file's name is rarely its title
# ("01 - So ein schoener Tag.mp3"). The library read the real one at scan time.