Skip to content

Commit 372ea19

Browse files
authored
Use UTC datetimes internally (pnbruckner#84)
Python datetime object comparisons and math don't work as expected when the objects are aware and have the same tzinfo attribute. Basically, the fold attribute is ignored in this case, which can lead to wrong results. Avoid this problem by using aware times in UTC internally. Only use local time zone for user visible attributes.
1 parent aa45575 commit 372ea19

File tree

2 files changed

+21
-13
lines changed

2 files changed

+21
-13
lines changed

custom_components/composite/device_tracker.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,13 @@ async def _restore_state(self) -> None:
378378
last_seen = dt_util.parse_datetime(
379379
self._attr_extra_state_attributes[ATTR_LAST_SEEN]
380380
)
381-
self._attr_extra_state_attributes[ATTR_LAST_SEEN] = last_seen
382-
self._prev_seen = last_seen
381+
if last_seen is None:
382+
self._attr_extra_state_attributes[ATTR_LAST_SEEN] = None
383+
else:
384+
self._attr_extra_state_attributes[ATTR_LAST_SEEN] = dt_util.as_local(
385+
last_seen
386+
)
387+
self._prev_seen = dt_util.as_utc(last_seen)
383388
if self.source_type in _SOURCE_TYPE_NON_GPS and (
384389
self.latitude is None or self.longitude is None
385390
):
@@ -410,21 +415,21 @@ async def _entity_updated( # noqa: C901
410415
# attributes defined by _LAST_SEEN_ATTRS, as a datetime.
411416

412417
def get_last_seen() -> datetime | None:
413-
"""Get last_seen from one of the possible attributes."""
418+
"""Get last_seen (in UTC) from one of the possible attributes."""
414419
if (raw_last_seen := new_attrs.get(_LAST_SEEN_ATTRS)) is None:
415420
return None
416421
if isinstance(raw_last_seen, datetime):
417-
return raw_last_seen
422+
return dt_util.as_utc(raw_last_seen)
418423
with suppress(TypeError, ValueError):
419424
return dt_util.utc_from_timestamp(float(raw_last_seen))
420425
with suppress(TypeError):
421-
return dt_util.parse_datetime(raw_last_seen)
426+
if (parsed_last_seen := dt_util.parse_datetime(raw_last_seen)) is None:
427+
return None
428+
return dt_util.as_utc(parsed_last_seen)
422429
return None
423430

424-
# Make sure last_seen is timezone aware in local timezone.
425-
# Note that dt_util.as_local assumes naive datetime is in local timezone.
426431
# Use last_updated from the new state object if no valid "last seen" was found.
427-
last_seen = dt_util.as_local(get_last_seen() or new_state.last_updated)
432+
last_seen = get_last_seen() or new_state.last_updated
428433

429434
old_last_seen = entity.seen
430435
if old_last_seen and last_seen < old_last_seen:
@@ -549,8 +554,8 @@ def get_last_seen() -> datetime | None:
549554
"last_seen not newer than previous update (%s) <= (%s)",
550555
self.entity_id,
551556
entity_id,
552-
last_seen,
553-
self._prev_seen,
557+
dt_util.as_local(last_seen),
558+
dt_util.as_local(self._prev_seen),
554559
)
555560
return
556561

@@ -563,7 +568,7 @@ def get_last_seen() -> datetime | None:
563568
if _entity.source_type
564569
),
565570
ATTR_LAST_ENTITY_ID: entity_id,
566-
ATTR_LAST_SEEN: _nearest_second(last_seen),
571+
ATTR_LAST_SEEN: dt_util.as_local(_nearest_second(last_seen)),
567572
}
568573
if charging is not None:
569574
attrs[ATTR_BATTERY_CHARGING] = charging
@@ -617,6 +622,9 @@ def _set_state(
617622
assert attributes
618623
last_ent = cast(str, attributes[ATTR_LAST_ENTITY_ID])
619624
last_seen = cast(datetime, attributes[ATTR_LAST_SEEN])
625+
# It's ok that last_seen is in local tz and self._prev_seen is in UTC.
626+
# last_seen's value will automatically be converted to UTC during the
627+
# subtraction operation.
620628
seconds = (last_seen - self._prev_seen).total_seconds()
621629
min_seconds = MIN_SPEED_SECONDS
622630
if last_ent != prev_ent:

custom_components/composite/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"codeowners": ["@pnbruckner"],
55
"config_flow": true,
66
"dependencies": ["file_upload"],
7-
"documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.4/README.md",
7+
"documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.5b0/README.md",
88
"iot_class": "local_polling",
99
"issue_tracker": "https://github.com/pnbruckner/ha-composite-tracker/issues",
1010
"requirements": ["filetype==1.2.0"],
11-
"version": "3.4.4"
11+
"version": "3.4.5b0"
1212
}

0 commit comments

Comments
 (0)