# 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).
