The Bulletproof Bedside Alarm Clock (with real voice assistant, on whatever screen you’ve got lying around)
Built on a jailbroken Facebook Portal running View Assist. Runs fine on basically anything Home Assistant can throw a dashboard at.
Why this exists
I wanted a bedside clock that:
Shows the time, big and readable
Has real per-day recurring alarms (Alexa-style — different times for weekdays vs. weekends, any day toggled independently)
Has a real, capable voice assistant — not a canned command list
Never, ever locks up my touchscreen when I go to change something at 6am half-asleep
Doesn’t need me to remember exact phrasing to control it by voice
I tried a popular HACS alarm-clock card first. It looked great, but its settings dialog used real text-input fields, and Android’s native text-selection popup (Cut/Copy/Paste) kept hijacking the screen and locking up the kiosk with no way back out except a manual page refresh. Tried multiple versions — same bug every time. If you’ve hit this exact wall, this build is the fix: everything here uses buttons and toggles only, never a single text field, so that failure mode is structurally impossible.
What you get
Big, clean digital clock
A dedicated Alarm Schedule page — one row per day, big +/- buttons that nudge the time (5 or 15 min, your call), a toggle to turn that day on/off, no text entry anywhere
Real recurring per-day alarms — set it once, it just runs
A Snooze and Dismiss button that appear on screen only while the alarm is actually ringing
Voice control for the whole thing: “snooze the alarm,” “turn off my alarm,” “set my Wednesday alarm to 6:30,” “don’t wake me up tomorrow” (skips one firing without touching the day’s schedule)
Saying your wake word alone, with no follow-up command, dismisses a ringing alarm — because half-asleep people don’t want to construct a sentence
A “Test Alarm Sound” button on the settings page, so you can check your volume balance without waiting for a real alarm
Optional photo slideshow filling the bottom half of the screen (via the excellent album_slideshow HACS integration) — totally optional, easy to remove if you just want the clock
The voice assistant piece (this is the part I’m proudest of)
Home Assistant’s stock Assist pipeline is great, but I wanted a personality split — a snappier, chattier assistant in one room and a terse, no-nonsense one in the bedroom — and I didn’t want to be locked into Google or pay full retail Anthropic pricing for two separate assistants.
OpenRouter solved this cleanly: one API key, one HA integration entry, and you add as many Conversation subentries as you want under it — each with its own model, its own system prompt, its own personality. I run GPT-4o-mini in the bedroom (fast, cheap, terse) and GPT-4o in the kitchen (more personality, still fast). Point a Voice Assistant pipeline at each subentry’s conversation.* entity, assign it per-device, done.
Important gotcha: if you want automations to fire off spoken phrases (like our wake-word dismiss), know that sentence-trigger automations only fire on Home Assistant’s built-in conversation agent — not on OpenRouter, not on any external LLM agent. If your pipeline uses a non-default agent, you have to expose a script to Assist instead and let the LLM call it as a tool. Cost us an hour of confusion to figure out; hopefully saves you the same.
Requirements
HACS (all free, all one-click installs):
card-mod — for sizing/spacing
kiosk-mode — hides HA’s header/sidebar so a stray tap can never open the dashboard editor on your kiosk device
album_slideshow — optional, only if you want the photo slideshow
Core Home Assistant (no add-ons needed):
input_datetime and input_boolean helpers (built in)
timer helper (built in)
Any conversation agent with tool-calling support (OpenRouter, Anthropic, OpenAI, or even the free built-in one — the alarm logic itself doesn’t care)
Setup
- Helpers (Settings → Devices & Services → Helpers)
For each day of the week (Monday–Sunday), create:
Aninput_datetime(Time only) — I named minealarm_monday,alarm_tuesday, etc.
Aninput_boolean—alarm_monday_enabled, etc.
Plus three more:
input_boolean.kobold_clock— the “ringer” flag (leftover name from the card I ditched, rename it if you like)
input_boolean.skip_next_alarm— for the “don’t wake me up tomorrow” voice command
timer.alarm_snooze— duration0:09:00(or however long you like your snooze) - Automations
Trigger automation — one dynamictimetrigger per day, pointedat:the matchinginput_datetimeentity (this means it auto-updates the instant you change the time, no polling needed). Each branch checks that day’stimeweekday condition + that day’s enabled toggle + that the skip flag isn’t set, then flips the ringer boolean on.
alias: Bedroom Alarm Trigger
mode: single
triggers:
- trigger: time
at: input_datetime.alarm_monday
id: mon
- trigger: time
at: input_datetime.alarm_tuesday
id: tue
- trigger: time
at: input_datetime.alarm_wednesday
id: wed
- trigger: time
at: input_datetime.alarm_thursday
id: thu
- trigger: time
at: input_datetime.alarm_friday
id: fri
- trigger: time
at: input_datetime.alarm_saturday
id: sat
- trigger: time
at: input_datetime.alarm_sunday
id: sun
actions:
- choose:
- conditions:
- condition: trigger
id: mon
- condition: time
weekday: [mon]
- condition: state
entity_id: input_boolean.alarm_monday_enabled
state: "on"
- condition: state
entity_id: input_boolean.skip_next_alarm
state: "off"
sequence:
- action: input_boolean.turn_on
target: {entity_id: input_boolean.kobold_clock}
# repeat this "choose" block for tue/wed/thu/fri/sat/sun,
# swapping the day abbreviation and entity_ids each time
- conditions:
- condition: or
conditions:
- condition: trigger
id: mon
- condition: trigger
id: tue
- condition: trigger
id: wed
- condition: trigger
id: thu
- condition: trigger
id: fri
- condition: trigger
id: sat
- condition: trigger
id: sun
- condition: state
entity_id: input_boolean.skip_next_alarm
state: "on"
sequence:
- action: input_boolean.turn_off
target: {entity_id: input_boolean.skip_next_alarm}
Ring automation — loops the alarm sound (respects your media player’s own volume) until dismissed. A fixed delay matching your song’s length is more reliable than watching for the media player’s idle state, which can fire early/flaky and cause stuttering restarts.
alias: Kobold Alarm - Ring
mode: restart
triggers:
- trigger: state
entity_id: input_boolean.kobold_clock
to: "on"
id: ring
- trigger: state
entity_id: input_boolean.kobold_clock
to: "off"
id: stop
actions:
- choose:
- conditions:
- condition: trigger
id: ring
sequence:
- repeat:
until:
- condition: state
entity_id: input_boolean.kobold_clock
state: "off"
sequence:
- action: media_player.play_media
target: {entity_id: media_player.YOUR_MEDIA_PLAYER}
data:
media_content_id: media-source://media_source/local/YOUR_ALARM_SOUND.mp3
media_content_type: music
- delay: {minutes: 2, seconds: 35} # match your song's length
- conditions:
- condition: trigger
id: stop
sequence:
- action: media_player.media_stop
target: {entity_id: media_player.YOUR_MEDIA_PLAYER}
Snooze re-ring + wake-word dismiss — the wake-word trigger watches your assist_satellite entity flip to listening (which fires the instant the wake word is heard, independent of what gets said afterward), and only acts if the alarm is actually ringing.
alias: Bedroom Alarm - Snooze Re-ring and Wake-Word Dismiss
mode: single
triggers:
- trigger: state
entity_id: timer.alarm_snooze
to: finished
id: snooze_finished
- trigger: state
entity_id: assist_satellite.YOUR_SATELLITE
to: listening
id: wake_word_heard
actions:
- choose:
- conditions:
- condition: trigger
id: snooze_finished
sequence:
- action: input_boolean.turn_on
target: {entity_id: input_boolean.kobold_clock}
- conditions:
- condition: trigger
id: wake_word_heard
- condition: state
entity_id: input_boolean.kobold_clock
state: "on"
sequence:
- action: input_boolean.turn_off
target: {entity_id: input_boolean.kobold_clock}
- action: timer.cancel
target: {entity_id: timer.alarm_snooze}
- Scripts (for the buttons and voice commands)
# Dismiss button + "turn off my alarm" voice command
alias: Dismiss Bedroom Alarm
sequence:
- action: input_boolean.turn_off
target: {entity_id: input_boolean.kobold_clock}
- action: timer.cancel
target: {entity_id: timer.alarm_snooze}
# Snooze button + "snooze the alarm" voice command
alias: Snooze Bedroom Alarm
sequence:
- action: input_boolean.turn_off
target: {entity_id: input_boolean.kobold_clock}
- action: timer.start
target: {entity_id: timer.alarm_snooze}
For voice control, expose both scripts to Assist (entity settings → Voice assistants → toggle on).
Optional — “set my alarm” and “skip tomorrow” by voice: build a Set Bedroom Alarm script with day and time fields (a choose block per day, each branch calling input_datetime.set_datetime on the matching entity), and expose input_boolean.skip_next_alarm directly to Assist so a phrase like “don’t wake me up tomorrow” just flips it — the trigger automation above already knows to check it. Name the skip-boolean something unambiguous (I learned this the hard way) — “Skip Next Alarm” got confused with “dismiss” by my LLM; renaming it to “Skip Tomorrows Scheduled Alarm” fixed it instantly. Precise entity naming matters more than clever prompting when you’re relying on an LLM to pick the right tool.
4. The dashboard
Two views under one dashboard:
Main view (panel type) — a single vertical-stack card containing:
A tiny settings-icon button, top-right, navigating to the schedule subview
A markdown card for the big clock digits ({{ now().strftime('%-I:%M') }})
A vertical-stack with visibility: [{condition: state, entity: input_boolean.kobold_clock, state: "on"}] — this is the trick that makes Snooze/Dismiss appear only while ringing. Inside: two big buttons calling the scripts above.
(Optional) the album-slideshow-card for photos
Schedule subview (sections type, subview: true) — one horizontal-stack per day: a minus button, a tile card showing that day’s time (with tap_action: none so it can never open an edit dialog), a plus button, and a toggle tile. The +/- buttons call a small script.alarm_adjust_time (fields: alarm_entity, delta_minutes) that does the time math and calls input_datetime.set_datetime — kept as a choose block with one hardcoded target per day rather than a templated target, since a templated target.entity_id fails silently if it ever resolves wrong.
One important detail if you’re on the View Assist Companion App specifically: it expects everything to live inside a dashboard registered at the exact url_path: view-assist — a separate top-level dashboard won’t be reachable by its internal routing. If you’re on Fully Kiosk or a plain browser instead, this doesn’t apply; any dashboard works.
Add kiosk_mode: {hide_header: true, hide_sidebar: true, hide_menubutton: true} at the top level of your dashboard config to strip out anything that could let someone accidentally wander into the dashboard editor.
The screenshot above
That’s the full schedule page — seven day rows, minus/plus/toggle, no text field anywhere, plus the Test Alarm Sound button at the bottom for dialing in volume without waiting for a real alarm.
A few honest notes if you build this
Whatever LLM you use for voice will occasionally answer confidently about a capability it doesn’t actually have (mine once told my partner it “could set alarms” before I’d built that script). This isn’t a bug in the setup — it’s a known weakness of smaller/faster models guessing instead of admitting uncertainty. Build the real thing if it matters to you.
If your device’s Android WebView has the same text-field lockup bug I hit, the fix isn’t a CSS trick — Android controls that native selection popup at the OS level, not the page. The only real fix is avoiding real <input> fields entirely, which is exactly why this whole build leans on buttons and toggles.
The voice assistant backend (OpenRouter, straight Anthropic, straight OpenAI, whatever) is fully swappable — none of the alarm logic above cares which one is answering.
Happy to answer questions if anyone wants to adapt this to their own hardware.

