Skip to content

Sound

A sound machine’s sound: the switch.* that plays it, and the volume number.* beside it.

import { useHaSound } from "@dashhaus/core";
/** A sound machine: its sound's switch, and its volume number found beside it. */
export function SoundMachine({ entityId }: { entityId: string }) {
const { entity, actions } = useHaSound(entityId);
const { onVolumeChange } = actions;
return (
<fieldset disabled={entity.state === "unavailable"}>
<legend>{entity.name}</legend>
<button type="button" aria-pressed={entity.state === "on"} onClick={actions.onToggle}>
{entity.state === "on" ? "Playing" : "Off"}
</button>
{onVolumeChange && (
<label>
Volume
<input
type="range"
min={0}
max={100}
value={entity.volume ?? 0}
onChange={(event) => onVolumeChange(event.currentTarget.valueAsNumber)}
/>
</label>
)}
</fieldset>
);
}

Without volumeEntityId, the hook looks beside the switch: for switch.nursery_music it tries number.nursery_music_volume, then number.nursery_volume. The volume is a percent of that number’s own range.

<HaSound> is useHaSound 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: HaSoundResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaSound(entityId: string, options?: HaSoundOptions): HaSoundResult

One live sound machine for any UI: the switch and its volume mapped together, today's history and its actions.

It reads the switch, the volume number, its area and today’s history.

volumeEntityId?string
The volume `number.*`. Default: `number.<device>_music_volume` or `number.<device>_volume` beside the switch.
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?SoundHistory
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.
entitySoundEntity
historySoundHistory
actionsSoundHandlers
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"
volume?number
Volume as a percent of the device's range, 0-100. Absent without a volume entity or a reading.
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[]
1 for hours it was on, 0 off, "unavailable", null for hours ahead. Fractions for part of an hour.
currentHournumber
onMinutesnumber
Minutes on today, including the current hour.
onToggle() => void
Calls `switch.toggle`.
onVolumeChange?(percent: number) => void
Only when a volume number with a usable range is found. Calls `number.set_value`, turning the percent back into the number's range and step. Setting the volume never switches the sound on or off.

Every action on an unavailable entity is reported to onActionError as unsupported and never sent.

A switch doesn’t say it plays a sound, so there’s nothing in the house to find them by: list them yourself. The Lovelace cards ask for the same thing, with card: sound on the switch.

import { HaSound } from "@dashhaus/core";
/**
* A sound is a switch, and nothing in a switch says it plays a sound, so the list is yours to name. The volume
* entity is found beside each one unless you give it.
*/
const SOUNDS = [
{ entityId: "switch.nursery_music" },
{ entityId: "switch.bedroom_noise", volumeEntityId: "number.bedroom_noise_level" },
];
export function AllSounds() {
return (
<ul>
{SOUNDS.map((sound) => (
<li key={sound.entityId}>
<HaSound {...sound} loadHistory={false}>
{({ entity, actions }) => (
<button type="button" aria-pressed={entity.state === "on"} onClick={actions.onToggle}>
{entity.name}
{entity.volume == null ? "" : `, volume ${entity.volume}%`}
</button>
)}
</HaSound>
</li>
))}
</ul>
);
}

Orb’s HaSoundTile and Field’s HaSoundModule are useHaSound above each skin’s card. They take the hook’s options as props, plus their own:

import { HaSoundTile } from "@dashhaus/orb";
import { HaSoundModule } from "@dashhaus/field";
export function OrbSound() {
return <HaSoundTile entityId="switch.nursery_music" footprint="compact" />;
}
export function FieldSound() {
return <HaSoundModule entityId="switch.nursery_music" volumeEntityId="number.nursery_volume" />;
}
entityIdstring
The sound's `switch.*` (the Tuya integration's `switch.<device>_music`).
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.