Remove sky connect config entry if USB stick is not plugged in (#85765)
* Remove sky connect config entry if USB stick is not plugged in * Tweak cleanup * Give some stuff more cromulent names * Do the needful * Add tests * Tweak
This commit is contained in:
parent
fa0d653216
commit
affea9a305
5 changed files with 164 additions and 19 deletions
|
@ -16,7 +16,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
|||
get_zigbee_socket,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@ -25,12 +25,10 @@ from .util import get_usb_service_info
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _multi_pan_addon_info(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> AddonInfo | None:
|
||||
"""Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect."""
|
||||
async def _wait_multi_pan_addon(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Wait for multi-PAN info to be available."""
|
||||
if not is_hassio(hass):
|
||||
return None
|
||||
return
|
||||
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
try:
|
||||
|
@ -50,7 +48,18 @@ async def _multi_pan_addon_info(
|
|||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if addon_info.state == AddonState.NOT_INSTALLED:
|
||||
|
||||
async def _multi_pan_addon_info(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> AddonInfo | None:
|
||||
"""Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect."""
|
||||
if not is_hassio(hass):
|
||||
return None
|
||||
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
addon_info: AddonInfo = await addon_manager.async_get_addon_info()
|
||||
|
||||
if addon_info.state != AddonState.RUNNING:
|
||||
return None
|
||||
|
||||
usb_dev = entry.data["device"]
|
||||
|
@ -62,8 +71,8 @@ async def _multi_pan_addon_info(
|
|||
return addon_info
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant Sky Connect config entry."""
|
||||
async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Finish Home Assistant Sky Connect config entry setup."""
|
||||
matcher = usb.USBCallbackMatcher(
|
||||
domain=DOMAIN,
|
||||
vid=entry.data["vid"].upper(),
|
||||
|
@ -74,8 +83,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
|
||||
if not usb.async_is_plugged_in(hass, matcher):
|
||||
# The USB dongle is not plugged in
|
||||
raise ConfigEntryNotReady
|
||||
# The USB dongle is not plugged in, remove the config entry
|
||||
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
|
||||
return
|
||||
|
||||
addon_info = await _multi_pan_addon_info(hass, entry)
|
||||
|
||||
|
@ -86,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
context={"source": "usb"},
|
||||
data=usb_info,
|
||||
)
|
||||
return True
|
||||
return
|
||||
|
||||
hw_discovery_data = {
|
||||
"name": "Sky Connect Multi-PAN",
|
||||
|
@ -101,6 +111,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
data=hw_discovery_data,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant Sky Connect config entry."""
|
||||
|
||||
await _wait_multi_pan_addon(hass, entry)
|
||||
|
||||
@callback
|
||||
def async_usb_scan_done() -> None:
|
||||
"""Handle usb discovery started."""
|
||||
hass.async_create_task(_async_usb_scan_done(hass, entry))
|
||||
|
||||
unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done)
|
||||
entry.async_on_unload(unsub_usb)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -61,6 +61,18 @@ def async_register_scan_request_callback(
|
|||
return discovery.async_register_scan_request_callback(callback)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_initial_scan_callback(
|
||||
hass: HomeAssistant, callback: CALLBACK_TYPE
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register to receive a callback when the initial USB scan is done.
|
||||
|
||||
If the initial scan is already done, the callback is called immediately.
|
||||
"""
|
||||
discovery: USBDiscovery = hass.data[DOMAIN]
|
||||
return discovery.async_register_initial_scan_callback(callback)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool:
|
||||
"""Return True is a USB device is present."""
|
||||
|
@ -186,6 +198,8 @@ class USBDiscovery:
|
|||
self.observer_active = False
|
||||
self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None
|
||||
self._request_callbacks: list[CALLBACK_TYPE] = []
|
||||
self.initial_scan_done = False
|
||||
self._initial_scan_callbacks: list[CALLBACK_TYPE] = []
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up USB Discovery."""
|
||||
|
@ -249,7 +263,7 @@ class USBDiscovery:
|
|||
self,
|
||||
_callback: CALLBACK_TYPE,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register a callback."""
|
||||
"""Register a scan request callback."""
|
||||
self._request_callbacks.append(_callback)
|
||||
|
||||
@hass_callback
|
||||
|
@ -258,6 +272,26 @@ class USBDiscovery:
|
|||
|
||||
return _async_remove_callback
|
||||
|
||||
@hass_callback
|
||||
def async_register_initial_scan_callback(
|
||||
self,
|
||||
callback: CALLBACK_TYPE,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register an initial scan callback."""
|
||||
if self.initial_scan_done:
|
||||
callback()
|
||||
return lambda: None
|
||||
|
||||
self._initial_scan_callbacks.append(callback)
|
||||
|
||||
@hass_callback
|
||||
def _async_remove_callback() -> None:
|
||||
if callback not in self._initial_scan_callbacks:
|
||||
return
|
||||
self._initial_scan_callbacks.remove(callback)
|
||||
|
||||
return _async_remove_callback
|
||||
|
||||
@hass_callback
|
||||
def _async_process_discovered_usb_device(self, device: USBDevice) -> None:
|
||||
"""Process a USB discovery."""
|
||||
|
@ -307,6 +341,12 @@ class USBDiscovery:
|
|||
async def _async_scan_serial(self) -> None:
|
||||
"""Scan serial ports."""
|
||||
self._async_process_ports(await self.hass.async_add_executor_job(comports))
|
||||
if self.initial_scan_done:
|
||||
return
|
||||
|
||||
self.initial_scan_done = True
|
||||
while self._initial_scan_callbacks:
|
||||
self._initial_scan_callbacks.pop()()
|
||||
|
||||
async def _async_scan(self) -> None:
|
||||
"""Scan for USB devices and notify callbacks to scan as well."""
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG_ENTRY_DATA = {
|
||||
"device": "bla_device",
|
||||
|
@ -29,7 +30,8 @@ async def test_hardware_info(
|
|||
hass: HomeAssistant, hass_ws_client, addon_store_info
|
||||
) -> None:
|
||||
"""Test we can get the board info."""
|
||||
mock_integration(hass, MockModule("usb"))
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
|
|
|
@ -9,7 +9,8 @@ from homeassistant.components import zha
|
|||
from homeassistant.components.hassio.handler import HassioAPIError
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -55,6 +56,9 @@ async def test_setup_entry(
|
|||
num_flows,
|
||||
) -> None:
|
||||
"""Test setup of a config entry, including setup of zha."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
|
@ -100,6 +104,9 @@ async def test_setup_zha(
|
|||
mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info
|
||||
) -> None:
|
||||
"""Test zha gets the right config."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
|
@ -146,6 +153,9 @@ async def test_setup_zha_multipan(
|
|||
hass: HomeAssistant, addon_info, addon_running
|
||||
) -> None:
|
||||
"""Test zha gets the right config."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"]
|
||||
|
||||
# Setup the config entry
|
||||
|
@ -197,6 +207,9 @@ async def test_setup_zha_multipan_other_device(
|
|||
mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running
|
||||
) -> None:
|
||||
"""Test zha gets the right config."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect"
|
||||
|
||||
# Setup the config entry
|
||||
|
@ -258,16 +271,23 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None:
|
|||
"homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in",
|
||||
return_value=False,
|
||||
) as mock_is_plugged_in:
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
# USB discovery starts, config entry should be removed
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_is_plugged_in.mock_calls) == 1
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
||||
|
||||
|
||||
async def test_setup_entry_addon_info_fails(
|
||||
hass: HomeAssistant, addon_store_info
|
||||
) -> None:
|
||||
"""Test setup of a config entry when fetching addon info fails."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
addon_store_info.side_effect = HassioAPIError("Boom")
|
||||
|
||||
# Setup the config entry
|
||||
|
@ -296,6 +316,9 @@ async def test_setup_entry_addon_not_running(
|
|||
hass: HomeAssistant, addon_installed, start_addon
|
||||
) -> None:
|
||||
"""Test the addon is started if it is not running."""
|
||||
assert await async_setup_component(hass, "usb", {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
|
|
|
@ -936,3 +936,59 @@ async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_cli
|
|||
assert response["success"]
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_callback.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_initial_scan_callback(hass, hass_ws_client):
|
||||
"""Test it's possible to register a callback when the initial scan is done."""
|
||||
mock_callback_1 = Mock()
|
||||
mock_callback_2 = Mock()
|
||||
|
||||
with patch("pyudev.Context", side_effect=ImportError), patch(
|
||||
"homeassistant.components.usb.async_get_usb", return_value=[]
|
||||
), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(
|
||||
hass.config_entries.flow, "async_init"
|
||||
):
|
||||
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||
cancel_1 = usb.async_register_initial_scan_callback(hass, mock_callback_1)
|
||||
assert len(mock_callback_1.mock_calls) == 0
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_callback_1.mock_calls) == 0
|
||||
|
||||
# This triggers the initial scan
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_callback_1.mock_calls) == 1
|
||||
|
||||
# A callback registered now should be called immediately. The old callback
|
||||
# should not be called again
|
||||
cancel_2 = usb.async_register_initial_scan_callback(hass, mock_callback_2)
|
||||
assert len(mock_callback_1.mock_calls) == 1
|
||||
assert len(mock_callback_2.mock_calls) == 1
|
||||
|
||||
# Calling the cancels should be allowed even if the callback has been called
|
||||
cancel_1()
|
||||
cancel_2()
|
||||
|
||||
|
||||
async def test_cancel_initial_scan_callback(hass, hass_ws_client):
|
||||
"""Test it's possible to cancel an initial scan callback."""
|
||||
mock_callback = Mock()
|
||||
|
||||
with patch("pyudev.Context", side_effect=ImportError), patch(
|
||||
"homeassistant.components.usb.async_get_usb", return_value=[]
|
||||
), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(
|
||||
hass.config_entries.flow, "async_init"
|
||||
):
|
||||
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||
cancel = usb.async_register_initial_scan_callback(hass, mock_callback)
|
||||
assert len(mock_callback.mock_calls) == 0
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_callback.mock_calls) == 0
|
||||
cancel()
|
||||
|
||||
# This triggers the initial scan
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_callback.mock_calls) == 0
|
||||
|
|
Loading…
Add table
Reference in a new issue