Skip to content

Sensor

A room’s reading: a temperature or humidity sensor.*, read with the humidity and battery sensors on the same device. Read-only.

import { useHaSensor } from "@dashhaus/core";
import { fmt1 } from "@dashhaus/core/entities";
/** A room's temperature, with the humidity and battery sensors on the same device. */
export function RoomReading() {
const { entity } = useHaSensor("sensor.bedroom_temperature", {
humidityEntityId: "sensor.bedroom_humidity",
batteryEntityId: "sensor.bedroom_battery",
});
if (entity.state === "unavailable") return <p>{entity.name}: no reading</p>;
return (
<p>
{entity.name}: {entity.temperature == null ? "—" : `${fmt1(entity.temperature)}${entity.unit}`}
{entity.humidity != null && `, ${fmt1(entity.humidity)}% humidity`}
{entity.battery != null && entity.battery < 20 && " (battery low)"}
</p>
);
}

Home Assistant gives each measurement its own entity. The hook reads the companions you name and puts them on one SensorEntity, so a card shows the room, not three sensors.

<HaSensor> is useHaSensor 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: HaSensorResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaSensor(entityId: string, options?: HaSensorOptions): HaSensorResult

One live, read-only sensor for any UI: the primary sensor composed with its humidity and battery companions.

It reads the sensor and its companions, its area, and today’s history of the temperature and the humidity companion. outdoor marks a sensor outside, where indoor comfort words don’t apply.

humidityEntityId?string
Optional companion entity with `device_class: humidity`.
batteryEntityId?string
Optional companion entity with `device_class: battery`.
name?string
Overrides Home Assistant's friendly name.
area?string
Overrides the area Home Assistant's registries resolve for the entity.
outdoor?boolean
lastSeen?string
Overrides when the entity was last reachable, shown while it is unavailable.
unit?"°C" | "°F"
history?SensorHistory
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.
entitySensorEntity
historySensorHistory
entityIdstring
The Home Assistant entity id.
namestring
Usually the room it measures.
area?string
Its area: the `area` option, else the one Home Assistant's registries resolve; none when neither has one.
state"unavailable" | "ok"
temperature?number
humidity?number
Relative humidity in percent. Omit for temperature-only sensors.
unit"°C" | "°F"
outdoor?boolean
Outdoor sensors skip the indoor comfort reading.
battery?number
Battery level in percent, for battery-powered sensors.
lastUpdated?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.

temperatureReading[]
Hourly readings for today: a value, "unavailable", or null for hours still ahead.
humidity?Reading[]
currentHournumber

A sensor’s domain doesn’t say what it measures, so the selector reads each one’s device_class.

import { HaSensor, useHaSelector } from "@dashhaus/core";
import type { HaSnapshot } from "@dashhaus/core";
import { fmt1 } from "@dashhaus/core/entities";
/** Every temperature sensor: a sensor's domain says nothing of what it measures, so read its device class. */
const temperatureIds = (snapshot: HaSnapshot) =>
Object.values(snapshot.entities)
.filter((entity) => entity.entity_id.startsWith("sensor.") && entity.attributes.device_class === "temperature")
.map((entity) => entity.entity_id)
.sort()
.join(" ");
export function AllTemperatures() {
const ids = useHaSelector(temperatureIds).split(" ").filter(Boolean);
return (
<ul>
{ids.map((id) => (
<li key={id}>
<HaSensor entityId={id} loadHistory={false}>
{({ entity }) => (
<>
{entity.name}: {entity.temperature == null ? "—" : `${fmt1(entity.temperature)}${entity.unit}`}
</>
)}
</HaSensor>
</li>
))}
</ul>
);
}
import { useHaSensor } from "@dashhaus/core";
import { fmt1 } from "@dashhaus/core/entities";
/** Today's high and low so far, from the hourly readings the hook reads from Home Assistant's recorder. */
export function HighLow({ entityId }: { entityId: string }) {
const { entity, history } = useHaSensor(entityId);
const readings = history.temperature.filter((reading): reading is number => typeof reading === "number");
if (readings.length === 0) return <p>{entity.name}: no readings yet today</p>;
return (
<p>
{entity.name} today: {fmt1(Math.min(...readings))} to {fmt1(Math.max(...readings))}
{entity.unit}
</p>
);
}

history.temperature has one reading per hour: a number, "unavailable", or null for hours ahead and before the first reading.

Orb’s HaSensorTile and Field’s HaSensorModule are useHaSensor above each skin’s card. They take the hook’s options as props, plus their own:

import { HaSensorTile } from "@dashhaus/orb";
import { HaSensorModule } from "@dashhaus/field";
export function OrbRoom() {
return <HaSensorTile entityId="sensor.living_room_temperature" humidityEntityId="sensor.living_room_humidity" />;
}
export function FieldOutside() {
return <HaSensorModule entityId="sensor.outside_temperature" outdoor footprint="compact" />;
}
entityIdstring
Primary sensor, normally the temperature entity for this device.
showControl?boolean
Compact only. false removes the inline reading, shows its value in its place, and makes the row open the detail sheet. 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.