Создавать правила можно разными способами. Поначалу кажется, что самое простое решение — поместить всё в один файл внутри папки wb-rules. Код перед глазами, всё чётко и последовательно. Казалось бы, что может пойти не так?
Сложности возникают по мере добавления функционала. Сначала это defineRule, затем объявления виртуальных устройств и подписка на топики MQTT, а позже появляется объёмная логика сценарного взаимодействия.
Код становится трудным для восприятия, ведь чтобы сложить полную картину происходящего, требуется вникнуть в каждую деталь.
Каждому устройству — свой модуль
Распутать клубок кода позволяет деление на модули. Нет смысла каждый раз «изобретать» работу с устройством заново, будь то многоканальный счётчик электроэнергии, термостат или же обычный сценарный пульт Zigbee на четыре кнопки.
Мы ведь не пытаемся создать с нуля микроволновку перед тем, как разогреть себе еду? Это справедливо и по отношению к модулями правил wb-rules. Один раз настроили, покрыли юнит-тестами для уверенности в отсутствии ошибок и больше туда не смотрим.
Всё, что экспортируется из модуля, является его публичным интерфейсом. Прямо как индикаторы и кнопки на приборной панели.
Есть две схемы работы с состоянием модуля:
- Опрос — когда постоянно поглядываем на микроволновку и уточняем, «еда готова? Нет, всё ещё крутится».
- Подписка на событие — когда занимаемся своими делами и просто ждём сигнала от прибора.
Во втором случае за дело берётся особая событийная модель. Код внутри модуля wb-rules-modules делится на этапы и всякий раз, как выполнение добирается до очередной ключевой точки, возникает соответствующее событие.
Расположенный в сценариях из папки wb-rules код подписывается на эти события и тем самым ловит наиболее удачный момент для реакции:
- Батарея разряжена? Событие низкого заряда;
- Реле перешло во включенное состояние? Событие включения;
- Нажата кнопка беспроводного пульта? Событие нажатия кнопки.
Модуль ничего не знает о коде, который расположен за его пределами. В нужный момент он просто «дёргает за ниточки» и сообщает, что пора бы как-то отреагировать.
По схожему принципу работает defineRule, когда внутри объявляем обработчик then. Только прилетают там события более низкого уровня, в которых сначала предстоит разобраться.
Допустим, есть модуль сценарного пульта. Устройство присылает в топик action значения 1_single, 4_double. Это конечно замечательно, но здесь зашито слишком много смысла — и номер кнопки, и тип нажатия. Всего 12 возможных состояний.
Разобрав входящее значение на составляющие, можно сгенерировать новое событие, уже без сложных преобразований: onSingleClick, onDoubleClick или onLongPress. А в качестве аргумента передать подписчикам номер кнопки.
Так мы избавляем логику основного сценария от попыток понять смысл топиков, отдавая ей конкретные события, называемые пользовательскими.
Как реализованы события?
Достаточно просто. У них есть собственный модуль event, который добавлен в папку wb-rules-modules. Этот модуль экспортирует функцию-построитель useEvent<TArgs>(); — она держит в себе всё, что относится к конкретному событию. Все его обработчики. Два вызова useEvent где бы то ни было — два совершенно разных события.
Функция возвращает объект с четырьмя методами:
- raise - для объявлений о наступлении события;
- on - для подписки на событие;
- once - для однократного выполнения действия при наступлении события;
- off - для отписки от события.
Принцип использования:
// Создать событие.
const foodReady = useEvent();
// Добавить слушатель события.
const listener = foodReady.on(() => log('We can get it now!'));
// Запустить событие где-нибудь в коде модуля.
foodReady.raise();
// Прекратить прослушивание события.
listener.off();
В каждом вызове useEvent живёт собственный массив внешних функций-обработчиков, попавших в него через выполнение методов on или once. При выполнении raise происходит последовательный перебор элементов массива и вызов содержащихся в нём функций.
Исходный код модуля событий можно найти по ссылке:
Он полностью проверен и покрыт юнит-тестами. Каждый тест воспроизводит одну из ситуаций использования:
Код примера для запуска на контроллере помещён в файл wb-rules/example_event.ts - при сборке проекта произойдёт его автоматическая адаптация под ES5:
Пример реального устройства
Наконец-то пришло это время. Код достаточно объёмный, поэтому рассмотрим лишь концептуальные моменты. Перейдём в модуль wb-rules-modules/moes-scene-switch.ts и найдём там объявление useSceneSwitch:
export function useSceneSwitch(options: SceneSwitchOptions) {
const singleClickEvent = useEvent<ClickEventArgs>()
defineRule({
whenChanged: `${options.deviceId}/action`,
then: (value) => // ...
})
return {
onSingleClick: singleClickEvent.on
}}
Это основная секция, определяющая суть работы с устройством. В самом начале создаются события. Затем добавляется ядро взаимодействия с движком wb-rules - через вызов defineRule создаётся правило реагирования на изменения в топике. Внутри происходит упомянутый ранее разбор на составляющие через вызов parseAction и дальше принимается решение о том, какое событие инициировать.
Возвращаемое функцией useSceneSwitch значение представляет из себя объект с возможностью подписки на события одинарного, двойного и долгого нажатий.
const moesSwitch = useSceneSwitch({
deviceId: process.env.APP_SCENESW_1
})
moesSwitch.onSingleClick(({ button }) => {
log(`Sceneswitch ${button} single click`)
})
Полный код модуля для работы с пультом доступен по ссылке:
Его юнит-тесты проверяют корректность реагирования на пользовательские события, используя имитатор defineRule. Этот принцип открывает возможность проведения симуляции аварийных ситуаций без необходимости выполнения кода на контроллере:
Тем не менее, код для запуска примера на контроллере тоже имеется:
При наличии пульта Moes ZS-SR-R01 можно указать его friendly_name как moes_sceneswitch_1 и понажимать кнопки, получая в лог контроллера соответствующие статусы.
Послесловие
Становится очевидным, что формат статей на Дзене не очень хорошо подходит для размещения больших «полотен» кода, в которые превращается подробное объяснение принципов продвинутого программирования.
Если у вас возникнут какие-либо вопросы по структуре кода, можем предметно обсудить любую тему в разделе Discussions репозитория проекта на Github, либо в уютном чате компании Wirenboard в Телеграме.
К настоящему моменту, проект полностью настроен и готов для воплощения ваших задумок. Предложения по улучшению приветствуется.