From a3cd302573a1dac42c2f880b2b273f52673b9978 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 14 Sep 2026 16:29:51 +0600 Subject: [PATCH] [2026-09-14] eagle: family/plans/t610-home-automation.md --- family/plans/t610-home-automation.md | 178 ++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 6 deletions(-) diff --git a/family/plans/t610-home-automation.md b/family/plans/t610-home-automation.md index 4fdb310f..f283ba14 100644 --- a/family/plans/t610-home-automation.md +++ b/family/plans/t610-home-automation.md @@ -9,7 +9,7 @@ ## 1. Состояние на 2026-09-14 (актуализировано 15:33 → дополнено вечерней сессией) **Этап 1 ✅ · Этап 2 ✅ · Этап 3 ✅ ЗАКРЫТ.** Этап 4 — **ПОЧТИ ЗАКРЫТ (3 из 4):** ① Caddy → t610 (`mallexxx.duckdns.org` → HA на t610, HTTP 200); ② Node-RED flows перенесены, `Connected to HA`, наружу не выпущен (решение Alex «оставляем так», доступ через ingress); ③ **ZONT MQTT-редирект переключён на t610** (DNAT на роутере `.197`→`.176`, живой поток идёт). **ОСТАЛОСЬ:** погасить/отключить сервисы TrueNAS (заблокировано — Caddy на TrueNAS держит точку входа) + хвосты (static IP, бэкап). -**Последняя верификация: 2026-09-14 (вечер-5) — правка логирования bridge ВЫПОЛНЕНА, ПРИЧИНА НАЙДЕНА фактом. Диагностический лог сырых байт показал: кадры приходят РАЗОРВАННЫМИ (1+7 байт), нераспознанные байты-сироты (`00`) НАКАПЛИВАЮТСЯ в буфере и не чистятся (стр. 960–961) → сдвигают выравнивание → длинный 19-байтный ответ гостиной отбрасывается молча. `timeout` как причина окончательно снят. Это же объясняет «замерзание» лога на 7 ч. План фикса (A/B/C) предложен Alex'у, выбор — за ним (§5-кватер-Ж). Детали — §5-кватер-Д («РАЗБОР КОДА» + «РАЗГАДКА»), §5-кватер-Е («GITEA»), §5-кватер-Ж, §9, §11 (журнал вечер-5). +**Последняя верификация: 2026-09-14 (вечер-6) — ФИКС СДЕЛАН, ГОСТИНАЯ РАБОТАЕТ. ✅✅ ЗАКРЫТО.** Причина (вечер-5, фактом): кадры приходят разорванными, байты-сироты `00` копились в голове буфера → сдвиг выравнивания → 19-байтный ответ гостиной отбрасывался молча. **Фикс (вечер-6): сборка кадров по канону — T3.5-разграничение (пауза ≥3.5 символа = конец кадра) + сброс битого буфера (= `resetFrame` в pymodbus)**, коммит `3748feb` → Gitea → scp → `ha apps rebuild`. **Итог: все 7 полей `dining` публикуются, `dining_summary`/`dining_air_summary` ожили, `unavailable` 10 → 8** (остались только slave 10 «снято Alex'ом» + `todo.shopping_list`). **Загадка «замерзания» лога на 7 ч — тот же баг, устранена.** Диагностика включена (`MODBUS_DEBUG_RAW=1`), выключить после наблюдения. Детали — §5-кватер-З, §11 (журнал вечер-6). | Что | Факт | |---|---| @@ -35,15 +35,14 @@ > ✅ **Срочный пункт из прошлой сессии («гнездо 4 осталось отключённым») ЗАКРЫТ** — шнур на месте, оба аддона работают на верных гнёздах, регресса нет. -**⚠️ НОВОЕ НАБЛЮДЕНИЕ (не исследовано, причина неизвестна):** лог `modbus-bridge` «замерзал» — последняя запись была `08:32:58`, при живом аддоне (`started`) и текущем времени `15:33` → **~7 часов без единой строки**. После `ha apps restart local_modbus-bridge` лог ожил и пошёл сниффинг. -**Что НЕ утверждается:** причина не установлена, теорий не строим. Возможные направления для будущей сессии (проверять фактом, не гипотезой): засыпание USB-контроллера / зависание serial-хендла в контейнере / ротация лога. **Проверить** при следующем появлении: `ha apps info local_modbus-bridge` (state), сверить время последней строки лога с `date`, на живом ли HA (bridge опрашивает HA-сенсоры). +**✅ РАЗГАДАНО (2026-09-14, вечер-6):** «замерзание» лога `modbus-bridge` (~7 ч тишины при `state: started`) — **тот же баг сборки кадров**: застрявший в голове буфера мусор блокировал распознавание новых кадров, поэтому валидные кадры перестали логироваться. **Устранено фиксом T3.5 + сброс битого буфера (§5-кватер-З).** Отдельной причины не искать. Приём `ha apps restart ` остаётся рабочим (безвреден, опции не трогает), но больше не требуется для «оживления». > 📌 Побочный эффект наблюдения: **`ha apps restart ` — рабочий приём «оживить» bridge**, если HA-опрос встал. Проверено, безопасно (опции не трогает). **Не работает / не доделано:** | Что | Состояние | |---|---| -| **10 сущностей `unavailable`** (было 44) | **✅ ОСНОВНОЕ РЕШЕНО 2026-09-14 (финал): аддоны стояли на перепутанных гнёздах — поменяны местами → ушли ВСЕ 32 заслонки** (см. §5 «✅✅ РЕШЕНИЕ»). Рабочая привязка: `mbusd` = гнездо 3 (вентиляция), `modbus-bridge` = гнездо 4 (**ZONT 485**). **Остались 10:** 7 — `switch.fan_3_high/medium/low` + `sensor.fan_at2_*` (slave 10 закомментирован в `configuration.yaml`, задача СНЯТА Alex'ом — не поломка); **2 — `sensor.dining_summary`/`dining_air_summary`. ПРИЧИНА УТОЧНЕНА 2026-09-14 вечер-3 (§5-кватер-Д): это МАРШРУТ MQTT, не железо.** ZONT публикует `modbus/sensors/dining/*` в брокер **TrueNAS** (DNAT), а не в t610; там эти данные — **retained** (`modbus_ha_bridge disconnected`). Датчик столовой **исправен** (валидные 24.2 °C на TrueNAS). Прежняя версия «датчик отвечает `0`» — **уточнена/опровергнута**. Фикс — задача 5-мк (переключить DNAT на t610); 1 — `todo.shopping_list` (системная) | +| **8 сущностей `unavailable`** (было 44 → 10 → **8**) | **✅ ОСНОВНОЕ РЕШЕНО 2026-09-14 (финал): аддоны стояли на перепутанных гнёздах — поменяны местами → ушли ВСЕ 32 заслонки** (см. §5 «✅✅ РЕШЕНИЕ»). Рабочая привязка: `mbusd` = гнездо 3 (вентиляция), `modbus-bridge` = гнездо 4 (**ZONT 485**). **Затем (вечер-6, §5-кватер-З) ушли ещё 2** — `dining_summary`/`dining_air_summary` ожили после фикса сборки кадров bridge (T3.5). **Остались 8:** 7 — `switch.fan_3_high/medium/low` + `sensor.fan_at2_*` (slave 10 закомментирован в `configuration.yaml`, задача СНЯТА Alex'ом — не поломка); 1 — `todo.shopping_list` (системная). **Ни одного неизвестного дефекта** | | ~~`verify` как причина~~ | **ОПРОВЕРГНУТО как причина:** при верной привязке шин заслонки ожили при том же `verify` (`state_on:1`/`state_off:0`). Причина была в перепутанных шинах, не в `verify` | | **`sensor.fan_at2_*` / `switch.fan_3_*` — сироты** | В `configuration.yaml` **весь блок slave 10 (AT2 fans) закомментирован** → эти сущности физически не могут получать данные. Не задача — просто известный факт состояния конфига | | `switch.sauna` = `unknown` | Розетка физически отключена (`lastSeen` 8+ ч) — не баг | @@ -1246,6 +1245,146 @@ Raw RTU: 01 03 00 02 00 07 A5 C8 ← запрос гостиной (sla --- +### 5-кватер-З. ✅✅ modbus-bridge: ФИКС СДЕЛАН — гостиная ПУБЛИКУЕТСЯ (2026-09-14, вечер-6) — ЗАКРЫТО + +> **Итог одной строкой:** наивная сборка кадров заменена на каноничную (T3.5-разграничение + сброс битого буфера) → **все 7 полей `dining` живы в HA**, `dining_summary`/`dining_air_summary` ожили, `unavailable` **10 → 8** (осталось только slave 10 «задача снята» + `todo.shopping_list`). + +**Триггер:** Alex — *«Конечно правим»* + *«С флагом»*: правку делать в git-репо, диагностику — под env-флагом. Плюс идея Alex'а: *«можем при склейке проверять интервал с последнего получения? если он большой и склейка дала битый буфер — дропать»* + просьба поискать, как это решают другие проекты. + +#### 🔬 Ресёрч канона (субагент, реальный код с GitHub) + +| Проект | Делимитация кадра | Мусор в буфере | +|---|---|---| +| **libmodbus** (C, эталон) | по **byte-count из function code**: читает 2 байта → смотрит функцию → знает, сколько ещё | `tcflush(TCIOFLUSH)` при bad CRC | +| **pymodbus** (новый dev, `framer/rtu.py`) | CRC-hunting: по-байтовый сдвиг `for used_len in range(data_len)`, допускает мусор **до и после** кадра | «hunting mode», ждёт/сдвигается | +| **pymodbus** (классич. v3.0, `framer/rtu_framer.py`) | длина из `calculateRtuFrameSize()` | **`resetFrame()` → `self._buffer = b""`** (весь буфер в ноль) | + +**Формула T3.5** (`pymodbus/client/serial.py`, v3.4.1 → v3.6.9): +```python +t0 = (1 start + bytesize + stopbits) / baudrate # 3.6.9; в 3.4.1 было хардкод (1+8+2) +if baudrate > 19200: + silent_interval = 1.75 / 1000 # фикс. 1.75 мс +else: + inter_char_timeout = 1.5 * t0 # межбайтовый (1.5 символа) + silent_interval = 3.5 * t0 # межкадровый (3.5 символа) +``` +**При 9600 бод:** `t0 = 11/9600 = 1.146 мс` → **`T3.5 = 4.01 мс`**, `T1.5 = 1.72 мс`. + +> 📌 **Вывод ресёрча:** канон — это **не «склейка с таймаутом»**, а детерминированное разграничение по паузе на шине (T3.5) + **полный сброс буфера при неудаче** (`resetFrame`). libmodbus в C использует `tcflush` — то же по смыслу. +> 📌 Исходники скачаны в `~/modbus_src/` (rtu.py, rtu_framer v3.0.2/3.4.1, serial.py v3.6.9, modbus-rtu.c, modbus.c и др.). Аналитическая заметка: `Modbus/RTU_Framing_Source_Analysis.md`. + +#### 🔧 ФИКС — что изменено в `modbus_ha_bridge.py` + +**Было (наивно, ломается):** +```python +buf += ser.read(ser.in_waiting or 1) +i = 0 +while i <= len(buf) - 5: # скан всегда, независимо от паузы на шине + if not found: i += 1 # буфер НЕ обрезается → мусор-сироты копятся вечно +``` + +**Стало (канон: T3.5 + resetFrame):** +```python +# 1) константы из baud (не хардкод) + диагностика под env-флагом +DEBUG_RAW = os.environ.get("MODBUS_DEBUG_RAW", "0") == "1" +_T0 = (1 + 8 + 2) / float(BAUDRATE) +T3_5 = 0.00175 if BAUDRATE > 19200 else 3.5 * _T0 +T1_5 = T3_5 / 3.5 * 1.5 if BAUDRATE > 19200 else 1.5 * _T0 + +# 2) в main loop: кадр завершён ТОЛЬКО после паузы >= T3.5 +if _raw: + buf += _raw + _last_byte_time = time.time() +if not buf or (time.time() - _last_byte_time) < T3_5: + time.sleep(0.002); continue # байты ещё идут — не разбираем +i = 0 +# ... существующий CRC-скан (hunting) ... + +# 3) после скана: всё, что не сложилось в валидный кадр — МУСОР, в ноль +# (= resetFrame() в pymodbus) +if len(buf) > 0: + if DEBUG_RAW: + print(f"[BUF-DROP {len(buf)}] {bytes(buf).hex(' ')}") + buf.clear() +_last_byte_time = 0.0 +``` + +**Итого 3 правки:** (1) константы `T3_5`/`T1_5`+`DEBUG_RAW`, (2) гейт main loop по паузе, (3) сброс битого буфера + чистка сирот. Диагностика (`[RAW]`/`[BUF-DROP]`/`[DROP-*]`) — **под `MODBUS_DEBUG_RAW=1`**, по умолчанию выключена. + +#### ✅ Верификация — офлайн-тест ДО деплоя + +`~/tmp-mbbridge/test_framing.py` — прогон реальных байтов из продакшн-лога: + +| Сценарий | Вход | Результат | +|---|---|---| +| A: kids, разрыв 1+16 | `02` + `03 0c 44 9f …` | ✅ кадр собран | +| **B: dining 19 б + мусор `00`** | `00` + `01 03 0e … d2 4e c4` | ✅ **кадр собран, мусор дропнут (1 б)** | +| B2: dining, разрыв 1+18 | `01` + `03 0e …` | ✅ кадр собран | +| C: чистый мусор `14 01 40 55 f8` | — | ✅ дропнут (0 кадров, 5 б выброшено) | + +**4/4 прошли**, включая ключевой случай гостиной. + +#### 📊 Результат на живом t610 + +**Лог bridge — гостиная собирается из того самого разрыва, что раньше её ломал:** +```log +[RAW 1] 01 +[RAW 18] 03 0e 03 89 00 0e 00 1a 00 04 00 04 01 01 01 cb 2d ad +Slave: 1 Func: 0x3 CRC OK: True +Raw RTU: 01 03 0E 03 89 … CB 2D AD + → Sniff: dining_co2 = 905.0 + → MQTT publish: modbus/sensors/dining/co2 = 905.0 [OK] +``` + +**HA API (`/api/states`, токен из `options.ha_token`) — все 7 полей + оба summary живы:** +``` +sensor.dining_co2 = 912.0 sensor.dining_temperature_2 = 24.8 +sensor.dining_formaldehyde = 1.4 sensor.dining_humidity = 45.9 +sensor.dining_tvoc = 31.0 sensor.dining_pm2_5 = 0.3 +sensor.dining_pm10 = 3.0 +sensor.dining_summary = 25° 912ppm ← БЫЛ unavailable → ожил +sensor.dining_air_summary = 31tvoc 3pm ← БЫЛ TemplateError → ожил +``` + +| Метрика | До | После | +|---|---|---| +| Поля `dining` | 0 из 7 | **7 из 7** ✅ | +| `[BUF-LEFT]` (мусор копится) | ×20+ подряд | **0** ✅ | +| `[BUF-DROP]` (дроп при паузе) | — | **0** (кадры собираются, дропать нечего) ✅ | +| `unavailable` в HA | 10 | **8** ✅ | + +**Оставшиеся 8 `unavailable` — известные, не дефекты:** 7× `switch.fan_3_*`/`sensor.fan_at2_*` (slave 10 закомментирован, задача снята Alex'ом) + 1× `todo.shopping_list`. +**`dining_summary`-ERROR'ы из HA-лога (в 14:59, «round got invalid input 'unknown'») — исчезли**, т.к. данные появились. + +#### 🧠 Уроки (durable) + +1. **Причина «замерзания» лога на 7 ч (§1) — НАЙДЕНА и УСТРАНЕНА тем же фиксом:** застрявший мусор в голове буфера блокировал распознавание новых кадров. **Отдельной загадки больше нет.** +2. **«Поднять timeout» — была ОШИБОЧНАЯ гипотеза** (снята ещё вечером-5 замером). Различать «не хватает таймаута» и «сдвиг выравнивания буфера» — **только сырым логом байт**. +3. **Правило диагностики: сомневаешься в причине — логируй СЫРЫЕ байты, а не результат.** Прежнее логирование печатало только *валидные* кадры → битые/неполные исчезали бесследно, и диагноз был невозможен. +4. **Правку в продакшене делать через git-репо, не «на месте»** (сначала коммит, потом scp+rebuild) — даёт чистую историю и откат. +5. **Перед правкой сложной логики — офлайн-тест на реальных данных** (`test_framing.py`) дешевле и быстрее, чем rebuild+деплой-цикл (~1 мин каждый). + +#### 📌 Осталось (по этой задаче) + +- **Диагностика ВКЛЮЧЕНА** (`MODBUS_DEBUG_RAW=1` в `run.sh` на t610) — понаблюдать за стабильностью `dining`, затем **выключить**: закомментировать строку в `run.sh` → `ha apps rebuild local_modbus-bridge`. Ждёт команды Alex. +- **Крон-наблюдение** за `dining` (не пропадает ли снова) — не заводилось. + +**Git-коммиты (репо `git_admin/HA-ZONT-Modbus`, ветка `main`):** +``` +3748feb Fix RTU frame assembly: T3.5 inter-frame delimiting + bad-buffer reset ← ФИКС +ad6345a Add diagnostic raw-byte logging to modbus-bridge (temp diag) +6a8ca3f Sync bridge files from t610 prod: rename z2m entity_ids +7e0b281 Sync HA config from t610 migration +``` +**Файлы:** `~/tmp-mbbridge/` — `patch_bridge_framing.py` (патчер v2), `patch_bridge_framing_v3.py` (фикс гейта), `test_framing.py` (тест), `run.sh` (с env-флагом), бэкапы `before-framing-patch.py.bak`, `before-v3-gatefix.py.bak`; `~/tmp-t610/check_ha_states2.sh` (проверка HA API), `setup_gitea_creds.sh`. На t610: `/addons/modbus-bridge/modbus_ha_bridge.py.bak-preframing-*`, `run.sh.bak-*`. +**Исходники ресёрча:** `~/modbus_src/`. + +> ⚠️ **Питфолл (Hermes-специфичный, дважды ловился):** inline-команды с `$( … | jq …)` и `$( … | sed … )` **маскируются/ломаются** при передаче в shell (Hermes маскирует вид `TOKEN=***`). **Правило: любые скрипты с токенами/подстановками — ТОЛЬКО файлом** (`write_file` → `bash файл`), без inline `$( )`. +> ⚠️ **Питфолл `ha apps info`:** `--raw-json` отдаёт `{"result":"ok","data":{…}}` → путь `.data.options.ha_token`, а НЕ `.options`. Без `--raw-json` — YAML-вывод, где путь `.options`. +> ⚠️ **Питфолл `ha apps info`:** значение `ha_token` (183 симв.) в выводе **маскируется** — использовать программно (файлом), не глазами. + +--- + ### 5-кватер-Г. ✅ Node-RED: перенос flows с TrueNAS на t610 (2026-09-14, вечер-2) **Триггер:** Alex открыл `nodered.mallexxx.duckdns.org` → увидел basic auth вместо страницы Node-RED. В ходе разбора выяснилось, что **на t610 `flows.json` = 124 байта (пусто)** — агент при миграции **не перенёс flows Node-RED с TrueNAS**. Alex: *«в смысле чистый лист?? ты не перенёс все с truenas значит»* → команда **«переносим как есть»**. @@ -1581,7 +1720,7 @@ curl -s -X POST -H "$HDR" -H "Content-Type: application/json" \ | ~~1b~~ | ~~`verify` в заслонках~~ — **✅ ОПРОВЕРГНУТО:** заслонки ожили при том же `verify`. Не причина. | — | | ~~1c~~ | ~~ZONT-шина на других гнёздах~~ — **✅ СДЕЛАНО:** физический тест доказал (ZONT = гнездо 4, вентиляция = гнездо 3), привязки аддонов обменяны, док приведён в соответствие. Осталась только проверка «① подтвердить у Alex» — **закрыто**: он сам это и тестировал. | — | | ~~2~~ | ~~Раскомментировать slave 10 (AT2 fans)~~ — **СНЯТО с плана (Alex 2026-09-14): задачи по slave 10 НЕ БЫЛО.** | — | -| 3 | **`sensor.dining_summary` / `dining_air_summary`** — **🎯 ПРИЧИНА НАЙДЕНА ФАКТОМ (2026-09-14, вечер-5; §5-кватер-Ж): диагностическое логирование внедрено, сырой лог показал МЕХАНИЗМ.** Кадры приходят **разорванными** (1+7, 1+16 байт); нераспознанные байты-сироты (`00`) **накапливаются в буфере и не чистятся** (стр. 960–961 `if not found: i += 1` — `buf` не обрезается) → **сдвиг выравнивания** → 19-байтный ответ гостиной отбрасывается молча. `timeout` снят окончательно. **Правка логирования сделана** (git `~/Automation/HA-ZONT-Modbus`, коммиты `6a8ca3f` sync + `ad6345a` logging patch → Gitea; scp на t610 + `ha apps rebuild` + `restart`; бэкап `.bak-prelogging-20260914-155143`). **ОСТАЛОСЬ: выбор фикса (A/B/C, §5-кватер-Ж) — за Alex** | отдельно | +| ~~3~~ | ~~**`sensor.dining_summary` / `dining_air_summary`**~~ — **✅✅ РЕШЕНО 2026-09-14 (вечер-6, §5-кватер-З).** Фикс сборки кадров по канону (T3.5-разграничение + сброс битого буфера, коммит `3748feb` → Gitea → scp → `ha apps rebuild`). **Все 7 полей `dining` публикуются, оба summary ожили, `unavailable` 10→8.** Осталось: выключить диагностику (`MODBUS_DEBUG_RAW`) после наблюдения | ✅ закрыто | | ~~3-гт~~ | ~~**Gitea remote для `~/Automation/HA-ZONT-Modbus`**~~ — **✅ СДЕЛАНО 2026-09-14 (см. §5-кватер-Е «GITEA»).** Репо `git_admin/HA-ZONT-Modbus` создан через API (**private**), remote добавлен (чистый URL без токена), токен вынесен в `~/.git-credentials` (chmod 600) + `credential.helper=store`, первый push прошёл (`7e0b281`, ветка `main`). **Осталось:** ⚠️ ротировать/вынести токен из НАМЕРТВО открытого remote у `nolvu-landing` (`https://git_admin:@…` — светился в выводах команд). Детали — §5-кватер-Е | ✅ сделано | | 4 | **Камера** — найти образ/папку, поднять на t610, поправить upstream в Caddy | отдельно | | ~~5~~ | ~~**Этап 4:** Caddy upstream → t610~~ — **✅ ЧАСТЬ «CADDY» ЗАКРЫТА 2026-09-14 (см. §5-кватер-Б/В):** Caddyfile залит (Alex подменил файл + `restart caddy`), `mallexxx.duckdns.org` → **HTTP 200** (HA на t610, подтверждено Alex'ом), попутно исправлен `trusted_proxies` в `.storage/http` (§5-кватер-В-1). **ОСТАЛОСЬ из Этапа 4:** ① `nodered.*` — починить порт (см. задачу 5-нр ниже); ② GPON-редирект → t610; ③ ZONT MQTT → t610. **🔴 Архитектурный вывод: Caddy НЕ переносить** (17 из 20 доменов — сервисы TrueNAS) | — | @@ -1626,7 +1765,7 @@ curl -s -X POST -H "$HDR" -H "Content-Type: application/json" \ | Шаг | Задача | Риск | Действие | |---|---|---|---| | ~~A1~~ | ~~№3: `\|default(0)`~~ — **❌ СНЯТ ОКОНЧАТЕЛЬНО** | — | Причина: данные на шине есть, bridge их теряет (§5-кватер-Д). Формула не чинится | -| **A0** | **№3: починка приёма кадра bridge** (приоритет сессии) | 🔶 средний | **✅ Шаг 1 (правка логирования) ВЫПОЛНЕН 2026-09-14 вечер-5:** коммиты `6a8ca3f`+`ad6345a` в `~/Automation/HA-ZONT-Modbus` → Gitea, scp на t610, `ha apps rebuild` + `restart`, бэкап `.bak-prelogging-*`. Сырой лог дал механизм (§5-кватер-Ж). **➡️ Шаг 2 (фикс) — ЖДЁТ ВЫБОРА ALEX: (A) обрезать нераспознанный префикс буфера [3 строки, быстрый тест] / (B) чтение по межбайтовой паузе ≥3.5 симв. [лечит причину] / (C) ещё диагностика перед правкой** | +| ~~**A0**~~ | ~~**№3: починка приёма кадра bridge** (приоритет сессии)~~ | — | **✅✅ ВЫПОЛНЕНО ЦЕЛИКОМ 2026-09-14 вечер-6 (§5-кватер-З):** Шаг 1 (логирование, вечер-5) дал механизм; **Шаг 2 — фикс сборки кадров по канону (T3.5 + `resetFrame`), коммит `3748feb` → Gitea → scp → `ha apps rebuild` → `restart`.** Проверено офлайн-тестом (4/4) и на живом: **7/7 полей `dining` в HA, оба summary ожили, `unavailable` 10→8.** Осталось: выключить диагностику после наблюдения | | ~~B1~~ | ~~**№5 (Этап 4): ДОЛИТЬ Caddyfile**~~ — **✅ СДЕЛАН + `trusted_proxies` фикс** | — | Alex подменил файл + `docker restart caddy`. `mallexxx.duckdns.org` → 200. Затем `.storage/http` → `trusted_proxies += 192.168.2.197/32` (§5-кватер-В) | | **B2** | **Node-RED наружу** — выставить порт аддона и поправить Caddy | 🔶 средний | Опции `a0d7b954_nodered`: `{ "1880/tcp": 11880 }`, `host_network` убрать → рестарт → Caddy `nodered.*` → `192.168.2.176:11880` + reload. **Сначала ответить: нужен ли пустой t610-Node-RED?** (§5-кватер-В-2) | | **B3** | **Этап 4, остаток:** GPON-редирект → t610, ZONT MQTT → t610 | 🔴 высокий | Трогает живое → окно + согласование | @@ -1943,6 +2082,33 @@ ssh root@192.168.2.176 'ha apps rebuild local_modbus-bridge && ha apps restart l --- +### 2026-09-14 (вечер-6: ФИКС сборки кадров bridge — гостиная ЗАРАБОТАЛА) + +**Контекст:** Alex — *«Конечно правим»*, *«С флагом»*. Перед этим дал идею: *«можем при склейке проверять интервал с последнего получения? если он большой и склейка дала битый буфер — дропать»* + просил поискать, как это делают другие проекты. + +**Что сделано:** + +| Шаг | Факт | +|---|---| +| 🔬 Ресёрч канона | Субагент вытащил реальный код: **libmodbus** (`src/modbus.c` — byte-count из function code + `tcflush`), **pymodbus** `framer/rtu.py` (CRC-hunting) и классич. `rtu_framer.py` (**`resetFrame()` → буфер `b""`**). Формула: `t0=(1+8+2)/baud`; `T3.5 = 3.5*t0` (baud≤19200), при 9600 → **4.01 мс**; при baud>19200 фикс. 1.75 мс. Исходники → `~/modbus_src/` | +| 🔧 Фикс | 3 правки: (1) константы `T3_5`/`T1_5` из baud + `DEBUG_RAW`; (2) гейт main loop — кадр завершён **только после паузы ≥ T3.5**; (3) **сброс битого буфера** после разбора (= `resetFrame`). Диагностика под env `MODBUS_DEBUG_RAW=1` | +| ✅ Тест офлайн | `~/tmp-mbbridge/test_framing.py` на реальных байтах: **4/4** (включая «dining 19 б + мусор `00`» → кадр собран, мусор дропнут) | +| 📦 Git | Коммит **`3748feb`** → Gitea (`ad6345a` — диагностика от вечер-5) | +| 🚀 Деплой | бэкап `.bak-preframing-*` → scp → `ha apps rebuild` + `restart` → `state: started` | +| 🎯 Результат | **7/7 полей `dining` живы в HA** (co2 912, tvoc 31, temp 24.8, humidity 45.9, …). `dining_summary = 25° 912ppm`, `dining_air_summary = 31tvoc 3pm` — **оба ожили**. `[BUF-LEFT]` = **0** (мусор больше не копится). **`unavailable` 10 → 8** | + +**Что менялось в железе:** код `modbus_ha_bridge.py` на t610 + `run.sh` (env-флаг) → rebuild + restart. **Функциональный фикс подтверждён фактами (лог bridge + HA API).** + +**Закрыто:** задача №3 (`dining_summary`) ✅, план A0 ✅, **загадка «замерзание лога на 7 ч»** — тот же баг, устранена ✅. + +**Не сделано:** выключить диагностику `MODBUS_DEBUG_RAW` после наблюдения (ждёт команды Alex). + +> 📌 **Процессный урок (положительный):** Alex дал **гипотезу + просьбу проверить канон** — агент **не гадал**, а: (1) нашёл реальный код libmodbus/pymodbus, (2) офлайн-тест на реальных байтах **до** деплоя, (3) деплой + верификация через независимый источник (HA API, а не только лог самого bridge). Схема «гипотеза → канон → офлайн-тест → деплой → независимая проверка» сработала с первого раза. +> ⚠️ **Питфолл Hermes (трижды за сессию):** inline `$( … | jq … )` / `$( … | sed … )` + токены → **маскируются и ломаются**. Только **скрипт файлом**. +> ⚠️ **Питфолл `ha apps info --raw-json`:** путь `.data.options.ha_token`, не `.options`. Значение `ha_token` в выводе маскируется → использовать программно. + +--- + ## Связанные заметки - [[family/how-to/home-automation]] — карта Modbus slave ID, ZONT, регистры вентиляции