[#101] Add phase protection logic + offline test suites - #147
Open
nickles-lee wants to merge 20 commits into
Open
[#101] Add phase protection logic + offline test suites#147nickles-lee wants to merge 20 commits into
nickles-lee wants to merge 20 commits into
Conversation
added 15 commits
June 3, 2026 12:42
…ies' into 101-phase-protection-peak-shaving
… & command_limits_available handling
added 5 commits
August 14, 2026 16:48
main re-exported both `01 start-flow.json` and `all-flows-in-one-file.json`
from Node-RED with entirely new node IDs (only 3 of 620 base IDs survive in
the combined file), so the textual conflicts were not meaningful. Both flow
files were resolved by taking main's export as the new baseline and
re-applying this branch's changes against the remapped IDs:
- 6 new nodes: Phase template, Phase, Max phase power,
Normalize phase limit, Phase grid power, Normalize phase power
- "Mapping": normalize + emit battery.phase
- "Loop end": clean up msg.battery_phase / msg.template
- "Energy max" -> Phase template (phase lookup ahead of Mapping)
- "Export limit" -> Max phase power (phase chain ahead of Battery Strategy)
- group "Peak shave settings": +4 members, widened to 1642
Verified main made no semantic change to any node this branch touches, so
the remap is the whole conflict.
house_battery_control.yaml: kept the new phase-assignment binary sensor and
accepted main's removal of the dynamic-v1 "Is below threshold cheapest
period tariff" sensor (nothing references it anymore).
.gitignore: union of both sides, de-duplicated .DS_Store.
contribute/check.ps1 passes.
Brings in the merged 102 branch, which already carries main v4.15.0.
As with 102, main re-exported `01 start-flow.json` and
`all-flows-in-one-file.json` with entirely new node IDs, so both were
resolved by re-applying this branch's changes onto main's export with the
IDs remapped (mappings verified unique by type+name, tab-scoped for the
combined file, groups disambiguated by coordinates and membership):
- 3 new nodes: Phase protection?, Phase protection guard,
Phase command limits
- "Strategy selection" keeps main's new EV-stop-trigger body and is
rewired through the phase protection guard
- "Normalize phase power" now feeds the phase protection chain
- Mapping / Set Batteries / Update battery order / Custom logger bodies
- groups "Strategy selection" (+guard) and "Peak shave settings"
(+2 members, widened to 2106)
The one genuine semantic conflict was "Mode selection" in strategy-partials:
main added peak-shave direction/target latching (so the battery's own
response flipping P1 import/export cannot flip an import shave into an
export shave mid-timeout) while this branch rewrote the same function for
per-phase protection. Hand-merged so both apply: 101's phase-aware
selectedDirection/selectedTarget now feed main's latched
peak_shave_direction / peak_shave_power_limit flow state.
strategy-sell.json auto-merged despite main's re-export; verified the
function body landed on the correct node and main had not changed it.
Validated: contribute/check.ps1 passes, all 60 tests pass, and the merged
"Mode selection" was exercised directly for both latching and per-phase
behaviour.
Known pre-existing issue (not introduced here): on this branch the
"Max power solution" body in `02 strategy-charge.json` / `02 strategy-sell.json`
is newer than the copy in all-flows-in-one-file.json — commit 852f1a1's
command_limits_available fix was never re-exported into the combined file.
Both sides were carried forward unchanged; the combined file still needs a
re-export.
"Phase template" and "Phase" carried g="Loop" (the battery information loop) but were never listed in that group's nodes[], and sat outside its bounds. Node-RED drives the runtime from the wires, so this was editor-only: the nodes did not move or collapse with the group. Add both to the group and widen it from 1612 to 1942, keeping the group's existing 96px right padding. The parent "Get batteries information" group grows 1678 -> 2008 so it still contains the nested group with its original margin; no other group on the tab is overlapped. Applied to both 01 start-flow.json and all-flows-in-one-file.json.
Commit 852f1a1 gated the phase charge/discharge limits on `phase_protection.command_limits_available` in the individual strategy flows, but all-flows-in-one-file.json was never re-exported, so it still carried the older ungated bodies for Strategy Charge and Strategy Sell. Since docs/08-how-to-update.md tells users to import the combined file, users following the documented path were getting the pre-fix logic: the phase limits were read even when the upstream node had reported them unavailable. Copy both bodies verbatim from `02 strategy-charge.json` and `02 strategy-sell.json`. Every individual flow file's function bodies now match the combined file's copies; 60/60 tests pass and check.ps1 passes.
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
Closes #101.
This PR adds optional per-phase peak-shaving protection on top of the existing aggregate import/export peak-shaving behavior.
When enabled and usable phase data is available, the controller can react when an individual phase exceeds the configured per-phase power limit, even if the aggregate grid value still looks safe.
What changed
grid_power_phase.L1/L2/L3.Standby / peak shave.Stacked PR note
This PR is stacked on top of #139.
#139 contains the phase assignment / phase exposure groundwork. This PR intentionally focuses only on the #101 phase-protection logic and offline test coverage. Please avoid reviewing #102-scoped changes here; review this PR as the delta from the head of
102-phase-assignments-for-batteriesto the head of101-phase-protection-peak-shaving.Suggested review compare:
Range-diff from the #139 stack branch to this branch:
Compatibility
Existing behavior remains unchanged when phase protection is disabled, phase sensors are unavailable, or no batteries are assigned to phases.
Per-phase protection only acts when all of the following are true:
Full stopis intentionally not preempted. Full stop should mean that the battery is fully in standby / offlined.Testing
Offline Node-RED flow test coverage was added for phase-protection behavior.
Validated locally on this branch:
Also verified that the phase-protection tests fail when run against
main, where the phase-protection logic does not exist:This confirms the tests are not simply passing vacuously on branches without the feature.
Regression checks were also run against earlier draft commits in this branch to make sure the test suite catches prior unwanted behavior, including missing guard logic, unfair first-fit allocation, missing priority preservation, and missing allocator helpers.
Real-world testing / safety notes
This is an experimental control feature. It does not come with any warranty, guarantee, or promise that your phase fuse will not blow. If you're worried about this, set a wider safety margin on the per-phase limit to account for any spikes that will take some time for the batteries to respond to.
This logic is software-based peak shaving, not a replacement for correct electrical design, breaker sizing, phase-aware charger limiting, inverter limits, or professional installation checks.
If you test this branch on a real installation:
This improves the safety of your system & lets you run "hotter", but it does not make the system safe in the way a 25A circuit breaker on each phase would.
Real-World Testing
I have been running this branch in my house for around 1 to 2 weeks without incident, with no PV + heavier loading on Phase 3. If I ran the Charge strategy with the power limits I now have set, one of the main fuses would have cooked.
Some graphs:
Approx. 1 day view:

A more zoomed-in view of a period where phase 3 is running at full load:

Start of a brief overload to 32A, caused by an appliance switching on while the batteries had Phase 3 fully loaded:

End of the overload, 10s later

Note that I've validated phase protection during the time period where the batteries are charging. I haven't tested phase protection when the batteries are on standby / not actively charging.