core/tests/components/zha/test_repairs.py

236 lines
7.8 KiB
Python

"""Test ZHA repairs."""
from collections.abc import Callable
import logging
from unittest.mock import patch
import pytest
from universal_silabs_flasher.const import ApplicationType
from universal_silabs_flasher.flasher import Flasher
from homeassistant.components.homeassistant_sky_connect import (
DOMAIN as SKYCONNECT_DOMAIN,
)
from homeassistant.components.zha.core.const import DOMAIN
from homeassistant.components.zha.repairs import (
DISABLE_MULTIPAN_URL,
ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
HardwareType,
_detect_radio_hardware,
probe_silabs_firmware_type,
warn_on_wrong_silabs_firmware,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from tests.common import MockConfigEntry
SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0"
def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]:
"""Set the app type on the flasher."""
def replacement(self: Flasher) -> None:
self.app_type = app_type
return replacement
def test_detect_radio_hardware(hass: HomeAssistant) -> None:
"""Test logic to detect radio hardware."""
skyconnect_config_entry = MockConfigEntry(
data={
"device": SKYCONNECT_DEVICE,
"vid": "10C4",
"pid": "EA60",
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
"manufacturer": "Nabu Casa",
"description": "SkyConnect v1.0",
},
domain=SKYCONNECT_DOMAIN,
options={},
title="Home Assistant SkyConnect",
)
skyconnect_config_entry.add_to_hass(hass)
assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT
assert (
_detect_radio_hardware(hass, SKYCONNECT_DEVICE + "_foo") == HardwareType.OTHER
)
assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.OTHER
with patch(
"homeassistant.components.homeassistant_yellow.hardware.get_os_info",
return_value={"board": "yellow"},
):
assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.YELLOW
assert _detect_radio_hardware(hass, "/dev/ttyAMA2") == HardwareType.OTHER
assert (
_detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT
)
def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None:
"""Test radio hardware detection failure."""
with patch(
"homeassistant.components.homeassistant_yellow.hardware.async_info",
side_effect=HomeAssistantError(),
), patch(
"homeassistant.components.homeassistant_sky_connect.hardware.async_info",
side_effect=HomeAssistantError(),
):
assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER
@pytest.mark.parametrize(
("detected_hardware", "expected_learn_more_url"),
[
(HardwareType.SKYCONNECT, DISABLE_MULTIPAN_URL[HardwareType.SKYCONNECT]),
(HardwareType.YELLOW, DISABLE_MULTIPAN_URL[HardwareType.YELLOW]),
(HardwareType.OTHER, None),
],
)
async def test_multipan_firmware_repair(
hass: HomeAssistant,
detected_hardware: HardwareType,
expected_learn_more_url: str,
config_entry: MockConfigEntry,
mock_zigpy_connect,
) -> None:
"""Test creating a repair when multi-PAN firmware is installed and probed."""
config_entry.add_to_hass(hass)
# ZHA fails to set up
with patch(
"homeassistant.components.zha.repairs.Flasher.probe_app_type",
side_effect=set_flasher_app_type(ApplicationType.CPC),
autospec=True,
), patch(
"homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize",
side_effect=RuntimeError(),
), patch(
"homeassistant.components.zha.repairs._detect_radio_hardware",
return_value=detected_hardware,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_ERROR
await hass.config_entries.async_unload(config_entry.entry_id)
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
)
# The issue is created when we fail to probe
assert issue is not None
assert issue.translation_placeholders["firmware_type"] == "CPC"
assert issue.learn_more_url == expected_learn_more_url
# If ZHA manages to start up normally after this, the issue will be deleted
with mock_zigpy_connect:
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
issue = issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
)
assert issue is None
async def test_multipan_firmware_no_repair_on_probe_failure(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test that a repair is not created when multi-PAN firmware cannot be probed."""
config_entry.add_to_hass(hass)
# ZHA fails to set up
with patch(
"homeassistant.components.zha.repairs.Flasher.probe_app_type",
side_effect=set_flasher_app_type(None),
autospec=True,
), patch(
"homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize",
side_effect=RuntimeError(),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_ERROR
await hass.config_entries.async_unload(config_entry.entry_id)
# No repair is created
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
)
assert issue is None
async def test_multipan_firmware_retry_on_probe_ezsp(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_zigpy_connect,
) -> None:
"""Test that ZHA is reloaded when EZSP firmware is probed."""
config_entry.add_to_hass(hass)
# ZHA fails to set up
with patch(
"homeassistant.components.zha.repairs.Flasher.probe_app_type",
side_effect=set_flasher_app_type(ApplicationType.EZSP),
autospec=True,
), patch(
"homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize",
side_effect=RuntimeError(),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# The config entry state is `SETUP_RETRY`, not `SETUP_ERROR`!
assert config_entry.state == ConfigEntryState.SETUP_RETRY
await hass.config_entries.async_unload(config_entry.entry_id)
# No repair is created
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(
domain=DOMAIN,
issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED,
)
assert issue is None
async def test_no_warn_on_socket(hass: HomeAssistant) -> None:
"""Test that no warning is issued when the device is a socket."""
with patch(
"homeassistant.components.zha.repairs.probe_silabs_firmware_type", autospec=True
) as mock_probe:
await warn_on_wrong_silabs_firmware(hass, device="socket://1.2.3.4:5678")
mock_probe.assert_not_called()
async def test_probe_failure_exception_handling(caplog) -> None:
"""Test that probe failures are handled gracefully."""
with patch(
"homeassistant.components.zha.repairs.Flasher.probe_app_type",
side_effect=RuntimeError(),
), caplog.at_level(logging.DEBUG):
await probe_silabs_firmware_type("/dev/ttyZigbee")
assert "Failed to probe application type" in caplog.text