Files
obsidian-vault/family/how-to/zont-config-compiler.md
T

662 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
aliases:
- ZONT config compiler
- config-to-yml
- yml-to-config
- HA-ZONT-Modbus
- ZONT конвертеры конфига
created: '2026-09-17'
namespace: family
related:
- '[[family/tech/zont-api]]'
- '[[family/tech/zont-config-object-types]]'
- '[[family/tech/zont-scenario-logic-11109]]'
- '[[family/how-to/home-automation]]'
- '[[family/how-to/gitea-config]]'
tags:
- family
- how-to
- zont
- modbus
- homeautomation
title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml
type: how-to
updated: 2026-09-17h
---
# ⚙️ ZONT Config Compiler — конвертеры `.txt ⇄ .yml`
Двусторонние конвертеры между конфигом контроллера **ZONT** (`.txt`) и читаемым **YAML**.
Позволяют править конфиг руками — имена реле, адреса Modbus, интервалы опроса, датчики — не заходя в UI контроллера.
| | |
|---|---|
| **Проект (Mac)** | `/Users/admin/Automation/HA-ZONT-Modbus` |
| **Repo (private)** | `https://git.mallexxx.duckdns.org/git_admin/HA-ZONT-Modbus` |
| **Скрипты** | `config-to-yml.py` (TXT → YAML) · `yml-to-config.py` (YAML → TXT) · `test_roundtrip.py` (проверка целостности) |
| **Конфиги** | `zont_config/` — свежие · `zont_config/archive/` — историчные |
| **Типы объектов** | [[family/tech/zont-config-object-types]] |
| **ZONT в общем контуре** | [[family/how-to/home-automation]] §6 |
| **Снять живой конфиг с прибора** | 🔴 `curl -s http://192.168.0.50/config.txt`**без авторизации**, формат ровно как у парсера. См. [[family/tech/zont-api]] §6bis |
---
## 1. Формат конфига ZONT
Текстовый файл, одна запись = одна строка, разделитель строк **CRLF**, кодировка **windows-1251**:
```
#Z<id>=<тип>,<поле>,<поле>,…
#S<id>=<значение>
```
| Префикс | Что это | Пример |
|---|---|---|
| `#Z<id>` | объект конфига: реле, датчик, сценарий, Modbus-устройство | `#Z12=14,'Спальня левый',…` — id 12, тип 14 (реле) |
| `#S<id>` | системная настройка | `#S7=H2000_PRO 723 678` — модель и версии ПО |
- Тип объекта — **первое поле** после `=`.
- Строки — в `'одинарных кавычках'`, списки — `[...]`, числа — как есть.
- `#Z<id>=*` — служебный маркер (пустой/унаследованный объект).
- **Порядок строк значим** и сохраняется при конвертации.
---
## 2. Как работает
### `config-to-yml.py` — TXT → YAML
1. Читает файл, определяет кодировку: UTF-8, при неудаче — windows-1251.
2. Парсит строки регекспом `^#([ZS])(\d+)=(.*)$` (хвостовые пробелы в payload сохраняются).
3. `split_payload()` — режет payload по запятым **с учётом кавычек и вложенных `[…]`**.
4. `parse_atom()` — пусто/`''``None`, `'строка'` → строка, `[…]` → список, иначе int → float → строка.
5. Раскладывает объекты по секциям YAML. Вложенное прячет под родителя: Modbus-регистры (тип 52) → внутрь своего устройства (тип 51).
6. `system_settings` (`#S…`) выводит **в начало файла**, чтобы было видно при правке.
### `yml-to-config.py` — YAML → TXT
1. Загружает **YAML (UTF-8)** и собирает строки `#Z<id>=…` обратно, в порядке типов.
2. Форматирование: числа без кавычек, строки в `'…'`, bool → `0/1`, пустая строка → `''`, списки → `[…]`.
3. Разворачивает вложенное: регистры типа 52 снова становятся отдельными строками после своего устройства.
4. `validate_config()` проверяет структуру **до вывода**. При ошибках — `❌ VALIDATION ERRORS`, **exit 4**, файл не отдаётся.
5. Вывод: **windows-1251**, переводы строк **CRLF**, финальный перевод строки как в оригинале.
### Коды возврата
| Скрипт | Код | Значение |
|---|---|---|
| `config-to-yml.py` | 1 | неверное число аргументов |
| | 2 | `ParseError` — неизвестный формат строки или неразбираемое значение |
| `yml-to-config.py` | 1 | файл не найден |
| | 2 | `ConversionError` — нет `raw` или неизвестный тип объекта |
| | 3 | прочее исключение |
| | 4 | ❌ ошибки валидации (файл не выдан) |
> Неизвестный тип объекта — **падение с ошибкой**, а не тихий пропуск. Сделано намеренно: молча потерять объект хуже, чем не отдать файл.
### Проверено против исходников (2026-09-17)
| Факт | Где в коде |
|---|---|
| `config-to-yml.py` exit-коды {1, 2} | `sys.exit(1)` — argc; `sys.exit(2)``ParseError` |
| `yml-to-config.py` exit-коды {1, 2, 3, 4} | argc / `FileNotFoundError` / `ConversionError` / общее / validation |
| `validate_config()` вызывается **до** вывода | стр. 913 `validation_errors = validate_config(data, lines)` |
| Вход YAML — UTF-8, выход — windows-1251 | стр. 907 `open(…, encoding='utf-8')`; стр. 921 `stdout.reconfigure(encoding='windows-1251', errors='replace')` |
| Дефолт типа 0 (16 полей) | `add_z_line(obj['id'], 0, register_ref, name, 0, 0, 1000, 2000, 7424, [], 20, [], [], 1, config_id, 0, 0)` |
| Дефолт типа 36 | `add_z_line(config_id, 36, [], [], [], 10, 0)` |
| README перечисляет 23 типа; код обрабатывает 25 | README пропускает `0` и `36` |
> 📌 `validate_config()` дополнительно проверяет кодируемость каждой строки в windows-1251 (символ вне CP1251 → ошибка валидации).
---
## 2.1 Формат сценариев (type 11 / 45 / 46 / 49) — разобран 2026-09-17
🔴 **Ключевое открытие:** поле 2 сценария — это **не список шагов типа 46**, а последовательность
ссылок, куда попадают и **шаги (46)**, и **задержки (45)**. Проверено на 65 сценариях боевого конфига.
```
#Z<step_46_id>=46,<priority>,[<action_ids…>],[]
#Z<act_id>=9,'<имя>',<relay_id>,'0|1' ← действие (тип 9)
#Z<delay_id>=45,<ms> ← задержка (тип 45)
#Z<cond_id>=49,<relay_id>,<operator>,<value> ← условие (тип 49)
#Z<scen_id>=11,'<имя>',[<step_46_id>…],0,0,<enabled>,0,0
```
| Поле | Значение |
|---|---|
| `11` поле 2 | список ссылок: шаги 46 **и** задержки 45 |
| `46` поле 2 | `[action_ids]` — действия 9 **и** задержки 45 внутри шага |
| `45` | пауза в **мс**; `45,0` = пауза 0 (заглушка) |
| `11` поле 6 | `1` = сценарий включён, `0` = выключен |
| `11` число полей | свежая прошивка — **8**, старая (`-2`) — **7** |
**Пример — `#Z11109` «Передернуть Автомат Котельной»** (единственный многошаговый из 65):
```
#Z11109=11,'Передернуть Автомат Котельной',[11827,11828],0,0,0,0,0 ← 11828 = задержка-«хвост»
#Z11827=46,0,11823,[11191,11030,11824,11029,11825,11192,11826],[] ← 7 действий, 2 из них задержки
#Z11823=49,11190,1,0 ← virt.Запретить == 0
#Z11824=45,20000 #Z11825=45,60000 #Z11826=45,0 #Z11828=45,0
```
Читается как: *вкл `virt.Запретить` → выкл Автомат → пауза 20 с → вкл Автомат → пауза 60 с →
выкл `virt.Запретить` → пауза 0*. Поле 6 = `0`**сценарий выключен**, защиты от повторного
передёргивания нет.
### Как это выглядит в YAML
> ⚠️ **ТЕКУЩАЯ форма (коммит `199f2b1`).** Переделывается по **§5c** — там одна вложенная форма
> `blocks[].if/then` для всех сценариев вместо плоского дубля и отдельных секций. Ниже — как есть
> сейчас, чтобы читать работающий код; целевая форма — в §5c.
Одношаговые сценарии (64 из 65) сохраняют плоский вид `when`/`then` — старые YAML не ломаются.
Многошаговые получают список `steps`, а хвостовые задержки — `extra_links`:
```yaml
scenarios:
- id: 11109
name: Передернуть Автомат Котельной
enabled: false
steps:
- id: 11827
when: {id: 11823, relay_id: 11190, operator: equals, value: 0}
then: {actions: [11191, 11030, 11824, 11029, 11825, 11192, 11826]}
extra_links: [11828]
delays:
- {id: 11824, ms: 20000}
- {id: 11825, ms: 60000}
- {id: 11826, ms: 0}
- {id: 11828, ms: 0}
```
**Как дописывать логику:** задержки правятся в секции `delays` (поле `ms`), порядок срабатывания
задаётся порядком id в `actions` шага. Чтобы включить сценарий — `enabled: true`.
---
## 2.2 Round-trip: все 4 конфига чистые
Проверено `test_roundtrip.py` (новый скрипт в репо) — TXT → YAML → TXT, сравнение по множеству
строк с нормализацией кодировки:
| Конфиг | Объектов | Результат |
|---|---|---|
| `zont_config/config_0FA7C33CC89F_…_2026-09-17_12-12-28.txt` | 598 | ✅ чисто |
| `zont_config/archive/H2000_PRO_config_actual-4.txt` | 558 | ✅ чисто |
| `zont_config/archive/H2000_PRO_config_actual-3.txt` | 558 | ✅ чисто |
| `zont_config/archive/H2000_PRO_config_actual-2.txt` | 594 | ✅ чисто |
```bash
cd /Users/admin/Automation/HA-ZONT-Modbus
python3 test_roundtrip.py # свежий конфиг из zont_config/
python3 test_roundtrip.py zont_config/archive/H2000_PRO_config_actual-4.txt
```
**Что исправлено 2026-09-17** (правки в обоих конвертерах):
| # | Проблема | Причина | Правка |
|---|---|---|---|
| 1 | Падение «поддерживается только 1 шаг» | `require(len(steps) == 1)` — не знал про задержки в поле 2 | цикл по ссылкам; не-46 → `extra_links` |
| 2 | Падение «поддерживается только 1 действие» | `require(len(actions) == 1)` | список действий целиком |
| 3 | Тип **45** отсутствовал | не было парсера/энкодера | секция `delays`, `KNOWN_TYPES += 45`, `TYPE_ORDER += delays` |
| 4 | Тип 5, поле 3 (`1``0`) терялось | энкодер хардкодил `1` | `_raw_field3` |
| 5 | `1.0``1` (порча float) | `parse_atom` превращал whole-float в int | whole-float остаётся float |
| 6 | Сценарии старой прошивки (7 полей) → 8 | энкодер всегда писал 8 | `_raw_field_count` |
| 7 | `divider=1.0``1` | дефолт подавлялся | `_raw_divider` |
---
## 3. Как пользоваться
**Требования:** `python3` + `PyYAML`. Проверка: `python3 -c 'import yaml; print(yaml.__version__)'`
```bash
cd /Users/admin/Automation/HA-ZONT-Modbus
# 1. Снять текущий конфиг с контроллера → в zont_config/
# имя файла: config_<SN>_<SN>_<YYYY-MM-DD_HH-MM-SS>.txt
# ✅ Напрямую с контроллера, без авторизации (см. §5g):
curl -s http://192.168.0.50/config.txt -o zont_config/config_$(date +%Y-%m-%d_%H-%M-%S).txt
# 2. TXT → YAML
python3 config-to-yml.py zont_config/config_XXXX.txt > /tmp/zont.yml
# 3. Править YAML: имена, адреса, интервалы опроса, пороги
# ⚠️ id объектов и их порядок значимы — не переставлять без нужды
# 4. YAML → TXT
python3 yml-to-config.py /tmp/zont.yml > /tmp/zont_new.txt
# 5. Сверить число объектов до/после
grep -c '^#Z' /tmp/zont_new.txt
# 6. Загрузить /tmp/zont_new.txt в контроллер (UI / облако ZONT)
```
### 🔴 Главное правило: `raw` не трогать
В YAML часть полей декодирована (адрес, интервал опроса, регистры), часть лежит как `raw` / `raw_params` / `raw_field_N` — это **страховка от потери данных**.
Если у объекта пустой `raw`, скрипт подставит **жёстко зашитые дефолты** (тип 0 — набор из 16 полей, тип 36 — `[],[],[],10,0`), и исходные настройки будут потеряны молча.
> ✅ **Правь декодированные поля. `raw`-поля не удаляй и не «чисти».** Если поле не декодировано — правка возможна только с пониманием исходного формата.
### Поддерживаемые типы
`1, 3, 4, 5, 6, 7, 9, 10, 11, 14, 16, 20, 24, 25, 27, 28, 42, 45, 46, 49, 51, 52, 53, 57`
➕ дополнительно обрабатываются **`0`** (дискретные датчики — индикаторы состояния реле) и **`36`** (их вложенные конфиги).
Полная таблица с полями — [[family/tech/zont-config-object-types]].
**Сценарные типы — полностью поддержаны с 2026-09-17 (см. §5b):**
| Тип | Роль | Формат | YAML-секция |
|---|---|---|---|
| `11` | сценарий | `[11, name, [step_ids], 0, 0, enabled, 0, 0]` | `scenarios` |
| `45` | задержка, мс | `[45, ms]` | `delays` |
| `46` | шаг | `[46, 0, cond_id, [action_ids], []]` | `scenario_steps` |
| `49` | условие | `[49, relay_id, operator, value]` | `scenario_conditions` |
---
## 4. Проверка целостности (round-trip)
**Есть готовый скрипт — `test_roundtrip.py`** (в репо с 2026-09-17, коммит `199f2b1`).
Делает `TXT → YAML → TXT`, сравнивает **множеством строк** с нормализацией кодировки
(источник UTF-8 или windows-1251, выход всегда windows-1251). Exit: `0` чисто / `1` расхождения / `2` ошибка запуска.
```bash
cd /Users/admin/Automation/HA-ZONT-Modbus
python3 test_roundtrip.py # свежий конфиг из zont_config/
python3 test_roundtrip.py zont_config/archive/H2000_PRO_config_actual-4.txt
```
Вывод при успехе: `✅ ROUND-TRIP ЧИСТЫЙ — расхождений нет` + счётчики `#Z` до/после.
### Ручная проверка (если скрипт недоступен)
```bash
cd /tmp && rm -rf zont-rt && mkdir zont-rt && cd zont-rt
SRC=/Users/admin/Automation/HA-ZONT-Modbus/zont_config/<файл>.txt
python3 /Users/admin/Automation/HA-ZONT-Modbus/config-to-yml.py "$SRC" > a.yml # проверить exit!
python3 /Users/admin/Automation/HA-ZONT-Modbus/yml-to-config.py a.yml > b.txt
iconv -f cp1251 -t utf-8 "$SRC" | tr -d '\r' | sort > A.txt
iconv -f cp1251 -t utf-8 b.txt | tr -d '\r' | sort > B.txt
diff A.txt B.txt # пусто = round-trip чистый
```
> 🔴 **Сравнивать множеством строк (`sort` + `diff`), НЕ построчно.** `yml-to-config.py` пишет объекты
> в порядке `TYPE_ORDER`, а в исходном файле порядок другой → наивный `diff` даёт ~56 **ложных**
> расхождений при полностью корректной конвертации.
> 📌 **Нюанс кодировки:** источник бывает в UTF-8, а `yml-to-config.py` всегда пишет windows-1251 → наивный `diff` покажет различия на кириллице. Нормализовать кодировку с обеих сторон (`iconv`), как в командах выше.
> ⚠️ **`>` в шелле затирает целевой файл ещё до старта питона** — поэтому рядом с непустым `.txt`
> появляется пустой `.yml`. Это признак **падения** конвертера на конкретном объекте: смотреть stderr.
> 🔴 **Не проверять результат через пайп** (`iconv … | grep -c`). Пустой промежуточный файл в пайпе
> даёт ложный «успех». Смотреть `wc -c` целевого файла напрямую. (Реальный случай 2026-09-17 — см. питфолл 16.)
---
## 5. Питфоллы
| # | Питфолл | Как обойти |
|---|---|---|
| 1 | 🔴 Вывод `yml-to-config.py`**windows-1251 + CRLF**, не UTF-8 | Редирект **в файл** и передавать байтами. Не копипастить из терминала, не пересохранять в редакторе |
| 2 | 🔴 Пустой `raw` у типов 0/36 → **молчаливая подстановка дефолтов** | Не трогать `raw*`-поля |
| 3 | 🔴 Правка `#S…` в YAML бессмысленна — они хранятся как `raw_payload` | Системные настройки менять только через UI контроллера |
| 4 | **YAML на входе `yml-to-config.py` — строго UTF-8**; выход — windows-1251. Плюс автоопределение кодировки при чтении `.txt` (UTF-8 → windows-1251) | Править YAML в UTF-8. Исходный `.txt` не пересохранять в редакторе |
| 5 | Неизвестный тип объекта → exit 2, файл не создаётся | Смотреть stderr — там причина |
| 6 | `validate_config()` → exit 4 | Печатает `❌ VALIDATION ERRORS` + список причин; файл намеренно **не выдан** (вывод пустой) |
| 7 | Регистр без своего устройства / аналоговый выход с битой ссылкой | WARNING в stderr, конвертация продолжается — проверить ссылки вручную |
| 8 | Загрузка конфига в контроллер — **руками**, скрипты только конвертируют | Конвертер не имеет доступа к ZONT. ⚠️ **Снятие конфига — уже не ручное:** `http://192.168.0.50/config.txt` отдаёт весь конфиг без авторизации (§5g). Заливка — утилита по USB / облако / WS **только для `#S`** (§5g-2) |
| 9 | `INFRASTRUCTURE.md`, `docker-compose.yml`, `docker run.txt` в проекте — **исторический TrueNAS-стек** | Актуальный контур — [[family/how-to/home-automation]]. Не искать `modbus-bridge`/`mbusd` на NAS |
| 10 | Дополнительных зависимостей нет | Только `pyyaml``jsonschema`/`ruamel` не нужны |
| 11 | ✅ **ИСПРАВЛЕНО 2026-09-17** — Сценарий с >1 шагом падал (exit 2) | `config-to-yml.py` теперь цикл по всем шагам; `yml-to-config.py` пишет все `step_ids` |
| 12 | ✅ **ИСПРАВЛЕНО 2026-09-17** — Шаг с >1 действием падал (exit 2) | Оба скрипта работают со всем списком действий |
| 13 | ✅ **ИСПРАВЛЕНО 2026-09-17** — Тип 45 (задержка, мс) не был поддержан | Парсер + эмиттер, секция YAML `delays`. См. §5b |
| 14 | ⚠️ `>` в шелле затирает `.yml` до старта питона | Проверять exit-код до переноса файла в репо |
| 15 | ⚠️ Один шаг может принадлежать нескольким сценариям | `yml-to-config.py` использует хелпер `_register()` — обновляет запись по id, а не добавляет дубль |
| 16 | 🔴 **`iconv … \| grep` в пайпе маскирует пустой файл** | Промежуточный `b.txt` был 0 байт, а `grep -c` в пайпе отработал «успешно» → ложный вывод «598 → 598, чисто». **Проверять `wc -c` целевого файла напрямую**, а не через пайп |
| 17 | 🔴 **Правка `parse_atom` ради `1.0` ломает `_raw_*`-путь** | Первая попытка нормализовала whole-float → int; `_raw_divider` стал мёртвым кодом (`isinstance(v[6], int)` не срабатывал). Итог: **whole-float остаётся float** — иначе энкодер печатает `1` вместо `1.0` |
| 18 | 🔴 **Порядок объектов в файле ≠ порядок типов в `TYPE_ORDER`** | Сравнение «как есть» даёт ~56 ложных расхождений. Сравнивать **множеством строк** (`sort` + `diff`), порядок не значим |
| 19 | ⚠️ **Негативный тест-детектор проверять реальной порчей** | Подмена `id: 11109``111099` ничего не ломает (id косметический). Ловить нужно удаление объекта: снести `delays[11824]` → детектор обязан сработать |
| 20 | 🔴 **`read_file` возвращает контент с номерами строк — не patch-ить им vault** | Правки Obsidian-заметок делать **через obsidian-MCP** (`mcp_obsidian_patch_note`), не файловыми скриптами. Alex 2026-09-17: «какого хуя ты скриптами лезешь в обсидиан» |
### Ограничения конвертера (найдено 2026-09-17) — ВСЕ ЗАКРЫТЫ
Прогон боевого конфига `config_0FA7C33CC89F_…_2026-09-17_12-12-28.txt` (598 `#Z`, 25 `#S`) вскрыл **4 дырки** в сценарной логике — все четыре независимы, падал на первой же. **Все четыре исправлены, см. §5b.**
| # | Чего не было | Что терялось | Масштаб в конфиге | Статус |
|---|---|---|---|---|
| 1 | 2-й и последующие шаги сценария | шаг `11828` сценария `11109` | 1 сценарий из 65 | ✅ закрыто |
| 2 | Тип **45** — задержка в мс, `[45, ms]` | 4 объекта: `11824`=20000, `11825`=60000, `11826`=0, `11828`=0 | 4 объекта | ✅ закрыто |
| 3 | Несколько действий в одном шаге | шаг `11827` содержит **7** действий | 1 шаг из 65 | ✅ закрыто |
| 4 | — (информационно) сценарий выключен | `#Z11109``enabled=0` | 1 сценарий | ℹ️ факт, не баг |
> ✅ Остальные 64 сценария — каноническая одношаговая форма `11 → [46] → 49`, конвертер их разбирал и разбирает корректно. Все 65 условий (49) имеют ровно 4 поля `[49, relay, op, value]`, `op=1` = equals у всех. Все 64 «простых» шага — ровно 5 полей `[46, prio, cond, [1 действие], []]`.
**Тип 45 vs `delay_ms` у типа 5 — это разные вещи:**
- `delay_ms` живёт **внутри** объекта-действия типа 5 (поле 4) — уже поддержан обоими скриптами.
- Тип **45****самостоятельный объект-задержка** в списке действий шага. Восстановленный формат: `[45, <миллисекунды>]`, 2 поля. Проверено на 4 объектах, больше в конфиге не встречается.
---
## 5b. Доработка сценариев — СДЕЛАНО 2026-09-17
Задача Alex: «перегнать в yml, дописав парсер и энкодер». Правка кода конвертеров, боевой конфиг не тронут.
### Изменения в `config-to-yml.py` (парсер)
| Что | Детали |
|---|---|
| Многошаговые сценарии | Убрано `require(len(steps) == 1)`. Цикл `for step_id in steps``parsed_steps[]`, каждый со своим `when`/`then` |
| Много действий в шаге | Убрано `require(len(actions) == 1)`. Все id проверяются на существование в `Z_dict` |
| Новый тип **45** | Отдельная секция `# --- scenario delays (type 45)`. Валидация: ровно 2 поля, `ms` — неотрицательный int → `out['delays']` = `[{'id':…, 'ms':…}]` |
| `KNOWN_TYPES` | Добавлен `45` (был `{0,1,…,42,46,49,…}`) |
| Выходной dict | Добавлен ключ `delays: []` |
| **Обратная совместимость** | Если у сценария 1 шаг — дополнительно пишутся плоские `when`/`then` (как раньше). Старые YAML не ломаются |
### Изменения в `yml-to-config.py` (энкодер)
| Что | Детали |
|---|---|
| Все шаги | Собирает `step_ids` из `scenario['steps']` (fallback — плоский `then.id`) и пишет в строку сценария |
| Все действия | `[46, 0, cond_id, actions, []]` — весь список, без `[action_id]` |
| Новый тип **45** | Секция эмиттера: `raw` passthrough **или** `ms``[45, ms]`. Валидация: `ms` — неотрицательный int. **Вставлена ДО шагов (46)** — порядок строк значим |
| `TYPE_ORDER` | Добавлено `('delays', 45)` между `gui_tabs` и `scenario_steps` |
| Хелпер `_register()` | Локальная функция рядом с `add_z_line`. Регистрирует шаг/условие по id, **обновляя** существующую запись вместо добавления дубля — один шаг может принадлежать нескольким сценариям |
| Без `when` в шаге | Если у шага нет `when`, но есть запись в `scenario_steps` — переиспользует известный `cond_id` из неё. Если нет — `ConversionError` |
### Что проверено
- `ast.parse()` на обоих файлах — синтаксис OK
- `config-to-yml.py` на боевом конфиге: **больше не падает** на сценарии `11109` (ранее `Ошибка: Сценарий 11109: поддерживается только 1 шаг`, exit 2)
-**Round-trip целиком — ПРОЙДЕН на всех 4 конфигах** (см. §2.2). Сверка по множеству строк с нормализацией кодировки.
### Коммит
**Закоммичено 2026-09-17 — `199f2b1`** «Support multi-step scenarios, delays (type 45) and multi-action steps».
3 файла, +327/−75. В коммит вошли: `config-to-yml.py`, `yml-to-config.py`, `test_roundtrip.py`.
Не запушено (`origin/main..HEAD` = 3 коммита впереди) — пуш ждёт команды Alex.
---
## 5c. 🔄 Переработка структуры YAML сценариев — ПРИОСТАНОВЛЕНО, ждёт решения (2026-09-17)
> ⏸ **СТАТУС: РАБОТА НЕ НАЧАТА. Alex дал «стоп» и поставил под сомнение сам проект.**
> Причина — исследование облачного API ZONT ([[family/tech/zont-api]]): Alex предположил, что
> у ZONT есть свой API/формат, покрывающий датчики и автоматику, и спросил — доделывать конвертер
> или переписать всё с 0.
>
> **Результат исследования: API конфиг не поддерживает** (сценарии/реле/11/14/46/49 — в API нет,
> `scenario` в доке 0 раз). Переписывать с 0 **не на что**. Конвертер остаётся единственным путём
> к сценариям. **Новый открытый вопрос Alex'у:** нужен ли онлайн-мониторинг ZONT в HA
> (отдельный проект на API, дополняет конвертер).
>
> ⚠️ **Код конвертера НЕ тронут.** Коммит `199f2b1` — последнее рабочее состояние.
> Не начинать §5c без явного «делай» от Alex.
**Задача Alex (исходная):** «переписать блок парсинга/сборки сценариев чтобы он составлял синтаксис как у Home Assistant automations вместо текущей разбросанной структуры. с опциональными айдишниками у операторов».
### 🔴 Правка постановки Alex'ом (ключевое — не повторять ошибку)
Первая версия плана натягивала **HA-синтаксис** (`triggers` / `platform: state` / `to:`) на ZONT-логику.
Alex отклонил:
> «нет структура должна быть как в zont. **триггеров нет.** у нас сценарий Передернуть Автомат Котельной буквально содержит вложенные инструкции: **если ... то... итд.** не надо натягивать сову структуры на глобус HA syntax.»
**Правило:** в ZONT **нет триггеров** — роль триггера играет само изменение реле, а сценарий читается
как вложенные инструкции «если … то …». Структура YAML должна повторять ZONT, а не HA.
### Утверждённая форма (предложена, ждёт финального ОК по двум вопросам)
```yaml
scenarios:
- id: 11109
name: 'Передернуть Автомат Котельной'
enabled: false
blocks:
- id: 11827 # шаг (46)
if:
id: 11823 # условие (49), id опционален
relay: 11190
operator: equals # equals | not_equals
value: 0
then:
- {id: 11191, action: relay_on, relay: 11190}
- {id: 11030, action: relay_off, relay: 11028}
- {id: 11824, action: wait, ms: 20000}
- {id: 11029, action: relay_on, relay: 11028}
- {id: 11825, action: wait, ms: 60000}
- {id: 11192, action: relay_off, relay: 11190}
- {id: 11826, action: wait, ms: 0}
tail: # хвостовые задержки из поля 2
- {id: 11828, action: wait, ms: 0}
```
**Что меняется по сравнению с текущим YAML:**
| Сейчас (разбросано) | Станет |
|---|---|
| `scenarios[].when` / `.then` — плоский дубль для 1-шаговых | убрано: **одна форма** `blocks[].if/.then` для всех 65 |
| `scenarios[].steps[]` + отдельные `delays` / `scenario_conditions` / `scenario_steps` | одна вложенная структура `blocks` |
| `wait` лежит отдельной секцией верхнего уровня | `wait`**внутри** `then`, по месту срабатывания |
| `extra_links: [11828]` — голые id | `tail:` с блоками `wait` |
**Имена ключей и нулевые паузы — решено, не переспрашивать:**
- Ключи: **`blocks` / `if` / `then`** — ближе к формулировке Alex «вложенные инструкции если … то …».
- Нулевые паузы (`ms: 0`) — **остаются как `wait`**. Честно отражают конфиг, нужны для round-trip.
- `id` у операторов — **опциональные**: пишутся, когда есть в конфиге; отсутствуют, когда нет.
**План работ:**
1. `config-to-yml.py` — блок type 11: генерировать `blocks[].if/then`; `wait` (45) встраивать в `then`; `tail` для хвостовых
2. `yml-to-config.py` — обратный разбор `blocks`; **поддержать старые формы** (`when`/`then`, `steps`) для совместимости
3. `test_roundtrip.py` — все 4 конфига должны остаться чистыми
4. Доки — обновить §2.1 этой заметки
**Обязано сохраниться байт-в-байт (риск round-trip):**
- порядок объектов в файле (45 идёт **после** 11, но **до** 46)
- `extra_links` — хвостовые задержки в поле 2 сценария
- число полей (7 vs 8) и `_raw_*`-поля (`_raw_field_count`, `_raw_field3`, `_raw_divider`) — **не удалять**
**Бэкап:** коммит `199f2b1` уже есть — откат возможен в любой момент, отдельных копий не делать.
---
## 5d. Что делает сценарий 11109 — и что логика УЖЕ есть
Разбор по факту (не гипотеза) — подробно в [[family/tech/zont-scenario-logic-11109]]:
**Порядок действий шага `11827`:**
```
вкл virt.Запретить(11191) → выкл Автомат(11030) → пауза 20 с(11824)
→ вкл Автомат(11029) → пауза 60 с(11825) → выкл virt.Запретить(11192) → пауза 0(11826)
```
Условие `11823`: `virt.Запретить(11190) == 0`. Смысл — **защита от повторного передёргивания**:
реле ставится в 1 первым действием, условие требует 0 → пока реле в 1, сценарий не перезапустится.
Минимальный интервал между передёргиваниями = 80 с (20+60).
> 🔴 **Защита в конфиге УЖЕ есть.** Поле v[5] сценария = `0` → сценарий **выключен в контроллере**,
> поэтому защита и функция передёргивания не работают. Вопрос «дописать логику» на деле может быть
> «включить существующую» (`enabled: true`) — это НЕ подтверждено Alex'ом, не додумывать.
---
## 5f. Ответы на вопросы постановки (этап 2 снят)
Вопрос Alex'у «что именно не хватает на сценариях» (варианты а/б/в) **закрыт его же реакцией**: задача —
«дописать парсер и энкодер», а не править логику. Правка YAML сценария `11109` (этап 2 прежнего плана)
с повестки снята — работа ограничена конвертерами.
> 🔴 **Урок коммуникации 2026-09-17.** Alex дважды резко реагировал на развёрнутые планы-опросники
> («нихуя не понял тебе че надо блядь? что блядь сломано?», «ТЫ ХУЛИ ВСТАЛ БЛЯДЬ!?»). Что сработало:
> **сжатый факт «что сломано» + сразу делать**. Не задавать уточняющих вопросов там, где симптом уже
> назван; не строить гипотезы вместо чтения конфига; не планировать сверх постановки.
> ⚙️ **Бэкап кода — только git, не `/tmp`.** Alex 2026-09-17: «какой нахуй бэкап скриптов — там в гите все». Изменения скриптов откатываются через git, отдельные копии в `/tmp/` не делать.
---
## 5g. 🔴 Локальный эндпоинт контроллера
Контроллер отдаёт **весь конфиг** по HTTP **без авторизации**:
```bash
curl -s http://192.168.0.50/config.txt -o zont_config/config_$(date +%Y-%m-%d_%H-%M-%S).txt
```
Формат — тот же `#Z…` / `#S…`, что парсит `config-to-yml.py`. Снятие конфига больше не ручная
операция через облако.
**Полный разбор локального интерфейса** — WebSocket-протокол, чтение/запись `#S`-настроек,
кнопка сохранения `#S15=1`, 25 настроек против 9 в UI, утечка секретов, инструменты разведки:
➡️ **[[family/tech/zont-api]] §6bis**
---
## 5g-2. 🔼 ЗАЛИВКА конфига обратно — чем и как (добыто 2026-09-17)
Снятие конфига автоматизировано (§5g), но **заливка остаётся ручной** — идёт **не** через
`config.txt` (этот путь только на чтение). Доступные каналы:
| Канал | Что умеет | Ограничение |
|---|---|---|
| **Настроечная утилита по USB** | заливка конфига + **прошивки** | Windows-only, нужен USB-кабель и драйвер |
| Облако ZONT (`my.zont.online`) | правка сценариев/реле через веб-UI | руками, по одному объекту |
| Локальный WebSocket `ws://192.168.0.50/ws` | запись `#S`-настроек (`{"scmd":"#S<n>=<val>"}` + `#S15=1`) | **`#Z`-объекты не пишутся** — только `#S` |
> 🔴 Локальный WS даёт запись **только системных `#S`-настроек** (Wi-Fi, MQTT, номер). Сценарии,
> реле, шаги (`#Z11/14/46/49`) через него **не заливаются** — для них по-прежнему утилита или облако.
### Утилита `H1000 Programmator` 2.8.5 — добыта
```bash
# официальный источник, скачивается без авторизации
curl -sL -o h1000_utility_beta.bin https://lk.zont-online.ru/download/simple/h1000_utility_beta
```
| Параметр | Значение |
|---|---|
| Версия | **2.8.5** (`prgm.2.8.5.exe`, 2.9 MB, Delphi/Borland) |
| Интерфейс | USB serial-over-USB (`usbser.sys` + `Hxxxx.inf`) |
| Архив на Mac | `~/rasputin-tmp/zont-util/h1000_utility_beta.bin` (4.5 MB) |
| Распаковано | `~/rasputin-tmp/zont-util/util_beta/H1000 Programmator/` |
| Хелпы внутри | `выходы.rtf`, `Отопление.rtf`, `пользователи.rtf`, `смс управление.rtf`, `DTMF управление.rtf` |
> 🔴 **Питфолл распаковки.** macOS `unzip` падает на кириллических именах в архиве
> (`write error (disk full?)` — на самом деле **не** disk full). Имена в CP866.
> Решение — Python `zipfile` с перекодировкой `cp437 → cp866`: `~/rasputin-tmp/zont-util/extract.py`.
> ⚠️ Пятно на репутации формата: файлы `Configs/*.set` внутри архива — **НЕ конфиги устройства**.
> Это UTF-8 JSON-словари подписей интерфейса (`{"Value":0,"Edit":"Пользователь 1"}`).
> Не пытаться парсить их нашим конвертером.
### Прошивка — формат `.enc` (зашифрован)
```bash
curl -sL -o H2000_fw.zip https://lk.zont-online.ru/download/firmwares/H2000_515__330_290.zip
```
| Файл | Размер | Энтропия | Вывод |
|---|---|---|---|
| `STM_MEGA_400_.enc` | 145 177 B | **7.9986** / 8.0 | зашифрован, не сжат |
| `main_c.evc` | 49 610 B | 5.6487 | частично структурный (`Mega-CX`) |
Прошивка тоже отдаётся **без авторизации**. Утилита грузит именно `*.enc`
(строки `firmware_.enc`, `.enc|*.enc` найдены в exe). Расшифровка — отдельное исследование.
#### 🎯 Схема URL прошивки — НАЙДЕНА 2026-09-17
```text
https://lk.zont-online.ru/download/firmwares/H2000_PRO_<HW>__<FW>_<PROFILE>.zip
H2000_PRO_723__678_1.zip ← наш контроллер, 1.2 MB
```
**Правило:** префикс серии (`H2000_PRO_`) + **двойное** подчёркивание перед версией ПО.
Одиночные варианты (`H2000_723__678_1.zip`, `H2000_PRO_723__678.zip`) → **404**.
Проверка соответствия (конфиг `#S7` ↔ API морды ↔ имя файла):
| Источник | Значение |
|---|---|
| `#S7` прибора | `H2000_PRO 723 678` |
| `get_firmware_releases` (DevTools) | `"version":"678:1"`, `"firmware_version":678` |
| Имя архива | `H2000_PRO_723__678_1.zip``h2000_pro_v2_.enc` |
**`723` = плата (HW), `678` = прошивка, `1` = profile_version.** `678` — последняя
**стабильная**; 602/585/564 — бета.
> 📁 Скачанные прошивки, утилита, скрипты разведки и живой конфиг лежат в проекте:
> `/Users/admin/Automation/HA-ZONT-Modbus/zont_local_ui_recon/` (см. её `README.md`).
➡️ Полный разбор утилиты, прошивок и API морды — **[[family/tech/zont-api]] §10**
---
## 5h. 🌐 Облачный API ZONT — конфига не даёт
Исследовано 2026-09-17. Доки скачаны в проект: `zont_api_docs/`.
**Главный вывод:** облачный API (`my.zont.online/api/*`, 11 методов) — это **состояния и история**,
не конфигурация. Слово `scenario` в доке — **0 раз**. Методов для типов `11/14/46/49` нет.
`z3k_config` упоминается только как источник ID.
**Следствие:** конвертер `.txt ⇄ .yml`**единственный** путь правки сценариев и реле.
**Что API всё же умеет (для H-2000 PRO):** чтение состояний (`devices?load_io=true`),
история (`load_data`), режимы отопления (`update_device`), сирена/охрана (`set_io_port`).
➡️ **Полный разбор API — [[family/tech/zont-api]]**
---
## 6. Состояние проекта (проверено 2026-09-17, вечер)
**Конвертеры закоммичены — `199f2b1`.** Рабочее дерево чистое (после этого коммита).
| Файл | Статус |
|---|---|
| `config-to-yml.py`, `yml-to-config.py` | ✅ **изменены и закоммичены** (`199f2b1`, §5b) |
| `test_roundtrip.py` | ✅ **новый файл**, в коммите `199f2b1` |
| `zont_config/config_0FA7C33CC89F_…_2026-09-17_12-12-28.txt` | 32 689 байт — свежий конфиг с контроллера, **не в git** (`untracked`) |
| одноимённый `.yml` | ✅ **пересобран, 80 472 байта / 4320 строк / 24 секции**, YAML валиден. **не в git** |
| `zont_config/archive/` | **не в git** (`untracked`) — лежит в `.gitignore`-нейтральном состоянии |
| `zont_config/H2000_PRO_config_actual-{2,3,4}.txt` / `.yml` | ✅ **перемещены в `archive/` и закоммичены** (`7ae0e32`) |
| `zont_api_docs/` | ✅ **новая папка** — локальная копия доки облачного API ZONT (`zont_api_docs.html` 250 KB, `zont_api_docs.txt` 99 KB, `convert.py`). **не в git**. Исследование — [[family/tech/zont-api]] |
| `~/rasputin-tmp/zont-util/` | ✅ **новая папка на Mac** (вне репо) — настроечная утилита `H1000 Programmator` 2.8.5, драйвер USB, прошивка `.enc`, скрипт распаковки `extract.py`. Разбор — §5g-2, [[family/tech/zont-api]] §10 |
| `~/rasputin-tmp/zont-{auth-probe,recon,recon2,ws-probe}.js` | ✅ скрипты разведки локального WS-интерфейса. **не в git** |
Не запушено: `origin/main..HEAD` = 3 коммита (`199f2b1`, `7ae0e32`, `12ba22b`).
> ✅ **Пустой `.yml` (0 байт) больше не актуален** — причина была в падении на сценарии `11109`,
> которое исправлено (§5b). Файл пересобран, 598 объектов.
**Что НЕ в git и почему:** `.txt` свежего конфига и его `.yml` — рабочие артефакты конвертации,
Alex их не добавлял. Не коммитить без команды.
**Содержимое проекта, не относящееся к конвертерам:**
- `INFRASTRUCTURE.md` (342 стр.), `docker-compose.yml`, `docker run.txt`**исторический TrueNAS-стек** (docker-контейнеры `homeassistant`, `mbusd`, `modbus-bridge`, `mosquitto`, `zigbee2mqtt`, `nodered`, `caddy`, `immich`, `transmission`, `webdav`, `inpxer`, `cups-splix`, `portainer`, `watchtower`, `rclone`). Стек **декомиссирован**, автоматизация живёт на t610 — актуальное: [[family/how-to/home-automation]].
- `modbus_ha_bridge.py`, `modbus_mqtt_bridge.py` — исходники мостов (исторические, для TrueNAS).
- `nodered-flows-backup.json`, `nodered-flows-updated.json` — дампы потоков Node-RED (Node-RED остановлен).
- `homeassistant/`, `floorplan/` — снапшоты конфига HA и планировки (исторические, там же workflow «fetch from NAS → edit → deploy»).
- `README_converters.md` — исходное описание конвертеров (типы: 23, без `0` и `36`).
- `.gitignore` — исключает `*.cur`, логи, `__pycache__`, `.venv`, `.DS_Store`, **`project_home.pdf`** (крупный бинарь).
---
## 7. Связанные заметки
- [[family/tech/zont-config-object-types]] — таблица типов объектов (0, 1…57, 36) и `#S`-настройки
- [[family/tech/zont-scenario-logic-11109]] — разобранная логика сценария «Передёрнуть Автомат Котельной»
- [[family/tech/zont-api]] — **облачный API ZONT: конфиг не поддерживает**; альтернатива конвертеру отсутствует
- [[family/how-to/home-automation]] — контур автоматизации, ZONT, Modbus slave ID и регистры (§6)
- [[family/how-to/gitea-config]] — Gitea: креды, создание репо, питфоллы
- [[family/how-to/ha-automations]] — автоматизации HA