ha(H2): исправления по ревью — несколько устройств, suggested values, YAML-import, типы ошибок
- critical: listen_port по умолчанию 0 (эфемерный) — несколько ConfigEntry не конфликтуют; тест двух записей на дефолтном порту - manifest: добавлен обязательный ключ codeowners - unload: set_log_handler(None) вынесен в executor (не блокирует loop); рефкаунт лог-пампа без гонки (старт после успешного setup) - config flow: возврат введённого в форму при ошибке (add_suggested_values_to_schema; секреты исключены), YAML-import подключён через async_setup + SOURCE_IMPORT, ProvisionError.kind вместо разбора русского текста, валидация device_port/key_id из JSON - trial: требует получения всех базовых свойств шаблона (не пустой ok), безопасное создание Session - py-ci.sh: отсутствие .venv-ha — ошибка (PY_CI_SKIP_HA=1 для пропуска); pytest.ini testpaths
This commit is contained in:
+21
-12
@@ -38,7 +38,15 @@ REGIONS = {
|
||||
|
||||
|
||||
class ProvisionError(Exception):
|
||||
"""Ошибка входа/списка устройств/ключа (текст — для показа пользователю)."""
|
||||
"""Ошибка provisioning (текст — для показа пользователю).
|
||||
|
||||
``kind``: "auth" | "no_devices" | "network" | "format" — для маппинга в
|
||||
ошибки config flow без разбора текста.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, kind: str = "unknown") -> None:
|
||||
super().__init__(message)
|
||||
self.kind = kind
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -102,9 +110,9 @@ async def _request_json(session, method, url, *, token=None, payload=None,
|
||||
data = None
|
||||
return resp.status, data
|
||||
except aiohttp.ClientError as err:
|
||||
raise ProvisionError(f"Сеть недоступна ({url}): {err}") from err
|
||||
raise ProvisionError(f"Сеть недоступна ({url}): {err}", "network") from err
|
||||
except TimeoutError as err:
|
||||
raise ProvisionError(f"Таймаут запроса ({url})") from err
|
||||
raise ProvisionError(f"Таймаут запроса ({url})", "network") from err
|
||||
|
||||
|
||||
async def _sign_in(session, email, password, region, base_url, insecure):
|
||||
@@ -125,10 +133,10 @@ async def _sign_in(session, email, password, region, base_url, insecure):
|
||||
)
|
||||
if status != 200 or not isinstance(data, dict):
|
||||
detail = data.get("error") if isinstance(data, dict) else data
|
||||
raise ProvisionError(f"Ошибка входа ({status}): {detail}")
|
||||
raise ProvisionError(f"Ошибка входа ({status}): {detail}", "auth")
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
raise ProvisionError("Вход выполнен без access_token")
|
||||
raise ProvisionError("Вход выполнен без access_token", "auth")
|
||||
return token
|
||||
|
||||
|
||||
@@ -139,12 +147,12 @@ async def _get_devices(session, token, region, base_url, insecure):
|
||||
token=token, insecure=insecure,
|
||||
)
|
||||
if status != 200 or not isinstance(data, list):
|
||||
raise ProvisionError(f"Ошибка списка устройств ({status}): {data}")
|
||||
raise ProvisionError(f"Ошибка списка устройств ({status}): {data}", "network")
|
||||
devices = []
|
||||
for item in data:
|
||||
dev = item.get("device") if isinstance(item, dict) else None
|
||||
if not isinstance(dev, dict) or not isinstance(dev.get("dsn"), str):
|
||||
raise ProvisionError("Облако вернуло неожиданный формат устройства")
|
||||
raise ProvisionError("Облако вернуло неожиданный формат устройства", "format")
|
||||
devices.append(dev)
|
||||
return devices
|
||||
|
||||
@@ -156,11 +164,12 @@ async def _get_lanip(session, token, region, base_url, insecure, dsn):
|
||||
token=token, insecure=insecure,
|
||||
)
|
||||
if status != 200 or not isinstance(data, dict):
|
||||
raise ProvisionError(f"Ошибка lan.json для {dsn} ({status}): {data}")
|
||||
raise ProvisionError(f"Ошибка lan.json для {dsn} ({status}): {data}", "network")
|
||||
lanip = data.get("lanip")
|
||||
if lanip is not None and not isinstance(lanip, dict):
|
||||
raise ProvisionError(
|
||||
f"Облако вернуло неожиданный lanip для {dsn}: {lanip!r}"
|
||||
f"Облако вернуло неожиданный lanip для {dsn}: {lanip!r}",
|
||||
"format",
|
||||
)
|
||||
return lanip or {}
|
||||
|
||||
@@ -176,7 +185,7 @@ async def discover(
|
||||
) -> list[Device]:
|
||||
"""Список устройств аккаунта с lanip_key (для config flow / CLI)."""
|
||||
if region not in REGIONS:
|
||||
raise ProvisionError(f"Неизвестный регион: {region}")
|
||||
raise ProvisionError(f"Неизвестный регион: {region}", "format")
|
||||
region_cfg = REGIONS[region]
|
||||
base = base_url.rstrip("/") if base_url else None
|
||||
|
||||
@@ -191,7 +200,7 @@ async def discover(
|
||||
session, token, region_cfg, base, insecure
|
||||
)
|
||||
if not raw_devices:
|
||||
raise ProvisionError("У аккаунта нет устройств")
|
||||
raise ProvisionError("У аккаунта нет устройств", "no_devices")
|
||||
out: list[Device] = []
|
||||
for dev in raw_devices:
|
||||
dsn = dev["dsn"]
|
||||
@@ -218,7 +227,7 @@ async def discover(
|
||||
)
|
||||
)
|
||||
if not out:
|
||||
raise ProvisionError("Не найдено устройств с ключом LAN")
|
||||
raise ProvisionError("Не найдено устройств с ключом LAN", "no_devices")
|
||||
return out
|
||||
finally:
|
||||
if own_session:
|
||||
|
||||
Reference in New Issue
Block a user