Files
petr.polezhaev 31cd56f082 core(M4): fglctl, fglair-discover, README библиотеки; M5 — вне скоупа core-релиза
- README.md (корень): сборка POSIX/ESP-IDF-компонентом, тесты, получение
  ключа (fglair-discover), примеры fglctl; planned-компоненты помечены.
- examples/cli/fglctl.cpp: monitor/get/set/status на публичном API
  (batch GET шаблона, конверсии, wait_online, логи в stderr, exit-коды;
  сборка с -Wall -Wextra -Werror).
- tools/fglair-discover: облачный provisioning (eu/us/cn, endpoints и
  app_id/secret из PROTOCOL §7), --format json|esphome-secrets, --out 0600,
  обработка URLError/KeyError; stdlib-only.
- tests/tools/test_fglair_discover.py: мок-облако (http.server), 4 теста,
  ctest tools_fglair_discover.
- docs/reports/M4_SOAK.md: 60-мин soak на AP-WC1E — 0 рассинхронов,
  33/33 свойства (вкл. строковые), 273 опроса модуля, pushes_bad=0;
  24-ч прогон — приёмочный шаг перед релизом.
- План: M4 ✅; M5 (FglHub/mDNS) — ВНЕ СКОУПА core-релиза.
- Гигиена: config_kata.json (корень+docs/legacy) в .gitignore; MQTT-креды
  из docs/legacy/run.sh заменены плейсхолдером (история чистится отдельно).
- Фиксы ревью: mock арм fail-pushes после активации (флейк san-сборки);
  fglctl: валидация strtoll (abc/99 — отказ, exit!=0), чтение config с
  предупреждением об обрезке, Config{} вместо memset; discover: 0600.
CI: 11/11 ×3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
2026-09-29 01:16:58 +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> --format 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, релиз |