diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 3a40e1baa09..99bcf83ec1a 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -87,7 +87,9 @@ async def async_setup_entry( QingpingBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) class QingpingBluetoothSensorEntity( diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 4ee1db90c6f..bc99ed80ff3 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -155,7 +155,9 @@ async def async_setup_entry( QingpingBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class QingpingBluetoothSensorEntity( diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index 5733f4f145b..78752372baa 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -1,12 +1,27 @@ """Test the Qingping 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.qingping.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from . import LIGHT_AND_SIGNAL_SERVICE_INFO +from . import LIGHT_AND_SIGNAL_SERVICE_INFO, NO_DATA_SERVICE_INFO -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) async def test_binary_sensors(hass: HomeAssistant) -> None: @@ -31,3 +46,58 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: + """Test setting up creates the binary sensors and restoring state.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + 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("binary_sensor")) == 0 + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("binary_sensor")) == 1 + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_OFF + assert motion_sensor.attributes[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Motion" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # 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() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Device is no longer available because its not in range + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_UNAVAILABLE + + # Device is back in range + + inject_bluetooth_service_info(hass, NO_DATA_SERVICE_INFO) + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == STATE_OFF diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py index d80522f47c9..2fedbba9e5c 100644 --- a/tests/components/qingping/test_sensor.py +++ b/tests/components/qingping/test_sensor.py @@ -1,13 +1,28 @@ """Test the Qingping 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.qingping.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from . import LIGHT_AND_SIGNAL_SERVICE_INFO +from . import LIGHT_AND_SIGNAL_SERVICE_INFO, NO_DATA_SERVICE_INFO -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + patch_all_discovered_devices, +) async def test_sensors(hass: HomeAssistant) -> None: @@ -35,3 +50,60 @@ async def test_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: + """Test setting up creates the binary sensors and restoring state.""" + start_monotonic = time.monotonic() + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + 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("sensor")) == 0 + inject_bluetooth_service_info(hass, LIGHT_AND_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + lux_sensor_attrs = lux_sensor.attributes + assert lux_sensor.state == "13" + assert lux_sensor_attrs[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Illuminance" + assert lux_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert lux_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # 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() + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Device is no longer available because its not in range + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + assert lux_sensor.state == STATE_UNAVAILABLE + + # Device is back in range + + inject_bluetooth_service_info(hass, NO_DATA_SERVICE_INFO) + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + assert lux_sensor.state == "13"