# План: компонент 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 ``` Установка пользователем: ```yaml external_components: - source: github:///aircon@main components: [fglair] ``` ## 2. YAML-конфигурация Базовый пример (такой же войдёт в README, с комментариями на английском; реальные значения — в secrets, см. §5): ```yaml external_components: - source: github:///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): ```yaml 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): ```bash # печатает блок для secrets.yaml python tools/fglair-discover --region eu --email --output esphome-secrets # ac_dsn: "AC000W00XXXXXXX" # ac_lanip_key: "" # 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-часть — с пропусками несущественных секций, помеченными `# ...`): ```yaml # 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/`) — стандартный, документированный механизм, отдельной авторизации не требуется. * **Режим 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, релиз |