Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Erik
a1fb1307d1 Remove sky connect config entry if USB stick is not plugged in 2023-01-12 17:46:50 +01:00
4 changed files with 112 additions and 10 deletions

View file

@ -1,6 +1,7 @@
"""The Home Assistant Sky Connect integration."""
from __future__ import annotations
import asyncio
import logging
from homeassistant.components import usb
@ -16,7 +17,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
@ -64,6 +65,30 @@ async def _multi_pan_addon_info(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Home Assistant Sky Connect config entry."""
usb_discovery_started: asyncio.Future[None] = asyncio.Future()
@callback
def async_usb_discovery_started(hass: HomeAssistant) -> None:
"""Handle usb discovery started."""
if not usb_discovery_started.cancelled():
usb_discovery_started.set_result(None)
@callback
def cancel_startup() -> None:
"""Stop waiting for USB discovery started."""
unsub_usb()
if not usb_discovery_started.cancelled():
usb_discovery_started.cancel()
unsub_usb = usb.async_at_discovery_started(hass, async_usb_discovery_started)
entry.async_on_unload(cancel_startup)
try:
await usb_discovery_started
except asyncio.CancelledError:
return False
matcher = usb.USBCallbackMatcher(
domain=DOMAIN,
vid=entry.data["vid"].upper(),
@ -74,8 +99,8 @@ 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))
addon_info = await _multi_pan_addon_info(hass, entry)

View file

@ -1,7 +1,7 @@
"""The USB Discovery integration."""
from __future__ import annotations
from collections.abc import Coroutine
from collections.abc import Callable, Coroutine
import dataclasses
import fnmatch
import logging
@ -20,12 +20,17 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT
from homeassistant.core import (
CALLBACK_TYPE,
Event,
HassJob,
HomeAssistant,
callback as hass_callback,
)
from homeassistant.data_entry_flow import BaseServiceInfo
from homeassistant.helpers import discovery_flow, system_info
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import USBMatcher, async_get_usb
@ -40,6 +45,8 @@ _LOGGER = logging.getLogger(__name__)
REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown
USB_DISCOVERY_STARTED = "usb_discovery_started"
__all__ = [
"async_is_plugged_in",
"async_register_scan_request_callback",
@ -89,6 +96,46 @@ def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> boo
)
@hass_callback
def async_at_discovery_started(
hass: HomeAssistant,
at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None],
) -> CALLBACK_TYPE:
"""Execute a job at_start_cb when USB discovery has started.
The job is executed immediately if USB discovery is already started.
"""
discovery: USBDiscovery = hass.data[DOMAIN]
at_start_job = HassJob(at_start_cb)
if discovery.started:
hass.async_run_hass_job(at_start_job, hass)
return lambda: None
unsub: None | CALLBACK_TYPE = None
@hass_callback
def _usb_started() -> None:
"""Call the callback when USB discovery started."""
hass.async_run_hass_job(at_start_job, hass)
nonlocal unsub
if unsub is not None:
unsub()
unsub = None
@hass_callback
def cancel() -> None:
if unsub is not None:
unsub()
unsub = async_dispatcher_connect(
hass,
USB_DISCOVERY_STARTED,
_usb_started,
)
return cancel
@dataclasses.dataclass
class UsbServiceInfo(BaseServiceInfo):
"""Prepared info from usb entries."""
@ -184,6 +231,7 @@ class USBDiscovery:
self.usb = usb
self.seen: set[tuple[str, ...]] = set()
self.observer_active = False
self.started = False
self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None
self._request_callbacks: list[CALLBACK_TYPE] = []
@ -195,6 +243,8 @@ class USBDiscovery:
async def async_start(self, event: Event) -> None:
"""Start USB Discovery and run a manual scan."""
await self._async_scan_serial()
async_dispatcher_send(self.hass, USB_DISCOVERY_STARTED)
self.started = True
async def _async_start_monitor(self) -> None:
"""Start monitoring hardware with pyudev."""

View file

@ -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(

View file

@ -1,4 +1,5 @@
"""Test the Home Assistant Sky Connect integration."""
import asyncio
from collections.abc import Generator
from typing import Any
from unittest.mock import MagicMock, Mock, patch
@ -9,7 +10,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 +57,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 +105,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 +154,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 +208,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 +272,24 @@ 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)
task = hass.async_create_task(
hass.config_entries.async_setup(config_entry.entry_id)
)
await asyncio.sleep(0.1)
assert not task.done()
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 +318,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,