diff --git a/family/how-to/home-automation.md b/family/how-to/home-automation.md index d8a64d94..30c8a2f3 100644 --- a/family/how-to/home-automation.md +++ b/family/how-to/home-automation.md @@ -745,10 +745,17 @@ ZONT ──[тип 51: устройство, slave_id]──────── только контур отопления `#Z10152=16,'Тёплый пол'` и его штатный датчик `#Z8432=27`. Ни одной записи типа 51/1 для тёплых полов нет → bridge их отдаёт, ZONT **не спрашивает**. +**⏳ Состояние регистрации (2026-09-18, круг 51):** работа **в процессе**. Энкодер научился +выдавать авто-id для типов **1 / 51 / 52** (коммит `9dfb2ed`), поэтому 21 новый объект +(7 × тип 51 + 7 × тип 52 + 7 × тип 1) добавляются **без прописанных id**. Имена на ZONT-стороне — +**короткий префикс «ТП: »** (`ТП: гостиная`, `ТП: серая`, …), решение Alex, не полная фраза +«Температура … тёплый пол». Скрипт `/tmp/add_floor_sensors.py` (флаг `--toilet` для 8-го датчика, +slave 112) готов и **не запущен** — ждёт ответа Alex по slave 112. Детали: §32 доки компилятора. + **Порядок работ (обе стороны!).** Bridge-сторона и ZONT-сторона правятся **разными файлами** и деплоятся по-разному: bridge — `/addons/modbus-bridge/data/config.template.tmpl` на t610 + `rebuild`; ZONT — конвертеры в `/Users/admin/Automation/HA-ZONT-Modbus` + заливка на контроллер. -⚙️ Механика ZONT-стороны: [[personal/projects/zont-config-compiler]] §31. +⚙️ Механика ZONT-стороны: [[personal/projects/zont-config-compiler]] §31, §32. ### AT2 (slave 10) — вентиляторы diff --git a/personal/projects/zont-config-compiler.md b/personal/projects/zont-config-compiler.md index fb71587b..b98a4a15 100644 --- a/personal/projects/zont-config-compiler.md +++ b/personal/projects/zont-config-compiler.md @@ -3,7 +3,7 @@ title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml namespace: personal type: how-to created: '2026-09-17' -updated: '2026-09-18 (круг 51: Zigbee тёплый пол → Modbus, решения Alex + замер авто-id для 51/52/1)' +updated: '2026-09-18 (круг 51: авто-id для типов 1/51/52 + липкий номер (питфолл 107), коммит 9dfb2ed)' tags: - personal - zont @@ -40,6 +40,24 @@ aliases: - ZONT keep renames питфолл - ZONT барьер id in obj - ZONT питфолл 104 + - ZONT круг 51 + - ZONT авто-id 1 51 52 + - ZONT modbus_devices без id + - ZONT virtual_sensor без id + - ZONT вложенный регистр auto id + - ZONT питфолл 106 + - ZONT питфолл 107 + - ZONT липкий id + - ZONT keep sticky id + - ZONT alloc None не кэширует + - ZONT двойной id объект пропадает + - ZONT круг чистый не значит выпущен + - ZONT ТП префикс + - ZONT тёплый пол modbus + - ZONT тест_autoid_granica2 + - ZONT accept_autoid_types + - ZONT add_floor_sensors + - ZONT _alloc_id два прохода - ZONT валидация ссылок - ZONT ссылка в никуда - ZONT конец сценария @@ -174,17 +192,18 @@ related: | **Снять живой конфиг** | 🔴 `curl -s http://192.168.0.50/config.txt` — **без авторизации** | | **ZONT в общем контуре** | [[family/how-to/home-automation]] §6 | -> ### 📍 Текущее состояние (2026-09-18, после §31) +> ### 📍 Текущее состояние (2026-09-18, после §32 = круг 51) > > | | | > |---|---| -> | **HEAD** | `7e34ee1` — «авто-id для листьев + валидация ссылок (круг 50)» | +> | **HEAD** | `9dfb2ed` — «авто-id: типы 1/51/52 (белый список + липкий номер)» | > | **Круг** | **10/10 зелёный**, 759 → 759 объектов | > | **Артефакт** | `zont_config/config_local_2026-09-18_00-45-31.yml` (идентичен `23-21-20`) | > | **Свежий дамп** | ✅ снят 2026-09-18 00:45 — **идентичен** `23-21-20` (`diff` пуст) | -> | **Открыто** | 🔴 **§31: 7 Zigbee-датчиков тёплых полов → slave'ы 105–111.** Решения Alex получены (авто-id, имена в стиле ZONT), замер показал: типы 51/52/1 требуют снятия барьеров `'id'`. Bridge-правка НЕ нужна. Ждёт 1 ответ: добавлять ли slave 112. · авто-id: верх окна не назван (§30.3) | +> | **✅ Готово** | Авто-id для типов **1 / 51 / 52** (`_LEAF_TYPES = {9, 1, 51}` + снятие барьеров `'id'` + **липкий номер** через `keep()`, питфолл 107). Приёмка зелёная, регресс 10/10 | +> | **Открыто** | 🔴 **§31: 7 Zigbee-датчиков тёплых полов → slave'ы 105–111.** Скрипт `/tmp/add_floor_sensors.py` готов, **не запущен** — ждёт 1 ответ: добавлять ли slave 112 (туалет 1). · авто-id: верх окна не назван (§30.3, стоит `19999`) | > -> Свежие круги: §27 (46 — `set_contour_target`), §28 (47–49 — `call_sub`, `send_sms`, `exit`, `scenario_orphans`), §30 (50 — авто-id + валидация ссылок), §31 (Zigbee → Modbus). +> Свежие круги: §27 (46 — `set_contour_target`), §28 (47–49 — `call_sub`, `send_sms`, `exit`, `scenario_orphans`), §30 (50 — авто-id + валидация ссылок), §31 (разведка Zigbee → Modbus), §32 (**51 — авто-id типов 1/51/52**). > > ✅ Снято с открытого в §28.6: «5 `objcmd`-орфанов» и «поле 4 = `2` у `expr`» — **работы не требуют** (орфанов нет; поле производное). @@ -4544,43 +4563,69 @@ ssh root@192.168.2.176 'grep -nE "floor_temperature" \ |---|---|---| | **1** | Снять + сконвертировать свежий конфиг, круг по всем снапшотам | ✅ **сделано**: 10/10 зелёный, `config_local_2026-09-18_00-45-31.{txt,yml}`, YAML идентичен `23-21-20` | | ~~2~~ | ~~Bridge: добавить 7 маппингов~~ | ✅ **не требуется** — уже есть (§31.6) | -| **3** | Энкодер: расширить `_LEAF_TYPES` до `{9, 1, 51}` + снять барьеры `'id'` в 4 точках (§31.5) | ❌ не начато | -| **4** | ZONT YAML: +21 объект — 7 × тип 51 (slave 105–111) + 7 × тип 52 (вложенные) + 7 × тип 1 | ❌ не начато | +| **3** | Энкодер: расширить `_LEAF_TYPES` до `{9, 1, 51}` + снять барьеры `'id'` в 4 точках (§31.5) | ✅ **сделано**, коммит `9dfb2ed` (§31.9) | +| **4** | ZONT YAML: +21 объект — 7 × тип 51 (slave 105–111) + 7 × тип 52 (вложенные) + 7 × тип 1 | ⏳ скрипт готов, ждёт ответа по slave 112 | | **5** | Энкодер → txt, круг зелёный, показать Alex'у 21 строку с выданными id | ❌ не начато | | **6** | **Проверка Alex'ом в UI ZONT** → заливка → коммит | ❌ не начато | -**Имена (стиль существующих, решение Alex):** +**Имена (префикс «ТП: », решение Alex 2026-09-18):** | Slave | HA-сущность | Имя в ZONT | |---|---|---| -| 105 | `sensor.living_room_floor_temperature_temperature` | Температура гостиная тёплый пол | -| 106 | `sensor.severnaia_floor_temperature_temperature` | Температура серая тёплый пол | -| 107 | `sensor.kabinet_floor_temperature_temperature` | Температура кабинет тёплый пол | -| 108 | `sensor.kitchen_floor_temperature_temperature` | Температура кухня тёплый пол | -| 109 | `sensor.vannaia_floor_temperature_temperature` | Температура ванная тёплый пол | -| 110 | `sensor.prikhozhaia_floor_temperature_temperature` | Температура прихожая тёплый пол | -| 111 | `sensor.dushevaia_floor_temperature_temperature` | Температура душевая тёплый пол | +| 105 | `sensor.living_room_floor_temperature_temperature` | ТП: гостиная | +| 106 | `sensor.severnaia_floor_temperature_temperature` | ТП: серая | +| 107 | `sensor.kabinet_floor_temperature_temperature` | ТП: кабинет | +| 108 | `sensor.kitchen_floor_temperature_temperature` | ТП: кухня | +| 109 | `sensor.vannaia_floor_temperature_temperature` | ТП: ванная | +| 110 | `sensor.prikhozhaia_floor_temperature_temperature` | ТП: прихожая | +| 111 | `sensor.dushevaia_floor_temperature_temperature` | ТП: душевая | +| 112? | `sensor.toilet_1_floor_temperature_temperature` | ТП: туалет 1 (под вопросом) | + +> 🔴 **Alex отверг длинный вариант имён.** Первый мой вариант был «Температура гостиная тёплый +> пол» — Alex ответил: **«Имена: префикс \"ТП: \"»**. Короткий префикс, не полная фраза. Формулировка +> «в стиле существующих на зонте» означала **тот же короткий регистр**, а не копирование шаблона +> `Температура <место>`. Урок: «в стиле существующих» ≠ «повтори существующий текст целиком». Форма каждого объекта (зеркало `#Z8381`/`#Z8383`/`#Z8382` из §31.2): ```yaml modbus_devices: - slave_id: 105 - name: Температура гостиная тёплый пол + name: 'ТП: гостиная' poll_interval: 5000 timeout: 300000 - registers:[{address: 100, function: holding_3_6, bit_width: 16, - signal_type: param_int16, direction: read}] + registers: + - name: 'Знач. темп.' + address: 100 + function: holding_3_6 + bit_width: 16 + signal_type: param_int16 + direction: read raw_params: [[], [], 0] virtual_sensors: - serial_number: '0' - name: Температура гостиная тёплый пол + name: 'ТП: гостиная' register_id: connection_loss_delay_ms: 60000 ``` -⏳ **Один открытый вопрос:** slave **112** (`toilet_1_floor_temperature_temperature`) — мост его уже -отдаёт, но в план Alex'а он не входил. Добавлять 8-й датчик или оставить на потом? +**ID — по решению Alex «Let allocator decide»:** ни один id в YAML **не прописывается**. +Все 21 объект идут без `id`, номера выдаёт аллокатор из окна `4098..19999` (дыры снизу вверх). +Регистр и виртуальный датчик ссылаются на выданный номер автоматически (`register_id` +подставляет проход устройства, см. §31.9). + +**Скрипт добавления (идемпотентный, флаг `--toilet` для 8-го датчика):** + +```bash +python3 /tmp/add_floor_sensors.py # 7 датчиков (105–111) +python3 /tmp/add_floor_sensors.py --toilet # 8 датчиков (+112) +``` + +⏳ **Один открытый вопрос (единственный):** slave **112** +(`toilet_1_floor_temperature_temperature`) — мост его уже отдаёт, но в план Alex'а он не входил. +Добавлять 8-й датчик или оставить на потом? Alex этот вопрос ещё не разобрал — +на повторную формулировку ответил «Второй вопрос не понял», поэтому вопрос переформулирован +в одну строку. ### 31.8. Статус @@ -4588,11 +4633,148 @@ virtual_sensors: |---|---| | Свежий конфиг снят, конвертирован | ✅ `config_local_2026-09-18_00-45-31.{txt,yml}` — идентичен `23-21-20`, круг **10/10** | | Разведка (11 slaves, 7 отсутствующих датчиков) | ✅ выполнена, факты §31.2–31.3 | -| Решения Alex | ✅ оба получены: авто-id + имена в стиле ZONT (§31.4) | -| Замер авто-id для 51/52/1 | ✅ выполнен — **все три типа падают** `Ошибка: 'id'` (§31.5) | +| Решения Alex | ✅ авто-id («Let allocator decide») + имена с префиксом «ТП: » (§31.4, §31.7) | +| Замер авто-id для 51/52/1 | ✅ выполнен — **все три типа падали** `Ошибка: 'id'` (§31.5) | | Bridge | ✅ правка НЕ нужна — slaves 105–112 уже в шаблоне (§31.6) | -| План | ✅ уточнён: 6 фаз, фаза 1 закрыта (§31.7) | -| Правки кода / конфига | ❌ не начаты (правило: план прежде кода) | -| HEAD | `7e34ee1` (авто-id листьев + валидация ссылок) | +| **Энкодер: авто-id для 1/51/52** | ✅ **сделано, коммит `9dfb2ed`** — приёмка зелёная, регресс 10/10 (§31.9) | +| Правка ZONT YAML (+21 объект) | ⏳ скрипт `/tmp/add_floor_sensors.py` готов, не запущен — ждёт ответа по slave 112 | +| HEAD | `9dfb2ed` (авто-id типов 1/51/52) | + +--- + +## 32. ✅ Круг 51 (2026-09-18) — авто-id для типов 1 / 51 / 52, липкий номер + +**Задача (Alex):** «Тяни и конвертируй свежий конфиг и добавь в него entity zigbee датчиков +температуры которых в нем нет через виртуальные modbus устройства в bridge». + +**Решения Alex по ходу:** +1. Способ выдачи id — **«Let allocator decide»** (не прописывать номера руками). +2. Имена — **префикс «ТП: »**. + +### 32.1. Что сделано + +| # | Шаг | Статус | +|---|---|---| +| 1 | Свежий конфиг: `curl` → `config-to-yml.py` → целевой `.yml` | ✅ 10/10 круг, YAML идентичен `23-21-20` | +| 2 | Разведка bridge | ✅ slaves 105–112 **уже есть**, правка не нужна | +| 3 | Замер авто-id 51/52/1 | ✅ все три падали `Ошибка: 'id'` (питфолл 104 в новых местах) | +| 4 | `_LEAF_TYPES = {9}` → `{9, 1, 51}` | ✅ | +| 5 | Снятие барьеров `obj['id']` в 4 точках | ✅ | +| 6 | 🔴 **Найден и исправлен баг «двойного id»** — см. §32.3 | ✅ | +| 7 | Приёмка: 3 типа, id в окне, круг чистый | ✅ `/tmp/accept_autoid_types.py` | +| 8 | Регресс по 10 снапшотам | ✅ **10/10** | +| 9 | Коммит | ✅ `9dfb2ed` | + +**Правки энкодера (идемпотентный скрипт `/tmp/fix_autoid_types.py`, 5 замен с `count == 1`):** + +| Место | Что | +|---|---| +| `_LEAF_TYPES` | `{9}` → `{9, 1, 51}`; тип 52 — вложенный, в список не идёт | +| `virtual_sensors` (~329) | `if 'id' not in obj` → выдача номера | +| `modbus_devices` (~1776) | выдача id вложенным регистрам **до** сбора `register_ids` | +| текст ошибки устройства | `obj['id']` → `obj.get('id')` | +| сборка строки устройства | `if 'id' not in obj` → выдача номера | + +### 32.2. 🔴 ПИТФОЛЛ 106 — «круг чистый» НЕ значит «объект выпущен» + +Промежуточная приёмка показывала `exit=0` и `roundtrip: ЧИСТЫЙ` для всех трёх типов — +**при том, что объект молча исчезал из вывода**. Круг проверяет «вывод парсера == вход», +а входом был вывод энкодера, из которого объект уже пропал. Зелёный круг на потере объекта. + +**Как ловить:** считать объекты **до и после** (`len(in) vs count(out)`) и искать объект по +имени в артефакте. Признак: `exit=0`, круг чистый, а строки нет. + +```python +# правильная проверка — по имени, а не по «круг чистый» +hits = [l for l in out.split("\r\n") if "ТЕСТ-ДАТЧИК" in l] +if not hits: print("ОБЪЕКТ ПОТЕРЯН") +``` + +### 32.3. 🔴 ПИТФОЛЛ 107 — `_alloc.alloc(None)` НЕ кэширует → два id на один объект + +**Первопричина потери объекта** (найдена трассировкой, не догадкой). + +`_IdAllocator.alloc(old_id=None)` кэширует выдачу в `self.renames` **только если +`old_id is not None`** (строка 63). Вызов `_alloc.alloc(None)` номер **не запоминает**. + +Энкодер проходит объекты **дважды**: +1. секционный цикл (`virtual_sensors` ~329, `modbus_devices` ~1776) — кладёт строку в `lines`; +2. цикл `TYPE_ORDER` (~2118) — собирает `result_lines`, ища `zid in z_dict`. + +В проходе 1 объект без id получал номер **A**. В проходе 2 `_alloc_id()` видел число, которого +нет ни в `_known_ids`, ни в `renames` → вызывал `alloc()` → выдавал **новый номер B**. +`B in z_dict` → `False` → строка **молча** терялась. + +**Доказательство трассировкой** (временные принты в копию энкодера): + +``` +DBG-T1 [..., "#Z4102=1,'0','ТЕСТ-ДАТЧИК',..."] ← проход 1 выдал 4102 +DBG-TZ obj_id=4101 -> zid=4101 in_z_dict=False ← проход 2 выдал 4101, строки нет +``` + +**Лечение:** номер делается **липким** через `_alloc.keep(new_id)` сразу после выдачи — +`keep()` регистрирует тождественное переименование `id -> id`, и любой последующий +`_alloc_id()` возвращает тот же номер (ветка `zid in _alloc.renames`). + +```python +if 'id' not in obj: + obj['id'] = _alloc.alloc(None) + _alloc.keep(obj['id']) # ← без этой строки объект пропадёт +``` + +Это **та же механика, что спасла 66 шагов типа 46 в круге 50** (см. docstring `keep()`): +там отсутствие `keep` дало «строку в никуда». Питфолл повторяется — при любой новой точке +авто-id `keep()` обязателен. + +**Скрипт правки:** `/tmp/fix_autoid_sticky.py` (3 замены, `count == 1`). + +### 32.4. ✅ Приёмка (проверено, не «должно работать») + +``` +=== A: тип 51 (modbus_device) без id === + 51 exit=0 id=4101 in_window=True + #Z4101=51,250,'ТЕСТ-ДЕВ',5000,300000,[],[],0,[] + roundtrip: ЧИСТЫЙ +=== B: тип 52 (вложенный регистр) без id === + 52 exit=0 id=4101 in_window=True + #Z4101=52,'ТЕСТ-РЕГ',100,16,0,1,16,0,1,29,1,4 + roundtrip: ЧИСТЫЙ +=== C: тип 1 (virtual_sensor) без id === + 1 exit=0 id=4101 in_window=True + #Z4101=1,'0','ТЕСТ-ДАТ',0,0,0,60000,[],[],[],8383,[],0,0,0 + roundtrip: ЧИСТЫЙ +=== ИТОГ: ВСЁ ЗЕЛЁНОЕ === +``` + +**Проверка ссылки устройство→регистр** (оба без id): + +``` +#Z4102=51,252,'ТЕСТ-ПАРА',5000,300000,[],[],0,[4101] +#Z4101=52,'ТЕСТ-РЕГ2',100,16,0,1,16,0,1,29,1,4 +>>> device id=4102, register id=4101, device refs [4101] -> LINK OK=True +``` + +Ссылка подставляется верно — критично, иначе ZONT опрашивал бы регистр по неверному id. + +**Регресс:** 10/10 снапшотов `ЧИСТЫЙ`. + +**Инструменты:** `/tmp/fix_autoid_types.py`, `/tmp/fix_autoid_sticky.py` (правки, идемпотентные), +`/tmp/accept_autoid_types.py` (приёмка), `/tmp/trace_zdict.py`, `/tmp/trace_typeorder.py`, +`/tmp/trace_iter.py`, `/tmp/trace_vs.py` (трассировки). Все в `/tmp`, в репо не кладутся. + +### 32.5. Границы расширения (обновлено) + +| Тип | Секция | Авто-id | Почему | +|---|---|---|---| +| 9 | `relay_commands` | ✅ | лист, проверено в круге 50 | +| **1** | `virtual_sensors` | ✅ | **круг 51** | +| **51** | `modbus_devices` | ✅ | **круг 51** | +| **52** | `modbus_devices[].registers` | ✅ | **круг 51**, выдаётся в проходе устройства (не через `_LEAF_TYPES`) | +| 16, 27 | `heating_circuits`, `temperature_sensors` | ❌ | ссылки **по типу** — нужна правка `_body_type` | +| 5, 57, 10 | `actions`, `mqtt_topics`, `gui_switches` | ❌ | цикл требует `id` у каждой записи | + +**Правило:** каждый новый тип обязан пройти `/tmp/accept_autoid_types.py`-подобный замер — +«снял id → собрал → объект есть в выводе → круг чистый». Питфолл 106/107 показывает, что +«круг чистый» сам по себе **не доказательство**. diff --git a/personal/tech/roundtrip-key-verification.md b/personal/tech/roundtrip-key-verification.md index 5469012d..287355b8 100644 --- a/personal/tech/roundtrip-key-verification.md +++ b/personal/tech/roundtrip-key-verification.md @@ -320,3 +320,55 @@ git stash pop База тоже сломана → унаследовано; база зелёная → это твоя правка. И **коммитить сразу, как только круг зелёный** — `git checkout --` затирает незакоммиченное без возврата. + +### Ловушка 6 — 🔴 «круг чистый» ≠ «объект выпущен» (2026-09-18, круг 51) + +Самая коварная из ловушек: **приёмка показала `exit=0` и `roundtrip: ЧИСТЫЙ` для всех трёх +проверяемых типов — при том, что объект молча исчезал из вывода.** Круг проверяет «вывод +парсера == вход», а входом был вывод энкодера, из которого объект уже пропал. Зелёный круг +на потере объекта. + +**Как ловить — считать объект по ИМЕНИ в артефакте, а не полагаться на круг:** + +```python +hits = [l for l in out.split("\r\n") if "ТЕСТ-ДАТЧИК" in l] +if not hits: + print("ОБЪЕКТ ПОТЕРЯН") # круг при этом может быть «ЧИСТЫЙ» +``` + +### Ловушка 6.1 — первопричина: `alloc(None)` не кэширует → два id на один объект + +Энкодер проходит объекты **дважды**: секционный цикл кладёт строки в `lines`, затем цикл +`TYPE_ORDER` собирает `result_lines`, ища `zid in z_dict`. Если выдача номера **не кэшируется**, +второй проход выдаёт **другой** номер, `zid` не находится в `z_dict` и строка теряется молча. + +Доказательство — временные принты в копию энкодера: + +``` +DBG-T1 [..., "#Z4102=1,'0','ТЕСТ-ДАТЧИК',..."] ← проход 1 выдал 4102 +DBG-TZ obj_id=4101 -> zid=4101 in_z_dict=False ← проход 2 выдал 4101, строки нет +``` + +**Лечение — номер должен быть «липким»:** сразу после выдачи зарегистрировать его +тождественным переименованием (`keep()`), чтобы любой последующий `_alloc_id()` вернул +тот же номер. + +```python +if 'id' not in obj: + obj['id'] = _alloc.alloc(None) + _alloc.keep(obj['id']) # ← без этой строки объект пропадёт +``` + +**Урок:** при внедрении авто-id проверять, сколько раз энкодер проходит один объект, и +кэшируется ли выдача между проходами. «Круг чистый» этого не покажет (см. ловушку 6). + +**Приёмка авто-id — полный чек-лист** (все пункты обязательны, ни один не заменяет другой): + +| # | Проверка | Что ловит | +|---|---|---| +| 1 | `exit=0` | падение кодирования | +| 2 | объект найден в выводе **по имени** | потерю объекта (ловушки 1, 2, 6) | +| 3 | выданный id **внутри окна** `ID_MIN..ID_MAX` | прыжок в железный диапазон | +| 4 | ссылка владельца → id ребёнка (устройство → регистр) | битую связь | +| 5 | круг чистый на **всём** файле | регресс | +| 6 | круг по **всем** снапшотам, не по свежему | регресс на старых дампах |