Skip to content

Climate

One climate.* entity: a thermostat, heat pump or air conditioner, with its setpoint or range, modes, presets and fan.

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.

<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): HaClimateResult

One 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.

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.
entityClimateEntity
historyClimateHistory
actionsClimateHandlers
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" | HvacMode
hvacModesHvacMode[]
hvacAction?HvacAction
currentTemperature?number
currentHumidity?number
targetTemperature?number
Single setpoint (heat, cool, auto).
targetLow?number
Range setpoints (heat_cool).
targetHigh?number
minTempnumber
maxTempnumber
stepnumber
Setpoint step, usually 0.5 or 1.
unit"°C" | "°F"
presetMode?string
presetModes?string[]
fanMode?string
fanModes?string[]
lastChanged?string
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[]
Share of each hour the unit was running, 0-1.
currentHournumber
activeMinutesnumber
Minutes running today, including the current hour.
activity"heating" | "cooling" | "running"
What it was mostly doing, for the summary line.
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.

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>
);
}
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.

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.

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?boolean
defaultAction?undefined

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