Skip to content

feat(config): add num_chargers so a repeated charger is caught rather than silently summed - #4880

Closed
chalfontchubby wants to merge 7 commits into
mainfrom
feat/num-chargers-validation
Closed

chalfontchubby wants to merge 7 commits into
mainfrom
feat/num-chargers-validation

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Written by Claude, posted on behalf of @chalfontchubby.

Fixes #4879

car_charging_power is the one car_charging_* key that is not one entry per car: it lists chargers, and update_car_charging_power() sums them. It sits among five keys that are indexed by num_cars, so a household with two cars sharing one charger naturally lists that charger twice — and Predbat reports double the power it is drawing. The reporter's diagram showed 14140 W against a Zappi reading 7.1 kW.

Why not just detect it

Deduplicating identical entity ids catches the literal [zappi, zappi] case, but not this, which is the shape a careful user is more likely to produce:

car_charging_power:
  - sensor.zappi_id_buzz_charging_power   # template reading the same Zappi
  - sensor.zappi_kia_ev6_charging_power   # template reading the same Zappi

Two distinct entities, one charger. We cannot see through a template sensor to its source, and there is no invariant to check the sum against — car_energy_reported_load exists precisely because the charger may sit outside the house CT clamp. A check that caught one shape and stayed silent on the other would be worse than none: the silence would read as "validated".

So ask instead

num_chargers names the concept Predbat was missing entirely — it models cars and has no notion of chargers at all, which is the root of the confusion. The existing entries validation then compares it against the length of car_charging_power, catching both shapes because it stops trying to infer something invisible.

  • Unset means no check, so no existing apps.yaml changes behaviour.
  • The shipped template sets num_chargers: 1 — the common case, and the one that gets this wrong.
  • Fewer entries than declared is warned but not an error — a charger with no live power sensor is a real, documented setup, so a short list must not force a false count; but an entry deleted by accident under-reports just as quietly as a repeated one over-reports, so it does not pass in silence either. The warning records no arg_error, so a legitimate setup is never left showing "apps.yaml has N errors".

Two supporting changes worth a reviewer's eye

1. entries validation only ever rejected too FEW entries. Extra entries have always been tolerated, and newly rejecting them everywhere would fail working installs. But for a list that is summed rather than indexed, an extra entry inflates the total instead of being ignored. Hence a new entries_exact flag, set only on this key, so every existing key keeps its current behaviour.

2. A latent hole in the item type check. It trims a list to required_entries before validating the items:

if required_entries is not None and len(value) > required_entries:
    value = value[:required_entries]

With a count key that is simply unset, required_entries is 0, so the list was trimmed to nothing and no items were type-checked at all. That silently disabled the existing "a nonsense charger sensor is rejected" behaviour the moment this key gained an entries rule — caught by an existing test, which is how I found it. Guarded with if required_entries instead. The same hole applied to every entries key whenever its count was 0.

Docs

docs/car-charging.md said only the positive case ("one per line, if you have more than one charger"). It now says the negative out loud: this list describes chargers not cars, num_cars has no bearing on it, list each charger once however many cars you have, and two cars sharing a charger is a single entry. Plus the self-check — compare predbat.car_charging_power against the charger's own app; if it reads double, an entry is repeated.

Testing

Four new cases in test_web_power_flow: a duplicated charger against num_chargers: 1 is an error; the same list validates at num_chargers: 2; fewer entries than declared warns without recording an arg_error; and a nonsense sensor is still type-checked with num_chargers unset (the truncation regression above).

./run_all --quick and ./run_all --test debug_cases both pass.

chalfontchubby and others added 2 commits August 31, 2026 12:12
…mmed

