Repository navigation
Conversation
The manual car SoC tracker was read with no clamp (100 kWh on a 10 kWh car) and written back from the plan's predicted next SoC, so it advanced every cycle even while the charger delivered nothing. Clamp it to [0, battery size] on read. On write-back, when a single car has a car_charging_energy session counter, advance by the energy delivered since the last write-back (less car_charging_loss, a counter drop counts as a new session); when a car_charging_now sensor says the car is not charging, do not advance. With no measured source the plan-based write-back is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TngskXRktQAMFTCf1TdsYZ
…itches, clamp to IOG battery - An unreadable car_charging_energy counter holds the SoC and keeps the baseline (for up to 60 minutes), so the gap's energy is counted once - it no longer falls back to the plan for that cycle and double counts when the counter returns. - A counter drop starts a new baseline and adds nothing; a rise larger than car_charging_rate could deliver since the last good reading is ignored and becomes the new baseline. - car_charging_now unknown/unavailable is no information: falls back to the plan (no counter) instead of freezing the SoC. - A tracker value that differs from the last write-back is a user edit: the energy measured before it is not added on top. - fetch re-clamps manual-SoC cars after IOG replaces the battery size with Octopus's figure. - Tests through fetch_sensor_data_cars() and for each rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TngskXRktQAMFTCf1TdsYZ
…ng them The jump cap measured from the last read and discarded anything above it, so a 1 kWh-resolution or 10-minute-update counter read every 5 minutes added nothing for an hour of charging. Rises are now clamped to a budget that refills at car_charging_rate x 1.1 between readings (at most 60 minutes' worth) and is spent by each rise, so lagging counters are counted in full and a low configured rate clamps rather than zeroes. A rise straight after a drop back to about the pre-drop value is a glitch recovering (500 -> 0 -> 500) and only the excess counts. The user edit check compares against the last write-back clamped to the current battery size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TngskXRktQAMFTCf1TdsYZ
springfall2008
left a comment
There was a problem hiding this comment.
Automated comment from the triage bot.
Automated review of PR #5671 at head commit 9e3f847: 3 findings posted inline (all in apps/predbat/predbat.py) and 1 in this body. The clamp-on-read, the counter baseline/reset arithmetic and the test's finally-restore of shared state all check out, and the new assertions would fail on main so the red/green story holds. The findings are about where the measured path engages and what happens when its sources stop working - in both cases a silent outcome.
docs/car-charging.md:879-884 - documented settings change behaviour with no docs update. The PR gives two documented settings a new job without touching docs/:
car_charging_energy(docs/car-charging.md:57-66): beyond the documented hold/load subtraction, it now advances the manual car SoC tracker whenever one car uses manual SoC - and (per the inline finding) it does so for any single readable entity, including non-car counters the docs bless for load subtraction. Nothing in the docs section says so.- the manual SoC tracker section (docs/car-charging.md:879-884): "Predbat will increment it during charging sessions but will not reset it automatically" stays true in outline, but the increment now follows the counter (or freezes while
car_charging_nowreports not charging), and per-session counters are newly tolerated - docs/car-charging.md:64-66 still insists the sensor must be daily incrementing resetting only at midnight, and users now also need to know how Predbat will fail over when the counter is unreadable (finding 3).
Per the project rule that user-facing behaviour changes get matching docs, a few lines in docs/car-charging.md (the manual-SoC section and the car_charging_energy section, with the heat-pump caveat and the unreadable-counter warning behaviour) would cover it.
| if self.car_charging_soc_next[car_n] is not None: | ||
| measured_kwh = self.car_manual_soc_measured_energy(car_n) | ||
| charging_now_sensor = is_entity_id(self.get_arg("car_charging_now", None, indirect=False, index=car_n)) | ||
| if measured_kwh is not None or (charging_now_sensor and not self.car_charging_now[car_n]): |
There was a problem hiding this comment.
Automated comment from the triage bot.
The measured branch engages for any readable single-entity car_charging_energy on a one-car manual-SoC install - but car_charging_energy has a documented second use that has nothing to do with cars: "You can also use car_charging_energy to remove other house load kWh from the data Predbat uses for the forecast ... exclude multiple loads such as heat pump load" (docs/car-charging.md:73-78). A single-car manual-SoC user subtracting only a heat pump via one sensor now has the heat pump's kWh advance the manual car SoC tracker (soc_next = car_charging_soc + measured x loss, capped at the battery), so the plan models a car filling from heat-pump energy: car charging is planned less or not at all, and with car_charging_hold on the battery is held as if the car were charging. Before this PR car_charging_energy never touched the tracker, so exactly this configuration was unaffected.
I verified by reading car_manual_soc_measured_energy() (predbat.py:1162-1188): the only gates are num_cars == 1 and is_entity_id(), nothing checks what the entity measures, and the clamped tracker then feeds the plan via fetch_sensor_data_cars (fetch.py:1902). I also confirmed the docs bless the non-car use, and that car-charging.md:64-66 requires a daily incrementing counter resetting only at midnight - the PR's drop-restart logic additionally tolerates per-session counters (a genuine improvement), which widens the set of eligible sensors beyond what the docs describe.
Suggestion: gate the measured advance on an expert opt-in (e.g. a per-car switch, default off, turned on for the gateway's OCPP registration), and log one Warn: the first time it engages on a user-set counter, so a heat-pump user can see in the log why their car SoC starts moving.
| if self.num_cars != 1: | ||
| return None | ||
| entity_id = self.get_arg("car_charging_energy", None, indirect=False) | ||
| if isinstance(entity_id, list) and len(entity_id) == 1: |
There was a problem hiding this comment.
Automated comment from the triage bot.
Only a length-1 list unwraps to a sensor; a >=2-entity car_charging_energy list silently disables the measured path (is_entity_id() of a list is False, so this returns None and the plan-based branch advances the tracker by the plan again). The docs' own example is a 3-entity list (docs/car-charging.md:77-82), the list form is blessed for "multiple EV car chargers" (docs/car-charging.md:75, 97), and wallbox/myenergi/alphaess auto-config writes lists (myenergi.py:872, wallbox.py:550, alphaess.py:840). The concrete failure is a two-zappi house charging one car: both entities are car chargers, their summed delivered energy is this car's delivered energy, and here the tracker silently falls back to plan-based drift - the config where measured advance is most useful is the one it skips.
I verified resolve_arg()'s list handling (userinterface.py:164-184): combine=True sums resolved items, but it drops required_unit in its recursive call and sums current values, so a mid-charging session reset on one charger makes the combined sum dip and the drop-as-new-session rule would re-count the other charger's already-counted energy. So the fix is to resolve and delta the entities individually, not to just flip on combine=True. Worth handling, or at minimum a Warn: when the counter is a multi-entity list on a car using manual SoC.
| soc_next = max(0.0, min(soc_next, self.car_charging_battery_size[car_n])) | ||
| self.log("Car {} charging Manual SoC advanced by measured energy {} kWh to {}".format(car_n, dp3(measured_kwh or 0.0), dp3(soc_next))) | ||
| self.expose_config("car_charging_manual_soc_kwh" + car_postfix, dp3(soc_next)) | ||
| elif self.car_charging_soc_next[car_n] is not None: |
There was a problem hiding this comment.
Automated comment from the triage bot.
Both quiet-degradation states this PR introduces are silent:
car_charging_energyconfigured but not readable this cycle - the state is unavailable/unknown, the entity is deleted, or the unit is one unit_conversion() does not map - so float() raises and car_manual_soc_measured_energy() returns None (predbat.py:1178-1181): the plan-based branch runs and advances the tracker by the plan's prediction again, i.e. the drift the PR fixes comes back whenever the counter blinks, with no warning. Contrast load_car_energy()'s history-parse failure, which warns and counts a failed fetch (fetch.py:4155-4174).car_charging_nowconfigured but its sensor is deleted/unavailable - is_entity_id() only tests the string shape, the arg still holds the entity id, so the no-advance branch keeps the tracker frozen indefinitely, with no warning.
The new test only feeds readable values, so none of the None/unavailable paths is exercised - worth one case for each, plus a Warn: (once per restart, per source) on the configured-source-failed path so users can tell a frozen tracker from a working one.
… gates measured energy - Consecutive drops keep the highest pre-drop value, and for 15 minutes after the latest drop rises below it are held back (baseline left at the low value) - 500 -> 250 -> 0 -> 500 and 500 -> 0 -> 250 -> 500 add nothing. After the window a rise from the low value is a real new session and is counted in full. - car_charging_now explicitly off holds the SoC without reading the counter, keeping its baseline; unknown/unavailable is still no information. It is read live as a tri-state rather than from the cached boolean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TngskXRktQAMFTCf1TdsYZ
…ap counted energy Brings in what #5671 covered and #5678 did not: - Clamp on read: the plan uses the manual SoC within zero and the battery size, and clamps again once Octopus Intelligent reports a (larger) vehicle battery size. The entity keeps what was typed. - car_charging_now reading not charging holds the plan's predicted write-back for a manual car without an energy sensor; unavailable/unknown is no reading. - Charger energy counted per cycle is capped at car_charging_rate x elapsed x 1.1 (+0.1 kWh), so a counter that drops to zero and comes back - which the history counts as a new session - or jumps cannot fill the car. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing as superseded by #5678, which now includes what this PR covered:
#5678 keeps its tracking state in One deliberate difference: when the energy counter has been unreadable for an hour, #5678 holds the SoC rather than falling back to the plan, so it under-counts rather than drifts. Thanks for this work, which shaped those parts of #5678. |
Problem
The manual car SoC tracker (
car_charging_manual_soc_kwh) had two problems:fetch_sensor_data_cars()used the tracker as given, so a home with 100 kWh entered for a 10 kWh car planned around a car that was "10x full".update_car_manual_soc()writes backcar_charging_soc_next, which is the plan's predicted SoC after its first step. The only clamp is tocar_charging_limit. The tracker therefore advances by whatever the plan intended (around 0.6 kWh per cycle on a 7 kW slot) even while the charger delivers 0 W. Within a few hours the car reads as full and the plan stops charging it.Fix
car_manual_soc_kwh(car_n)keeps the tracker within[0, car_charging_battery_size]for that car. For an IOG car the battery size is replaced partway through the fetch by Octopus'svehicle_battery_size_in_kwh, so manual-SoC cars are clamped again after that update. Without the second clamp, a 60 kWh tracker on a 75 kWh IOG car would be cut to the configured size.car_charging_energy). This is a single total, so it is only used whennum_cars == 1. The tracker advances by the energy delivered since the last good reading, multiplied by(1 - car_charging_loss), and is capped at the battery size.unavailable): the tracker holds and the baseline is kept, so the energy delivered across the gap is counted once by the next good reading. It never falls back to the plan for that cycle, which would double count.CAR_MANUAL_SOC_COUNTER_HOLD_MINUTES), the baseline is dropped and the write-back falls back to the behaviour below until the counter returns. A missing or broken counter therefore cannot freeze the SoC for good. When the counter comes back it only sets a new baseline.CAR_MANUAL_SOC_GLITCH_MINUTES), PredBat remembers the highest value seen before the drops, even across several drops in a row. Rises that stay below it are held back, and the baseline stays at the low value.car_charging_rate× 1.1 between readings, holds at most 60 minutes' worth (CAR_MANUAL_SOC_MAX_GAP_MINUTES), and each counted rise spends from it. Clamping is logged at Info. Counters with coarse steps (1 kWh) or slow updates (cloud, every 10 minutes) are read every 5 minutes; they lag and then catch up, and are still counted in full. Acar_charging_rateset below the real rate clamps the count rather than zeroing it.car_charging_nowreports not charging (checked first). The tracker holds and any energy counter is left unread, so its baseline is kept. Energy hidden by a lagging sensor is picked up by the next reading, within the budget. The sensor is read live, as a tri-state. Without a counter this case is the same: the sensor can be an on/off sensor or a power sensor read in watts, and the tracker does not advance. If the sensor readsunknown/unavailable, that is treated as no information, not as "not charging".car_charging_limitcap (octopus_intelligent_consider_full has no effect on the prediction - the car is always modelled as filling #4967). Users without a measured source are unaffected.The PredBat gateway's EV registration sets
car_charging_energy(session energy),car_charging_now(charging binary sensor) andnum_cars: 1, so gateway OCPP chargers move to the measured path.Known limitations (all under-count, which is the safe direction)
Tests
test_multi_car_iog.py:car_manual_soc_measuredcovers:car_charging_nowoff, on, and unavailable (falls back to the plan)car_charging_rate3.7 kW against a real 7.4 kW counts 4.17 kWh (clamped, not 0)car_charging_nowoff, a 12 → 900 counter jump counts 0car_charging_nowunavailable, the counter is still countedcar_manual_soc_fetch_clampgoes through the realfetch_sensor_data_cars(). It checks the clamp to the configured battery, and to a 75 kWh IOGvehicle_battery_size_in_kwh(60 → 60, 100 → 75).Each fix was mutation-checked: reverting it fails its test. The reviewer's repro (5-minute cycles, 1 kWh and 10-minute counters) now counts 7.0 / 7.4 kWh. Mutations tried: no IOG re-clamp, no fetch clamp, no hold on an unavailable counter, no jump cap, and
car_charging_nowunavailable treated as off.--test multi_car_iogpasses,--quickpasses (TZ=UTC), and pre-commit is clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01TngskXRktQAMFTCf1TdsYZ