Skip to content

feat(control): optional single-battery scope for manual hold#608

Draft
frahlg wants to merge 1 commit into
masterfrom
feat/single-battery-manual-hold
Draft

feat(control): optional single-battery scope for manual hold#608
frahlg wants to merge 1 commit into
masterfrom
feat/single-battery-manual-hold

Conversation

@frahlg

@frahlg frahlg commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Extend the existing battery manual_hold endpoint with an optional driver scope.
  • Bind a scoped hold to the driver's current FTW hardware ID in memory. Core cancels the hold if the name points to other hardware or the target becomes unsafe.
  • Send the requested power only to the chosen battery and stop online sibling batteries at 0 W.
  • Keep requests without driver on the existing pool path.
  • Add the battery scope and active scope to the current battery control UI.

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_hold reason.

API

POST /api/battery/manual_hold

{ "direction": "charge", "power_w": 5000, "hold_s": 900, "driver": "bat_a" }

Omit driver for the old pool behavior. The endpoint still limits hold_s to 1–1800 seconds. DELETE /api/battery/manual_hold still stops the hold.

Safety review

  • Hardware ID: Core resolves the same canonical device ID used by FTW state. It rejects a target without live identity and checks the driver name and device ID again before every dispatch.
  • Restart: Battery manual holds remain in memory only. A process restart starts with no hold, so old service state cannot return.
  • Expiry: The existing absolute expiry and 30-minute maximum stay in force.
  • Target health: The API rejects unknown, observe-only, offline, faulted, or unreadable battery targets. The control tick cancels an active scoped hold when health, battery telemetry, SoC, or hardware identity fails.
  • Site health: A stale site meter sends autonomous defaults and clears the hold before normal battery dispatch can run.
  • Driver default: This change does not alter driver default mode, Lua drivers, driver packages, or repository data.
  • Core limits: Per-driver power caps, SoC limits, slew, battery-boost reserve, and the site fuse guard remain after the hold request. The emergency fuse action remains last and can replace the requested hold.
  • Siblings: Online sibling batteries go to 0 W at once. This standdown may skip slew because it only removes charge or discharge; the fuse guard may still require discharge.
  • Planner: The hold remains a core operator action. It works without MPC and never sends planner output to hardware.

Automated tests

  • go test ./internal/control -run BatteryManualHold -count=1
  • go test ./internal/api -run BatteryHold -count=1
  • go test ./cmd/ftw -run 'Battery|DispatchBlock|SiteDispatchFreshness|StaleSiteDefault' -count=1
  • npm test — 67 passed
  • make verify after rebase
  • Pre-push make verify-all, including Linux ARM64, Linux AMD64, and Windows AMD64 builds

The 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

  • On a two-battery site, start a scoped charge from battery A and confirm battery B moves to 0 W.
  • Repeat with scoped discharge and idle.
  • Start a pool hold and confirm the old pool split still works.
  • Let a short hold expire, then use Stop on another hold; confirm normal control returns in both cases.
  • Restart Core during a scoped hold; confirm the hold stays inactive after start.
  • Stop target telemetry long enough to trip the watchdog; confirm autonomous default and inactive hold.
  • Mark the target offline or faulted; confirm the hold cancels and does not return after recovery.
  • Stop or stale the site meter; confirm all controllable drivers get defaults and the hold clears.
  • Rebind the same driver name to other hardware; confirm the old hold cannot command it.
  • Reach local battery SoC min/max and configured power caps; confirm Core clamps the request.
  • Exercise a near-fuse-limit site; confirm the fuse guard can lower or replace the requested target.
  • Start battery boost, then start a battery hold; confirm boost stops with battery_hold.
  • Run without MPC enabled; confirm scoped hold, expiry, stop, and safety cancellation still work.
  • Confirm an observe-only battery has no hold action and the API rejects it.
  • Confirm a driver planet opens that battery scope and the merged battery planet opens pool scope.

Keep this PR in draft until the field checks pass.

@frahlg
frahlg force-pushed the feat/single-battery-manual-hold branch from a315acd to 9480208 Compare July 22, 2026 05:28
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.

feat(control): optional single-battery scope for manual hold

1 participant