529 lines
17 KiB
Python
529 lines
17 KiB
Python
"""Test hardware utilities."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from universal_silabs_flasher.common import Version as FlasherVersion
|
|
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
|
|
|
from homeassistant.components.hassio import (
|
|
AddonError,
|
|
AddonInfo,
|
|
AddonManager,
|
|
AddonState,
|
|
)
|
|
from homeassistant.components.homeassistant_hardware.helpers import (
|
|
async_register_firmware_info_provider,
|
|
)
|
|
from homeassistant.components.homeassistant_hardware.util import (
|
|
ApplicationType,
|
|
FirmwareInfo,
|
|
OwningAddon,
|
|
OwningIntegration,
|
|
get_otbr_addon_firmware_info,
|
|
guess_firmware_info,
|
|
probe_silabs_firmware_info,
|
|
probe_silabs_firmware_type,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
ZHA_CONFIG_ENTRY = MockConfigEntry(
|
|
domain="zha",
|
|
unique_id="some_unique_id",
|
|
data={
|
|
"device": {
|
|
"path": "/dev/ttyUSB1",
|
|
"baudrate": 115200,
|
|
"flow_control": None,
|
|
},
|
|
"radio_type": "ezsp",
|
|
},
|
|
version=4,
|
|
)
|
|
|
|
ZHA_CONFIG_ENTRY2 = MockConfigEntry(
|
|
domain="zha",
|
|
unique_id="some_other_unique_id",
|
|
data={
|
|
"device": {
|
|
"path": "/dev/ttyUSB2",
|
|
"baudrate": 115200,
|
|
"flow_control": None,
|
|
},
|
|
"radio_type": "ezsp",
|
|
},
|
|
version=4,
|
|
)
|
|
|
|
|
|
async def test_guess_firmware_info_unknown(hass: HomeAssistant) -> None:
|
|
"""Test guessing the firmware type."""
|
|
|
|
await async_setup_component(hass, "homeassistant_hardware", {})
|
|
|
|
assert (await guess_firmware_info(hass, "/dev/missing")) == FirmwareInfo(
|
|
device="/dev/missing",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version=None,
|
|
source="unknown",
|
|
owners=[],
|
|
)
|
|
|
|
|
|
async def test_guess_firmware_info_integrations(hass: HomeAssistant) -> None:
|
|
"""Test guessing the firmware via OTBR and ZHA."""
|
|
|
|
await async_setup_component(hass, "homeassistant_hardware", {})
|
|
|
|
# One instance of ZHA and two OTBRs
|
|
zha = MockConfigEntry(domain="zha", unique_id="some_unique_id_1")
|
|
zha.add_to_hass(hass)
|
|
|
|
otbr1 = MockConfigEntry(domain="otbr", unique_id="some_unique_id_2")
|
|
otbr1.add_to_hass(hass)
|
|
|
|
otbr2 = MockConfigEntry(domain="otbr", unique_id="some_unique_id_3")
|
|
otbr2.add_to_hass(hass)
|
|
|
|
# First ZHA is running with the stick
|
|
zha_firmware_info = FirmwareInfo(
|
|
device="/dev/serial/by-id/device1",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version=None,
|
|
source="zha",
|
|
owners=[AsyncMock(is_running=AsyncMock(return_value=True))],
|
|
)
|
|
|
|
# First OTBR: neither the addon or the integration are loaded
|
|
otbr_firmware_info1 = FirmwareInfo(
|
|
device="/dev/serial/by-id/device1",
|
|
firmware_type=ApplicationType.SPINEL,
|
|
firmware_version=None,
|
|
source="otbr",
|
|
owners=[
|
|
AsyncMock(is_running=AsyncMock(return_value=False)),
|
|
AsyncMock(is_running=AsyncMock(return_value=False)),
|
|
],
|
|
)
|
|
|
|
# Second OTBR: fully running but is with an unrelated device
|
|
otbr_firmware_info2 = FirmwareInfo(
|
|
device="/dev/serial/by-id/device2", # An unrelated device
|
|
firmware_type=ApplicationType.SPINEL,
|
|
firmware_version=None,
|
|
source="otbr",
|
|
owners=[
|
|
AsyncMock(is_running=AsyncMock(return_value=True)),
|
|
AsyncMock(is_running=AsyncMock(return_value=True)),
|
|
],
|
|
)
|
|
|
|
mock_zha_hardware_info = MagicMock(spec=["get_firmware_info"])
|
|
mock_zha_hardware_info.get_firmware_info = MagicMock(return_value=zha_firmware_info)
|
|
async_register_firmware_info_provider(hass, "zha", mock_zha_hardware_info)
|
|
|
|
async def mock_otbr_async_get_firmware_info(
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
) -> FirmwareInfo | None:
|
|
return {
|
|
otbr1.entry_id: otbr_firmware_info1,
|
|
otbr2.entry_id: otbr_firmware_info2,
|
|
}.get(config_entry.entry_id)
|
|
|
|
mock_otbr_hardware_info = MagicMock(spec=["async_get_firmware_info"])
|
|
mock_otbr_hardware_info.async_get_firmware_info = AsyncMock(
|
|
side_effect=mock_otbr_async_get_firmware_info
|
|
)
|
|
async_register_firmware_info_provider(hass, "otbr", mock_otbr_hardware_info)
|
|
|
|
# ZHA wins for the first stick, since it's actually running
|
|
assert (
|
|
await guess_firmware_info(hass, "/dev/serial/by-id/device1")
|
|
) == zha_firmware_info
|
|
|
|
# Second stick is communicating exclusively with the second OTBR
|
|
assert (
|
|
await guess_firmware_info(hass, "/dev/serial/by-id/device2")
|
|
) == otbr_firmware_info2
|
|
|
|
# If we stop ZHA, OTBR will take priority
|
|
zha_firmware_info.owners[0].is_running.return_value = False
|
|
otbr_firmware_info1.owners[0].is_running.return_value = True
|
|
assert (
|
|
await guess_firmware_info(hass, "/dev/serial/by-id/device1")
|
|
) == otbr_firmware_info1
|
|
|
|
|
|
async def test_owning_addon(hass: HomeAssistant) -> None:
|
|
"""Test `OwningAddon`."""
|
|
|
|
owning_addon = OwningAddon(slug="some-addon-slug")
|
|
|
|
# Explicitly running
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager"
|
|
) as mock_manager:
|
|
mock_manager.return_value.async_get_addon_info = AsyncMock(
|
|
return_value=AddonInfo(
|
|
available=True,
|
|
hostname="core_some_addon_slug",
|
|
options={},
|
|
state=AddonState.RUNNING,
|
|
update_available=False,
|
|
version="1.0.0",
|
|
)
|
|
)
|
|
assert (await owning_addon.is_running(hass)) is True
|
|
|
|
# Explicitly not running
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager"
|
|
) as mock_manager:
|
|
mock_manager.return_value.async_get_addon_info = AsyncMock(
|
|
return_value=AddonInfo(
|
|
available=True,
|
|
hostname="core_some_addon_slug",
|
|
options={},
|
|
state=AddonState.NOT_RUNNING,
|
|
update_available=False,
|
|
version="1.0.0",
|
|
)
|
|
)
|
|
assert (await owning_addon.is_running(hass)) is False
|
|
|
|
# Failed to get status
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager"
|
|
) as mock_manager:
|
|
mock_manager.return_value.async_get_addon_info = AsyncMock(
|
|
side_effect=AddonError()
|
|
)
|
|
assert (await owning_addon.is_running(hass)) is False
|
|
|
|
|
|
async def test_owning_addon_temporarily_stop_info_error(hass: HomeAssistant) -> None:
|
|
"""Test `OwningAddon` temporarily stopping with an info error."""
|
|
|
|
owning_addon = OwningAddon(slug="some-addon-slug")
|
|
mock_manager = AsyncMock()
|
|
mock_manager.async_get_addon_info.side_effect = AddonError()
|
|
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager",
|
|
return_value=mock_manager,
|
|
):
|
|
async with owning_addon.temporarily_stop(hass):
|
|
pass
|
|
|
|
# We never restart it
|
|
assert len(mock_manager.async_get_addon_info.mock_calls) == 1
|
|
assert len(mock_manager.async_stop_addon.mock_calls) == 0
|
|
assert len(mock_manager.async_wait_until_addon_state.mock_calls) == 0
|
|
assert len(mock_manager.async_start_addon_waiting.mock_calls) == 0
|
|
|
|
|
|
async def test_owning_addon_temporarily_stop_not_running(hass: HomeAssistant) -> None:
|
|
"""Test `OwningAddon` temporarily stopping when the addon is not running."""
|
|
|
|
owning_addon = OwningAddon(slug="some-addon-slug")
|
|
|
|
mock_manager = AsyncMock()
|
|
mock_manager.async_get_addon_info.return_value = AddonInfo(
|
|
available=True,
|
|
hostname="core_some_addon_slug",
|
|
options={},
|
|
state=AddonState.NOT_RUNNING,
|
|
update_available=False,
|
|
version="1.0.0",
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager",
|
|
return_value=mock_manager,
|
|
):
|
|
async with owning_addon.temporarily_stop(hass):
|
|
pass
|
|
|
|
# We never restart it
|
|
assert len(mock_manager.async_get_addon_info.mock_calls) == 1
|
|
assert len(mock_manager.async_stop_addon.mock_calls) == 0
|
|
assert len(mock_manager.async_wait_until_addon_state.mock_calls) == 0
|
|
assert len(mock_manager.async_start_addon_waiting.mock_calls) == 0
|
|
|
|
|
|
async def test_owning_addon_temporarily_stop(hass: HomeAssistant) -> None:
|
|
"""Test `OwningAddon` temporarily stopping when the addon is running."""
|
|
|
|
owning_addon = OwningAddon(slug="some-addon-slug")
|
|
|
|
mock_manager = AsyncMock()
|
|
mock_manager.async_get_addon_info.return_value = AddonInfo(
|
|
available=True,
|
|
hostname="core_some_addon_slug",
|
|
options={},
|
|
state=AddonState.RUNNING,
|
|
update_available=False,
|
|
version="1.0.0",
|
|
)
|
|
|
|
mock_manager.async_stop_addon = AsyncMock()
|
|
mock_manager.async_wait_until_addon_state = AsyncMock()
|
|
mock_manager.async_start_addon_waiting = AsyncMock()
|
|
|
|
# The error is propagated but it doesn't affect restarting the addon
|
|
with (
|
|
patch(
|
|
"homeassistant.components.homeassistant_hardware.util.WaitingAddonManager",
|
|
return_value=mock_manager,
|
|
),
|
|
pytest.raises(RuntimeError),
|
|
):
|
|
async with owning_addon.temporarily_stop(hass):
|
|
raise RuntimeError("Some error")
|
|
|
|
# We restart it
|
|
assert len(mock_manager.async_get_addon_info.mock_calls) == 1
|
|
assert len(mock_manager.async_stop_addon.mock_calls) == 1
|
|
assert len(mock_manager.async_wait_until_addon_state.mock_calls) == 1
|
|
assert len(mock_manager.async_start_addon_waiting.mock_calls) == 1
|
|
|
|
|
|
async def test_owning_integration(hass: HomeAssistant) -> None:
|
|
"""Test `OwningIntegration`."""
|
|
config_entry = MockConfigEntry(domain="mock_domain", unique_id="some_unique_id")
|
|
config_entry.add_to_hass(hass)
|
|
|
|
owning_integration = OwningIntegration(config_entry_id=config_entry.entry_id)
|
|
|
|
# Explicitly running
|
|
config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
|
assert (await owning_integration.is_running(hass)) is True
|
|
|
|
# Explicitly not running
|
|
config_entry.mock_state(hass, ConfigEntryState.NOT_LOADED)
|
|
assert (await owning_integration.is_running(hass)) is False
|
|
|
|
# Missing config entry
|
|
owning_integration2 = OwningIntegration(config_entry_id="some_nonexistenct_id")
|
|
assert (await owning_integration2.is_running(hass)) is False
|
|
|
|
|
|
async def test_owning_integration_temporarily_stop_missing_entry(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test temporarily stopping the integration when the config entry doesn't exist."""
|
|
missing_integration = OwningIntegration(config_entry_id="missing_entry_id")
|
|
|
|
with (
|
|
patch.object(hass.config_entries, "async_unload") as mock_unload,
|
|
patch.object(hass.config_entries, "async_setup") as mock_setup,
|
|
):
|
|
async with missing_integration.temporarily_stop(hass):
|
|
pass
|
|
|
|
# Because there's no matching entry, no unload or setup calls are made
|
|
assert len(mock_unload.mock_calls) == 0
|
|
assert len(mock_setup.mock_calls) == 0
|
|
|
|
|
|
async def test_owning_integration_temporarily_stop_not_loaded(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test temporarily stopping the integration when the config entry is not loaded."""
|
|
entry = MockConfigEntry(domain="test_domain")
|
|
entry.add_to_hass(hass)
|
|
entry.mock_state(hass, ConfigEntryState.NOT_LOADED)
|
|
|
|
integration = OwningIntegration(config_entry_id=entry.entry_id)
|
|
|
|
with (
|
|
patch.object(hass.config_entries, "async_unload") as mock_unload,
|
|
patch.object(hass.config_entries, "async_setup") as mock_setup,
|
|
):
|
|
async with integration.temporarily_stop(hass):
|
|
pass
|
|
|
|
# Since the entry was not loaded, we never unload or re-setup
|
|
assert len(mock_unload.mock_calls) == 0
|
|
assert len(mock_setup.mock_calls) == 0
|
|
|
|
|
|
async def test_owning_integration_temporarily_stop_loaded(hass: HomeAssistant) -> None:
|
|
"""Test temporarily stopping the integration when the config entry is loaded."""
|
|
entry = MockConfigEntry(domain="test_domain")
|
|
entry.add_to_hass(hass)
|
|
entry.mock_state(hass, ConfigEntryState.LOADED)
|
|
|
|
integration = OwningIntegration(config_entry_id=entry.entry_id)
|
|
|
|
with (
|
|
patch.object(hass.config_entries, "async_unload") as mock_unload,
|
|
patch.object(hass.config_entries, "async_setup") as mock_setup,
|
|
pytest.raises(RuntimeError),
|
|
):
|
|
async with integration.temporarily_stop(hass):
|
|
raise RuntimeError("Some error during the temporary stop")
|
|
|
|
# We expect one unload followed by one setup call
|
|
mock_unload.assert_called_once_with(entry.entry_id)
|
|
mock_setup.assert_called_once_with(entry.entry_id)
|
|
|
|
|
|
async def test_firmware_info(hass: HomeAssistant) -> None:
|
|
"""Test `FirmwareInfo`."""
|
|
|
|
owner1 = AsyncMock()
|
|
owner2 = AsyncMock()
|
|
|
|
firmware_info = FirmwareInfo(
|
|
device="/dev/ttyUSB1",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version="1.0.0",
|
|
source="zha",
|
|
owners=[owner1, owner2],
|
|
)
|
|
|
|
# Both running
|
|
owner1.is_running.return_value = True
|
|
owner2.is_running.return_value = True
|
|
assert (await firmware_info.is_running(hass)) is True
|
|
|
|
# Only one running
|
|
owner1.is_running.return_value = True
|
|
owner2.is_running.return_value = False
|
|
assert (await firmware_info.is_running(hass)) is False
|
|
|
|
# No owners
|
|
firmware_info2 = FirmwareInfo(
|
|
device="/dev/ttyUSB1",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version="1.0.0",
|
|
source="zha",
|
|
owners=[],
|
|
)
|
|
|
|
assert (await firmware_info2.is_running(hass)) is False
|
|
|
|
|
|
async def test_get_otbr_addon_firmware_info_failure(hass: HomeAssistant) -> None:
|
|
"""Test getting OTBR addon firmware info failure due to bad API call."""
|
|
|
|
otbr_addon_manager = AsyncMock(spec_set=AddonManager)
|
|
otbr_addon_manager.async_get_addon_info.side_effect = AddonError()
|
|
|
|
assert (await get_otbr_addon_firmware_info(hass, otbr_addon_manager)) is None
|
|
|
|
|
|
async def test_get_otbr_addon_firmware_info_failure_bad_options(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test getting OTBR addon firmware info failure due to bad addon options."""
|
|
|
|
otbr_addon_manager = AsyncMock(spec_set=AddonManager)
|
|
otbr_addon_manager.async_get_addon_info.return_value = AddonInfo(
|
|
available=True,
|
|
hostname="core_some_addon_slug",
|
|
options={}, # `device` is missing
|
|
state=AddonState.RUNNING,
|
|
update_available=False,
|
|
version="1.0.0",
|
|
)
|
|
|
|
assert (await get_otbr_addon_firmware_info(hass, otbr_addon_manager)) is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("app_type", "firmware_version", "expected_fw_info"),
|
|
[
|
|
(
|
|
FlasherApplicationType.EZSP,
|
|
FlasherVersion("1.0.0"),
|
|
FirmwareInfo(
|
|
device="/dev/ttyUSB0",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version="1.0.0",
|
|
source="probe",
|
|
owners=[],
|
|
),
|
|
),
|
|
(
|
|
FlasherApplicationType.EZSP,
|
|
None,
|
|
FirmwareInfo(
|
|
device="/dev/ttyUSB0",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version=None,
|
|
source="probe",
|
|
owners=[],
|
|
),
|
|
),
|
|
(
|
|
FlasherApplicationType.SPINEL,
|
|
FlasherVersion("2.0.0"),
|
|
FirmwareInfo(
|
|
device="/dev/ttyUSB0",
|
|
firmware_type=ApplicationType.SPINEL,
|
|
firmware_version="2.0.0",
|
|
source="probe",
|
|
owners=[],
|
|
),
|
|
),
|
|
(None, None, None),
|
|
],
|
|
)
|
|
async def test_probe_silabs_firmware_info(
|
|
app_type: FlasherApplicationType | None,
|
|
firmware_version: FlasherVersion | None,
|
|
expected_fw_info: FirmwareInfo | None,
|
|
) -> None:
|
|
"""Test getting the firmware info."""
|
|
|
|
def probe_app_type() -> None:
|
|
mock_flasher.app_type = app_type
|
|
mock_flasher.app_version = firmware_version
|
|
|
|
mock_flasher = MagicMock()
|
|
mock_flasher.app_type = None
|
|
mock_flasher.app_version = None
|
|
mock_flasher.probe_app_type = AsyncMock(side_effect=probe_app_type)
|
|
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.Flasher",
|
|
return_value=mock_flasher,
|
|
):
|
|
result = await probe_silabs_firmware_info("/dev/ttyUSB0")
|
|
assert result == expected_fw_info
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("probe_result", "expected"),
|
|
[
|
|
(
|
|
FirmwareInfo(
|
|
device="/dev/ttyUSB0",
|
|
firmware_type=ApplicationType.EZSP,
|
|
firmware_version=None,
|
|
source="unknown",
|
|
owners=[],
|
|
),
|
|
ApplicationType.EZSP,
|
|
),
|
|
(None, None),
|
|
],
|
|
)
|
|
async def test_probe_silabs_firmware_type(
|
|
probe_result: FirmwareInfo | None, expected: ApplicationType | None
|
|
) -> None:
|
|
"""Test getting the firmware type from the probe result."""
|
|
with patch(
|
|
"homeassistant.components.homeassistant_hardware.util.probe_silabs_firmware_info",
|
|
autospec=True,
|
|
return_value=probe_result,
|
|
):
|
|
result = await probe_silabs_firmware_type("/dev/ttyUSB0")
|
|
assert result == expected
|