ha(H2): ниты повторного ревью — pump без параллельных читателей, pending-рефкаунт, точная ошибка свойств

- session.py: новый лог-памп не поднимается, пока предыдущий не завершился
  (stopping/is_alive), параллельное чтение кольца исключено
- __init__: счётчик pending закрывает окно гонки setup/unload последней
  записи; warning при не-словарном fglair: в YAML
- trial: properties_missing — отдельная ошибка формы/abort (en/ru/strings)
- ConfigEntryNotReady: «порт 0» больше не показывается
- тесты: контраст одинакового явного порта, неблокирующий unload (тикер loop)
- план H5: публикация pyfglair именно на PyPI (manylinux x86_64/aarch64)
This commit is contained in:
2026-09-29 12:29:35 +03:00
parent da01c607a6
commit 29f62b639c
9 changed files with 109 additions and 26 deletions
+22 -9
View File
@@ -24,6 +24,7 @@ _LOG_LEVELS = {
}
DATA_CLIENTS = f"{DOMAIN}_clients"
DATA_PENDING = f"{DOMAIN}_pending"
def _log_handler(level: int, message: str) -> None:
@@ -31,22 +32,34 @@ def _log_handler(level: int, message: str) -> None:
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
if isinstance(config.get(DOMAIN), dict):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config[DOMAIN],
)
if DOMAIN not in config:
return True
if not isinstance(config[DOMAIN], dict):
_LOGGER.warning(
"fglair: ожидался словарь параметров устройства, получено %s",
type(config[DOMAIN]).__name__,
)
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config[DOMAIN],
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
clients: dict[str, FglairClient] = hass.data.setdefault(DATA_CLIENTS, {})
hass.data[DATA_PENDING] = hass.data.get(DATA_PENDING, 0) + 1
client = FglairClient(hass, entry.data)
await client.async_start()
try:
await client.async_start()
finally:
hass.data[DATA_PENDING] -= 1
clients[entry.entry_id] = client
entry.runtime_data = client
if len(clients) == 1:
@@ -64,6 +77,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
clients: dict[str, FglairClient] = hass.data.get(DATA_CLIENTS, {})
clients.pop(entry.entry_id, None)
if not clients:
if not clients and not hass.data.get(DATA_PENDING, 0):
await hass.async_add_executor_job(pyfglair.set_log_handler, None)
return True
+1 -1
View File
@@ -36,5 +36,5 @@ TRIAL_ERRORS = {
"unreachable": "cannot_connect",
"timeout": "timeout",
"start_failed": "cannot_connect",
"properties_missing": "cannot_connect",
"properties_missing": "properties_missing",
}
+3 -2
View File
@@ -155,9 +155,10 @@ class FglairClient:
raise
if not started:
await session.async_stop()
port = self._data.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT)
raise ConfigEntryNotReady(
"не удалось запустить локальный HTTP-сервер FGLair "
f"(порт {self._data.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT)})"
"не удалось запустить локальный HTTP-сервер FGLair"
+ (f" (порт {port})" if port else "")
)
self._session = session
self._state = session.state
+4 -2
View File
@@ -61,7 +61,8 @@
"invalid_auth": "Invalid email or password",
"invalid_import": "Invalid config JSON",
"no_devices": "No devices found in the account",
"unknown": "Unexpected error"
"unknown": "Unexpected error",
"properties_missing": "The device connected but did not report its base properties"
},
"abort": {
"already_configured": "This device is already configured",
@@ -69,7 +70,8 @@
"cannot_connect": "Could not connect to the device",
"no_slots": "Both LAN slots are busy",
"key_mismatch": "LAN key does not match the device",
"timeout": "Timed out waiting for the device"
"timeout": "Timed out waiting for the device",
"properties_missing": "The device connected but did not report its base properties"
}
}
}
@@ -61,7 +61,8 @@
"invalid_auth": "Invalid email or password",
"invalid_import": "Invalid config JSON",
"no_devices": "No devices found in the account",
"unknown": "Unexpected error"
"unknown": "Unexpected error",
"properties_missing": "The device connected but did not report its base properties"
},
"abort": {
"already_configured": "This device is already configured",
@@ -69,7 +70,8 @@
"cannot_connect": "Could not connect to the device",
"no_slots": "Both LAN slots are busy",
"key_mismatch": "LAN key does not match the device",
"timeout": "Timed out waiting for the device"
"timeout": "Timed out waiting for the device",
"properties_missing": "The device connected but did not report its base properties"
}
}
}
@@ -61,7 +61,8 @@
"invalid_auth": "Неверный e-mail или пароль",
"invalid_import": "Некорректный config JSON",
"no_devices": "В аккаунте не найдено устройств",
"unknown": "Неожиданная ошибка"
"unknown": "Неожиданная ошибка",
"properties_missing": "Устройство подключилось, но не отдало базовые свойства"
},
"abort": {
"already_configured": "Устройство уже настроено",
@@ -69,7 +70,8 @@
"cannot_connect": "Не удалось подключиться к устройству",
"no_slots": "Оба LAN-слота заняты",
"key_mismatch": "LAN-ключ не совпадает с устройством",
"timeout": "Устройство не вышло на связь за отведённое время"
"timeout": "Устройство не вышло на связь за отведённое время",
"properties_missing": "Устройство подключилось, но не отдало базовые свойства"
}
}
}