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¶
- Find the theme you need via section headers above
- Open
specs/XXX-name/spec.mdfor requirements and acceptance criteria - Open
specs/XXX-name/architecture.mdfor technical design, data model changes, file-level impact - Open
specs/XXX-name/plan.mdfor 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.