⚠️ Alpha — not for production. Under active development; the API and the config format can still change.
Herold is a Home Assistant custom integration for prioritised omnichannel notifications: room-aware voice output through Assist satellites and media players, mobile push, Telegram with answer buttons, an alarm clock, and a five-level priority model with do-not-disturb logic and an offline TTS fallback.
Herold is the successor of the script
System: Universal Omnichannel Communicator (Priority Edition) — as a
maintainable, testable integration with UI configuration.
A note on languages. Code, comments, commit messages, the changelog and every prompt Herold sends to an LLM are English. The user-facing surfaces — config flow, entity names and this README's German twin — are German, because that is what the household speaks. See
translations/.
- Home Assistant 2026.7.0 or newer
- At least one Assist satellite or a media player with TTS
- Optional: mobile app (for push), occupancy sensors (for room detection)
- Open HACS → menu (⋮ top right) → Custom repositories
- Add the repository URL
https://github.com/BobMcGlobus/herold, type: Integration - Search for "Herold" in HACS and install it
- Restart Home Assistant
- Settings → Devices & services → Add integration → Herold
The config flow walks through nine steps:
- Basics — recipient person, instance name
- Rooms (repeatable) — per room: occupancy sensors (several possible, OR-linked), Assist satellite and/or media player, optional alarm lights and scenes for the P4 flash, optional volume levels
- Voice — primary TTS (e.g. ElevenLabs), optional fallback TTS (e.g. Piper), internet detection sensor
- Push — mobile app notify entities
- Chat — optional Telegram chat id for messages and answer buttons, optional pending-question helper (legacy compatibility for open questions)
- LLM — conversation agent for P0 self-reminders plus a fallback agent, toggles for speakable confirmations and the self-check
- DND — optional external DND entity, internal DND switch, quiet hours
- Alarm clock — bed sensor, bedroom, alarm speaker, volume bounds, snooze and give-up settings, getting-up verification, pre-alarm lead times for lights and blinds, good-morning routine, workday and sick-day sensors
- Offline — offline TTS fallback (opt-in), offline queue (planned)
Every section stays editable through the integration options; rooms and templates can be added, edited and removed without re-running setup.
| Feature | Status |
|---|---|
herold.send service (P0–P4) |
✅ 0.1.0 |
| Room-aware voice delivery (occupancy → satellite) | ✅ 0.1.0 |
| Multiple occupancy sensors per room (OR-linked) | ✅ 0.1.0 |
Media-player-only rooms (tts.speak fallback) |
✅ 0.1.0 |
| TTS chain: primary → offline fallback (e.g. ElevenLabs → Piper) | ✅ 0.1.0 |
| Mobile app push (critical sound for P4) | ✅ 0.1.0 |
| DND switch plus external DND entity | ✅ 0.1.0 |
herold.query — questions expecting an answer (yesno / open / choice) |
✅ 0.2.0 |
| Telegram channel with inline buttons (legacy compatible) | ✅ 0.2.0 |
Query persistence across restarts, timeout and default_answer |
✅ 0.2.0 |
| Multi-occupancy conflict resolution (weight plus recency) | ✅ 0.2.0 |
| Last-known-room fallback (15 min TTL) | ✅ 0.2.0 |
| P4 alarm flash: several lights and scenes per room | ✅ 0.2.0 |
herold.schedule and herold.remind_self (persisted) |
✅ 0.3.0 |
P0 internal channel (LLM self-callback via conversation.process) |
✅ 0.3.0 |
| Native LLM tools | ✅ 0.3.0 |
| Todo inbox for P1 notifications | ✅ 0.3.0 |
| Escalation chains for unanswered queries | ✅ 0.4.0 |
| Voice timeout: buttons move to Telegram when nobody answers | ✅ 0.4.0 |
| Rate limiting plus P2 aggregation (anti-spam) | ✅ 0.4.0 |
DND sessions (until, until_home) |
✅ 0.4.0 |
| Notification templates with Jinja placeholders | ✅ 0.4.0 |
| pytest suite (dispatcher, router, legacy compat, limiter, …) | ✅ 0.5.0 |
| Dashboard card (inbox / scheduled / alarms / logbook) plus history | ✅ 0.6.0 |
| Response inspection and self-check for LLM instructions | ✅ 0.7.0 |
State triggers (herold.watch, herold_remind_when) |
✅ 0.8.0 |
| Per-room volume levels plus quiet hours | ✅ 0.9.0 |
| Alarm clock with ramp-up, snooze and automation hooks | ✅ 1.0.0 |
| Alarm clock: wake sounds, urgency levels, get-up verification, standalone card | ✅ 1.2.0 |
| Offline queue, multi-user | 🔜 backlog |
The release history lives in CHANGELOG.md.
service: herold.send
data:
message: "Die Waschmaschine ist fertig"
priority: 2 # 0 internal · 1 todo · 2 normal · 3 important · 4 alarm
# title: "Optional push title"
# target_player: assist_satellite.living_room
# tag: waschmaschine
# ttl_minutes: 30
# callback_event: AI_CONFIRMservice: herold.query
data:
question: "Soll ich das Licht ausschalten?"
mode: yesno # yesno · open · choice
# choices: ["Pizza", "Pasta", "Salat"] # only for mode: choice
priority: 2
timeout_minutes: 60
# voice_timeout_seconds: 90 # no voice answer → buttons go to Telegram
# default_answer: "Nein" # used automatically on timeout
# callback_event: AI_CONFIRMAnswer paths: satellite conversation (start_conversation), Telegram inline
buttons, free text in the Telegram chat (open mode), the card, or
herold.acknowledge (id plus answer). Open queries survive restarts. On an
answer Herold fires herold_answered with a structured payload — for yesno
additionally the legacy event (AI_YES/AI_NO resp. <custom>_YES/_NO).
service: herold.schedule
data:
scheduled_for: "+1h30m" # also "18:00" or an ISO datetime
message: "Ofen vorheizen nicht vergessen"
priority: 2service: herold.remind_self # P0 convenience for the assistant
data:
when: "+30m"
instruction: "Frage den User via herold.query: Wie ist der Kuchen geworden?"
task_context: "Es backt gerade ein Kuchen."Scheduled notifications survive restarts; deliveries missed while Home
Assistant was down still fire within five minutes, older ones are marked
herold_expired. P0 instructions run through the configured conversation
agent (options → LLM) with the [HEROLD_INTERNAL] prefix, a fallback agent
and an anti-runaway limit (max 20 per hour).
Herold registers an LLM API named Herold — enable it per conversation agent under Voice assistants → agent → LLM APIs. Tools:
| Tool | Purpose |
|---|---|
herold_list_pending |
todos, open queries, schedules and watches |
herold_acknowledge |
mark a todo as done |
herold_answer_query |
answer an open query (with fuzzy matching) |
herold_remind_self |
time-based self-reminder |
herold_remind_when |
state-triggered reminder |
herold_cancel |
cancel a reminder, watch or query |
herold_set_alarm / herold_list_alarms / herold_cancel_alarm |
alarm clock |
Every tool returns a ready-to-speak confirmation sentence that the tool
description requires the agent to read back, so the user always hears whether
something was really stored.
The prompt is English like all LLM-facing text; the quoted example phrases stay German because they are what the user actually says.
## Herold — the household notification system
You have access to the Herold tools (the "Herold" LLM API must be enabled):
- Use `herold_list_pending` when the user asks "was ist neu", "gibt es was
für mich", "hab ich was verpasst" — and proactively at the end of a
conversation if something might be open.
- Use `herold_remind_self` for ALL time-delayed tasks ("in einer Stunde",
"um 18 Uhr", "morgen früh"). Never tell the user you cannot perform
time-delayed actions. Do NOT use the calendar or any other scheduling
helper for this.
- Use `herold_remind_when` when the reminder is tied to an event instead of
a time ("wenn ich das nächste Mal die Haustür öffne", "sobald die
Waschmaschine fertig ist", "wenn es unter 5 Grad wird").
- Use `herold_set_alarm` for alarm clocks ("stell mir einen Wecker für
halb sieben"). While an alarm is ringing, "aus"/"stopp" and "snooze" are
handled by Herold itself — do not call a tool for them.
- ALWAYS read the "confirmation" field of a tool result back to the user so
they know something was really stored. On success=false, say clearly that
it did NOT work.
- Use `herold_answer_query` when the user answers an open question (map
fuzzy yes/no: "klar" → "Ja", "bloß nicht" → "Nein").
- Use `herold_acknowledge` when the user reports a todo as done.
Messages starting with [HEROLD_INTERNAL] are internal reminders from
yourself (scheduled earlier via herold_remind_self). Execute the instruction
silently and do not reply to the user, unless the instruction explicitly
asks for a message or an announcement.Important when migrating: remove the old script.ai_schedule_command
from the Assist exposure (Settings → Voice assistants → Entities), otherwise
the LLM keeps reaching for the old calendar workflow instead of
herold_remind_self. Todos do not end up in the prompt —
herold_list_pending is a live tool call, so there is no caching problem.
Herold wakes you in the room you sleep in, with an actual tone rather than a spoken sentence — a TTS line has no attack and no frequency peaks, which is exactly why it is easy to sleep through.
service: herold.alarm_set
data:
time: "06:30"
days: [mon, tue, wed, thu, fri] # omit for a one-shot alarm
label: Work
urgency: insistent # gentle · normal · insistent
workday_only: true # skip holidays and sick days
# key: shift_plan # an automation can own this alarm
# valid_until: "+7d" # temporary alarmThe card is the comfortable way in. Add custom:herold-alarm-card to a
dashboard: alarms as tiles with big times, weekday strip, status badges and a
toggle. + creates one, tapping a tile opens its settings sheet — time,
repeat (with Weekdays / Weekend / Daily presets), label, urgency, sound
source with an audition button, the spoken message, workday and voice-snooze
flags, and under "more settings" the good-morning routine and an expiry date.
On iOS the time field opens the native wheel.
type: custom:herold-alarm-card
title: Wecker
card_style: glass # default · glass · material · bubble · mirror
columns: 2| Option | Type | Default | Description |
|---|---|---|---|
title / subtitle |
string | – | Heading |
entity |
string | auto | The alarm sensor; found automatically |
card_style |
string | default |
default, glass, material, bubble, mirror |
layout |
string | grid |
grid or carousel |
columns |
number | 1 |
Tile columns (1–3) |
tiles |
boolean | true |
false renders flat rows instead of tiles |
background |
boolean | true |
false removes the card background and shadow |
flush |
boolean | false |
true removes the outer padding |
The styles and their tokens are the ones the Weatherglass card uses, so the two match on a shared dashboard. Everything is editable in the visual card editor.
Sound — four tones ship with the integration (chime, beep, siren,
sunrise; synthesised by scripts/generate_sounds.py, so no third-party
audio licensing — 44.1 kHz mono WAV, because AirPlay receivers reject
anything else). sound_mode picks the source:
| Mode | What sound holds |
|---|---|
builtin |
chime, beep, siren or sunrise |
media |
a media id or URL — media-source://media_source/local/wake.mp3, https://…/wake.mp3 |
music_assistant |
a Music Assistant URI (library://playlist/12), or a plain name if you also set sound_media_type |
announce |
nothing; the alarm only speaks |
The spoken message is optional and follows the tone.
Bring your own MP3. Drop an audio file onto the card's sound section and
it is uploaded into your Home Assistant media library, stored as the alarm's
sound and resolved to a playable URL at ring time. This needs the media
source to be writable — with default_config that is the media_dirs
folder (/media in a standard install). Supported: MP3, M4A, WAV, OGG,
FLAC.
Music Assistant wants to know what it is looking for. Rather than
typing a name and hoping, the card searches: pick a media type, search, tap
a result, and the exact URI is stored. From YAML or an automation, set
sound_media_type (track, album, artist, playlist, radio)
alongside a plain name, or pass a URI and leave the type out. The queue is
replaced rather than appended to, so last night's playlist does not play
first. herold.alarm_search_media exposes the same search and returns a
response.
Test it before you rely on it — herold.alarm_test rings now:
action: herold.alarm_test
data:
scope: sound # sound · light · cover · all
volume: 0.4 # optional, overrides the floor for this test
# id: a1b2c3d4 # test one alarm's own sound settingsThe card has Ton testen and Licht testen chips in the settings sheet,
and there are two buttons (button.*_test_alarm_sound /
button.*_test_alarm_light) for a dashboard. Lights and blinds are
snapshotted before the test and restored afterwards (default 12 s), so a
test at 22:00 does not leave the bedroom lit.
The test reports rather than raising: the response carries media_player,
satellite, sound, and on failure an error plus a hint about what to
change. The card renders that inline — Spielt auf Schlafzimmer Speaker, or
the reason it did not.
Urgency decides how stubborn it is:
| Level | Interval | Gives up after | Snoozes |
|---|---|---|---|
| gentle | 90 s | 3 rings | unlimited |
| normal | 45 s | 6 rings | 3 |
| insistent | 25 s | 15 rings | 1, then refused |
Volume stays between a configurable floor and ceiling (options → alarm clock). Ring one starts at the floor and climbs towards the ceiling as the alarm is ignored — a speaker left quiet overnight cannot swallow it.
Where it rings. Without any hint, Herold resolves a target itself: in bed → the bedroom, already up → the active room, nothing occupied → the bedroom. That is a guess, and it can land on the wrong device — the active room's media player may well be a TV. Two per-alarm fields override it:
| Field | Effect |
|---|---|
target |
Pin this alarm to one media_player or assist_satellite. Wins over everything |
follow_me |
Ring wherever you are right now, ignoring the bed sensor, the bedroom and the configured alarm speaker — for a nap on the couch or a timer at the desk |
Both are in the card's settings sheet under Wo klingelt er, which also shows what an automatically-resolved alarm currently points at. For a house-wide default instead, set the alarm speaker in options → alarm clock.
Media players with device_class: tv are listed in a separate group and
flagged when picked. A television is a poor alarm: it sleeps at night, wakes
slowly, and its streaming stack refuses plenty of ordinary audio files — an
Apple TV in particular. If the chosen player refuses the tone at ring time,
the alarm falls back to speaking the message rather than staying silent for
all fifteen rings.
Getting up is verified: a dismiss while the bed sensor still reports occupancy counts as a reflex, and after a grace period the alarm resumes. Turn it off in the options if that is too strict.
Skipping one occurrence (herold.alarm_skip_next, or the chip in the
card) pins the skip to that one ring rather than setting a flag. Rescheduling
— which happens on every Home Assistant restart — then lands on the same
ring instead of stepping over another one. herold.alarm_update with
skip_next: false cancels a pending skip, and both the tile badge and the
card name the date being skipped.
Ring only when I'm in bed. require_bed skips an alarm — sound and
sunrise phase — when the bed sensor reports an empty bed at wake-up time.
Off by default, and deliberately one-sided: only a sensor that positively
says "empty" may block a wake-up. unknown, unavailable, a missing
entity or no bed sensor at all all ring anyway and log a warning, because a
missed alarm costs far more than one that rings while you are already up.
Ignored for follow_me alarms, which are by definition not about the bed.
Getting up also ends it. Leaving the bed while an alarm is ringing,
verifying or snoozed dismisses it, once the bed has stayed empty for
alarm_up_seconds (default 30 s — long enough that rolling over does not
cancel the alarm). Set it to 0 to switch that off.
Voice snooze asks a real question. With voice_snooze the satellite
uses assist_satellite.ask_question and listens for the answer:
"schlummern" / "snooze" / "noch fünf Minuten" snoozes, "aus" / "ich bin
wach" / "stopp" dismisses. Needs a Home Assistant with
assist_satellite.ask_question; without it the alarm logs a warning and
carries on ringing.
Songs are played, not restarted. With sound_mode: media or
music_assistant the alarm starts the track once and later rings only turn
it up — re-issuing playback every 45 seconds would restart the song or skip
to the next one. The volume is held for the whole alarm instead of being
restored between rings, and dismissing or snoozing stops playback.
Before the sound, lights fade up (default 20 minutes early) and blinds
open (default 5 minutes early) — both times configurable, 0 disables the
stage. The good-morning routine (a script or scene) runs once you really
got up, not on the first snooze.
Work alarms flagged workday_only are skipped when the configured
workday sensor is off or the sick-day toggle is on. One-off and holiday
alarms ring regardless — that is usually the point of setting them.
Control: herold.alarm_snooze, herold.alarm_dismiss,
herold.alarm_update, herold.alarm_skip_next, herold.alarm_cancel.
Without an id, snooze and dismiss act on the ringing alarm.
For automations: sensor.*_next_alarm (timestamp, plus target and
in_bed), binary_sensor.*_alarm_ringing, and the events
herold_alarm_set, herold_alarm_triggered, herold_alarm_snoozed,
herold_alarm_dismissed, herold_alarm_skipped, herold_alarm_pre.
Each room can define three volume levels (options → rooms): quiet, normal, loud. Herold applies the matching level before an announcement and restores the previous volume afterwards — only once the player actually finished speaking. Levels left blank change nothing, so without configuration everything behaves as before.
Which level applies: P4 is always loud, otherwise normal — except inside the quiet hours (options → DND, e.g. 22:00–07:00), where it is quiet. An alarm at three in the morning stays loud, a routine message does not.
Besides time-based reminders, Herold can wait for state changes — small one-shot automations the assistant creates for itself:
"Remind me to hand the parcel to the postman the next time I open the front door."
service: herold.watch
data:
entity_id: binary_sensor.front_door
to_state: "on"
message: Denk an das Paket für den Postboten!
priority: 3
ttl_hours: 72 # 0 = never expiresNumeric thresholds work too (above / below, firing only on the crossing,
not continuously):
service: herold.watch
data:
entity_id: sensor.outdoor_temperature
below: 5
message: Es wird frostig — denk an die Pflanzen auf dem Balkon.Watches are one-shot (they remove themselves afterwards), survive restarts,
expire after their TTL and appear in the card under scheduled → by event.
Automations can trigger on herold_watch_triggered.
The LLM creates them via herold_remind_when and does not need to know
exact entity ids: the reference is matched against the exposed entities,
their friendly names and their voice aliases, so "Klimaanlage Arbeitszimmer"
resolves on its own. If nothing matches, the error lists concrete entity ids
so the model can correct itself instead of guessing again. to_state: "on"
works for every domain — for entities that never report "on" (climate,
media_player, cover, …) Herold rewrites it to "left the off state", which is
what "turns on" means.
Herold ships its own Lovelace card — the integration registers it as a resource automatically, no manual setup needed. Pick Add card → "Herold Card" on a dashboard, or use YAML:
type: custom:herold-card
title: HeroldFour tabs:
- 📥 Inbox — open queries with answer buttons (yes/no resp. the choice options, directly clickable) and the todo list with check-off and delete
- 🕐 Scheduled — upcoming deliveries with a countdown and a cancel button, split into time-based and event-based entries
- ⏰ Alarms — configured alarm clocks with snooze, dismiss and delete
- 📜 Logbook — the last 50 events (delivered, dropped including the reason, answered, escalated, rate limited, …) from the history sensor; survives restarts
Entities are discovered automatically; override them with todo_entity,
pending_entity, scheduled_entity, history_entity, watches_entity or
alarms_entity if needed.
Escalation (on herold.query): unanswered questions are redelivered at a
higher priority on a schedule:
service: herold.query
data:
question: "Haustür ist offen — soll ich abschließen?"
priority: 2
voice_timeout_seconds: 60 # no voice answer → buttons go to Telegram
escalation:
- after_minutes: 5
raise_to_priority: 3
- after_minutes: 15
raise_to_priority: 4Rate limiting (automatic): P3 has a 60 s cooldown per tag/message
(dedup), P2 allows max 3 per 5 minutes — the overflow is buffered and
delivered as one aggregated message ("3 Meldungen: …"). P4 is never limited.
Bypass with ignore_rate_limit: true. Drops are visible in the reason
attribute of the last-delivery sensor.
DND sessions: herold.dnd_on with until: "+1h" / until: "15:30" or
until_home: true — ends automatically and survives restarts.
herold.dnd_off or the switch ends the session manually.
Templates (options → templates): reusable messages with Jinja placeholders:
service: herold.send
data:
template: appliance_done # template: "{{ appliance }} ist fertig"
template_vars:
appliance: Waschmaschine| Prio | Name | Behaviour |
|---|---|---|
| 0 | Internal | LLM self-callback via conversation.process, never user-facing |
| 1 | Todo | Lands silently in the todo inbox |
| 2 | Normal | Voice when home, otherwise push plus Telegram; blocked by DND |
| 3 | Important | Voice plus push plus Telegram, ignores DND |
| 4 | Alarm | Warning announcement, alarm flash, critical push, Telegram; ignores DND |
Herold is designed as a drop-in successor of the omnichannel communicator script:
input_boolean.do_not_disturbcan be configured as the external DND entity in the DND step — existing automations (goodnight, sports popup) keep working unchanged.- Callback events stay bit-exact compatible:
callback_event: AI_CONFIRM(the default) produces Telegram buttons with the callback data/AI_YES//AI_NO— without the CONFIRM part, exactly like the original script. Existingtelegram_callbackautomations keep running; on an answer Herold additionally fires the HA eventAI_YES/AI_NO(resp.<custom>_YES/_NO) andherold_answered. Herold deliberately does not callanswer_callback_query— your existing handler automation still does that. - Open questions (
mode: open) still mirror the question into the configuredinput_texthelper (e.g.input_text.pending_question), so the existing Telegram chat automation keeps its context. - Suggested rollout: install the integration alongside the script,
compare behaviour, migrate automations to
herold.*step by step, and delete the script only after two stable weeks.
./scripts/setup-dev.sh /path/to/ha-config # symlinks the integrationRun the test suite:
pip install -r requirements_test.txt && pytest tests/