Skip to content

Vacuum

One vacuum.* entity, a robot vacuum: whether it is cleaning, docked, on its way back, paused, idle or stopped with an error; its battery and fan speed, which it reports itself; and what it can do, from what it declares to Home Assistant.

import { useHaVacuum } from "@dashhaus/core";
import { pressWord, vacuumPress, vacuumWord } from "@dashhaus/core/entities/vacuum/words";
/**
* One button that does what the vacuum can do now: pause it while it moves, start or resume it while it stands. What it
* can do comes from what it declares to Home Assistant, so a vacuum that can't pause is sent home instead.
*/
export function RobotVacuum({ entityId }: { entityId: string }) {
const { entity, actions } = useHaVacuum(entityId, { loadHistory: false });
const press = vacuumPress(entity);
const run = { start: actions.onStart, pause: actions.onPause, return: actions.onReturnToBase };
return (
<p>
{entity.name}: {vacuumWord(entity)}
{entity.battery != null && `, battery ${entity.battery}%`}
{press && <button type="button" onClick={run[press]}>{pressWord(press, entity)}</button>}
</p>
);
}

Integrations name the same activity differently: one says charging where Home Assistant says docked, another returning_home or stopped. They are mapped onto Home Assistant’s own, a kind of cleaning named its own way (spot_cleaning, mopping) is cleaning, and any other state is said in its own words, Emptying the bin, never guessed. An error is a warning; so is a low battery while it stands away from its dock, not cleaning, not on its way home and not charging.

<HaVacuum> is useHaVacuum 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: HaVacuumResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaVacuum(entityId: string, options?: HaVacuumOptions): HaVacuumResult

One live robot vacuum for any UI: the mapped entity (its battery is its own attribute, not a companion), today's cleaning, and its service actions.

It reads the vacuum, its area and today’s cleaning. The battery is the vacuum’s own, so there is no companion to pass.

name?string
area?string
Overrides the area Home Assistant's registries resolve.
lastSeen?string
history?VacuumHistory
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
entityVacuumEntity
historyVacuumHistory
actionsVacuumHandlers

Each supports… flag comes from what the vacuum declares to Home Assistant. Its battery, status and fan speed are known only while it is available.

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 lives: "Hall".
stateVacuumState
stateText?string
An `other` state in words, as the integration names it: `zone_cleaning` → "Zone cleaning".
status?string
HA `status`: the integration's own words for what it is doing ("Charging", "Cleaning the kitchen"), where it says.
error?string
While it is in error, why, where the integration says (HA `error`): "Brush jammed".
battery?number
Its battery, 0–100 (HA `battery_level`, an attribute of the vacuum itself).
charging?boolean
It is charging, where HA's state, `battery_icon` or `status` says so. A charging or docked vacuum never warns of its battery.
supportsStart?boolean
It starts or resumes cleaning (`VacuumEntityFeature.START`).
supportsPause?boolean
It pauses (`VacuumEntityFeature.PAUSE`).
supportsReturn?boolean
It goes back to its dock on request (`VacuumEntityFeature.RETURN_HOME`).
supportsLocate?boolean
It plays a sound to be found (`VacuumEntityFeature.LOCATE`).
supportsFanSpeed?boolean
It takes a fan speed (`VacuumEntityFeature.FAN_SPEED`) and lists at least one.
fanSpeeds?string[]
Its fan speeds, as Home Assistant names them ("quiet", "turbo").
fanSpeed?string
The fan speed it runs at.
lastChanged?string
When it last changed state (HA `last_changed`), formatted.
changedAt?number
The same, in ms since the epoch.
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: the hours it was cleaning. Pass history to supply your own, or loadHistory={false} to skip the read.

samplesHourSample[]
1 for hours it was cleaning, 0 for hours it wasn't, "unavailable", null for hours ahead. Fractions for part of an hour.
currentHournumber
cleaningMinutesnumber
Minutes cleaning today, including the current hour.
onStart() => void
Calls `vacuum.start`: it starts cleaning, or resumes after a pause. A vacuum without `START` reports `unsupported`.
onPause() => void
Calls `vacuum.pause`, where it stops in place. A vacuum without `PAUSE` reports `unsupported`.
onReturnToBase() => void
Calls `vacuum.return_to_base`. A vacuum without `RETURN_HOME` reports `unsupported`.
onLocate() => void
Calls `vacuum.locate`: it plays a sound to be found. A vacuum without `LOCATE` reports `unsupported`.
onFanSpeedChange(speed: string) => void
Calls `vacuum.set_fan_speed`. A vacuum without fan speeds reports `unsupported`; a speed it doesn't list reports `invalid_input`.

An action the vacuum doesn’t declare, or any action while it is unavailable, is reported to onActionError as unsupported and never sent; a fan speed it doesn’t list is reported as invalid_input.

Hooks can’t be called in a loop, so a list renders one headless component per entity. Lists skip each item’s history read with loadHistory={false}.

import { HaVacuum, useHaSelector } from "@dashhaus/core";
import type { HaSnapshot } from "@dashhaus/core";
import { vacuumWarning, vacuumWord } from "@dashhaus/core/entities/vacuum/words";
const vacuumIds = (snapshot: HaSnapshot) =>
Object.keys(snapshot.entities).filter((id) => id.startsWith("vacuum.")).sort().join(" ");
/** Every robot vacuum in the house, what it is doing, and its warning: an error, or a low battery away from its dock. */
export function AllVacuums() {
const ids = useHaSelector(vacuumIds).split(" ").filter(Boolean);
return (
<ul>
{ids.map((id) => (
<li key={id}>
<HaVacuum entityId={id} loadHistory={false}>
{({ entity }) => (
<>
{entity.name}: {vacuumWord(entity)}
{vacuumWarning(entity) ? ` (${vacuumWarning(entity)})` : ""}
</>
)}
</HaVacuum>
</li>
))}
</ul>
);
}
import { useHaVacuum } from "@dashhaus/core";
import { fanSpeedLabel } from "@dashhaus/core/entities/vacuum/words";
/** Its fan speeds, the running one pressed: only where the vacuum declares them, in their own words. */
export function VacuumFanSpeed({ entityId }: { entityId: string }) {
const { entity, actions } = useHaVacuum(entityId, { loadHistory: false });
if (!entity.supportsFanSpeed || !entity.fanSpeeds) return null;
return (
<div role="group" aria-label={`${entity.name} fan speed`}>
{entity.fanSpeeds.map((speed) => (
<button key={speed} type="button" aria-pressed={speed === entity.fanSpeed} onClick={() => actions.onFanSpeedChange(speed)}>
{fanSpeedLabel(speed)}
</button>
))}
</div>
);
}

fanSpeeds is Home Assistant’s fan_speed_list, in the integration’s own words: quiet, standard, turbo, max_plus.

Orb’s HaVacuumTile and Field’s HaVacuumModule are useHaVacuum above each skin’s card. One press pauses it while it moves and starts it while it stands; returning to its dock, locating it and its fan speeds are in its details. An error is marked as a warning, never a glow. They take the hook’s options as props, plus their own:

import { HaVacuumTile } from "@dashhaus/orb";
import { HaVacuumModule } from "@dashhaus/field";
export function OrbVacuum() {
return <HaVacuumTile entityId="vacuum.robot_vacuum" />;
}
/** A row for a phone's column: the first key, the name, and the screen. */
export function FieldVacuum() {
return <HaVacuumModule entityId="vacuum.upstairs_vacuum" footprint="compact" />;
}
entityIdstring
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?boolean
defaultAction?undefined

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