fix(car-charging): don't let an empty exclusive plan block later cars - #4364
Merged
springfall2008 merged 3 commits intoJul 29, 2026
Conversation
In a shared-charger multi-car setup, car_charging_planned/now can read true for every car profile that shares the same physical sensor (e.g. two car entries both pointed at one Zappi's status entity), even when only one is actually plugged in. The exclusive-charging break in fetch_sensor_data_car_planning() fired on car_charging_planned + car_charging_exclusive alone, so a car with no real charging need (SoC already at its own limit, so plan_car_charging() correctly produced an empty plan) could still stop the loop before a later car - the one genuinely plugged in and charging - ever got planned at all. Confirmed against the reporter's own debug.yaml: octopus_intelligent_charging was off (not on, as originally assumed), car_charging_planned/now read [true, true] for both cars, and car_charging_slots ended up [[], []] even though the second car had a real ~4% SoC/limit gap. Only lets the exclusive break fire once a car has actually produced a non-empty plan. Fixes springfall2008#4305.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a multi-car shared-charger edge case where an “exclusive charging” early-exit in fetch_sensor_data_car_planning() could prevent later car profiles from being planned when an earlier car reports planned=True via a shared sensor but produces an empty plan (e.g., already at its limit). It also adds a regression test based on the reported scenario in #4305.
Changes:
- Refines the exclusive-charging break condition in
fetch_sensor_data_car_planning()to only trigger once an actual non-empty charging plan exists. - Adds a regression test to ensure a later car still gets planned in shared-sensor + exclusive setups.
- Registers the new regression test in the multi-car IOG test suite.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/predbat/fetch.py | Adjusts exclusive-charging loop break logic during car planning to avoid skipping later cars when the current car produced no plan. |
| apps/predbat/tests/test_multi_car_iog.py | Adds and wires a regression test for the shared-charger exclusive-planning scenario from issue #4305. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
low_rates windows were absolute minute offsets (0-150), but plan_car_charging() skips windows before minutes_now - once the test suite runs later than ~02:30, all three windows would be stale. Car 1's outcome was actually still deterministic because car_charging_now=True unconditionally injects an extra "charging now" slot, but making the windows relative to minutes_now removes the ambiguity entirely (Copilot review finding on springfall2008#4364).
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
Reported in #4305 - a shared-charger, 2-car setup (both car profiles pointed at the same Zappi's status sensor) showed zero
Car kWhfor an actively-charging session, despite every individual signal (plugged in, dispatch active, real session energy accumulating, genuine SoC/limit gap) confirming the car really was charging and really did need more.Root cause, confirmed against the reporter's own debug.yaml: in a shared-charger setup,
car_charging_planned/car_charging_nowcan readtruefor every car profile sharing the same physical sensor, not just the one actually plugged in.fetch_sensor_data_car_planning()'s exclusive-charging break (fetch.py:1141) fired oncar_charging_planned[car_n] and car_charging_exclusive[car_n]alone - so car 0 (not actually plugged in, but readingplanned=Trueoff the shared sensor) hit the break and stopped the loop before car 1 (the car genuinely charging) was ever considered. Car 0's own charging need happened to be zero (SoC already at its own limit), soplan_car_charging()correctly returned nothing for it - but the loop still treated it as "the" exclusive session and bailed out, silently eating car 1's turn.car_charging_slotsended up[[], []].Fix: only let the exclusive break fire once a car has actually produced a non-empty plan:
Behaviour is unchanged for the normal case (a car with a genuine charging need and
exclusive=Truestill breaks immediately, same as before) - the only change is that a car withplanned=Truebut an empty resulting plan no longer blocks later cars from getting their turn.(A fix based on checking a
car_charging_plugged_inentity was considered first, but that concept doesn't actually exist anywhere in Predbat's config today - would have meant introducing a whole new per-car entity rather than using data already computed a few lines above.)Test plan
run_multi_car_shared_charger_exclusive_testintest_multi_car_iog.py, modelled on the setup from the reporter's debug.yaml (car 0:planned=True,exclusive=True, SoC already at its own limit; car 1: genuine SoC/limit gap) - asserts car 1 still gets plannedfetch.pylocally, confirmed the test catches it, then restored)./run_all --test multi_car_iog- all passpre-commit(ruff, black, cspell) run against the changed files, passedFixes #4305
🤖 Generated with Claude Code