Fan
One fan.* entity: a tower or desk fan, a ceiling fan, an extractor. On and off, its speed, and the presets,
oscillation and direction it declares.
Quick example
Section titled “Quick example”import { useHaFan } from "@dashhaus/core";
/** On and off, and its speed when it has speeds: a slider with one stop per speed. */export function Speed({ entityId }: { entityId: string }) { const { entity, actions } = useHaFan(entityId); const count = entity.speedCount ?? 0; const step = 100 / Math.max(1, count); return ( <fieldset disabled={entity.state === "unavailable"}> <legend>{entity.name}: {entity.state === "on" ? `${entity.percentage ?? "—"}%` : entity.state}</legend> <button type="button" aria-pressed={entity.state === "on"} onClick={actions.onToggle}> {entity.state === "on" ? "On" : "Off"} </button> {entity.supportsSpeed && count >= 2 && ( <label> Speed <input type="range" min={step} max={100} step={step} value={entity.percentage ?? step} onChange={(event) => actions.onPercentageChange(event.currentTarget.valueAsNumber)} /> </label> )} </fieldset> );}speedCount is how many speeds the fan has, from Home Assistant’s percentage_step: 3 for a three-speed fan, 100 for a
stepless one. Its speed is always a percentage, so a three-speed fan runs at 33, 67 or 100.
Component
Section titled “Component”<HaFan> is useHaFan 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: HaFanResult) => ReactNode- Draws the hook's result. The component itself draws nothing.
useHaFan(entityId: string, options?: HaFanOptions): HaFanResultOne live fan for any UI: the mapped entity, today's on/off history and its five capability-gated service actions.
It reads the fan, its area and today’s history.
Options
Section titled “Options”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.
history?FanHistory- 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.
Returns
Section titled “Returns”entityFanEntityhistoryFanHistoryactionsFanHandlers
entity: FanEntity
Section titled “entity: FanEntity”Each supports… flag comes from what the fan declares to Home Assistant. Its speed and preset are known only while it
is on; a fan that is off reports neither.
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: "Bedroom".
state"unavailable" | "off" | "on"supportsSpeed?boolean- True when the fan takes a speed (`FanEntityFeature.SET_SPEED`).
percentage?number- Its speed in percent, 1-100, while it is on and reports one. A fan that is off reports none.
speedCount?number- How many speeds it has, from HA `percentage_step`: 3 for a three-speed fan, 100 for a stepless one.
supportsPreset?boolean- True when it takes a preset (`FanEntityFeature.PRESET_MODE`) and lists at least one.
presetModes?string[]- Its presets, as Home Assistant names them ("sleep", "nature").
presetMode?string- The preset it runs, while on.
supportsOscillate?boolean- True when it oscillates on request (`FanEntityFeature.OSCILLATE`).
oscillating?booleansupportsDirection?boolean- True when it reverses (`FanEntityFeature.DIRECTION`): a ceiling fan's summer and winter direction.
direction?FanDirectionlastChanged?stringlastSeen?string- While it is unavailable, when it was last reachable: the `lastSeen` option, else what Home Assistant reports.
history: FanHistory
Section titled “history: FanHistory”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 on, 0 off, "unavailable", null for hours ahead. Fractions for part of an hour.
currentHournumberonMinutesnumber- Minutes on today, including the current hour.
actions: FanHandlers
Section titled “actions: FanHandlers”onToggle() => void- Calls `fan.toggle`.
onPercentageChange(percent: number) => void- Calls `fan.set_percentage`, rounded to a whole percent from 1 to 100, so it never switches the fan off; setting a speed while it is off turns it on. A fan without speeds reports `unsupported`.
onPresetChange(preset: string) => void- Calls `fan.set_preset_mode`, which turns the fan on. A fan without presets reports `unsupported`; a preset it doesn't list reports `invalid_input`.
onOscillateChange(oscillating: boolean) => void- Calls `fan.oscillate`. A fan that doesn't oscillate reports `unsupported`.
onDirectionChange(direction: FanDirection) => void- Calls `fan.set_direction` with `forward` or `reverse`. A fan that doesn't reverse reports `unsupported`.
Every action on an unavailable entity is reported to onActionError as unsupported and never sent.
Every fan
Section titled “Every fan”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 { HaFan, useHaSelector } from "@dashhaus/core";import type { HaSnapshot } from "@dashhaus/core";
const fanIds = (snapshot: HaSnapshot) => Object.keys(snapshot.entities).filter((id) => id.startsWith("fan.")).sort().join(" ");
export function AllFans() { const ids = useHaSelector(fanIds).split(" ").filter(Boolean); return ( <ul> {ids.map((id) => ( <li key={id}> <HaFan entityId={id} loadHistory={false}> {({ entity }) => ( <> {entity.name}: {entity.state === "on" && entity.percentage != null ? `${entity.percentage}%` : entity.state} </> )} </HaFan> </li> ))} </ul> );}Presets, oscillation and direction
Section titled “Presets, oscillation and direction”import { useHaFan } from "@dashhaus/core";
/** Its presets, its sweep and a ceiling fan's direction: each only when the fan declares it. */export function Extras({ entityId }: { entityId: string }) { const { entity, actions } = useHaFan(entityId, { loadHistory: false }); return ( <> {entity.supportsPreset && ( <div role="group" aria-label={`${entity.name} preset`}> {entity.presetModes?.map((preset) => ( <button key={preset} type="button" aria-pressed={entity.presetMode === preset} onClick={() => actions.onPresetChange(preset)}> {preset} </button> ))} </div> )} {entity.supportsOscillate && ( <button type="button" aria-pressed={entity.oscillating === true} onClick={() => actions.onOscillateChange(!entity.oscillating)}> Oscillate </button> )} {entity.supportsDirection && ( <button type="button" onClick={() => actions.onDirectionChange(entity.direction === "forward" ? "reverse" : "forward")}> {entity.direction === "reverse" ? "Reverse" : "Forward"} </button> )} </> );}presetModes is Home Assistant’s preset_modes: normal, sleep, nature, whatever the fan has. Direction is a ceiling
fan’s: forward for a summer downdraft on most, reverse for winter.
In a skin
Section titled “In a skin”Orb’s HaFanTile and Field’s HaFanModule are useHaFan above each skin’s card. Both set the speed on the light’s
groove or fader, one stop per speed, and keep presets, oscillation and direction in its details. They take the hook’s
options as props, plus their own:
import { HaFanTile } from "@dashhaus/orb";import { HaFanModule } from "@dashhaus/field";
export function OrbFan() { return <HaFanTile entityId="fan.bedroom" />;}
export function FieldFan() { return <HaFanModule entityId="fan.living_room_ceiling" footprint="compact" />;}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.
footprint?CardFootprint- Card footprint. The skin's stylesheet sets the heights; the three names are the shared vocabulary.
defaultOpen?booleandefaultAction?undefined
entityIdstring- The Home Assistant entity id.
footprint?FieldFootprintdefaultOpen?boolean- Open the detail panel on first render (stories).