134 lines
7.4 KiB
Markdown
134 lines
7.4 KiB
Markdown
# mic-follow — the microphone follows the person
|
|
|
|
A voice call that survives walking out of the room. The person leaves their desk, the
|
|
locator says which room they are in now, and their machine's live microphone switches
|
|
to one that can hear them there — then back to the good desk mic when they sit down.
|
|
|
|
Built for the specific case of a smoke break on the Loggia during a game, and written
|
|
**per client** so a second person with a second desktop is one more entry in
|
|
`CoreSystemConfig.json` and nothing else.
|
|
|
|
## 1. The one rule
|
|
|
|
**Off means the desk mic.**
|
|
|
|
The Follow-me switch being off is not "ignore me", it is an active guarantee that the
|
|
machine is on its own microphone. Being live on the wrong mic is the failure you notice
|
|
in front of five other people, so the safe state is reachable by one tap on the dock,
|
|
one button on a watch, or one MQTT message — and it does not depend on presence being
|
|
right, on the remote machine being up, or on the agent having seen a recent update.
|
|
The agent also returns to the desk mic when it shuts down.
|
|
|
|
## 2. Who decides what
|
|
|
|
```
|
|
locator (BLE via identity; RuView/Frigate corroborate — see §3)
|
|
│ presence_entity's state is an area_id
|
|
▼
|
|
Home Assistant ── generated automation ──> select.mic_follow_<node>_input
|
|
│ (only while switch.mic_follow_<node>_armed is on)
|
|
▼ MQTT
|
|
desktop_agent.py ──> pactl: set the default source, and MOVE the running
|
|
capture streams of the configured applications
|
|
```
|
|
|
|
Home Assistant decides **where the person is and therefore which mic should be live**.
|
|
The agent knows **how to change the input on this machine** and nothing about presence,
|
|
people or rooms beyond the names in its own config. Same rule as every other agent in
|
|
this repo: the inbound control surface is MQTT discovery entities and nothing else.
|
|
|
|
**Moving the streams matters as much as setting the default.** Changing the default
|
|
source only affects applications that asked for "default", and anyone who owns a studio
|
|
mic has picked it explicitly in Discord. `move_streams` names the applications whose
|
|
already-running capture stream gets moved too.
|
|
|
|
## 3. What the locator can and cannot tell you
|
|
|
|
`presence_entity` must be an entity whose **state is an HA `area_id`**
|
|
(`docs/rooms-and-endpoints.md`). Getting one is the part of this that is not built here:
|
|
|
|
| Source | Gives | Usable to drive this? |
|
|
|---|---|---|
|
|
| **BLE / Bermuda**, surfaced by `identity`'s `/presence` | a *person*, resolved to a room | **Yes** — the only identity-bearing room-level source in this household |
|
|
| **RuView** (CSI radar) | a room is occupied, by somebody | No on its own — it cannot say who. Good for corroboration |
|
|
| **Frigate** | a recognised face at the door | No — it is a peephole camera, not per-room |
|
|
|
|
So this rides on BLE, with everything else as confirmation. That matters because
|
|
**room-level presence has never been measured in this flat** — the repo says so
|
|
elsewhere and it is still true. Before trusting it with your microphone, watch the
|
|
entity in Developer Tools while you walk to the Loggia and back. If it lags by a minute
|
|
or reports a friendly room name instead of an `area_id`, the automation will never fire
|
|
and the mic will never move.
|
|
|
|
`dwell_seconds` (default 20) is the guard against a twitchy locator: a walk past a door
|
|
should not move your microphone. `return_dwell_seconds` (default 5) is deliberately
|
|
shorter — sitting down should give you the good mic back before you say anything into
|
|
it.
|
|
|
|
## 4. Three ways to flip it, one place to read it
|
|
|
|
Every surface shows the same entity: `sensor.mic_follow_<node>_status`, whose **state is
|
|
the name of the microphone that is live right now** — `Desk`, `Loggia`. That sensor
|
|
exists so no surface has to reimplement the same three-way template, and so the answer
|
|
to "what am I being heard through" is one string anything can print.
|
|
|
|
- **Stream Dock key** — displays that sensor and calls `switch.toggle`. The
|
|
Home Assistant plugin subscribes to HA's websocket, so the key updates when the state
|
|
changes rather than on a timer: **nothing polls**, and the key is right within a moment
|
|
of the mic actually moving, including when it was the watch or the automation that
|
|
moved it. Bindings are generated into `generated/dock-bindings.md`; leave the key's
|
|
title empty and let the state be the whole label, because `Loggia` in large type is
|
|
readable across a room and `Follow-me mic` over a small `Loggia` is not.
|
|
- **Pebble watchapp** — long-press Select on the plan screen. The screen shows the live
|
|
microphone in large type, `FOLLOWING` or `DESK ONLY` as a coloured pill, and Select
|
|
flips it. Up/Down cycles clients if there is more than one.
|
|
- **Home Assistant** — the switch and the select, like anything else.
|
|
|
|
The watch goes through `identity`'s `/toggles`, not straight at Home Assistant: identity
|
|
already holds an HA token and the phone already holds identity's, so one button on a
|
|
wrist does not put an HA admin token in a watchapp's settings. It is an **allowlist**,
|
|
not a proxy — only `switch.*` entities that are named in `TOGGLE_ALLOWLIST_JSON`, only
|
|
on/off/toggle, and an id that is not listed is a 404. Paste the line from
|
|
`generated/identity-toggles.env` into `identity.env` or the watch sees nothing.
|
|
|
|
## 5. Setting it up
|
|
|
|
1. Fill in `mic_follow` in `CoreSystemConfig.json`. Get the `desk_source` and each
|
|
`source` string by running `mic-follow/desktop_agent.py --list-sources` on the
|
|
machine in question — any part of a name or description matches.
|
|
2. `tools/validate-config.py`.
|
|
3. On each client machine: `mic-follow/setup-client.sh <node_id>`.
|
|
4. Put `generated/ha-package/mic_follow.yaml` in HA's `packages/` (the same directory
|
|
`stream-dock` uses) and reload YAML.
|
|
5. Paste `generated/identity-toggles.env` into `identity.env`, restart identity.
|
|
6. Bind the dock key from `generated/dock-bindings.md`.
|
|
|
|
## 6. A remote microphone that is not plugged into this machine
|
|
|
|
`start_command` / `stop_command` on a source are the hook for that — bringing up a
|
|
network mic stream, for instance, when the switch moves to that room.
|
|
|
|
**The stop hook is the important one.** A room microphone that keeps streaming after the
|
|
switch has left it is a hot mic in somebody's flat. The agent runs the stop hook on
|
|
every transition away and again on shutdown, and the validator warns about a
|
|
`start_command` with no matching `stop_command`.
|
|
|
|
## 7. What is tested, and what is not
|
|
|
|
Tested here, and it runs anywhere:
|
|
|
|
- `test_selection.py` — 17 cases over the audio layer's parsing and selection: monitor
|
|
sources can never be selected (picking one transmits what the desktop is *playing*,
|
|
the worst outcome available), exact names beat substrings, an ambiguous pattern
|
|
resolves the same way after a reboot instead of coin-flipping, and only the
|
|
configured applications' streams get moved.
|
|
- `pebble-presence/test/run-tests.sh` — the toggles line is packed by the real JS and
|
|
parsed by the real C, so those two implementations of one format cannot drift.
|
|
- The generated HA package parses as YAML, for one client and for several.
|
|
|
|
**Not tested, because it needs the actual machines:** every command that changes state
|
|
(`pactl set-default-source`, `move-source-output`), the pactl JSON shapes the fixtures
|
|
imitate, the MQTT discovery payloads against a real Home Assistant, and — the one most
|
|
likely to bite — whether the presence entity reports rooms quickly and accurately
|
|
enough to be worth wiring to a microphone at all (§3).
|