# Анализ legacy-скрипта (`docs/legacy/`): соответствие протоколу и найденные проблемы Скрипт — форк проекта hisense_ac (deiger), адаптированный под FGLair. Общая логика протокола воспроизведена верно, но есть критические расхождения с APK и поведением модуля (проверено живыми экспериментами на приборе), которые объясняют оба наблюдаемых симптома: «рассинхронизацию ключей» и перегрузку модуля. > Живые проверки прибора (AP-WC1E) показали: модуль игнорирует 400/401 на свои > POST, восстанавливается только принудительным re-key по `local_reg` (порог > возраста сессии ≈44 с); максимум 2 LAN-сессии; записи не эхируются. > Подробности — PROTOCOL.md §4.4, §5.3, §6.3, §10. ## 1. Что воспроизведено корректно | Часть | Файл | Оценка | |-------|------|--------| | KDF ключей (app/dev, suffix 0/1/2) | `config.py` | Точно совпадает с `AylaEncryption.generateSessionKeys`; подтверждено на приборе | | AES-256-CBC, zero-pad, HMAC-sign | `query_handlers.py` | Совпадает; CBC-цепочка подтверждена на приборе (несколько последовательных сообщений) | | CBC-цепочка в рамках сессии | `config.py` (один объект cipher) | Совпадает с Java (persist state) | | Роуты `/local_lan/*` | `main.py` | Совпадают с `AylaHttpServer.addMappings` | | Формат commands.json (по одной команде, seq_no, `{}` при пустой очереди) | `query_handlers.py` | Совпадает. **Но**: нет 206/200-различения (см. 2.6) | | Формат datapoint push и GET-ответов | `query_handlers.py` | Совпадает | | local_reg body/методы POST/PUT | `notifier.py` | Совпадает (формат некритичен — проверено) | | Оптимистичное обновление при записи | `aircon.py` (property_updater) | Верно: записи не эхируются (проверено на приборе) | | Облачный discovery (sign_in/devices/lan.json, секреты) | `discovery.py`, `app_mappings.py` | Совпадает (проверено: EU secret = base64url из SECRET_MAP) | | Таблица свойств FGL (шаблон A) | `properties.py` | Частично; много свойств отсутствует (op_status, error_code, powerful_mode, min_heat, coil_dry, device_capabilities, …) — см. PROTOCOL.md §8.2 | ## 2. Расхождения с APK/прибором (= баги) ### 2.1. [ГЛАВНАЯ ПРИЧИНА «РАССИНХРОНИЗАЦИИ»] Keep-alive 1200 с вместо 10–15 с `notifier.py:_KEEP_ALIVE_INTERVAL = 1200.0`. APK: 10 с (или `lan.json:keepAlive/3`). Проверено на приборе: **единственный механизм восстановления после расхождения CBC-цепочек — принудительный re-key, который модуль делает при получении `local_reg` для сессии старше ≈44 с. Ответы 400/401 модуль игнорирует.** Следствие для legacy: любая потерянная пара запрос-ответ/обрыв соединения → обе стороны «глохнут» на срок до 20 минут (до следующего local_reg). Наблюдаемый симптом «перестаёт понимать кондиционер» с самопроизвольным восстановлением — именно это. Дополнительно: длинные паузы между local_reg держат сессию «полуживой» (модуль не видит keep-alive, но слот может удерживаться), и конфликт за 2 доступных слота с телефоном/вторым клиентом становится вероятнее. ### 2.2. [ТЕОРЕТИЧЕСКОЕ] Неверная обработка смены `lanip_key_id` `config.py:update` бросает `KeyIdReplaced` → `key_exchange_handler` отвечает **404 Not Found** вместо **412 Precondition Failed** (APK) и никогда не перечитывает `lan.json`. За 5 лет эксплуатации ротация ключа не наблюдалась ни разу (ключ, по-видимому, статичен и зашит в модуль), так что на практике благополучен — но код вводит в заблуждение и чинится тривиально. ### 2.3. Drop легитимных обновлений по seq_no `aircon.py:is_update_valid` отбрасывает обновления с `seq_no` меньше последнего (кроме 0). На приборе: seq_no модуля сбрасывается в 0 **при каждом re-key** и растёт внутри сессии. При штатных (для legacy — раз в 1200 с) re-key'ах фильтр пропускает только первый push сессии (seq 0) и отбрасывает все последующие (1, 2, … < накопленного максимума). APK не проверяет seq_no входящих вообще. Итог: пропущенные обновления состояния после каждого re-key — второй вклад в «скрипт не видит изменений». ### 2.4. 400 вместо 401 при ошибке расшифровки `query_handlers.property_update_handler` возвращает **400**, APK — **401**. На приборе модуль игнорирует оба кода, так что это НЕ причина рассинхрона (первоначальная гипотеза опровергнута экспериментом). Исправить стоит для APK-совместимости, потому что код ответа — часть интерфейса. ### 2.5. Мелочи шифрования * Паддинг: скрипт НЕ добавляет обязательный завершающий NUL (Java добавляет `len+1`). На приборе работает оба варианта; для совместимости повторить Java. * `t_fan_speed`/`t_control_value` (AcDevice/Hisense-свойства) для FGLair-устройств не используются — кодовая basePath висит мёртвым грузом. ### 2.6. Отсутствие 206-ответов `command_handler` всегда отвечает 200. APK отвечает 206, пока очередь не пуста. Без 206 модуль вынужден либо перепрашивать local_reg, либо опрашивать вслепую — вероятный вклад в перегрузку. ### 2.7. Нет DELETE-команды сессии при завершении Скрипт не отправляет `delete_session` — модуль держит полумёртвую сессию в одном из 2 слотов. ## 3. Причины перегрузки модуля (спам → модуль отключается от Wi-Fi) ### 3.1. Статусный цикл: 33 GET-команды каждые 600 с `main.py:query_status_device` ставит в очередь **по одной GET-команде на каждое свойство** (поля dataclass) каждые 600 с, плюс ещё раз при старте. APK запрашивает все свойства **один раз** при установке сессии (`fetchPropertiesLAN`) и далее живёт на push-обновлениях; поллит отдельные свойства только после команд с побочными эффектами. Постоянный циклический опрос — лишние сотни HTTP-транзакций и AES-операций на приборе, у которого слабый CPU. ### 3.2. local_reg на каждую команду без debounce Каждый `queue_command` → `_queue_listener()` → немедленный `local_reg notify=1`. Действие из HA (mode+temp+fan) = 3 команды = до 3 local_reg подряд. APK шлёт **один** local_reg на пакет команд (AylaLocalNetwork.performRequest). ### 3.3. Агрессивный цикл Notifier при непустой очереди `notifier.py:start`: пока `qsize > 1` — sleep всего 60 с и повторная отправка local_reg. Если модуль «застрял» (не забирает команды), очередь растёт (см. 3.1), local_reg продолжает долбить каждые 60 с + retry-логика tenacity (6 попыток, экспоненциально). Мёртвый цикл под нагрузкой. На приборе подтверждён паттерн: после серий неудачных попыток регистрации модуль может «зависать» в режиме «KE без активации» — долбить его повторными local_reg бесполезно, нужен backoff и пауза (PROTOCOL.md §4.4 п.6). ### 3.4. Странный старт При старте: `query_status_device` немедленно (без начальной задержки) наполняет очередь 33 GET-командами, а `Notifier.start` в первой же итерации отправляет `local_reg` (таймер `last_timestamp=0` срабатывает сразу). Возникает гонка: `notify` в первом POST/PUT зависит от того, успела ли очередь наполниться, и модуль сразу получает «тяжёлый» старт — массовая выдача 33 команд новой сессии. Правильная последовательность (APK): local_reg notify=0 → key exchange → один пакет GET-запросов → далее только push. ## 4. Прочие замечания * MQTT: подписка на `$SYS/broker/log/M/subscribe/#` — hack для перепосылки статуса новым подписчикам; в HA-интеграции не понадобится. * `f_temp_in`/`t_power`-мэппинги — код Hisense-ветки, для FGL не нужен. * Потокобезопасность: pycryptodome cipher используется из одного event-loop — ок, но при любом выносе в треды потребует сериализации (CBC-цепочка!). ## 5. Требования к новой реализации, вытекающие из анализа 1. Keep-alive по APK-таймингам: 10–15 с. Это одновременно и период самолечения десинхрона (модуль сам сделает re-key на ≈44-й секунде). 2. Воспроизводить Java-поведение в кодах ответов: 401 при ошибках расшифровки, 412 при несовпадении key_id, 206/200 в commands.json, NUL-паддинг. 3. Начальная синхронизация: один пакет GET всех нужных свойств после key exchange; далее — push-driven. Периодический опрос — только как diagnosка с большим интервалом и по требованию. 4. Записи не эхируются: оптимистичное обновление + при необходимости GET-подтверждение. 5. Не проверять seq_no входящих сообщений. 6. Debounce команд: копить 100–300 мс, отправлять одним пакетом; один local_reg notify=1 на пакет. Не более одного local_reg в ~1 с. 7. Rate-limit очереди, backoff при ошибках (включая режим «KE без poll» — пауза, а не долбёжка), корректное завершение (delete_session) для освобождения слота. 8. Считать lanip_key статичным: при несовпадении key_id — устойчивая ошибка и перепровижининг вручную (облако не дергать в рантайме).