car_charging_power is the one car_charging_* key that is not one entry per car:
it lists chargers and update_car_charging_power() sums them. Sitting among five
keys that ARE indexed by num_cars, it reads as per-car, so a household with two
cars sharing one charger naturally lists that charger twice - and Predbat then
reports double the power the charger is drawing (#4879).

We cannot detect this. Deduplicating identical entity ids catches the literal
case but not two template sensors that both read the same charger, and there is
no invariant to check the sum against - car_energy_reported_load exists because
the charger may sit outside the house CT clamp. A check that caught one shape
and stayed silent on the other would be worse than none, since the silence would
read as validated.

So ask instead. num_chargers names the concept Predbat was missing entirely -
it models cars and has no notion of chargers - and lets the existing entries
validation compare it against the length of car_charging_power. Unset means no
check, so no existing apps.yaml changes behaviour; the shipped template sets 1,
which is the common case and the one that gets this wrong.

Two supporting changes:

entries validation only ever rejected too FEW entries. Extra entries have always
been tolerated, and newly rejecting them everywhere would fail working installs -
but for a list that is summed rather than indexed an extra entry inflates the
total instead of being ignored. Hence entries_exact, set only on this key.

The item type check trims a list to required_entries before validating it, so a
count key that is simply unset (0) trimmed the list to nothing and type-checked
no items at all - which silently disabled the existing "a nonsense charger sensor
is rejected" behaviour the moment this key gained an entries rule. Guarded with
"if required_entries" rather than "is not None"; the same latent hole applied to
every entries key whenever its count was 0.

Docs say the missing part out loud: this list describes chargers, not cars, list
each one once however many cars you have, and two cars sharing a charger is a
single entry.

Fixes #4879

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tolerating a short list silently made num_chargers mean "an upper bound" rather
than "how many chargers I have". A charger with no live power sensor is a real
and documented setup, so a short list must not force a false count - but an
entry deleted by accident under-reports the Car figure just as quietly as a
repeated one over-reports it, which is the whole problem this key exists to stop.

Log it without recording an arg_error, so a legitimate setup is not left showing
"apps.yaml has N errors" forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

validate_config() still misses warning-on-short-count when optional_entries is used with a scalar (non-list) value, and the integer-type validation chain contains unreachable duplicated logic that should be addressed for correctness/maintainability.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces a new configuration concept, num_chargers, to prevent EV charging power from being accidentally double-counted when car_charging_power (a summed list) contains repeated entries in multi-car / shared-charger setups.

Changes:

  • Add num_chargers and apply it to car_charging_power via entries validation, with a new entries_exact flag to reject extra entries for this specific summed list.
  • Fix validate_config() list-truncation logic so entries rules with a count of 0 don’t silently skip per-item type validation.
  • Update docs and add unit tests covering duplicated entries, exact matching, short-list warnings, and type-checking when num_chargers is unset.
File summaries
File Description
docs/car-charging.md Clarifies that car_charging_power describes chargers (summed), not cars, and documents num_chargers.
docs/apps-yaml.md Adds num_chargers and notes car_charging_power is charger-based (not per-car).
apps/predbat/tests/test_web_power_flow.py Adds regression tests for duplicated charger entries, exact-count validation, short-list warnings, and type-checking.
apps/predbat/predbat.py Extends validate_config() with entries_exact, short-list warning behavior, and fixes the “count=0 truncates away validation” hole.
apps/predbat/config/apps.yaml Updates the shipped template to include num_chargers.
apps/predbat/config.py Adds num_chargers schema entry and applies entries_exact/optional_entries rules to car_charging_power.
Review details

Suppressed comments (1)

apps/predbat/predbat.py:1485

  • When optional_entries is true and required_entries > 1, a non-list value (single sensor) currently passes with no warning. That contradicts the intent of optional_entries here (warn-but-not-error when fewer entries are provided) and can let an under-specified config go by silently (e.g., num_chargers=2 with a single car_charging_power sensor). Consider emitting the same warning used for short lists in this branch.
                    elif required_entries > 1:
                        if not optional_entries:
                            self.log("Warn: Validation of apps.yaml found configuration item '{}' is not a list, but requires {} entries based on {}".format(name, required_entries, entries))
                            self.arg_errors[name] = "Invalid type, expected list"
                            errors += 1
                            continue
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/predbat.py Outdated
Comment thread apps/predbat/predbat.py
chalfontchubby and others added 3 commits September 5, 2026 13:45
…lowed-list check

Addresses two Copilot review findings on #4880.

validate_config()'s optional_entries handling warned when a list was
shorter than required_entries, but silently accepted a bare scalar the
same way even though that is exactly as short (a de-facto list of 1) -
e.g. num_chargers=2 with a single car_charging_power sensor passed
without the "lists 1 of the 2 declared" warning a too-short list gets.
Now warns the same way.

Separately, the integer/integer_list branch of the expected_types elif
chain was declared twice. Since the outer loop only runs the first
elif whose condition matches, the second occurrence - the only one that
checked spec's "allowed" list - was unreachable dead code, so an
integer value outside a key's allowed set (e.g. threads: 21, only
"auto" or 0-20 are declared) passed validation silently. Merged the
allowed-list check into the one reachable block and deleted the dead
duplicate.

Tests: a scalar-vs-num_chargers case alongside the existing short-list
one in test_web_power_flow.py, and a threads=21 case in
test_validate_config.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…line

