Files
fgl-aircon/docs/PLAN_ESPHOME.md
Petr Polezhaev 024b290b89 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 исключены из версионирования.
2026-09-17 19:44:49 +03:00

8.4 KiB
Raw Blame History

План: внешний компонент 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-конфигурация

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, релиз