Skip to content

Latest commit

 

History

History
342 lines (274 loc) · 24.7 KB

overview-api.md

File metadata and controls

342 lines (274 loc) · 24.7 KB

Overview API

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.

1. Folder shape

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, navigator or customElements at import time.
  • Imports are relative and stay inside the overview's own folder, src/core/ and src/overviews/_shared/; no other overview, no packages, no controller/, elements/ or editor/. fixtures.js imports nothing.
  • Never callService, localStorage, sessionStorage, fetch, XMLHttpRequest, setInterval, requestAnimationFrame, confirm, Math.random or Date.now() (use ctx.now()); no network URLs in the source. Timed work goes through the tick hook and animation through CSS.
  • render is pure and returns one root element string whose class starts with ov-scene.

2. Definition

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.

Interactive overviews

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_ACTIONS in src/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-action still needs an action widget 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 a data-entity. For goto-widget the entity is widget:<index>, the index of the widget in the card's normalised config.widgets list (the number of its data-widget attribute).
  • 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-scene root, no ids, no <svg>, own ov-* 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.

Control panels

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-dismiss button and carries the class hv-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 carrying overview-close (a core feature action, nothing is called on Home Assistant) and still goes away on autoWake, a blocker or a config change like any layer.
  • Besides INTERACTIVE_ACTIONS the buttons may carry the names listed in actions: widget actions that the dashboard already offers (the thermostat view lists the thermostat widget's up and down, so one piece of code steps, clamps and rounds the target). The overview itself still has no call.
  • 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.

3. Options

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>.

4. Activation

Idle (activation: 'idle')

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:

  1. Entries that are disabled, event entries, and entries held back by edit mode are out.
  2. Entries whose eligible(ctx) is false are out (an implicit entry with onPageIdle also needs its widget on the page that is showing).
  3. The highest priority wins; equal priorities take turns, round robin from a cursor that survives dismissals.
  4. 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.

Event (activation: 'event')

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: true shows it whatever the predicate says and again five seconds after each dismissal.

priority and eligible are not allowed on an event overview.

Manual open

  • calendar-view: a date or calendar widget asks for the overview named calendar_month. Skipped when the card has calendar_view: false. The options of an explicit screensaver entry of that type apply.
  • weather-view: a weather widget asks for the overview named weather. Skipped when the card has weather_view: false.
  • climate-view: a thermostat widget asks for the overview named climate with its entity, and its name when it has one. Skipped when the card has climate_view: false (the widget then renders info instead).
  • vacuum-view: a vacuum widget asks for the overview named vacuum with its entity, and its name when it has one. Skipped when the card has vacuum_view: false (the widget then renders info instead).
  • alarm-view: an alarm panel widget asks for the overview named alarm_control_panel with its entity, and its name when it has one. Skipped when the card has alarm_view: false (the widget then renders info instead).
  • overview-open: a card with display: tile, or an overview with tap: {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).

Implicit entries (automatic displays)

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.

5. The layer, taps and the first tap

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").

6. Context

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.

7. CSS

  • 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-filter or SVG filters. Animate only transform and opacity, and run no animation under prefers-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-lite or [data-covered].

8. Strings

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.

9. Tests

  • check:overviews lints the sources and the CSS; tests/overviews/overview-contract.test.mjs imports every overview in Node and holds it to this document; tests/overviews/*.test.mjs test the pure model.js files.
  • test.js runs 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:input and 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:overviews renders each overview to images for review.

10. Add an overview: checklist

  1. npm run new:overview -- --type energy --label "Energy" --domains sensor creates the folder from the templates, picks the next free order and refreshes the generated files.
  2. Write the pure data mapping in model.js first (state in, view model out) and unit-test it.
  3. Fill in index.js: options, render, and activation, priority, eligible, tap, tick as needed.
  4. Write strings.js, fixtures.js (every state the scene can be in), style.css and test.js.
  5. Write README.md: front matter (type, label, entity, domains, since), an option table that matches options, a YAML example. Run npm run gen-docs.
  6. Run npm run gen, npm run check:overviews, npm run test:unit, npm run test:overviews and npm 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.