Files
musicmouse/python-backend
Martin Bauer d44c24ec97 Full rearchitecture using Claude
- event bus systen
- all components are independent
- preparation for web frontend
2026-08-26 13:22:28 +02:00
..
2026-08-26 13:22:28 +02:00
2026-08-26 13:22:28 +02:00
2026-08-26 13:22:28 +02:00

MusicMouse backend

The host application: it reads RFID tags, buttons and touch areas from the ESP32 firmware over serial, plays music through VLC, drives three LED strips, and exposes everything to Home Assistant over MQTT.

  ESP32 ⇄ MusicMouseDevice ─┐                        ┌─► MqttService   (state out, intents in)
   VLC  ⇄ VlcPlayer ────────┼──► EventBus ──────────►┤
 broker ⇄ MqttService ──────┘         ▲               └─► (a web service would go here)
                                      │
                              reactions/*.py  ── call actions on ──►  device / player

Three objects own the outside world, one bus carries everything, and the reactions are the only place that decides what should happen. Adding a new way to control the mouse means adding a service that emits the same intents - no device or reaction changes.

Running it

pip install -e '.[dev]'
python -m musicmouse --config /media/musicmouse/config.yml

See config.yml.example for the schema and musicmouse.service for the systemd unit. Config problems are reported all at once with the path to each one; unknown keys are errors, not silent no-ops.

Without hardware

The simulator runs the entire app - real bus, real device, real reactions, real MQTT if configured - against a fake serial link and a fake player.

python -m musicmouse --config ./config.yml --simulate
musicmouse> place fuchs
  in   RfidTokenRead(04a1b2c3d4, figure='fuchs')
  rfid fuchs
  led   ring: SwipeAndChange(AlexaSwipe(#(1.0, 0.4, 0.0, 0) -> ...))
  play playing
musicmouse> press right
  play track 1: 01 - Song 1

help lists the verbs. The same verbs go in a scenario file:

python -m musicmouse --config ./config.yml --simulate --script scenarios/smoke.txt

Scenario files run on a virtual clock under pytest, so wait 1s costs microseconds and every file in scenarios/ is part of the test suite. A session reproduced by hand at the prompt becomes a regression test by pasting it into a .txt file.

Layout

Path What it is
musicmouse/bus.py One FIFO queue on one loop. Thread-safe emit() - which is how libVLC's callback thread stops reaching the serial transport.
musicmouse/events.py The vocabulary: input (something happened), intent (something was requested), state (something changed).
musicmouse/devices/ mouse.py (firmware), player.py (VLC), wire.py (pure codec), serial_link.py (transport + reconnect).
musicmouse/reactions/ The policy. @on(SomeEvent) functions that get the app and act.
musicmouse/services/mqtt/ Home Assistant entities: three lights, a player sensor, a volume number, transport buttons, device triggers, a tag scanner.
musicmouse/simulator/ Fake transport and player, the driver vocabulary, the REPL and the script runner.
musicmouse/config.py Pydantic schema, validation, and human-readable error formatting.

Checks

pytest            # unit + scenario tests
ruff check .
mypy              # --strict, configured in pyproject.toml

tests/test_wire.py parses ../esp-firmware/src/Messages.h and fails if the Python message ids drift from the firmware's - the contract is hand-duplicated in two languages, and it had already drifted once (BUTTON_EVENT was missing on the Python side). tests/test_effects.py pins the exact bytes of every effect payload.

LED arbitration

MusicMouseDevice is the single writer to each zone, and the most recent effect wins - whether it came from a figure animation or from Home Assistant. There is no priority scheme. Every write emits LedEffectChanged, and the MQTT light entities publish their state from that rather than echoing their own commands, so HA keeps showing the strip's real state when a figure animation overrides a colour it set.

Home Assistant

The backend no longer calls Home Assistant directly (hass-client is gone). It publishes what happened; the automations live in HA.

MQTT device triggers are published for every button (pressed, double_clicked, long_pressed), every touch area (touched/released), and the RFID reader appears as a tag scanner. Topics are under musicmouse/trigger/… and musicmouse/tag.

Recreating the old room-light behaviour

Two behaviours used to be hard-coded in main.py and now need automations:

Rotary press toggled the room light. Trigger on the rotary_pressed device trigger, action light.toggle on light.kinderzimmer_fluter.

Touching a body part set a colour on light.kinderzimmer_fluter and light.music_mouse_regal_licht:

Touch area Old service data
right_foot rgb_color: [235, 255, 67]
left_foot color_temp: 469
right_ear rgb_color: [101, 49, 255]
left_ear rgb_color: [255, 74, 254]

Trigger on the corresponding *_touched device trigger and call light.turn_on with that data.