- 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 исключены из версионирования.
34 KiB
FGLair / Ayla LAN-протокол — спецификация
Документ реконструирован по декомпилированному APK FGLair 3.4.3 (com.fujitsu.fglair,
SDK com.aylanetworks.aylasdk, см. AylaLanModule.java, AylaEncryption.java,
AylaLanMessage.java, CreateDatapointCommand.java, AylaHttpServer.java,
com.cafbit.netlib.dns.NetThread) и сверён с существующим скриптом legacy/.
Всё, что помечено [APK], подтверждено кодом приложения; [LEGACY] — известно
только из скрипта; [ПРОВЕРЕНО НА ПРИБОРЕ] — проверено живыми экспериментами
на AP-WC1E (сентябрь 2026, см. §10); [HYP] — правдоподобная гипотеза,
требует проверки на приборе.
1. Обзор
Кондиционер (модуль Wi-Fi, далее «модуль») работает с облаком Ayla
(ads-eu.aylanetworks.com для EU). Приложение FGLair дополнительно умеет работать
с модулем напрямую в локальной сети («LAN mode»), не выходя в облако.
Протокол — HTTP/1.1 JSON поверх TCP, где модуль сам инициирует почти всё общение:
(1) local_reg (POST/PUT) (2) key_exchange (POST)
Приложение ------------------------------> Модуль (порт 80)
(HTTP-сервер <------------------------------- ...
:10275) 202 Accepted (3) commands.json (GET) ------>
<------------------------------- (4) datapoint.json (POST) ---->
(5) datapoint/ack.json (POST)->
Роли:
- Приложение (наш будущий код) — HTTP-сервер на порту 10275 (fallback:
любой свободный, номер сообщается модулю в
local_reg) и HTTP-клиент дляlocal_reg. - Модуль — HTTP-сервер на порту 80 и HTTP-клиент для запросов (3)–(5) к приложению.
Модуль поддерживает до 2 одновременных LAN-сессий [ПРОВЕРЕНО НА ПРИБОРЕ] — например, телефон с приложением + сервер умного дома; третья регистрация отклоняется (HTTP 503).
2. Обнаружение устройства
- Облако:
GET https://ads-eu.aylanetworks.com/apiv1/devices.jsonсодержитlan_ipкаждого устройства. ПлюсGET /apiv1/dsns/<DSN>/lan.jsonотдаёт{ "lanip": { "lanip_key": ..., "lanip_key_id": ..., "keepAlive": ..., "autoSync": ... } }. [APK] - mDNS: приложение опрашивает A-запись
<DSN>.local(напримерAC000W002879281.local), отправляя DNS-query на224.0.0.251:5353и на224.0.0.251:10276(нестандартный порт Ayla). [ПРОВЕРЕНО НА ПРИБОРЕ: модуль отвечает ТОЛЬКО на :10276, на :5353 — нет. A-ответ, TTL 10, имяAC000W002879281.local→ IP модуля. Проба:tools/probe_mdns.py.] - Кэш приложения хранит последние lan_ip/lanip_key. [APK]
Для библиотеки минимумом является статическая конфигурация вида config_kata.json
(ip, lanip_key, lanip_key_id, dsn); mDNS — опциональное улучшение
(запрос только на порт 10276).
3. Шифрование
3.1. Обмен ключами
Модуль отправляет на сервер приложения:
POST /local_lan/key_exchange.json
{"key_exchange":{"ver":1,"proto":1,"key_id":62888,"random_1":"<16 алфанум. симв.>","time_1":<int>,"sec":""}}
verиprotoобязаны быть1(AES-256-CBC + HMAC-SHA256). Иначе — 426 Upgrade Required. [APK]secнепустой только для RSA-режима первичной настройки (setup); в LAN-режее должен отсутствовать/быть пустым. [APK]key_id— номерlanip_key, полученного из облака (lan.json). Если не совпал с локальным — 412 Precondition Failed + приложение обязано перечитатьlan.jsonиз облака (refreshLanConfig) и разрешить LAN заново. [APK]
Приложение отвечает (HTTP 200):
{"random_2":"<16 алфанум. симв.>","time_2":<int>}
time_2 в Java — System.nanoTime(); значения time НЕ синхронизируются и не
проверяются — это просто материал для KDF. [APK]
3.2. Вывод сессионных ключей (KDF)
Обозначим: K = lanip_key.encode('utf-8') (строка base64 как есть, НЕ декодированная),
R1, R2, T1, T2 — utf-8 байты random_1, random_2, str(time_1), str(time_2).
msg_app = R1 | R2 | T1 | T2 | X # X — один байт: 0x30, 0x31 или 0x32
msg_dev = R2 | R1 | T2 | T1 | X # те же варианты X
key = HMAC_SHA256(K, HMAC_SHA256(K, msg) || msg) # 32 байта
- X=0x30 →
sign_key(ключ HMAC для подписи сообщений) - X=0x31 →
crypto_key(ключ AES-256) - X=0x32 →
iv_seed= первые 16 байт результата (начальный IV)
Направления:
- app-ключи (приложение шифрует/подписывает, модуль проверяет) — из
msg_app; - dev-ключи (модуль шифрует, приложение проверяет) — из
msg_dev.
Совпадает с legacy/config.py. [APK: AylaEncryption.generateSessionKeys]
3.3. Формат защищённого сообщения (envelope)
Все сообщения после key exchange в обе стороны — JSON:
{"enc":"<base64 AES-256-CBC>","sign":"<base64 HMAC-SHA256>"}
Открытый текст: {"seq_no":<int>,"data":<JSON-объект или {}>}.
- AES-256-CBC без стандартизированного паддинга. Паддинг нулями до кратности
16 байт, причём Java-реализация добавляет как минимум один нулевой байт
(C-string терминатор:
len+1, затем до кратности 16). При чтении — обрезаются все завершающие нулевые байты. Реализация должна корректно принимать оба варианта паддинга. [APK: encryptEncapsulateSign / unencodeDecrypt] sign— HMAC-SHA256 сsign_keyсоответствующего направления поверх байт открытого текста без паддинга (включаяseq_noиdata).seq_noприложения — статический счётчик, инкрементируется на каждое исходящее сообщение (никогда не сбрасывается, в т.ч. между сессиями). [APK]seq_noмодуля — свой счётчик; периодически сбрасывается в 0 [LEGACY].
3.4. КРИТИЧНО: цепочность CBC
AES-CBC объект создаётся один раз на сессию, и каждое следующее сообщение
продолжает цепочку CBC с того места, где закончилось предыдущее (в Java —
повторные вызовы Cipher.update; в pycryptodome — повторные encrypt/decrypt
одного объекта). Начальный IV — только iv_seed.
Следствия:
- Потеря или повреждение любого сообщения в канале (таймаут, обрыв соединения, перезагрузка одной из сторон, отклонённое сообщение) безвозвратно разводит цепочки сторон — все последующие сообщения не расшифровываются.
- Единственный механизм восстановления — новый key exchange (см. §6.3).
- Реализация обязана строго сериализовать все шифрования/расшифровки сессии.
4. Канал управления (приложение → модуль)
4.1. Регистрация / keep-alive: local_reg.json
POST http://<модуль>/local_reg.json?dsn=<DSN> # первый раз (sessionType не активна)
PUT http://<модуль>/local_reg.json # далее, пока сессия жива
Content-Type: application/json
{"local_reg":{"ip":"<ip приложения>","port":10275,"uri":"/local_lan","notify":0|1}}
notify=1— «у меня есть команды, забери».notify=0— просто keep-alive. [APK]- Успешный ответ модуля —
202 Accepted[ПРОВЕРЕНО НА ПРИБОРЕ]. - Слоты сессий: максимум 2 одновременные LAN-сессии (на приборе: 3-я
регистрация получает
HTTP 503) [ПРОВЕРЕНО НА ПРИБОРЕ]. Т.е. телефон + сервер умного дома уживаются; третий клиент — нет. - Формат тела/заголовков некритичен (проверены компактный/spaced JSON, с полным набором заголовков и без) [ПРОВЕРЕНО НА ПРИБОРЕ].
- Параметр
?dsn=добавляется только пока сессия ещё не активна. [APK] - Для setup-устройств добавляется поле
key(RSA public key) — вне scope. [APK]
4.2. Выборка команд: commands.json
После local_reg (особенно с notify=1) модуль опрашивает:
GET http://<ip приложения>:<порт>/local_lan/commands.json
Приложение возвращает ровно одну команду из очереди (из головы) в envelope:
{"seq_no":123,"data":{"properties":[{"property":{"base_type":"integer","name":"fan_speed","value":3,"id":"<8 симв.>","dsn":"<DSN>","metadata":...}}]}}
или запрос свойства:
{"seq_no":124,"data":{"cmds":[{"cmd":{"cmd_id":5,"method":"GET","resource":"property.json?name=fan_speed","data":"","uri":"/local_lan/property/datapoint.json"}}]}}
или {} («пусто»): {"seq_no":125,"data":{}}.
- HTTP-статус: 206 Partial Content, если в очереди остались ещё команды; иначе 200 OK. Модуль сам продолжает опрос при 206. [APK: getResponseCode]
cmd_id— инкрементальный id GET-команд; ответ модуля на GET придёт вdatapoint.jsonс query-параметром?cmd_id=5(см. §5.1). [APK]idвнутри property-команды — случайные 8 символов; нужен только если свойства включёнack_enabled(для FGLair-свойств ack не используется); по нему сопоставляется ack. [APK: CreateDatapointCommand]- Удаление сессии — тоже команда:
{"cmds":[{"cmd":{"cmd_id":0,"method":"DELETE","resource":"local_reg.json","data":"delete_session","uri":"/local_lan"}}]}. [APK: DeleteSessionCommand]
4.3. Тайминги (по APK; уточнено на приборе)
- Keep-alive: приложение отправляет
local_regкаждые 10 с по умолчанию; еслиlan.jsonвернулkeepAlive(секунды), интервал =keepAlive / 3. [APK] - При постановке команд в очередь приложение шлёт
local_regсnotify=1немедленно — но один на пакет команд, не на каждую команду. [APK: AylaLocalNetwork.performRequest — registerCommands() + sendLocalRegistration()] - Каждый обработанный
commands.jsonперезапускает таймер keep-alive (startKeepalive()после выдачи команды) — во время активного опроса дополнительный keep-alive не отправляется. [APK] - Ожидание ответа GET-команды:
max(50 s, n * 1.5 s)на пакет из n команд, без ретраев. Ack-таймаут datapoint — 10 с (по умолчанию). [APK] - Чтение свойств: в официальном приложении стартовые значения приходят из облака
или кэша; по LAN полный слепок можно получить пакетом из n GET-команд
(
fetchPropertiesLAN— все имена одним пакетом, ответ придёт push'ами). Далее приложение полагается на push-обновления (§5). Опрос конкретного свойства — по необходимости. [APK + JS]
4.4. Политика re-key и жизненный цикл сессии [ПРОВЕРЕНО НА ПРИБОРЕ]
Ключевое эмпирическое поведение модуля (AP-WC1E, fw 2.6.17-fgl2):
local_regот endpoint'а без живой сессии → модуль отправляет key exchange (если есть свободный слот), затем сразу (≈0.2–0.5 с) делает один «пустой» опросcommands.json— это признак принятой сессии.local_regот endpoint'а с живой сессией моложе ~40 с → только keep-alive, без key exchange.local_regот endpoint'а с сессией старше ~44 с → модуль принудительно инициирует новый key exchange (ротация сессионных ключей). Т.е. при штатном keep-alive каждые 10–15 с ключи ротируются примерно каждые 45–60 с.time_1модуля — тикающий счётчик с шагом ≈10 нс (аптайм); порог, вероятно, 44 с в этих единицах либо просто 4.4e9 тиков.- Ответы 401/400 на POST модуля игнорируются: сессия продолжает работать,
re-key не вызывается. Единственный механизм восстановления после расхождения
CBC-цепочек — принудительный re-key по
local_reg(п. 3). Поэтому интервал keep-alive = интервал потенциального «зависания» при десинхроне. delete_sessionосвобождает слот немедленно; следующийlocal_regтого же endpoint'а создаёт новую сессию.- Наблюдавшийся (не воспроизведённый повторно) режим отказа: модуль отвечает key exchange'ом, но не делает «пустой» опрос и не забирает команды; сессия не активируется. Возникал после серий неудачных key exchange (возможно, «застрявшие» слоты); проходил сам через ~10–20 минут покоя. При реализации: детектировать отсутствие poll'а в течение N секунд после KE и уходить в backoff, а не долбить повторными local_reg.
seq_noмодуля инкрементируется на каждый push в рамках сессии (0, 1, 2, …) и сбрасывается в 0 при каждом re-key. Проверять его на монотонность нельзя (см. также §9.5).
5. Канал телеметрии (модуль → приложение)
5.1. Обновление свойства
POST http://<ip приложения>:<порт>/local_lan/property/datapoint.json?cmd_id=N&status=200
Content-Type: application/json
{"enc":"...","sign":"..."}
- Query-параметры [ПРОВЕРЕНО НА ПРИБОРЕ]: ответ на GET-команду приходит с
?cmd_id=N&status=200(статус применения команды;cmd_idсоответствует id запроса). Спонтанные обновления — без параметров. - Открытый текст
data:
{"name":"operation_mode","value":3,"metadata":{...},"dsn":"<DSN узла>","dev_time_ms":0}
metadata,dsn(для узловых устройств),dev_time_ms— опциональны. [APK]- Ответ приложения: 200/206 с пустым телом. Java при ошибке расшифровки отвечает 401 Unauthorized; на приборе доказано, что модуль игнорирует и 401, и 400 (сессия продолжает работать) — это НЕ механизм восстановления, см. §4.4.
- Варианты путей:
/local_lan/node/property/datapoint.json— то же для узлов (гейтвей), вне scope. [APK]
5.2. Ack на datapoint
POST .../local_lan/property/datapoint/ack.json
data: {"id":"<id команды>","ack_status":200,"ack_message":0,"dsn":"..."}
ack_status != 200 — ошибка применения. Только для свойств с ack_enabled.
Для проверенных свойств FGLair (wifi_led_enable, get_prop) ack не приходит.
[частично ПРОВЕРЕНО НА ПРИБОРЕ]
5.3. Эхо на записи НЕТ [ПРОВЕРЕНО НА ПРИБОРЕ]
Запись свойства (properties-команда, §4.2) забирается модулем и применяется,
но не эхируется в LAN: ни datapoint-push с новым значением, ни ack.
(Именно поэтому legacy-скрипт обновляет состояние оптимистично в момент выдачи
команды.) Если нужно подтверждение — запросить свойство GET-командой через
короткую задержку. Спонтанные push'и приходят только на изменения, инициированные
самим прибором/пультом (и, вероятно, для свойств с побочными эффектами).
5.3. Прочие callback-пути (для полноты)
/local_lan/node/conn_status.json— статус узлов гейтвея./local_lan/status.json,/local_lan/connect_status,/local_lan/wifi_scan*.json,/local_lan/regtoken.json,/local_lan/wifi_stop_ap.json— режим setup (не нужны в рабочей сессии).
6. Сессия
6.1. Установка [ПРОВЕРЕНО НА ПРИБОРЕ]
приложение: POST local_reg.json (notify=0|1) # регистрирует свой ip:port (202)
модуль: POST /local_lan/key_exchange.json # генерирует сессионные ключи
приложение: 200 {"random_2","time_2"}
модуль: GET /local_lan/commands.json # «пустой» опрос сразу (≈0.5с)
приложение: (пакет GET-команд) + local_reg notify=1 # начальная синхронизация
модуль: GET commands.json (цикл по 206) + POST datapoint.json × n
...далее: push-обновления свойств + опрос commands.json после notify=1
6.2. Поддержание
Приложение шлёт local_reg каждые 10–15 с (APK: 10 с по умолчанию /
keepAlive/3 из lan.json). Сессия живёт, пока приходят local_reg; при
возрасте сессии ≥ ~44 с очередной local_reg вызывает принудительный re-key
(ротацию ключей) — это штатный режим (§4.4). При пропадании модуля (сеть/питание)
— повторные попытки с backoff, mDNS-переобнаружение.
6.3. Разрыв и восстановление [ПРОВЕРЕНО НА ПРИБОРЕ]
- Потеря CBC-цепочки (§3.4): модуль не может расшифровать ответ приложения /
приложение не может расшифровать push модуля. Ответы 401/400 на POST модуля
игнорируются — модуль продолжает слать в «сломанный» канал. Восстановление
происходит только когда очередной
local_reg(по возрасту ≥ ~44 с или от нового endpoint'а) вызовет новый key exchange. Следствие: интервал keep-alive = максимальное время «мёртвой» сессии при десинхроне (10–15 с — незаметно; 1200 с как в legacy-скрипте — 20 минут глухоты). - Смена lanip_key (
key_idне совпал): теоретический путь по APK — 412 +refreshLanConfig()из облака. За 5 лет эксплуатации прибора ротации ключа не наблюдалось ни разу; ключ, по-видимому, зашит в модуль, облако лишь хранит его копию. Реализация: 412 + переход в устойчивое состояние ошибки до перепровижининга вручную (см. планы). - Явное завершение: команда DELETE
local_reg.json/delete_session(§4.2) — освобождает слот немедленно. Рекомендуется слать при штатном выключении, чтобы не занимать один из 2 слотов модуля.
7. Облачная часть (для provisioning/discovery)
Серверы (Field) [APK: ServiceUrls.java]:
| Регион | User-сервис | Device-сервис |
|---|---|---|
| EU | user-field-eu.aylanetworks.com |
ads-eu.aylanetworks.com |
| US | user-field.aylanetworks.com |
ads-field.aylanetworks.com |
| CN | user-field.ayla.com.cn |
ads-field.ayla.com.cn |
Аутентификация приложения [APK: AylaNetworkWrapper.java]:
- EU:
app_id=FGLair-eu-id,app_secret=FGLair-eu-gpFbVBRoiJ8E3QWJ-QRULLL3j3U - US:
app_id=CJIOSP-id,app_secret=CJIOSP-Vb8MQL_lFiYQ7DKjN0eCFXznKZE - CN:
app_id=FGLairField-cn-id,app_secret=FGLairField-cn-zezg7Y60YpAvy3HPwxvWLnd4Oh4
app_secret = <prefix>-<base64url-nopad(секрет)>; байты секрета — в
legacy/app_mappings.py (проверено: EU совпадает).
Endpoints:
POST https://<user>/users/sign_in.json
{"user":{"email":"...","password":"...","application":{"app_id":"...","app_secret":"..."}}}
→ {"access_token":"...", ...}
GET https://<ads>/apiv1/devices.json (Authorization: auth_token <t>)
GET https://<ads>/apiv1/dsns/<dsn>/lan.json → {"lanip":{lanip_key, lanip_key_id, keepAlive,...}}
GET https://<ads>/apiv1/dsns/<dsn>/properties.json[?names[]=..&..] → описание свойств
properties.json для FGLair-устройств возвращает объекты Ayla-свойств:
name, base_type, read_only, ack_enabled, direction, display_name, ...
(см. AylaProperty.java). Для LAN-only работы библиотека может хранить таблицу
свойств статически (§8).
8. Модель свойств FGLair
8.1. Типы устройств (oem_model → шаблон) [APK: FGLDeviceTemplateType.java + JS template_types]
| Шаблон | Модели |
|---|---|
| A | AP-WA1E…WA6E, AP-WC1E…WC4E, AP-WD1E |
| B | AP-WB1E…WB4E |
| F | AP-WF1E…WF4E |
config_kata.json → AP-WC1E → шаблон A.
8.2. Список свойств по шаблонам
A: operation_mode, fan_speed, adjust_temperature, af_vertical_direction, af_vertical_swing, af_horizontal_direction, af_horizontal_swing, outdoor_low_noise, indoor_fan_control, human_det_auto_save, min_heat, powerful_mode, coil_dry_mode, economy_mode, master_timer_on_off_1, master_timer_on_off_2, error_code, demand_control, filter_sign_reset_display, op_status, device_name, building_name, wifi_led_enable, service_contact_name, service_contact_phone, service_contact_email, af_horizontal_num_dir, af_vertical_num_dir, device_capabilities, display_temperature, get_prop, human_det, refresh.
B: operation_mode, fan_speed, adjust_temperature, af_vertical_move_step1, af_horizontal_move_step1, economy_mode, master_timer_on_off_1/2, error_code, demand_control, filter_sign_reset_display, op_status, device_name, building_name, wifi_led_enable, service_contact_*, device_capabilities, refresh.
F: как A + monitor1, filter_sign_reset (вместо filter_sign_reset_display).
8.3. Семантика значений (шаблон A; подтверждено JS-бандлом приложения)
| Свойство | Тип | Значения |
|---|---|---|
operation_mode |
int | 0=OFF, 1=ON, 2=AUTO, 3=COOL, 4=DRY, 5=FAN, 6=HEAT. Вкл/выкл питания — запись 0/1. |
fan_speed |
int | 0=Quiet, 1=Low, 2=Medium, 3=High, 4=Auto |
adjust_temperature |
int | Уставка, единица 0.1 °C (250 = 25.0). Диапазон таблицы приложения: −10.0…45.0 (−100…450); фактически прибор ограничен 16…30 [LEGACY]. Шаг UI: 0.5 °C (шаблон B — 1.0 °C). |
display_temperature |
int, ro | Температура в помещении, единица 0.01 °C, смещение 5000 (5000 = 50.00 °C), шаг 25. Приближение: T = (v − 5000)/100. Приложение использует таблицу соответствия display↔adjust (221 строка, −10…45 °C). |
af_vertical_direction, af_horizontal_direction |
int | Положение заслонки 0…N−1, где N = af_vertical_num_dir / af_horizontal_num_dir (если N>15 → поддерживается только 0). |
af_vertical_swing, af_horizontal_swing |
int | 0=выкл, 1=вкл |
economy_mode, powerful_mode, coil_dry_mode, min_heat, outdoor_low_noise, human_det_auto_save, wifi_led_enable, indoor_fan_control |
bool(int) | 0/1 |
op_status |
int, ro | Битовая маска, см. §8.4 |
device_capabilities |
int, ro | Битовая маска, см. §8.5 |
error_code |
int, ro | Код ошибки (0 — нет; таблица приложения до 4095) |
demand_control |
int | Деманд-контроль (ограничение мощности) |
get_prop |
int | Триггер: запись 1 → прибор обновит display_temperature (свойство вернётся в 0) |
refresh |
int | Триггер полной синхронизации (приложение пишет через облако, value "1") |
master_timer_on_off_1/2 |
int | Таймеры вкл/выкл (2 шт.) |
filter_sign_reset_display |
int | Сброс индикации замены фильтра |
8.4. op_status — биты
| Бит | Значение |
|---|---|
| 0–18 | Запреты (центральное управление): 0 все операции, 1 таймер, 2 уставка температуры, 3 режим, 4 старт/стоп, 5 старт, 6 сброс фильтра, 7 работа, 8 температура, 9 auto, 10 cool, 11 dry, 12 heat, 13 fan, 14 level1-работа, 15 level1-старт/стоп, 16 level2-работа, 17 level2-таймер, 18 level2-локальные настройки |
| 21 | (только B) разморозка/масло/разные режимы |
| 22 | обслуживание (maintenance) |
| 24 | разморозка (defrost) |
| 25 | «разные режимы» (одновременные операции) |
| 28 | oil recovery |
| 29 | pump down |
| 30 | check operation |
8.5. device_capabilities — биты
| Бит | Возможность |
|---|---|
| 0 | cool |
| 1 | dry |
| 2 | fan |
| 3 | heat |
| 4 | auto |
| 5 | fan auto |
| 6 | fan high |
| 7 | fan medium |
| 8 | fan low |
| 9 | fan quiet |
| 10 | вертикальный swing |
| 11 | горизонтальный swing |
| 12 | economy |
| 13 | minimum heat |
| 14 | energy swing fan (indoor_fan_control) |
| 16 | powerful |
| 17 | outdoor low noise |
| 18 | coil dry |
8.6. Особые последовательности приложения (для справки)
- Включение питания = запись
operation_mode = 1, выключение =operation_mode = 0(сохранённый режим восстанавливается прибором сам). - min_heat ON → прибор сам меняет режим на heat и уставку 24 °C; приложение
дополнительно поллит
operation_mode/min_heat/adjust_temperatureдо стабилизации. - Изменение
af_*_directionпри включённом swing → ждёт обновления swing. - После команд с побочными эффектами приложение поллит 1 свойство с интервалом 1 с, таймаут 30–600 с (облако); в LAN-режиме поллинг не нужен — приходит push.
9. Ограничения и наблюдения для реализации
- Модуль чувствителен к частоте запросов: официальное приложение отправляет
local_reg≤ 1/10 с и всегда «пакетом», никогда — по одному на команду. Спамlocal_reg/большими пачками команд перегружает модуль до отвала Wi-Fi (подтверждено опытом legacy-скрипта, см.LEGACY_ANALYSIS.md). - Ответ модуля на
local_reg: 202 (успех), 503 — нет свободных слотов (2 сессии), при недоступности — таймаут/отказ соединения. commands.jsonвозвращает одну команду за запрос; батч реализуется цепочкой 206-ответов. Не следует отдавать несколько команд в одном ответе — формат это формально позволяет (cmds/properties— массивы), но приложение так не делает; модуль, вероятно, применяет только первую [HYP].- Zero-padding без NUL работает (на приборе), но для совместимости лучше повторять Java-вариант (всегда ≥ 1 нулевой байт).
- seq_no модуля сбрасывается при каждом re-key и растёт внутри сессии — не отбрасывайте «устаревшие» обновления из-за seq_no (в Java seq_no входящих вообще не проверяется; «прилипший» фильтр по seq_no в legacy-скрипте — источник потерянных обновлений, см. LEGACY_ANALYSIS §2.4).
- Записи не эхируются — обновляйте локальное состояние оптимистично и/или подтверждайте GET-ом (§5.3).
- Максимальное окно «глухоты» при десинхроне = интервал keep-alive (§6.3).
10. Проверено на приборе / осталось неизвестным
Проверено на AP-WC1E (fw 2.6.17-fgl2, ключ из config_kata.json), см. также §2,
§4.1, §4.4, §5.1, §5.3, §6: полный цикл сессии, KDF/CBC-цепочка/подписи в обе
стороны, GET/запись свойств, re-key, 401/400-игнорирование, 2 слота + 503,
delete_session, mDNS :10276. Рабочий эталонный клиент: tools/probe_reference.py.
Осталось неизвестным / требует проверки:
- Точная семантика
status=в query ответов на GET-команды (видели только 200). - Таймаут фактического освобождения слота при пропадении приложения без delete_session (ориентировочно ≤ 60–120 с; re-key-порог 44 с измерен точно).
- Реакция модуля на несколько команд в одном
commands.json-ответе. - Причина редкого режима «KE без активации сессии» (§4.4 п.6) — воспроизводится только после серий неудачных попыток.
- Ровно ли 44 с порог re-key (измерено в границах 39–44 с; принято «≈44 с», возможно 4.4e9 тиков внутреннего счётчика).