[2026-09-18] eagle: family/how-to/home-automation.md personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md

This commit is contained in:
Alexey Martemyanov
2026-09-18 00:57:58 +06:00
parent d30ddb3e3e
commit 9b974e5b00
3 changed files with 268 additions and 27 deletions
+8 -1
View File
@@ -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) — вентиляторы
+208 -26
View File
@@ -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'ы 105111.** Решения 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'ы 105111.** Скрипт `/tmp/add_floor_sensors.py` готов, **не запущен** — ждёт 1 ответ: добавлять ли slave 112 (туалет 1). · авто-id: верх окна не назван (§30.3, стоит `19999`) |
>
> Свежие круги: §27 (46 — `set_contour_target`), §28 (4749 — `call_sub`, `send_sms`, `exit`, `scenario_orphans`), §30 (50 — авто-id + валидация ссылок), §31 (Zigbee → Modbus).
> Свежие круги: §27 (46 — `set_contour_target`), §28 (4749 — `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 105111) + 7 × тип 52 (вложенные) + 7 × тип 1 | ❌ не начато |
| **3** | Энкодер: расширить `_LEAF_TYPES` до `{9, 1, 51}` + снять барьеры `'id'` в 4 точках (§31.5) | **сделано**, коммит `9dfb2ed` (§31.9) |
| **4** | ZONT YAML: +21 объект — 7 × тип 51 (slave 105111) + 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: <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 датчиков (105111)
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 105112 **уже есть**, правка не нужна |
| 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(<int>)` видел число, которого
нет ни в `_known_ids`, ни в `renames` → вызывал `alloc(<int>)` → выдавал **новый номер 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(<int>)` возвращает тот же номер (ветка `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 показывает, что
«круг чистый» сам по себе **не доказательство**.
@@ -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(<int>)` вернул
тот же номер.
```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 | круг по **всем** снапшотам, не по свежему | регресс на старых дампах |