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. |
…harger energy and reset on unplug (#5678) * feat(car): manual car SoC tracks cars with no SoC sensor, charger energy and unplugging - A car with no car_charging_soc sensor uses the manual SoC without the switch being turned on; the switch now only forces manual SoC for a car that has a sensor. The manual SoC kWh entity is shown for every car, and for a car on its sensor it shows the sensor's reading. - With car_charging_energy set for the car, the manual SoC rises by the energy the charger's history shows since the entity last changed, instead of the plan's prediction. - With a plugged-in sensor, the manual SoC resets to zero once the car has been unplugged since the entity last changed. - Both work from the entity's last_changed in Home Assistant, so nothing is lost over a restart. - New optional car_charging_plugged for a pure plugged-in sensor, used for the reset in preference to car_charging_planned. Ohme publishes binary_sensor.predbat_ohme_plugged and wires it automatically, as its connected sensor goes off when a charge finishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(car): track manual car SoC from a saved record, and only auto-enable it with energy and plug sensors - Drop last_changed as the reference. Predbat's own database (no Home Assistant) stores it with no timezone, which the parser rejected, and stamps it on every write rather than on a change. Each manually tracked car now keeps a record (value last seen or written, time energy is counted from, last plug reading), saved through the storage component so a restart picks up where it left off. - A value that differs from the record is a user edit: kept, and counted on from then. - Unplug reset on a plugged-in -> unplugged transition of the plug sensor; unavailable is no reading. - A car with no SoC sensor is only tracked automatically when it has both car_charging_energy and a plug sensor entity. Without them the plan write-back would climb to the charge limit with nothing to reset it, and a car modelled as full loses its Octopus Intelligent slots and battery hold. - Gateway: only wire car_charging_soc when the charger reports the car's SoC (the estimate restarts at 0% each session), so hub cars go on the tracked path; re-run auto-config when the car's charger starts reporting a SoC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(car): publish the manual SoC tracking record as a sensor instead of the storage component Each tracked car's record now lives in sensor.predbat_car_manual_soc_tracked[_N]: the tracked kWh as the state, with since (time energy is counted from) and plugged (last plug reading) as attributes. A restart reads it back from Home Assistant or Predbat's database, which load every entity with its attributes at startup. Published only when the SoC or plug reading changes. No storage dependency, no expiry, and the record is visible to users and support. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(car): don't reset the manual car SoC on a charger reconnecting - Debounce the unplug reset: the plugged-in sensor must read unplugged for CAR_MANUAL_SOC_UNPLUG_MINUTES (10) before the SoC goes to zero. A pending unplug is dropped if the car reads plugged in again, kept through an unavailable reading, and published as the tracking sensor's unplugged_since attribute so it survives a restart. - Gateway: publish binary_sensor.<pfx>_plugged, unavailable while the charger itself is offline rather than off, and wire it as car_charging_plugged. The connected sensor (car_charging_planned) is unchanged. The hub synthesises "Available" on an OCPP reconnect and marks the known charger Offline until BootNotification (predbat-gateway#504), which read as an unplug several times a day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(car): clamp the manual car SoC, hold it while not charging, and cap 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> * fix(car): reset the manual car SoC only on car_charging_plugged, and address review - Only car_charging_plugged resets the manual SoC on unplug, and auto-tracking a car with no SoC sensor needs it. car_charging_planned turns off when a charge finishes on many chargers, so it reset a still-plugged-in car. It still holds the plan's write-back. A car with no car_charging_plugged is reset after 12 hours idle instead (no edit, no charge counted or written, car_charging_now off). - The tracked value is no longer clamped to the configured battery size when energy is counted: Octopus's larger size is only known later in the cycle. The plan still reads it clamped. - The kWh entity is only mirrored for cars with a SoC sensor, so a car that drops out of tracking no longer has its value overwritten with 0. - Gateway: session energy reads unavailable while the charger is offline rather than 0, which the history counted as a new session. car_charging_soc is no longer wired, and sensor.<pfx>_soc holds the last SoC the charger reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(car): wire car_charging_plugged from myenergi, Wallbox and GE Cloud, and add it to the templates Since d2951cf a car with no SoC sensor is only tracked automatically when car_charging_plugged is set, so the three charger components that wired only car_charging_planned no longer got their cars tracked. - myenergi, Wallbox and GE Cloud EVC each publish a _plugged binary sensor (GE: _evc_car_plugged) and wire it as car_charging_plugged. It is on with a car plugged in, including after a charge finishes, and off only when the charger positively says no car: Zappi "EV Disconnected", Wallbox Ready or Locked with no car, GE available/idle. Anything else - offline, a fault, Wallbox Disconnected (also what an unreported status reads as), a status not known - is unavailable, so a dropout never resets the manual SoC. - Every template with a car section gets a commented car_charging_plugged example, and those that had no car_charging_now get the commented car_charging_now example and its response list. - components.md and car-charging.md list the new sensors and wiring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(wallbox): record what each status id means for the cable, from live HA history Three weeks of history from an OCPP Pulsar Plus confirm the mapping behind binary_sensor.<prefix>_plugged: 209 is no car, 210/164/181 are plugged in, 161 is a no-car blip, and status 0 "Disconnected" is the charger dropping off the cloud (seen mid-charge too), so it is rightly unavailable rather than unplugged. Replaying the history gave one reset per real unplug and no others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: CI <ci@example.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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