- config flow: template (A/B/F с пояснениями, значения с устройства, определение по oem_model; для неизвестных моделей — пробинг A/B/F тремя короткими сессиями) -> capabilities (23 тумблера с описаниями, дефолты из device_capabilities/num_dir/ответов) -> limits (диапазон/шаг, ручная конверсия); каждая страница с пометкой «значения определены автоматически, можно пропустить» - features.py: FeatureSet/LiveValues/device_default/resolve_features; entry.data["features"] хранит выбор пользователя; reconfigure предзаполняет; YAML-import получает дефолты - сущности создаются только для включённых фич: climate (режимы/скорости/ swing/пресеты), switch, select заслонок (caps+num_dir), датчик комнаты; исправлено появление select без ламелей - trial: caps/num_dir и presence-зонды (необязательные свойства), answered; координатор ждёт первые свойства перед созданием сущностей (feature- дефолты без гонки) - атрибут description у сущностей (HA не поддерживает тултипы) + описания фич в мастере (en/ru) - тесты: features, probing, обновлённые flow/entities/repairs; 57 components
114 lines
4.3 KiB
Python
114 lines
4.3 KiB
Python
"""Repair issue при несовпадении LAN-ключа и его fix-flow."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
|
|
from helpers import DSN, KEY_ID
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import issue_registry as ir
|
|
from homeassistant.setup import async_setup_component
|
|
from test_entities import _setup_entry
|
|
|
|
from custom_components.fglair.const import DOMAIN
|
|
from pyfglair import State
|
|
|
|
|
|
async def _wait_for(predicate, timeout: float = 20.0):
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
result = predicate()
|
|
if result:
|
|
return result
|
|
await asyncio.sleep(0.1)
|
|
raise AssertionError("условие не выполнено за отведённое время")
|
|
|
|
|
|
async def test_key_error_creates_and_fixes_issue(
|
|
hass: HomeAssistant, mock_ac, socket_enabled
|
|
):
|
|
assert await async_setup_component(hass, "repairs", {})
|
|
proc = mock_ac([], key_id=KEY_ID + 1)
|
|
entry = await _setup_entry(hass, proc.port)
|
|
runtime = entry.runtime_data
|
|
await _wait_for(lambda: runtime.client.state == State.KEY_ERROR)
|
|
|
|
registry = ir.async_get(hass)
|
|
issue_id = f"key_mismatch_{DSN}"
|
|
issue = registry.async_get_issue(DOMAIN, issue_id)
|
|
assert issue is not None
|
|
assert issue.is_fixable
|
|
assert issue.translation_key == "key_mismatch"
|
|
assert issue.translation_placeholders["device"] == "Mock AC"
|
|
|
|
flow_manager = hass.data["repairs"]["flow_manager"]
|
|
result = await flow_manager.async_init(DOMAIN, data={"issue_id": issue_id})
|
|
assert result["type"] == "form"
|
|
result = await flow_manager.async_configure(result["flow_id"], {})
|
|
assert result["type"] == "create_entry"
|
|
await hass.async_block_till_done()
|
|
# Ключ всё ещё неверен: после reload issue создан снова, а не потерян.
|
|
assert registry.async_get_issue(DOMAIN, issue_id) is not None
|
|
|
|
await hass.config_entries.async_unload(entry.entry_id)
|
|
|
|
|
|
async def test_reconfigure_fixes_key_error(
|
|
hass: HomeAssistant, mock_ac, socket_enabled
|
|
):
|
|
"""Repair-путь целиком: bad key_id → issue → reconfigure → online → issue снят."""
|
|
from homeassistant.config_entries import SOURCE_RECONFIGURE
|
|
from test_config_flow import LIMITS_INPUT, MANUAL_INPUT
|
|
|
|
proc = mock_ac(
|
|
["--set", "operation_mode=6"], key_id=KEY_ID + 1
|
|
)
|
|
entry = await _setup_entry(hass, proc.port)
|
|
runtime = entry.runtime_data
|
|
await _wait_for(lambda: runtime.client.state == State.KEY_ERROR)
|
|
|
|
registry = ir.async_get(hass)
|
|
issue_id = f"key_mismatch_{DSN}"
|
|
assert registry.async_get_issue(DOMAIN, issue_id) is not None
|
|
|
|
# Mock помнит только один app_addr; останавливаем «залипшую» сессию,
|
|
# чтобы пробная сессия config flow получила KE (штатно это делает reload).
|
|
await runtime.client.async_stop()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id},
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "manual"}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
**MANUAL_INPUT,
|
|
"host": "127.0.0.1",
|
|
"device_port": proc.port,
|
|
"lanip_key_id": KEY_ID + 1,
|
|
},
|
|
)
|
|
assert result["step_id"] == "template", result.get("errors")
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"template": "A"}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {}
|
|
)
|
|
assert result["step_id"] == "limits"
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], LIMITS_INPUT
|
|
)
|
|
assert result["type"] == "abort"
|
|
assert result["reason"] == "reconfigure_successful"
|
|
await hass.async_block_till_done()
|
|
|
|
runtime = entry.runtime_data
|
|
await _wait_for(lambda: runtime.client.state == State.ONLINE)
|
|
assert registry.async_get_issue(DOMAIN, issue_id) is None
|
|
assert entry.data["lanip_key_id"] == KEY_ID + 1
|
|
assert await hass.config_entries.async_unload(entry.entry_id)
|