# 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 ```sh 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. ```sh 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: ```sh 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 ```sh 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.