Keep sleepy xiaomi-ble devices that don't broadcast regularly available (#87654)

Co-authored-by: J. Nick Koston <nick@koston.org>
fixes undefined
This commit is contained in:
Ernst Klamer 2023-02-09 23:42:08 +01:00 committed by GitHub
parent 4cebc767b5
commit 9dd806278b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 150 additions and 20 deletions

View File

@ -3,8 +3,7 @@ from __future__ import annotations
import logging
from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData
from xiaomi_ble.parser import EncryptionScheme
from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData
from homeassistant import config_entries
from homeassistant.components.bluetooth import (

View File

@ -1,6 +1,7 @@
"""Support for Xiaomi binary sensors."""
from __future__ import annotations
from xiaomi_ble import SLEEPY_DEVICE_MODELS
from xiaomi_ble.parser import (
BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass,
ExtendedBinarySensorDeviceClass,
@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.const import ATTR_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity(
def is_on(self) -> bool | 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."""
if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS:
# These devices sleep for an indeterminate amount of time
# so there is no way to track their availability.
return True
return super().available

View File

@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==0.16.1"]
"requirements": ["xiaomi-ble==0.16.3"]
}

View File

@ -2644,7 +2644,7 @@ xbox-webapi==2.0.11
xboxapi==2.0.1
# homeassistant.components.xiaomi_ble
xiaomi-ble==0.16.1
xiaomi-ble==0.16.3
# homeassistant.components.knx
xknx==2.4.0

View File

@ -1869,7 +1869,7 @@ wolf_smartset==0.1.11
xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble
xiaomi-ble==0.16.1
xiaomi-ble==0.16.3
# homeassistant.components.knx
xknx==2.4.0

View File

@ -1,12 +1,29 @@
"""Test Xiaomi binary sensors."""
from datetime import timedelta
import time
from unittest.mock import patch
from homeassistant.components.bluetooth import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
)
from homeassistant.components.xiaomi_ble.const import DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from . import make_advertisement
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info_bleak
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import (
inject_bluetooth_service_info_bleak,
patch_all_discovered_devices,
)
async def test_door_problem_sensors(hass: HomeAssistant) -> None:
@ -34,19 +51,19 @@ async def test_door_problem_sensors(hass: HomeAssistant) -> None:
door_sensor = hass.states.get("binary_sensor.door_lock_be98_door")
door_sensor_attribtes = door_sensor.attributes
assert door_sensor.state == "off"
assert door_sensor.state == STATE_OFF
assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door"
door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open")
door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off"
assert door_left_open.state == STATE_OFF
assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open"
)
pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door")
pry_the_door_attribtes = pry_the_door.attributes
assert pry_the_door.state == "off"
assert pry_the_door.state == STATE_OFF
assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -77,12 +94,12 @@ async def test_light_motion(hass: HomeAssistant) -> None:
motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion")
motion_sensor_attribtes = motion_sensor.attributes
assert motion_sensor.state == "on"
assert motion_sensor.state == STATE_ON
assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion"
light_sensor = hass.states.get("binary_sensor.nightlight_9321_light")
light_sensor_attribtes = light_sensor.attributes
assert light_sensor.state == "off"
assert light_sensor.state == STATE_OFF
assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -116,7 +133,7 @@ async def test_moisture(hass: HomeAssistant) -> None:
sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture")
sensor_attr = sensor.attributes
assert sensor.state == "on"
assert sensor.state == STATE_ON
assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture"
assert await hass.config_entries.async_unload(entry.entry_id)
@ -148,12 +165,12 @@ async def test_opening(hass: HomeAssistant) -> None:
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "on"
assert opening_sensor.state == STATE_ON
assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening"
)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@ -183,7 +200,7 @@ async def test_opening_problem_sensors(hass: HomeAssistant) -> None:
opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening")
opening_sensor_attribtes = opening_sensor.attributes
assert opening_sensor.state == "off"
assert opening_sensor.state == STATE_OFF
assert (
opening_sensor_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Opening"
@ -193,7 +210,7 @@ async def test_opening_problem_sensors(hass: HomeAssistant) -> None:
"binary_sensor.door_window_sensor_e567_door_left_open"
)
door_left_open_attribtes = door_left_open.attributes
assert door_left_open.state == "off"
assert door_left_open.state == STATE_OFF
assert (
door_left_open_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Door left open"
@ -203,7 +220,7 @@ async def test_opening_problem_sensors(hass: HomeAssistant) -> None:
"binary_sensor.door_window_sensor_e567_device_forcibly_removed"
)
device_forcibly_removed_attribtes = device_forcibly_removed.attributes
assert device_forcibly_removed.state == "off"
assert device_forcibly_removed.state == STATE_OFF
assert (
device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME]
== "Door/Window Sensor E567 Device forcibly removed"
@ -238,8 +255,111 @@ async def test_smoke(hass: HomeAssistant) -> None:
smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke")
smoke_sensor_attribtes = smoke_sensor.attributes
assert smoke_sensor.state == "on"
assert smoke_sensor.state == STATE_ON
assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke"
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
async def test_unavailable(hass):
"""Test normal device goes to unavailable after 60 minutes."""
start_monotonic = time.monotonic()
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="A4:C1:38:66:E5:67",
data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"},
)
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"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5",
),
)
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")
# Normal devices should go to unavailable
assert opening_sensor.state == STATE_UNAVAILABLE
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
async def test_sleepy_device(hass):
"""Test sleepy device does not go to unavailable after 60 minutes."""
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()