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:
2026-09-29 14:06:39 +03:00
parent dee5819743
commit 57ee555704
15 changed files with 451 additions and 10 deletions
+34 -2
View File
@@ -8,7 +8,7 @@ from functools import partial
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.helpers import selector
import pyfglair
@@ -259,6 +259,29 @@ class FglairConfigFlow(ConfigFlow, domain=DOMAIN):
self._devices: list[Device] = []
self._pending_data: dict[str, Any] = {}
self._trial_properties: dict[Any, Any] = {}
self._reconfigure_entry: ConfigEntry | None = None
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Повторная настройка (в т.ч. смена LAN-ключа после key_error)."""
self._reconfigure_entry = self._get_reconfigure_entry()
return await self.async_step_user()
def _reconfigure_suggestions(self) -> dict[str, Any] | None:
entry = self._reconfigure_entry
if entry is None:
return None
fields = (
CONF_HOST,
CONF_DEVICE_PORT,
CONF_DSN,
CONF_LANIP_KEY,
CONF_LANIP_KEY_ID,
CONF_NAME,
CONF_MODEL,
)
return {key: entry.data[key] for key in fields if key in entry.data}
def _show_form(
self,
@@ -366,6 +389,8 @@ class FglairConfigFlow(ConfigFlow, domain=DOMAIN):
if error is None:
return await self.async_step_preview()
errors["base"] = error
if user_input is None and self._reconfigure_entry is not None:
user_input = self._reconfigure_suggestions()
return self._show_form(
"manual", _manual_schema(), errors, user_input,
exclude=(CONF_LANIP_KEY,),
@@ -472,7 +497,10 @@ class FglairConfigFlow(ConfigFlow, domain=DOMAIN):
async def _async_trial(self, data: dict[str, Any]) -> str | None:
"""None — сессия поднялась; иначе ключ ошибки формы."""
await self.async_set_unique_id(data[CONF_DSN], raise_on_progress=False)
self._abort_if_unique_id_configured()
if self._reconfigure_entry is not None:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
result: TrialResult = await self.hass.async_add_executor_job(
partial(_run_trial, data)
)
@@ -488,4 +516,8 @@ class FglairConfigFlow(ConfigFlow, domain=DOMAIN):
def _create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
title = data.get(CONF_NAME) or data[CONF_DSN]
if self._reconfigure_entry is not None:
return self.async_update_reload_and_abort(
self._reconfigure_entry, title=title, data=data
)
return self.async_create_entry(title=title, data=data)