Skip to content

feat!: add airing, and deliver commands by comparing desired and reported state - #7

Merged
subpathdev merged 4 commits into
mainfrom
feat/airing
Nov 1, 2026
Merged

subpathdev merged 4 commits into
mainfrom
feat/airing

Conversation

@subpathdev

@subpathdev subpathdev commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Why

The last acceptance criterion: "window open / airing, don't heat" from the phone. The independent architecture review recommended reshaping the controller first, because airing needs retries, timeouts and restore after a restart.

What

  • Contract (openapi.yaml): POST and DELETE /api/rooms/{id}/airing; Room gains targetState, airingState, airingUntil, lastSeen and commandFailed; RoomList gains connected. Breaking: POST .../target returns 202, and 502 is gone.
  • internal/controller: desired state separate from reported state, with room.decide choosing the command and Controller.Run the only place that publishes or saves. Commands repeat until a report confirms them.
  • Page: "Airing 15/30/60", "Heat again", and notes for held targets, unconfirmed commands, old reports and a lost broker.
  • Metrics: heating_control_room_airing{room} and heating_control_room_command_failed{room}.
  • Config: config.clientId is new.
  • No saved state (Jan's decision): everything is in memory. A restart during airing forgets it, so that room stays off until someone taps "Heat again", and a target waiting to be sent is lost.

Additions beyond the agreed design: targetState also has failed, there's a separate commandFailed field, and off and heat-again keep retrying every 5 minutes after the first 5 minutes rather than stopping, because stopping would mean heating with the window open or a cold room.

Review fixes are in: QoS 0 for commands, a restore or target only confirmed when that can't be a stale report, and DELETE .../airing also turning on a thermostat that is off with no airing.

How to validate

  • go test -race, golangci-lint and helm lint pass locally. Tests drive time by hand through airing, retries and knob flips.
  • Page checked at phone width with all reads and writes stubbed.
  • Read-only run against the real broker: rooms and state are right.
  • Not tested on a real TRV yet: off followed by restoring mode and target in one message, and the 30 s retry timing. That's the phone test after deployment.

Risk

A bug here could leave a room off or heat with a window open. The TRV's frost protection stays active as a floor, and "Heat again" works on any TRV that is off. The stuck-off alert comes in the next smart-home PR. Any pod restart during airing, including a release, leaves that room off until someone taps "Heat again". On a rollback to 0.1.0, which has no such button, the room has to be turned back on at the TRV or through MQTT.

Rollback

Set the chart back to 0.1.0 in smart-home. If a room is airing, first tap "Heat again".

🤖 Generated with Claude Code

…rted state

A room can now be aired for 15, 30 or 60 minutes: off is sent, and at the end
or on "Heat again" the room gets back its mode and target. End times and held
targets are saved to a file, so they survive a restart.

The controller now keeps what a room should be doing apart from what its
thermostat reports. Run is the only place that publishes or saves, and repeats a
command until a report confirms it. This replaces the send lock, the flush
goroutine and the rollback code, and closes the gap where a command could be
resent after a reconnect without still being wanted.

BREAKING CHANGE: POST /api/rooms/{id}/target returns 202 instead of 204, and 502
is gone. Whether a target arrived shows in targetState in the room list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
subpathdev and others added 3 commits November 1, 2026 15:52
- Commands use QoS 0, because paho resends QoS 1 messages after a reconnect and
  a late off can undo a restore. The previous commit wrongly said this gap was
  closed. The controller already repeats commands until they're confirmed.
- State is saved before a command goes out, so a crash can't leave a room off
  with no record.
- A report can't confirm a restore while one from before an unconfirmed off
  could still arrive, and only confirms a target that was sent.
- A corrupt state file is moved aside and reported through the health check.
- A thermostat stuck at the frost value no longer holds a target forever.
- "Heat again" also turns on a thermostat that is off with no record.
- The last on mode is saved, and two rooms can't share a thermostat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All state is now in memory: no state file, Store interface or PVC. A restart
during airing forgets it, so that room stays off until someone taps "Heat
again", and a target waiting to be sent is lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@subpathdev
subpathdev merged commit 805a786 into main Nov 1, 2026
5 checks passed
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