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

236 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: компонент 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://<user>/aircon@main
components: [fglair]
```
## 2. YAML-конфигурация
Базовый пример (такой же войдёт в README, с комментариями на английском;
реальные значения — в secrets, см. §5):
```yaml
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):
```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 <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-часть —
с пропусками несущественных секций, помеченными `# ...`):
```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/<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, релиз |