fix(battery): stop scheduled batteries and the PID from double-correcting (oscillation), add SoC limits and reactive fallback - #8
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:Two more issues in the scheduled path:
schedule_w = read_power_w(...) or 0.0turned 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
max_soc, no discharging at/belowmin_soc.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