From 372ea19d39a6f6227f8a271f8ec8afd7d1163f50 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 20 Nov 2024 09:17:29 -0600 Subject: [PATCH 1/2] Use UTC datetimes internally (#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. --- custom_components/composite/device_tracker.py | 30 ++++++++++++------- custom_components/composite/manifest.json | 4 +-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/custom_components/composite/device_tracker.py b/custom_components/composite/device_tracker.py index 16d6e7c..c53c792 100644 --- a/custom_components/composite/device_tracker.py +++ b/custom_components/composite/device_tracker.py @@ -378,8 +378,13 @@ async def _restore_state(self) -> None: last_seen = dt_util.parse_datetime( self._attr_extra_state_attributes[ATTR_LAST_SEEN] ) - self._attr_extra_state_attributes[ATTR_LAST_SEEN] = last_seen - self._prev_seen = last_seen + if last_seen is None: + self._attr_extra_state_attributes[ATTR_LAST_SEEN] = None + else: + self._attr_extra_state_attributes[ATTR_LAST_SEEN] = dt_util.as_local( + last_seen + ) + self._prev_seen = dt_util.as_utc(last_seen) if self.source_type in _SOURCE_TYPE_NON_GPS and ( self.latitude is None or self.longitude is None ): @@ -410,21 +415,21 @@ async def _entity_updated( # noqa: C901 # attributes defined by _LAST_SEEN_ATTRS, as a datetime. def get_last_seen() -> datetime | None: - """Get last_seen from one of the possible attributes.""" + """Get last_seen (in UTC) from one of the possible attributes.""" if (raw_last_seen := new_attrs.get(_LAST_SEEN_ATTRS)) is None: return None if isinstance(raw_last_seen, datetime): - return raw_last_seen + return dt_util.as_utc(raw_last_seen) with suppress(TypeError, ValueError): return dt_util.utc_from_timestamp(float(raw_last_seen)) with suppress(TypeError): - return dt_util.parse_datetime(raw_last_seen) + if (parsed_last_seen := dt_util.parse_datetime(raw_last_seen)) is None: + return None + return dt_util.as_utc(parsed_last_seen) return None - # Make sure last_seen is timezone aware in local timezone. - # Note that dt_util.as_local assumes naive datetime is in local timezone. # Use last_updated from the new state object if no valid "last seen" was found. - last_seen = dt_util.as_local(get_last_seen() or new_state.last_updated) + last_seen = get_last_seen() or new_state.last_updated old_last_seen = entity.seen if old_last_seen and last_seen < old_last_seen: @@ -549,8 +554,8 @@ def get_last_seen() -> datetime | None: "last_seen not newer than previous update (%s) <= (%s)", self.entity_id, entity_id, - last_seen, - self._prev_seen, + dt_util.as_local(last_seen), + dt_util.as_local(self._prev_seen), ) return @@ -563,7 +568,7 @@ def get_last_seen() -> datetime | None: if _entity.source_type ), ATTR_LAST_ENTITY_ID: entity_id, - ATTR_LAST_SEEN: _nearest_second(last_seen), + ATTR_LAST_SEEN: dt_util.as_local(_nearest_second(last_seen)), } if charging is not None: attrs[ATTR_BATTERY_CHARGING] = charging @@ -617,6 +622,9 @@ def _set_state( assert attributes last_ent = cast(str, attributes[ATTR_LAST_ENTITY_ID]) last_seen = cast(datetime, attributes[ATTR_LAST_SEEN]) + # It's ok that last_seen is in local tz and self._prev_seen is in UTC. + # last_seen's value will automatically be converted to UTC during the + # subtraction operation. seconds = (last_seen - self._prev_seen).total_seconds() min_seconds = MIN_SPEED_SECONDS if last_ent != prev_ent: diff --git a/custom_components/composite/manifest.json b/custom_components/composite/manifest.json index 4fc77e5..7c9b3ba 100644 --- a/custom_components/composite/manifest.json +++ b/custom_components/composite/manifest.json @@ -4,9 +4,9 @@ "codeowners": ["@pnbruckner"], "config_flow": true, "dependencies": ["file_upload"], - "documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.4/README.md", + "documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.5b0/README.md", "iot_class": "local_polling", "issue_tracker": "https://github.com/pnbruckner/ha-composite-tracker/issues", "requirements": ["filetype==1.2.0"], - "version": "3.4.4" + "version": "3.4.5b0" } From 3429c56a624ca9b03c2f195cbc3cbea70659f709 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 20 Nov 2024 09:19:17 -0600 Subject: [PATCH 2/2] Bump version to 3.4.5 --- custom_components/composite/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/composite/manifest.json b/custom_components/composite/manifest.json index 7c9b3ba..64bf124 100644 --- a/custom_components/composite/manifest.json +++ b/custom_components/composite/manifest.json @@ -4,9 +4,9 @@ "codeowners": ["@pnbruckner"], "config_flow": true, "dependencies": ["file_upload"], - "documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.5b0/README.md", + "documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/3.4.5/README.md", "iot_class": "local_polling", "issue_tracker": "https://github.com/pnbruckner/ha-composite-tracker/issues", "requirements": ["filetype==1.2.0"], - "version": "3.4.5b0" + "version": "3.4.5" }