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:
puddly
2025-02-06 14:46:07 -05:00
committed by GitHub
parent 75772ae40f
commit 2e8bc56be4
18 changed files with 923 additions and 241 deletions
@@ -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=[],
),
),
):
+1
View File
@@ -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)],
)
)