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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions