From 531c7f6a9d26b5e4f0fffad278b5989e69839d3f Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Thu, 16 Jul 2026 21:36:22 +0200 Subject: [PATCH] Keep last seen sensor available after first value --- custom_components/opendisplay/sensor.py | 19 ++++++++++++++-- tests/test_last_seen.py | 29 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/custom_components/opendisplay/sensor.py b/custom_components/opendisplay/sensor.py index 8107ed0..d5ab26a 100644 --- a/custom_components/opendisplay/sensor.py +++ b/custom_components/opendisplay/sensor.py @@ -141,6 +141,20 @@ def native_value(self) -> float | int | str | datetime | None: class OpenDisplayLastSeenSensor(OpenDisplaySensorEntity): """last_seen sourced from the bluetooth stack, not the gated callback.""" + def __init__( + self, + coordinator, + description: OpenDisplaySensorEntityDescription, + ) -> None: + """Initialize the last_seen sensor.""" + super().__init__(coordinator, description) + self._last_seen: datetime | None = None + + @property + def available(self) -> bool: + """Stay available once a last_seen value has been observed.""" + return self._last_seen is not None or super().available + @property def native_value(self) -> datetime | None: # connectable=False matches the Bluetooth advertisement monitor's @@ -150,8 +164,9 @@ def native_value(self) -> datetime | None: self.hass, self.coordinator.address, connectable=False ) if info is None: - return None + return self._last_seen # info.time is a monotonic clock (monotonic_time_coarse); convert to # wall time with the same offset the advertisement monitor uses. wall = info.time + (time.time() - time.monotonic()) - return datetime.fromtimestamp(wall, tz=timezone.utc) + self._last_seen = datetime.fromtimestamp(wall, tz=timezone.utc) + return self._last_seen diff --git a/tests/test_last_seen.py b/tests/test_last_seen.py index 94af071..c9682e3 100644 --- a/tests/test_last_seen.py +++ b/tests/test_last_seen.py @@ -59,6 +59,35 @@ def test_native_value_none_when_no_service_info(): assert entity.native_value is None +def test_last_seen_available_after_first_value_when_coordinator_unavailable(): + entity = _make_sensor() + entity.coordinator.available = False + mono = time.monotonic() + + with patch.object( + sensor_mod, + "async_last_service_info", + return_value=SimpleNamespace(time=mono), + ): + value = entity.native_value + + assert value is not None + assert entity.available is True + + with patch.object(sensor_mod, "async_last_service_info", return_value=None): + assert entity.native_value == value + assert entity.available is True + + +def test_last_seen_unavailable_before_first_value_when_coordinator_unavailable(): + entity = _make_sensor() + entity.coordinator.available = False + + with patch.object(sensor_mod, "async_last_service_info", return_value=None): + assert entity.native_value is None + assert entity.available is False + + if __name__ == "__main__": import pytest