Skip to content

feat(car): manual car SoC for cars with no SoC sensor, tracked from charger energy and reset on unplug - #5678

Merged
springfall2008 merged 9 commits into
mainfrom
feat/car-manual-soc-auto
Oct 23, 2026
Merged

springfall2008 merged 9 commits into
mainfrom
feat/car-manual-soc-auto

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Reworks manual car SoC (car_charging_manual_soc) so it can run without being switched on for cars that can be tracked from what really happens. It follows what the charger actually delivered, resets when the car is unplugged, and survives a restart.

  • No SoC sensor → tracked automatically, when it can be. A car with no car_charging_soc entry is tracked in input_number.predbat_car_charging_manual_soc_kwh[_N] when it has both a car_charging_energy sensor and a car_charging_plugged sensor. car_charging_planned is not enough, because many chargers turn it off when a charge finishes. A fixed value such as "on" doesn't count as a plugged-in sensor. Without both, the car is planned from empty as before. Tracking it from the plan's prediction with nothing to reset it would climb to the charge limit and stay there, and a car modelled as full has its Octopus Intelligent slots ignored and no battery hold during them. Predbat never turns the switch on; the switch forces manual SoC for any car.
  • The kWh entity is shown for every car (gated on num_cars, like the switch). For a car on its sensor, Predbat writes the sensor's reading into it each live cycle, so it is clear that editing it does nothing, and turning the switch on starts from the car's real level.
  • Energy-based tracking. With car_charging_energy set for the car, the manual SoC rises by the energy the charger's history shows (less car_charging_loss) instead of the plan's predicted charge. Each cycle's count is capped at car_charging_rate × elapsed time × 1.1 (+0.1 kWh): the history counts a counter that drops to zero and comes back as a new session, and the cap stops one glitch or jump from filling the car. The end-of-cycle predicted write-back skips these cars so nothing is counted twice.
  • Plan-based tracking held while not charging. For a switched-on car without an energy sensor, the plan's predicted write-back is skipped when car_charging_now reads not charging, or when car_charging_plugged (else car_charging_planned) reads unplugged. Unavailable/unknown is no reading, and the prediction is written as before.
  • Clamp on read. The plan uses the manual SoC within 0 and the battery size, clamped again once Octopus Intelligent reports a larger vehicle battery size. The entity keeps whatever was typed or counted. Counting doesn't clamp it to the configured size, as Octopus's size is only known later in the cycle.
  • Reset on unplug. The manual SoC resets to 0 when car_charging_plugged goes from plugged in to unplugged and stays unplugged for 10 minutes (CAR_MANUAL_SOC_UNPLUG_MINUTES), so a charger that reads unplugged for a few minutes while it reconnects doesn't reset it. A pending unplug is dropped if the car reads plugged in again and kept through an unavailable reading. Unavailable/unknown is never an unplug, and a value entered while unplugged is kept.
  • Reset when idle. A car with no car_charging_plugged (in practice one forced by the switch) resets to 0 after 12 hours idle (CAR_MANUAL_SOC_IDLE_MINUTES): no value entered, no charge counted or written by the plan, and car_charging_now not reading charging. car_charging_planned never resets it.
  • Sensor mirror. Only a car with a car_charging_soc sensor has its reading shown in the kWh entity, so a car that drops out of tracking keeps its value.
  • User edits. A value that differs from the one Predbat last saw or wrote is the user's. It is kept, and energy is counted from then on.
  • New optional car_charging_plugged (per car) for a sensor that only reports whether the cable is in, used for the reset and needed for auto-tracking. Ohme publishes binary_sensor.predbat_ohme_plugged and wires it under ohme_automatic, because binary_sensor.predbat_ohme_connected deliberately goes off at FINISHED_CHARGE.
  • 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 there is no car (Zappi "EV Disconnected", Wallbox Ready / Locked with no car, GE available/idle). Offline, faulted, Wallbox "Disconnected" and unknown statuses read unavailable, so a dropout never resets the car.
  • Templates. Every template with a car section has a commented car_charging_plugged example. Those without a car_charging_now example now have one, with its response list.
  • Gateway (gateway_evc_automatic).
    • car_charging_soc is no longer wired: a charger rarely knows the car's SoC, and only during a session. The car is tracked from session energy instead. If a user maps it to sensor.<pfx>_soc by hand, that sensor holds the last SoC the charger reported rather than falling back to the session estimate.
    • sensor.<pfx>_session_energy reads unavailable while the charger is offline, not 0, so the energy history doesn't count the session twice.
    • New binary_sensor.<pfx>_plugged, wired as car_charging_plugged: whether a car is plugged in, but unavailable while the charger itself is offline, where _connected reads off. The hub also synthesises Available on an OCPP reconnect (Predictive-Cloud-Ltd/predbat-gateway#504), which the unplug wait above absorbs.

Design: a published tracking record

Each tracked car's state is published as sensor.predbat_car_manual_soc_tracked[_N]:

  • state: the tracked kWh, i.e. the value Predbat last saw in or wrote to the kWh entity
  • since attribute: the UTC time charger energy has been counted up to
  • plugged attribute: the plugged-in sensor's last real reading (true, false or null)
  • active attribute: when the car was last active, for the idle reset
  • unplugged_since attribute: when a pending unplug was first read, until it has lasted 10 minutes

Each live cycle compares the kWh entity with the record (edit or not), adds the energy since since, and checks for a plugged-in → unplugged transition. After a restart the record is read back from the sensor. Home Assistant and Predbat's own database both load every entity with its attributes at startup.

It is published only when the SoC or plug reading changes. When nothing was charged, since is kept at most 60 minutes back (CAR_MANUAL_SOC_LATE_MINUTES), to allow for energy the history records late, and that move stays in memory.

Entity last_changed is deliberately not used. Without Home Assistant, Predbat's database stores it with no timezone, which str2time rejects, and updates it on every write rather than on a change.

Tracking only runs for the live plan (fetch_sensor_data_cars(save=True)), not compare.py's re-runs.

Covers what #5671 does, apart from its fall-back to the plan when an energy counter has been unreadable for an hour (see below).

Things for review

  • Reset value is 0 kWh. The car is planned as empty until someone enters its level.
  • HA restart together with a Predbat restart loses the record (HA does not keep states set through its API), and that car counts from now. That under-counts, the safe direction. A Predbat restart alone, or any restart without HA, reads it back.
  • An unplug and re-plug while Predbat is down is not seen as an unplug.
  • The energy cap uses car_charging_rate. A rate set below the real one under-counts, the safe direction. Energy clamped in one cycle is not carried to the next.
  • No fallback to the plan for a dead energy counter. Unlike fix(car): clamp manual car SoC and advance it by measured energy, not the plan #5671, a car whose energy sensor stops reporting is held, not moved on by the plan. That under-counts rather than drifts.
  • The record sensor is left behind for a car that stops being tracked. If the car is tracked again, its value no longer matches, so it is treated as an edit.
  • A sensor read via entity$attribute works for the plug reading. Only the transition is used.
  • Impact analysis rated fetch_sensor_data_cars and update_car_manual_soc HIGH (main plan-cycle path, one direct caller each); the new behaviour only acts on manually tracked cars. The gateway changes rated LOW.
  • Not tested against real hardware.

Test plan

  • car_manual_soc test module (49 cases):
    • auto-enable needs both energy and plug sensors (none, either alone, fixed plug value)
    • switch forcing, per-car lists and unresolved regex
    • a first sight counts from now; energy since the record, less losses; nothing counted twice
    • a user edit is kept and counted on from then; a time older than the history counts the whole history
    • the 60-minute late cap; no history; battery-size cap; a per-car history fetch
    • the write-back skips energy cars and records its own write, so a following unplug still resets
    • unplug transition, unavailable plug reading, value entered while unplugged, first seen unplugged, car_charging_plugged precedence
    • the record is published; a restart counts downtime energy once, sees an edit made while down and an unplug while down; a missing or unavailable sensor counts from now; nothing changed publishes nothing
    • compare.py re-runs publish nothing; a car with no sensors never climbs; sensor mirror; switch forcing via fetch
    • unplug wait: pending at first, dropped on a plugged-in reading, kept through unavailable, reset once it has lasted, read back after a restart
    • energy cap on a counter glitch, full count within the rate, clamp on read (incl. negative and the Octopus battery size), car_charging_now off holds / on and unavailable let the write-back through
  • Mutation-checked: removing each guard (plug requirement, edit detection, transition-only reset, unavailable handling, late cap, read-back, publish, plug attribute, unplug wait, energy cap, clamps, car_charging_now hold) fails a test
  • Review fixes (d2951cf): planned is not a plug sensor for auto-enable or reset but still holds the write-back; 12-hour idle reset (edit, energy, write-back and car_charging_now keep it active, read back after a restart); no store clamp; mirror only with a SoC sensor. Each fix mutation-checked.
  • test_gateway: no car_charging_soc wired whether or not the charger reports one, no reconfigure on SoC reporting; _soc holds the last reported value; _session_energy unavailable while offline; _plugged published and unavailable while the charger is offline; car_charging_plugged wired
  • test_myenergi / test_wallbox / test_ge_cloud: _plugged states for every plug/status case and car_charging_plugged wiring order (bbc0494, mutation-checked); all templates still parse
  • test_ohme: plugged_sensor across every Ohme status; automatic config sets car_charging_plugged
  • ./run_all --quick passes; pre-commit clean
  • Live check with an Ohme charger, a Gateway car and a car without a SoC sensor

🤖 Generated with Claude Code

CI and others added 5 commits October 21, 2026 16:42
…rgy 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>
…ble 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>
…tead 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>
- 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>
…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>

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Automated comment from the triage bot.

Reviewed at head f80579e (manual car SoC for cars with no SoC sensor: auto-enable needs both an energy and a plugged-in sensor, energy-based tracking with a rate cap, reset on unplug after 10 minutes, a published record sensor read back on restart, plus the gateway/Ohme car_charging_plugged wiring). 1 finding posted inline, 0 in the body.

Checked: the new tracking flow end to end (auto-enable gating in car_manual_soc_config, edit detection, the 60-minute late cap, the pending-unplug wait, publish-on-change with restart read-back) against the live-cycle call order (fetch_config_options → load_car_energy → fetch_sensor_data_cars → plan → update_car_manual_soc); scalar-vs-list car-arg semantics and the enable/enable_condition gating (the same num_cars pattern the switch items already use); the energy add's consistency with car_charging_loss's effectiveness semantics (multiplied by 1 - loss, as prediction.py/plan.py use it); the record dict is YAML-safe for the debug dump; compare.py's save=False re-runs publish nothing; the docs (apps-yaml.md, car-charging.md, customisation.md) match the code, including the #predbat-led-charging anchors; and the test module is registered in TEST_REGISTRY. gh pr checks was denied in this session, so CI status was not independently confirmed.

Comment thread apps/predbat/gateway.py Outdated
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 21, 2026

@mgazza mgazza left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at f80579e. The new commits address what we raised earlier (no more last_changed, auto-tracking gated on energy + plug sensors, clamp on read, rate cap, car_charging_now hold). Four issues below, each reproduced with a scratch test reusing test_car_manual_soc.py fixtures. A fifth note is a reply on the triage bot's gateway.py:2279 thread.

Comment thread apps/predbat/fetch.py Outdated
Comment thread apps/predbat/gateway.py
Comment thread apps/predbat/fetch.py Outdated
Comment thread apps/predbat/fetch.py
CI and others added 4 commits October 22, 2026 08:47
…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>
…oud, 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>
…ive 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>
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