Skip to content

Alarm

One alarm_control_panel.* entity: its state, the modes it arms to, whether it wants a code, and its exit and entry delays.

import { useState } from "react";
import { useHaAlarm } from "@dashhaus/core";
/**
* Arm away, with the code when the panel asks for one. The handler answers false for a wrong code and rejects when
* the call didn't go through, so each outcome gets its own words.
*/
export function ArmAway({ entityId }: { entityId: string }) {
const { entity, actions } = useHaAlarm(entityId, { loadHistory: false });
const [code, setCode] = useState("");
const [answer, setAnswer] = useState("");
const needsCode = entity.codeFormat != null && entity.codeArmRequired !== false;
async function arm() {
try {
const accepted = await actions.onArm("away", needsCode ? code : undefined);
setAnswer(accepted ? "Arming…" : "Wrong code.");
} catch {
setAnswer("Home Assistant didn't take it. Try again.");
}
setCode("");
}
if (!entity.modes.includes("away")) return null;
return (
<form
onSubmit={(event) => {
event.preventDefault();
void arm();
}}
>
{needsCode && (
<label>
Code
<input
type="password"
inputMode={entity.codeFormat === "number" ? "numeric" : "text"}
autoComplete="off"
value={code}
onChange={(event) => setCode(event.currentTarget.value)}
/>
</label>
)}
<button type="submit" disabled={entity.state !== "disarmed"}>
Arm away
</button>
<p role="status">{answer}</p>
</form>
);
}

Arming and disarming answer, unlike the other actions: true when Home Assistant took the call, false for a wrong code, and a rejection when the call didn’t go through. The store never keeps a call with a code in it, so a failed disarm is never offered again with the code.

<HaAlarm> is useHaAlarm as a component that draws nothing of its own. It takes every option of the hook as a prop, and hands the hook’s result to its child function. Use it where hooks can’t go, such as one per item of a list.

