Skip to content

Lock

One lock.* entity, with the battery sensor on its device: whether it is locked, on its way or jammed, who or what changed it last, whether it wants a code, and whether it can unlatch the door too.

import { useState } from "react";
import { useHaLock } from "@dashhaus/core";
/**
* Lock on one press; unlock only after asking, with the code when the lock wants one. The handlers never reject: they
* answer "done", "wrong_code" or "failed", so each outcome gets its own words.
*/
export function FrontDoor({ entityId }: { entityId: string }) {
const { entity, actions } = useHaLock(entityId);
const [asking, setAsking] = useState(false);
const [code, setCode] = useState("");
const [answer, setAnswer] = useState("");
async function unlock() {
const outcome = await actions.onUnlock(entity.codeFormat ? code : undefined);
setAnswer(outcome === "wrong_code" ? "Wrong code." : outcome === "failed" ? "Home Assistant didn't take it. Try again." : "");
if (outcome === "done") setAsking(false);
setCode("");
}
if (entity.state === "unavailable") return <p>{entity.name} is unavailable</p>;
if (entity.state !== "locked") return <button type="button" onClick={() => void actions.onLock()}>Lock {entity.name}</button>;
if (!asking) return <button type="button" onClick={() => setAsking(true)}>Unlock {entity.name}…</button>;
return (
<form onSubmit={(event) => { event.preventDefault(); void unlock(); }}>
<p>Unlock {entity.name}? Anyone at the door can open it.</p>
{entity.codeFormat && (
<label>
Code
<input type="password" inputMode={entity.codeFormat === "number" ? "numeric" : "text"} autoComplete="off" value={code} onChange={(event) => setCode(event.currentTarget.value)} />
</label>
)}
<button type="button" onClick={() => setAsking(false)}>Cancel</button>
<button type="submit">Unlock</button>
<p role="status">{answer}</p>
</form>
);
}

Locking is the safe direction, so it can take one press. Unlocking never should: a stray tap, a child or a pet must not open a door, so both skins ask first and take the code when the lock wants one. Build your own controls the same way.

The handlers never reject. Each answers "done" when Home Assistant took the call, "wrong_code" when the code was refused or can’t fit the lock’s pattern, and "failed" when the call didn’t go through; a failure also goes to onActionError.

<HaLock> is useHaLock 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: HaLockResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaLock(entityId: string, options?: HaLockOptions): HaLockResult

One live lock for any UI: the mapped entity composed with its device's battery, and its lock, unlock and open actions. A lock shows its last change only, so there is no history read.

It reads the lock, its area and, given batteryEntityId, the battery sensor on the same device. A lock shows its last change, not a day’s history, so there is no history read and no history in the result.

batteryEntityId?string
Optional companion on the same device, `device_class: battery` (the automatic layout finds it).
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.
onActionError?(error: HaActionError) => void
Called with an HaActionError when an action is refused or fails.
entityLockEntity
actionsLockHandlers
entityIdstring
The Home Assistant entity id.
namestring
Its name: the `name` option, else Home Assistant's friendly name, else its entity id.
area?string
Where it is: "Front door".
stateLockState
supportsOpenboolean
It can unlatch the door too (`lock.open`, `LockEntityFeature.OPEN`).
codeFormat?"number" | "text" | null
Whether a code is asked for, and what kind. Home Assistant's lock `code_format` is a pattern: one that only matches digits is "number" (a keypad), any other "text". Null or omitted: no code.
codePattern?string
Home Assistant's `code_format` pattern, so a code that can't fit is answered before it is sent.
changedBy?string
HA `changed_by`: who or what last locked or unlocked it, where the integration says.
lastChanged?string
When it last changed state (HA `last_changed`), formatted.
battery?number
The same device's battery, 0–100, where it has one.
lastSeen?string
While it is unavailable, when it was last reachable: the `lastSeen` option, else what Home Assistant reports.

Home Assistant’s code_format for a lock is a pattern, such as ^\d{4}$. codeFormat says which keyboard it wants: number when the pattern only takes digits, text otherwise.

onLock(code?: string | undefined) => Promise<LockOutcome>
Calls `lock.lock`, with the code when given. A tap may do this: locking is the safe direction.
onUnlock(code?: string | undefined) => Promise<LockOutcome>
Calls `lock.unlock`, with the code when given. Ask before you call it: only after a deliberate confirmation, with the code where the lock asks for one.
onOpen(code?: string | undefined) => Promise<LockOutcome>
Calls `lock.open` to unlatch the door, only where `supportsOpen`; otherwise answers `failed` with an `unsupported` error and sends nothing. Ask first, as for an unlock.

Hooks can’t be called in a loop, so a list renders one headless component per entity.

import { HaLock, useHaSelector } from "@dashhaus/core";
import type { HaSnapshot } from "@dashhaus/core";
const lockIds = (snapshot: HaSnapshot) =>
Object.keys(snapshot.entities).filter((id) => id.startsWith("lock.")).sort().join(" ");
/** Every lock in the house and how it was left: a lock has no history read, so a list costs nothing extra. */
export function AllLocks() {
const ids = useHaSelector(lockIds).split(" ").filter(Boolean);
return (
<ul>
{ids.map((id) => (
<li key={id}>
<HaLock entityId={id}>
{({ entity }) => (
<>
{entity.name}: {entity.state}
{entity.changedBy ? ` by ${entity.changedBy}` : ""}
{entity.lastChanged ? `, ${entity.lastChanged}` : ""}
</>
)}
</HaLock>
</li>
))}
</ul>
);
}

Orb’s HaLockTile and Field’s HaLockModule are useHaLock above each skin’s card. A press on a locked lock opens its details on the question, or on the keypad; Cancel takes the focus, so Enter never unlocks. A jam is marked as a warning, never a glow. They take the hook’s options as props, plus their own:

import { HaLockTile } from "@dashhaus/orb";
import { HaLockModule } from "@dashhaus/field";
/** The front door with the battery sensor on its device. */
export function OrbLock() {
return <HaLockTile entityId="lock.front_door" batteryEntityId="sensor.front_door_battery" />;
}
/** A lock Home Assistant can read but you shouldn't press from this panel: its state and last change, nothing to unlock. */
export function FieldLock() {
return <HaLockModule entityId="lock.back_door" 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 details. Default true.
readOnly?boolean
View only: the lock's state and last change with nothing to press. Default: on when neither onLock nor onUnlock is given.
footprint?CardFootprint
Card footprint. The skin's stylesheet sets the heights; the three names are the shared vocabulary.
defaultOpen?boolean
defaultAction?NonNullable<"open" | "lock" | "unlock">

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