Skip to content

Commit db66994

Browse files
authored
Merge pull request #33 from kvanbiesen/beta-channel
- Fixed #32 - Another attempt for #15 - Changed some icons and doors/states sensors
2 parents 0ea25bc + f22abef commit db66994

6 files changed

Lines changed: 128 additions & 108 deletions

File tree

custom_components/cardata/binary_sensor.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@
2222
if TYPE_CHECKING:
2323
pass
2424

25-
25+
DOOR_NON_DOOR_DESCRIPTORS = (
26+
"vehicle.body.trunk.isOpen",
27+
"vehicle.body.hood.isOpen",
28+
"vehicle.body.trunk.isOpen",
29+
"vehicle.body.trunk.door.isOpen",
30+
)
31+
32+
DOOR_DESCRIPTORS = (
33+
"vehicle.cabin.door.row1.driver.isOpen",
34+
"vehicle.cabin.door.row1.passenger.isOpen",
35+
"vehicle.cabin.door.row2.driver.isOpen",
36+
"vehicle.cabin.door.row2.passenger.isOpen",
37+
)
2638
class CardataBinarySensor(CardataEntity, BinarySensorEntity):
2739
"""Binary sensor for boolean telematic data."""
2840

@@ -33,16 +45,12 @@ def __init__(
3345
) -> None:
3446
super().__init__(coordinator, vin, descriptor)
3547
self._unsubscribe = None
36-
descriptor_lower = descriptor.lower()
37-
DOOR_DESCRIPTORS = (
38-
"vehicle.cabin.door.row1.driver.isopen",
39-
"vehicle.cabin.door.row1.passenger.isopen",
40-
"vehicle.cabin.door.row2.driver.isopen",
41-
"vehicle.cabin.door.row2.passenger.isopen",
42-
)
43-
if descriptor_lower and descriptor_lower in DOOR_DESCRIPTORS:
48+
49+
if descriptor and descriptor in DOOR_NON_DOOR_DESCRIPTORS:
50+
self._attr_device_class = BinarySensorDeviceClass.DOOR
51+
52+
if descriptor and descriptor in DOOR_DESCRIPTORS:
4453
self._attr_device_class = BinarySensorDeviceClass.DOOR
45-
self._attr_icon = "mdi:car-door"
4654

4755
async def async_added_to_hass(self) -> None:
4856
"""Restore state and subscribe to updates."""
@@ -98,7 +106,45 @@ def _handle_update(self, vin: str, descriptor: str) -> None:
98106
# State changed or sensor is unknown - update it!
99107
self._attr_is_on = new_value
100108
self.schedule_update_ha_state()
109+
110+
@property
111+
def icon(self) -> str | None:
112+
"""Return dynamic icon based on state."""
113+
# Door sensors - dynamic icon based on state
114+
if self.descriptor and self.descriptor in DOOR_DESCRIPTORS:
115+
return "mdi:car-door"
116+
117+
# Door non Door sensors - dynamic icon based on state
118+
if self.descriptor and self.descriptor in DOOR_NON_DOOR_DESCRIPTORS:
119+
is_open = getattr(self, "_attr_is_on", False)
120+
if is_open:
121+
return "mdi:circle-outline"
122+
else:
123+
return "mdi:circle"
124+
125+
# Return existing icon attribute if set
126+
return getattr(self, "_attr_icon", None)
127+
101128

129+
''' For future options and colors
130+
@property
131+
def extra_state_attributes(self) -> dict[str, Any]:
132+
"""Return extra attributes."""
133+
attrs = super().extra_state_attributes or {}
134+
135+
# Add color hint for window sensors
136+
descriptor_lower = self._descriptor.lower()
137+
if "window" in descriptor_lower:
138+
value = str(self._attr_native_value).lower() if self._attr_native_value else ""
139+
if "open" in value:
140+
attrs["color_hint"] = "red"
141+
elif "closed" in value:
142+
attrs["color_hint"] = "green"
143+
else:
144+
attrs["color_hint"] = "orange"
145+
146+
return attrs
147+
'''
102148

103149
async def async_setup_entry(
104150
hass: HomeAssistant, entry: ConfigEntry, async_add_entities

custom_components/cardata/device_tracker.py

Lines changed: 42 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -228,19 +228,13 @@ def _process_coordinate_pair(self) -> None:
228228

229229
# Wait until both coordinates exist
230230
if lat is None or lon is None:
231-
_LOGGER.debug(
232-
"Waiting for coordinate pair for %s (lat=%s, lon=%s)",
233-
self._vin,
234-
"present" if lat is not None else "missing",
235-
"present" if lon is not None else "missing"
236-
)
237231
return
238232

239233
# Calculate time difference and ages
240234
time_diff = abs(lat_time - lon_time)
241235
lat_age = now - lat_time
242236
lon_age = now - lon_time
243-
237+
244238
# Discard if both coordinates are very stale
245239
if lat_age > self._MAX_STALE_TIME and lon_age > self._MAX_STALE_TIME:
246240
_LOGGER.debug(
@@ -251,78 +245,39 @@ def _process_coordinate_pair(self) -> None:
251245
)
252246
return
253247

254-
# Determine which coordinate is fresher
255-
lat_is_newer = lat_time > lon_time
256-
newer_age = lat_age if lat_is_newer else lon_age
257-
older_age = lon_age if lat_is_newer else lat_age
258-
248+
# CRITICAL: Only accept coordinates that arrived close together
249+
if time_diff > self._PAIR_WINDOW:
250+
_LOGGER.debug(
251+
"Coordinates too far apart for %s (Δt=%.1fs > %.1fs window) - waiting for pair",
252+
self._vin,
253+
time_diff,
254+
self._PAIR_WINDOW
255+
)
256+
return
257+
258+
# Final coordinates (may be smoothed)
259+
final_lat = lat
260+
final_lon = lon
261+
259262
# Check if coordinates changed from previous position
260-
lat_changed = False
261-
lon_changed = False
263+
lat_changed = True
264+
lon_changed = True
262265
if self._current_lat is not None and self._current_lon is not None:
263266
lat_changed = abs(lat - self._current_lat) > self._COORD_PRECISION
264267
lon_changed = abs(lon - self._current_lon) > self._COORD_PRECISION
265268

266-
# Decide how to handle the coordinate pair
267-
final_lat = lat
268-
final_lon = lon
269-
update_reason = None
270-
271-
# Case 1: Both coordinates arrived close together (ideal case)
272-
if time_diff <= self._PAIR_WINDOW:
273-
if lat_changed or lon_changed:
274-
update_reason = f"paired update (Δt={time_diff:.1f}s)"
275-
else:
276-
_LOGGER.debug("Ignoring update for %s - no movement detected", self._vin)
277-
return
278-
279-
# Case 2: Coordinates arrived far apart - handle stale coordinate
280-
elif time_diff <= self._MAX_DELAY:
281-
# One coordinate is fresher, the other is stale but not too old
282-
if lat_changed and lon_changed:
283-
# Both changed - accept the pair even though timing is off
284-
update_reason = f"delayed pair (Δt={time_diff:.1f}s, both changed)"
285-
elif lat_changed and not lon_changed:
286-
# Only lat changed - use new lat with old lon (keep last known lon)
287-
update_reason = f"lat update (lon unchanged, Δt={time_diff:.1f}s)"
288-
elif lon_changed and not lat_changed:
289-
# Only lon changed - use new lon with old lat (keep last known lat)
290-
update_reason = f"lon update (lat unchanged, Δt={time_diff:.1f}s)"
291-
else:
269+
if not lat_changed and not lon_changed:
292270
_LOGGER.debug("Ignoring update for %s - no movement detected", self._vin)
293271
return
294-
295-
# Case 3: One coordinate is too stale (> _MAX_DELAY)
296-
else:
297-
# Use the fresher coordinate with the last known value of the stale one
298-
if self._current_lat is not None and self._current_lon is not None:
299-
if lat_is_newer:
300-
if lat_changed:
301-
# Use new lat, keep old lon from restored position
302-
final_lon = self._current_lon
303-
update_reason = f"lat update (lon stale {older_age:.1f}s, using last known)"
304-
else:
305-
_LOGGER.debug("Ignoring update for %s - lat unchanged and lon too stale", self._vin)
306-
return
307-
else:
308-
if lon_changed:
309-
# Use new lon, keep old lat from restored position
310-
final_lat = self._current_lat
311-
update_reason = f"lon update (lat stale {older_age:.1f}s, using last known)"
312-
else:
313-
_LOGGER.debug("Ignoring update for %s - lon unchanged and lat too stale", self._vin)
314-
return
315-
else:
316-
# No previous position - accept even with stale coordinate
317-
update_reason = f"initial position (Δt={time_diff:.1f}s)"
318-
272+
319273
# Apply movement threshold check
320-
if self._current_lat is not None and self._current_lon is not None and update_reason:
274+
update_reason = None
275+
if self._current_lat is not None and self._current_lon is not None:
321276
distance = self._calculate_distance(
322277
self._current_lat, self._current_lon,
323278
final_lat, final_lon
324279
)
325-
280+
326281
if distance < self._MIN_MOVEMENT_DISTANCE:
327282
_LOGGER.debug(
328283
"Ignoring update for %s - movement too small (%.1fm < %dm threshold)",
@@ -331,37 +286,30 @@ def _process_coordinate_pair(self) -> None:
331286
self._MIN_MOVEMENT_DISTANCE
332287
)
333288
return
334-
335-
# Add distance to update reason for logging
336-
update_reason = f"{update_reason}, moved {distance:.1f}m"
337289

338-
# Apply smoothing to reduce GPS jitter (optional)
339-
if (self._SMOOTHING_FACTOR > 0 and
340-
self._current_lat is not None and
341-
self._current_lon is not None and
342-
update_reason and "initial" not in update_reason):
343-
344-
# Weighted average: new_value = (1 - factor) * new + factor * old
345-
# factor=0 means no smoothing (use new value)
346-
# factor=1 means full smoothing (keep old value)
347-
smoothed_lat = (1 - self._SMOOTHING_FACTOR) * final_lat + self._SMOOTHING_FACTOR * self._current_lat
348-
smoothed_lon = (1 - self._SMOOTHING_FACTOR) * final_lon + self._SMOOTHING_FACTOR * self._current_lon
290+
update_reason = f"paired update (Δt={time_diff:.1f}s, moved {distance:.1f}m)"
291+
292+
# Apply smoothing to reduce GPS jitter (optional)
293+
if self._SMOOTHING_FACTOR > 0:
294+
smoothed_lat = (1 - self._SMOOTHING_FACTOR) * final_lat + self._SMOOTHING_FACTOR * self._current_lat
295+
smoothed_lon = (1 - self._SMOOTHING_FACTOR) * final_lon + self._SMOOTHING_FACTOR * self._current_lon
349296

350-
_LOGGER.debug(
351-
"Applying smoothing for %s (factor=%.1f): raw=(%.6f, %.6f) -> smoothed=(%.6f, %.6f)",
352-
self._vin,
353-
self._SMOOTHING_FACTOR,
354-
final_lat, final_lon,
355-
smoothed_lat, smoothed_lon
356-
)
297+
_LOGGER.debug(
298+
"Applying smoothing for %s (factor=%.1f): raw=(%.6f, %.6f) -> smoothed=(%.6f, %.6f)",
299+
self._vin,
300+
self._SMOOTHING_FACTOR,
301+
final_lat, final_lon,
302+
smoothed_lat, smoothed_lon
303+
)
357304

358-
final_lat = smoothed_lat
359-
final_lon = smoothed_lon
360-
update_reason = f"{update_reason}, smoothed"
361-
305+
final_lat = smoothed_lat
306+
final_lon = smoothed_lon
307+
update_reason = f"{update_reason}, smoothed"
308+
else:
309+
update_reason = f"initial position (Δt={time_diff:.1f}s)"
310+
362311
# Update the tracker position
363-
if update_reason:
364-
self._apply_new_coordinates(final_lat, final_lon, update_reason)
312+
self._apply_new_coordinates(final_lat, final_lon, update_reason)
365313

366314
def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
367315
"""Calculate distance between two GPS coordinates in meters using Haversine formula."""

custom_components/cardata/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"documentation": "https://github.com/kvanbiesen/bmw-cardata-ha/",
1010
"iot_class": "cloud_push",
1111
"requirements": [],
12-
"version": "4.1.5",
12+
"version": "4.1.6",
1313
"integration_type": "device"
1414
}

custom_components/cardata/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def get_images_directory(hass: HomeAssistant) -> Path:
2727
Returns Path to: /config/media/cardata/
2828
Creates directory if it doesn't exist.
2929
"""
30-
images_dir = Path(hass.config.path("media", "cardata"))
30+
images_dir = Path(hass.config.path("www/community/cardata"))
3131
images_dir.mkdir(parents=True, exist_ok=True)
3232
return images_dir
3333

custom_components/cardata/sensor.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,14 @@ def _handle_update(self, vin: str, descriptor: str) -> None:
283283
@property
284284
def icon(self) -> str | None:
285285
"""Return dynamic icon based on state."""
286+
if self.descriptor and self.descriptor =="vehicle.cabin.door.status":
287+
value = str(self._attr_native_value).lower() if self._attr_native_value else ""
288+
if "unlocked" in value:
289+
return "mdi:lock-open-variant-outline"
290+
else:
291+
return "mdi:lock-outline"
292+
286293
# Window sensors - dynamic icon based on state
287-
descriptor_lower = self._descriptor.lower()
288294
if self.descriptor and self.descriptor in WINDOW_DESCRIPTORS:
289295
value = str(self._attr_native_value).lower() if self._attr_native_value else ""
290296
if "open" in value:
@@ -297,6 +303,26 @@ def icon(self) -> str | None:
297303
# Return existing icon attribute if set
298304
return getattr(self, "_attr_icon", None)
299305

306+
''' For future options and colors
307+
@property
308+
def extra_state_attributes(self) -> dict[str, Any]:
309+
"""Return extra attributes."""
310+
attrs = super().extra_state_attributes or {}
311+
312+
# Add color hint for window sensors
313+
descriptor_lower = self._descriptor.lower()
314+
if "window" in descriptor_lower:
315+
value = str(self._attr_native_value).lower() if self._attr_native_value else ""
316+
if "open" in value:
317+
attrs["color_hint"] = "red"
318+
elif "closed" in value:
319+
attrs["color_hint"] = "green"
320+
else:
321+
attrs["color_hint"] = "orange"
322+
323+
return attrs
324+
'''
325+
300326
class CardataDiagnosticsSensor(SensorEntity, RestoreEntity):
301327
"""Diagnostic sensor for connection, quota, and polling info."""
302328

hacs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"repository": "kvanbiesen/bmw-cardata-ha",
44
"homeassistant": "2024.6.0",
55
"zip_release": false,
6-
"version": "4.1.5"
6+
"version": "4.1.6"
77
}

0 commit comments

Comments
 (0)