@@ -379,18 +379,35 @@ def reanchor_driving_session(self, vin: str, new_soc: float, current_mileage: fl
379379 if session .anchor_soc == new_soc and session .anchor_mileage == current_mileage :
380380 return
381381 old_anchor = session .anchor_soc
382- session .anchor_soc = new_soc
382+ # BMW sends integer SOC. If our sub-integer prediction rounds to that
383+ # integer (abs < 0.5), keep prediction as anchor to avoid cosmetic jumps.
384+ # Otherwise BMW disagrees and we correct to their value.
385+ #
386+ # P=pred N=bmw |diff| branch anchor display
387+ # 54.7 55 0.3 keep 54.7 54.7 (rounding, smooth)
388+ # 54.1 54 0.1 keep 54.1 54.1 (rounding, smooth)
389+ # 54.0 55 1.0 correct 55 55.0 (real drift up)
390+ # 54.0 54 0.0 keep 54.0 54.0 (exact match)
391+ # 54.7 54 0.7 correct 54 54.0 (real drift down)
392+ # 54.7 57 2.3 correct 57 57.0 (real drift up)
393+ # 54.4 54 0.4 keep 54.4 54.4 (rounding, smooth)
394+ # 54.4 55 0.6 correct 55 55.0 (54.4 != round(55))
395+ if abs (new_soc - session .last_predicted_soc ) < 0.5 :
396+ session .anchor_soc = session .last_predicted_soc
397+ else :
398+ session .anchor_soc = new_soc
399+ session .last_predicted_soc = new_soc
383400 session .anchor_mileage = current_mileage
384- session .last_predicted_soc = new_soc
385401 # Transfer segment aux to trip total before resetting for new segment
386402 session .trip_total_aux_kwh += session .accumulated_aux_kwh
387403 session .accumulated_aux_kwh = 0.0
388404 _LOGGER .debug (
389- "Magic SOC: Re-anchored %s from %.1f%% to %.1f%% at %.1f km" ,
405+ "Magic SOC: Re-anchored %s %.1f%% → %.1f%% at %.1f km (BMW: %d%%) " ,
390406 redact_vin (vin ),
391407 old_anchor ,
392- new_soc ,
408+ session . anchor_soc ,
393409 current_mileage ,
410+ new_soc ,
394411 )
395412
396413 def end_driving_session (self , vin : str , end_soc : float | None , end_mileage : float | None ) -> None :
@@ -588,14 +605,19 @@ def get_magic_soc(self, vin: str, bmw_soc: float | None, mileage: float | None)
588605 if last_driving is not None :
589606 predicted_soc , saved_at = last_driving
590607 if (time .time () - saved_at ) < DRIVING_SOC_CONTINUITY_SECONDS :
591- # Use last prediction if BMW SOC appears stale (higher or equal)
592- if bmw_soc is None or bmw_soc >= predicted_soc :
608+ # Use last prediction if BMW SOC is stale (higher/equal) or
609+ # within rounding of our sub-integer prediction
610+ if bmw_soc is None or bmw_soc >= predicted_soc or abs (bmw_soc - predicted_soc ) < 0.5 :
593611 self ._last_magic_soc [vin ] = predicted_soc
594612 return predicted_soc
595613 # Expired or BMW sent fresh lower SOC — discard
596614 del self ._last_driving_predicted_soc [vin ]
597615
598616 if bmw_soc is not None :
617+ # Keep existing sub-integer prediction if BMW agrees within rounding
618+ existing = self ._last_magic_soc .get (vin )
619+ if existing is not None and abs (bmw_soc - existing ) < 0.5 :
620+ return existing
599621 self ._last_magic_soc [vin ] = bmw_soc
600622 return bmw_soc
601623 return self ._last_magic_soc .get (vin )
0 commit comments