mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
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:
parent
4cebc767b5
commit
9dd806278b
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user