# План: внешний компонент 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:///esphome-fglair@main components: [fglair] fglair: devices: - id: ac_living ip_address: 192.168.88.3 # либо dsn + mdns: true (запрос на :10276) dsn: AC000W002879281 lanip_key: 1uWOP3nGbSz2ysEfjJJVu+P0fxAQTg== 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, релиз |