ha(H4): repair key_mismatch, диагностика для ESPHome, reconfigure, переводы
- repairs: issue key_mismatch (ERROR, fixable) при состоянии key_error, ConfirmRepairFlow, снятие issue при online/unload/remove - config flow: async_step_reconfigure (меню, prefill ручной формы, unique_id_mismatch, update_reload_and_abort) — путь «перепровижинировать» - diagnostics: config/esp_home (dsn/lanip_key/key_id/host для secrets.yaml) + esp_home_masked и redacted entry_data, runtime-снимок значений (raw/display) из кэша ядра - translations: issues.key_mismatch (title/description/fix_flow) en/ru, abort unique_id_mismatch/reconfigure_successful; тест согласованности strings↔en↔ru - FglairClient.events_dropped; mock_ac: параметр key_id в фикстуре - тесты: diagnostics, repairs issue+fix-flow, reconfigure (успех/чужое устройство), translations (45 components)
This commit is contained in:
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
from helpers import DSN, KEY_ID, LANIP_KEY
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import SOURCE_RECONFIGURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
@@ -413,3 +414,68 @@ async def test_import_json_null_device_port(hass: HomeAssistant):
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"]["device_port"] == 80
|
||||
assert trial.call_args.args[0]["device_port"] == 80
|
||||
|
||||
|
||||
RECONFIGURE_ENTRY_DATA = {
|
||||
"host": "127.0.0.1",
|
||||
"device_port": 9,
|
||||
"dsn": DSN,
|
||||
"lanip_key": LANIP_KEY,
|
||||
"lanip_key_id": KEY_ID,
|
||||
"name": "Old name",
|
||||
"model": "AP-WC1E",
|
||||
"template": "A",
|
||||
}
|
||||
|
||||
|
||||
async def test_reconfigure_success(hass: HomeAssistant, mock_ac, socket_enabled):
|
||||
from test_entities import _setup_entry
|
||||
|
||||
proc = mock_ac(["--set", "operation_mode=6"])
|
||||
entry = await _setup_entry(hass, proc.port)
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_OK,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id},
|
||||
)
|
||||
assert result["type"] == "menu"
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
assert result["type"] == "form"
|
||||
assert _suggested(result["data_schema"], "host") == "127.0.0.1"
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {**MANUAL_INPUT, "name": "Renamed"}
|
||||
)
|
||||
assert result["step_id"] == "preview"
|
||||
result = await _configure(hass, result["flow_id"], PREVIEW_INPUT)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
await hass.async_block_till_done()
|
||||
assert entry.data["name"] == "Renamed"
|
||||
assert entry.state.name == "LOADED"
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_reconfigure_wrong_device(hass: HomeAssistant):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=RECONFIGURE_ENTRY_DATA, unique_id=DSN
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_OK,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id},
|
||||
)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await _configure(
|
||||
hass,
|
||||
result["flow_id"],
|
||||
{**MANUAL_INPUT, "dsn": "AC000W00OTHER001"},
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Диагностика: параметры для ESPHome и маскировка ключа."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from helpers import DSN, LANIP_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from test_entities import MOCK_SETTINGS, _setup_entry
|
||||
|
||||
from custom_components.fglair.diagnostics import (
|
||||
async_get_config_entry_diagnostics,
|
||||
)
|
||||
from pyfglair import Prop, 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_diagnostics(hass: HomeAssistant, mock_ac, socket_enabled):
|
||||
proc = mock_ac(MOCK_SETTINGS)
|
||||
entry = await _setup_entry(hass, proc.port)
|
||||
runtime = entry.runtime_data
|
||||
await _wait_for(
|
||||
lambda: runtime.client.state == State.ONLINE
|
||||
and runtime.client.cached(Prop.OPERATION_MODE) is not None
|
||||
and runtime.client.cached(Prop.DISPLAY_TEMPERATURE) is not None
|
||||
)
|
||||
|
||||
diag = await async_get_config_entry_diagnostics(hass, entry)
|
||||
assert diag["config"]["dsn"] == DSN
|
||||
assert diag["config"]["host"] == "127.0.0.1"
|
||||
assert diag["config"]["template"] == "A"
|
||||
|
||||
assert diag["esp_home"]["lanip_key"] == LANIP_KEY
|
||||
assert diag["esp_home"]["lanip_key_id"] == 64201
|
||||
assert diag["esp_home_masked"]["lanip_key"] != LANIP_KEY
|
||||
assert "REDACTED" in diag["esp_home_masked"]["lanip_key"]
|
||||
assert diag["entry_data"]["lanip_key"] == "**REDACTED**"
|
||||
|
||||
assert diag["runtime"]["state"] == "online"
|
||||
assert diag["runtime"]["events_dropped"] == 0
|
||||
assert diag["runtime"]["values"]["operation_mode"]["raw"] == 6
|
||||
assert diag["runtime"]["values"]["operation_mode"]["display"] == 6
|
||||
assert (
|
||||
diag["runtime"]["values"]["display_temperature"]["display"] == 200
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""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"
|
||||
assert registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Согласованность strings.json / translations en/ru."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
COMPONENT = ROOT / "custom_components" / "fglair"
|
||||
|
||||
|
||||
def _keys(node, prefix: str = "") -> set[str]:
|
||||
keys: set[str] = set()
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
keys.add(prefix + key)
|
||||
keys |= _keys(value, prefix + key + ".")
|
||||
return keys
|
||||
|
||||
|
||||
def test_en_matches_strings():
|
||||
strings = json.loads((COMPONENT / "strings.json").read_text())
|
||||
english = json.loads((COMPONENT / "translations" / "en.json").read_text())
|
||||
assert strings == english
|
||||
|
||||
|
||||
def test_ru_covers_all_keys():
|
||||
strings = json.loads((COMPONENT / "strings.json").read_text())
|
||||
russian = json.loads((COMPONENT / "translations" / "ru.json").read_text())
|
||||
assert _keys(strings) == _keys(russian)
|
||||
+4
-2
@@ -146,13 +146,15 @@ async def cloud():
|
||||
def mock_ac():
|
||||
procs: list[Proc] = []
|
||||
|
||||
def start(extra: list[str] | None = None) -> Proc:
|
||||
def start(
|
||||
extra: list[str] | None = None, key_id: int | None = None
|
||||
) -> Proc:
|
||||
port = free_port()
|
||||
proc = Proc([
|
||||
sys.executable, str(MOCK_AC),
|
||||
"--port", str(port),
|
||||
"--lanip-key", LANIP_KEY,
|
||||
"--key-id", str(KEY_ID),
|
||||
"--key-id", str(KEY_ID if key_id is None else key_id),
|
||||
*(extra or []),
|
||||
])
|
||||
procs.append(proc)
|
||||
|
||||
Reference in New Issue
Block a user