Files
fgl-aircon/docs/PROTOCOL.md
Petr Polezhaev bfdf22ba64 core(M2): машина состояний сессии Ayla LAN + mock-модуль + интеграционные сценарии
- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
  offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
  прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
  datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
  сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
  x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
  с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
  двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
  (переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
  pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
  'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
  (обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
  local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
  самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
  тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
  при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
  PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
2026-09-27 10:53:28 +03:00

35 KiB
Raw Blame History

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) и сверён с существующим скриптом docs/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. Обнаружение устройства

  1. Облако: 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]
  2. mDNS: приложение опрашивает A-запись <DSN>.local (например <DSN>.local, например AC000W00ABCD1234.local), отправляя DNS-query на 224.0.0.251:5353 и на 224.0.0.251:10276 (нестандартный порт Ayla). [ПРОВЕРЕНО НА ПРИБОРЕ: модуль отвечает ТОЛЬКО на :10276, на :5353 — нет. A-ответ, TTL 10, имя <DSN>.local → IP модуля. Проба: tools/probe_mdns.py.]
  3. Кэш приложения хранит последние 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":62999,"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.

Совпадает с docs/legacy/aircon/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):

  1. local_reg от endpoint'а без живой сессии → модуль отправляет key exchange (если есть свободный слот), затем сразу (≈0.2–0.5 с) делает один «пустой» опрос commands.json — это признак принятой сессии.
  2. local_reg от endpoint'а с живой сессией моложе ~40 с → только keep-alive, без key exchange.
  3. local_reg при зазоре ≥ ~44–50 с от предыдущего local_reg → модуль принудительно инициирует новый key exchange («вернувшееся» приложение получает свежие ключи). При штатном keep-alive каждые 10–15 с re-key НЕ происходит — сессия живёт сколь угодно долго (проверено: 100 с при 15 с keep-alive — 0 re-key; 125 с при 50 с keep-alive — 3 re-key, оба без потерь). time_1 модуля — тикающий счётчик с шагом ≈10 нс (аптайм); порог, вероятно, 4.4e9 тиков (~44 с) от последнего local_reg.
  4. Ответы 401/400 на POST модуля игнорируются: сессия продолжает работать, re-key не вызывается. Восстановление после расхождения CBC-цепочек — намеренная «тишина» приложения на > порога из п. 3 с последующим local_reg: модуль сочтёт приложение вернувшимся и ре-кает. Т.е. стратегия самовосстановления: при ошибке расшифровки — пауза keep-alive ~50–60 с, затем возобновить (проверено на приборе). Отдельный случай — бракованная подпись при живой цепочке (сообщение расшифровано, подпись не сошлась): цепочка НЕ расходится, следующий push восстанавливает работу без re-key.
  5. delete_session освобождает слот немедленно; следующий local_reg того же endpoint'а создаёт новую сессию.
  6. Наблюдавшийся (не воспроизведённый повторно) режим отказа: модуль отвечает key exchange'ом, но не делает «пустой» опрос и не забирает команды; сессия не активируется. Возникал после серий неудачных key exchange (возможно, «застрявшие» слоты); проходил сам через ~10–20 минут покоя. При реализации: детектировать отсутствие poll'а в течение N секунд после KE и уходить в backoff, а не долбить повторными local_reg.
  7. 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 модуля игнорируются — модуль продолжает слать в «сломанный» канал. Восстановление: приложение замолкает на > ~44–50 с (порог «возврата» из п. 4.4.3) и шлёт local_reg — модуль переkey'ается. Реализация ядра: при ошибке расшифровки пауза keep-alive ~50 с, затем возобновление. В legacy-скрипте пауза получалась «бесплатно» из-за keep-alive 1200 с: каждый цикл завершался re-key при возврате — потому рассинхрон «сам чинился» через ~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(секрет)>; байты секрета — в docs/legacy/aircon/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. Ограничения и наблюдения для реализации

  1. Модуль чувствителен к частоте запросов: официальное приложение отправляет local_reg ≤ 1/10 с и всегда «пакетом», никогда — по одному на команду. Спам local_reg/большими пачками команд перегружает модуль до отвала Wi-Fi (подтверждено опытом legacy-скрипта, см. LEGACY_ANALYSIS.md).
  2. Ответ модуля на local_reg: 202 (успех), 503 — нет свободных слотов (2 сессии), при недоступности — таймаут/отказ соединения.
  3. commands.json возвращает одну команду за запрос; батч реализуется цепочкой 206-ответов. Не следует отдавать несколько команд в одном ответе — формат это формально позволяет (cmds/properties — массивы), но приложение так не делает; модуль, вероятно, применяет только первую [HYP].
  4. Zero-padding без NUL работает (на приборе), но для совместимости лучше повторять Java-вариант (всегда ≥ 1 нулевой байт).
  5. seq_no модуля сбрасывается при каждом re-key и растёт внутри сессии — не отбрасывайте «устаревшие» обновления из-за seq_no (в Java seq_no входящих вообще не проверяется; «прилипший» фильтр по seq_no в legacy-скрипте — источник потерянных обновлений, см. LEGACY_ANALYSIS §2.4).
  6. Записи не эхируются — обновляйте локальное состояние оптимистично и/или подтверждайте GET-ом (§5.3).
  7. Максимальное окно «глухоты» при десинхроне = интервал keep-alive (§6.3).

10. Проверено на приборе / осталось неизвестным

Проверено на AP-WC1E (fw 2.6.17-fgl2, ключ из docs/legacy/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 тиков внутреннего счётчика).