From aebded049bb9972810d04b48f70b2c304b602efe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 May 2023 16:32:24 -0500 Subject: [PATCH] Mark oralb devices as sleepy (#93250) --- homeassistant/components/oralb/sensor.py | 17 ++++++++++ tests/components/oralb/test_sensor.py | 41 ++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 4a63fccb88c..76104c75164 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -124,3 +124,20 @@ class OralBBluetoothSensorEntity( def native_value(self) -> str | int | None: """Return the native value.""" return self.processor.entity_data.get(self.entity_key) + + @property + def available(self) -> bool: + """Return True if entity is available. + + The sensor is only created when the device is seen. + + Since these are sleepy devices which stop broadcasting + when not in use, we can't rely on the last update time + so once we have seen the device we always return True. + """ + return True + + @property + def assumed_state(self) -> bool: + """Return True if the device is no longer broadcasting.""" + return not self.processor.available diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 8c7bacce234..d43997fe7ed 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -1,8 +1,17 @@ """Test the OralB sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + async_address_present, +) from homeassistant.components.oralb.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import ( ORALB_IO_SERIES_4_SERVICE_INFO, @@ -10,10 +19,11 @@ from . import ( ORALB_SERVICE_INFO, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.bluetooth import ( inject_bluetooth_service_info, inject_bluetooth_service_info_bleak, + patch_all_discovered_devices, ) @@ -44,6 +54,7 @@ async def test_sensors( toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "Smart Series 7000 48BE Toothbrush State" ) + assert ATTR_ASSUMED_STATE not in toothbrush_sensor_attrs assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -53,6 +64,8 @@ async def test_sensors_io_series_4( hass: HomeAssistant, entity_registry_enabled_by_default: None ) -> None: """Test setting up creates the sensors with an io series 4.""" + start_monotonic = time.monotonic() + entry = MockConfigEntry( domain=DOMAIN, unique_id=ORALB_IO_SERIES_4_SERVICE_INFO.address, @@ -71,6 +84,30 @@ async def test_sensors_io_series_4( toothbrush_sensor_attrs = toothbrush_sensor.attributes assert toothbrush_sensor.state == "gum care" assert toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "IO Series 4 48BE Mode" + assert ATTR_ASSUMED_STATE not in toothbrush_sensor_attrs + + # Fast-forward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + assert ( + async_address_present(hass, ORALB_IO_SERIES_4_SERVICE_INFO.address) is False + ) + + toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") + # Sleepy devices should keep their state over time + assert toothbrush_sensor.state == "gum care" + toothbrush_sensor_attrs = toothbrush_sensor.attributes + assert toothbrush_sensor_attrs[ATTR_ASSUMED_STATE] is True assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done()