docs: реконструкция LAN-протокола FGLair, анализ legacy, планы fglair-core/HA/ESPHome

- PROTOCOL.md: полная спецификация (KDF, envelope/CBC-цепочка, local_reg,
  key exchange, commands.json 206/200, datapoint push, тайминги, таблицы
  свойств шаблонов A/B/F, облачный provisioning). Факты из APK помечены
  [APK], проверенные живыми экспериментами на AP-WC1E — [ПРОВЕРЕНО НА
  ПРИБОРЕ]: mDNS только :10276; лимит 2 LAN-сессии (3-я -> 503);
  принудительный re-key при возрасте сессии >= ~44с (единственный механизм
  самолечения десинхрона — 400/401 модуль игнорирует); записи не
  эхируются; delete_session освобождает слот.
- LEGACY_ANALYSIS.md: причины рассинхрона (keep-alive 1200с вместо 10-15с
  + seq_no-фильтр) и перегрузки модуля; требования к новой реализации.
- PLAN_CORE_LIBRARY.md: план C++20-библиотеки fglair-core (Linux + ESP-IDF),
  ключ lanip_key считается статичным, ротация — только ошибка + ручной
  перепровижининг.
- PLAN_HOME_ASSISTANT.md: pyfglair (cffi wheel) + custom component,
  облачный provisioning только в config flow, ключ виден в диагностике
  для копирования в ESPHome.
- PLAN_ESPHOME.md: external component, только ESP-IDF framework.
- tools/probe_reference.py: эталонный клиент протокола (проверен на
  приборе end-to-end); tools/probe_mdns.py — mDNS-проба.
