Skip to content

feat(drivers): let a control declare what it does to heat - #1995

Draft
hannesb90 wants to merge 3 commits into
srcfl:masterfrom
hannesb90:feat/thermal-control-declaration
Draft

hannesb90 wants to merge 3 commits into
srcfl:masterfrom
hannesb90:feat/thermal-control-declaration

Conversation

@hannesb90

@hannesb90 hannesb90 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem and result

Step 1 of the proposal in #1994 (ADR 0012), toward #520. Core cannot tell
what a declared control does to heat, or which metric is a room's or a
tank's temperature, so nothing can reason about heat devices without knowing
them by name. This adds that declaration, and only that.

  • A control may declare asset (space_heat, dhw, cooling, buffer),
    strategy (curve_offset, setpoint, on_off, discrete_mode), zone,
    min_interval_s, and for on_off only, min_on_s and min_off_s. The
    vocabulary is the existing thermal package's; on_off is added there.
  • The lease, per the owner's answer 2 on docs(adr): propose heat control from a target temperature and a learned twin #1994. A control may declare
    device_hold_max_s: the device itself keeps a commanded value at most that
    long and then returns to its own regulation, so it falls back even if FTW
    stops. A number input may declare its default, the value the device runs
    at by itself, which "a small step from the device default" is measured
    from. A zero or non-numeric hold, or a default outside min..max or on a
    non-number input, drops the control.
  • A driver may list sensors = { { metric, asset, zone } }, naming metrics
    it already emits.
  • A string input may declare options. POST /api/drivers/{name}/control
    now refuses a value outside them with a 400, without sending it.
  • GET /api/drivers/{name} returns controls[].thermal,
    controls[].input.options and sensors, each omitted when not declared.

Scope and safety

Nothing plans for, schedules or sends a heat command because of these
fields. The operator control path is unchanged apart from the options check.

  • Misdeclaration drops the control, as a bad evidence kind already does.
    That covers a strategy that does not fit the input type, an unknown asset,
    a half declaration, or a timing that is negative, fractional, over a week
    or written as a string. A later planner should never trust a wrong
    strategy, or miss a compressor's minimum off time because it was a string.
    A malformed sensor is dropped on its own.
  • Existing drivers are unchanged. A control without these fields parses
    exactly as before, and its JSON has no thermal key. The bundled catalog
    test still passes.
  • One parser fix: nestedBlock matched a name anywhere, so
    temp_sensors = { would have been read as sensors, and my_controls as
    controls. It now requires the whole name.
  • No open PR touches these files. feat(web): the charts take the iOS app's palette #1859 touches web/heating.js; this PR
    does not.

Verification

Run on a Raspberry Pi 4 (linux/arm64), Go 1.26.10, against master at
7350a3e with the pinned drivers fetched:

go test ./internal/drivers ./internal/thermal -count=1      ok
go test ./internal/api -run 'DriverControl|DriverDetail|SendDriverDefault' -count=1   ok
go vet ./internal/drivers ./internal/thermal                clean

The full make verify did not run locally; CI covers it. No hardware is
involved, because no device behaviour changes.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.

🤖 Generated with Claude Code

A control may now declare asset, strategy, zone and write timings in the
thermal contract's vocabulary, and a driver may list the metrics that are
a heat store's temperature. /api/drivers/{name} exposes both. Nothing
plans for or sends a heat command because of them; this is the
declaration step of the proposal in srcfl#1994.

A string control may declare a closed set of options, which the control
API now enforces. A misdeclared thermal field drops the control, as an
unrenderable declaration already does. nestedBlock now requires the whole
field name, so temp_sensors is not read as sensors.

Refs srcfl#520

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
The owner's answer on srcfl#1994: planned control needs the device to fall
back on its own through a device-side timed hold; otherwise planned
values stay a small step from the device default. A control may now
declare device_hold_max_s, and a number input its default, so Core can
own that lease rule. A zero or non-numeric hold, or a default outside
min..max or on a non-number input, drops the control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
TestWatchedPathsCoverImports fails once drivers imports thermal, since a
change there would skip the rig.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant