core/tests/components/esphome/test_repairs.py
2025-04-14 07:10:05 -10:00

259 lines
8.4 KiB
Python

"""Test ESPHome repairs."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from unittest.mock import AsyncMock
from aioesphomeapi import (
APIClient,
BinarySensorInfo,
BinarySensorState,
DeviceInfo,
EntityInfo,
EntityState,
UserService,
)
import pytest
from homeassistant.components.esphome import repairs
from homeassistant.components.esphome.const import DOMAIN
from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from .conftest import MockESPHomeDevice
from tests.common import MockConfigEntry
from tests.components.repairs import (
async_process_repairs_platforms,
get_repairs,
process_repair_fix_flow,
start_repair_fix_flow,
)
from tests.typing import ClientSessionGenerator, WebSocketGenerator
async def test_create_fix_flow_raises_on_unknown_issue_id(hass: HomeAssistant) -> None:
"""Test create_fix_flow raises on unknown issue_id."""
with pytest.raises(ValueError):
await repairs.async_create_fix_flow(hass, "no_such_issue", None)
async def test_device_conflict_manual(
hass: HomeAssistant,
mock_client: APIClient,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
mock_config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test guided manual conflict resolution."""
disconnect_done = hass.loop.create_future()
async def async_disconnect(*args, **kwargs) -> None:
disconnect_done.set_result(None)
mock_client.disconnect = async_disconnect
mock_client.device_info = AsyncMock(
return_value=DeviceInfo(
mac_address="1122334455ab", name="test", model="esp32-iso-poe"
)
)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
async with asyncio.timeout(1):
await disconnect_done
assert "Unexpected device found" in caplog.text
issue_id = DEVICE_CONFLICT_ISSUE_FORMAT.format(mock_config_entry.entry_id)
issues = await get_repairs(hass, hass_ws_client)
assert issues
assert len(issues) == 1
assert any(True for issue in issues if issue["issue_id"] == issue_id)
await async_process_repairs_platforms(hass)
client = await hass_client()
data = await start_repair_fix_flow(client, DOMAIN, issue_id)
flow_id = data["flow_id"]
assert data["description_placeholders"] == {
"ip": "192.168.1.2",
"mac": "11:22:33:44:55:ab",
"model": "esp32-iso-poe",
"name": "test",
"stored_mac": "11:22:33:44:55:aa",
}
assert data["type"] == FlowResultType.MENU
assert data["step_id"] == "init"
data = await process_repair_fix_flow(
client, flow_id, json={"next_step_id": "manual"}
)
flow_id = data["flow_id"]
assert data["description_placeholders"] == {
"ip": "192.168.1.2",
"mac": "11:22:33:44:55:ab",
"model": "esp32-iso-poe",
"name": "test",
"stored_mac": "11:22:33:44:55:aa",
}
assert data["type"] == FlowResultType.FORM
assert data["step_id"] == "manual"
mock_client.device_info = AsyncMock(
return_value=DeviceInfo(
mac_address="11:22:33:44:55:aa", name="test", model="esp32-iso-poe"
)
)
caplog.clear()
data = await process_repair_fix_flow(client, flow_id)
assert data["type"] == FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
assert "Unexpected device found" not in caplog.text
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
async def test_device_conflict_migration(
hass: HomeAssistant,
mock_client: APIClient,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
issue_registry: ir.IssueRegistry,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
caplog: pytest.LogCaptureFixture,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test migrating existing configuration to new hardware."""
entity_info = [
BinarySensorInfo(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
is_status_binary_sensor=True,
)
]
states = [BinarySensorState(key=1, state=None)]
user_service = []
device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_ON
mock_config_entry = device.entry
ent_reg_entry = entity_registry.async_get("binary_sensor.test_mybinary_sensor")
assert ent_reg_entry
assert ent_reg_entry.unique_id == "11:22:33:44:55:AA-binary_sensor-mybinary_sensor"
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries is not None
for entry in entries:
assert entry.unique_id.startswith("11:22:33:44:55:AA-")
disconnect_done = hass.loop.create_future()
async def async_disconnect(*args, **kwargs) -> None:
if not disconnect_done.done():
disconnect_done.set_result(None)
mock_client.disconnect = async_disconnect
new_device_info = DeviceInfo(
mac_address="11:22:33:44:55:AB", name="test", model="esp32-iso-poe"
)
mock_client.device_info = AsyncMock(return_value=new_device_info)
device.device_info = new_device_info
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
async with asyncio.timeout(1):
await disconnect_done
assert "Unexpected device found" in caplog.text
issue_id = DEVICE_CONFLICT_ISSUE_FORMAT.format(mock_config_entry.entry_id)
issues = await get_repairs(hass, hass_ws_client)
assert issues
assert len(issues) == 1
assert any(True for issue in issues if issue["issue_id"] == issue_id)
await async_process_repairs_platforms(hass)
client = await hass_client()
data = await start_repair_fix_flow(client, DOMAIN, issue_id)
flow_id = data["flow_id"]
assert data["description_placeholders"] == {
"ip": "test.local",
"mac": "11:22:33:44:55:ab",
"model": "esp32-iso-poe",
"name": "test",
"stored_mac": "11:22:33:44:55:aa",
}
assert data["type"] == FlowResultType.MENU
assert data["step_id"] == "init"
data = await process_repair_fix_flow(
client, flow_id, json={"next_step_id": "migrate"}
)
flow_id = data["flow_id"]
assert data["description_placeholders"] == {
"ip": "test.local",
"mac": "11:22:33:44:55:ab",
"model": "esp32-iso-poe",
"name": "test",
"stored_mac": "11:22:33:44:55:aa",
}
assert data["type"] == FlowResultType.FORM
assert data["step_id"] == "migrate"
caplog.clear()
data = await process_repair_fix_flow(client, flow_id)
assert data["type"] == FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
assert "Unexpected device found" not in caplog.text
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
assert mock_config_entry.unique_id == "11:22:33:44:55:ab"
ent_reg_entry = entity_registry.async_get("binary_sensor.test_mybinary_sensor")
assert ent_reg_entry
assert ent_reg_entry.unique_id == "11:22:33:44:55:AB-binary_sensor-mybinary_sensor"
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries is not None
for entry in entries:
assert entry.unique_id.startswith("11:22:33:44:55:AB-")
dev_entry = device_registry.async_get_device(
identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:44:55:ab")}
)
assert dev_entry is not None
old_dev_entry = device_registry.async_get_device(
identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:44:55:aa")}
)
assert old_dev_entry is None