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
+95
View File
@@ -0,0 +1,95 @@
"""Диагностика fglair: параметры устройства для копирования в ESPHome.
План §3: на странице устройства видны dsn/lanip_key/lanip_key_id/host —
источник для secrets.yaml ESPHome; рядом маскированная копия ключа для
пересылки дампов. Дамп доступен только администратору HA; полный ключ
включён намеренно (это статический ключ модуля, не пароль облака).
"""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from pyfglair.templates import template_info
from .const import (
CONF_DEVICE_PORT,
CONF_DSN,
CONF_HOST,
CONF_LANIP_KEY,
CONF_LANIP_KEY_ID,
CONF_LISTEN_PORT,
CONF_MODEL,
CONF_NAME,
CONF_TEMP_STEP,
CONF_TEMPLATE,
)
from .coordinator import FglairRuntime
TO_REDACT = {CONF_LANIP_KEY, "password", "email"}
REDACTED = "**REDACTED**"
def _masked(value: Any) -> Any:
if not isinstance(value, str) or not value:
return REDACTED
if len(value) <= 4:
return REDACTED
return f"{value[:4]}…{REDACTED}"
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
runtime: FglairRuntime = entry.runtime_data
client = runtime.client
data = client.data
key = entry.data.get(CONF_LANIP_KEY)
values: dict[str, Any] = {}
for info in template_info(client.template):
value = client.cached(info.prop)
if value is None:
continue
values[info.name] = {
"raw": value.int_value,
"display": runtime.conversions.to_display(
info.prop, value.int_value
),
}
return {
"config": {
CONF_HOST: data.get(CONF_HOST),
CONF_DEVICE_PORT: data.get(CONF_DEVICE_PORT),
CONF_DSN: data.get(CONF_DSN),
CONF_NAME: data.get(CONF_NAME),
CONF_MODEL: data.get(CONF_MODEL),
CONF_TEMPLATE: data.get(CONF_TEMPLATE),
CONF_TEMP_STEP: data.get(CONF_TEMP_STEP),
CONF_LISTEN_PORT: data.get(CONF_LISTEN_PORT),
},
"esp_home": {
CONF_DSN: data.get(CONF_DSN),
CONF_LANIP_KEY: key,
CONF_LANIP_KEY_ID: data.get(CONF_LANIP_KEY_ID),
CONF_HOST: data.get(CONF_HOST),
},
"esp_home_masked": {
CONF_DSN: data.get(CONF_DSN),
CONF_LANIP_KEY: _masked(key),
CONF_LANIP_KEY_ID: data.get(CONF_LANIP_KEY_ID),
CONF_HOST: data.get(CONF_HOST),
},
"entry_data": async_redact_data(dict(entry.data), TO_REDACT),
"overrides": entry.data.get("overrides", {}),
"runtime": {
"state": client.state.name.lower(),
"last_error": client.last_error.name.lower(),
"events_dropped": client.events_dropped,
"values": values,
},
}