Skip to content

Calendar

One calendar.* entity with its events from the start of today to a horizon a week away, and the events of any other calendars you read beside it, merged into one list. What’s next, and when. Read-only.

import { useHaCalendar } from "@dashhaus/core";
import { useHourCycle } from "@dashhaus/core/hooks";
import { calendarAgenda } from "@dashhaus/core/entities/calendar/agenda";
import { eventWhen, freeFor } from "@dashhaus/core/entities/calendar/words";
/** The family's next event and when it is, with the school's calendar read beside it, on the page's clock. */
export function WhatsNext() {
const { entity } = useHaCalendar("calendar.family", { calendarEntityIds: ["calendar.school"] });
const hourCycle = useHourCycle();
if (entity.state === "unavailable") return <p>{entity.name} is unavailable</p>;
if (!entity.events) return <p>Reading {entity.name}…</p>;
const { next } = calendarAgenda(entity.events, entity.now);
if (!next) return <p>{freeFor(entity.days).line}</p>;
return <p>{next.title}: {eventWhen(next, entity.now, hourCycle).line}</p>;
}

calendarAgenda picks the event a card leads with: one under way, else the next to start. Within a day a timed event comes before the all-day ones, so bin day never hides the dentist at two. eventWhen says when it is, counted from the entity’s own minute: In 2h 6m, Until 12:15, All day, Tomorrow 09:30, or on a 12-hour page Tomorrow 9:30 AM (pass it the page’s useHourCycle()). Every skin says it in these words.

<HaCalendar> is useHaCalendar 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: HaCalendarResult) => ReactNode
Draws the hook's result. The component itself draws nothing.
useHaCalendar(entityId: string, options?: HaCalendarOptions): HaCalendarResult

One live, read-only calendar for any UI: the mapped entity composed with its events, and those of any other calendars read beside it, from today's start to the horizon; and today's plan by hour. The events are their own ranged read, `calendar.get_events` through the store's query, as the weather's forecasts are: read again each hour, on a new day, and whenever the calendar itself changes (an event starts or ends), the last events staying up while the next read loads. Today's plan is computed from those events, never read from the recorder; `loadHistory: false` leaves it out. It sends no services.

It reads the calendar and its area, and its events through calendar.get_events, one call per calendar, from the start of today for days days. They are read again each hour, on a new day, and whenever the calendar itself changes (an event starts or ends); the last events stay up while the next read loads. Until the first read settles, the calendar’s own current or next event stands in. Pass events to supply your own, or loadEvents={false} to skip the read.

calendarEntityIds?readonly string[]
More `calendar.*` entities read beside it, their events merged into its own and named by their calendar.
days?number
How many days to look ahead, today included. Default 7.
name?string
Overrides Home Assistant's friendly name.
area?string
Overrides the area Home Assistant's registries resolve.
lastSeen?string
Overrides when the entity was last reachable, shown while it is unavailable.
events?readonly CalendarEvent[]
Host-owned events override (every calendar's, merged); when omitted, they are read with `calendar.get_events`.
loadEvents?boolean
Set false when the events are supplied by another application-owned query: the entity's own event stands in.
onEventsError?(error: unknown) => void
loadHistory?boolean
Set false to leave out today's plan by hour (YAML `history: false`): `history` is then undefined.
entityCalendarEntity
historyCalendarHistory | undefined
Today's plan by hour, from the events: no recorder read. Undefined with `loadHistory: false`.
entityIdstring
The primary calendar.
namestring
area?string
Its area: the `area` option, else the one Home Assistant's registries resolve; none when neither has one.
state"unavailable" | "ok"
"unavailable" while the primary calendar can't be reached; "ok" otherwise, even before its events are read.
events?CalendarEvent[]
Every event from today's local midnight to the end of the horizon, on every calendar it reads, by start; ones already over today are kept, so the details can show the whole day. Omitted until the first read settles and the entity names no event of its own.
nownumber
The minute the card counts from, in ms since the epoch: the hook's minute clock, or a seed's fixed 11:54. "In 25m", "Now" and today's events are all counted from it, so a card draws the same whenever it is drawn.
daysnumber
How many days it looks ahead, today included (the `days` option, default 7).
calendarsnumber
How many calendars it reads, itself included (1 plus the `calendars` option's).
lastUpdated?string
When its events were last read ("11:54").
lastSeen?string
While it is unavailable, when it was last reachable.

One of entity.events.

titlestring
Its summary: "Dentist". An event with none is "Busy", as a shared calendar shows it.
startnumber
When it starts, in ms since the epoch. An all-day event starts at local midnight of its first day.
endnumber
When it ends, in ms since the epoch, exclusive. An all-day event ends at local midnight after its last day.
allDayboolean
A whole day or days, with no time of its own (a birthday, bin day).
location?string
calendar?string
The calendar it is on, by name, when the card reads more than one: "School".

Today’s plan, drawn from the events rather than Home Assistant’s recorder: the share of each hour taken by timed events, the hours still to come included, and the minutes that adds up to. All-day events take no hours. Pass loadHistory: false to leave it out: history is then undefined.

samplesHourSample[]
Per hour, 0 to 1: the share of it taken by timed events (all-day events take none).
currentHournumber
busyMinutesnumber
Minutes taken by timed events today, the whole day: those over, under way and still to come.

calendarAgenda also sorts the day: today is every event on it, all-day first, those already over kept so the day reads whole; left those not over yet; later what starts after today. eventSpan says an event’s span (14:00–15:00, All day).

import { useHaCalendar } from "@dashhaus/core";
import { calendarAgenda } from "@dashhaus/core/entities/calendar/agenda";
import { eventSpan, isOver, leftToday } from "@dashhaus/core/entities/calendar/words";
/** Today's events, all-day first, those already over struck through, and how many are left. */
export function Today() {
const { entity } = useHaCalendar("calendar.family", { days: 1 });
const { today, left } = calendarAgenda(entity.events, entity.now);
return (
<section>
<h2>Today · {leftToday(left.length, today.length)}</h2>
<ol>
{today.map((event) => (
<li key={`${event.start}-${event.title}`} style={{ textDecoration: isOver(event, entity.now) ? "line-through" : undefined }}>
{eventSpan(event, entity.now)} {event.title}
</li>
))}
</ol>
</section>
);
}

Orb’s HaCalendarTile and Field’s HaCalendarModule are useHaCalendar above each skin’s card. Each leads with the next event and when it is, counts what’s left today, and opens on today’s events and what’s coming. With nothing to the horizon they say Free. They take the hook’s options as props, plus their own:

import { HaCalendarTile } from "@dashhaus/orb";
import { HaCalendarModule } from "@dashhaus/field";
/** The family's calendar with the school's beside it, looking two weeks ahead. */
export function OrbFamily() {
return <HaCalendarTile entityId="calendar.family" calendarEntityIds={["calendar.school"]} days={14} />;
}
/** The same as a compact row: the next event's title on the row's line, the clock it starts at on the screen. */
export function FieldFamily() {
return <HaCalendarModule entityId="calendar.family" footprint="compact" />;
}
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 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.