State Management¶
This doc covers the card's state layer: the state/ module inventory, how each module stores and clears its data, the hass setter and its load-once pattern, and the rule that state modules never talk to each other directly β everything routes through the card instance. It is one spoke of the frontend doc set; start at architecture-overview.md for the hub, and see module-reference.md for the per-file navigation map of every other frontend directory (actions, bindings, renderers, styles, theme-tokens, i18n, cards/, and the entry points).
Module inventory¶
The table below covers the state/ modules. For every other frontend module β actions,
bindings, renderers, styles, theme-tokens, i18n, the cards/ elements, and the entry points β
see the per-file navigation map in module-reference.md.
| Module | File | What it owns |
|---|---|---|
| core | state/core.js |
hass.states access helpers; vacuum entity, state, attributes, battery; vacuumObjectId |
| rooms | state/rooms.js |
Room list building from switch entities; active map resolution; enabled room counting; access graph logic |
| rooms-order | state/rooms-order.js |
Order adapter for rooms; drag/selector state for room reordering |
| room-access | state/room-access.js |
Room access editor open/close state |
| room-editor | state/room-editor.js |
In-modal field editor state (active room, field values, profile picker); the derived supportsSettableMop() getter (reads dashboardSnapshot().supports_water_control) that gates whether the editor shows the clean_mode picker or a read-only observe-only tank indicator β true for Eufy and settable-mop Roborock (S7+), false for observe-only tanks (Roborock S6) |
| room-estimate | state/room-estimate.js |
Room-level time estimates storage |
| room-profiles | state/room-profiles.js |
Room profile library cache |
| room-rules | state/room-rules.js |
Room rules editor state |
| run-profiles | state/run-profiles.js |
Saved run profile library cache (setRunProfilesLibrary / savedRunProfiles), selected profile, and the profile editor draft. The draft carries an ordered steps list β room GROUPS interleaved with charge_wait / wait STOP steps (runProfileDraftSteps / addDraftChargeStep / addDraftWaitStep / setDraftChargeTarget / setDraftWaitMinutes / removeDraftStep / moveDraftStep / captureCurrentRoomsAsDraftGroup, all delegating the immutable array math to state/steps-order.js); _normalizeRunProfile surfaces two backend flags: has_charge_steps (charge-only β any charge_wait step) and the broader has_stops (a SEQUENCED run β any wait/charge_wait boundary OR more than one room_group); has_stops is what drives the stepped card UI (preview/chips/'Runs as' summary/Start routing), with _deriveHasStops as a local fallback when the backend hasn't stamped it. Also tracks the applied-profile slice β setAppliedRunProfile / pendingStepRunProfileId (the applied profile's id IFF it has has_stops, the signal that Start should dispatch start_run_profile instead of a flat start_selected_rooms; gates on has_stops β NOT charge-only has_charge_steps β so a wait-only or multi-group profile still routes through the stepped path; cleared when the user hand-edits rooms) β and the collapsible "This run" preview state (isSteppedPreviewCollapsed / toggleSteppedPreviewCollapsed). Registered in state/index.js via applyRunProfilesState |
| steps-order | state/steps-order.js |
Pure step-mutation helpers for the run-profile steps editor (no card state, not registered on the prototype β imported by state/run-profiles.js). Immutably insert/remove/move/retarget STEP-level entries (insertChargeStep / insertWaitStep / removeStep / moveStep / setChargeTarget / setWaitMinutes), classify steps (isRoomGroupStep / isChargeStep / isWaitStep), clamp targets to the CHARGE_TARGET_MIN/CHARGE_TARGET_MAX (1β100, default 95) and WAIT_MIN_MINUTES/WAIT_MAX_MINUTES (1β1440, default 30) ranges, snapshot the enabled Rooms view into a room_group step (roomsToGroupStep, omitting unset per-room fields so they fall through to the global room settings at dispatch), and sanitizeStepsForSave mirroring the backend normalize_run_profile_steps. Has its own steps-order.test.mjs |
| steps-manifest | state/steps-manifest.js |
Pure "Runs As" step-manifest renderer (renderStepsManifest; no card state, not registered on the prototype). Takes a profile's steps, a room-idβname lookup, and the caller's i18n (t) + HTML escaper, and returns the manifest HTML string ("" when there are no steps) β shared by the command-center run-profiles panel (renderers/run-profiles.js) and the standalone cards/profile-card.js so the two surfaces can't drift on how a routine reads. Has its own steps-manifest.test.mjs |
| dock | state/dock.js |
Dock action status; pause-timeout settings |
| maintenance | state/maintenance.js |
Maintenance snapshot; dock event data |
| metrics | state/metrics.js |
Metrics snapshot; filter state |
| review | state/review.js |
Learning history snapshot; filter state |
| external-jobs | state/external-jobs.js |
External-run review: subtab selection, pending list, and the confirm wizard (split/merge toggles + per-segment assignments). See external-run ingestion |
| learning | state/learning.js |
Live-job learning state: estimate, reanchored estimate, next room, completed rooms, job-active flag; incomplete run log; trouble rooms log |
| order | state/order.js |
Generic order selector (scope, item, position) shared by rooms and run profiles |
| theme | state/theme.js |
Active theme id, working draft, draft dirty flag, theme library; editor UI state (search query, group filter, open groups) |
| map | state/map.js |
Map segments data; zoom/pan transform; segment selection + segmentβroom overlay; dot-anchor overlay; active segmentation_mode; the named custom layouts β customLayouts() / activeCustomLayoutId() / activeCustomLayout() plus the layout-editor slice (openNewLayoutEditor / openRenameLayoutEditor / closeLayoutEditor / isLayoutEditorOpen / layoutEditorMode / layoutDraftName / setLayoutDraftName); the custom-segment composer draft (shapes, grouping/merge/cut, move-scope, rotate, nudge step) via proto.compose* β the draft load and mascot anchors are keyed on ${map_id}:${active_custom_layout_id} (setMapSegmentsData resets the draft when either changes; _composeKey/maybeLoadComposeDraft reload on a layout switch); animal selection/scale; mapAnimalEnabled plus the split mapFloorTextureEnabled / roomFloorTextureEnabled toggles (localStorage evcc_animal_on_<vac> / evcc_floor_tex_map_<vac> / evcc_floor_tex_rooms_<vac>, default on); the live-map display-rotation slice (mapRotation / setMapRotationOptimistic / the _mapRotationOverlay optimistic value β applied only to the live image, never to CV/custom maps); the dwell-debounced mascot follow (mascotDwelledRoomId, committing a room only after sustained dwell); the live-backdrop URL slice β mapImageUrl (the active backdrop URL, short-circuiting to the live image via isLiveBackdropActive when the active scope is a backdrop_source: "live" layout) and _liveMapImageUrl (appends the live entity's last_updated as a query param to cache-bust a stable-token camera. entity each frame); and the per-vacuum room-label visibility toggle mapRoomLabelsEnabled (localStorage evcc_map_labels_<vac>, default on) gating the .evcc-map-label render so VA's labels don't stack on a label-baked live backdrop. It also owns the zone-clean draft (zoneDrafts / zoneDrawMode / canDrawZone / zoneMax / addZoneDraft β the rectangles fed to start_zone_clean), the hidden-regions draw slice (hiddenRegions / hideDrawMode / canDrawHideArea β set_hidden_regions), the area-label anchor slice (areaLabelAnchor β set_area_label_anchor), the overlay-visibility slice (overlaysAligned β set_map_overlay_visibility), and the live-pose slice (livePose, fed by get_map_live_pose) β each with its own *.test.mjs under src/state/ (zone-draft, hidden-regions, area-label-anchor, live-pose-overlay). Note: liveMapImageEntity is owned by state/learning.js (reads dashboardSnapshot().live_map_image_entity), not this module β but mapRotation, mascotDwelledRoomId, the live-URL slice, and mapRoomLabelsEnabled live here |
| live-trail | state/live-trail.js |
Pure cleaning-trail accumulator (accumulateTrail; no card state, not registered on the prototype β imported by state/map.js, which owns the _liveTrail / liveTrail() / resetLiveTrail() slice). Folds one live-pose anchor sample into the position-built trail: appends while cleaning (dedup stationary repeats, bounded length) and FREEZES while docked so a mid-clean recharge pauses rather than splits the trace. Reset is external β the card calls resetLiveTrail() from its own clean-dispatch points. Has its own live-trail.test.mjs |
| mascot-facing | state/mascot-facing.js |
Pure mascot-facing helpers (mascotFacingSign / commitFacing; not registered on the prototype β imported by state/map.js). Derive travel direction for the follow-mode mascot from the CHANGE in the robot anchor between live-pose updates (Eufy exposes no heading), projecting the anchor delta through the map display rotation and applying a jitter deadband + hold so the sprite mirrors instead of "moonwalking". Has its own mascot-facing.test.mjs |
| setup | state/setup.js |
Setup status; setup loading flag |
| confirmations | state/confirmations.js |
Two-tap confirm state for destructive actions |
| toasts | state/toasts.js |
Transient toast / notice queue |
| viewport | state/viewport.js |
Viewport / responsive (mobile vs desktop) state |
| saved-zones | state/saved-zones.js |
Saved-zone library (savedZones / setSavedZonesLibrary, backed by _savedZonesLibrary with a map-segments fallback); panel multi-select "will be cleaned" set (toggleSavedZoneSelection / selectedSavedZoneIds / selectedSavedZoneCount / clearSavedZoneSelection); collapsible-section state (savedZonesCollapsed); grouping-by-room for the panel (savedZonesGrouped β one group per room in map order, Unassigned bucket last). Registered in state/index.js via applySavedZonesState |
| dialog | state/dialog.js |
Card-native confirm/alert/prompt dialog spec (openDialog / pendingDialog / resolveDialog / cancelDialog), replacing browser-native window.confirm/alert/prompt (which use the browser locale and are suppressed in the HA webview). One dialog open at a time; the spec carries a resolve promise fn and a kind-appropriate cancel value (confirm β false, prompt β null, alert β undefined). Registered in state/index.js via applyDialogState |
Init shape and clear shape¶
Each state module stores data in plain properties on this (the VacuumCardState instance). There is no central store object β properties are scattered across the prototype by module. The pattern is consistent:
- A
set*method assigns the property. - A getter method reads it with a fallback.
- A
clear*method (where appropriate) resets to null or{}.
Example:
proto.setDockActionStatus = function(payload) { this._dockActionStatus = payload; };
proto.dockActionStatus = function() { return this._dockActionStatus ?? null; };
Properties are not initialized in the constructor β they are lazily created by the first setter call. This means a getter that fires before the first set returns undefined ?? null β null, which is the intended "not yet loaded" sentinel.
The hass setter and the load-once pattern¶
The hass setter in main.js runs on every HA state push. It:
- Calls
state.sync(hass, config)andactions.sync(hass, state)to refresh references. - Reads the theme sensor attributes and calls
state.setBackendThemeState(). - Calls
_scheduleRender(). - Schedules debounced refreshes for all service-fetched data (dashboard snapshot, start status, dock action status, pause timeout, metrics, learning history, run profiles, incomplete run log, trouble rooms log).
Most of these scheduled refreshes use clearTimeout + setTimeout with different delays (350 ms to 1400 ms) to avoid hammering the backend on rapid HA state bursts.
Load-once pattern: Some fetches should only happen once per session because they are expensive or their data rarely changes. The card implements this with boolean flags (_themeLoaded, _incompleteRunLogLoaded, _troubleRoomsLogLoaded). Once set to true, the corresponding scheduler exits early:
The theme library is loaded once via _loadInitialThemeState(), which is also guarded by this._themeLoaded. Subsequent HA pushes only sync the theme sensor attributes (cheap β already in hass.states); they do not re-fetch the library.
How state modules communicate¶
They don't. Every inter-module interaction routes through the card instance:
- Bindings hold
this.cardand callthis.card._state.someMethod()andthis.card._actions.someAction(). - Actions hold
this.stateand read from it but never write to other action modules. - Renderers hold
this.cardand readthis.card._state.
If a binding needs a value from two different state modules, it calls each module's getter separately and combines the results inline.