Skip to content

Commit 2ff0f60

Browse files
authored
Merge pull request #373 from renaudallard/main
Let live V×A keep the charging prediction alive between API polls
2 parents ba4891d + f21d31a commit 2ff0f60

1 file changed

Lines changed: 34 additions & 19 deletions

File tree

custom_components/cardata/soc_prediction.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -867,11 +867,24 @@ def periodic_update_all(self) -> list[str]:
867867
Falls back to last_power_kw for sessions without V×A data (e.g. DC, or AC
868868
vehicles that only report charging.power).
869869
870-
Gated by session.last_external_power_update: if no real MQTT-driven power
871-
update has arrived within STALE_EXTERNAL_POWER_SECONDS, replay is skipped
872-
and cached power values are cleared. This prevents phantom energy
873-
accumulation when BMW keeps reporting CHARGING but stops pushing power
874-
data (e.g. EVSE stopped externally but BMW still reports session active).
870+
Two replay paths with different freshness rules:
871+
872+
1. V×A recompute. When the session has positive voltage and current,
873+
treat the recomputed power as a real reading and refresh the
874+
freshness marker. Stable AC charging can go 30+ min between BMW
875+
MQTT updates because the values do not change, and value-changed
876+
filtering means even API polls do not always flow back through
877+
update_ac_charging_data. The heartbeat is then the only thing
878+
keeping the prediction live, so it has to mark fresh itself.
879+
Self-protecting: when an EVSE stops, BMW reports V=0/I=0 and
880+
_calc_ac_power_kw returns None, dropping into the gated fallback.
881+
882+
2. Cached last_power_kw fallback (no live V×A). Here we cannot tell
883+
ongoing charging apart from a session BMW still flags as active
884+
after an external EVSE stop, so the freshness gate clears the
885+
scalar once STALE_EXTERNAL_POWER_SECONDS has passed without a
886+
real external power update. This is the path that issue #355
887+
guards against; #372 was caused by path 1 being gated as well.
875888
876889
Returns:
877890
List of VINs that had their prediction updated
@@ -883,34 +896,36 @@ def periodic_update_all(self) -> list[str]:
883896
if not session:
884897
continue
885898

886-
# Freshness gate: skip replay if external power data is stale.
899+
# Path 1: live V×A (AC sessions only). DC is excluded because a
900+
# session that flipped AC → DC mid-life can carry stale V×A and
901+
# _calc_ac_power_kw would compute a meaningless number for it.
902+
if session.charging_method != "DC":
903+
power_kw = _calc_ac_power_kw(session)
904+
if power_kw is not None:
905+
self.update_power_reading(vin, power_kw, aux_power_kw=session.last_aux_kw)
906+
updated_vins.append(vin)
907+
continue
908+
909+
# Path 2: cached last_power_kw fallback. Gate on freshness so a
910+
# session BMW keeps flagging as active after an external EVSE
911+
# stop cannot inflate the prediction indefinitely (issue #355).
887912
if (
888913
session.last_external_power_update is None
889914
or now - session.last_external_power_update > STALE_EXTERNAL_POWER_SECONDS
890915
):
891-
# Clear cached power so get_predicted_soc extrapolation also stops.
892-
# V×A values are left intact: if BMW resumes pushing fresh data,
893-
# update_ac_charging_data will refresh them and re-arm the gate.
894916
if session.last_power_kw > 0:
895917
age = (now - session.last_external_power_update) if session.last_external_power_update else -1
896918
_LOGGER.debug(
897-
"Heartbeat stale for %s (age=%.0fs) clearing last_power_kw to freeze prediction",
919+
"Heartbeat stale for %s (age=%.0fs), clearing last_power_kw to freeze prediction",
898920
redact_vin(vin),
899921
age,
900922
)
901923
session.last_power_kw = 0.0
902924
continue
903925

904-
# Prefer V×A recalculation for AC sessions
905-
power_kw = _calc_ac_power_kw(session)
906-
if power_kw is not None:
907-
self.update_power_reading(vin, power_kw, aux_power_kw=session.last_aux_kw, mark_fresh=False)
908-
updated_vins.append(vin)
909-
910-
# Fallback: use last known power for AC sessions without V×A data.
911-
# DC excluded — power tapers during charging, so replaying stale
926+
# DC excluded: power tapers during charging, so replaying stale
912927
# power would overestimate energy.
913-
elif session.last_power_kw > 0 and session.charging_method != "DC":
928+
if session.last_power_kw > 0 and session.charging_method != "DC":
914929
self.update_power_reading(
915930
vin, session.last_power_kw, aux_power_kw=session.last_aux_kw, mark_fresh=False
916931
)

0 commit comments

Comments
 (0)