Skip to content

Ignore a frozen car SoC sensor while the car is plugged in - #45

Merged
dandwhelan merged 3 commits into
mainfrom
claude/car-soc-stale-guard
Aug 20, 2026
Merged

dandwhelan merged 3 commits into
mainfrom
claude/car-soc-stale-guard

Conversation

@dandwhelan

Copy link
Copy Markdown
Owner

Problem

An EV integration that stops updating the car's SoC sensor while the car is plugged in leaves Predbat planning against a frozen value — and the frozen value is the worst one to be wrong about, because it is whatever the sensor reached before it died, which is normally just under the charge limit.

Observed on a live system over the night of 17–18 August. sensor.<vin>_soc last updated at 04:33 the previous day and sat at 89.9% against a 90% limit, while the charger delivered a steady 7 kW for the whole cheap-rate window:

Car 0 plan charging from 55.4683kWh to 90.0% (55.53kWh)   <- identical on every cycle, all night
charging_now [False, False]
sensor.hypervolt_ev_energy  877.07 -> 894.70 kWh          <- ~17.6 kWh actually delivered

Two things follow from a car that looks like it needs 0.06 kWh:

  1. The real load disappears from the plan. With no overnight car load to save the battery for, and a flat 12p export against 3.49p import, the optimiser scored exporting the house battery above keeping it charged. It zeroed the live charge window mid-window and scheduled a forced export instead:

    Raw charge windows [ 18-08 00:00:00 - 18-08 05:30:00 @ 3.49p 0%, ... ]
    Previous plan best metric is -222.24 and new plan best metric is -272.0
    New plan metric is significantly better from previous plan, using new plan
    

    The battery went 8% → 72%, then got cancelled twice and finished the window at ~65% instead of 100%.

  2. The battery hold never engages. switch.predbat_car_charging_from_battery was off, but the hold in execute.py requires a slot with window.get("kwh", 0) > 0. With ~0 kWh planned it was skipped every cycle — Disabling battery discharge whilst car ... is charging appears zero times in the log — so the house battery discharged into the car.

Nothing in the logs identifies the sensor as the cause; the plan changes all look like ordinary optimisation.

Change

Adds an optional car_charging_soc_max_age (minutes) to apps.yaml. When set, and when car_charging_planned reports the car as plugged in, Predbat checks the SoC sensor's own last_updated. If it is older than the limit, Predbat logs a warning and plans as if the car were empty:

Warn: Car 0 SoC sensor last updated 1200 minutes ago (limit car_charging_soc_max_age 240) while plugged in - ignoring the reading of 89.9% and assuming a charge is needed

The true SoC is unrecoverable, so this picks the recoverable direction. Over-planning car charging costs some cheap-rate allocation; under-planning it strands the house battery and then drains it into the car.

Deliberate limits on when it fires:

  • Only when the car is plugged in. An unplugged car that has gone to sleep legitimately stops reporting for days, and that is not a fault.
  • Only when configured. Default is 0, which disables the check, so behaviour is unchanged for everyone who does not set it.
  • Not for manual SoC cars (switch.predbat_car_charging_manual_soc), which read no sensor.
  • A stale verdict sticks for the cycle — the Octopus battery-size path recalculates the SoC, and re-reading the sensor there would quietly reinstate the frozen percentage against the new battery size.
  • An unreadable or unparseable last_updated is treated as "cannot tell" and the reading is left alone, rather than assumed stale.

Tests

tests/test_car_soc_stale.py, registered as car_soc_stale and car_soc_age:

  • fresh sensor is used as-is; frozen-for-20-hours while plugged in is ignored
  • age exactly equal to the limit is not stale (boundary)
  • max_age: 0 leaves the frozen reading alone — the backwards-compatibility guarantee
  • stale but unplugged is not a fault
  • missing, unparseable and naive (no UTC offset) last_updated all degrade to trusting the reading rather than raising
  • the age helper resolves attribute-qualified entities to their parent, and returns None for a missing entity or a plain numeric config value

Verification

  • unit_test.py --keyword car_soc passes, and the warning fires exactly once for the stale case.
  • Full --quick suite run with black/ruff clean.
  • web_if was not exercised locally: it binds port 5052, which the live Predbat instance on the test machine holds. CI runs it on a clean runner.

🤖 Generated with Claude Code

dandwhelan and others added 3 commits August 18, 2026 17:03
A car SoC sensor that stops updating mid-charge keeps reporting the value
it last reached, which is normally just under the charge limit. Predbat
then plans ~0 kWh of car charging: the real overnight load vanishes from
the plan (observed: the optimiser cancelled a live 100% charge window to
export the house battery instead, while the charger drew a steady 7 kW),
and the car-charging battery hold in execute.py never fires because it
requires a slot with kwh > 0.

Add car_charging_soc_max_age (minutes, default 0 = disabled). When set,
and when car_charging_planned reports the car plugged in, a SoC reading
whose own last_updated is older than the limit is ignored and the car is
planned as if empty - the recoverable direction. Unplugged cars are never
checked (a sleeping car legitimately goes quiet for days), manual-SoC
cars read no sensor, and an unreadable last_updated leaves the reading
alone. A stale verdict sticks through the Octopus battery-size
recalculation so the frozen percentage cannot be reinstated there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dandwhelan
dandwhelan merged commit 5e1ca36 into main Aug 20, 2026
2 checks passed
@dandwhelan
dandwhelan deleted the claude/car-soc-stale-guard branch August 20, 2026 06:55
@dandwhelan dandwhelan mentioned this pull request Aug 21, 2026
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.

2 participants