Skip to content

Per-driver power limits with observed & self-tune-learned maxima (master) #145

Description

@frahlg

Motivation

Today `MaxCommandW = 5000` is hard-coded in `go/internal/control/dispatch.go` and applied to every battery command regardless of hardware. Real batteries vary wildly:

  • Ferroamp EnergyHub can push 10-15 kW depending on configuration.
  • Sungrow hybrid inverters are typically 5-10 kW.
  • Pixii EssLi is 5-7 kW.

A 5 kW ceiling across the board means Ferroamp + Sungrow on a 16 A / 11 kW site loses 3-5 kW of headroom on charging slots where the operator would benefit. On bigger fuses (32 A / 22 kW) the loss is even larger.

Operator feedback (2026-04-20): "vi började med 5 kW för att börja försiktigt, men bättre om vi utgår från riktiga gränser nu."

Phased plan

Phase A — per-driver config (unblocks operators immediately)

  • `config.Driver` gains `max_charge_w` and `max_discharge_w` (both optional, 0 = fall through to `MaxCommandW = 5000` default for backcompat).
  • `control.batteryInfo` + `clampWithSoC` become per-battery aware: each battery clamps against its own limit, not a global constant.
  • Fuse-guard is made bidirectional: the charge path gets the same "site boundary must not exceed fuse" protection the discharge path already has. Operator feedback: "FuseGuard förstås alltid vara överordnad spelar ju ingen roll om det är discharge eller charge."
  • UI surfaces the limits in the Devices / Settings view.

Phase B — observed maxima (passive insight)

  • `control` or `telemetry` tracks a rolling `max_observed_charge_w` and `max_observed_discharge_w` per driver over a 30-day window.
  • Surfaced via `/api/devices` and `/api/battery_models` so the UI can show "you configured 5 kW, we've seen 6.2 kW actually flow".

Phase C — self-tune discovers limits (active insight)

  • Extend `selftune` step-response to probe max amplitude: ramp to 3 kW → 5 kW → 7 kW until the battery plateaus before reaching the command. That plateau is the observed ceiling.
  • Gated on safe conditions (good PV, high-ish SoC, grid stable). Runs at operator command, not automatically.
  • Writes a learned value to `state.db` per device.

Phase D — UI suggests raise, operator approves

  • Dashboard shows side-by-side: Configured | Observed | Self-tune measured. One-click to raise the configured cap toward the learned value.
  • Never auto-raises. Human-in-loop.

Safety invariants (all phases)

  • Site fuse-guard is the non-negotiable ceiling above any per-driver cap.
  • SoC clamps (don't discharge when < 5 %) stay.
  • Slew limits stay.
  • A per-driver cap above the fuse budget gets clamped down to the fuse per-dispatch-cycle — config can suggest 15 kW but if the fuse is 11 kW the dispatcher still respects 11 kW at the site boundary.

PR sequence

  1. `feat(control): per-driver power limits + bidirectional fuse-guard` (Phase A + safety invariant).
  2. `feat(telemetry): rolling observed max per battery` (Phase B).
  3. `feat(selftune): probe battery max power during step response` (Phase C).
  4. `feat(ui): suggest raising per-driver caps from observed/learned data` (Phase D).

Each PR links back here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions