Compare commits
1 commit
dev
...
sky_connec
Author | SHA1 | Date | |
---|---|---|---|
|
a1fb1307d1 |
4 changed files with 112 additions and 10 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue