Skip to content

fix(battery): stop scheduled batteries and the PID from double-correcting (oscillation), add SoC limits and reactive fallback - #8

Merged
bvweerd merged 1 commit into
devfrom
fix/scheduled-battery-feedforward
Jul 3, 2026
Merged

fix(battery): stop scheduled batteries and the PID from double-correcting (oscillation), add SoC limits and reactive fallback#8
bvweerd merged 1 commit into
devfrom
fix/scheduled-battery-feedforward

Conversation

@bvweerd

@bvweerd bvweerd commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Problem

The scheduled-battery correction (filtered × capacity share, summed over batteries = the full grid error) and the PID both corrected the same grid error every cycle. Combined loop gain ≈ 2 → the closed loop diverges. Simulated with an ideal battery, schedule = 0, initial import 1000 W:

c01 grid= -1000  sp= 60   bat_target= 1000
c02 grid= +2000  sp= 40   bat_target=-1000
c04 grid= +5000  sp= 20   bat_target=-3000
c06 grid= +8000  sp=  0   bat_target=-5000   ← sustained limit cycle
c07 grid= -7000  sp=100   bat_target=+5000   ← battery bang-bangs at full power, every 5 s

Two more issues in the scheduled path:

  • No SoC limits: the correction could push a full battery into charge or an empty one into discharge — SoC gating only existed in the reactive layer.
  • Silent degradation on optimizer failure: schedule_w = read_power_w(...) or 0.0 turned an unavailable/unknown/non-numeric schedule sensor into schedule 0, leaving the battery under pure P-on-grid control with no SoC protection and no notification.

Fix

  • The commanded-but-not-yet-measured scheduled battery response is fed forward into the PID residual (same 30 s trust-window mechanism as reactive batteries), so the PID only handles what the battery will not absorb. The oscillation scenario now converges within the deadband (closed-loop regression test included).
  • SoC limits bound the corrected target: no charging at/above max_soc, no discharging at/below min_soc.
  • Optimizer sensor unavailable → reactive fallback: the battery drops into the SoC-aware incremental reactive layer, with a warning logged once until the sensor recovers. The four existing fallback tests were updated to document the new (safer) behaviour.
  • Scheduled setpoint writes are skipped when unchanged (< 1 W), matching the reactive layer.

Known limitation (unchanged, by design): the proportional correction steers the grid toward zero, so a schedule that intentionally imports/exports (e.g. cheap night charging) is partially cancelled by the correction. Fixing that properly needs a grid-plan signal from the optimizer alongside the battery schedule — happy to pick that up separately if desired.

Tests

New: closed-loop no-oscillation regression, SoC max blocks scheduled charging, SoC min blocks scheduled discharge, unchanged target not rewritten. Full suite: 243 passed.

https://claude.ai/code/session_01RUWpwxbGsgR3PoLHLq4Djz


Generated by Claude Code

…ting

The scheduled-battery correction (filtered x capacity share, summed to
the full grid error) and the PID both corrected the same grid error
every cycle. With a combined loop gain of ~2 the closed loop diverged:
a 1000 W import escalated into a sustained full-power oscillation
(grid swinging between -7 kW and +8 kW, battery bang-banging between
full charge and full discharge every 5 s).

- The commanded-but-not-yet-measured scheduled battery response is now
  fed forward into the PID residual (same trust-window mechanism as
  reactive batteries), so the PID only handles what the battery will
  not absorb. Closed-loop regression test included.
- SoC limits now also bound the corrected schedule target: no charging
  at/above max_soc, no discharging at/below min_soc. Previously the
  correction could push a full battery into charge or an empty one
  into discharge (SoC was only enforced in the reactive layer).
- An unavailable/unknown/non-numeric schedule sensor no longer degrades
  silently to correction-only control without SoC protection; the
  battery falls back to the reactive layer (SoC-aware, incremental)
  and a warning is logged once until the sensor recovers.
- Scheduled setpoint writes are skipped when the target is unchanged
  (< 1 W), matching the reactive layer and removing per-cycle service
  call spam.

Known limitation (unchanged, by design): the proportional correction
steers the grid toward zero, so a schedule that intentionally imports
or exports (e.g. cheap night charging) is partially cancelled. Fixing
that needs a grid-plan signal from the optimizer alongside the battery
schedule.

https://claude.ai/code/session_01RUWpwxbGsgR3PoLHLq4Djz
@github-actions github-actions Bot added enhancement New feature or request bug Something isn't working labels Jul 3, 2026
@bvweerd
bvweerd merged commit 940ec5b into dev Jul 3, 2026
9 checks passed
@bvweerd
bvweerd deleted the fix/scheduled-battery-feedforward branch July 3, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant