Меня зовут ZolAnd, я — независимый инженер‑разработчик embedded‑систем в основном для дома и сельской усадьбы. В какой‑то момент мне захотелось собрать под одной крышей разношёрстный парк ZigBee‑устройств от разных производителей, работать с ним локально, без обязательной привязки к облаку, и при этом иметь возможность не только интегрировать чужие устройства, но и проектировать свои. Так появился проект ZBFun. Эта статья — про то, с какими вызовами я столкнулся, какие решения уже существуют и почему они мне не подошли, и про архитектуру, к которой я в итоге пришёл.

С чего всё началось

Задача звучала просто: хочу python‑библиотеку для ZigBee на ESP32-C6. Готовой не нашлось — есть отличный esp‑zigbee‑lib от Espressif на C, но ничего сопоставимого на Python или MicroPython для этой платформы не было. Значит, писать самому. И тут выяснилась вторая, более фундаментальная проблема. Чтобы написать библиотеку, нужно понимать, что такое ZigBee Cluster Library (ZCL) — стандарт, который описывает, из чего вообще состоит ZigBee‑устройство: кластеры, атрибуты, команды, типы данных, зависимости между ними. Официальный документ Alliance — 1213 страниц PDF, написанный для инженеров, имплементирующих стек с нуля, без какой‑либо машиночитаемой версии. Ни JSON, ни XML — только текст, рассчитанный на то, что человек прочитает и перепечатает нужное руками. Дальше — хуже. Когда я стал сверять этот текст с реальными SDK и реальными устройствами, обнаружились расхождения. Причём не косметические, а такие, из‑за которых устройство просто не будет работать правильно.

Пример, который я привожу во всех разговорах об этом: атрибут MeasuredValue кластера Temperature Measurement (0×0402). • ESP‑IDF SDK описывает его как uint8. Это не ошибка Espressif — SDK честно использует uint8 как заглушку‑placeholder почти для всех атрибутов, а реальный тип назначается программистом руками при создании кластера. • zigpy, проверенный на тысячах реальных устройств в проде, указывает int16s. • Официальный текст ZCL тоже говорит int16s. Если поверить SDK буквально, датчик будет отправлять температуру как беззнаковый байт с диапазоном 0–255, и любое отрицательное значение превратится в мусор. Это ровно та ошибка, которую совершают многие DIY‑проекты, копирующие примеры из SDK без проверки.

Стало ясно, что задача не сводится к «написать библиотеку». Сначала нужно разобраться, что такое ZigBee вообще, на уровне достаточном, чтобы не доверять слепо ни одному отдельному источнику. И параллельно я хотел заложить в проект ещё одну вещь: возможность самому решать, как распределять логику между конечным устройством и хабом — не по правилам вендора, а по своим. Разработчику видней, что должно продолжать работать при обрыве связи, а что разумнее вынести на более мощный узел.

Что уже есть на рынке

Экосистема разработки IoT‑устройств сейчас поделена на несколько лагерей, и у каждого своя логика, вполне разумная внутри себя, но плохо подходящая под мою задачу.

ESPHome — ближайший по духу конкурент. YAML‑файл описывает и железо, и логику, Python‑слой валидирует конфигурацию и превращает её в C++ (main.cpp), который компилируется под конкретную плату. В версии 2026.5.0 появился визуальный Device Builder (пока в бете), но по архитектуре это надстройка над тем же YAML: GUI генерирует тот же текстовый DSL, а не создаёт независимый от синтаксиса промежуточный контракт.

Zigbee2MQTT и ZHA идут в противоположную сторону: они вообще не занимаются прошивками устройств. Вся погонка под нестандартные девайсы — это JS‑конвертеры (Z2M) или Python‑quirks (ZHA), написанные вручную мейнтейнерами под каждую конкретную вендорскую железку. Подход отлично работает для интеграции готовых устройств, но никак не помогает, если вы хотите спроектировать собственное с нуля.

Я для себя сформулировал требования к архитектуре так:

  • опираться на стандарт ZCL, но не доверять ни одному отдельному его источнику вслепую;

  • разделить визуальное проектирование, промежуточный обменный формат и генерацию кода под конкретную платформу на три независимые друг от друга сущности;

  • не поддерживать вручную соответствие «атрибут кластера ↔ вызов API SDK» — это должно вычисляться автоматически;

  • честно разграничить, какая логика обязана жить на самом устройстве, чтобы оно не превращалось в кирпич без сервера, а какая — на локальном хабе, и оставить это разграничение на усмотрение того, кто проектирует конкретное решение.

Пайплайн на контрактах

В основе архитектуры ZBFun лежит идея контракта: каждый шаг пайплайна получает на входе строго описанный формат и отдаёт на выходе строго описанный формат, независимо от того, чем реализован сам шаг. Это позволяет менять реализацию любого звена, не трогая соседние — а на некоторых этапах даже подменять инструмент человеком или нейросетью, если контракт соблюдён. Поток действий по шагам выглядит так:

  • Гармонизация стандарта — до Designer, работа с базами знаний.

  • Designer — визуальная сборка модели устройства, на выходе спецификация по контракту.

  • Постпроцессоры — на входе спецификация, на выходе прошивка под конкретную платформу.

  • Прикладная логика — код, который реально исполняется на устройстве или на хабе.

  • Composer — локальное облако, оркестрация сценариев между устройствами.

Дальше — подробнее про каждый этап, начиная с того, что было сделано ещё до появления самого Designer.

Шаг 0: гармонизация стандарта

Прежде чем рисовать какой‑либо визуальный конструктор, нужно было решить проблему разночтений, о которой я писал выше. Я построил систему провенанса — у каждой записи в базе знаний есть источник и вес доверия к нему:

  • source_code trust что это

  • ESP_SDK 3/5 официальный SDK Espressif, но с заглушками

  • ZIGPY 4/5 Python‑реализация, проверена на реальном железе

  • ZHA / Z2M 4/5 маппинги из живых, годами работающих хабов

  • ZCL_SPEC 5/5 официальный текст стандарта (когда есть, чем сверить) SEED

  • 2/5 ручные дополнения там, где источники молчат

  • USER 1/5 пользовательские DIY‑данные (из Designer)

Правило разрешения конфликта простое на бумаге и муторное в реализации: при расхождении по конкретному полю побеждает источник с большим trust именно для этого поля, а не «источник вообще лучше» — zigpy может ошибаться в одном поле и быть правым во всех остальных. Всё, что не удаётся разрешить автоматически, попадает в очередь ручной проверки, а не тихо перезаписывается наугад.

Отдельная головная боль — сами парсеры источников. Espressif SDK, например, генерирует список кластеров через C‑макрос с auto‑increment (ESP_ZB_ZCL_ATTR_SET_WITH_ATTR_ID), который обычный regex не разбирает в принципе. Пришлось признать этот пробел и делегировать такие места более качественному источнику (zigpy), вместо того чтобы городить хрупкий парсер C‑препроцессора. Результат этой работы — zb_zcl.db: 95 кластеров, 1879 атрибутов, 329 команд, и у каждой записи в базе можно посмотреть, откуда она взята и почему победила именно эта версия при конфликте. Ни в Zigbee2MQTT, ни в ZHA такой прозрачности нет — там стандарт используется как данность, без публикации того, откуда конкретное число или тип взялись и насколько им можно доверять.

Отдельно к zb_zcl.db был построен zb_catalog.db — уже не стандарт, а каталог реальных устройств от вендоров (сейчас 568, парсинг ещё нескольких тысяч из Z2M/ZHA продолжается).

Разница принципиальная: zb_zcl.db отвечает на вопрос «что должно быть по стандарту», zb_catalog.db — “что реально делают производители” (а они, как выяснилось, стандарт соблюдают не всегда). Эти две базы — фундамент, на котором строится всё остальное.

Шаг 1: Designer

Designer — визуальный конструктор дерева устройства: конечные точки (endpoints), кластеры, атрибуты. Он опирается на zb_zcl.db и zb_catalog.db, подсказывая допустимые кластеры, обязательные атрибуты и их типы вместо того, чтобы заставлять держать это в голове.

На выходе Designer формирует JSON‑спецификацию — контракт, описывающий структуру модели устройства независимо от того, на какой платформе она в итоге будет реализована.

Важное следствие такого подхода: раз контракт формализован и документирован, спецификацию можно получить не только через GUI, но и вручную — написав JSON по документации — или сгенерировать нейросетью, используя описание контракта в качестве промпта. Designer в этом смысле не единственный источник спецификаций, а один из возможных инструментов их создания.

Шаг 2: постпроцессоры

Постпроцессор берёт на входе JSON‑спецификацию и отдаёт на выходе готовую прошивку под конкретную платформу.

Сейчас реализован один постпроцессор — ESP32Builder, генерирующий C‑код для ESP32-C6 на базе Espressif esp‑zigbee‑lib, дополненный собственным компактным асинхронным диспетчером процессов Spinner.

Ключевое решение здесь: соответствие «атрибут кластера ↔ вызов API SDK» не описывается вручную для каждого кластера, а вычисляется автоматически на основе сканирования заголовков SDK и той же zb_zcl.db. Ручное описание API каждого кластера — гарантированный источник рассинхронизации при обновлении SDK; автоматическое сопоставление, завязанное на единую базу знаний, оказалось надёжнее. Формат контракта на входе постпроцессора при этом не меняется — в будущем на его основе можно реализовать постпроцессоры под другие платформы и языки, не трогая ни Designer, ни базы знаний.

Шаг 3: прикладная логика

Дальше начинается зона, которую я сознательно не стал регламентировать жёстким правилом. Прошивка, сгенерированная постпроцессором, даёт устройству базовый API для работы с его ZigBee‑деревом, но не решает, что именно устройство должно делать при получении команды или изменении атрибута — это прикладная логика, которую пишет сам разработчик.

Здесь встаёт тот самый вопрос, который меня изначально и интересовал: где должна жить конкретная логика — на самом устройстве или на хабе. Критерий, которым я пользуюсь: переживёт ли эта логика обрыв связи с хабом. Если да — она обязана быть на устройстве, иначе автономность (одна из исходных целей проекта) превращается в пустые слова. Если логика принципиально межустройственная — например, зависит от состояния нескольких приборов сразу — ей место на хабе.

Между этими двумя случаями есть честная серая зона — логика, которая технически может быть реализована в обоих местах. Я не стал закрывать её правилом: решение, где именно её реализовать, отдаётся на усмотрение того, кто проектирует конкретное устройство или сценарий.

Это отличается от закрытых экосистем, где для логики предписано ровно одно место — ZBFun допускает сосуществование обоих вариантов одновременно.

Шаг 4: Composer, локальное облако

Composer — верхний уровень архитектуры, оркестрация сценариев между разными устройствами: то, что физически не может жить на одном приборе, потому что зависит от нескольких сразу. Здесь же — статистика, конфигурация сети и, при необходимости, общение с внешним миром.

Ключевое слово — «при необходимости»: облако в ZBFun не обязательный элемент системы, а опциональная надстройка над локальным хабом, который сам по себе полностью работоспособен без выхода в интернет.

Что дальше

В следующей статье, когда до неё дойдут руки, разберу подробнее, как устроен ESP32Builder — включая три реальных бага, которые всплыли только на первой настоящей компиляции сгенерированного кода, и почему автоматическое сканирование заголовков SDK оказалось надёжнее ручного описания API каждого кластера.