Files
fgl-aircon/docs/PLAN_ESPHOME.md
Petr Polezhaev b21817ab9f docs: корректировки планов — монорепо, слои ayla/aircon, конверсии, приёмка
- PLAN_CORE: монорепозиторий (CMakeLists в корне; components/fglair,
  custom_components/fglair, include/fgl-aircon, src/{ayla,aircon},
  src/ayla/platform); логическое разделение ay­la (протокол+цикл) /
  aircon (конверсии+шаблоны+API); тесты зеркалят слои (tests/{ayla,aircon});
  конверсии: шаблон / линейные коэффициенты / функция-указатель
  (лямбды ESPHome); оценка httpd/json (jsmn вендор, свой мини-httpd на
  BSD-сокетах, conan не нужен); README библиотеки в M4.
- PLAN_ESPHOME: host вместо ip_address (DNS + Ayla-mDNS :10276),
  секреты в примерах, кастомные конверсии через !lambda, advanced-пример
  (триггеры режимов + LVGL с пропусками), README-план, скрипт приёмки
  (aioesphomeapi + HA REST).
- PLAN_HOME_ASSISTANT: шаг config flow с превью рассчитанных значений
  шаблона (через cffi в C-ядро, без дублей), README с HACS-инструкцией
  и заглушками под скриншоты с описаниями, приёмка (long-lived token).
- Убраны реальные dsn/ip/lanip_key/key_id из примеров; probe_mdns.py
  принимает DSN аргументом.
2026-09-21 13:20:29 +03:00

12 KiB
Raw Blame History

План: компонент ESPHome fglair (components/fglair)

Аудитория — агенты-реализатели. Протокол — docs/PROTOCOL.md; ядро — docs/PLAN_CORE_LIBRARY.md (fgl-aircon, C++20, монорепо: библиотека в корне, компонент здесь). Требования к среде: только ESP-IDF framework (Arduino-фреймворк ESPHome не поддерживаем).

1. Структура

components/fglair/            # ESPHome external component
  __init__.py                 # FglairHub : Component — владеет FglSession
  climate.py                  # FglairClimate : climate::Climate
  sensor.py  switch.py  select.py  binary_sensor.py
  config_validation.py  const.py
  translations/
  CMakeLists.txt              # подключает библиотеку из корня репозитория
                              # (EXTRA_COMPONENT_DIRS / relative),
                              # REQUIRES lwip esp_timer mbedtls

Установка пользователем:

external_components:
  - source: github://<user>/aircon@main
    components: [fglair]

2. YAML-конфигурация

Базовый пример (такой же войдёт в README, с комментариями на английском; реальные значения — в secrets, см. §5):

external_components:
  - source: github://<user>/aircon@main
    components: [fglair]

fglair:
  devices:
    - id: ac_living
      host: ac.local               # DNS-имя (или IP); .local — mDNS :10276
      dsn: !secret ac_dsn
      lanip_key: !secret ac_lanip_key
      lanip_key_id: !secret ac_lanip_key_id
      template: A                  # A | B | F
      # keepalive: 15s             # по умолчанию 15s
      # port: 10275                # локальный порт сервера (по умолчанию)

climate:
  - platform: fglair
    device_id: ac_living
    name: "Living Room AC"

sensor:
  - platform: fglair
    device_id: ac_living
    room_temperature: { name: "Room Temperature" }
    error_code:        { name: "AC Error Code" }

switch:
  - platform: fglair
    device_id: ac_living
    economy:          { name: "Economy" }
    powerful:         { name: "Powerful" }
    coil_dry:         { name: "Coil Dry" }
    min_heat:         { name: "Minimum Heat" }
    outdoor_low_noise:{ name: "Outdoor Low Noise" }
    wifi_led:         { name: "Wi-Fi LED" }

2.1. host вместо IP (требование)

  • Валидатор — cv.string (имя или IP). Разрешение выполняет ядро (fgl_config.host, PLAN_CORE §3): getaddrinfo (lwip DNS); для имён *.local — Ayla-mDNS A-запрос на 224.0.0.251:10276 (модуль не отвечает на :5353 — проверено). Ретраи разрешения при потере связи, mDNS-кэш TTL.
  • В YAML планах/примерах использовать ac.local-стиль имён, никаких реальных IP.

2.2. Кастомная конверсия через лямбду

Переопределение конверсии свойства (вместо шаблонной) — передаётся в ядро как fgl_conversion{custom_fn} (PLAN_CORE §4):

fglair:
  devices:
    - id: ac_living
      host: ac.local
      # ...
      convert:
        - property: adjust_temperature
          to_display: !lambda "return x * 0.1;"     # raw -> display
          from_input:  !lambda "return (int32_t)(x * 10);"  # display -> raw
          # range: [16, 30]

ESPHome-лямбды компилируются в C++-функции и передаются в ядро напрямую (capture недоступен — если нужен контекст, использовать глобальные конфиг-переменные; задокументировать).

3. Компонент fglair (hub)

  • FglairHub : Component — создаёт FglSession на каждое device в setup() (после network::is_connected()); колбэки ядра приходят из его задачи — мост в main-loop через Component::defer().
  • dump_config(): версия ядра, состояние, статистика (re-key счётчик, команды, потерянные push), измеренный возраст re-key.
  • Состояния ядра → диагностика: online/recovering/offline/key_error (key_error логирует «lanip_key устарел, обновите secrets» и останавливает сессию — обновление только правкой YAML, см. §5).
  • Watchdog: 60 с без push и без успешного local_reg → entities NaN.

