Add manual_soc_max: a discharge-direction ceiling sibling to manual_soc - #4841
Open
chalfontchubby wants to merge 2 commits into
Open
Add manual_soc_max: a discharge-direction ceiling sibling to manual_soc#4841chalfontchubby wants to merge 2 commits into
chalfontchubby wants to merge 2 commits into
Conversation
…nual_soc (#1578) manual_soc already lets an automation set a minimum SoC floor for a specific time - Predbat plans charging to reach it. There was no equivalent for the opposite direction: forcing the battery down to a ceiling by a deadline, which is what #1578 actually needs (a periodic calibration discharge finishing right before a known cheap import slot, e.g. an Octopus Intelligent Go midnight slot). manual_soc_max mirrors manual_soc's existing mechanism exactly, just as a ceiling: it merges into a new all_active_keep_max per-minute dict (fetch.py), and the optimiser treats being above the ceiling as a cost penalty priced at the export rate (prediction.py) - the same "keep" scoring approach already used for the floor, just inverted. Because it's a real cost signal integrated into the normal search rather than a fixed-duration forced export, the optimiser is free to use the energy against load or export depending on what's actually cheapest at each moment, which should make better use of the energy than a blunt forced discharge automation. A ceiling below the floor at the same minute is a contradiction (most likely a stale override left over from a previous automation run) - the floor wins as the safety-relevant constraint, and the conflicting ceiling is dropped with a warning rather than handing the optimiser two penalties pulling opposite ways. This touches the hot loop that has both a pure-Python and a C++ kernel implementation (prediction_kernel.cpp) for performance - mirrored the new ceiling logic there too and bumped both parity revisions per the kernel's own documented rule, so a stale prebuilt kernel binary falls back to the Python engine instead of silently disagreeing with it. Verified locally: rebuilt the kernel and the full parity suite passes, including a new randomized ceiling scenario added to its existing seeded sweep (constructed to consume no additional RNG draws, so the existing seeded scenarios are unaffected). Also covered: the web plan page's SOC dropdown gained a second "Maximum SOC" control (web.py/web_helper.py), plan-status markers mirroring the existing floor indicator (execute.py/output.py), and the export-window merge guard already applied to the floor is now applied to the ceiling too (plan.py) so two export windows can't be silently combined across a ceiling deadline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
select.predbat_manual_socalready lets an automation set a minimum SoC floor for a specific time - Predbat plans charging to reach it by the deadline. There was no equivalent for the opposite direction: forcing the battery down to a ceiling by a deadline, which is what #1578 actually needs - a periodic calibration discharge finishing right before a known cheap import slot (e.g. an Octopus Intelligent Go midnight slot), so the BMS can re-anchor its SoC estimate before recharging cheaply.Design discussion (including why the naive
select.predbat_manual_soc"just set a lower target" approach doesn't work - it's documented as a minimum, not an exact/maximum target) happened on the issue and in-session; this PR is the resulting implementation.Why this should give better results than the community's existing workaround (a fixed-duration forced-export automation, see #1578's thread): because the ceiling is a real cost signal integrated into the normal optimiser search rather than a blunt forced block, Predbat is free to use the discharged energy against load or export depending on whichever is actually cheapest at each moment, and stays coordinated with everything else already in the plan (car charging, existing charge/export windows). A fixed force-export block can't do either of those.
Design
manual_soc_max/manual_soc_max_valuemirrormanual_soc/manual_soc_valueexactly, just inverted:fetch.py: merges into a new per-minuteall_active_keep_maxdict (mirrorsall_active_keep), usingmin()across sources instead ofmax()since it's a ceiling.prediction.py: the optimiser treats being above the ceiling at a "kept" minute as a cost penalty priced at the export rate (mirrors the floor's penalty, which prices being below it at the import rate - see the "pretend the excess should have been exported" comment).Warn:log line rather than handing the optimiser two penalties pulling opposite ways.The C++ kernel
prediction.py's hot loop has a C++ mirror (prediction_kernel.cpp) for performance, with its own documented parity rule: any behavioural change must be mirrored there and bothPK_PARITY_REVISION/KERNEL_PARITY_REVISIONbumped, so a stale prebuilt binary is rejected at load time (falls back to the Python engine) rather than silently disagreeing with it. Done here - both bumped from 10 to 11.Verified locally:
build_kernel.sh) - compiles clean with-Wall -Werrortest_kernel_parity.py's existing seeded sweep - constructed to derive its values from the sweep's existing floor-keep draws rather than call freshrng.*()methods, so the existing seeded scenarios (including car-charging edge cases the suite depends on) are unaffected./run_all --test random) matches bit-for-bit against the committed baseline, confirming zero behavioural change when the feature is unusedAlso covered
execute.py/output.py: plan status markers mirroring the existing floor indicator ([Manual SoC Max], a ⬇ plan icon)plan.py: the export-window merge guard already applied to the floor (discard_unused_charge_slots) is now applied to the ceiling too (discard_unused_export_slots), so two export windows can't be silently combined across a ceiling deadlineweb.py/web_helper.py: the plan page's SOC dropdown gained a second "Maximum SOC" control, reusing the existinghandleSocOverrideJS function (parametrised with anisMaxflag rather than duplicated)docs/customisation.md: new section mirroring the existingmanual_socwriteupFixes #1578
Test plan
test_manual_soc_max.py: parsing, independence frommanual_soc,offclearing, and the floor/ceiling conflict resolutiontest_discard_unused_export_slots.pyfor the ceiling merge guardtest_kernel_parity.py's random sweep to exercise the ceiling (RNG-draw-neutral, see above)saving_session,manual_soc,discard_unused_charge_slots,discard_unused_export_slots,optimise_levels, and the 20-scenario baseline regression all pass unchanged./run_pre_commitpasses (black/ruff/cspell/markdownlint)🤖 Generated with Claude Code