Skip to content

Sowel Specs Index

This file is a navigation aid over specs/. Every spec under specs/XXX-name/ typically contains three files: spec.md (requirements + acceptance criteria), architecture.md (technical design), plan.md (implementation steps).

Use this index to quickly recover context: scan descriptions, find the relevant spec, then read the full folder for details.

Specs are grouped by theme and annotated with status:

  • βœ… active β€” implemented and in production
  • πŸ” superseded β€” replaced by a later spec (follow the arrow)
  • 🟑 partial β€” implemented, but scope has evolved

Foundations (V0.x) β€” core engine

# Title Status Summary
001 V0.1 MQTT devices βœ… First integration with Zigbee2MQTT bridge. Raw device auto-discovery via MQTT.
002 V0.1 UI scaffolding devices βœ… Initial React frontend with device list.
003 V0.2 Zones βœ… Hierarchical nestable zones. Parent-child tree structure.
004 V0.3 Equipments βœ… User-facing equipments that bind to devices via data keys.
005 V0.5 UI restructuring βœ… Navigation overhaul (home, zones, equipments, devices, admin).
006 V0.6 Sensor equipments βœ… Temperature, humidity, motion, luminance sensor types.
007 V0.7 Zone aggregation βœ… Auto-compute zone metrics from equipment data (motion=OR, temp=AVG, etc.).
008 Shutter equipments βœ… Position + state + cover orders (open/close/stop).
009 V0.8 Recipes βœ… Automation engine with typed slots. First built-in recipes.
010 V0.9 Modes βœ… Named zone-level states (Day/Night/Away) with impacts.
011 V0.10a Integration plugin architecture πŸ” superseded by 040, 053 Initial plugin interface for integrations.

V0.10 β€” built-in integrations (most are now πŸ” externalized as plugins)

# Title Status Summary
012 V0.10b Panasonic Comfort Cloud πŸ” β†’ 050 Panasonic AC cloud API polling (now a plugin).
013 V0.10c MCZ Maestro πŸ” β†’ 049 MCZ pellet stove Socket.IO integration (now a plugin).
014 V0.10d Netatmo Home+Control πŸ” β†’ 048a, 048b, 048c Netatmo HC integration (now split into 3 plugins: weather, control, energy).

V0.11 β€” logging, backup, shutters UX

# Title Status Summary
015 V0.11 Logging system βœ… Pino structured logging, ring buffer, module tagging, UI log viewer.
016 V0.8b Motion Light enhancements βœ… Motion-light recipe refinements (time slots, override, fallback).
017 V0.11b Backup hardening πŸ” β†’ 046, 058, 060 First backup system (export/import).
018 Recipes roadmap βœ… (meta) Roadmap document for planned recipes.
019 V0.8c Switch light βœ… Switch-light recipe (toggle on button press).
020 V0.8e Presence thermostat βœ… Presence-based thermostat setpoint logic with cocoon.
021 V0.8f Zone commands βœ… Zone-level order batching (allShuttersOpen/Close, allLightsOn/Off).

UX & dashboard

# Title Status Summary
022 Dark mode βœ… Tailwind class-based dark mode with user preference.
023 Sunrise / sunset βœ… SunCalc-based sunlight manager with offset settings.
024 Motion light split βœ… Split motion-light into basic + dimmable variants.
025 V0.13 History (InfluxDB) βœ… Time-series history for numeric device data.
026 V0.8 Cocoon thermostat βœ… Bedtime cocoon logic for presence thermostat.
027 V0.8 Presence heater βœ… Presence-based heater recipe (eco/comfort).
028 MQTT publishers βœ… Outbound MQTT publisher manager (mappings from events to topics). v1.2.6: onChangeOnly option β€” publish only on value change to avoid flooding external displays with periodic heartbeats.
029 MQTT brokers βœ… Multi-broker support for MQTT publishers.
030 Logging audit βœ… Consolidated log level strategy and module taxonomy.
031 Notification publishers βœ… Telegram / webhook / FCM / ntfy notification channels.
032 State watch recipe βœ… Generic data-key watch with alarm recipe.
033 Dashboard widgets βœ… Customizable zone widgets on dashboard.
034 Progressive Web App βœ… PWA manifest, service worker (NetworkOnly for /api/), offline banner.
035 Energy dashboard βœ… Day/week/month/year energy breakdown with HP/HC classification.
036 Order dispatch error handling βœ… Graceful fallback when order publish fails.
037 Panasonic CC connection resilience πŸ” β†’ 050 Reconnect logic for Panasonic Comfort Cloud.
038 MCZ connection resilience πŸ” β†’ 049 Reconnect logic for MCZ Maestro.
039 Integrations page redesign βœ… Unified integrations page (list, configure, status).

Plugin system V2 (crucial β€” current architecture)

# Title Status Summary
040 Plugin engine 🟑 superseded by 053 First generation plugin engine (install from local zip).
041 Weather forecast plugin βœ… Open-Meteo-based weather forecast plugin (reference example).
042 Weather forecast equipment βœ… Equipment type for forecast data display.
043 Plugin update βœ… In-place plugin update from GitHub release.
044 Plugin SmartThings βœ… Samsung SmartThings plugin (polling + orders).
045 Plugin SmartThings OAuth βœ… OAuth2 flow for SmartThings authentication.
046 Backup v2 πŸ” β†’ 058, 060 Revised backup format (includes InfluxDB line protocol).
047 Prebuilt plugins βœ… Plugin distribution via GitHub releases (tarball).

V1.0 β€” externalizing all integrations as plugins

# Title Status Summary
048a Plugin Netatmo Weather βœ… Externalized Netatmo Weather Station integration.
048b Plugin Legrand Control βœ… Externalized Legrand Home+Control (lights/shutters/plugs).
048c Plugin Legrand Energy βœ… Externalized Legrand energy monitoring (NLPC meters).
049 Plugin MCZ Maestro βœ… Externalized MCZ Maestro integration.
050 Plugin Panasonic CC βœ… Externalized Panasonic Comfort Cloud integration.
051 Plugin LoRa2MQTT βœ… LoRa2MQTT bridge as plugin.
052 Plugin Zigbee2MQTT βœ… Zigbee2MQTT as plugin (last built-in to be externalized).
053 Package manager βœ… Major refactor: PackageManager service manages all packages (integrations + recipes). GitHub-based distribution with plugins/registry.json.
054 Recipe packages βœ… Recipes externalized as packages (same distribution model as plugins).
055 Versioning + CI/CD + Docker βœ… GitHub Actions release workflow, scripts/release.sh, ghcr.io image, semver tags. Introduced v1.0.0.

V1.0+ β€” self-update & deployment