4. climate.py

  • 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 из шаблона/override.
  • control(const ClimateCall&): все изменения вызова — в ОДИН batch_begin()/batch_commit(); OFF → operation_mode=0; turn_on → =1.
  • current_temperature ← display_temperature; значения из кэша ядра; push-колбэк → publish_state(); записи — optimistic (эха нет, PROTOCOL §5.3).
  • Presets: ECO/BOOST → economy_mode/powerful_mode.
  • sensor.py: room temp (°C, точность 0.25), error_code, op_status-флаги.
  • switch.py: bool-свойства. select.py (v2): положения заслонок. binary_sensor.py: connectivity.

5. Откуда брать ключ (для README)

  1. Из Home Assistant (если интеграция уже настроена): настройки устройства → диагностика — там показаны lanip_key, lanip_key_id, dsn; скопировать в secrets.yaml.
  2. CLI-дискавери (облако Ayla, без установки HA):
    # печатает блок для secrets.yaml
    python tools/fglair-discover --region eu --email <email> --output esphome-secrets
    # ac_dsn: "AC000W00XXXXXXX"
    # ac_lanip_key: "<base64>"
    # ac_lanip_key_id: 62999
    
  3. Существующий config_*.json от legacy-скрипта — поля переносятся в secrets вручную. Ключ статичен; при несовпадении key_id — правка secrets вручную.

6. README компонента (после реализации; для людей, коротко)

Разделы:

  1. Quick start — минимальный YAML (§2) с комментариями на английском; все чувствительные значения через !secret.
  2. Where to get the key — §5 (HA-диагностика / fglair-discover CLI / legacy-конфиг).
  3. Custom conversions — пример с лямбдами (§2.2).
  4. Advanced — пример «с действиями и событиями»: переключение режимов по внешнему триггеру + вывод данных на дисплей. Схема примера (LVGL-часть — с пропусками несущественных секций, помеченными # ...):
    # External trigger: switch the AC to powerful cool mode on demand
    binary_sensor:
      - platform: gpio
        id: hot_day_trigger
        on_press:
          then:
            - climate.control:
                id: ac_living
                hvac_mode: COOL
                preset: BOOST
            - logger.log: "Hot day: powerful cooling enabled"
    
    schedule:  # rotate operation modes by time of day
      - platform: time
        on_time:
          - hours: 7
            then:
              - climate.control: { id: ac_living, hvac_mode: AUTO }
    
    display:  # LVGL dashboard (relevant fragments only)
      lvgl:
        # ... widget definitions omitted ...
        - label:
            id: room_temp_label
            text:
              format: "%.1f°C"
            # bound via lambda to id(ac_living).current_temperature
        - label:
            id: mode_label
            # ... omitted ...
            # bound to id(ac_living).mode via lambda
    script:
      - id: push_mode_to_display
        # called on climate state change (on_state trigger), omitted
    
  5. Troubleshooting — 503 (оба слота заняты), key_error, «KE без активации» → подождать/перезапустить.

7. Ограничения

  • esp32/esp32s3/esp32c3; ≥25–30 КБ свободной RAM; порт 10275 свободен.
  • Одно устройство на ESP в v1 (FglHub — M5 ядра).
  • OTA-ребут поверх живой сессии: слот освободится сам (<2 мин), ядро переживает штатно (проверено).

8. Приёмка (полуавтоматическая, tests/acceptance/)

Топология: один кондиционер, HA-интеграция (на сервере HA) + ESPHome-устройство (ESP32) — занимают оба слота модуля. Топология обязательна для приёмки и заодно проверяет совместное владение.

Скрипт tests/acceptance/test_esphome_ha.py (python):

  • ESPHome-сторона: официальный aioesphomeapi — подключение к устройству по имени, subscribe_states, climate_command(...) для изменений.
  • HA-сторона: long-lived access token (Создаётся пользователем: Profile → Security → Long-lived access tokens) + REST API (/api/services/climate/set_*, /api/states/<entity_id>) — стандартный, документированный механизм, отдельной авторизации не требуется.
  • Режим quick (~5 мин): матрица {параметр: hvac_mode, target_temp, fan_mode, swing} × {направление: HA→ESP, ESP→HA} × {одиночное изменение, burst из 10}. Каждый шаг: изменение на стороне A → ожидание отражения на стороне B (таймаут 10 с) → возврат → проверка возврата. Отдельно: после burst — проверка согласованности финальных состояний и отсутствия ошибок в логах обеих сторон.
  • Режим --long (24 ч): раз в час — одно псевдослучайное изменение (ротация по списку параметров), проверка на другой стороне, возврат, проверка. CSV-лог + итоговый отчёт (успехи/провалы/задержки).
  • Запуск вручную; входит в релизный чек-лист компонента (E4).

9. Этапы

# Содержимое
E1 каркас компонента, компиляция ядра (esp-idf), hub + connectivity
E2 climate (mode/fan/temp/swing, batch, optimistic), host-резолвер
E3 sensor/switch, capabilities, кастомные конверсии (лямбды), translations
E4 README (§6), скрипт приёмки (§8), CI, релиз