ha(H3): исправления по ревью — turn_on/off, кламп конверсий, reload, гонка ONLINE

- M1: supported_features += TURN_ON/TURN_OFF (сервисы в HA 2026.9),
  async_turn_off; удалён мёртвый _enable_turn_on_off_backwards_compat;
  тест turn_on/turn_off → operation_mode 1/0
- M2: достижимый диапазон = override ∩ raw-таблица шаблона; raw клампится
  в from_display, clamp() по достижимому диапазону (молчаливый кламп ядра
  устранён); тесты scaled override
- M3: mock_ac сбрасывает crypto/active на DELETE; тест reload → снова ONLINE
- гонка: async_sync_now больше не перезаписывает данные устаревшим снимком
  (ONLINE мог потеряться при быстром старте) — стабильность 4×37 тестов
- m1: превью-текст учитывает выбранный в форме шаблон; m2: убрана
  недостижимая проверка шага; m3: удалён мёртвый translation_key climate
- m4: бит 21 op_status (шаблон B); m5: lru_cache template_info/prop_info;
  m6: operation_mode=1 без истории → hvac_mode unknown; m7: ошибка команды
  → HomeAssistantError; m8: unload возвращает False, не разбирая runtime
- n2: уточнены описания data_description в переводах
This commit is contained in:
2026-09-29 13:41:20 +03:00
parent 3e0b6b3653
commit d86ed60b51
17 changed files with 202 additions and 59 deletions
+40 -6
View File
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field, replace
from typing import Optional
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from pyfglair import Error, Prop, PropertyEvent, State, Template, Value, ValueKind
@@ -93,10 +94,34 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
available=state in AVAILABLE_STATES,
)
self.async_set_updated_data(self.data)
if state == State.ONLINE and not self._synced and self._sync_task is None:
if state == State.ONLINE:
if not self._synced and self._sync_task is None:
self._sync_task = self.hass.async_create_task(
self._async_initial_sync()
)
else:
# После разрыва при следующем ONLINE нужна повторная синхронизация.
self._synced = False
async def async_sync_now(self) -> None:
"""Гарантирует первичный GET-батч (если уже online)."""
if self._sync_task is not None:
await self._sync_task
elif self.client.state == State.ONLINE and not self._synced:
self._sync_task = self.hass.async_create_task(
self._async_initial_sync()
)
await self._sync_task
# Снимок создавался до ONLINE: обновляем состояние/доступность из
# актуального состояния клиента (значения приходят событиями).
state = self.client.state
self.data = replace(
self.data,
state=state,
last_error=self.client.last_error,
available=state in AVAILABLE_STATES,
)
self.async_set_updated_data(self.data)
@callback
def _on_property(self, event: PropertyEvent) -> None:
@@ -127,7 +152,11 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
"""Одно действие пользователя — один batch_commit()."""
if not updates:
return
await self.hass.async_add_executor_job(self._write, updates)
ok = await self.hass.async_add_executor_job(self._write, updates)
if not ok:
raise HomeAssistantError(
"FGLair: команда не отправлена (сессия недоступна)"
)
values = dict(self.data.values)
for prop, _ in updates:
value = self.client.cached(prop)
@@ -136,11 +165,16 @@ class FglairCoordinator(DataUpdateCoordinator[FglairData]):
self.data = replace(self.data, values=values)
self.async_set_updated_data(self.data)
def _write(self, updates: list[tuple[Prop, int]]) -> None:
self.client.batch_begin()
def _write(self, updates: list[tuple[Prop, int]]) -> bool:
if not self.client.batch_begin():
return False
ok = True
for prop, value in updates:
self.client.set_int(prop, value)
self.client.batch_commit()
if not self.client.set_int(prop, value):
ok = False
if not self.client.batch_commit():
ok = False
return ok
async def async_shutdown(self) -> None:
for unsub in self._unsubs: