Fix startup race in BLE integrations (#75780)

This commit is contained in:
J. Nick Koston 2022-07-26 09:29:23 -10:00 committed by GitHub
parent 157f7292d7
commit 1e85ddabfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 54 additions and 32 deletions

View File

@ -42,17 +42,6 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
super()._async_handle_unavailable(address) super()._async_handle_unavailable(address)
self.async_update_listeners() self.async_update_listeners()
@callback
def async_start(self) -> CALLBACK_TYPE:
"""Start the data updater."""
self._async_start()
@callback
def _async_cancel() -> None:
self._async_stop()
return _async_cancel
@callback @callback
def async_add_listener( def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None self, update_callback: CALLBACK_TYPE, context: Any = None

View File

@ -78,21 +78,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
def remove_processor() -> None: def remove_processor() -> None:
"""Remove a processor.""" """Remove a processor."""
self._processors.remove(processor) self._processors.remove(processor)
self._async_handle_processors_changed()
self._processors.append(processor) self._processors.append(processor)
self._async_handle_processors_changed()
return remove_processor return remove_processor
@callback
def _async_handle_processors_changed(self) -> None:
"""Handle processors changed."""
running = bool(self._cancel_bluetooth_advertisements)
if running and not self._processors:
self._async_stop()
elif not running and self._processors:
self._async_start()
@callback @callback
def _async_handle_unavailable(self, address: str) -> None: def _async_handle_unavailable(self, address: str) -> None:
"""Handle the device going unavailable.""" """Handle the device going unavailable."""

View File

@ -38,6 +38,17 @@ class BasePassiveBluetoothCoordinator:
self._present = False self._present = False
self.last_seen = 0.0 self.last_seen = 0.0
@callback
def async_start(self) -> CALLBACK_TYPE:
"""Start the data updater."""
self._async_start()
@callback
def _async_cancel() -> None:
self._async_stop()
return _async_cancel
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if the device is available.""" """Return if the device is available."""

View File

@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Govee BLE device from a config entry.""" """Set up Govee BLE device from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
hass, hass,
@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address=address, address=address,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True

View File

@ -135,12 +135,12 @@ async def async_setup_entry(
data.update(service_info) data.update(service_info)
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
GoveeBluetoothSensorEntity, async_add_entities GoveeBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
class GoveeBluetoothSensorEntity( class GoveeBluetoothSensorEntity(

View File

@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up INKBIRD BLE device from a config entry.""" """Set up INKBIRD BLE device from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
hass, hass,
@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address=address, address=address,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True

View File

@ -135,12 +135,12 @@ async def async_setup_entry(
data.update(service_info) data.update(service_info)
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
INKBIRDBluetoothSensorEntity, async_add_entities INKBIRDBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
class INKBIRDBluetoothSensorEntity( class INKBIRDBluetoothSensorEntity(

View File

@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Moat BLE device from a config entry.""" """Set up Moat BLE device from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
hass, hass,
@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address=address, address=address,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True

View File

@ -142,12 +142,12 @@ async def async_setup_entry(
data.update(service_info) data.update(service_info)
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
MoatBluetoothSensorEntity, async_add_entities MoatBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
class MoatBluetoothSensorEntity( class MoatBluetoothSensorEntity(

View File

@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SensorPush BLE device from a config entry.""" """Set up SensorPush BLE device from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
hass, hass,
@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address=address, address=address,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True

View File

@ -136,12 +136,12 @@ async def async_setup_entry(
data.update(service_info) data.update(service_info)
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
SensorPushBluetoothSensorEntity, async_add_entities SensorPushBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
class SensorPushBluetoothSensorEntity( class SensorPushBluetoothSensorEntity(

View File

@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Xiaomi BLE device from a config entry.""" """Set up Xiaomi BLE device from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
hass, hass,
@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address=address, address=address,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True

View File

@ -167,12 +167,12 @@ async def async_setup_entry(
data.update(service_info) data.update(service_info)
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
XiaomiBluetoothSensorEntity, async_add_entities XiaomiBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor))
class XiaomiBluetoothSensorEntity( class XiaomiBluetoothSensorEntity(

View File

@ -107,6 +107,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
_async_register_callback, _async_register_callback,
): ):
unregister_processor = coordinator.async_register_processor(processor) unregister_processor = coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
entity_key = PassiveBluetoothEntityKey("temperature", None) entity_key = PassiveBluetoothEntityKey("temperature", None)
entity_key_events = [] entity_key_events = []
@ -171,6 +172,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
assert coordinator.available is True assert coordinator.available is True
unregister_processor() unregister_processor()
cancel_coordinator()
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
@ -206,6 +208,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
_async_register_callback, _async_register_callback,
): ):
unregister_processor = coordinator.async_register_processor(processor) unregister_processor = coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
mock_entity = MagicMock() mock_entity = MagicMock()
mock_add_entities = MagicMock() mock_add_entities = MagicMock()
@ -259,6 +262,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
assert processor.available is False assert processor.available is False
unregister_processor() unregister_processor()
cancel_coordinator()
async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
@ -290,6 +294,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
_async_register_callback, _async_register_callback,
): ):
unregister_processor = coordinator.async_register_processor(processor) unregister_processor = coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
all_events = [] all_events = []
@ -310,6 +315,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
assert len(all_events) == 1 assert len(all_events) == 1
unregister_processor() unregister_processor()
cancel_coordinator()
async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start):
@ -346,6 +352,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
_async_register_callback, _async_register_callback,
): ):
unregister_processor = coordinator.async_register_processor(processor) unregister_processor = coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
processor.async_add_listener(MagicMock()) processor.async_add_listener(MagicMock())
@ -361,6 +368,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
assert processor.available is True assert processor.available is True
unregister_processor() unregister_processor()
cancel_coordinator()
async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
@ -397,6 +405,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
_async_register_callback, _async_register_callback,
): ):
unregister_processor = coordinator.async_register_processor(processor) unregister_processor = coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
processor.async_add_listener(MagicMock()) processor.async_add_listener(MagicMock())
@ -413,6 +422,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
assert processor.available is True assert processor.available is True
unregister_processor() unregister_processor()
cancel_coordinator()
GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo(
@ -737,6 +747,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
_async_register_callback, _async_register_callback,
): ):
coordinator.async_register_processor(processor) coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
processor.async_add_listener(MagicMock()) processor.async_add_listener(MagicMock())
@ -781,6 +792,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
assert entity_one.entity_key == PassiveBluetoothEntityKey( assert entity_one.entity_key == PassiveBluetoothEntityKey(
key="temperature", device_id="remote" key="temperature", device_id="remote"
) )
cancel_coordinator()
NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo(
@ -845,6 +857,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
_async_register_callback, _async_register_callback,
): ):
coordinator.async_register_processor(processor) coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
mock_add_entities = MagicMock() mock_add_entities = MagicMock()
@ -873,6 +886,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
assert entity_one.entity_key == PassiveBluetoothEntityKey( assert entity_one.entity_key == PassiveBluetoothEntityKey(
key="temperature", device_id=None key="temperature", device_id=None
) )
cancel_coordinator()
async def test_passive_bluetooth_entity_with_entity_platform( async def test_passive_bluetooth_entity_with_entity_platform(
@ -907,6 +921,7 @@ async def test_passive_bluetooth_entity_with_entity_platform(
_async_register_callback, _async_register_callback,
): ):
coordinator.async_register_processor(processor) coordinator.async_register_processor(processor)
cancel_coordinator = coordinator.async_start()
processor.async_add_entities_listener( processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity, PassiveBluetoothProcessorEntity,
@ -926,6 +941,7 @@ async def test_passive_bluetooth_entity_with_entity_platform(
hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure")
is not None is not None
) )
cancel_coordinator()
SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
@ -999,6 +1015,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
): ):
coordinator.async_register_processor(binary_sensor_processor) coordinator.async_register_processor(binary_sensor_processor)
coordinator.async_register_processor(sesnor_processor) coordinator.async_register_processor(sesnor_processor)
cancel_coordinator = coordinator.async_start()
binary_sensor_processor.async_add_listener(MagicMock()) binary_sensor_processor.async_add_listener(MagicMock())
sesnor_processor.async_add_listener(MagicMock()) sesnor_processor.async_add_listener(MagicMock())
@ -1056,3 +1073,4 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="motion", device_id=None key="motion", device_id=None
) )
cancel_coordinator()