entityIdstring
The Home Assistant entity id.
children(live: HaAlarmResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaAlarm(entityId: string, options?: HaAlarmOptions): HaAlarmResult

One live alarm panel for any UI: the mapped entity, today's history and its arm and disarm actions.

It reads the panel, its area and today’s history. Home Assistant’s alarm entity has no delay length, so exitDelay and entryDelay give it (Alarmo’s delay attribute wins when it reports one). openSensors and triggeredBy are yours to compose from your door and window sensors.

name?string
Overrides Home Assistant's friendly name.
area?string
Overrides the area Home Assistant's registries resolve for the entity.
lastSeen?string
Overrides when the entity was last reachable, shown while it is unavailable.
exitDelay?number
entryDelay?number
openSensors?string[]
triggeredBy?string
What set off a pending/triggered state; Home Assistant has no such attribute, so the host composes it.
history?AlarmHistory
Host-owned history override; when omitted, today's history is read from Home Assistant.
loadHistory?boolean
Set false to skip the Home Assistant history read.
onActionError?(error: HaActionError) => void
Called with an HaActionError when an action is refused or fails.
entityAlarmEntity
historyAlarmHistory
actionsAlarmHandlers
entityIdstring
The Home Assistant entity id.
namestring
Its name: the `name` option, else Home Assistant's friendly name, else its entity id.
area?string
Its area: the `area` option, else the one Home Assistant's registries resolve; none when neither has one.
stateAlarmState
modesAlarmMode[]
The arm modes this panel supports, in the order to show them.
codeFormat?"number" | "text" | null
Whether a code is asked for, and what kind (HA code_format). Null or omitted: no code.
codeArmRequired?boolean
HA code_arm_required: the code is needed to arm too, not only to disarm. Home Assistant's default is true.
armingTo?AlarmMode
While arming: the mode it is arming to (HA's manual alarm and Alarmo expose it as next_state).
delay?number
Length of the current exit or entry delay, in seconds. Home Assistant's alarm entity doesn't expose it: Alarmo has it as the `delay` attribute; for the manual alarm it's your arming_time / delay_time. With `changedAt`, the card counts down. Without it, the slot shows the delay with no timer.
changedAt?number
When the current state started, in ms since the epoch (HA last_changed).
changedBy?string
HA changed_by.
lastChanged?string
triggeredBy?string
Pending or triggered: what set it off.
openSensors?string[]
Doors and windows open right now (from their binary sensors). Arming would bypass them.
lastSeen?string
While it is unavailable, when it was last reachable: the `lastSeen` option, else what Home Assistant reports.

Today, from Home Assistant’s recorder, kept up to date by every change since. Pass history to supply your own, or loadHistory={false} to skip the read.

samplesHourSample[]
1 for hours it was armed, 0 disarmed, "unavailable", null for hours ahead.
currentHournumber
armedMinutesnumber
Minutes armed today.
onArm(mode: AlarmMode, code?: string | undefined) => Promise<boolean>
Calls `alarm_control_panel.alarm_arm_<mode>` (`home`, `away`, `night`, `vacation`, `custom_bypass`) with the code when given. A mode the panel doesn't support rejects with `unsupported`.
onDisarm(code?: string | undefined) => Promise<boolean>
Calls `alarm_control_panel.alarm_disarm` with the code when given.

A failure other than a wrong code also goes to onActionError before the promise rejects.

Hooks can’t be called in a loop, so a list renders one headless component per entity. The selector returns a string: useHaSelector re-renders only when its value changes, and a string of ids changes only when an entity comes or goes. Lists skip each item’s history read with loadHistory={false}.

import { HaAlarm, useHaSelector } from "@dashhaus/core";
import type { HaSnapshot } from "@dashhaus/core";
const alarmIds = (snapshot: HaSnapshot) =>
Object.keys(snapshot.entities).filter((id) => id.startsWith("alarm_control_panel.")).sort().join(" ");
export function AllAlarms() {
const ids = useHaSelector(alarmIds).split(" ").filter(Boolean);
return (
<ul>
{ids.map((id) => (
<li key={id}>
<HaAlarm entityId={id} loadHistory={false}>
{({ entity }) => (
<>
{entity.name}: {entity.state}
{entity.openSensors?.length ? ` (open: ${entity.openSensors.join(", ")})` : ""}
</>
)}
</HaAlarm>
</li>
))}
</ul>
);
}
import { useHaAlarm } from "@dashhaus/core";
import { useDelayLeft } from "@dashhaus/core/hooks";
const WORDS: Record<string, string> = {
disarmed: "Disarmed",
arming: "Arming",
pending: "Entry delay",
triggered: "Triggered",
unavailable: "Unavailable",
};
/** The panel's state, with the exit or entry delay counting down when the host knows its length. */
export function AlarmState({ entityId }: { entityId: string }) {
const { entity } = useHaAlarm(entityId, { exitDelay: 30, entryDelay: 30 });
const counting = entity.state === "arming" || entity.state === "pending";
const left = useDelayLeft(entity.delay, entity.changedAt, counting);
return (
<p role="status">
{entity.name}: {WORDS[entity.state] ?? entity.state.replace("armed_", "Armed ")}
{left != null && `, ${left} s`}
{entity.triggeredBy && ` by ${entity.triggeredBy}`}
</p>
);
}

useDelayLeft from @dashhaus/core/hooks counts the delay down from changedAt, the moment the state began, and catches up at once when a sleeping tablet wakes.

Orb’s HaAlarmTile and Field’s HaAlarmModule are useHaAlarm above each skin’s card. They take the hook’s options as props, plus their own:

import { HaAlarmTile } from "@dashhaus/orb";
import { HaAlarmModule } from "@dashhaus/field";
export function OrbAlarm() {
return <HaAlarmTile entityId="alarm_control_panel.house" exitDelay={30} entryDelay={30} />;
}
/** A panel Home Assistant can read but not control: the state and today, with nothing to arm. */
export function FieldAlarm() {
return <HaAlarmModule entityId="alarm_control_panel.ring" readOnly />;
}
entityIdstring
The Home Assistant entity id.
showControl?boolean
Compact only. false removes the inline control, shows the value in its place, and makes the row open the detail sheet. Default true.
readOnly?boolean
View only: show the alarm's state, countdowns and history, with nothing to arm, disarm or type a code into. For panels Home Assistant can read but not control (a Ring Alarm without a subscription, say). Default: on when neither onArm nor onDisarm is given.
footprint?CardFootprint
Card footprint. The skin's stylesheet sets the heights; the three names are the shared vocabulary.
defaultOpen?boolean
defaultAction?NonNullable<{ kind: "arm"; mode: AlarmMode; } | { kind: "disarm"; }>

Every state of both is in Storybook: Orb and Field.