- 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 аргументом.
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# План: компонент 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, релиз |
|