Climate
One climate.* entity: a thermostat, heat pump or air conditioner, with its setpoint or range, modes, presets and fan.
Quick example
Section titled “Quick example”import { useHaClimate } from "@dashhaus/core";
export function Thermostat({ entityId }: { entityId: string }) { const { entity, actions } = useHaClimate(entityId); const target = entity.targetTemperature; return ( <div> <p> {entity.name}: {entity.currentTemperature ?? "—"} {entity.unit}, {entity.hvacAction ?? entity.state} </p> {target != null && ( <> <button type="button" onClick={() => actions.onTargetChange(target - entity.step)}> Cooler </button> <span> {target} {entity.unit} </span> <button type="button" onClick={() => actions.onTargetChange(target + entity.step)}> Warmer </button> </> )} </div> );}step is the unit’s own setpoint step and unit its temperature unit, so the buttons move as the thermostat does.
Component
Section titled “Component”<HaClimate> is useHaClimate 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: HaClimateResult) => ReactNode- Draws the hook's result. The component itself draws nothing.
useHaClimate(entityId: string, options?: HaClimateOptions): HaClimateResultOne live climate entity for any UI: the mapped entity, today's history and its service actions.
It reads the unit, its area and today’s history. unit overrides the temperature unit when the entity doesn’t say.
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.
unit?"°C" | "°F"history?ClimateHistory- 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”entityClimateEntityhistoryClimateHistoryactionsClimateHandlers
entity: ClimateEntity
Section titled “entity: ClimateEntity”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.
state"unavailable" | HvacModehvacModesHvacMode[]hvacAction?HvacActioncurrentTemperature?numbercurrentHumidity?numbertargetTemperature?number- Single setpoint (heat, cool, auto).
targetLow?number- Range setpoints (heat_cool).
targetHigh?numberminTempnumbermaxTempnumberstepnumber- Setpoint step, usually 0.5 or 1.
unit"°C" | "°F"presetMode?stringpresetModes?string[]fanMode?stringfanModes?string[]lastChanged?stringlastSeen?string- While it is unavailable, when it was last reachable: the `lastSeen` option, else what Home Assistant reports.
history: ClimateHistory
Section titled “history: ClimateHistory”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[]- Share of each hour the unit was running, 0-1.
currentHournumberactiveMinutesnumber- Minutes running today, including the current hour.
activity"heating" | "cooling" | "running"- What it was mostly doing, for the summary line.
actions: ClimateHandlers
Section titled “actions: ClimateHandlers”onToggle() => void- Calls `climate.turn_on` when the unit is off, `climate.turn_off` otherwise.
onTargetChange(temperature: number) => void- Calls `climate.set_temperature` with `temperature`. A value outside `minTemp` to `maxTemp` reports `invalid_input`.
onRangeChange?(low: number, high: number) => void- Only for a unit with a `heat_cool` mode. Calls `climate.set_temperature` with `target_temp_low` and `target_temp_high`, in either order.
onModeChange?(mode: HvacMode) => void- Only when Home Assistant lists HVAC modes. Calls `climate.set_hvac_mode`; a mode the unit doesn't list reports `unsupported`.
onPresetChange?(preset: string) => void- Only with preset modes. Calls `climate.set_preset_mode`.
onFanChange?(fan: string) => void- Only with fan modes. Calls `climate.set_fan_mode`.
Every action on an unavailable entity is reported to onActionError as unsupported and never sent.
Every thermostat
Section titled “Every thermostat”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 { HaClimate, useHaSelector } from "@dashhaus/core";import type { HaSnapshot } from "@dashhaus/core";
const climateIds = (snapshot: HaSnapshot) => Object.keys(snapshot.entities).filter((id) => id.startsWith("climate.")).sort().join(" ");
export function AllClimate() { const ids = useHaSelector(climateIds).split(" ").filter(Boolean); return ( <ul> {ids.map((id) => ( <li key={id}> <HaClimate entityId={id} loadHistory={false}> {({ entity }) => ( <> {entity.name}: {entity.currentTemperature ?? "—"} {entity.unit} ({entity.state}) </> )} </HaClimate> </li> ))} </ul> );}Pick the mode
Section titled “Pick the mode”import { useHaClimate } from "@dashhaus/core";import type { HvacMode } from "@dashhaus/core/entities/climate/types";
/** The unit's own modes; the handler is absent when Home Assistant lists none. */export function ModePicker({ entityId }: { entityId: string }) { const { entity, actions } = useHaClimate(entityId, { loadHistory: false }); if (!actions.onModeChange || entity.state === "unavailable") return null; const onModeChange = actions.onModeChange; return ( <label> Mode <select value={entity.state} onChange={(event) => onModeChange(event.currentTarget.value as HvacMode)}> {entity.hvacModes.map((mode) => ( <option key={mode} value={mode}> {mode} </option> ))} </select> </label> );}The mode list is the unit’s own hvacModes, and the handler is missing when there are none.
Keep a range
Section titled “Keep a range”import { useHaClimate } from "@dashhaus/core";
/** In heat_cool, the unit keeps the room between two setpoints; onRangeChange sets both at once. */export function HeatCool({ entityId }: { entityId: string }) { const { entity, actions } = useHaClimate(entityId, { loadHistory: false }); const { targetLow: low, targetHigh: high, step } = entity; if (!actions.onRangeChange || entity.state !== "heat_cool" || low == null || high == null) return null; const setRange = actions.onRangeChange; return ( <p> Keep between {low} and {high} {entity.unit} <button type="button" onClick={() => setRange(low - step, high + step)}> Widen </button> <button type="button" onClick={() => setRange(low + step, high - step)}> Narrow </button> </p> );}In heat_cool, the unit has targetLow and targetHigh in place of targetTemperature.
In a skin
Section titled “In a skin”Orb’s HaClimateTile and Field’s HaClimateModule are useHaClimate above each skin’s card. They take the hook’s
options as props, plus their own:
import { HaClimateTile } from "@dashhaus/orb";import { HaClimateModule } from "@dashhaus/field";
export function OrbClimate() { return <HaClimateTile entityId="climate.living_room" unit="°C" />;}
export function FieldClimate() { return <HaClimateModule entityId="climate.living_room" name="Thermostat" />;}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. The orb keeps its tap and hold actions. 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).