- PLAN_CORE: монорепозиторий (CMakeLists в корне; components/fglair,
custom_components/fglair, include/fgl-aircon, src/{ayla,aircon},
src/ayla/platform); логическое разделение ayla (протокол+цикл) /
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 аргументом.
12 KiB
План: компонент 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)
- Из Home Assistant (если интеграция уже настроена): настройки
устройства → диагностика — там показаны
lanip_key,lanip_key_id,dsn; скопировать вsecrets.yaml. - 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 - Существующий
config_*.jsonот legacy-скрипта — поля переносятся в secrets вручную. Ключ статичен; при несовпаденииkey_id— правка secrets вручную.
6. README компонента (после реализации; для людей, коротко)
Разделы:
- Quick start — минимальный YAML (§2) с комментариями на английском;
все чувствительные значения через
!secret. - Where to get the key — §5 (HA-диагностика / fglair-discover CLI / legacy-конфиг).
- Custom conversions — пример с лямбдами (§2.2).
- 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 - 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, релиз |