Skip to content

Hooks reference

Every signature on this page is read from the package’s own declarations when the site is built.

One per kind of entity, from @dashhaus/core. Each has its own page under Entities, with its options and what it returns.

useHaLight(entityId: string, options?: HaLightOptions): HaLightResult
useHaToggle(entityId: string, options?: HaToggleOptions): HaToggleResult
useHaCover(entityId: string, options?: HaCoverOptions): HaCoverResult
useHaSound(entityId: string, options?: HaSoundOptions): HaSoundResult
useHaVacuum(entityId: string, options?: HaVacuumOptions): HaVacuumResult
useHaMediaPlayer(entityId: string, options?: HaMediaPlayerOptions): HaMediaPlayerResult
useHaClimate(entityId: string, options?: HaClimateOptions): HaClimateResult
useHaHumidifier(entityId: string, options?: HaHumidifierOptions): HaHumidifierResult
useHaFan(entityId: string, options?: HaFanOptions): HaFanResult
useHaSensor(entityId: string, options?: HaSensorOptions): HaSensorResult
useHaNumeric(entityId: string, options?: HaNumericOptions): HaNumericResult
useHaWeather(entityId: string, options?: HaWeatherOptions): HaWeatherResult
useHaGuestWifi(entityId: string, options?: HaGuestWifiOptions): HaGuestWifiResult
useHaCalendar(entityId: string, options?: HaCalendarOptions): HaCalendarResult
useHaSun(entityId: string, options?: HaSunOptions): HaSunResult
useHaAlarm(entityId: string, options?: HaAlarmOptions): HaAlarmResult
useHaLock(entityId: string, options?: HaLockOptions): HaLockResult
useHaBinarySensor(entityId: string, options?: HaBinarySensorOptions): HaBinarySensorResult
useHaOwlet(entityId: string, options?: HaOwletOptions): HaOwletResult
useHaCamera(entityId: string, options?: HaCameraOptions): HaCameraResult

The provider and the hooks that read the store directly, from @dashhaus/core. Raw state and selectors shows when to use which.

HaProvider(options: HaProviderProps): Element

Own the store lifecycle at the application boundary, not inside a card.

storeHaStore
The store from createHaStore. The provider connects it while mounted and disconnects it after the last provider using it unmounts; it never closes the transport.
moreInfo?(entityId: string) => void
Open the host's own dialog for an entity: Home Assistant's more-info, which plays a camera's live stream. The Lovelace card base passes one; a host with no such dialog leaves it out, and `useHaMoreInfo` returns nothing.
childrenReactNode
Your app.
useHaStore(): HaStore

The store itself, for its actions and background reads. Throws outside an HaProvider.

useHaSelector<T>(selector: (snapshot: HaSnapshot) => T): T

Select one value from the snapshot; the caller re-renders only when it changes (`Object.is`). The selector must return a stable value: a primitive, or an object the store already holds.

useHaEntity(entityId: string): HassEntity | undefined

One raw entity. Unchanged entities keep their identity across updates, so other entities' changes don't re-render.

useHaEntities(entityIds: readonly string[]): ReadonlyArray<HassEntity | undefined>

Several raw entities, in the order asked. The list keeps its identity until one of these entities changes, so a tile composed of many entities re-renders for its own, never for the rest of the house.

useHaStatus(): HaConnectionStatus

The connection’s state: idle, connecting, connected, disconnected or error. See Connection status.

useHaGeneration(): number

A number that moves on every connect, disconnect, loss and recovery. A read or action that settles under another generation than it began under has an unknown outcome.

useHaSnapshot(): HaSnapshot

The whole snapshot; re-renders on every entity change in the house. Prefer a narrower hook.

From @dashhaus/core. Areas, history and requests has examples.

useHaRegistry(): HaRegistry | undefined

The Home Assistant registries, typed, read once per connection generation and shared by every caller. Resolves to `undefined` while connecting, after a failure, and when the transport lacks generic commands. It depends on registry data only, so an entity changing state never re-reads or recomputes it.

useHaAreas(): readonly HaAreaEntities[] | undefined

Every area with the entity ids that belong to it, from the same cached registry read. The entity ids keep the entity registry's order, and an entity that inherits its device's area is grouped there. Areas with no entities are still returned, with an empty list.

useHaArea(entityId: string): string | undefined

Resolve an entity's Home Assistant area name. The registries are shared with every tile; any failure (including a transport without `sendMessage`) resolves to undefined and never touches action state.

useHaHistory(entityIds: readonly string[], load?: boolean, now?: Date): HaHistoryRecord[] | undefined

Today's history for one or more entities: the recorded day from Home Assistant (`history/history_during_period`), extended by every live change seen since that read. The read is shared by callers asking for the same entities, and repeated for every new connection generation and local day, so the strip rolls over at midnight. Any failure (including a transport without `sendMessage`) collapses to `undefined`. Pass the tile's clock as `now` so the day follows it.

