The contract of an overview module. An overview is a full-screen view that HouseView can show in four ways:
| Way | What starts it |
|---|---|
Idle screensaver (activation: 'idle', the default) |
After idle_timeout seconds without a touch, as an entry of the carousel's screensaver option |
Event display (activation: 'event') |
The overview's own autoShow predicate turns true (a timer starts) |
| Opened by pressing a widget | A clock widget opens World Clock (world-clock-view); a date or calendar widget opens the month calendar (calendar-view); a weather widget opens the weather scene (weather-view); a thermostat widget opens the thermostat view (climate-view); a vacuum widget opens the cleaning panel (vacuum-view); an alarm panel widget opens the alarm view (alarm-view); a weather overview or photo frame card opens its full-screen view (overview-open) |
| Card | Placed on a page or on a dashboard as custom:houseview-<name>-overview-card |
One module serves all of them: render(ctx, mode) is called with mode 'screensaver' or 'card'. The framework adds
the layer, the idle timing, the dismissal, the card element, the picker entry, the editor form and the documentation
index. An overview only reads Home Assistant: its context has no call, no card and no confirm seam. An interactive overview
(section 2) may put buttons in its card mode that ask the card to run an existing action; the overview never calls anything.
Read the widget API and architecture first; overviews reuse the widget registry for
their card mode. Users find the configuration in overviews and screensavers. To scaffold one run
npm run new:overview, see CONTRIBUTING.
src/overviews/<type>/
index.js required export default defineOverview({...})
strings.js required export default {en: {...}} keys start with "<type>_overview."
fixtures.js required export const variants = {...} no imports (the browser harness loads it unbundled)
test.js required export default async function (h) {...}
README.md required front matter and option table; the source of docs/overviews/<type>.md
style.css required one or more sections, all `/* @phase overviews */`
<part>.js optional model.js (pure data mapping), ring.js, sky.js, library.js, ...
Shared files: src/overviews/_shared/overview.css (the layer, the .ov surface, the dismiss and tap buttons),
_shared/performance.css (the lite rules) and _shared/dom.js (setText). src/overviews/index.js is generated by
npm run gen.
Rules (checked by npm run check:overviews and the overview contract test):
- Importable in plain Node. No
window,document,navigatororcustomElementsat import time. - Imports are relative and stay inside the overview's own folder,
src/core/andsrc/overviews/_shared/; no other overview, no packages, nocontroller/,elements/oreditor/.fixtures.jsimports nothing. - Never
callService,localStorage,sessionStorage,fetch,XMLHttpRequest,setInterval,requestAnimationFrame,confirm,Math.randomorDate.now()(usectx.now()); no network URLs in the source. Timed work goes through thetickhook and animation through CSS. renderis pure and returns one root element string whose class starts withov-scene.
import {defineOverview} from '../../core/define-overview.js';
import strings from './strings.js';
export default defineOverview({
type: 'weather', // snake_case; the folder name; `screensaver.type`
label: 'Weather overview',
order: 10, // unique integer: picker order (the derived widget gets 200 + order)
entity: 'required', // 'required' | 'optional' | 'none'
domains: ['weather'], // needed unless entity is 'none'
strings,
options: {/* section 3 */},
render: (ctx, mode) => '<div class="ov-scene ov-weather-scene">...</div>'
});| Field | Default | Meaning |
|---|---|---|
type, label, order, render(ctx, mode) |
required | Identity and the pure render |
entity |
'required' |
'required', 'optional' (validated only when given) or 'none' |
domains |
[] |
Entity domains; required unless entity is 'none' |
strings |
required | The default export of strings.js. en must define <type>_overview.label and <type>_overview.description |
options |
{} |
Flat option keys, identical in a screensaver block and on an overview card (section 3) |
validate(options) |
none | Cross-field rules; throw Error('<exact message>') |
watch(options, config) |
none | Extra entity ids whose change re-renders (weather: the sun entity). The primary entity is always watched. config is the card config as the controller holds it: the derived widget's watch, the screensaver change signature and the carousel's hass-signature all pass it, so an overview built from the dashboard's own widgets (home) can watch every entity of the configured config.widgets |
interactive |
false |
true allows <button data-action data-entity> in card mode (see "Interactive overviews" below). The derived card then has no full-card ov-tap overlay |
controls |
false |
Needs interactive. true makes the full-page layer a control panel (see "Control panels" below): the buttons stay in screensaver mode, a press on one runs its action, a press elsewhere does nothing, and the layer closes through an overview-close button or Escape |
manualClose |
false |
A layer opened from a button stays until its close button or Escape. A tap on the scene does nothing. The idle screensaver still dismisses on the first tap (weather) |
actions |
[] |
controls only. Widget action names the buttons may carry besides INTERACTIVE_ACTIONS (the thermostat view: up, down). Each must be an action a widget registers |
selectable |
true |
false keeps the overview out of the editor's screensaver list. Only the clock, photo frame, weather and home summary are offered; the others (month calendar, world clock, timer display, cover art, the control panels) are overlays, automatic displays or cards. It can still be written in YAML |
data({hass, options, entityId}) |
none | Subscriptions, keyed by the name render reads from ctx.data[name] (section 6) |
afterRender(hctx) |
none | After a card-mode root is created or patched; idempotent |
tick |
none | {every, run, align?} on the shared ticker while the overview is on screen. align: 'minute' runs when the minute changes |
tile(ctx) |
none | A compact face. The card gets a display: 'scene' | 'tile' option; tile renders this small widget, and pressing it opens the full overview as a one-off layer (weather overview) |
tap: {card} |
{card: 'info'} |
What a press on the card does (section 5). A screensaver press always dismisses |
autoWake(ctx) |
none | Called on every hass update while the layer is up; true dismisses it at once, with no tap swallow |
activation |
'idle' |
'idle' or 'event' (section 4) |
autoShow(ctx) |
none | Required for 'event', forbidden for 'idle': the predicate that starts an event display |
priority |
0 |
Idle only. Integer. Among eligible idle entries the highest wins an activation (cover art 10, weather 0); equal priorities take turns |
eligible(ctx) |
always | Idle only. Asked at the moment an activation picks an entry (mode 'screensaver'); false takes the entry out of that activation. Pure, cheap, false for a missing entity |
onPageIdle |
none | Idle seconds that apply instead of the normal timeout while this overview's implicit entry has its widget on the page that is showing and is eligible (cover art: 5) |
defineOverview validates the shape (the messages start with Overview <type>:), freezes the result and derives the
card-mode widget (type <type>_overview, tag houseview-<type-with-hyphens>-overview-card, default size large,
entity policy custom, availability always) that it registers with the widget registry.
Most overviews are pictures: no buttons, a press on a screensaver only wakes the dashboard. An overview that is a page of
the dashboard in its own right (the home summary) sets interactive: true and may render <button data-action="..." data-entity="..."> in card mode. The rules (checked by tests/overviews/overview-contract.test.mjs):
- Only these actions:
goto-widget,camera-grid,run-action,info,overview-close(INTERACTIVE_ACTIONSinsrc/core/define-overview.js). Each is an existing action; the card's controller runs it through the ordinary dispatcher (src/core/dispatch.js) with its own guards.run-actionstill needs anactionwidget for that entity on the carousel, so an overview cannot run anything the dashboard does not already offer. - Every button is a real
<button>with adata-entity. Forgoto-widgetthe entity iswidget:<index>, the index of the widget in the card's normalisedconfig.widgetslist (the number of itsdata-widgetattribute). - In screensaver mode an interactive overview renders without buttons (a screensaver press dismisses it), unless it is a control panel.
- Markup stays free of inline handlers and service calls; the rest of the scene contract (one
ov-sceneroot, no ids, no<svg>, ownov-*pictures only) is unchanged.
goto-widget is a core feature action (src/core/core-actions.js): it finds the carousel page whose placements contain the
widget and calls the controller's _setPage. An unknown or malformed id is a no-op; it works only on a page that exists at the
current size and is ignored while the camera gallery owns the screen. When the home summary is the layer on screen, the same
press closes that layer after the page change, including when the widget is already on the page showing.
A screensaver is a picture: the layer swallows every gesture and the first press only wakes. A view that is operated
rather than looked at (the thermostat view) sets controls: true beside interactive: true. Then:
- The overview renders its buttons in both modes. The layer has no full-area
ov-dismissbutton and carries the classhv-controls; a press on a<button data-action data-entity>runs that action through the ordinary dispatcher (card._act), a press anywhere else does nothing, Tab moves between the buttons, and Escape closes the layer. The layer closes through a button carryingoverview-close(a core feature action, nothing is called on Home Assistant) and still goes away onautoWake, a blocker or a config change like any layer. - Besides
INTERACTIVE_ACTIONSthe buttons may carry the names listed inactions: widget actions that the dashboard already offers (the thermostat view lists the thermostat widget'supanddown, so one piece of code steps, clamps and rounds the target). The overview itself still has nocall. - Such an overview is normally
selectable: false(an idle screensaver that only wakes on a press is not what a control panel is for) and is opened by a widget press (climate-view). - The layer follows the entity like any layer: a hass update re-renders the scene in place (
_screensaverRender), so the view shows the new target after a press.
Control panels may also render a native <input type="range" data-value-action="light-brightness-" data-entity="light.example">. The controller delegates its change event to the existing prefix-action dispatcher
(light-brightness-50 for value 50), serializing writes per entity and retaining the latest queued value. The light
view uses afterRender to paint local pointer changes and emits change on release; network calls remain in the
light widget's action. Its transparent native range retains keyboard and assistive-technology input while the
surrounding surface captures pointer gestures to avoid competing native touch drags.
Reserved option names: type, widget_type, entity, name, enabled, idle_timeout, start_in_edit_mode,
preview, page, size.
Option types are the widget ones plus the two list and entity types: text, icon, enum (values, optional
error), boolean, number (min, max, step), entity (domains) and enum_list (values, max).
options: {
sun_entity: {type: 'entity', domains: ['sun'], default: 'sun.sun'},
details: {type: 'enum_list', values: DETAIL_KEYS, default: ['sunset', 'wind'], max: 6},
daylight: {type: 'enum', values: ['auto', 'day', 'night'], default: 'auto'}
}Validation messages are <Label> <key> must be ... (for example Month calendar week_start must be auto, monday, sunday or saturday. through the error field). The entity rule reads <type> screensaver needs a <domain> entity. in a
screensaver block and <type> overview needs a <domain> entity. on a card.
Editor labels come from strings.js: <type>_overview.option.<key>.label, .helper, and for enums
<type>_overview.option.<key>.value.<value>.
The controller keeps one idle manager per carousel that has at least one enabled entry
(src/core/idle.js, src/controller/screensaver.js). Activity is a pointerdown, keydown, wheel or touchstart
on the card. Time is measured against the clock, so a tablet that slept counts as touched when it wakes. When the
timeout is up and nothing blocks, the controller picks an entry:
- Entries that are disabled, event entries, and entries held back by edit mode are out.
- Entries whose
eligible(ctx)is false are out (an implicit entry withonPageIdlealso needs its widget on the page that is showing). - The highest
prioritywins; equal priorities take turns, round robin from a cursor that survives dismissals. - When nothing is left, nothing shows and the timeout starts over.
The timeout is the idle_timeout of the lead entry: the next explicit idle entry, else an implicit one, default 120
seconds; 0 means never by idle time. onPageIdle shortens it while its condition holds.
Blockers (the timeout restarts after them; a running layer is dismissed by them): the camera grid is open, the timer
alarm is showing, a service call is pending (event entries ignore this one, so a timer started from a HouseView widget can
bring its display up), the dashboard is in edit mode (unless the entry has start_in_edit_mode: true or preview: true),
and a hidden browser tab.
autoShow(ctx) is evaluated on every hass update (only when something the overview reads changed) and on the shared
one-second tick. The layer appears on the false-to-true edge when nothing blocks; it also appears when the page loads
with the predicate already true. It replaces whatever layer is showing. It is not part of the round robin and
idle_timeout is ignored.
- A dismissal (tap, swipe, Escape, Enter, Space) puts the dashboard back and the display stays away until the predicate has gone false and true again. A config reload does not bring a dismissed display back.
autoWake(ctx)removes it when it is no longer wanted (the timer ended).- Two event displays: the newer replaces the older; the older, still wanted and not dismissed, comes back when the newer is gone.
preview: trueshows it whatever the predicate says and again five seconds after each dismissal.
priority and eligible are not allowed on an event overview.
calendar-view: a date or calendar widget asks for the overview namedcalendar_month. Skipped when the card hascalendar_view: false. The options of an explicitscreensaverentry of that type apply.weather-view: a weather widget asks for the overview namedweather. Skipped when the card hasweather_view: false.climate-view: a thermostat widget asks for the overview namedclimatewith its entity, and itsnamewhen it has one. Skipped when the card hasclimate_view: false(the widget then rendersinfoinstead).vacuum-view: a vacuum widget asks for the overview namedvacuumwith its entity, and itsnamewhen it has one. Skipped when the card hasvacuum_view: false(the widget then rendersinfoinstead).alarm-view: an alarm panel widget asks for the overview namedalarm_control_panelwith its entity, and itsnamewhen it has one. Skipped when the card hasalarm_view: false(the widget then rendersinfoinstead).overview-open: a card withdisplay: tile, or an overview withtap: {card: 'open'}(photo frame), opens its own overview.
A manual layer is one-off: the entry never starts by itself (idle_timeout: 0), runs even in edit mode, is shown whatever
the priority and eligibility of the entries around it, and is dropped when the layer goes away, so a configured
rotation keeps its position. It behaves like a screensaver: the first tap only closes it and presses nothing underneath,
unless the overview is a control panel (its own buttons work, its close button or Escape closes it) or it sets
manualClose (the weather scene stays until its close button or Escape; the idle screensaver still dismisses on a tap).
AUTOMATIC_DISPLAYS in src/core/overview-registry.js maps a card option to an overview and a widget type:
| Card option | Overview | Entries come from |
|---|---|---|
timer_display |
timer |
every timer widget on the carousel |
music_screensaver |
cover_art |
every media_player widget on the carousel |
true (the default) uses the widgets' entities, false switches the display off, a list of entities of that domain
replaces the widget-derived list. An explicit screensaver entry for the same overview and entity wins over the implicit
one, enabled or not (enabled: false is how one entity is switched off). Implicit entries have no timeout of their own,
and a stand-alone card (one widget on a Home Assistant dashboard) never creates them. Entries exist only when the bundle
contains the overview.
The layer is created once per activation inside the carousel's shell, so bedtime dimming dims it:
<div class="hv-screensaver" data-screensaver="<type>" data-state="entering" role="dialog" aria-modal="true" aria-label="...">
<div class="ov ov-<type> ov-mode-screensaver"><!-- render(ctx, 'screensaver') --></div>
<button type="button" class="ov-dismiss" aria-label="Dismiss screensaver"></button>
</div>It fades in over 350 ms (no fade with performance: lite or reduced motion). From 500 ms on the layer is opaque, the
shell gets data-covered, and HouseView hides the carousel and stops its work (no widget render, countdown, clock
hands or camera request); a wake catches everything up. Later renders patch the scene in place.
Dismissal: a tap or swipe, Escape, Enter or Space, a blocker, autoWake. The click that follows a dismissing tap is
swallowed for 350 ms so it cannot press the control underneath. With reduced motion the layer stays 150 ms for the same
reason. Arrow keys and swipes never reach the carousel while a layer is up.
Card mode: the derived widget renders <section class="ov ov-<type> ov-mode-card size-..."> with the scene and, by tap:
tap.card |
A press on the card |
|---|---|
'info' (default) |
Opens more-info of the entity (needs an entity) |
'open' |
Opens the full-screen overview as a one-off layer (overviews without an entity: photo frame) |
'none' |
Does nothing (month calendar) |
With tile defined and display: tile, the card renders the tile face and a press opens the full overview.
Accessibility: the layer is a modal dialog labelled with the overview's name; the dismiss button spans the layer; scenes
carry a role and aria-label where they convey information (the timer display has role="timer").
render(ctx, mode), eligible, autoShow, autoWake and tile receive an OverviewContext; afterRender and
tick.run receive it plus el (the .ov element), requestRender() and store (a Map that survives renders).
Screensaver hooks also receive elapsedMs: milliseconds since the current layer appeared, independent of preview_time. It survives data/locale re-renders and resets on each new activation; card-mode hooks omit it.
| Member | Meaning |
|---|---|
hass, config |
Read only. config.preview_time pins the clock |
options |
Resolved options incl. entity and name, defaults applied |
mode |
'screensaver' or 'card' |
overview |
The overview type |
entityId, state, attrs, ok |
The primary entity; ok is false when missing, unavailable or unknown |
name, rawName |
options.name, else the friendly name, else the label; escaped and raw |
data |
Latest payload of each declared subscription (null until known) |
stateOf(id), attrsOf(id), availableOf(id) |
Other entities |
esc, num, t(key, vars), lang(), unit(), icon(name) |
As in the widget context |
now() |
Current time, honouring preview_time |
timeParts(ms), formatTime(ms) |
{main, period, text} in the user's time format and time zone |
formatNumber(n, opts) |
Intl.NumberFormat in the user's language |
localParts(ms), dayOffset(ms) |
Calendar fields and whole days from today, in the display time zone |
Subscriptions (data): return {name: DataSpec}, where a spec is {id, subscribe(hass, emit), supported?(hass), fallback?(hass)}. They are shared and refcounted by src/core/subscriptions.js: one websocket subscription per
(connection, id), released two seconds after the last user goes. Nothing is polled. Without a connection (a mock hass), when
unsupported or after a failure, fallback supplies the value. The weather overview uses this for forecasts and falls
back to the entity's forecast attribute.
- Every section starts with
/* @phase overviews */;/* @owns ... */lists the classes the file owns; selectors are rooted at an owned class and stay within specificity(0,3,0). - No
!important,url(),filter: blur(),backdrop-filteror SVG filters. Animate onlytransformandopacity, and run no animation underprefers-reduced-motion. - Size with container query units inside
.ov(it is a size container), so the scene fits a phone, a 1280 x 800 tablet and a 380 px card. - Rules that apply only on weak devices or under the cover are conditioned on
.perf-liteor[data-covered].
Keys start with <type>_overview.. en must have .label and .description; the editor reads
.option.<key>.label, .helper and .value.<value>. Core strings of the family live in
src/core/screensaver-strings.js (screensaver.dismiss, screensaver.none, ...). The weather overview also ships a
German table; other languages are siblings of en.
check:overviewslints the sources and the CSS;tests/overviews/overview-contract.test.mjsimports every overview in Node and holds it to this document;tests/overviews/*.test.mjstest the puremodel.jsfiles.test.jsruns in a browser harness (npm run test:overviews) that mounts each fixture variant in both modes.- The scripts with real input (
npm run test:overviews:inputand its siblings) drive a real browser with real touch and keyboard events: the first-tap-only-wakes rule, the dismissals and the automatic displays are tested there. npm run shots:overviewsrenders each overview to images for review.
npm run new:overview -- --type energy --label "Energy" --domains sensorcreates the folder from the templates, picks the next freeorderand refreshes the generated files.- Write the pure data mapping in
model.jsfirst (state in, view model out) and unit-test it. - Fill in
index.js: options,render, andactivation,priority,eligible,tap,tickas needed. - Write
strings.js,fixtures.js(every state the scene can be in),style.cssandtest.js. - Write
README.md: front matter (type,label,entity,domains,since), an option table that matchesoptions, a YAML example. Runnpm run gen-docs. - Run
npm run gen,npm run check:overviews,npm run test:unit,npm run test:overviewsandnpm run lint.
Editor previews restore their selected overview after the card connects. Home Assistant can prepare a replacement card before attaching it, or detach and reconnect the same card when an option changes. The card first connects the screensaver controller, then forces editor-preview synchronisation so the selected face remains visible.
Colour options use type: 'colour': opaque hex strings or RGB triplets, edited with Home Assistant’s color_rgb selector. An overview may provide editor.toForm(config) to prepare its form defaults in both editors, and when(data) on options to control visibility.