# Title Status Summary
049 Externalize MCZ Maestro as Plugin βœ… Shipped. See specs/049-plugin-mcz-maestro/.
050 Externalize Panasonic CC as Plugin βœ… Shipped. See specs/050-plugin-panasonic-cc/.
051 Externalize LoRa2MQTT as Plugin βœ… Shipped. See specs/051-plugin-lora2mqtt/.
052 Externalize Zigbee2MQTT as Plugin βœ… Shipped. See specs/052-plugin-zigbee2mqtt/.
053 PackageManager Extraction + Plugin Adaptation βœ… Shipped. See specs/053-package-manager/.
054 Recipe Packages βœ… Shipped. See specs/054-recipe-packages/.
055 Versioning 1.0.0, CI/CD & Docker βœ… Shipped in v1.0.0. See specs/055-versioning-cicd-docker/.
057 Self-update UI πŸ” β†’ 060 Initial self-update via UI (had race condition).
058 Backup completeness βœ… Auto-download missing plugins on startup; dynamic data file scan; FK-safe restore.
059 Remote registry + backup fix βœ… Remote plugins/registry.json fetch with cache + local fallback. InfluxDB ensureBuckets before restore.
060 Self-update helper + detection improvements βœ… Current self-update architecture: helper container pattern (spawn docker:25-cli that survives sowel death), auto pre-update backup in data/backups/ (rotate keep 3), 1h version poll, WebSocket push of update.available, "Check for updates" button, composeManaged detection.
061 Timezone from home location βœ… Auto-derive process.env.TZ from home.latitude/home.longitude via tz-lookup at boot (runs before createLogger() to avoid V8 TZ caching). Endpoints GET /system/timezone + POST /system/restart (helper container). UI: TZ in Settings, CurrentTimePill in header, RestartToast on location change.
062 Water valve equipment βœ… New water_valve equipment type with custom valve icon, water widget family, zone aggregation (open/total + flow sum), zone pill, dashboard widget (close-all), and detail card with toggle + timed watering. Targets SONOFF SWV and similar smart irrigation valves. Foundation for future auto-watering recipe (spec 063).

Order dispatch refactoring (progressive migration)

# Title Status Summary
063 Auto-watering recipe plugin βœ… Shipped in v1.1.0. See specs/063-auto-watering-recipe/.
064 Weather computed rain data + cumulative bar charts βœ… Shipped in v1.1.0. See specs/064-weather-computed-rain/.
065 Freecooling recipe plugin βœ… Shipped in v1.1.2. See specs/065-freecooling-recipe/.
066 Registry independence from Sowel releases βœ… Shipped in v1.2.0. See specs/066-registry-independence/.
067 Order dispatch β€” core + lora2mqtt βœ… New executeOrder(device, orderKey, value) signature with v1 retro-compat. First migration: lora2mqtt v2.0.0. Enum case-insensitive resolution.
068 Order dispatch β€” zigbee2mqtt βœ… Migrate z2m plugin to v2.0.0 (apiVersion 2). Composite payload support preserved.
069 Order dispatch β€” legrand-control Planned Migrate legrand-control (cloud API IDs stored in plugin memory).
070 Order dispatch β€” panasonic-cc Planned Migrate panasonic-cc (guid/param stored in plugin memory).
071 Order dispatch β€” mcz-maestro Planned Migrate mcz-maestro (commandId stored in plugin memory).
072 Order dispatch β€” netatmo-security Planned Migrate netatmo-security (single param: monitoring).
073 Order dispatch β€” smartthings Planned Migrate smartthings (command names stored in plugin memory).
074 Order dispatch β€” cleanup Planned Remove v1 retro-compat. Drop dispatch_config column from device_orders.

Pool equipments

# Title Status Summary
075 Order Dispatch: Legrand Energy Migration βœ… Shipped. See specs/075-order-dispatch-legrand-energy/.
076 Order Dispatch: Netatmo Weather Migration βœ… Shipped in v1.2.12. See specs/076-order-dispatch-netatmo-weather/.
077 Order Categories βœ… Shipped in v1.2.12. See specs/077-order-category/.
078 Button Zone Orders & Zone-First Equipment Selection βœ… Shipped in v1.2.14. See specs/078-button-zone-orders/.
079 Device Data Enum Values βœ… Shipped in v1.2.14. See specs/079-data-enum-values/.
080 Tasmota Plugin Integration βœ… Shipped in v1.2.15. See specs/080-tasmota-plugin/.
081 Pool equipments βœ… pool_pump, pool_cover types with candidate-based binding for multi-channel relays
082 Pool pump schedule βœ… Recipe plugin with 3 daily on/off slots
083 Pool heat pump (Polytropic) βœ… pool_heat_pump type + Modbus integration plugin (Polytropic Master Inverter)

Shelly energy refactor (multi-iteration)

# Title Status Summary
084 Shelly energy β€” overview Planned Guiding principles for the 4-iteration migration from Legrand to Shelly Pro 3EM
085 Iteration 1 β€” shelly-em plugin (live) Planned Sowel plugin: live act_power + counters per channel, side-by-side with Legrand
086 Iteration 2 β€” Shelly drives roles Planned Promote Shelly channels to main_energy_meter + energy_production_meter, retire Legrand
087 Iteration 3 β€” energydata-stack Rejected Replaced by hardware-native archive on Shelly Pro 3EM (60d in-device); see 088 for the actual fix
088 Iteration 4 β€” Shelly gap backfill Planned Plugin queries EM1Data.GetData RPC on boot + hourly to replay missing minutes through the live pipeline

V1.5 β€” by-usage chart, MQTT toggles, state-trigger recipe

# Title Status Summary
089 Security hardening: plugin supply chain & backup path safety βœ… Shipped in v1.6.2. See specs/089-security-hardening-supply-chain-backup/.
090 MQTT mapping enable/disable βœ… Per-mapping enabled flag on MQTT publishers, complementing the existing publisher-level toggle. UI power-off icon + reduced opacity for disabled rows. Lets users silence a single mapping (seasonal source) without losing its configuration.
091 By-usage consumption chart βœ… New GET /energy/by-usage endpoint and a Total / By usage toggle on the Energy page rendering a stacked breakdown per submeter energy_meter + an "Other" residual (main meter - Ξ£ submeters).
092 State-triggered light recipe βœ… New external recipe plugin state-trigger-light (in plugins/registry.json). Turns lights on for a fixed duration when a watched equipment's state alias transitions to a target value. Optional nightOnly filter via the sunlight manager. Introduces crossZone and includeDescendants slot constraints.

V1.7 β€” WAN hardening & security

# Title Status Summary
094 UI Redesign (umbrella + Phase 0 palette swap) βœ… Shipped. See specs/094-ui-redesign/.
095 Design System Phase 1: Typography Polish βœ… Shipped. See specs/095-design-system-typography/.
096 Design System Phase 2: Sidebar βœ… Shipped. See specs/096-design-system-sidebar/.
097 Design System Phase 3: Strip Pills (Zone Aggregation) βœ… Shipped. See specs/097-design-system-strip-pills/.
098 Design System Phase 4: Dashboard Widgets βœ… Shipped. See specs/098-design-system-dashboard/.
099 Design System Phase 5: Equipment Row βœ… Shipped. See specs/099-design-system-equipment-row/.
100 Design System Phase 6: Zone View 2-Column Layout βœ… Shipped. See specs/100-design-system-comportements/.
101 Activity Feed (Zone view right column) βœ… Shipped in v1.8.0. See specs/101-design-system-activity-feed/.
102 Design System Phase 8: Recipe Edit Modal + Surcharges par mode βœ… Shipped. See specs/102-design-system-recipe-modal/.
104 Self-update resilience βœ… Shipped in v1.6.3. See specs/104-self-update-resilience/.
105 WAN hardening βœ… Closes WAN-exposure gaps: @fastify/helmet (CSP/HSTS/X-Frame-Options), CORS default tightened from * to localhost, WebSocket mandatory auth via Sec-WebSocket-Protocol subprotocol + Origin validation, non-root container via entrypoint+gosu (transparent upgrade from root-owned volumes), docker.sock self-update opt-in via override file, auth-by-default invariant on all /api/v1/* routes.

V1.11 β€” plugin runtime hardening

# Title Status Summary
106 Make the topbar "update available" pill actionable βœ… Shipped in v1.9.0. See specs/106-update-pill-actionable/.
107 Surface a changelog link per row in the UpdatesSheet βœ… Shipped in v1.10.0. See specs/107-update-row-changelog-link/.
108 Block releases without release notes βœ… Shipped in v1.10.0. See specs/108-release-notes-required/.
109 Preserve bound device data/orders across partial re-discoveries βœ… Shipped in v1.10.1. See specs/109-device-discovery-preserve-bound/.
110 Category-first binding resolution across the codebase βœ… Shipped in v1.10.3. See specs/110-category-first-binding-resolution/.
111 Plugin soft isolation βœ… Scoped Proxies wrap each plugin's PluginDeps (settings, event bus, device manager). Enforces 4 invariants at the JS layer: settings scoped to integration.<own-id>.*, event whitelist of system.* types, device ownership forced by integrationId, and error confinement on lifecycle methods. Unconditional since v1.11.0. No breaking change for plugin authors.
112 Process crash handlers βœ… Global uncaughtException and unhandledRejection listeners installed at boot. A throw that escapes every other guard now produces a structured fatal log line (stdout + data/logs/sowel.N.log) before the process exits, so Docker's restart policy reboots the container with a usable trace instead of a silent loop. Audit F03, spec 112. Shipped in v1.11.1.
113 Audit log βœ… Persistent SQLite trail of every security-sensitive action (auth, user CRUD, settings, mode, backup, plugin). New audit_log table + AuditLogger service called from API route handlers with actor and IP context. Admin-only GET /api/v1/audit endpoint with pagination and filtering. 365-day retention purged at boot. Sensitive values redacted from meta. Audit F13, spec 113. Shipped in v1.11.1.

V1.13 β€” awning equipment + Somfy RTS bridge

# Title Status Summary
115 Awning equipment + Somfy RTS bridge βœ… New awning equipment type (sibling of shutter) with its own awnings widget family and dedicated V3 colored illustration (window + cassette + 10 trapezoidal scalloped stripes when deployed, retracted fringe when closed). Reuses shutter_position / shutter_move / set_shutter_position categories β€” any integration emitting them can drive an awning. Three zone commands allAwningsExtend/Stop/Retract. Companion plugin sowel-plugin-somfy-rts for the somfyrts2mqtt ESP32+CC1101 bridge. Shipped in v1.13.0.

V1.14 β€” weather station UX

# Title Status Summary
114 Weather station UX βœ… Weather station widget rework: dedicated outdoor module section (temperature, humidity, min/max envelope), bindings exposed on the device selector. Outdoor module integration across 4 layers (DeviceSelector, bindingUtils, SENSOR_DATA_CATEGORIES, CATEGORY_DEFAULTS_ON) so the Netatmo outdoor module historizes by default and shows in Analyse. Same-category label disambiguation in humanBindingLabel (suffix with device name when several bindings of the same category coexist on the chart). Shipped in v1.14.x.

V1.15 β€” Analyse calendar navigator + live submeter breakdown

# Title Status Summary
116 Equipment availability πŸ“ Draft Propagate equipment availability (offline / degraded / online) from device state to equipment level, with UI badges and aggregated zone status. Drafted 2026-05-24, ~1.5-2 days. Not started.
117 Analyse calendar + submeter βœ… Calendar navigator on Analyse view (jour/sem/mois/annΓ©e + arrows) replacing the legacy TimeRange picker, with absolute window fetch and time-scale X-axis. Live submeter breakdown donut on the Energy page. Mobile burger nav for the Analyse view mirroring the Energy nav. Shipped in v1.15.0.
118 Analyse chart improvements πŸ“ Draft Min/max envelope on line charts at 1h / 1d resolution (semi-transparent band around the mean, with a global "Enveloppe min/max" toggle). Bar chart for rain/energy-only selections in AnalyseView (mirrors isCumulativeBarChart already used by HistoryPanel). Proper daily rain total computed locally by the Netatmo backfill (sum_rain_24 daily bucket = sum of 48 raw 30-min values). Backfill safety: only delete aliases the run can restore, plus --only=<filter> to scope re-runs. Documents the 2026-05-30 sowelox rain data-loss incident and the recovery path.

V1.17 β€” V1.19 β€” Energy history API + supervised displays

# Title Status Summary
119 Energy history per-period aggregation βœ… Energy history endpoints return a fixed, calendar-aligned set of pre-aggregated buckets per period (24 hourly for day, 7 daily for week, 28-31 daily for month, 12 monthly for year) so web UI and firmware consumers stop re-aggregating client-side. Single InfluxDB aggregateWindow aligned to the server TZ, HP/HC split preserved per bucket, zero-filled empty buckets, new "1mo" resolution. Shipped in v1.17.0.
120 Display equipment type βœ… New display equipment type for Sowel-supervised screens (first vendor: the sowel-energy-display AMOLED firmware): DisplayPanel detail page with firmware/uptime/rssi, inline language dropdown and brightness slider. Adds firmware/uptime/rssi/language/brightness data + order categories and a displays widget family with zone online/total aggregation. Shipped in v1.18.0.
121 Displays MQTT supervision plugin βœ… Companion MQTT plugin (separate repo, now sowel-plugin-displays) discovering displays via a retained state payload with LWT-driven availability, exposed as devices bindable to the spec 120 equipment. Defines the canonical state JSON + cmd/<key> wire contract reused by any future display hardware. Shipped alongside v1.18.0.
122 Display wake action βœ… Displays gain a display_wake no-value order telling the screen to restore the user's preferred brightness from NVS, so presence-driven sleep recipes don't need to know the level. Core order category + plugin routing to cmd/wake + firmware NVS split current/user brightness. Shipped in v1.19.0.

V1.20 β€” Cost valuation + shadow mode

# Title Status Summary
123 Energy cost valuation βœ… Wh/€ toggle on the Energy page re-pricing history, summary totals and the by-usage stacked bar into euros using the existing HP/HC tariff. Costs computed at read time on /energy/history and /energy/by-usage (submeters via a period-blended €/kWh), no InfluxDB schema change; toggle persisted in localStorage. Shipped in v1.20.0.
124 Shadow mode βœ… SOWEL_SHADOW_MODE=1 makes a container safe to run against a copy of production: UI serves normally, every outbound subsystem (plugins, recipes, MQTT/notification publishers, GitHub polling) is gated off at boot and at runtime, so an admin clicking Enable never dials out. Boot warn log, GET /api/v1/system/mode, amber banner. Shipped in v1.20.0.

V1.21 β€” V1.23 β€” Solar + recipe building blocks

# Title Status Summary
125 Solar panel equipment + APsystems βœ… New read-only solar_panel equipment (one equipment = one PV panel = one inverter channel) with DC power/energy/voltage/current/temperature, dedicated widget, Solar dashboard group and detail panel. Adds the temperature_device category (kept out of room-temp averages) and the companion sowel-plugin-apsystems. Shipped in v1.21.0.
126 Recipe select slot + getSunlight() βœ… Recipe forms gain a select slot type (closed dropdown with per-language labels) and recipes gain ctx.helpers.getSunlight() (sunrise/sunset/isDaylight with spec 023 offsets), the building blocks behind the store's Schedule On/Off recipe. Plus hiddenWhen slot rule. Shipped in v1.23.0.

V1.24 β€” V1.26 β€” Notifications

# Title Status Summary
127 Web Push notifications (PWA) βœ… Native push on the installed PWA over HTTPS, next to Telegram: enable push per device in Settings, then map a Web Push publisher to any equipment/zone/recipe value. VAPID keys generated on first boot, per-user subscriptions with expired-endpoint pruning. Shipped in v1.24.0 (iOS fixes through v1.24.3).
128 Notification re-notify βœ… Notification mappings gain an explicit re-notify option: while a mapped value stays active (e.g. a State Watch alarm) the notification re-sends on a fixed cadence and stops silently once it clears. None / Indefinitely / Limited to N, distinct from the anti-spam throttle. Shipped in v1.26.0.

V1.27 β€” V1.30 β€” Metering, watering, RBAC, three-phase

# Title Status Summary
129 Metering-aware switch βœ… A Switch / Plug surfaces live power and energy when the device reports them (e.g. SONOFF S60ZBTPF): power beside the ON/OFF toggle, energy dashboard + HP/HC feed, live submeter breakdown. Plain relays behave exactly as before. Shipped in v1.27.0.
130 Auto-watering per-slot weekdays βœ… Each watering slot can be limited to chosen weekdays (empty = every day), e.g. 07:30 on school days and 09:00 on weekends. Powered by a new multi-select recipe option rendered as toggle chips. Shipped in v1.28.0.
131 RBAC: config admin-only βœ… The Standard role is scoped to viewing and operating: browse dashboards, see states, actuate equipments, but no create/rename/delete of equipments, recipes, zones or modes and no configuration. Enforced server-side (403) and hidden in the UI. Shipped in v1.29.0. Amended by issue #912: activating, deactivating and applying a mode moved to the standard allowlist β€” which mode is on is runtime state and an actuation, while what a mode is stays admin-only.
132 Per-phase power on Energy Live βœ… Three-phase installs can show a per-phase power breakdown under the live flow diagram: a main meter carrying power_l1/l2/l3 bindings renders one bar per phase, making an unbalanced phase visible at a glance. Binding convention any integration can adopt; pairs with Legrand Energy 2.1.0. Shipped in v1.30.0.

V1.31 β€” V1.34 β€” Camera, weather min/max, water heater

# Title Status Summary
133 Camera equipment type βœ… Vendor-agnostic camera equipment: periodically refreshed snapshot (detail page + widget), on-demand HLS live view, optional monitoring/light/siren controls when the integration exposes them. All media proxied through the backend so the browser never talks to the camera or vendor relay and real URLs stay hidden; binding-gated per feature. Shipped in v1.31.0.
134 Weather daily min/max βœ… Weather station equipments show today's measured min/max temperature under each reading (desktop + mobile widgets, detail page, outdoor and indoor modules). Sowel tracks the envelope itself from samples it already receives β€” vendor-agnostic, no configuration; resets at local midnight and survives restarts. Shipped in v1.32.0.
135 Water heater equipment βœ… New water_heater equipment: ON/OFF through the standard relay channel (Tuya WHD02 works out of the box), optional water temperature under its own alias so it never skews room-temp averages, automatic power/energy display on metering relays, state-aware heating icon. Shipped in v1.34.0.

V1.35 β€” Personal plugin sources

# Title Status Summary
136 Personal plugin sources βœ… Third plugin trust tier next to official/community (spec 089): an admin registers their own public GitHub repos as plugin sources, no central registry PR needed. TOFU security model: tarball SHA256 shown in a confirmation modal, pinned in DB, re-confirmation on every content change; backup restores verify against the pinned hash. New plugin_sources table, PersonalSourceManager, /api/v1/plugins/sources routes, Personal badge + sources section in the Plugins UI. Shipped in v1.35.0.

V1.36 β€” Plugin categories, search, energy live metering

# Title Status Summary
137 Plugins page categories + search βœ… Recipes on the Plugins page grouped under a curated closed category enum (category on registry entries + manifests, validated at read time, display fallback Other); shared search field for integrations + recipes on both tabs matching localized name/description/tags, diacritics-insensitive. getStore() now passes registry i18n/tags through, so the Store tab is finally localized. 14 registry recipes backfilled in place, no plugin release. Shipped in v1.36.0.

V1.37 β€” Recipe tariff helper, zone-qualified pickers (upcoming)

# Title Status Summary
138 Recipe tariff helper βœ… ctx.helpers.getTariff() gives recipe packages the configured HP/HC schedule (spec 126's getSunlight() pattern), so a load-shifting recipe stops re-asking for off-peak hours the instance already knows. Read-only by construction: the snapshot is copied out of the TariffClassifier cache so a package cannot mutate the schedule energy billing runs on, and tariff prices are deliberately withheld β€” third-party code can republish what it is handed. slotRanges()/isWithinSlot() extracted so schedule interpretation has one definition. Merged 2026-08-09 (#379), unreleased.
139 Zone-qualified recipe pickers βœ… Recipe forms flattened the zone tree to leaf names, so an installation with a Salle de bain per building offered several identical options and some equipment renderers showed no zone at all β€” issue #385. New ui/src/lib/zone-path.ts labels each zone with the shortest ancestor suffix that separates it from its homonyms (bare name when unique), and labels equipment candidates against their own list, so a compact dropdown is never truncated by ancestors nobody needed. Five duplicated <option> loops and two duplicated checkbox lists collapse into shared components; AnalyseView drops its own flattening. UI-only: no type, route, event or migration. Shipped in v1.37.1.
141 Order delivery confirmation βœ… executeOrder succeeding only proves the order reached the integration; a 104-second device-offline window on 2026-08-10 swallowed a pool pump OFF and the pump ran 15.5 h unnoticed β€” issue #398. New OrderConfirmationTracker watches every confirmable order (mirror data binding with a comparable vocabulary), expects the ordered value on equipment.data.changed within 30 s (immediate device_offline verdict when every target device is offline), then emits the new equipment.order.unconfirmed event and raises a warning alarm that the notification pipeline already forwards as a push. One bounded self-heal: when the device comes back online within 1 h, the unconfirmed order is re-dispatched once with OrderSource {kind: external, channel: delivery-retry}. No migration, no API route, no UI change. Shipped in v1.39.0.
142 Zone paths in the equipments list βœ… Administration β€Ί Γ‰quipements grouped its equipments by zone name, so homonym zones merged into one heading listing rooms that are not the same room β€” with generically-named sensors under it, the page misreported its own content. Grouping moves to the zone id (groupEquipmentsByZone in spec 139's zone-path.ts), headings spell out the full path with ancestors muted (a section heading owns the page width, unlike the pickers that needed the shortest suffix), and groups follow depth-first tree order instead of the alphabet, so the page reads like the Maison page. Orphaned equipments get a trailing translated heading instead of a hardcoded English one. UI-only: no type, route, event, migration or i18n key. Shipped in v1.37.1.
143 Low battery alerts βœ… Battery-powered devices died silently: spec 116 classifies the radio silence of event-driven battery hardware as normal (#348), so a flat cell showed online while the percentage that predicted it sat unread in the database. New BatteryMonitor watches every battery data of every battery-powered device β€” Device.powerSource is now declared at discovery (Zigbee2MQTT 2.5.0 maps power_source), falling back to "no mains metering" when unknown; voltage is not a mains marker because battery sensors report their cell voltage. Low at ≀ 20 %, recovered at β‰₯ 25 % (hysteresis), battery_low booleans included whatever their category. Raises a system alarm, reminds weekly from a persisted clock, resolves on replacement. sendSystemAlarm now broadcasts to every enabled publisher instead of the first Telegram one β€” a web-push-only install received no system alarm at all. The equipment card shows the battery marker left of its sensor values, driven by the alert (so it also covers equipments that bind no battery data). Migration 017, route GET /devices/battery-alerts. Shipped in v1.43.0.
144 States and measurements on one Analyse chart βœ… Following a water heater means reading the tank temperature and the relay driving it together, and the Analyse page could not draw them on the same chart: actuator feedback categories (light_state, appliance_state, lock_state, gate_state, cover_state) were unclassified, so a relay was charted as a smoothly interpolated 0β†’1 measurement with a 0/1 tooltip, and spec 118's F7 locked a chart to a single family. They now belong to the states family, familiesCompatible() replaces the strict lock (only cumulative stays exclusive β€” it owns the plot as bars), and a mixed chart puts the states on their own right-hand [0, 1] axis with stepAfter lines and semantic ticks (ArrΓͺt/Marche, FermΓ©/Ouvert, DΓ©verrouillΓ©/VerrouillΓ©) while the measurements keep the left axis untouched. The tooltip picks its formatter per series. States-only, cumulative-only and pre-144 saved charts render exactly as before. UI-only: no type, route, event or migration; historization of actuator states stays opt-in per binding. Shipped in v1.42.0.
145 Series colours and Y axes on the Analyse chart βœ… Three things about an Analyse chart could not be controlled. Colours were the eight-entry SERIES_COLORS palette indexed by insertion order with nothing persisted, so removing a series in the middle recoloured every series after it; the pill dot and the legend entries now open a picker (the eight palette colours + a native free-hue input) and the colour is written into SavedChartSeriesConfig.color. The measurement axis declared no domain, so Recharts anchored it at zero and a tank temperature living between 48 Β°C and 55 Β°C read as a flat line in the top eighth of the plot; a toggle writes SavedChartConfig.yAxisFit and fits each measurement axis to [min, max] padded by 8 %, with round ticks picked inside that domain (rounding the domain outwards would hand most of the margin back), the lower bound clamped at zero for a series that never goes negative. Every measurement shared one scale, so a 21 Β°C curve was squashed flat under a 60 % one; a chart plotting exactly two quantities now gets an axis each, left and right, grouped by unit so two temperatures (or humidity + battery, both %) keep one shared scale β€” three or more quantities stay on the shared left axis, and in split mode the ticks carry their unit and are tinted like their curve (single-series axes only). measurementUnits() / axisForCategory() live in history-utils.ts so the rule is testable; the colour lives in a map beside series rather than inside it, because series is a dependency of the history-fetch effect and recolouring must not refetch InfluxDB. Both config fields are optional: pre-145 charts keep the palette order and the zero-anchored axis. The states [0, 1] axis and the cumulative bar chart are untouched. UI-only: no route, event or migration. Shipped in v1.43.0.

| 146 | Confirmation before a sensitive gate action | βœ… | Shipped. See specs/146-gate-action-confirmation/. | | 147 | Persist the activity feed and the arbiter decision journal | βœ… | Shipped. See specs/147-persist-activity-arbiter-history/. | | 148 | Energy arbiter UI polish (design-system + redesigned timeline) | βœ… | Shipped. See specs/148-arbiter-ui-polish/. | | 149 | Dashboard presentation resolver (phase 1) | βœ… | Shipped. See specs/149-dashboard-presentation-resolver/. | | 150 | Typed value normalization + unified binding candidates | βœ… | Kills the recurring "works in Zigbee2MQTT, fails in Sowel" class caused by small type differences (boolean vs "ON" text, enum casing). One coercion authority (shared/value-normalization.ts) now runs once at ingestion in DeviceManager.updateDeviceData: values are stored and propagated at the type the plugin declared at discovery (un-coercible values stay raw + warn-once; polarity-ambiguous vocabularies like OPEN/CLOSED deliberately not guessed). CATEGORY_EXPECTED_TYPE documents the categoryβ†’type contract and flags contradictory declarations at discovery (ON/OFF enums tolerated β€” Tasmota). computeBindingCandidates becomes ONE shared module (shared/binding-candidates.ts, UI re-exports it), fixing the diverged UI copy that silently offered zero candidates for light_dimmable/light_color. gate becomes candidate-based: any on/off relay (Zigbee dry-contact like SONOFF MINI-ZBD, LoRa R1..R4, Somfy gate_trigger) is offered as the command, contact sensors/reeds stay bindable for state derivation, and executeOrder resolves an empty value on a boolean binding to true so the momentary gate button actually actuates a Zigbee relay. No plugin changes, no migration, no API change. Shipped in v1.48.0. | | 151 | Two-Factor Authentication (TOTP + Backup Codes) | βœ… | Shipped. See specs/151-mfa-totp/. | | 152 | Equipment solar command channel | βœ… | Shipped in v1.52.0. See specs/152-equipment-solar-command/. | | 153 | VMC (2-speed ventilation) equipment type | βœ… | New vmc EquipmentType modelling a mechanical ventilation as one functional unit with states OFF / V1 / V2, instead of two switch equipments. Binds up to two on/off relay orders under fixed aliases low (required) and high (optional, single-speed VMC = plain on/off). A logical speed order (off/v1/v2) is decomposed by EquipmentManager into sequenced break-before-make relay orders in src/equipments/vmc-controller.ts, enforcing the universal VMC safety invariant (the two windings are never energized at once). Observed speed exposed as a computed speed value. Full UI surface: type picker, Fan icon, dashboard widget + equipment card + detail-page OFF/V1/V2 selector, energy defaults, EN/FR i18n. No SQL migration (type is free-text). Maps a 2-channel Zigbee relay (Sonoff MINI DUO) to one equipment. Shipped in v1.52.0. | | 154 | Per-equipment invert direction for shutter-family equipments | βœ… | Shipped in v1.52.5. See specs/154-shutter-invert/. | | 155 | Invert Direction for Boolean Gate Triggers (issue #627) | βœ… | Shipped. See specs/155-gate-invert-direction/. | | 156 | UPS (uninterruptible power supply) equipment type | βœ… | New read-only ups EquipmentType modelling an inverter/UPS as one functional unit, with three new data categories: ups_status (closed, severity-ordered enum β€” UPS_STATUS_VALUES), battery_runtime (seconds) and ups_load (percent). Binds whatever telemetry its plugin reports as a single "all data" candidate, like display (spec 120); no order surface, on purpose β€” an accidental shutdown order to a UPS is unrecoverable and the orderly-shutdown chain belongs to upsmon on each protected host. Two contracts sit on the plugin side rather than the core: the load is a percentage and never the power category (submeter enrolment is a blocklist, so a power binding would fold an estimate into the house consumption breakdown), and the device must declare powerSource: "mains" (else the spec 143 low-battery monitor raises "replace the battery" on every outage). Full read-only UI surface: type picker, BatteryCharging icon, new "Power" zone group, dashboard + mobile widget, compact card, detail panel, EN/FR i18n. No SQL migration (type is free-text). Companion integration: sowel-plugin-nut. Shipped in v1.53.0. | | 157 | Shared FlowDiagram, UPS panel rebuilt on it | βœ… | Extracts the Energy Β· Live power-routing diagram (spec 148) into ui/src/components/flow/ so a second surface can use it, then rebuilds the spec 156 UPS panel on it. The topology maps term for term β€” Maison/RΓ©seau/Production becomes Load/Mains/Battery, and the bottom loop that carries solar export carries the battery charge. FlowDiagram owns the viewBox, slots, Manhattan routes, skeleton, overlay, bubbles, pill placement and status tag; callers own formatting, colours and which edges are live. The bottom edge exists in both directions so bubbles run the right way per surface, and motion-path ids are per-instance (useId) β€” they were hardcoded and would have collided with two diagrams on a page. The Energy page renders identically: a characterization test was written against the pre-extraction implementation and run green before anything moved, then kept. The UPS panel becomes three cards β€” diagram, margins & thresholds, technical sheet β€” replacing 29 undifferentiated rows; labels are translated EN/FR (46 keys), booleans render as βœ“/β€” instead of the word false, and spec 156's non-redundancy rule holds: no value appears twice. Shipped in v1.54.0. |

Spec 140 β€” Energy capacity arbiter (upcoming)

# Title Status Summary
140 Energy capacity arbiter βœ… Core arbitration of solar surplus between flexible loads: every surplus-aware recipe controlled on a threshold over grid export and consumed the signal it observed (self-oscillation alone, chaos at two). One core arbiter is now the single meter reader with reservation accounting on the signed grid reading, three-tier effective watts (live draw / learned nominal / declared), a user-owned priority list with self-demotion-only slack, per-claim toleratedImportW, manual-override + wall-switch divergence suspensions, audit signals (revoke-not-honored with anti-cascade, comfort-off-after-revoke, watts-divergence, unclaimed-run), a ctx.helpers.energy.claimCapacity() recipe API, and the FR-10 arbitration surface on Energy β†’ Live (allocation bar, day timeline, decision journal). Default off, phase 1 issues no orders, no-PV homes never see it. Spec reviewed by maintainer + contributor before implementation. Implemented, unreleased.
158 Arbiter baseline metrics βœ… Pure instrumentation for the spec 140 arbiter, with no line of capacity-arbiter.ts changed: the decision journal and the surplus series are purged after 7 days, so nothing could say how often a load short-cycles, how much time it was granted, or how much surplus was exported while a declared load sat idle. An hour-aligned rollup recomputes today and yesterday from the two persisted stores into two daily tables kept 400 days, reusing sustainedAfter() so a metric can never disagree with the ribbon the user is looking at. The headline figure is the short cycle: a grant revoked for a surplus deficit inside minOnS + releaseHoldS, i.e. a load that started on a surplus that did not hold. Validated against 9 days of production data, which immediately showed the pool pump short-cycling on 35 % of its grants, and forced two corrections: revoke-not-honored is a follow-up on an existing revocation rather than a second one (26 % inflation on that installation), and the "missed surplus" figure has to separate "a load was claiming it" (3 %) from "a shiftable load was idle" (46 %) or a healthy arbiter reads as 75 % wasteful. Reads are capped per day and a truncation is logged, never silent; a whole tick is one transaction, so it costs one fsync an hour rather than fourteen. Exposed as GET /api/v1/energy/arbiter/metrics and as scripts/energy/arbiter-metrics.ts, which reads SQLite directly and therefore works on a restored backup. This is phase 0 of the predictive roadmap: adaptive hysteresis and any forecast are deliberately NOT here, because they change the control loop and should be measured against this baseline before being written. Shipped in v1.55.0.
159 Weather forecast: multi-model and ensemble confidence βœ… The forecast plugin stops trusting Open-Meteo's best_match blindly and reads several models, resolving them per variable rather than per call: whichever models the site's grid actually serves are averaged, so a household near a high-resolution domain gets it without any hard-coded geography. Confidence comes from the ensemble API (ensemble-api.open-meteo.com), not from model spread: the members of one model's perturbed run answer "how sure is this forecast" far better than the disagreement between two deterministic models, and they also give a usable rain probability. Measured over 92 days against the site's own station: the ensemble spread correlates with the realised error, and model selection plus a debias term roughly halves the temperature MAE. The UI states it in words, not in engineering notation, because Β± 0.9 Β°C reads as noise to anyone who did not ask for it: a three-level traffic-light pill (high, moderate, low) with the contributing models named underneath. Plugin 2.0.0/2.1.0.
160 PV production forecast βœ… An hourly expected-production curve to J+5 for a declared solar array, learned from the installation's own history rather than modelled from first principles. The household declares the array (tilt, orientation, peak power, one entry per roof plane); everything else is measured. Geometry converts Open-Meteo's horizontal direct radiation to normal incidence and projects it per plane with max(0, cos ΞΈ) clipping β€” averaging an east and a west plane into one produces something facing nowhere. The model is deliberately unremarkable arithmetic: a scalar gain plus one coefficient per local hour, refit nightly on a rolling 45 days. Measured against three alternatives on 92 days of production data it beat a physical model of the array (158 W of hourly error against 310 W) and a non-negative fit over candidate planes (323 W). Gain and shape are fitted separately because the normalised shape came out identical either side of a real +1 kW addition: a declared capacity change therefore re-estimates the gain alone from post-change hours (523 W of error down to 253 W after three days, against six weeks of waiting for the window to drift). The shape is also the site's own record of itself β€” 53 % at 08 h and 61 % at 20 h on the reference installation, which is its trees. Forecast points are persisted with their lead time so the curve can be scored against the meter afterwards; the provisional clear-sky estimate shown before a model exists is deliberately never persisted, so it cannot be scored as if it were a forecast. Requires weather-forecast plugin 2.2.0, which publishes the irradiance series. Shipped in v1.57.0.
161 Fit the PV model from existing history βœ… Spec 160 learns an array from production it watches arrive, which takes about twelve days of an empty panel. This fits it immediately from production the installation has already recorded, paired with the irradiance of those same hours β€” a second series the weather plugin publishes once a day over the past 45 days, daylight hours only (about 630 points instead of 2 200, because republishing that on every poll would put a quarter of a megabyte on the wire). Measured on days the model had never seen: 186 W of hourly error against 325 W for a naive fit, and 101.7 kWh predicted against 100.1 recorded. The gap between those two numbers is the whole design: the reference installation gained 1 kWc on 2026-08-04, and fitted across that date the gain lands between the two arrays and describes neither. So the window is bounded by the shorter of 45 days β€” measured, because a six-week-stale hourly shape costs 149 W against 101 W, the shape moves with the season β€” and an optional "unchanged since" date only the household can supply. Automatic step detection stays out, as it did for spec 160, and the measurement is what justifies asking instead of guessing. Explicit action, never automatic: a model appearing on its own would be indistinguishable from a learned one and would fire before the owner could give that date. Backfilled rows are upserted on the same key the live path uses, so running it twice is harmless. Requires weather-forecast plugin 2.3.0. Shipped in v1.57.0.
162 PV health: tell the household when the panels stop performing βœ… Spec 160 taught Sowel what an array should produce in any given hour; nothing noticed when it stopped. Once a day, measured production is divided by the plane-of-array irradiance the forecaster already computes, on clear midday hours only, and compared with what the array has recently shown itself capable of. Two things in this spec were settled by measurement against the reference installation's own history, and both overturned the first design. The clear-hour criterion: the planning note reached its numbers with cloud cover, which the weather plugin does not publish, so the direct fraction was measured in its place and turned out to be the better criterion β€” restricting to 10-16 h above 0.75 takes day-to-day noise from 9.5 % to 4.3 % while keeping 39 of 47 days. The reference: a rolling median, the obvious choice, is a placebo. Replayed against a real single-panel outage lasting eight months, with the repair date known from the owner, a 20-day median covered 7 % of it β€” the fault fills the window, becomes the reference, and the detector accepts it as the new normal. An 80th centile of 180 qualifying days covers 91 % at the same 2 % false-alert rate, because a fault filling a fifth of a window cannot move a high centile. The question the reference answers is "what is this array capable of", not "what does it typically do". Consequences: the day series is kept 500 days, far longer than the 45-day samples that produce it; soiling is now reported rather than absorbed; and the standing alert is persisted with the reference frozen at the raise, because a restart otherwise re-raised it as new and lost its resolution, and a recomputed reference cleared it after fourteen clear days with the panel still dead. Winter is stated rather than papered over: 182 qualifying days April to September against 50 October to March, so the detector is near-dormant exactly when snow and leaves do their damage, and the card says how fast it can actually see at the rate clear days are arriving. It names a fault's size, never which panel. Shipped in v1.58.0.
163 PV UI reorg: monitoring on the Production page, declaration in Settings βœ… Pure relocation, no backend change. Specs 160/161/162 had parked the forecast, its accuracy record, the declaration form, the backfill and the health card on the production meter's equipment page β€” a page every role can open, mixing hourly observation with a once-per-array-change admin act, and hiding "how is my solar doing" behind Equipments while Energy -> Production was a lone bar chart. The forecast and health panels now render on Energy -> Production, one block per declared meter (named as soon as several meters exist); the declaration form and the fit-from-history action live in a new section of Settings -> Energy next to tariffs and the arbiter, with a meter selector only when several exist; the equipment page keeps only cumuls and electrical measures. The backend's existing permission line needed nothing: forecast and health reads were already open to all roles, backfill and profile writes already admin. The declared kWc stays visible on the monitoring view β€” a stale declaration must be seen where it does the damage β€” and for admins it links to the settings section that fixes it, via a new ?tab= deep link on Settings. One real bug was caught by the review: a meter switch in the settings section could let meter A's profile response land late in a form already keyed to meter B, and the next save would have written A's declaration onto B β€” fixed with a request-sequence guard, and the regression test is proven to fail without it. Shipped in v1.58.0.
164 Granted-but-idle on the arbiter timeline βœ… The ribbon painted one green for every quarter a load held a grant, whether the load consumed the surplus or nothing at all β€” which is exactly what issue #732 surfaced on the reference installation, where a water heater sat off for a week under an unbroken green ribbon. The arbiter now observes, on each granted load, whether its OWN power measurement is above or below the idle threshold, and journals draw-stopped / draw-started once the observation holds five minutes; the timeline gains a granted-idle state rendered as the grant green at 35 %. The evidence is deliberately the measurement alone: the reported relay state lies on an inertial load (the contact opens while the heat pump keeps drawing for another half hour, #631/#733), and a load with no power binding keeps the current green rather than being described from something the arbiter does not know. A grant starts optimistic, so a load that never starts flips exactly once, five minutes in. Spec 158 metrics are held constant by design β€” granted-idle still counts as granted time in grantedS β€” and the review caught the second place that assumption lived (the home-level idle-opportunity figure) plus a restart tail that would have painted the muted green forward for ever. Observation only: not one grant, revoke or reservation changes. Shipped in v1.59.0.
165 One load-state model for the arbitration surface βœ… The arbitration surface is two components in one card, and they were built from two independent state models: the roster table resolved its states in the browser by flattening four arrays of the read model, the ribbon resolved its own in the engine by replaying the decision journal. Nothing kept them in step, and spec 164 proved it by adding granted-idle to the ribbon alone β€” a water heater holding a grant while drawing nothing (#732) read as muted green in one half of the card and solid "AccordΓ©" in the other, at the same instant. Same cause, two more effects: the night state (#577) rewrote a waiting claim into "at rest" for the roster while the ribbon painted it yellow, and the vocabulary had drifted into three key families for one set of states, with two colour maps agreeing by convention only. Now ArbiterQuarterState is promoted to ArbiterLoadState, getPublicState() resolves each load into it, and the roster renders loads without deciding anything; dormancy is published by the engine and applied by one shared helper, to the roster pill and to the ribbon's CURRENT cell only, because past cells are a journal replay and must stay a faithful record. One i18n root (arbiter.loadState.*) and one colour function replace three key families and two maps, and the dead pre-148 lane builder is deleted. The cross-component test is the point of the spec: one fixture, both halves rendered, the pill text and the ribbon cell asserted equal β€” proven to fail when they diverge. Deliberately out of scope: painting suspensions apart on the ribbon, which would move time between the spec 158 metric buckets and deserves its own decision. Read-model and presentation only: no grant, revoke, reservation or journal row differs. Shipped in v1.59.0.
166 Claimant-declared need for a granted load βœ… Spec 164 describes a granted load as consuming or not from its OWN power measurement, which leaves every load without a dedicated meter permanently undescribed: on the reference installation, two of four arbitrated loads (a pool pump exposing only state, and an inverter pool heat pump whose state is published by a different device than the one the arbiter commands and which has no on/off at all). A relay-state fallback was designed and rejected, because it would need gating both on declared shutdown inertia and on the state coming from the commanded device, and still could not describe an inverter. The component that knows is the claimant: CapacityClaimHandle gains reportNeed(boolean), expected of every capacity-claiming recipe, and the arbiter stays domain-agnostic (it receives a boolean and never asks why a pool is warm enough). Precedence is the whole design: a fresh measurement always wins, because the declaration says what the recipe WANTS while the measurement says what the appliance DOES, and the gap between them is exactly what spec 164 exists to show (#732, a recipe wanting to heat while the appliance drew nothing for a week). Two rules came out of the review rather than the design: a state a measurement has already set is HELD through staleness instead of being handed back to the declaration, otherwise a load reporting slower than the 120 s freshness window flaps between sources once per reporting gap; and the first contradicting measurement overturns a declaration immediately rather than serving the 5 min confirmation window, which on such a load could never mature at all, leaving a load drawing 2 kW reading "granted, consuming nothing" for ever. Deliberately NOT a fault state: a heat pump between compressor cycles and a water heater whose thermostat has cut off are both "declared needing, measured idle" and both healthy. No migration, no new state value, no UI change. Shipped in v1.60.0.
167 Documentation currency gates βœ… Shipped. See specs/167-documentation-currency/.
168 Forecast widget: five-day strip and detail panel βœ… Shipped. See specs/168-forecast-widget-five-days/.
169 Dashboard tile for a recipe instance βœ… Shipped. See specs/169-dashboard-recipe-widget/.
170 A zone sums the power its submeters measure βœ… Shipped. See specs/170-zone-power-aggregate/.
171 A click on a recipe tile fires its control βœ… Shipped. Spec 169 gave a recipe instance a Dashboard tile and left the whole card inert, so a card reading "Ready, one click opens the Gate for 15 min" did nothing when clicked, while every other widget has actuated on a card click since spec 098. A tile rendering exactly one control now fires it from the whole card, the same cycle and the same next value as the pill, which stays for anyone who prefers to aim; two controls, none, a disabled instance and edit mode all leave the card inert. Because that turns a 240 px square into a gate opener, the package declares where the confirmation question is answered: tile.confirmFrom names the equipment slot the control actuates and that equipment's own requireConfirmation (spec 146) then decides alone, so the answer is given once and every surface that actuates the gate asks the same question; confirmParam hands it to the instance where no equipment can be derived, and tile.confirm is the package's default. On mobile a guarded card opens the spec 146 slide-to-confirm sheet; the pill is never guarded and desktop never confirms. See specs/171-recipe-tile-primary-action/.
172 The updates panel can finish a personal update βœ… Shipped. See specs/172-updates-sheet-personal-confirm/.
173 A meter that sits inside another one βœ… Shipped. See specs/173-nested-submeters/.
174 A timed action on an actuable equipment βœ… Phases 1 and 2 (engine, API, and the two surfaces it is used from). Nothing in the engine could say "act now, revert after N minutes", so every instance of it was a recipe holding its own clock β€” motion-light, state-trigger-light, delivery-gate β€” three copies with three sets of cancellation rules that already disagreed. An equipment now carries at most one revert the engine owes it, persisted as a row rather than a setTimeout: a deadline still ahead survives a restart on its remainder, one that passed during the outage fires on the way up, which is the case the whole feature exists for. Four rules are decided rather than discovered: a hand-revert on the mirror binding disarms (firing later would undo the user's own hand, and on a toggling command re-open the gate they just closed); a second arm of the same action moves the deadline and dispatches nothing ("open again" from somebody looking at an open gate means "give me more time"); a revert that could not be sent alarms and stops instead of replaying blind; deleting the equipment takes the deadline with it. Both halves go through executeOrder, so inversion (spec 154), value resolution (spec 150) and delivery confirmation (spec 141) are inherited rather than re-stated. Phase 2 configures the command on the equipment page and renders it on the Dashboard tile and the compact card, through ONE shared countdown component; the three remaining surfaces stay for spec 149 (#325), which now inherits one implementation rather than five. It also dropped the refusal of an action and a revert carrying the same value, which had excluded the sequential-impulse gate the feature exists for, and replaced it with an eligibility rule: the equipment must carry the order and a state reading tied to it, or a revert done by hand could never end the window. Also out: a replaySafe declared by the integration, which is what would turn the failure rule from a safe default into the right answer. See specs/174-equipment-timed-action/.
175 A power reading is judged on its own cadence βœ… Four surfaces decided whether a wattage could be drawn as live, and they disagreed: a meter three minutes into a healthy 300 s cycle was silent in the Live banner, "outdated" on its Dashboard tile and dropped from its zone total, at one instant. Each held a constant chosen from the equipment's TYPE, which says nothing about how often a device speaks: main_energy_meter covers both a Shelly at 1 Hz and a cloud poller at 300 s, and one number has to be wrong for one of them. The engine now derives the window from what the source actually does, the median interval between its recent arrivals or, failing that, the polling interval its integration declares, as clamp(2.5 x cadence, 120 s, 30 min), and carries it on the binding as freshnessBudgetMs. The surfaces compare an age to a number they are given, so the divergence goes by construction rather than by discipline, and detection scales: a 1 Hz meter that dies surfaces in two minutes where it took ten, a 300 s poller keeps its quiet. The estimator is in memory and its statistic is a median, which is what absorbs a six-hour outage among one-second samples; a restart falls back to the declared cadence, then to a conservative ten minutes, rather than calling every meter outdated at boot. It also removed demand_5min, an alias LIVE_POWER_ALIASES and the budget rule both special-cased on the premise that a Legrand NLPC has no power channel: it has one, no plugin in the registry has ever produced that alias, and its only declaration in this repository's history was a test fixture. Out of scope, deliberately: the spec 116 equipment status keeps its per-category windows. See specs/175-cadence-derived-freshness/.
176 A thermostat's run state has its own alias βœ… On a submetered thermostat the power alias is the clamp wattage (the metering convention), and the alias is unique per equipment, so the boolean on/off the device reports about itself had nowhere to live: every UI surface compared that wattage to true and read the unit as permanently off. The card toggle never settled, each tap sent ON again (five ON orders in 90 s measured on production while the PAC ran at 2974 W), and turning the unit off took a double-tap race because the card cleared ALL optimistic values on any data change while the clamp pushes one every few seconds. Same root cause as issue #901, one layer up. The device boolean now binds under state, the SAME on/off alias every relay-style equipment already uses (no new name in the data model; scoped to thermostats), and the spec 152 category override tags it appliance_state so the capacity arbiter recognizes it as the run state while metering, the submeter integrator and the energy panel never mistake it for the wattage. Every on/off read goes through one helper (the state alias first, a legacy declared-boolean power as fallback, never a wattage), optimistic clearing became per-alias with a 90 s TTL backstop, and two latent bugs died on the way: RELEVANT_DATA.thermostat predated the spec 077 categories (a freshly bound thermostat silently lost power/setpoint/outdoor), and a toggle_power order aliased state, which no thermostat surface reads. Companion: panasonic-cc 2.3.2 adds a 45 s follow-up on-demand poll for Comfort Cloud's lag. See specs/176-thermostat-run-state/.
177 What a thermostat is; extras rendered as extras βœ… Before, a Sowel thermostat was whatever the Panasonic and MCZ devices exposed: their keys (nanoe, airSwingUD, profile, resetAlarm) were the auto-binding list in the product UI, a device needed the raw key targetTemperature to be offered as a thermostat at all, and the card had a branch per vendor β€” the cause behind spec 176, whose own words were "an alias is not a vocabulary". The core is now declared once in src/shared/thermostat-contract.ts (temperature, setpoint, state, power, operationMode, optional outsideTemperature), a device is a thermostat when it carries a setpoint / set_setpoint category, the room temperature and the operating mode resolve from categories like the rest of the core (operation_mode / set_operation_mode join the taxonomy, values auto/heat/cool/dry/fan/off), and a thermostat binds every order its device exposes β€” the core canonicalised, the rest as extras under their own keys. The card leads with the core and renders the extras generically from the order's type; the resetAlarm, stoveState, profile, fanSpeed and ecoMode branches are gone. No data, plugin, binding or migration change: an existing thermostat keeps every binding and control it had. Part 1 of issue #919; part 2 (#922) makes the plugins publish the contract. See specs/177-thermostat-contract/.
178 Pressing again asks for longer, then gives up βœ… Spec 174 gave the engine one gesture: a second press extends the window by the same length, for ever. That answers "not yet" but never "how much longer" β€” on the reference installation, getting an hour out of a gate configured at a quarter of one takes four identical presses, and the control cannot say what the next press will do because every press does the same thing. An equipment now declares a LADDER of window lengths (durationStepsMs, two to six, increasing) and a press walks up it: the first acts and arms the shortest, each further one moves the deadline to now + next rung and dispatches nothing, and a press past the top gives the deadline up without reverting β€” the gate stays open and nothing will close it. That last press is deliberately not the cancel button, which sends the revert: "stop counting" and "close it now" are two intents, and both keep a control. The rung is persisted (timed_actions.step_index), so a restart cannot silently restart the climb and turn a give-up press into another hour. A ladder edited under a running window re-places it by length rather than by index, because a stored index means something else once the rungs move. durationMs is forced to the first rung at write time β€” two places claiming what the first press does is how they come to disagree. Everything without a ladder keeps spec 174 rule 3 exactly. See specs/178-timed-action-duration-steps/.
179 A meter fed by a separate supply βœ… Spec 173 declared one topology the reconciliation arithmetic could not see β€” a meter inside another one. This is the other: a clamp on a circuit fed by a second utility supply (the case that raised it: an EV charging socket on another subscription). Enrolment being a blocklist (#523), the clamp lands in every reconciliation against the main meter, and three numbers go wrong at once: the partition shows a 3 kW slice the main total never carried, the "Other" residual is eaten to its clamp at 0, and spec 123 prices another supply's kilowatt-hours at the main tariff. Same design principle as 173 β€” declare what is true of the switchboard, not a "hide this" tick box: Equipment.separateSupply says the meter hangs off a different supply, and both surfaces (by-usage and the spec 117 live donut) render it apart β€” its own group, raw series, kWh only, never in € β€” while its card, history and charts stay untouched. Refused on the main meter and production types (the reference cannot be outside itself), and a separate-supply meter cannot be a spec 173 parent β€” nothing the partition renders can be "inside" a meter on another supply; a containment declaration pointing at one is stored and simply unused, so clearing the flag restores the subtraction untouched. Zone aggregation (spec 170) deliberately stays raw: the garage genuinely draws those watts, whichever meter bills them. See specs/179-separate-supply-meter/.

How to use this index after context loss

  1. Find the theme you need via section headers above
  2. Open specs/XXX-name/spec.md for requirements and acceptance criteria
  3. Open specs/XXX-name/architecture.md for technical design, data model changes, file-level impact
  4. Open specs/XXX-name/plan.md for implementation steps

For the current plugin-based architecture, start with spec 053 (PackageManager) β€” it's the root of everything plugin-related.

For self-update, start with spec 060 β€” it supersedes spec 057 and is the current design.

For the full system overview, see technical/architecture.md.

For production operations (deploy, backup, self-update, logs), see technical/deployment.md.