From fb12c237ab29c213d5c63a6fc2cc2cc9daf8c06b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Aug 2023 07:58:27 -1000 Subject: [PATCH] Restore xiaomi_ble state at start when device is in range or sleepy (#97979) --- .../components/xiaomi_ble/binary_sensor.py | 4 +- homeassistant/components/xiaomi_ble/sensor.py | 4 +- .../xiaomi_ble/test_binary_sensor.py | 61 +++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 65 ++++++++++++++++++- 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 5490676ad1a..2894b8d2f3f 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -122,7 +122,9 @@ async def async_setup_entry( XiaomiBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) class XiaomiBluetoothSensorEntity( diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 56bfbb1b020..cdb7b3a8fd8 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -195,7 +195,9 @@ async def async_setup_entry( XiaomiBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class XiaomiBluetoothSensorEntity( diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 5dd1b965f25..32d1fea7f62 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -367,3 +367,64 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.data[CONF_SLEEPY_DEVICE] is True + + +async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: + """Test sleepy device does not go to unavailable after 60 minutes and restores state.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"@0\xd6\x03$\x19\x10\x01\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward 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() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time + assert opening_sensor.state == STATE_ON + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time and restore it + assert opening_sensor.state == STATE_ON + + assert entry.data[CONF_SLEEPY_DEVICE] is True diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index a2b0e62821a..b0ddd99a7c2 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.components.xiaomi_ble.const import CONF_SLEEPY_DEVICE, DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -713,7 +713,7 @@ async def test_unavailable(hass: HomeAssistant) -> None: async def test_sleepy_device(hass: HomeAssistant) -> None: - """Test normal device goes to unavailable after 60 minutes.""" + """Test sleepy devices stay available.""" start_monotonic = time.monotonic() entry = MockConfigEntry( @@ -759,3 +759,64 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: + """Test sleepy devices stay available.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + assert mass_non_stabilized_sensor.state == "86.55" + + # Fastforward 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() + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + + # Sleepy devices should keep their state over time + assert mass_non_stabilized_sensor.state == "86.55" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + + # Sleepy devices should keep their state over time and restore it + assert mass_non_stabilized_sensor.state == "86.55" + + assert entry.data[CONF_SLEEPY_DEVICE] is True