mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Keep track of addons and integrations when determining HA radio firmware type (#134598)
* Replace `FirmwareGuess` with `FirmwareInfo` with owner tracking
* Fix up config flow
* Account for OTBR addon existing independent of integration
* Fix remaining unit tests
* Add some tests for ownership
* Unit test `get_zha_firmware_info`
* ZHA `homeassistant_hardware` platform
* OTBR `homeassistant_hardware` platform
* Rework imports
* Fix unit tests
* Add OTBR unit tests
* Add hassfest exemption for `homeassistant_hardware` and `otbr`
* Invert registration to decouple the hardware integration
* Revert "Add hassfest exemption for `homeassistant_hardware` and `otbr`"
This reverts commit c8c6e7044f.
* Fix circular imports
* Fix unit tests
* Address review comments
* Simplify API a little
* Fix `| None` mypy issues
* Remove the `unregister_firmware_info_provider` API
* 100% coverage
* Add `HardwareInfoDispatcher.register_firmware_info_callback`
* Unit test `register_firmware_info_callback` (zha)
* Unit test `register_firmware_info_callback` (otbr)
* Update existing hardware helper tests to use the new function
* Add `async_` prefix to helper function names
* Move OTBR implementation to a separate PR
* Update ZHA diagnostics snapshot
* Switch from `dict.setdefault` to `defaultdict`
* Add some error handling to `iter_firmware_info` and increase test coverage
* Oops
This commit is contained in:
@@ -23,6 +23,7 @@ from homeassistant.components.homeassistant_hardware.util import (
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
@@ -106,7 +107,7 @@ class FakeFirmwareOptionsFlowHandler(BaseFirmwareOptionsFlow):
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_test_firmware_platform(
|
||||
async def mock_test_firmware_platform(
|
||||
hass: HomeAssistant,
|
||||
) -> Generator[None]:
|
||||
"""Fixture for a test config flow."""
|
||||
@@ -116,6 +117,8 @@ def mock_test_firmware_platform(
|
||||
mock_integration(hass, mock_module)
|
||||
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
||||
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
with mock_config_flow(TEST_DOMAIN, FakeFirmwareConfigFlow):
|
||||
yield
|
||||
|
||||
@@ -189,6 +192,10 @@ def mock_addon_info(
|
||||
"homeassistant.components.homeassistant_hardware.firmware_config_flow.get_otbr_addon_manager",
|
||||
return_value=mock_otbr_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_otbr_addon_manager",
|
||||
return_value=mock_otbr_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.firmware_config_flow.get_zigbee_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
@@ -197,6 +204,10 @@ def mock_addon_info(
|
||||
"homeassistant.components.homeassistant_hardware.firmware_config_flow.is_hassio",
|
||||
return_value=is_hassio,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.is_hassio",
|
||||
return_value=is_hassio,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type",
|
||||
return_value=app_type,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test the Home Assistant hardware firmware config flow failure cases."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -9,7 +9,11 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
STEP_PICK_FIRMWARE_THREAD,
|
||||
STEP_PICK_FIRMWARE_ZIGBEE,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
OwningIntegration,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@@ -548,21 +552,28 @@ async def test_options_flow_zigbee_to_thread_zha_configured(
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
# Set up ZHA as well
|
||||
zha_config_entry = MockConfigEntry(
|
||||
domain="zha",
|
||||
data={"device": {"path": TEST_DEVICE}},
|
||||
)
|
||||
zha_config_entry.add_to_hass(hass)
|
||||
# Pretend ZHA is using the stick
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.firmware_config_flow.guess_hardware_owners",
|
||||
return_value=[
|
||||
FirmwareInfo(
|
||||
device=TEST_DEVICE,
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
firmware_version="1.2.3.4",
|
||||
source="zha",
|
||||
owners=[OwningIntegration(config_entry_id="some_config_entry_id")],
|
||||
)
|
||||
],
|
||||
):
|
||||
# Confirm options flow
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
# Confirm options flow
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
# Pick Thread
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD},
|
||||
)
|
||||
|
||||
# Pick Thread
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD},
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "zha_still_using_stick"
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Test hardware helpers."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, call
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.const import DATA_COMPONENT
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_notify_firmware_info,
|
||||
async_register_firmware_info_callback,
|
||||
async_register_firmware_info_provider,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
FIRMWARE_INFO_EZSP = FirmwareInfo(
|
||||
device="/dev/serial/by-id/device1",
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
firmware_version=None,
|
||||
source="zha",
|
||||
owners=[AsyncMock(is_running=AsyncMock(return_value=True))],
|
||||
)
|
||||
|
||||
FIRMWARE_INFO_SPINEL = FirmwareInfo(
|
||||
device="/dev/serial/by-id/device2",
|
||||
firmware_type=ApplicationType.SPINEL,
|
||||
firmware_version=None,
|
||||
source="otbr",
|
||||
owners=[AsyncMock(is_running=AsyncMock(return_value=True))],
|
||||
)
|
||||
|
||||
|
||||
async def test_dispatcher_registration(hass: HomeAssistant) -> None:
|
||||
"""Test HardwareInfoDispatcher registration."""
|
||||
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
# Mock provider 1 with a synchronous method to pull firmware info
|
||||
provider1_config_entry = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id1",
|
||||
data={},
|
||||
)
|
||||
provider1_config_entry.add_to_hass(hass)
|
||||
provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
provider1_firmware = MagicMock(spec=["get_firmware_info"])
|
||||
provider1_firmware.get_firmware_info = MagicMock(return_value=FIRMWARE_INFO_EZSP)
|
||||
async_register_firmware_info_provider(hass, "zha", provider1_firmware)
|
||||
|
||||
# Mock provider 2 with an asynchronous method to pull firmware info
|
||||
provider2_config_entry = MockConfigEntry(
|
||||
domain="otbr",
|
||||
unique_id="some_unique_id2",
|
||||
data={},
|
||||
)
|
||||
provider2_config_entry.add_to_hass(hass)
|
||||
provider2_config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
provider2_firmware = MagicMock(spec=["async_get_firmware_info"])
|
||||
provider2_firmware.async_get_firmware_info = AsyncMock(
|
||||
return_value=FIRMWARE_INFO_SPINEL
|
||||
)
|
||||
async_register_firmware_info_provider(hass, "otbr", provider2_firmware)
|
||||
|
||||
# Double registration won't work
|
||||
with pytest.raises(ValueError, match="Domain zha is already registered"):
|
||||
async_register_firmware_info_provider(hass, "zha", provider1_firmware)
|
||||
|
||||
# We can iterate over the results
|
||||
info = [i async for i in hass.data[DATA_COMPONENT].iter_firmware_info()]
|
||||
assert info == [
|
||||
FIRMWARE_INFO_EZSP,
|
||||
FIRMWARE_INFO_SPINEL,
|
||||
]
|
||||
|
||||
callback1 = Mock()
|
||||
cancel1 = async_register_firmware_info_callback(
|
||||
hass, "/dev/serial/by-id/device1", callback1
|
||||
)
|
||||
|
||||
callback2 = Mock()
|
||||
cancel2 = async_register_firmware_info_callback(
|
||||
hass, "/dev/serial/by-id/device2", callback2
|
||||
)
|
||||
|
||||
# And receive notification callbacks
|
||||
await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP)
|
||||
await async_notify_firmware_info(hass, "otbr", firmware_info=FIRMWARE_INFO_SPINEL)
|
||||
await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP)
|
||||
cancel1()
|
||||
await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP)
|
||||
await async_notify_firmware_info(hass, "otbr", firmware_info=FIRMWARE_INFO_SPINEL)
|
||||
cancel2()
|
||||
|
||||
assert callback1.mock_calls == [
|
||||
call(FIRMWARE_INFO_EZSP),
|
||||
call(FIRMWARE_INFO_EZSP),
|
||||
]
|
||||
|
||||
assert callback2.mock_calls == [
|
||||
call(FIRMWARE_INFO_SPINEL),
|
||||
call(FIRMWARE_INFO_SPINEL),
|
||||
]
|
||||
|
||||
|
||||
async def test_dispatcher_iter_error_handling(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test HardwareInfoDispatcher ignoring errors from firmware info providers."""
|
||||
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
provider1_config_entry = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id1",
|
||||
data={},
|
||||
)
|
||||
provider1_config_entry.add_to_hass(hass)
|
||||
provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
provider1_firmware = MagicMock(spec=["get_firmware_info"])
|
||||
provider1_firmware.get_firmware_info = MagicMock(side_effect=Exception("Boom!"))
|
||||
async_register_firmware_info_provider(hass, "zha", provider1_firmware)
|
||||
|
||||
provider2_config_entry = MockConfigEntry(
|
||||
domain="otbr",
|
||||
unique_id="some_unique_id2",
|
||||
data={},
|
||||
)
|
||||
provider2_config_entry.add_to_hass(hass)
|
||||
provider2_config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
provider2_firmware = MagicMock(spec=["async_get_firmware_info"])
|
||||
provider2_firmware.async_get_firmware_info = AsyncMock(
|
||||
return_value=FIRMWARE_INFO_SPINEL
|
||||
)
|
||||
async_register_firmware_info_provider(hass, "otbr", provider2_firmware)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
info = [i async for i in hass.data[DATA_COMPONENT].iter_firmware_info()]
|
||||
|
||||
assert info == [FIRMWARE_INFO_SPINEL]
|
||||
assert "Error while getting firmware info from" in caplog.text
|
||||
|
||||
|
||||
async def test_dispatcher_callback_error_handling(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test HardwareInfoDispatcher ignoring errors from firmware info callbacks."""
|
||||
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
provider1_config_entry = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id1",
|
||||
data={},
|
||||
)
|
||||
provider1_config_entry.add_to_hass(hass)
|
||||
provider1_config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
provider1_firmware = MagicMock(spec=["get_firmware_info"])
|
||||
provider1_firmware.get_firmware_info = MagicMock(return_value=FIRMWARE_INFO_EZSP)
|
||||
async_register_firmware_info_provider(hass, "zha", provider1_firmware)
|
||||
|
||||
callback1 = Mock(side_effect=Exception("Some error"))
|
||||
async_register_firmware_info_callback(hass, "/dev/serial/by-id/device1", callback1)
|
||||
|
||||
callback2 = Mock()
|
||||
async_register_firmware_info_callback(hass, "/dev/serial/by-id/device1", callback2)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
await async_notify_firmware_info(hass, "zha", firmware_info=FIRMWARE_INFO_EZSP)
|
||||
|
||||
assert "Error while notifying firmware info listener" in caplog.text
|
||||
|
||||
assert callback1.mock_calls == [call(FIRMWARE_INFO_EZSP)]
|
||||
assert callback2.mock_calls == [call(FIRMWARE_INFO_EZSP)]
|
||||
@@ -1,18 +1,21 @@
|
||||
"""Test hardware utilities."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_register_firmware_info_provider,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareGuess,
|
||||
FlasherApplicationType,
|
||||
get_zha_device_path,
|
||||
guess_firmware_type,
|
||||
probe_silabs_firmware_type,
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
guess_firmware_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -21,7 +24,21 @@ ZHA_CONFIG_ENTRY = MockConfigEntry(
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"device": {
|
||||
"path": "socket://1.2.3.4:5678",
|
||||
"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,
|
||||
},
|
||||
@@ -31,153 +48,202 @@ ZHA_CONFIG_ENTRY = MockConfigEntry(
|
||||
)
|
||||
|
||||
|
||||
def test_get_zha_device_path() -> None:
|
||||
"""Test extracting the ZHA device path from its config entry."""
|
||||
assert (
|
||||
get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"]
|
||||
)
|
||||
|
||||
|
||||
def test_get_zha_device_path_ignored_discovery() -> None:
|
||||
"""Test extracting the ZHA device path from an ignored ZHA discovery."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id",
|
||||
data={},
|
||||
version=4,
|
||||
)
|
||||
|
||||
assert get_zha_device_path(config_entry) is None
|
||||
|
||||
|
||||
async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None:
|
||||
async def test_guess_firmware_info_unknown(hass: HomeAssistant) -> None:
|
||||
"""Test guessing the firmware type."""
|
||||
|
||||
assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess(
|
||||
is_running=False, firmware_type=ApplicationType.EZSP, source="unknown"
|
||||
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_type(hass: HomeAssistant) -> None:
|
||||
"""Test guessing the firmware."""
|
||||
path = ZHA_CONFIG_ENTRY.data["device"]["path"]
|
||||
async def test_guess_firmware_info_integrations(hass: HomeAssistant) -> None:
|
||||
"""Test guessing the firmware via OTBR and ZHA."""
|
||||
|
||||
ZHA_CONFIG_ENTRY.add_to_hass(hass)
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED)
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=False, firmware_type=ApplicationType.EZSP, source="zha"
|
||||
# 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))],
|
||||
)
|
||||
|
||||
# When ZHA is running, we indicate as such when guessing
|
||||
ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED)
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.EZSP, source="zha"
|
||||
# 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)),
|
||||
],
|
||||
)
|
||||
|
||||
mock_otbr_addon_manager = AsyncMock()
|
||||
mock_multipan_addon_manager = AsyncMock()
|
||||
# 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)),
|
||||
],
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_otbr_addon_manager",
|
||||
return_value=mock_otbr_addon_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
return_value=mock_multipan_addon_manager,
|
||||
),
|
||||
):
|
||||
mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError()
|
||||
mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError()
|
||||
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)
|
||||
|
||||
# Hassio errors are ignored and we still go with ZHA
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.EZSP, source="zha"
|
||||
)
|
||||
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_addon_manager.async_get_addon_info.side_effect = None
|
||||
mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": "/some/other/device"},
|
||||
state=AddonState.RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
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)
|
||||
|
||||
# We will prefer ZHA, as it is running (and actually pointing to the device)
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.EZSP, source="zha"
|
||||
)
|
||||
# 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
|
||||
|
||||
mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": path},
|
||||
state=AddonState.NOT_RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
# Second stick is communicating exclusively with the second OTBR
|
||||
assert (
|
||||
await guess_firmware_info(hass, "/dev/serial/by-id/device2")
|
||||
) == otbr_firmware_info2
|
||||
|
||||
# We will still prefer ZHA, as it is the one actually running
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.EZSP, source="zha"
|
||||
)
|
||||
|
||||
mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": path},
|
||||
state=AddonState.RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
# Finally, ZHA loses out to OTBR
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr"
|
||||
)
|
||||
|
||||
mock_multipan_addon_manager.async_get_addon_info.side_effect = None
|
||||
mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": path},
|
||||
state=AddonState.RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
# Which will lose out to multi-PAN
|
||||
assert (await guess_firmware_type(hass, path)) == FirmwareGuess(
|
||||
is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol"
|
||||
)
|
||||
# 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_probe_silabs_firmware_type() -> None:
|
||||
"""Test probing Silabs firmware type."""
|
||||
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.Flasher.probe_app_type",
|
||||
side_effect=RuntimeError,
|
||||
):
|
||||
assert (await probe_silabs_firmware_type("/dev/ttyUSB0")) is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.Flasher.probe_app_type",
|
||||
side_effect=lambda self: setattr(self, "app_type", FlasherApplicationType.EZSP),
|
||||
autospec=True,
|
||||
) as mock_probe_app_type:
|
||||
# The application type constant is converted back and forth transparently
|
||||
result = await probe_silabs_firmware_type(
|
||||
"/dev/ttyUSB0", probe_methods=[ApplicationType.EZSP]
|
||||
"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 result is ApplicationType.EZSP
|
||||
assert (await owning_addon.is_running(hass)) is True
|
||||
|
||||
flasher = mock_probe_app_type.mock_calls[0].args[0]
|
||||
assert flasher._probe_methods == [FlasherApplicationType.EZSP]
|
||||
# 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_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_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
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareGuess,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -32,11 +32,13 @@ async def test_config_entry_migration_v2(hass: HomeAssistant) -> None:
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.guess_firmware_type",
|
||||
return_value=FirmwareGuess(
|
||||
is_running=True,
|
||||
"homeassistant.components.homeassistant_sky_connect.guess_firmware_info",
|
||||
return_value=FirmwareInfo(
|
||||
device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
firmware_version=None,
|
||||
firmware_type=ApplicationType.SPINEL,
|
||||
source="otbr",
|
||||
owners=[],
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components import zha
|
||||
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareGuess,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -49,11 +49,13 @@ async def test_setup_entry(
|
||||
return_value=onboarded,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.guess_firmware_type",
|
||||
return_value=FirmwareGuess( # Nothing is setup
|
||||
is_running=False,
|
||||
"homeassistant.components.homeassistant_yellow.guess_firmware_info",
|
||||
return_value=FirmwareInfo( # Nothing is setup
|
||||
device="/dev/ttyAMA1",
|
||||
firmware_version=None,
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
source="unknown",
|
||||
owners=[],
|
||||
),
|
||||
),
|
||||
):
|
||||
|
||||
@@ -155,6 +155,7 @@ async def zigpy_app_controller():
|
||||
app.state.node_info.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32")
|
||||
app.state.node_info.manufacturer = "Coordinator Manufacturer"
|
||||
app.state.node_info.model = "Coordinator Model"
|
||||
app.state.node_info.version = "7.1.4.0 build 389"
|
||||
app.state.network_info.pan_id = 0x1234
|
||||
app.state.network_info.extended_pan_id = app.state.node_info.ieee
|
||||
app.state.network_info.channel = 15
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
'manufacturer': 'Coordinator Manufacturer',
|
||||
'model': 'Coordinator Model',
|
||||
'nwk': 0,
|
||||
'version': None,
|
||||
'version': '7.1.4.0 build 389',
|
||||
}),
|
||||
}),
|
||||
'config': dict({
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Test Home Assistant Hardware platform for ZHA."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from zigpy.application import ControllerApplication
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_register_firmware_info_callback,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
OwningIntegration,
|
||||
)
|
||||
from homeassistant.components.zha.homeassistant_hardware import get_firmware_info
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_get_firmware_info_normal(hass: HomeAssistant) -> None:
|
||||
"""Test `get_firmware_info`."""
|
||||
|
||||
zha = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"device": {
|
||||
"path": "/dev/ttyUSB1",
|
||||
"baudrate": 115200,
|
||||
"flow_control": None,
|
||||
},
|
||||
"radio_type": "ezsp",
|
||||
},
|
||||
version=4,
|
||||
)
|
||||
zha.add_to_hass(hass)
|
||||
zha.mock_state(hass, ConfigEntryState.LOADED)
|
||||
|
||||
# With ZHA running
|
||||
with patch(
|
||||
"homeassistant.components.zha.homeassistant_hardware.get_zha_gateway"
|
||||
) as mock_get_zha_gateway:
|
||||
mock_get_zha_gateway.return_value.state.node_info.version = "1.2.3.4"
|
||||
fw_info_running = get_firmware_info(hass, zha)
|
||||
|
||||
assert fw_info_running == FirmwareInfo(
|
||||
device="/dev/ttyUSB1",
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
firmware_version="1.2.3.4",
|
||||
source="zha",
|
||||
owners=[OwningIntegration(config_entry_id=zha.entry_id)],
|
||||
)
|
||||
assert await fw_info_running.is_running(hass) is True
|
||||
|
||||
# With ZHA not running
|
||||
zha.mock_state(hass, ConfigEntryState.NOT_LOADED)
|
||||
fw_info_not_running = get_firmware_info(hass, zha)
|
||||
|
||||
assert fw_info_not_running == FirmwareInfo(
|
||||
device="/dev/ttyUSB1",
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
firmware_version=None,
|
||||
source="zha",
|
||||
owners=[OwningIntegration(config_entry_id=zha.entry_id)],
|
||||
)
|
||||
assert await fw_info_not_running.is_running(hass) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
[
|
||||
# Missing data
|
||||
{},
|
||||
# Bad radio type
|
||||
{"device": {"path": "/dev/ttyUSB1"}, "radio_type": "znp"},
|
||||
],
|
||||
)
|
||||
async def test_get_firmware_info_errors(
|
||||
hass: HomeAssistant, data: dict[str, str | int | None]
|
||||
) -> None:
|
||||
"""Test `get_firmware_info` with config entry data format errors."""
|
||||
zha = MockConfigEntry(
|
||||
domain="zha",
|
||||
unique_id="some_unique_id",
|
||||
data=data,
|
||||
version=4,
|
||||
)
|
||||
zha.add_to_hass(hass)
|
||||
|
||||
assert (get_firmware_info(hass, zha)) is None
|
||||
|
||||
|
||||
async def test_hardware_firmware_info_provider_notification(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_zigpy_connect: ControllerApplication,
|
||||
) -> None:
|
||||
"""Test that the ZHA gateway provides hardware and firmware information."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
callback = MagicMock()
|
||||
async_register_firmware_info_callback(hass, "/dev/ttyUSB0", callback)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
callback.assert_called_once_with(
|
||||
FirmwareInfo(
|
||||
device="/dev/ttyUSB0",
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
firmware_version="7.1.4.0 build 389",
|
||||
source="zha",
|
||||
owners=[OwningIntegration(config_entry_id=config_entry.entry_id)],
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user