- PROTOCOL.md: полная спецификация (KDF, envelope/CBC-цепочка, local_reg, key exchange, commands.json 206/200, datapoint push, тайминги, таблицы свойств шаблонов A/B/F, облачный provisioning). Факты из APK помечены [APK], проверенные живыми экспериментами на AP-WC1E — [ПРОВЕРЕНО НА ПРИБОРЕ]: mDNS только :10276; лимит 2 LAN-сессии (3-я -> 503); принудительный re-key при возрасте сессии >= ~44с (единственный механизм самолечения десинхрона — 400/401 модуль игнорирует); записи не эхируются; delete_session освобождает слот. - LEGACY_ANALYSIS.md: причины рассинхрона (keep-alive 1200с вместо 10-15с + seq_no-фильтр) и перегрузки модуля; требования к новой реализации. - PLAN_CORE_LIBRARY.md: план C++20-библиотеки fglair-core (Linux + ESP-IDF), ключ lanip_key считается статичным, ротация — только ошибка + ручной перепровижининг. - PLAN_HOME_ASSISTANT.md: pyfglair (cffi wheel) + custom component, облачный provisioning только в config flow, ключ виден в диагностике для копирования в ESPHome. - PLAN_ESPHOME.md: external component, только ESP-IDF framework. - tools/probe_reference.py: эталонный клиент протокола (проверен на приборе end-to-end); tools/probe_mdns.py — mDNS-проба. - legacy/: снимок скрипта (апстрим gyro-labs/AirCon, вложенный клон не версионируется); apk/*.apk исключены из версионирования.
154 lines
8.4 KiB
Markdown
154 lines
8.4 KiB
Markdown
# План: внешний компонент ESPHome (`fglair`)
|
||
|
||
Аудитория — агенты-реализаторы. Протокол — `docs/PROTOCOL.md`; ядро —
|
||
`docs/PLAN_CORE_LIBRARY.md` (`fglair-core`, C++20). Компонент строится по
|
||
образцу штатных climate-модулей ESPHome (midea, hisense-ac, tuya), но протокол
|
||
вынесен в переиспользуемое C++-ядро.
|
||
|
||
Требования к среде: **только ESP-IDF framework** (Arduino-фреймворк ESPHome
|
||
считается устаревшим и не поддерживается). Ядро — C++20 без исключений/RTTI,
|
||
что совместимо с дефолтными флагами сборки ESPHome для IDF.
|
||
|
||
## 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/
|
||
```
|
||
|
||
Ядро компилируется как статическая библиотека через CMakeLists компонента;
|
||
платформенный слой — `platform/esp-idf` (lwip-сокеты, esp_timer, esp_random).
|
||
|
||
## 2. YAML-конфигурация
|
||
|
||
```yaml
|
||
external_components:
|
||
- source: github://<user>/esphome-fglair@main
|
||
components: [fglair]
|
||
|
||
fglair:
|
||
devices:
|
||
- id: ac_living
|
||
ip_address: 192.168.88.3 # либо dsn + mdns: true (запрос на :10276)
|
||
dsn: AC000W002879281
|
||
lanip_key: 1uWOP3nGbSz2ysEfjJJVu+P0fxAQTg==
|
||
lanip_key_id: 62888
|
||
template: A # A|B|F
|
||
# port: 10275 # локальный порт сервера (default)
|
||
# keepalive: 15s
|
||
|
||
climate:
|
||
- platform: fglair
|
||
device_id: ac_living
|
||
name: "Кондиционер"
|
||
# swing: both # off|vertical|horizontal|both
|
||
|
||
sensor:
|
||
- platform: fglair
|
||
device_id: ac_living
|
||
room_temperature: {name: "Температура в комнате"}
|
||
error_code: {name: "Код ошибки"}
|
||
|
||
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"}
|
||
```
|
||
|
||
**Откуда брать ключ**: у пользователя обычно уже есть HA-интеграция (или
|
||
запускается `fglair-discover` CLI из репозитория ядра). HA-диагностика
|
||
устройства показывает `lanip_key`/`lanip_key_id` — значения копируются в YAML
|
||
вручную. Ключ статичен (зашит в модуль), автоматическая синхронизация не
|
||
предусмотрена.
|
||
|
||
**Поведение при смене ключа**: ядро отвечает 412 и переходит в `key_error`;
|
||
компонент логирует ошибку с текстом «lanip_key устарел, обновите конфиг» и
|
||
останавливает сессию (не долбит модуль). Обновление — правка YAML вручную.
|
||
|
||
## 3. Компонент `fglair` (hub, `__init__.py`)
|
||
|
||
* `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) возвращает.
|
||
|
||
## 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.
|
||
|
||
## 5. Остальные платформы
|
||
|
||
* `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.
|
||
|
||
## 6. Ограничения и требования
|
||
|
||
* Платы: 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 мин); ядро переживает это штатно (проверено на приборе).
|
||
|
||
## 7. Тесты и приёмка
|
||
|
||
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 с.
|
||
|
||
## 8. Этапы
|
||
|
||
| # | Содержимое |
|
||
|---|-----------|
|
||
| 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, релиз |
|