Repository navigation
HA Integration
Since 0.8.0 Murdock ships a companion HA integration
(custom_components/murdock). It solves the one problem MQTT and the
system prompt can't: getting the speaker to your conversation agent
fresh on every turn, without touching the transcript.
The integration is purely additive — MQTT, the REST input_text
path and the token setup all keep working exactly as before. You can run
the integration alongside them, or not at all.
| Capability | Why it matters |
|---|---|
| LLM API "Murdock" | One prompt line per turn, rebuilt by HA on every request — no cache staleness, no transcript pollution |
| Vocabulary mirroring | Your entity/area/floor names and aliases feed the STT bias prompt, so "Bett-Lightstrip" stops becoming "Bad-Lightstrip" |
| Speaker sensors | Per satellite, with confidence, distance, nearest speaker, margin, weight and role |
async_get_speaker() |
Other integrations read the speaker directly, bypassing the model entirely |
Murdock has two delivery paths, and the integration consumes both — this matters, because they are not interchangeable:
| Path | Fed by | Needs |
|---|---|---|
HA event bus (speaker_recognition_detected) |
Murdock's REST push | a long-lived token in Murdock's Settings → Home Assistant |
MQTT topic <prefix>/event/recognition
|
Murdock's MQTT client | a broker (auto-wired in the add-on) |
An MQTT message is not a Home Assistant event. Integration 0.1.x only listened on the event bus, so the recommended token-free MQTT setup delivered nothing at all — fixed in 0.2.0, which subscribes to the topic as well. Running both is fine; duplicates are dropped by satellite and timestamp.
sensor.murdock_delivery_path tells you which transports are actually
live (mqtt+event, event (waiting), …). Check it first whenever the
speaker stays unbekannt.
Timing: Home Assistant starts the intent stage — where the prompt is built — about a millisecond after receiving the transcript. Murdock therefore publishes the recognition and waits for it before answering the satellite (add-on 0.8.1+). Older add-ons published afterwards, so the speaker only became visible one turn later.
The repository is HACS-ready — hassfest and the HACS validation action
run on every push.
- HACS → three-dot menu → Custom repositories
- Repository
https://github.com/BobMcGlobus/Murdock, type Integration - Add it, then download Murdock from the HACS list.
HACS offers the repository's release tags; those are the add-on's
version numbers (v0.8.3, …) because add-on and integration live in one
repository. Take the newest — the integration's own version is in its
manifest.json.
Updates then show up in HACS like any other integration.
- Download
murdock-integration-<version>.zipfrom the latest release and unpack it so you end up with/config/custom_components/murdock/. (Equivalent: copy that folder out of the repository.)
-
Make Murdock's API reachable. The integration talks to Murdock's REST API over HTTP. In the add-on the Web UI port is unpublished by default, because ingress covers the browser:
Murdock add-on → Configuration → Network → set port 8099 → restart the add-on.
Ingress alone is not enough — it authenticates browser sessions, not integration calls. docker-compose users already publish 8099.
-
Restart Home Assistant (a custom component is only picked up at startup).
-
Add the integration. Settings → Devices & Services → Add integration → Murdock. Enter
http://<ha-host>:8099. The flow tests the connection, reads the version, and offers the satellite IDs Murdock has already seen in its recognition log. -
Map your satellites. For each Murdock satellite ID pick the matching
assist_satelliteentity. Repeat via the "Map another satellite" checkbox. -
Enable the LLM API. Settings → Voice assistants → your agent → LLM APIs → tick Murdock (next to "Assist" and any others).
Every turn the agent receives:
Sprecher: Jonas (Konfidenz 0.94, Satellit Wohnzimmer) — Rolle: admin
Die Zeile "Sprecher:" nennt die per Stimmerkennung identifizierte Person.
Steht dort "unbekannt" oder "unsicher", nimm nicht an, dass es der
Hauptnutzer ist — frage nach, bevor du etwas Personenbezogenes tust oder
speicherst.
The line is never omitted. With no recognition, a stale one, or an
ambiguous one it reads Sprecher: unbekannt or Sprecher: unsicher
explicitly — a missing hint would be read by the model as "no
objections, probably the main user", which is exactly the failure mode
we're avoiding.
Why per turn works here: Home Assistant calls the LLM API afresh for
every user message, so the prompt is rebuilt each time. That's the
mechanism extra_system_prompt lacks — it's set once per conversation.
Murdock thinks in Wyoming satellite IDs; the conversation request carries
an HA device_id. Nothing reliably connects the two, so the integration
never guesses:
-
Direct hit — the mapped
assist_satelliteentity's device matches the requestingdevice_id. - Area fallback — the requesting device sits in the same area as a mapped satellite. This survives device-registry IDs changing when a satellite is re-paired.
- Otherwise →
unbekannt.
A wrong mapping would mean right speaker, wrong room — worse than no answer, so an unmapped satellite is simply unknown.
Default 30 s, configurable. A recognition older than that counts as
unbekannt for the prompt line and for async_get_speaker(). The
sensors deliberately keep showing the last known speaker, so a dashboard
doesn't flicker to "unbekannt" between commands.
Only entities exposed to the voice assistant are mirrored — that flag describes exactly the set people talk about. Mirroring the whole registry would inflate the fuzzy index and make false replacements more likely on noisy input.
Pushed with a 5 s debounce (a bulk rename lands as one snapshot), payload
{version, generated_at, entities[{entity_id, name, aliases, area, floor, domain}], areas, floors}.
Murdock stores each push as a versioned snapshot and keeps using the latest one when HA is unreachable — source, not dependency. The terms feed the STT vocabulary prompt (capped at 25, priority: entity names → aliases → areas → floors) and are combined with anything you typed manually under Settings → Transcription → Transcript quality.
Turn it off in the integration's options if you'd rather curate the vocabulary by hand.
Murdock Web UI → Settings → Transcription → Transcript quality → "Mirrored from Home Assistant" lists every term the integration pushed. Terms beyond the cap are greyed out: they are stored, but not sent — worth knowing, because with more than ~25 exposed entities most of your names never reach the STT engine. The same panel shows the effective prompt, i.e. your manual terms plus the capped mirrored ones exactly as they go out.
The blue chips are clickable — that is how you choose which terms go to the engine rather than accepting the automatic first 25. Full detail in Transcript Correction.
Same data over the API:
curl -s http://<murdock-host>:8099/api/vocabulary | jq '.term_count, .effective_prompt'Per mapped satellite:
-
sensor.murdock_<satellite>_speaker— state is the speaker name,unsicher, orunbekannt. Attributes:confidence,distance,nearest_speaker,nearest_distance,margin,weight,role,uncertain,whisper,whisper_score,reason,recognized_at,satellite_id. -
binary_sensor.murdock_<satellite>_whisper— on while the last utterance there was whispered. Its own entity rather than an attribute, so an automation can duck the TTS volume without templating. Attribute:whisper_score.
Proxy diagnostics:
binary_sensor.murdock_connection-
sensor.murdock_last_recognition(timestamp) sensor.murdock_vocabulary_version
These are a dashboard by-product. Automations and other integrations should use
async_get_speaker()or the dispatcher signal — the entity-state detour adds recorder latency for no benefit.
from homeassistant.components.murdock.helpers import async_get_speaker
state = await async_get_speaker(hass, device_id=device_id)
if state and state.speaker:
...Returns a SpeakerState (or None when unmapped, stale, or the
integration isn't set up). Subscribe to SIGNAL_SPEAKER_UPDATE via the
dispatcher to react to changes.
The deliberate design point: read the speaker here, not from a tool parameter the model filled in. The prompt line is for conversational tone; data integrity has to bypass the model.
| Symptom | Cause |
|---|---|
| "Could not reach Murdock at this address" | Port 8099 not published on the add-on, or wrong host |
| Satellite dropdown is empty | Murdock hasn't logged any recognition yet — speak once, or type the ID manually |
Prompt always says unbekannt
|
Satellite unmapped, or the recognition event isn't arriving: check Settings → Home Assistant in Murdock's Web UI (the add-on wires this via the supervisor token automatically) |
| Speaker is right but room is wrong | Satellite mapped to the wrong assist_satellite entity |
TypeError: '<' not supported between instances of 'str' and 'ComputedNameType' |
Integration 0.1.0 on HA 2026.7+ — update to 0.1.1 |
Murdock's log shows a hit, the sensor stays unbekannt
|
No delivery path is live. Check sensor.murdock_delivery_path: on MQTT you need integration 0.2.0+, on REST a token in Murdock's HA settings |
Sensor shows the speaker, but the prompt said unbekannt for that turn |
Add-on older than 0.8.1 — it published after answering, losing the race against the intent stage |
Vocabulary mirroring is best-effort by design: if it fails, it logs and the speaker path keeps working. A mirroring problem should never cost you the integration.
When changing the integration, run the API check against a real Home
Assistant before shipping — the add-on's pytest suite cannot import
custom_components/ (HA needs Python 3.14, the image is on 3.11):
docker run --rm -v "$(pwd):/repo:ro" -w /repo murdock-ha-verify python scripts/verify_integration_api.pyThe script's docstring contains the Dockerfile for that image.
Entity icons ship with the integration (icons.json). The Murdock logo on
Home Assistant's integrations page comes from the
home-assistant/brands repository, which only carries integrations that
have been submitted there; the brand-sized assets are prepared in
custom_components/murdock/brand/ (256/512 icons, transparent), which is
also what HACS reads for its own brands check.
Learn-tool for corrections (propose/confirm across turns, role-gated), acoustic-coupling context prior with ducking, number entities for live threshold tuning, and enrollment straight from HA.