Найти в Дзене

Правила wb-rules и парадигма EDP: пользовательские события

Создавать правила можно разными способами. Поначалу кажется, что самое простое решение — поместить всё в один файл внутри папки wb-rules. Код перед глазами, всё чётко и последовательно. Казалось бы, что может пойти не так? Сложности возникают по мере добавления функционала. Сначала это defineRule, затем объявления виртуальных устройств и подписка на топики MQTT, а позже появляется объёмная логика сценарного взаимодействия. Код становится трудным для восприятия, ведь чтобы сложить полную картину происходящего, требуется вникнуть в каждую деталь. Распутать клубок кода позволяет деление на модули. Нет смысла каждый раз «изобретать» работу с устройством заново, будь то многоканальный счётчик электроэнергии, термостат или же обычный сценарный пульт Zigbee на четыре кнопки. Мы ведь не пытаемся создать с нуля микроволновку перед тем, как разогреть себе еду? Это справедливо и по отношению к модулями правил wb-rules. Один раз настроили, покрыли юнит-тестами для уверенности в отсутствии ошибок и
Оглавление

Создавать правила можно разными способами. Поначалу кажется, что самое простое решение — поместить всё в один файл внутри папки wb-rules. Код перед глазами, всё чётко и последовательно. Казалось бы, что может пойти не так?

Сложности возникают по мере добавления функционала. Сначала это defineRule, затем объявления виртуальных устройств и подписка на топики MQTT, а позже появляется объёмная логика сценарного взаимодействия.

Код становится трудным для восприятия, ведь чтобы сложить полную картину происходящего, требуется вникнуть в каждую деталь.

Каждому устройству — свой модуль

Распутать клубок кода позволяет деление на модули. Нет смысла каждый раз «изобретать» работу с устройством заново, будь то многоканальный счётчик электроэнергии, термостат или же обычный сценарный пульт Zigbee на четыре кнопки.

Мы ведь не пытаемся создать с нуля микроволновку перед тем, как разогреть себе еду? Это справедливо и по отношению к модулями правил wb-rules. Один раз настроили, покрыли юнит-тестами для уверенности в отсутствии ошибок и больше туда не смотрим.

Всё, что экспортируется из модуля, является его публичным интерфейсом. Прямо как индикаторы и кнопки на приборной панели.

Есть две схемы работы с состоянием модуля:

  1. Опрос — когда постоянно поглядываем на микроволновку и уточняем, «еда готова? Нет, всё ещё крутится».
  2. Подписка на событие — когда занимаемся своими делами и просто ждём сигнала от прибора.

Во втором случае за дело берётся особая событийная модель. Код внутри модуля 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-typescript/src/wb-rules-modules/event.ts at latest · wihome-dev/wb-rules-typescript

Он полностью проверен и покрыт юнит-тестами. Каждый тест воспроизводит одну из ситуаций использования:

wb-rules-typescript/tests/wb-rules-modules/event.test.ts at latest · wihome-dev/wb-rules-typescript

Код примера для запуска на контроллере помещён в файл wb-rules/example_event.ts - при сборке проекта произойдёт его автоматическая адаптация под ES5:

wb-rules-typescript/src/wb-rules/example_event.ts at latest · wihome-dev/wb-rules-typescript

Пример реального устройства

Наконец-то пришло это время. Код достаточно объёмный, поэтому рассмотрим лишь концептуальные моменты. Перейдём в модуль 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`)
})

Полный код модуля для работы с пультом доступен по ссылке:

wb-rules-typescript/src/wb-rules-modules/moes-scene-switch.ts at latest · wihome-dev/wb-rules-typescript

Его юнит-тесты проверяют корректность реагирования на пользовательские события, используя имитатор defineRule. Этот принцип открывает возможность проведения симуляции аварийных ситуаций без необходимости выполнения кода на контроллере:

wb-rules-typescript/tests/wb-rules-modules/moes-scene-switch.test.ts at latest · wihome-dev/wb-rules-typescript

Тем не менее, код для запуска примера на контроллере тоже имеется:

wb-rules-typescript/src/wb-rules/example_moes-scene-switch.ts at latest · wihome-dev/wb-rules-typescript

При наличии пульта Moes ZS-SR-R01 можно указать его friendly_name как moes_sceneswitch_1 и понажимать кнопки, получая в лог контроллера соответствующие статусы.

Код примера работы с событиями сценарного пульта Moes
Код примера работы с событиями сценарного пульта Moes

Послесловие

Становится очевидным, что формат статей на Дзене не очень хорошо подходит для размещения больших «полотен» кода, в которые превращается подробное объяснение принципов продвинутого программирования.

Если у вас возникнут какие-либо вопросы по структуре кода, можем предметно обсудить любую тему в разделе Discussions репозитория проекта на Github, либо в уютном чате компании Wirenboard в Телеграме.

К настоящему моменту, проект полностью настроен и готов для воплощения ваших задумок. Предложения по улучшению приветствуется.