docs: корректировки планов — монорепо, слои ayla/aircon, конверсии, приёмка
- 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 аргументом.
This commit is contained in:
@@ -1,153 +1,235 @@
|
||||
# План: внешний компонент ESPHome (`fglair`)
|
||||
# План: компонент ESPHome `fglair` (components/fglair)
|
||||
|
||||
Аудитория — агенты-реализаторы. Протокол — `docs/PROTOCOL.md`; ядро —
|
||||
`docs/PLAN_CORE_LIBRARY.md` (`fglair-core`, C++20). Компонент строится по
|
||||
образцу штатных climate-модулей ESPHome (midea, hisense-ac, tuya), но протокол
|
||||
вынесен в переиспользуемое C++-ядро.
|
||||
Аудитория — агенты-реализатели. Протокол — `docs/PROTOCOL.md`; ядро —
|
||||
`docs/PLAN_CORE_LIBRARY.md` (`fgl-aircon`, C++20, монорепо: библиотека в
|
||||
корне, компонент здесь). Требования к среде: **только ESP-IDF framework**
|
||||
(Arduino-фреймворк ESPHome не поддерживаем).
|
||||
|
||||
Требования к среде: **только ESP-IDF framework** (Arduino-фреймворк ESPHome
|
||||
считается устаревшим и не поддерживается). Ядро — C++20 без исключений/RTTI,
|
||||
что совместимо с дефолтными флагами сборки ESPHome для IDF.
|
||||
|
||||
## 1. Распределение кода
|
||||
## 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/
|
||||
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
|
||||
```
|
||||
|
||||
Ядро компилируется как статическая библиотека через CMakeLists компонента;
|
||||
платформенный слой — `platform/esp-idf` (lwip-сокеты, esp_timer, esp_random).
|
||||
Установка пользователем:
|
||||
```yaml
|
||||
external_components:
|
||||
- source: github://<user>/aircon@main
|
||||
components: [fglair]
|
||||
```
|
||||
|
||||
## 2. YAML-конфигурация
|
||||
|
||||
Базовый пример (такой же войдёт в README, с комментариями на английском;
|
||||
реальные значения — в secrets, см. §5):
|
||||
|
||||
```yaml
|
||||
external_components:
|
||||
- source: github://<user>/esphome-fglair@main
|
||||
- source: github://<user>/aircon@main
|
||||
components: [fglair]
|
||||
|
||||
fglair:
|
||||
devices:
|
||||
- id: ac_living
|
||||
ip_address: 192.0.2.3 # либо dsn + mdns: true (запрос на :10276)
|
||||
dsn: AC000W00REDACTED
|
||||
lanip_key: REDACTED-LANIP-KEY
|
||||
lanip_key_id: 62888
|
||||
template: A # A|B|F
|
||||
# port: 10275 # локальный порт сервера (default)
|
||||
# keepalive: 15s
|
||||
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: "Кондиционер"
|
||||
# swing: both # off|vertical|horizontal|both
|
||||
name: "Living Room AC"
|
||||
|
||||
sensor:
|
||||
- platform: fglair
|
||||
device_id: ac_living
|
||||
room_temperature: {name: "Температура в комнате"}
|
||||
error_code: {name: "Код ошибки"}
|
||||
room_temperature: { name: "Room Temperature" }
|
||||
error_code: { name: "AC Error Code" }
|
||||
|
||||
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"}
|
||||
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" }
|
||||
```
|
||||
|
||||
**Откуда брать ключ**: у пользователя обычно уже есть HA-интеграция (или
|
||||
запускается `fglair-discover` CLI из репозитория ядра). HA-диагностика
|
||||
устройства показывает `lanip_key`/`lanip_key_id` — значения копируются в YAML
|
||||
вручную. Ключ статичен (зашит в модуль), автоматическая синхронизация не
|
||||
предусмотрена.
|
||||
### 2.1. `host` вместо IP (требование)
|
||||
|
||||
**Поведение при смене ключа**: ядро отвечает 412 и переходит в `key_error`;
|
||||
компонент логирует ошибку с текстом «lanip_key устарел, обновите конфиг» и
|
||||
останавливает сессию (не долбит модуль). Обновление — правка YAML вручную.
|
||||
* Валидатор — `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.
|
||||
|
||||
## 3. Компонент `fglair` (hub, `__init__.py`)
|
||||
### 2.2. Кастомная конверсия через лямбду
|
||||
|
||||
* `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) возвращает.
|
||||
Переопределение конверсии свойства (вместо шаблонной) — передаётся в ядро
|
||||
как `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`
|
||||
|
||||
* `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.
|
||||
* `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. Остальные платформы
|
||||
## 5. Откуда брать ключ (для README)
|
||||
|
||||
* `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.
|
||||
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. Ограничения и требования
|
||||
## 6. README компонента (после реализации; для людей, коротко)
|
||||
|
||||
* Платы: 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 мин); ядро переживает это штатно (проверено на приборе).
|
||||
Разделы:
|
||||
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"
|
||||
|
||||
## 7. Тесты и приёмка
|
||||
schedule: # rotate operation modes by time of day
|
||||
- platform: time
|
||||
on_time:
|
||||
- hours: 7
|
||||
then:
|
||||
- climate.control: { id: ac_living, hvac_mode: AUTO }
|
||||
|
||||
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 с.
|
||||
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 без
|
||||
активации» → подождать/перезапустить.
|
||||
|
||||
## 8. Этапы
|
||||
## 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 | каркас 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, релиз |
|
||||
| E1 | каркас компонента, компиляция ядра (esp-idf), hub + connectivity |
|
||||
| E2 | climate (mode/fan/temp/swing, batch, optimistic), host-резолвер |
|
||||
| E3 | sensor/switch, capabilities, кастомные конверсии (лямбды), translations |
|
||||
| E4 | README (§6), скрипт приёмки (§8), CI, релиз |
|
||||
|
||||
Reference in New Issue
Block a user