Third Copilot review finding on #4880: entries_exact's log line says
"expected exactly N" but the recorded arg_errors message just said
"expected N", losing the detail that this is an exact-count check
rather than an at-most one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved validation, default-configuration, and warning-scope issues remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

apps/predbat/config.py:2633

  • num_chargers currently accepts negative integers: the integer validator only checks type and the optional zero rule, so num_chargers: -1 passes. The count logic then treats it like an unset value because all enforcement is guarded by required_entries > 0, allowing a repeated car_charging_power list to pass without the new check. Since this is a physical count and zero is the documented no-charger value, values below zero should be rejected.
    "num_chargers": {"type": "integer", "zero": True},

apps/predbat/predbat.py:1522

  • This warning is in the generic optional_entries path, so it also changes existing valid configurations for octopus_intelligent_slot, octopus_ready_time, and octopus_charge_limit (the other optional_entries specs in apps/predbat/config.py:2738-2740) to emit a Warn on every validation when a shorter list is intentional. Gate this diagnostic on entries_exact (or add a dedicated warning flag) so the new warning is limited to car_charging_power.
                            if required_entries:
                                self.log("Warn: Validation of apps.yaml found configuration item '{}' lists {} of the {} declared by {} - only the entries listed are used".format(name, len(value), required_entries, entries))
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread apps/predbat/config/apps.yaml
Comment thread apps/predbat/predbat.py Outdated
…ejects entries (Copilot review on #4880)

Two findings from Copilot's review of the num_chargers/entries_exact validation
added for #4879, both confirmed and fixed.

Auto-discovered multi-charger setups tripped entries_exact spuriously
------------------------------------------------------------------------
apps.yaml ships num_chargers: 1 by default, but myenergi, GECloud, and
AlphaESS auto-discovery all populate car_charging_power with one entry per
discovered charger without ever touching num_chargers. A genuine two-charger
site therefore had car_charging_power's entries_exact check see 2 > 1 and
report a configuration error on a correct, working setup. Each auto-discovery
path now calls set_arg_auto("num_chargers", <discovered count>) alongside its
existing car_charging_power auto-discovery, consistent with how num_cars is
already raised by GECloud's EV charger discovery.

An explicit num_chargers: 0 was indistinguishable from the key being unset
------------------------------------------------------------------------------
validate_config() derives required_entries via get_arg(entries, 0,
indirect=False), so a count key that is simply absent defaults to the same 0
an explicit "zero": True value produces. The entries_exact over-count check
guarded against the unset case with "required_entries > 0", which
inadvertently also let an explicitly configured num_chargers: 0 skip the
check entirely - a real "I have zero chargers" declaration validated
successfully against any non-empty car_charging_power list instead of being
rejected. Replaced the ambiguous required_entries > 0 guard with an explicit
entries_key_present check (entries in self.args), computed once alongside
required_entries.

Both verified with mutation testing: reverting each fix in turn reproduces
exactly the failure its regression test describes, and only that test fails.

./run_all --quick and ./run_pre_commit both green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Scalar charger values bypass explicit zero-count validation, and the multi-charger diagnostic documentation needs clarification.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

docs/car-charging.md:116

  • For a household with multiple physical chargers, predbat.car_charging_power is the aggregate, so comparing it with one charger's app can legitimately look doubled when both chargers are active. Please tell users to compare against the sum of all charger readings (or limit the single-charger/double-reading diagnostic to the one-charger case).
To confirm what Predbat is reading, compare the **predbat.car_charging_power** sensor against your charger's own
app or display while a car is charging - if it reads double, an entry is repeated.
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread apps/predbat/predbat.py Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mgazza

mgazza commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Came here from #4928 to work out how the two should fit together, since they touch the same symptom. Two things worth raising — the first is independent of my PR.

num_chargers is written per-component, so it ends up a max rather than a sum

alphaess.py, gecloud.py and myenergi.py each call set_arg_auto("num_chargers", len(<own list>)), and set_arg_auto defaults to overwrite=True (component_base.py:103). On a site with a Zappi and a GivEnergy EVC, both write the key and the last one to run wins, so num_chargers is 1 with two chargers present.

It doesn't misbehave on main today, because car_charging_power is clobbered by exactly the same mechanism — each component overwrites both keys together, so the count always matches whichever list survived. The count is right for the wrong reason, and only while the list is being overwritten rather than composed.

The new regression tests each drive one component, so this sits just outside their reach.

The entries_exact check and a composed car_charging_power

#4928 replaces that last-writer-wins clobber with a registry that composes car_charging_power across every source. That interacts badly with the check here, and the ordering is against us: auto_config() (predbat.py:1965) → components.initialize(phase=1) (1969, where the composed list is written) → validate_config() (1981). Validation sees the composed list and the un-composed count.

Zappi + GivEnergy EVC, stock apps.yaml:

  • registry writes car_charging_power with 2 entries
  • myenergi sets num_chargers=1; gecloud overwrites with 1
  • len(value) > required_entries, entries_key_present is true because the template now ships num_chargers: 1 uncommented, entries_exact is set → arg_errors["car_charging_power"] = "Too many entries, expected exactly 1"

So a correct two-charger install reports an apps.yaml error. That's the setup #4928 exists to make work, so I'd rather sort it out between us than have it land as a surprise.

There's also a rebase detail: #4928 removes the zappi_power_entities / power_entities locals that len(...) reads here, so the count needs another source either way.

What I think the shape is

Derive the count where the chargers are actually known, and keep num_chargers as the declared value it is in the title — user-written, never set by a component, checked against what discovery found. A mismatch is then exactly the thing worth telling someone about, and it can't be manufactured by two components disagreeing.

Worth noting that once the count is derived, entries_exact can't fire for an auto-discovered setup — the list and the count come from the same place. Its remaining value is hand-written car_charging_power, which is precisely #4879's setup: that reporter had duplicated every car entry by hand (on AI advice, per the thread), and the issue was closed by @gcoan's docs clarification in cbccb1c7 rather than by code. That's a narrower case than the current check covers, but it's a real one and I don't think it disappears.

Happy to be told I've read the intent wrong. If this direction is agreeable I'm glad to do the derivation work on my side so it isn't extra on yours, in whichever order the two land.

@gcoan

gcoan commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

@mgazza I think the proposal you make, to allow the auto discover to run, and then compare that to what the user declared in num_chargers, highlighting any discrepancies makes sense, but only if the EV chargers are auto-discoverable.

There are plenty of EV chargers that cannot be auto discovered so trying to build too much logic into the auto-discovery seems the wrong way to go.

e.g. GivEVC but using GivTCP, no auto discovery

Zappi charger, locally controlled and GivEVC controlled by GE Cloud, so auto discovery finds only 1.

I'm wondering if this isn't solvable in code?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

This is one of those seemingly trivial patches that has been gaming around for weeks, so I'm rusty on context, and I've not looked much at auto discovery...

Is the risk that auto discovery overwrites num_chargers (to 1) each time, where it should be appending to a register etc...
but that fails it's the only mechanism and auto discovery doesn't discover the charger... (not run, not discoverable etc)

Is a direction to not set num chargers explicitly, but to manually register chargers from apps.yaml via the same mechanism that discovered chargers do, perhaps with checks for duplication where possible?
No explicit list, just a sequence of manual charger declarations?

(Hmm. If some chargers are inside the ct clamp, others outside? Do we cover that?)

@gcoan

gcoan commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

As I understand it ...

The suggestion was to introduce a new num_chargers concept to Predbat so that Predbat could better auto-detect charger configuration issues.

At the moment Predbat doesn't have a num_chargers concept, only num_cars.

car_charging_power should be defined by charger, not by car, and the OP in #4879 mis-configured car_charging_power, entering the car charging power twice, once for each car rather than once for the single (shared) car charger.

The intent of introducing num_chargers was that Predbat could then better auto detect such configuration mis-matches.

The problem as I see it is that car charging detection is at best going to be patchy with a whole different range of car chargers being used, from granny chargers to fully integrated IOG aware devices.

Additionally different chargers could be auto detected in different components and those components would need to increment num_chargers not overwrite it with the last value found.

And of course, introducing a new concept to apps.yaml means it will need a default value as most people won't configure this at least initially (if at all).

I fear it risks creating more logged warnings that people won't understand or we get support tickets because it flags a mis-match that doesn't exist.

So whilst a good concept to introduce this, I'm not sure it is viable to deliver.

Just my POV

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Num chargers as introduced in this patch is overkill - it's a bit of a crutch to detect an incorrect setup - but it was in theory simple if chargers are not auto detected - or there is only one autodetected.
Anything else got messy.
Happy to drop this patch.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

(2 plus weeks to try and merge a minor patch is a good sign it was ill conceived :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Power flow diagram doubles EV charging draw for 2-car/1-charger setup

4 participants