feat(control): optional single-battery scope for manual hold - #608
Conversation
9e95e08 to
a315acd
Compare
a315acd to
9480208
Compare
|
Rebased onto current One real conflict, in the control tick.
Still a draft, and I have deliberately left it that way. It changes the control path, it has no review, and the safety-relevant question is a judgement call rather than a mechanical one: a scoped hold binds to the driver's hardware ID and stops online sibling batteries at 0 W, so the review that matters is whether cancelling on hardware-identity mismatch covers every way the name can move. That is worth a proper read rather than a green tick. |
55d0d2f to
8f19c7a
Compare
|
Rebased onto current The control-tick resolution is carried forward unchanged. Two new conflicts, both the same shape. #761 added a #791 needed no resolution. Golden corpus: no records moved. All 435 records across the seven files replay identically — Verified on Still a draft, and still deliberately so. The rebase does not change the judgement call that needs a human read: a scoped hold binds to the driver's hardware ID and stops online sibling batteries at 0 W, and the question is whether cancelling on hardware-identity mismatch covers every way the name can move. The field checklist is also still unticked. |
8f19c7a to
bc4ff32
Compare
Rebased onto master. Three resolutions are carried by this commit. The control tick in go/cmd/ftw/main.go: master added per-tick telemetry persistence to the blocked-dispatch branch while this branch added the manual-hold release. Both belong there. The release runs first because persistTelemetryTick snapshots ctrl, so the stored tick has to show the hold already cleared rather than one the blocked tick never executed. Two struct conflicts, both the same shape: master's ConfigApplier landed where this branch adds BatteryIdentity, in the api.Deps type and in main.go's composite literal. Both fields are kept. The golden-clock seam in go/internal/control/dispatch.go: #801 made state.now() the only wall-clock read in the package, and it rewrote the same line this branch extends. The manual-hold read keeps master's state.now(), and the scoped target-validity gate is appended after it. dispatch.go holds no ambient clock read outside now() itself. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
bc4ff32 to
cfbe525
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cfbe525ef2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (clicked.observe_only) return; | ||
| var bc = document.getElementById("battery-control"); | ||
| if (bc && typeof bc.open === "function") bc.open(); | ||
| if (bc && typeof bc.open === "function") bc.open(d.name || d.id || ""); |
There was a problem hiding this comment.
Open aggregate battery planets at pool scope
When multiple batteries are aggregated, aggregateGroups gives the merged planet a display name such as 2× and an ID such as agg-top-right. Passing d.name here therefore opens a driver-scoped form for the nonexistent driver 2×; installing the hold then receives unknown controllable battery driver instead of creating the advertised pool hold. Detect the aggregated planet and call open("") for it.
Useful? React with 👍 / 👎.
| health := tel.DriverHealth(name) | ||
| reading := tel.Get(name, telemetry.DerBattery) | ||
| return health != nil && health.IsOnline() && reading != nil && reading.SoC != nil |
There was a problem hiding this comment.
Cancel holds when the target leaves controllable inventory
If a hot reload marks the target observe_only, classifies it as a loadpoint, or otherwise removes it from driverCapacitiesFrom(..., true), this validator can still return true because the same healthy driver and identity remain live. ComputeDispatch then omits the target from onlineBats while the scoped hold stays active, causing every remaining controllable sibling to be commanded to 0 W until expiry. Revalidate membership in the current controllable-capacity map and clear the hold when it disappears.
Useful? React with 👍 / 👎.
Summary
manual_holdendpoint with an optionaldriverscope.driveron the existing pool path.Closes #607.
Relation to battery boost
This is not a battery-boost lease. Battery boost grants one loadpoint a short right to use the home battery pool and keeps the state needed for safe restart. This change adds a short service hold for one home battery and stores it only in memory. The two features share the core safety path. An active battery manual hold stops battery boost with the existing
battery_holdreason.API
POST /api/battery/manual_hold{ "direction": "charge", "power_w": 5000, "hold_s": 900, "driver": "bat_a" }Omit
driverfor the old pool behavior. The endpoint still limitshold_sto 1–1800 seconds.DELETE /api/battery/manual_holdstill stops the hold.Safety review
Automated tests
go test ./internal/control -run BatteryManualHold -count=1go test ./internal/api -run BatteryHold -count=1go test ./cmd/ftw -run 'Battery|DispatchBlock|SiteDispatchFreshness|StaleSiteDefault' -count=1npm test— 67 passedmake verifyafter rebasemake verify-all, including Linux ARM64, Linux AMD64, and Windows AMD64 buildsThe Go tests cover target scope, immediate sibling standdown with slew enabled, missing and changed hardware identity, unavailable targets, core power and reserve limits, and stale-site cancellation.
Field checklist
battery_hold.Keep this PR in draft until the field checks pass.