Skip to content

Commit 9bcfda2

Browse files
committed
fixed motion detector
- used xwrong door lock detection - Litle to agressive on the stops
1 parent 54cb27c commit 9bcfda2

2 files changed

Lines changed: 35 additions & 40 deletions

File tree

custom_components/cardata/coordinator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,8 +1095,8 @@ async def _async_handle_message_locked(
10951095
except (TypeError, ValueError):
10961096
pass
10971097

1098-
# Wire door lock state to motion detector for parking detection
1099-
elif descriptor == "vehicle.cabin.door.lock.status":
1098+
# Wire door state to motion detector for parking detection
1099+
elif descriptor == "vehicle.cabin.door.status":
11001100
if value is not None:
11011101
self._motion_detector.update_door_lock_state(vin, str(value))
11021102

custom_components/cardata/motion_detection.py

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ class MotionDetector:
2323
2. GPS (primary) - 2 minute window, most accurate for small movements
2424
3. Door lock state - if doors unlocked after GPS stale → car stopped
2525
4. Mileage (fallback) - must show actual odometer increase since GPS went stale
26-
5. MQTT silence - no MQTT stream data for 2 min → NOT MOVING (car off)
2726
"""
2827

2928
# Door lock states that indicate the car is driving (doors auto-lock while moving)
@@ -35,9 +34,6 @@ class MotionDetector:
3534
# Mileage movement window (longer, less frequent updates)
3635
MILEAGE_ACTIVE_WINDOW_MINUTES: ClassVar[float] = 7.0
3736

38-
# MQTT silence threshold - no MQTT stream data for this long → car is off
39-
MQTT_SILENCE_MINUTES: ClassVar[float] = 2.0
40-
4137
# Park zone radius - GPS readings within this distance are considered "parked jitter"
4238
PARK_RADIUS_M: ClassVar[float] = 35.0
4339

@@ -48,6 +44,11 @@ class MotionDetector:
4844
# Max GPS readings to keep for centroid calculation while parked
4945
MAX_PARK_READINGS: ClassVar[int] = 10
5046

47+
# Minimum time span (seconds) for the 3 park-confirming readings.
48+
# BMW sends GPS in bursts (3 readings within <1s at the same position).
49+
# Without this guard the burst immediately parks the car while driving.
50+
MIN_PARK_SPAN_SECONDS: ClassVar[float] = 30.0
51+
5152
# Minutes without GPS update to consider GPS unavailable (switch to mileage fallback)
5253
# Longer than MOTION_ACTIVE_WINDOW to handle BMW's bursty GPS (every 2-3 min)
5354
GPS_UPDATE_STALE_MINUTES: ClassVar[float] = 5.0
@@ -198,21 +199,30 @@ def update_location(self, vin: str, lat: float, lon: float) -> bool:
198199
self._park_readings[vin] = park_readings
199200

200201
# Check if we've been stationary for enough readings to confirm parked
201-
# Need at least 3 readings within park radius to confirm stop
202+
# Need at least 3 readings within park radius AND spread over time
203+
# (BMW sends GPS in bursts — 3 readings within <1s is not real parking)
202204
if len(park_readings) >= 3:
203205
# Check if ALL recent readings are within park radius
204206
all_within_park = all(
205207
self._calculate_distance(park_anchor[0], park_anchor[1], r[0], r[1]) <= self.PARK_RADIUS_M
206208
for r in park_readings[-3:]
207209
)
208210
if all_within_park:
211+
# Require readings to span a minimum time window to avoid
212+
# treating a single GPS burst as parking
213+
first_park_time = park_readings[-3][2]
214+
time_span = (now - first_park_time).total_seconds()
215+
if time_span < self.MIN_PARK_SPAN_SECONDS:
216+
# Burst detection: readings too close together, not real parking
217+
# Stay in driving mode
218+
return True
219+
209220
# Vehicle has stopped - exit driving mode
210221
# Backdate last movement to when the car first entered the park zone
211-
# (oldest of the 3 confirming readings) for accurate timing
212-
first_park_time = park_readings[-3][2]
213222
_LOGGER.debug(
214-
"Motion: %s stopped (3 readings within park radius) - NOW PARKED",
223+
"Motion: %s stopped (3 readings within park radius over %.0fs) - NOW PARKED",
215224
redact_vin(vin),
225+
time_span,
216226
)
217227
self._is_driving[vin] = False
218228
self._last_location_change[vin] = first_park_time
@@ -339,7 +349,7 @@ def update_mqtt_activity(self, vin: str) -> None:
339349
"""Record that an MQTT stream message was received for this VIN.
340350
341351
Only call this for real MQTT stream messages, NOT telematics API responses.
342-
Used as a last-resort fallback: if no MQTT for 2 min, car is off.
352+
Currently unused by motion detection but kept for diagnostics.
343353
"""
344354
self._last_mqtt_stream_at[vin] = datetime.now(UTC)
345355

@@ -377,12 +387,11 @@ def is_moving(self, vin: str) -> bool | None:
377387
Priority chain:
378388
1. Charging → always False (absolute override, trumps everything)
379389
2. GPS (primary) → fresh GPS within 2 min, with driving mode trust
380-
3. GPS gap + MQTT active → trust driving mode (unless doors unlocked)
390+
3. GPS gap → trust driving mode during gap (door lock overrides)
381391
4. Door lock state → doors changed from locked/selectiveLocked → stopped
382392
5. Mileage (fallback) → actual odometer increase since GPS went stale
383-
6. MQTT silence → no MQTT stream for 2 min → False (car is off)
384-
7. No data at all → None (fall back to BMW-provided isMoving)
385-
8. Default → False
393+
6. No data at all → None (fall back to BMW-provided isMoving)
394+
7. Default → False
386395
387396
Returns:
388397
True - Active proof of movement
@@ -403,11 +412,9 @@ def is_moving(self, vin: str) -> bool | None:
403412
last_gps_change = self._last_location_change.get(vin)
404413
last_mileage = self._last_mileage.get(vin)
405414
last_mileage_change = self._last_mileage_change.get(vin)
406-
last_mqtt = self._last_mqtt_stream_at.get(vin)
407415

408416
# Calculate ages (None if no data)
409417
gps_update_age = (now - last_gps_update).total_seconds() / 60.0 if last_gps_update else None
410-
mqtt_age = (now - last_mqtt).total_seconds() / 60.0 if last_mqtt else None
411418

412419
# 2. GPS PRIMARY - GPS data arrived within 2 minutes (freshest source)
413420
if gps_update_age is not None and gps_update_age < self.MOTION_ACTIVE_WINDOW_MINUTES:
@@ -443,30 +450,28 @@ def is_moving(self, vin: str) -> bool | None:
443450
return result
444451

445452
# 3. GPS GAP HANDLING - GPS between 2-5 min old, driving mode active
446-
# BMW GPS arrives in bursts every 2-3 min; trust driving mode if MQTT is still active
447-
# BUT: check door lock first - if doors unlocked, driver exitednot moving
453+
# BMW GPS arrives in bursts every 2-3 min; trust driving mode during gaps.
454+
# Door lock overrides: if doors changed from lockedunlocked, car stopped.
448455
if self._is_driving.get(vin, False) and gps_update_age is not None:
449456
if gps_update_age < self.GPS_UPDATE_STALE_MINUTES:
450457
# Door lock override: if doors changed from driving state, car stopped
451458
door_unlocked_at = self._door_unlocked_at.get(vin)
452459
if door_unlocked_at is not None and last_gps_update is not None and door_unlocked_at > last_gps_update:
453460
door_state = self._door_lock_state.get(vin, "unknown")
454461
_LOGGER.debug(
455-
"Motion: %s door lock changed to '%s' after GPS stale - NOT MOVING",
462+
"Motion: %s door state changed to '%s' after GPS stale - NOT MOVING",
456463
redact_vin(vin),
457464
door_state,
458465
)
459466
self._is_driving[vin] = False
460467
return False
461468

462-
if mqtt_age is not None and mqtt_age < self.MQTT_SILENCE_MINUTES:
463-
_LOGGER.debug(
464-
"Motion: %s driving mode, GPS gap (%.1f min) but MQTT active (%.1f min) - MOVING",
465-
redact_vin(vin),
466-
gps_update_age,
467-
mqtt_age,
468-
)
469-
return True
469+
_LOGGER.debug(
470+
"Motion: %s driving mode, GPS gap (%.1f min) - MOVING",
471+
redact_vin(vin),
472+
gps_update_age,
473+
)
474+
return True
470475

471476
# 4. DOOR LOCK FALLBACK - GPS stale, doors changed from driving to parked state
472477
# This catches the case where GPS is >5 min stale but door state signals arrival
@@ -517,17 +522,7 @@ def is_moving(self, vin: str) -> bool | None:
517522
)
518523
self._is_driving[vin] = False
519524

520-
# 6. MQTT SILENCE FALLBACK - no MQTT stream data for 2+ min → car is off
521-
if mqtt_age is not None and mqtt_age >= self.MQTT_SILENCE_MINUTES:
522-
_LOGGER.debug(
523-
"Motion: %s MQTT silent for %.1f min (threshold=%.1f) - NOT MOVING",
524-
redact_vin(vin),
525-
mqtt_age,
526-
self.MQTT_SILENCE_MINUTES,
527-
)
528-
return False
529-
530-
# 7. No GPS data at all for this VIN — use mileage if available,
525+
# 6. No GPS data at all for this VIN — use mileage if available,
531526
# otherwise return None so the caller falls back to BMW-provided vehicle.isMoving.
532527
if last_gps_update is None:
533528
if last_mileage_change is not None:
@@ -536,7 +531,7 @@ def is_moving(self, vin: str) -> bool | None:
536531
return True
537532
return None
538533

539-
# 8. DEFAULT: Not moving
534+
# 7. DEFAULT: Not moving
540535
return False
541536

542537
def has_signaled_entity(self, vin: str) -> bool:

0 commit comments

Comments
 (0)