- legacy/: снимок скрипта (апстрим gyro-labs/AirCon, вложенный клон не
  версионируется); apk/*.apk исключены из версионирования.
This commit is contained in:
2026-09-17 19:44:49 +03:00
commit 024b290b89
25 changed files with 3904 additions and 0 deletions

153
docs/PLAN_ESPHOME.md Normal file
View File

@@ -0,0 +1,153 @@
# План: внешний компонент ESPHome (`fglair`)
Аудитория — агенты-реализаторы. Протокол — `docs/PROTOCOL.md`; ядро —
`docs/PLAN_CORE_LIBRARY.md` (`fglair-core`, C++20). Компонент строится по
образцу штатных climate-модулей ESPHome (midea, hisense-ac, tuya), но протокол
вынесен в переиспользуемое C++-ядро.
Требования к среде: **только ESP-IDF framework** (Arduino-фреймворк ESPHome
считается устаревшим и не поддерживается). Ядро — C++20 без исключений/RTTI,
что совместимо с дефолтными флагами сборки ESPHome для IDF.
## 1. Распределение кода
```
esphome-fglair/ (external component, установка через
components/fglair/ external_components: - source: github://...)
__init__.py # FglairHub: Component; владеет FglSession ядра
climate.py # FglairClimate : climate::Climate
sensor.py # комнатная температура, error_code, op_status-флаги
switch.py # economy/powerful/coil_dry/min_heat/...
select.py # положения заслонок (v2)
binary_sensor.py # connectivity
config_validation.py
const.py
core/ # git-subtree/symlink fglair-core (src+include+platform/esp-idf)
CMakeLists.txt # STATIC LIBRARY; REQUIRES lwip esp_timer mbedtls
translations/
```
Ядро компилируется как статическая библиотека через CMakeLists компонента;
платформенный слой — `platform/esp-idf` (lwip-сокеты, esp_timer, esp_random).
## 2. YAML-конфигурация
```yaml
external_components:
- source: github://<user>/esphome-fglair@main
components: [fglair]
fglair:
devices:
- id: ac_living
ip_address: 192.0.2.3 # либо dsn + mdns: true (запрос на :10276)
dsn: AC000W00REDACTED
lanip_key: REDACTED-LANIP-KEY
lanip_key_id: 62888
template: A # A|B|F
# port: 10275 # локальный порт сервера (default)
# keepalive: 15s
climate:
- platform: fglair
device_id: ac_living
name: "Кондиционер"
# swing: both # off|vertical|horizontal|both
sensor:
- platform: fglair
device_id: ac_living
room_temperature: {name: "Температура в комнате"}
error_code: {name: "Код ошибки"}
switch:
- platform: fglair
device_id: ac_living
economy: {name: "Эко"}
powerful: {name: "Мощный"}
coil_dry: {name: "Осушка змеевика"}
min_heat: {name: "Мин. обогрев"}
outdoor_low_noise: {name: "Тихий наружный блок"}
wifi_led: {name: "LED Wi-Fi"}
```
**Откуда брать ключ**: у пользователя обычно уже есть HA-интеграция (или
запускается `fglair-discover` CLI из репозитория ядра). HA-диагностика
устройства показывает `lanip_key`/`lanip_key_id` — значения копируются в YAML
вручную. Ключ статичен (зашит в модуль), автоматическая синхронизация не
предусмотрена.
**Поведение при смене ключа**: ядро отвечает 412 и переходит в `key_error`;
компонент логирует ошибку с текстом «lanip_key устарел, обновите конфиг» и
останавливает сессию (не долбит модуль). Обновление — правка YAML вручную.
## 3. Компонент `fglair` (hub, `__init__.py`)
* `FglairHub : public Component` — на каждое `device` создаёт `FglSession`
(API ядра) при `setup()`; колбэки ядра приходят из его внутренней задачи —
мост в main-loop ESPHome через `Component::defer()`.
* `dump_config()`: версия ядра, состояние, статистика (re-keys, команды,
lost-push), измеренный возраст re-key.
* `loop()`: поллинг mailbox (транзакции из main-loop в ядро — тоже через
mailbox ядра, ядро однопоточное внутри).
* Зависимости: `network`; старт сессии только после `network::is_connected()`;
при смене IP самой ESP ядро перерегистрируется само (local_reg с новым ip).
* Доступность: таймаут watchdog 60 с без push и без успешного local_reg →
entities в NaN/unavailable; восстановление ядром (backoff) возвращает.
## 4. `climate.py`
* `FglairClimate : public climate::Climate, public Component`:
* `traits()`: modes OFF/COOL/HEAT/DRY/FAN_ONLY/AUTO (фильтр по
`device_capabilities`); fan quiet/low/medium/high/auto; swing off/vertical/
horizontal/both; step 0.5 °C (шаблон B — 1.0 °C); min/max 16–30 °C;
* `control(const ClimateCall&)`: все изменения вызова — в ОДИН
`batch_begin()/batch_commit()` ядра (один local_reg notify на действие);
OFF → `operation_mode=0`; turn_on → `operation_mode=1`;
* `current_temperature` ← `display_temperature`; остальные значения из кэша
ядра; push-колбэк ядра → `publish_state()`;
* записи не эхируются (PROTOCOL.md §5.3) — optimistic update в кэше ядра,
state публикуется сразу;
* presets: `CLIMATE_PRESET_ECO` / `CLIMATE_PRESET_BOOST` → economy_mode /
powerful_mode.
## 5. Остальные платформы
* `sensor.py`: `room_temperature` (°C, точность 0.25), `error_code`,
`op_status` (text-флаги defrost/oil_recovery/pump_down/maintenance/
check_operation/запреты — по битовой маске PROTOCOL.md §8.4).
* `switch.py`: bool-свойства; `write_state` → `set_bool` ядра.
* `select.py` (v2): `af_vertical_direction`/`af_horizontal_direction`, N опций
из `af_*_num_dir`.
* `binary_sensor.py`: `connectivity` = ONLINE.
## 6. Ограничения и требования
* Платы: esp32/esp32s3/esp32c3 (lwip + ≥ 25–30 КБ свободной RAM под сессию
и буферы ядра; для c3 проверить стек задачи ядра).
* Порт 10275 (или настраиваемый) должен быть свободен; конфликт с `api`
(6053)/`ota` исключён.
* **Одно устройство на ESP** в v1 (мульти-устройства — M6 ядра/FglHub).
* Колбэки ядра — не в main-loop; мост через `defer()` обязателен.
* OTA-обновление ESPHome поверх живой сессии: `stop()` в `on_shutdown`-
триггере не гарантируется — слот на модуле освободится сам по таймауту
(< 2 мин); ядро переживает это штатно (проверено на приборе).
## 7. Тесты и приёмка
1. CI: `esphome compile` для тестовых конфигов (esp32-idf, esp32s3-idf).
2. Mock-модуль ядра + локальная сборка компонента — smoke: старт, online,
одна запись, публикация state.
3. На приборе: чек-лист M5 ядра + OTA-ребут поверх сессии + 24 ч uptime
под управлением из HA через native API.
4. Приёмка: 24 ч без рассинхронов; параллельно телефон (2 слота); изменение
с пульта отражается в HA < 1 с.
## 8. Этапы
| # | Содержимое |
|---|-----------|
| E1 | каркас external component, компиляция ядра (esp-idf), hub + connectivity |
| E2 | climate (mode/fan/temp/swing, batch-запись, optimistic state) |
| E3 | sensor/switch, capabilities-фильтры, translations, README с YAML |
| E4 | select (заслонки), mdns-опция (:10276), CI, релиз |