mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
Reload the ZBT-1 integration on USB state changes (#141287)
* Reload the config entry when the ZBT-1 is unplugged * Register the USB event handler globally to react better to re-plugs * Fix existing unit tests * Add an empty `CONFIG_SCHEMA` * Add a unit test * Fix unit tests * Fix unit tests for Linux * Address most review comments * Address remaining review comments
This commit is contained in:
parent
b428196149
commit
a7c43f9b49
@ -3,19 +3,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import USBDevice, async_register_port_event_callback
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
|
||||
from .const import DESCRIPTION, DEVICE, DOMAIN, FIRMWARE, FIRMWARE_VERSION, PRODUCT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ZBT-1 integration."""
|
||||
|
||||
@callback
|
||||
def async_port_event_callback(
|
||||
added: set[USBDevice], removed: set[USBDevice]
|
||||
) -> None:
|
||||
"""Handle USB port events."""
|
||||
current_entries_by_path = {
|
||||
entry.data[DEVICE]: entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
for device in added | removed:
|
||||
path = device.device
|
||||
entry = current_entries_by_path.get(path)
|
||||
|
||||
if entry is not None:
|
||||
_LOGGER.debug(
|
||||
"Device %r has changed state, reloading config entry %s",
|
||||
path,
|
||||
entry,
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async_register_port_event_callback(hass, async_port_event_callback)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
device_path = entry.data[DEVICE]
|
||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -47,3 +47,13 @@ def mock_zha_get_last_network_settings() -> Generator[None]:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_usb_path_exists() -> Generator[None]:
|
||||
"""Mock os.path.exists to allow the ZBT-1 integration to load."""
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
@ -1,15 +1,28 @@
|
||||
"""Test the Home Assistant SkyConnect integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.components.usb import USBDevice
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.usb import (
|
||||
async_request_scan,
|
||||
force_usb_polling_watcher, # noqa: F401
|
||||
patch_scanned_serial_ports,
|
||||
)
|
||||
|
||||
|
||||
async def test_config_entry_migration_v2(hass: HomeAssistant) -> None:
|
||||
@ -58,3 +71,119 @@ async def test_config_entry_migration_v2(hass: HomeAssistant) -> None:
|
||||
}
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_setup_fails_on_missing_usb_port(hass: HomeAssistant) -> None:
|
||||
"""Test setup failing when the USB port is missing."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": "ezsp",
|
||||
"firmware_version": "7.4.4.0",
|
||||
},
|
||||
version=1,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Set up the config entry
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists"
|
||||
) as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed to set up, the device is missing
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
mock_exists.return_value = True
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Now it's ready
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("force_usb_polling_watcher")
|
||||
async def test_usb_device_reactivity(hass: HomeAssistant) -> None:
|
||||
"""Test setting up USB monitoring."""
|
||||
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": "ezsp",
|
||||
"firmware_version": "7.4.4.0",
|
||||
},
|
||||
version=1,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists"
|
||||
) as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed to set up, the device is missing
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
# Now we make it available but do not wait
|
||||
mock_exists.return_value = True
|
||||
|
||||
with patch_scanned_serial_ports(
|
||||
return_value=[
|
||||
USBDevice(
|
||||
device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
vid="10C4",
|
||||
pid="EA60",
|
||||
serial_number="3c0ed67c628beb11b1cd64a0f320645d",
|
||||
manufacturer="Nabu Casa",
|
||||
description="SkyConnect v1.0",
|
||||
)
|
||||
],
|
||||
):
|
||||
await async_request_scan(hass)
|
||||
|
||||
# It loads immediately
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Wait for a bit for the USB scan debouncer to cool off
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5))
|
||||
|
||||
# Unplug the stick
|
||||
mock_exists.return_value = False
|
||||
|
||||
with patch_scanned_serial_ports(return_value=[]):
|
||||
await async_request_scan(hass)
|
||||
|
||||
# The integration has reloaded and is now in a failed state
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
Loading…
x
Reference in New Issue
Block a user