useHaRequest<T>(key: string | undefined, fetch: (store: HaStore) => Promise<T>, options?: HaRequestOptions): HaRequestState<T>

One background read through the shared store, deduplicated across callers by store, connection generation and `key`. It runs while connected, again for every new generation, and whenever `key` changes; pass `undefined` to skip it. Results from an older generation are dropped, so a value never outlives the connection that produced it.

A camera’s still or a player’s artwork is the entity’s own picture. useHaPicture gives it as a URL your page can load, resolved against your Home Assistant, and nothing while the entity is unavailable, so a past frame never reads as now. usePicture, from @dashhaus/core/hooks, loads it only once its box is on screen and the page is visible, refreshes a still only while both stay true, and keeps the last frame up while the next one loads. useHaMoreInfo opens Home Assistant’s own dialog for the entity, which plays a camera’s live stream; outside Home Assistant it gives nothing, so open your own details instead.

useHaPicture(entityId: string | undefined): string | undefined

An entity's own picture (`entity_picture`: a camera's still, a player's artwork) as a URL this page can load, resolved by the connection that serves it; nothing while the entity is missing, unavailable or unknown, or names none. A picture follows the attribute, so a rotated token or a new track is a new URL. Draw it with `usePicture` (`@dashhaus/core/hooks`), which owns loading and refreshing it.

usePicture<T extends Element = HTMLElement>(src: string | undefined, options?: PictureOptions): Picture<T>

A picture's lifecycle, for a camera's still or a player's artwork: it loads only once its box is on screen and the page is visible, refreshes (`refresh`) only while both stay true, never in the background, and decodes each frame off the page (`Image.decode()`) before drawing it, so a frame swaps without a flash. The last good frame stays up while the next loads and when a refresh fails (`stale`); a new source that fails clears it (`failed`); no source clears it at once and cancels its load. It never sizes anything: the box that draws it has a fixed size.

useHaMoreInfo(entityId: string | undefined): (() => void) | undefined

Open the host's own dialog for an entity: inside Home Assistant, its more-info dialog, which plays a camera's live stream with Home Assistant's own player. Nothing where the host has none (Storybook, a demo, a standalone app), so a card falls back to its own details rather than drawing a control that opens nothing.

Every time a card says, Since 11:31, Until 12:15 or 70% rain at 14:00, is on one hour cycle: the page’s. Put HourCycleProvider around your page with your header’s hourCycle ("h12" or "h23"; leave it out for the browser locale’s), and every useHa<Name> hook under it formats its times on that clock (Since 2:05 PM). A card that works a time out itself, with core’s words such as eventWhen, precipitation or stillNote, passes them useHourCycle().

HourCycleProvider(options: HourCycleProviderProps): Element

One hour cycle for a dashboard: every card under it, and every `useHa<Name>` hook, says its times on it (spec/core.md → One hour cycle per dashboard). A skin's page puts it around its header and cards with the header's `hourCycle` and `locale`; given neither, it keeps the cycle around it, or the browser locale's.

useHourCycle(options?: HourCycleOptions): HourCycle

The hour cycle every time on this dashboard is said on: the one asked for, else `locale`'s, else the nearest `HourCycleProvider`'s, else the browser locale's. A card calls it with nothing; a header passes its own props, so it and the cards under it read one clock.

From @dashhaus/core/hooks, for any page.

useNow(fixed?: Date): Date

The current time, updated on each minute boundary (not every 60s from mount, which drifts). Background tabs throttle timers and a wall tablet may sleep, so when the page becomes visible again the clock catches up at once. Pass `fixed` to freeze it (stories, screenshots, tests).

useDelayLeft(delay: number | undefined, since: number | undefined, active: boolean): number | undefined

Seconds left in an alarm's exit or entry delay, ticking each second: `delay` is the delay's length in seconds, `since` when it started (ms since the epoch). A tab woken from the background catches up at once. Undefined while not `active`, or with no way to know (no length, or no start).

useScrollSpy<T extends HTMLElement>(rootRef: RefObject<T | null>, key?: unknown): readonly [string | null, (id: string) => void]

Which section of a page is being read, for a nav that marks it, and a way to jump to one. Sections are the elements under `rootRef` with a `data-section-id`; the one in view is the last whose top has passed a line a third of the way down the viewport, the first while the page hasn't scrolled past its top, or the last section once the page is scrolled to its end. It listens for scrolls on the whole document (in the capture phase), so it follows the page whichever ancestor scrolls it: the window in Storybook, Home Assistant's view inside the frontend, through shadow roots. `key` re-reads the sections when the page's content changes.

useHashPage(pages: readonly string[], main: string): readonly [string, (id: string) => void]

The page a dashboard shows, kept in the URL hash (`#page=living`; the main page has none), so the phone's back button returns to the previous page and a link can open one. Going to a page pushes a history entry and keeps the path and whatever state the host router keeps there; back and forward are read from the hash again.

usePlayhead(e: Pick<MediaPlayerEntity, "state" | "duration" | "position" | "positionAt">): PlayheadNow | undefined

Where a player is, worked out when its state, position or duration change, and only then: never on a timer. A card draws `at` and lets CSS run on from it while `running` (`@dashhaus/core/entities/mediaPlayer/position`), so the position ticks with no re-render. Nothing for a stream with no duration, or a player off, asleep or unavailable.

useDemoClimate is there too: the local climate state the skins’ climate stories share, for stories only.

From @dashhaus/core/primitives: behaviour and accessibility for controls you draw yourself. Interaction primitives shows each in use.

usePress(options: PressOptions): { type, aria-pressed, aria-disabled, disabled, data-holding, onPointerDown, onPointerMove, onPointerUp, … }

A button's behaviour, for any skin to draw: tap to press, optionally hold, optionally latch. Spread the result onto a `<button>`. A hold fires after 450 ms unless the pointer moves more than 10 px, buzzes where the device can, sets `data-holding` while it builds, and swallows the click that ends it. The context menu a long touch would open is suppressed while a hold is wired.

useSlider<T extends HTMLElement>(options: SliderOptions<T>): { value, fraction, rootProps, hitProps, inputProps }

A one-value slider's behaviour, for any skin to draw. Spread `rootProps` on the track's box (it gets `data-anim` while a jump animates), `hitProps` on a layer over it that takes the pointer (tap anywhere, or drag; it leaves vertical swipes to the page), and `inputProps` on a visually hidden `<input>`, which carries the keyboard and screen readers: give it its id and label. `fraction(v)` places a value along the track, 0-1.

useRangeSlider<T extends HTMLElement>(options: RangeSliderOptions<T>): { low, high, fraction, rootProps, hitProps, lowInputProps, highInputProps }

Two ordered values on one track (a heat/cool range), low <= high always, drawn by any skin. A tap moves the nearer thumb; a drag moves the thumb nearer where it began, and when the thumbs overlap the tap's side or the drag's first direction picks one. Spread `rootProps`, `hitProps` as for `useSlider`, and `lowInputProps` and `highInputProps` on two hidden range inputs.

useSheet(options: SheetOptions): { mounted, dialogProps }

A modal sheet's behaviour, for any skin to draw: a `<dialog>` that mounts its content when opened, shows as a modal, locks the page's scroll, and on close sets `data-closing` for the skin's exit transition before it closes and unmounts. Escape and a backdrop click close it. On phones the handle drags it down: past 30% of its height (140 px at most) or any flick dismisses it, a short slow drag snaps back, and dragging up meets growing friction. One pointer at a time. Spread `dialogProps` on a `<dialog>` and render the content only while `mounted`; `[data-autofocus]` inside takes focus on open. It stays a native modal `<dialog>`, never a portalled one: the platform puts it in the top layer and makes the page behind inert, and that holds inside the Lovelace card's shadow root, where a portalled dialog's document-level focus trap does not.

useKeypad(options: KeypadOptions): { code, error, busy, press, back, type, submit, inputProps, … }

A code keypad's behaviour, for any skin to draw: the code typed so far, a digit key's press, delete, enter, and the answer (busy, then a wrong code or a failed call). A physical keyboard types too: digits (and letters for a text code), Backspace, and Enter, which submits unless it lands on a button outside the keypad with nothing typed. Spread `rootProps` onto the keypad's root: it takes focus when it appears inside an open dialog, and a dialog that is opening focuses it through `data-autofocus`. A text code can't be typed on the digit keys, and a touch screen has no physical keyboard, so a text keypad also draws a real input: spread `inputProps` onto an `<input>`. It takes the focus in the root's place, so a phone or a wall panel brings up its own keyboard, and Enter in it submits. The digit keys still type into the same code.

Each entity hook is three pure functions you can call without React or a store, for a host that keeps Home Assistant’s state its own way: a mapper from raw state to the model, history parsing, and an action factory over anything with a callService. For a light:

lightFromHass(raw: HassEntity | undefined, entityId: string, options?: LightMapOptions): LightEntity

Map one raw Home Assistant light state to the existing presentational model.

lightHistoryFromHass(records: Records, entityId: string, now?: Date): LightHistory

Replicates the hourly intensity + onMinutes computation of `useDemoLight`.

createLightHandlers(caller: LightServiceCaller, entity: LightEntity, options?: LightHandlerOptions): LightHandlers

Create safe callback handlers for the existing LightTile contract.

Every kind has the same three: climateFromHass and createClimateHandlers, coverFromHass and createCoverHandlers, and so on; a reading has no handlers.