ha(ux): мастер из трёх страниц, feature-модель и capabilities-фильтрация
- config flow: template (A/B/F с пояснениями, значения с устройства, определение по oem_model; для неизвестных моделей — пробинг A/B/F тремя короткими сессиями) -> capabilities (23 тумблера с описаниями, дефолты из device_capabilities/num_dir/ответов) -> limits (диапазон/шаг, ручная конверсия); каждая страница с пометкой «значения определены автоматически, можно пропустить» - features.py: FeatureSet/LiveValues/device_default/resolve_features; entry.data["features"] хранит выбор пользователя; reconfigure предзаполняет; YAML-import получает дефолты - сущности создаются только для включённых фич: climate (режимы/скорости/ swing/пресеты), switch, select заслонок (caps+num_dir), датчик комнаты; исправлено появление select без ламелей - trial: caps/num_dir и presence-зонды (необязательные свойства), answered; координатор ждёт первые свойства перед созданием сущностей (feature- дефолты без гонки) - атрибут description у сущностей (HA не поддерживает тултипы) + описания фич в мастере (en/ru) - тесты: features, probing, обновлённые flow/entities/repairs; 57 components
This commit is contained in:
@@ -12,7 +12,7 @@ from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from pyfglair import Error, Prop, PropertyEvent, State, Template, Value, ValueKind
|
||||
from pyfglair.templates import template_info
|
||||
from pyfglair.templates import prop_info, template_info
|
||||
|
||||
from .const import (
|
||||
AVAILABLE_STATES,
|
||||
@@ -21,6 +21,7 @@ from .const import (
|
||||
CONF_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from .features import FeatureSet, LiveValues
|
||||
from .fglair_client import FglairClient
|
||||
from .overrides import ConversionSet
|
||||
|
||||
@@ -40,6 +41,7 @@ class FglairRuntime:
|
||||
client: FglairClient
|
||||
coordinator: "FglairCoordinator"
|
||||
conversions: ConversionSet
|
||||
features: "FeatureSet"
|
||||
|
||||
|
||||
def key_issue_id(dsn: str) -> str:
|
||||
@@ -70,6 +72,9 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
|
||||
)
|
||||
self.client = client
|
||||
self.conversions = conversions
|
||||
self.features = FeatureSet(
|
||||
explicit=None, values=LiveValues(client.cached)
|
||||
)
|
||||
self.entry_id = entry_id
|
||||
self.key_issue_id = key_issue_id(client.data[CONF_DSN])
|
||||
self._synced = False
|
||||
@@ -118,8 +123,19 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
|
||||
# После разрыва при следующем ONLINE нужна повторная синхронизация.
|
||||
self._synced = False
|
||||
|
||||
async def async_sync_now(self) -> None:
|
||||
"""Гарантирует первичный GET-батч (если уже online)."""
|
||||
async def async_sync_now(self, timeout: float = 2.0) -> None:
|
||||
"""Гарантирует первичный GET-батч и ожидание ключевых свойств.
|
||||
|
||||
Ждёт ONLINE (ограниченно), затем — завершения синхронизации, чтобы
|
||||
сущности создавались с корректными feature-дефолтами.
|
||||
"""
|
||||
deadline = self.hass.loop.time() + timeout
|
||||
while (
|
||||
self._sync_task is None
|
||||
and self.client.state != State.ONLINE
|
||||
and self.hass.loop.time() < deadline
|
||||
):
|
||||
await asyncio.sleep(0.05)
|
||||
if self._sync_task is not None:
|
||||
await self._sync_task
|
||||
elif self.client.state == State.ONLINE and not self._synced:
|
||||
@@ -169,12 +185,35 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
|
||||
async def _async_initial_sync(self) -> None:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self._initial_sync)
|
||||
await self._async_wait_first_props()
|
||||
self._synced = True
|
||||
except Exception: # pragma: no cover - защитный путь
|
||||
_LOGGER.exception("первичная синхронизация свойств не удалась")
|
||||
finally:
|
||||
self._sync_task = None
|
||||
|
||||
async def _async_wait_first_props(self, timeout: float = 3.0) -> None:
|
||||
"""Ждёт ключевые свойства (caps/режим/num_dir), чтобы сущности
|
||||
создавались с корректными feature-дефолтами."""
|
||||
template = self.client.template
|
||||
wanted = [
|
||||
Prop.OPERATION_MODE,
|
||||
Prop.DEVICE_CAPABILITIES,
|
||||
Prop.AF_VERTICAL_NUM_DIR,
|
||||
Prop.AF_HORIZONTAL_NUM_DIR,
|
||||
Prop.DISPLAY_TEMPERATURE,
|
||||
Prop.HUMAN_DET_AUTO_SAVE,
|
||||
Prop.WIFI_LED_ENABLE,
|
||||
]
|
||||
wanted = [
|
||||
prop for prop in wanted if prop_info(template, prop) is not None
|
||||
]
|
||||
deadline = self.hass.loop.time() + timeout
|
||||
while self.hass.loop.time() < deadline:
|
||||
if all(self.client.cached(prop) is not None for prop in wanted):
|
||||
return
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
def _initial_sync(self) -> None:
|
||||
self.client.batch_begin()
|
||||
for prop in _writable_props(self.client.template):
|
||||
|
||||
Reference in New Issue
Block a user