diff --git a/Makefile b/Makefile index 5fd61b17..121ab5db 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,24 @@ SCENARIOS_ROOT := scenarios/ # Папка для схем и изображений в схеме SCHEMA_DIR := schema/ +# Целевые пути +CONFIG_DEST := $(DESTDIR)/etc +IMAGE_DEST := $(DESTDIR)/var/www/images +SCHEMA_DEST := $(DESTDIR)$(PREFIX)/share/wb-mqtt-confed/schemas +# Используем системный путь до скриптов /usr/share/wb-rules-system/rules +# так как /etc/wb-rules/* должно использоваться для пользовательских скриптов +RULES_DEST := $(DESTDIR)$(PREFIX)/share/wb-rules-system/rules +# Используем системный путь до модулей /usr/share/wb-rules-modules +# так как /etc/wb-rules-modules/* должно использоваться +# для пользовательских модулей +MODULES_DEST := $(DESTDIR)$(PREFIX)/share/wb-rules-modules + # Поиск папок сценариев внутри папки scenarios SCENARIO_DIRS := $(wildcard $(SCENARIOS_ROOT)*) +# @note: Потенциально могут быть пробелы и спец символы в именах файлов +# или папок - можно сделать проверку перед началом работы + # Предварительно вычисленные списки файлов для копирования CONFIG_FILES := $(wildcard *.conf) IMAGE_FILES := $(wildcard $(SCHEMA_DIR)*.png) @@ -37,46 +52,44 @@ install: @# Копирование всех конфигурационных файлов из корня проекта @$(foreach file,$(CONFIG_FILES),\ - echo "Copying $(file) to $(DESTDIR)/etc";\ - install -Dm644 $(file) -t $(DESTDIR)/etc;) + echo "Copying $(file) to $(CONFIG_DEST)";\ + install -Dm644 $(file) -t $(CONFIG_DEST);) @# Копирование изображений из папки schema @$(foreach file,$(IMAGE_FILES),\ - echo "Copying image $(file) to $(DESTDIR)/var/www/images";\ - install -Dm644 $(file) -t $(DESTDIR)/var/www/images;) + echo "Copying image $(file) to $(IMAGE_DEST)";\ + install -Dm644 $(file) -t $(IMAGE_DEST);) @# Копирование схем из папки schema @$(foreach file,$(SCHEMA_FILES),\ - echo "Copying schema $(file) to $(DESTDIR)$(PREFIX)/share/wb-mqtt-confed/schemas";\ - install -Dm644 $(file) -t $(DESTDIR)$(PREFIX)/share/wb-mqtt-confed/schemas;) + echo "Copying schema $(file) to $(SCHEMA_DEST)";\ + install -Dm644 $(file) -t $(SCHEMA_DEST);) @# Установка каждого сценария из подпапок @$(foreach dir,$(SCENARIO_DIRS),\ echo "Installing from directory $(dir)...";\ $(MAKE) -s install-$(dir);) -define TEMPLATE +define INSTALL_SCENARIO_TEMPLATE install-$(1): @echo " + Processing directory $(1)..." + @# Используем уникальные имена *_$(1) чтобы не перезаписывать переменные + $(eval MODULE_FILES_$(1) := $(wildcard $(1)/*.mod.js)) + $(eval JS_FILES_$(1) := $(wildcard $(1)/*.js)) @# Собираем все файлы .js, кроме модулей (заканчиваются на .mod.js) - $(eval RULE_FILES := $(filter-out $(wildcard $(1)/*.mod.js), $(wildcard $(1)/*.js))) - $(eval MODULE_FILES := $(wildcard $(1)/*.mod.js)) - - @# Используем системный путь до скриптов /usr/share/wb-rules-system/rules - @# так как /etc/wb-rules/* должно использоваться для пользовательских скриптов - @if [ -n "$(RULE_FILES)" ]; then \ - echo " - Copying rule files: $(RULE_FILES) to $(DESTDIR)$(PREFIX)/share/wb-rules-system/rules";\ - install -Dm644 $(RULE_FILES) -t $(DESTDIR)$(PREFIX)/share/wb-rules-system/rules;\ + $(eval RULE_FILES_$(1) := $(filter-out $(MODULE_FILES_$(1)), $(JS_FILES_$(1)))) + + @if [ -n "$$(RULE_FILES_$(1))" ]; then \ + echo " - Copying rule files: $$(RULE_FILES_$(1)) to $(RULES_DEST)";\ + install -Dm644 $$(RULE_FILES_$(1)) -t $(RULES_DEST);\ fi - @# Используем системный путь до модулей /usr/share/wb-rules-modules - @# так как /etc/wb-rules-modules/* должно использоваться для пользовательских модулей - @if [ -n "$(MODULE_FILES)" ]; then \ - echo " - Copying module files: $(MODULE_FILES) to $(DESTDIR)$(PREFIX)/share/wb-rules-modules";\ - install -Dm644 $(MODULE_FILES) -t $(DESTDIR)$(PREFIX)/share/wb-rules-modules;\ + @if [ -n "$$(MODULE_FILES_$(1))" ]; then \ + echo " - Copying module files: $$(MODULE_FILES_$(1)) to $(MODULES_DEST)";\ + install -Dm644 $$(MODULE_FILES_$(1)) -t $(MODULES_DEST);\ fi .PHONY: dummy install-$(1) endef -$(foreach dir,$(SCENARIO_DIRS),$(eval $(call TEMPLATE,$(dir)))) +$(foreach dir,$(SCENARIO_DIRS),$(eval $(call INSTALL_SCENARIO_TEMPLATE,$(dir)))) diff --git a/README.md b/README.md index 65416908..9af04537 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ Данный репозиторий содержит готовые сценарии которые помогают быстрее подключать устройства и связывать их между собой +На данный момент реализованые сценарии: + +- [Управление устройствами](scenarios/devices-control/README.md) + ## Установка Пакет `wb-scenarios` можно установить из репозиториев wirenboard @@ -14,3 +18,6 @@ После этого у вас в пункте `Настройки` -> `Конфигурационные файлы` должен появится пункт `Сценарии автоматизации` + +Для информации о разработке новых сценариев - смотрите +файл для разработчиков [README](develop/README.md). diff --git a/debian/changelog b/debian/changelog index f6ea04b9..bfc9fa95 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,10 @@ -wb-scenarios (1.0.4) stable; urgency=medium +wb-scenarios (1.0.5) stable; urgency=medium + + * feat: add counters control functional in devices-control scenario + + -- Vitalii Gaponov Tue, 05 Nov 2024 08:30:00 +0300 + + wb-scenarios (1.0.4) stable; urgency=medium * feat: implement new scenario for light control diff --git a/debian/control b/debian/control index 5928fac6..1b2da605 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,5 @@ Vcs-Git: https://github.com/wirenboard/wb-scenarios Package: wb-scenarios Architecture: all -Depends: ${shlibs:Depends}, ${misc:Depends}, wb-rules (>= 2.20.0~~), wb-utils (>= 2.1) -Recommends: linux-image-wb2 | linux-image-wb6 (>= 4.9+wb20180729224630) | linux-image-wb7, - wb-hwconf-manager (>= 1.30.1), wb-release-info +Depends: ${shlibs:Depends}, ${misc:Depends}, wb-rules (>= 2.20.0~~) Description: Scenarios for Wiren Board diff --git a/develop/README.md b/develop/README.md index 7106d2f7..992a5f27 100644 --- a/develop/README.md +++ b/develop/README.md @@ -11,7 +11,15 @@ # cd ~ ``` -Клонирование на контроллер +Установить git и make на контроллера + +```terminal +# apt update && \ + apt install git -y && \ + apt install make +``` + +Клонирование на контроллер с помошью гит ```terminal # git clone https://github.com/wirenboard/wb-scenarios @@ -21,9 +29,10 @@ В процессе разработки удобно клонировать свою ветку и сразу провести установку ```terminal -# git clone -b feature/add-light-scenario-first-iteration --single-branch https://github.com/wirenboard/wb-scenarios.git -# cd wb-scenarios -# make +# GIT_BRANCH_NAME="feature/implement-counters-processing" +# git clone -b "${GIT_BRANCH_NAME}" --single-branch "https://github.com/wirenboard/wb-scenarios.git" && \ + cd wb-scenarios && \ + make ``` ### Сборка пакета и установка через apt @@ -118,8 +127,6 @@ Installing from directory scenarios/link-in-to-out... /etc/scenarios.conf ``` -Для более подробной информации о разработке новых сценариев - смотрите -файл [README](develop/README.md) для разработчиков. @@ -146,6 +153,9 @@ Installing from directory scenarios/link-in-to-out... 4. Модуль `*.mod.js` Это опциональная единица для удобного использования Сюда можно перенести методы init и другую логику, чтобы их было удобно использовать в том числе из скриптов. +ВАЖНО - если пишется модуль - то нужно стараться делать его так, чтобы он не занал что работает в сценариях: +Он должен принимать общие структуры данных - например массивы входных и выходных контролов, но не объект самого сценария. +Это нужно чтобы его можно было использовать вообще без сценариев, там где конфиг имеет другую структуру. Ниже описывается процесс создания новых сценариев @@ -291,4 +301,19 @@ Installing from directory scenarios/link-in-to-out... ---- -Скрипт \ No newline at end of file +Скрипт + + +---- +Модуль + + +Проверка входящих в функцию параметров: +Для того чтобы инициализировать правило мы должны быть уверены что +пользователь сконфтгурировал корректно работающую конфигурацию топиков +и выбранных событий и действий +Для этого нам нужно проверить: + - Что введённые пользователем устройства и топики существуют + - Что тип выбранных пользователем топиков поддерживает выбранный + тип события для аходных топиков или вид действия для выходных топиков + \ No newline at end of file diff --git a/develop/scenarios.drawio b/develop/scenarios.drawio index 94e0679a..9063cfd8 100644 --- a/develop/scenarios.drawio +++ b/develop/scenarios.drawio @@ -1,4 +1,4 @@ - + @@ -22,72 +22,69 @@ - + - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - + + + + + diff --git a/scenarios/devices-control/README.md b/scenarios/devices-control/README.md index f13473bd..610a56c0 100644 --- a/scenarios/devices-control/README.md +++ b/scenarios/devices-control/README.md @@ -1,10 +1,61 @@ -# Сценарий управления светом +# Сценарий управления устройствами `devices-control` Позволяет управлять светом соединив один или несколько выключателей с одним или несколькими световыми приборами Удобен для реализации разных случаев: + - Мастер выключателя - один выключатель выключает весь свет в помешениях - Проходной выключатель - несколько выключателей управляют одним световым прибором +- Управление шторами Конфигурация выглядит следующим образом -![alt text](doc/image.png) \ No newline at end of file +![alt text](doc/scenario-config.png) + +Каждому сценарию создается виртуальное устройство + +![virtual-device](doc/virtual-device.png) + +## Добавление новых типов поведения + +Поведение это событие/действие просто названное одним словом. + +Для добавления новых типов отслеживаемых событий или действий над контролом - нужно +1) Изменить WEBUI + +Добавить в файл схемы `wb-scenarios.schema.json` новый тип события/действия в соответствующий выпадающий список + +- Для событий: `inControls.items.properties.behaviorType` +- Для действий: `outControls.items.properties.behaviorType` + +2) Реализовать логику обработки + +Описания событий и действий находятся в отдельных файл с таблицами: + +- Для событий: `table-handling-events.mod.js` +- Для действий: `table-handling-actions.mod.js` + +Процесс добавления выглядит следующим образом: + +- Добавить в таблицу событие или действие с тем же названием enum которое выбрали в схеме +- Описать функцию хендлер которая + - Событие - вернет тру если событие произошло + - Действие - вернет новое значение параметра в зависимости от логики действия + +## Таблицы событий и действий + +Так как логика сценариев подразумевает связь трех сущностей между собой +Каждое событие или действие имеет +- ключ, который соответствует типу события/действия и +включает параметры: +- @param {Array} reqCtrlTypes - Required Control Types + Разрешенные типы контрол топиков MQTT для данного + события/действия +- @param {function} handler - Функция обработчика события или действия +При изменении состояния любого из входных топиков, согласно настроенным событиям, +все выходные топики изменяют своё состояние в соответствии с настроенным действием. + +Хендлер события должен содержать логику обработки события и отвечать на вопрос - произошел ли данный тип события +Возвращать должна для ясности переменную с именем `isEventTriggered` + +Хендлер действия должен содержать логику вычисления нового значения контрола и отвечать на вопрос - какое новое значение контрола нужно записать в соответствии с данным типом действия +Возвращать должна для ясности переменную с именем `newControlValue` diff --git a/scenarios/devices-control/devices-control.mod.js b/scenarios/devices-control/devices-control.mod.js index d2095cd5..afd20fe8 100644 --- a/scenarios/devices-control/devices-control.mod.js +++ b/scenarios/devices-control/devices-control.mod.js @@ -1,32 +1,148 @@ /** * @file Модуль для инициализации связи между множеством входных и выходных - * switch топиков MQTT. Все выходные топики изменяют состояние - * при изменении любого из входных топиков + * топиков MQTT. Поддерживаемые типы контролов: + * - 'switch' + * - 'value' + * При изменении любого из входных топиков по настроенному + * событию - все выходные топики изменяют состояние в соответствии + * с настроенным действием + * * @author Vitalii Gaponov * @link Комментарии в формате JSDoc */ +var eTable = require("table-handling-events.mod"); +var aTable = require("table-handling-actions.mod"); + +/** + * Проверяет, входит ли тип контрола в список допустимых типов + * + * @param {string} controlName - Имя контрола + * @param {Array} reqCtrlTypes - Список допустимых типов + * @returns {boolean} Возвращает true, если тип контрола допустим, иначе false + */ +function isControlTypeValid(controlName, reqCtrlTypes) { + var controlType = dev[controlName + "#type"]; + + // Обработка на случай не существования контрола + if (!controlType) { + log.debug("Control type for " + controlName + " not found, return: " + controlType); + return false; + } + log.debug("Control: " + controlName + " | Type: " + controlType); + + var isTypeValid = (reqCtrlTypes.indexOf(controlType) !== -1); + return isTypeValid; +} + + +/** + * Проверяет типов всех контролов в массиве на соответсвие + * требованиям в таблице + * + * @param {Array} controls - Массив конфигураций контролов + * @param {Object} table - Таблица, содержащая допустимые типы для + * каждого события/действия + * @returns {boolean} - Возвращает true, если все контролы имеют + * допустимые типы, иначе false + */ +function validateControls(controls, table) { + for (var i = 0; i < controls.length; i++) { + var curCtrlName = controls[i].control; + var curBehaviorType = controls[i].behaviorType; + var reqCtrlTypes = table[curBehaviorType].reqCtrlTypes; + + if (!isControlTypeValid(curCtrlName, reqCtrlTypes)) { + log.debug("Error: Control '" + curCtrlName + "' is not of a valid type"); + return false; + } + } + return true; +} + +/** + * Основная функция для проверки всех входных и выходных контролов + * Проверяет соответствие типов контролов и событий/действий + * В случае несоответствия типа контролов, логирует ошибку и прерывает выполнение + * + * @param {Array} inControls - Массив конфигураций входных контролов + * @param {Array} outControls - Массив конфигураций выходных контролов + * @returns {void} + */ +function checkControls(inControls, outControls) { + log.debug("Input Controls conf: " + JSON.stringify(inControls)); + log.debug("Output Controls conf: " + JSON.stringify(outControls)); + + // @todo:vg Добавить проверку существования указанных контролов перед работой + // чтобы мы были уверенны что каждый контрол реально существует + + var isInputControlsValid = validateControls(inControls, eTable.eventsTable); + var isOutputControlsValid = validateControls(outControls, aTable.actionsTable); + + var isAllCtrlTypesValid = (isInputControlsValid && isOutputControlsValid); + if (!isAllCtrlTypesValid) { + log.error("Error: One or more controls are not of a valid type. Operation aborted!"); + return; + } + + log.debug("All controls have valid types"); +} + + /** * Инициализирует виртуальное устройство и определяет правило для управления * множеством выходных топиков на основе множества входных топиков * @param {string} idPrefix - Префикс сценария, используемый для идентификации * виртуального устройства и правила - * @param {Array} inControls - Массив входных контролов, значения - * которых следует слушать - * Пример: ["device1/control1", - * "device2/control2"] - * @param {Array} outControls - Массив выходных контролов, значения - * которых будут изменены - * Пример: ["device3/control3", - * "device4/control4"] + * @param {Array} inControls - Массив входных контролов, значения + * которых нужно слушать. Каждый объект содержит: + * - Имя контрола + * - Тип события которое ловится + * Пример: + * [ + * { + * "control": "vd-wall-switch1/enabled", + * "behaviorType": "whenChange" + * }, + * { + * "control": "vd-wall-switch2/enabled", + * "behaviorType": "whenDisabled" + * } + * ] + * @param {Array} outControls - Массив выходных контролов, значения + * которых будут изменены. Каждый объект содержит: + * - Имя контрола + * - Тип выполняемого с контролом действия + * - Значение для установки (актуально для value) + * Пример: + * [ + * { + * "control": "vd-pump1/enabled", + * "behaviorType": "setDisable", + * "actionValue": 0 + * }, + * { + * "control": "vd-pump2/enabled", + * "behaviorType": "setValue", + * "actionValue": 22 + * } + * ] + * @returns {void} */ -function init(idPrefix, inControls, outControls) { - defineVirtualDevice("GenVd_" + idPrefix, { - title: "Generated VD: " + idPrefix, +function init(idPrefix, deviceTitle, inControls, outControls) { + // Проверка входящей в функцию конфигурации параметров + checkControls(inControls, outControls); + + var genVirtualDeviceName = "wbsc_" + idPrefix; + var genRuleName = "wbru_" + idPrefix; + + defineVirtualDevice(genVirtualDeviceName, { + title: deviceTitle, cells: { - enabled: { + active: { + title: {en: 'Activate scenario rule', ru: 'Активировать правило сценария'}, type: "switch", - value: false + value: true }, } }); @@ -37,51 +153,52 @@ function init(idPrefix, inControls, outControls) { inControlNames.push(inControls[i].control); } - defineRule("GenRule_" + idPrefix, { - whenChanged: inControlNames, - then: function (newValue, devName, cellName) { - var controlFullName = devName + '/' + cellName; - var matchedInControl = null; - - // Ищем контрол, который вызвал изменение и его тип изменения - for (var i = 0; i < inControls.length; i++) { - if (inControls[i].control === controlFullName) { - matchedInControl = inControls[i]; - break; - } - } - if (!matchedInControl) return; - var eventType = matchedInControl.eventType; - - // @todo:vg Реализовать нормальную обработку счетчиков - // Для этого нужно изменить обработку чтобы - // не было инверсии !newValue - - // Проверяем настроенное условие срабатывания - if (eventType === "onEnabled" && !newValue) return; - if (eventType === "onDisabled" && newValue) return; - // Для "onChange" продолжаем всегда - - // Выполняем действия на выходных контролах - for (var j = 0; j < outControls.length; j++) { - var curControlName = outControls[j].control; - var curUserAction = outControls[j].actionType; - var resAction = false; // По умолчанию - выключаем - - if (curUserAction === 'toggle') - resAction = !dev[curControlName]; - else if (curUserAction === 'setOn') - resAction = true; - - dev[curControlName] = resAction; - log("Control " + curControlName + " updated to state: " + resAction); + function thenHandler(newValue, devName, cellName) { + var isActive = dev[genVirtualDeviceName + "/active"]; + if (!isActive) { + return; + } + + var controlFullName = devName + '/' + cellName; + var matchedInControl = null; + + // Ищем контрол вызвавший изменение, получаем прослушиваемый тип события + for (var i = 0; i < inControls.length; i++) { + if (inControls[i].control === controlFullName) { + matchedInControl = inControls[i]; + break; } + } + if (!matchedInControl) return; + var eventType = matchedInControl.behaviorType; + + // Проверяем настроенное условие срабатывания + // @note: Для "whenChange" продолжаем всегда + if (!eTable.eventsTable[eventType].handler(newValue)) return; - log("Output controls updated for " + idPrefix); + // Выполняем действия на выходных контролах + // Не усложняем проверками так как проверили все заранее в инициализации + for (var j = 0; j < outControls.length; j++) { + var curCtrlName = outControls[j].control; + var curUserAction = outControls[j].behaviorType; + var curActionValue = outControls[j].actionValue; + var actualValue = dev[curCtrlName]; + var newCtrlValue = aTable.actionsTable[curUserAction].handler(actualValue, curActionValue); + + log.debug("Control " + curCtrlName + " will updated to state: " + newCtrlValue); + dev[curCtrlName] = newCtrlValue; + log.debug("Control " + curCtrlName + " successfull updated"); } + + log.debug("Output controls updated for generate 'idPrefix': " + idPrefix); + } + + defineRule(genRuleName, { + whenChanged: inControlNames, + then: thenHandler }); } -exports.init = function (idPrefix, inControls, outControls) { - init(idPrefix, inControls, outControls); +exports.init = function (idPrefix, deviceTitle, inControls, outControls) { + init(idPrefix, deviceTitle, inControls, outControls); }; diff --git a/scenarios/devices-control/doc/image.png b/scenarios/devices-control/doc/image.png deleted file mode 100644 index 3641979e..00000000 Binary files a/scenarios/devices-control/doc/image.png and /dev/null differ diff --git a/scenarios/devices-control/doc/scenario-config.png b/scenarios/devices-control/doc/scenario-config.png new file mode 100644 index 00000000..e4b3e2bc Binary files /dev/null and b/scenarios/devices-control/doc/scenario-config.png differ diff --git a/scenarios/devices-control/doc/virtual-device.png b/scenarios/devices-control/doc/virtual-device.png new file mode 100644 index 00000000..9bf56158 Binary files /dev/null and b/scenarios/devices-control/doc/virtual-device.png differ diff --git a/scenarios/devices-control/init-devices-control.js b/scenarios/devices-control/init-devices-control.js index 91e318bb..331459e5 100644 --- a/scenarios/devices-control/init-devices-control.js +++ b/scenarios/devices-control/init-devices-control.js @@ -1,9 +1,11 @@ /** * @file Скрипт для инициализации сценариев с типом SCENARIO_TYPE_STR - * @overview Этот скрипт загружает все конфигурации сценарииев с типом - * SCENARIO_TYPE_STR из файла, находит все активные сценарии - * данного типа, и инициализирует их согласно настройкам, указанным - * в каждом сценарии. + * @overview Этот скрипт: + * - Загружает все конфигурации сценарииев с типом + * SCENARIO_TYPE_STR из файла + * - Находит все активные сценарии данного типа + * - Инициализирует их согласно настройкам, указанным + * в каждом сценарии * @author Vitalii Gaponov * @link Комментарии в формате JSDoc */ @@ -11,17 +13,16 @@ var moduleInToOut = require("devices-control.mod"); /** - * Глобальная переменная, хранящая строку типа сценария для поиска в конфиге - * Сценарии SCENARIO_TYPE_STR могут соединять только два MQTT switch топика + * Глобальная переменная, хранящая строку пути расположения файла конфигурации * @type {string} */ -var SCENARIO_TYPE_STR = "devicesControl"; +var CONFIG_PATH = "/etc/wb-scenarios.conf"; /** - * Глобальная переменная, хранящая строку пути расположения файла конфигурации + * Глобальная переменная, хранящая строку типа сценария для поиска в конфиге * @type {string} */ -var CONFIG_PATH = "/etc/wb-scenarios.conf"; +var SCENARIO_TYPE_STR = "devicesControl"; /** * Находит и возвращает все включеные сценарии с типом searchScenarioType @@ -30,16 +31,16 @@ var CONFIG_PATH = "/etc/wb-scenarios.conf"; * @returns {Array} Массив активных сценариев с типом searchScenarioType */ function findAllScenariosWithType(listScenario, searchScenarioType) { - var resScenarios = []; + var matchedScenarios = []; for (var i = 0; i < listScenario.length; i++) { var scenario = listScenario[i]; var isTarget = (scenario.scenarioType === searchScenarioType) && (scenario.enable === true); if (isTarget) { - resScenarios.push(scenario); + matchedScenarios.push(scenario); } } - return resScenarios; + return matchedScenarios; } /** @@ -47,73 +48,68 @@ function findAllScenariosWithType(listScenario, searchScenarioType) { * @param {object} scenario - Объект сценария, содержащий настройки */ function initializeScenario(scenario) { - log("Scenario found: " + JSON.stringify(scenario)); - - var inControls = scenario.inControls; - var outControls = scenario.outControls; - log("Input Controls conf: " + inControls); - log("Output Controls conf: " + outControls); - - // Check type prop - must be "switch" and equal - // @todo:vg Реализовать нормальную обработку счетчиков - // Для этого нужно изменить обработку входних параметров - var isAllInputsSwitchTypes = true; - for (var i = 0; i < inControls.length; i++) { - var curInControl = inControls[i].control; - var inputType = dev[curInControl + "#type"]; - log("Input control: " + curInControl + " | Type: " + inputType); - - if (inputType !== "switch") { - isAllInputsSwitchTypes = false; - log("Error: Input control '" + curInControl + "' is not of type 'switch'"); - break; - } + log.debug("Processing scenario: " + JSON.stringify(scenario)); + + moduleInToOut.init(scenario.id_prefix, + scenario.name, + scenario.inControls, + scenario.outControls); + + log.debug("Initialization successful for: " + scenario.name); +} + +/** + * Читает конфигурационный файл и возвращает объект конфигурации. + * @param {string} configPath Путь к конфигурационному файлу. + * @returns {Object|null} Возвращает: + * - Массив: сценариев если проверки пройденыили + * - null: в случае ошибки + */ +function readAndValidateConfig(configPath) { + log.debug("Input config path: " + configPath); + var config = readConfig(configPath); + + if (!config) { + log.error("Error: Could not read config from " + configPath); + return null; } + log.debug("Input config contain: " + JSON.stringify(config)); - var isAllOutputsSwitchTypes = true; - for (var j = 0; j < outControls.length; j++) { - var curOutControl = outControls[j].control; - var outputType = dev[curOutControl + "#type"]; - log("Output control: " + curOutControl + " | Type: " + outputType); + // Проверяем существование поля, тип массив, что не пуст + if (!config.hasOwnProperty('scenarios')) { + log.error("Error: 'scenarios' does not exist in the configuration."); + return null; + } - if (outputType !== "switch") { - isAllOutputsSwitchTypes = false; - log("Error: Output control '" + curOutControl + "' is not of type 'switch'"); - break; - } + var listAllScenarios = config.scenarios; + if (!Array.isArray(listAllScenarios)) { + log.error("Error: 'scenarios' is not an array."); + return null; } - // var isValidTypes = (isAllInputsSwitchTypes && isAllOutputsSwitchTypes); - var isValidTypes = (isAllOutputsSwitchTypes); - if (!isValidTypes) { - log("Error: One or more controls are not of type 'switch' for: " + scenario.name); - return; + if (listAllScenarios.length === 0) { + log.error("Error: 'scenarios' array is empty."); + return null; } - moduleInToOut.init(scenario.id_prefix, - scenario.inControls, - scenario.outControls); - log("Initialization successful for: " + scenario.name); + return listAllScenarios; } function main() { - var config = readConfig(CONFIG_PATH); - log("Input config: " + JSON.stringify(config)); - - var listScenario = config.scenarios; - if (!Array.isArray(listScenario) || listScenario.length === 0) { - log("Error: 'scenarios' is not an array, does not exist, or is empty."); - return; - } + var listAllScenarios = readAndValidateConfig(CONFIG_PATH); + if (!listAllScenarios) return; - var resScenarios = findAllScenariosWithType(listScenario, SCENARIO_TYPE_STR); - if (resScenarios.length === 0) { - log("Error: No scenarios of type '" + SCENARIO_TYPE_STR + "' found."); + var matchedScenarios = findAllScenariosWithType(listAllScenarios, + SCENARIO_TYPE_STR); + if (matchedScenarios.length === 0) { + log.error("Error: No scenarios of type '" + SCENARIO_TYPE_STR + "' found."); return; } + + log.debug("Number of matched scenarios: " + JSON.stringify(matchedScenarios)); - for (var i = 0; i < resScenarios.length; i++) { - initializeScenario(resScenarios[i]); + for (var i = 0; i < matchedScenarios.length; i++) { + initializeScenario(matchedScenarios[i]); } } diff --git a/scenarios/devices-control/table-handling-actions.mod.js b/scenarios/devices-control/table-handling-actions.mod.js new file mode 100644 index 00000000..42a216f2 --- /dev/null +++ b/scenarios/devices-control/table-handling-actions.mod.js @@ -0,0 +1,104 @@ +/** + * @file Модуль таблицы производимых над контролами действий + * + * @author Vitalii Gaponov + * @link Комментарии в формате JSDoc + */ + +/** + * Действие отключения контрола + * @param {boolean} actualValue - Актуальное состояние контрола на данный момент + * @returns {boolean} Всегда возвращает false + */ +function setDisable(actualValue, actionValue) { + var newCtrlValue = false; + return newCtrlValue; +} + +/** + * Действие включения контрола + * @param {boolean} actualValue - Актуальное состояние контрола на данный момент + * @returns {boolean} Всегда возвращает true + */ +function setEnable(actualValue, actionValue) { + var newCtrlValue = true; + return newCtrlValue; +} + +/** + * Действие переключения состояния контрола + * @param {boolean} actualValue - Актуальное состояние контрола на данный момент + * @returns {boolean} Возвращает противоположное текущему состоянию контрола + */ +function toggle(actualValue, actionValue) { + var newCtrlValue = !actualValue; + return newCtrlValue; +} + +/** + * Действие установки значения контрола величиной в actionValue + * @param {number} actualValue - Актуальное состояние контрола на данный момент + * @param {number} actionValue - Значение заданное пользователем + * @returns {number} Возвращает новое значение контрола + */ +function setValue(actualValue, actionValue) { + var newCtrlValue = actionValue; + return newCtrlValue; +} + +/** + * Действие увеличения значения контрола на величину в actionValue + * @param {number} actualValue - Актуальное состояние контрола на данный момент + * @param {number} actionValue - Значение заданное пользователем + * @returns {number} Возвращает новое значение контрола + */ +function increaseValueBy(actualValue, actionValue) { + var newCtrlValue = actualValue + actionValue; + return newCtrlValue; +} + +/** + * Действие уменьшения значения контрола на величину в actionValue + * @param {number} actualValue - Актуальное состояние контрола на данный момент + * @param {number} actionValue - Значение заданное пользователем + * @returns {number} Возвращает новое значение контрола + */ +function decreaseValueBy(actualValue, actionValue) { + var newCtrlValue = actualValue - actionValue; + return newCtrlValue; +} + +/** +* Таблица действий +* Содержит имя действия и соответствующие ему: +* - Разрешенные типы контрола +* - Обработчик +*/ +var actionsTable = { + 'toggle': { + reqCtrlTypes: ['switch'], + handler: toggle + }, + 'setEnable': { + reqCtrlTypes: ['switch'], + handler: setEnable + }, + 'setDisable': { + reqCtrlTypes: ['switch'], + handler: setDisable + }, + 'setValue': { + reqCtrlTypes: ['value'], + handler: setValue + }, + 'increaseValueBy': { + reqCtrlTypes: ['value'], + handler: increaseValueBy + }, + 'decreaseValueBy': { + reqCtrlTypes: ['value'], + handler: decreaseValueBy + } +}; + +exports.actionsTable = actionsTable; diff --git a/scenarios/devices-control/table-handling-events.mod.js b/scenarios/devices-control/table-handling-events.mod.js new file mode 100644 index 00000000..8bcd20fa --- /dev/null +++ b/scenarios/devices-control/table-handling-events.mod.js @@ -0,0 +1,59 @@ +/** + * @file Модуль таблицы регистрируемых событий над контролами + * + * @author Vitalii Gaponov + * @link Комментарии в формате JSDoc + */ + +/** + * Событие активации контрола + * @param {boolean} newValue - Новое состояние контрола + * @returns {boolean} Возвращает true, если контрол включен + */ +function whenEnabled(newValue) { + var isEventTriggered = (newValue === true); + return isEventTriggered; +} + +/** + * Событие деактивации контрола + * @param {boolean} newValue - Новое состояние контрола + * @returns {boolean} Возвращает true, если контрол выключен + */ +function whenDisabled(newValue) { + var isEventTriggered = (newValue === false); + return isEventTriggered; +} + +/** + * Событие изменения состояния контрола + * @param {any} newValue - Новое состояние контрола + * @returns {boolean} Всегда возвращает true + */ +function whenChange(newValue) { + var isEventTriggered = true; // Всегда срабатывает при изменении + return isEventTriggered; +} + +/** +* Таблица событий +* Содержит имя события и соответствующие ему: +* - Разрешенные типы контрола +* - Обработчик +*/ +var eventsTable = { + 'whenChange': { + reqCtrlTypes: ['switch', 'value'], + handler: whenChange + }, + 'whenDisabled': { + reqCtrlTypes: ['switch'], + handler: whenDisabled + }, + 'whenEnabled': { + reqCtrlTypes: ['switch'], + handler: whenEnabled + } +}; + +exports.eventsTable = eventsTable; diff --git a/scenarios/link-in-to-out/README.md b/scenarios/link-in-to-out/README.md deleted file mode 100644 index bfa5dce5..00000000 --- a/scenarios/link-in-to-out/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Сценарий соединения входа и выхода - -Позволяет соединить вход с выходом -Конфигурация выглядит следующим образом - -![alt text](doc/image.png) \ No newline at end of file diff --git a/scenarios/link-in-to-out/doc/image.png b/scenarios/link-in-to-out/doc/image.png deleted file mode 100644 index b2a08694..00000000 Binary files a/scenarios/link-in-to-out/doc/image.png and /dev/null differ diff --git a/scenarios/link-in-to-out/init-link-in-to-out.js b/scenarios/link-in-to-out/init-link-in-to-out.js deleted file mode 100644 index 339ff3e7..00000000 --- a/scenarios/link-in-to-out/init-link-in-to-out.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file Скрипт для инициализации сценариев с типом LINK_IN_TO_OUT_TYPE - * @overview Этот скрипт загружает все конфигурации сценарииев с типом - * LINK_IN_TO_OUT_TYPE из файла, находит все активные сценарии - * данного типа, и инициализирует их согласно настройкам, указанным - * в каждом сценарии. Сценарии могут быть инвертированы в - * зависимости от указанных параметров - * @author Vitalii Gaponov - * @link Комментарии в формате JSDoc - */ - -var moduleInToOut = require("link-in-to-out.mod"); - -/** - * Глобальная переменная, хранящая строку типа сценария для поиска в конфиге - * Сценарии LINK_IN_TO_OUT_TYPE могут соединять только два MQTT switch топика - * @type {string} - */ -var LINK_IN_TO_OUT_TYPE = "linkInToOut"; - -/** - * Глобальная переменная, хранящая строку пути расположения файла конфигурации - * @type {string} - */ -var CONFIG_PATH = "/etc/wb-scenarios.conf"; - -/** - * Находит и возвращает все включеные сценарии с типом LINK_IN_TO_OUT_TYPE - * @param {Array} listScenario - Массив всех сценариев из конфигурации - * @returns {Array} Массив активных сценариев с типом LINK_IN_TO_OUT_TYPE - */ -function findAllLinkInToOutScenarios(listScenario) { - var scenarios = []; - for (var i = 0; i < listScenario.length; i++) { - var scenario = listScenario[i]; - var isTarget = (scenario.scenarioType === LINK_IN_TO_OUT_TYPE) && - (scenario.enable === true); - if (isTarget) { - scenarios.push(scenario); - } - } - return scenarios; -} - -/** - * Инициализирует сценарий с использованием указанных настроек - * @param {object} scenario - Объект сценария, содержащий настройки - */ -function initializeScenario(scenario) { - log("Scenario found: " + JSON.stringify(scenario)); - log("Control Input: " + scenario.inControl); - log("Control Output: " + scenario.outControl); - - // Check type prop - must be "switch" and equal - inputType = dev[scenario.inControl + "#type"]; - outputType = dev[scenario.outControl + "#type"]; - log("Input type: " + inputType); - log("Output type: " + outputType); - - var isValidTypes = (inputType === outputType) && (inputType === "switch"); - if (!isValidTypes) { - log("Error: In and Out types are not the same for: " + scenario.name); - return; - } - - moduleInToOut.init(scenario.id_prefix, - scenario.inControl, - scenario.outControl, - scenario.inverseLink); - log("Initialization successful for: " + scenario.name); -} - -function main() { - var config = readConfig(CONFIG_PATH); - log("Input config: " + JSON.stringify(config)); - - var listScenario = config.scenarios; - if (!Array.isArray(listScenario) || listScenario.length === 0) { - log("Error: 'scenarios' is not an array, does not exist, or is empty."); - return; - } - - var linkScenarios = findAllLinkInToOutScenarios(listScenario); - if (linkScenarios.length === 0) { - log("Error: No scenarios of type '" + LINK_IN_TO_OUT_TYPE + "' found."); - return; - } - - linkScenarios.forEach(initializeScenario); -} - -main(); \ No newline at end of file diff --git a/scenarios/link-in-to-out/link-in-to-out.mod.js b/scenarios/link-in-to-out/link-in-to-out.mod.js deleted file mode 100644 index 5da71ff1..00000000 --- a/scenarios/link-in-to-out/link-in-to-out.mod.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file Модуль для инициализации прямой или инвертированной связи - * между двумя switch топиками MQTT - * @author Vitalii Gaponov - * @link Комментарии в формате JSDoc - */ - -/** - * Инициализирует виртуальное устройство и определяет правило для управления - * устройством - * @param {string} idPrefix - Префикс сценария, используемое для - * идентификации виртуального устройства - * @param {string} inControl - Идентификатор входного контроля, значение - * которого следует слушать - * Пример: "vd_wall_switch/enabled" - * @param {string} outControl - Идентификатор выходного контроля, значение - * которого следует контролировать - * Пример: "vd_pump/enabled" - * @param {boolean} inverseLink - Указывает, должна ли связь быть - * инвертированной - */ -function init(idPrefix, inControl, outControl, inverseLink) { - device = defineVirtualDevice("GenVd_" + idPrefix, { - title: "Generated VD: " + idPrefix, - cells: { - enabled: { - type: "switch", - value: false - }, - } - }); - - defineRule("GenRule_" + idPrefix, { - whenChanged: inControl, - then: function (newValue, devName, cellName) { - // Проверка инверсии и присваивание значения в зависимости от него - if (inverseLink) { - dev[outControl] = !newValue; // Инвертирование значения - } else { - dev[outControl] = newValue; // Прямое присваивание значения - } - } - }); -}; - -exports.init = function (idPrefix, inControl, outControl, inverseLink) { - init(idPrefix, inControl, outControl, inverseLink); -}; diff --git a/schema/scenarios-devices-control.png b/schema/scenarios-devices-control.png deleted file mode 100644 index d6580d67..00000000 Binary files a/schema/scenarios-devices-control.png and /dev/null differ diff --git a/schema/scenarios-link-in-to-out.png b/schema/scenarios-link-in-to-out.png deleted file mode 100644 index a1da3094..00000000 Binary files a/schema/scenarios-link-in-to-out.png and /dev/null differ diff --git a/schema/wb-scenarios.schema.json b/schema/wb-scenarios.schema.json index 3c3055ca..7cb93499 100644 --- a/schema/wb-scenarios.schema.json +++ b/schema/wb-scenarios.schema.json @@ -1,8 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "title": "♥ Automation scenarios", - "description": "Сценарии автоматизации дают возможность управления устройствами и настраивания
связи между ними без необходимости создания скриптов.
Подробнее смотрите в wiki: Сценарии Wiren Board", + "title": "Automation scenarios", "configFile": { "path": "/etc/wb-scenarios.conf", "service": "wb-rules" @@ -14,9 +13,6 @@ "oneOf": [ { "$ref": "#/definitions/devicesControl" - }, - { - "$ref": "#/definitions/linkInToOut" } ], "_format": "wb-multiple", @@ -30,109 +26,10 @@ } } }, - "linkInToOut": { - "type": "object", - "title": "Link in to out", - "description": "Данный сценарий предоставляет возможность прямого соединения дискретного входа с дискретным выходом
", - "_format": "grid", - "properties": { - "scenarioType": { - "type": "string", - "enum": [ - "linkInToOut" - ], - "default": "linkInToOut", - "options": { - "hidden": true - } - }, - "enable": { - "type": "boolean", - "title": "Enable", - "default": true, - "_format": "checkbox", - "propertyOrder": 1, - "options": { - "grid_columns": 12 - } - }, - "name": { - "type": "string", - "title": "Scenario name", - "default": "Управление нагрузкой", - "minLength": 1, - "maxLength": 30, - "propertyOrder": 2, - "options": { - "grid_columns": 12 - } - }, - "id_prefix": { - "type": "string", - "title": "ID Prefix", - "description": "Одно слово на английском языке исключая: пробел, /, +, #. Длина до 15 символов.", - "_pattern_comment": "Запрещает пробелы, /, +, и #, а также ограничивает строку использованием только цифр, нижнего подчеркивания и английских букв", - "pattern": "^[0-9a-zA-Z_]+$", - "default": "link_from_to", - "minLength": 1, - "maxLength": 15, - "propertyOrder": 3, - "options": { - "grid_columns": 12 - } - }, - "inControl": { - "type": "string", - "_format": "wb-autocomplete", - "title": "Input control", - "description": "What input control we need use in format: device/control", - "pattern": "^[^/+#]+/[^/+#]+$", - "propertyOrder": 4, - "options": { - "grid_columns": 12, - "wb": { - "data": "devices" - } - }, - "minLength": 1 - }, - "inverseLink": { - "type": "boolean", - "title": "Inverse link behavior", - "default": false, - "_format": "checkbox", - "propertyOrder": 5, - "options": { - "grid_columns": 12 - } - }, - "outControl": { - "type": "string", - "_format": "wb-autocomplete", - "title": "Output control", - "description": "What output control we need use in format: device/control", - "pattern": "^[^/+#]+/[^/+#]+$", - "propertyOrder": 6, - "options": { - "grid_columns": 12, - "wb": { - "data": "devices" - } - }, - "minLength": 1 - } - }, - "required": [ - "scenarioType", - "enable", - "name", - "id_prefix" - ] - }, "devicesControl": { - "type": "object", "title": "Devices control", - "description": "Сценарий для любых действий над одним или несколькими устройствами.
Может пременятся для группового управления освещением, шторами и тд.
Подробнее и примеры использования в wiki: Сценарий: Управление устройствами
", + "type": "object", + "description": "devicesControl_description", "_format": "grid", "properties": { "scenarioType": { @@ -146,8 +43,8 @@ } }, "enable": { + "title": "enable_title", "type": "boolean", - "title": "Enable", "default": true, "_format": "checkbox", "propertyOrder": 1, @@ -156,37 +53,38 @@ } }, "name": { - "type": "string", "title": "Scenario name", + "type": "string", "default": "Управление устройствами", "minLength": 1, - "maxLength": 30, + "maxLength": 120, "propertyOrder": 2, "options": { "grid_columns": 12 } }, "id_prefix": { + "title": "id_prefix_title", "type": "string", - "title": "ID Prefix", - "description": "Одно слово на английском языке исключая: пробел, /, +, #. Длина до 15 символов.", + "description": "id_prefix_description", "_pattern_comment": "Запрещает пробелы, /, +, и #, а также ограничивает строку использованием только цифр, нижнего подчеркивания и английских букв", "pattern": "^[0-9a-zA-Z_]+$", "default": "devices_control", "minLength": 1, - "maxLength": 15, + "maxLength": 120, "propertyOrder": 3, "options": { - "grid_columns": 12 + "grid_columns": 12, + "patternmessage": "error_regexp_patternmessage" } }, "inControls": { + "title": "inControls_arr_title", "type": "array", "minItems": 1, "propertyOrder": 4, "_title_note": "When options-> compact: true - this title not shown", - "title": "Input controls", - "description": "List of input controls with event types", + "description": "inControls_arr_description", "_format": "table", "options": { "disable_collapse": true, @@ -195,21 +93,22 @@ "disable_array_delete_last_row": true, "disable_array_reorder": true, "wb": { - "disable_panel": true + "disable_panel": true } }, "items": { - "title": "Input control", + "title": "inControls_obj_title", "type": "object", "properties": { "control": { "type": "string", "propertyOrder": 1, - "title": "Input control topic name", - "description": "Input control in format: device/control", + "title": "inControls_control_title", + "description": "control_description", "pattern": "^[^/+#]+/[^/+#]+$", "_format": "wb-autocomplete", "options": { + "patternmessage": "error_regexp_patternmessage", "wb": { "data": "devices" } @@ -217,21 +116,28 @@ "_minLength_note": "Device name (1) + slash (1) + topic name (1)", "minLength": 3 }, - "eventType": { + "behaviorType": { "type": "string", "propertyOrder": 2, - "title": "Event Type", + "title": "inControls_behaviorType_title", "enum": [ - "onChange", - "onEnabled", - "onDisabled" + "whenChange", + "whenDisabled", + "whenEnabled" ], - "default": "onChange" + "default": "whenChange", + "options": { + "enum_titles": [ + "When Change", + "When Disabled (switch)", + "When Enabled (switch)" + ] + } } }, "required": [ "control", - "eventType" + "behaviorType" ] } }, @@ -240,8 +146,8 @@ "minItems": 1, "propertyOrder": 5, "_title_note": "When options-> compact: true - this title not shown", - "title": "Output controls", - "description": "List of output controls with action types", + "title": "outControls_arr_title", + "description": "outControls_arr_description", "_format": "table", "options": { "disable_collapse": true, @@ -250,21 +156,22 @@ "disable_array_delete_last_row": true, "disable_array_reorder": true, "wb": { - "disable_panel": true + "disable_panel": true } }, "items": { - "title": "Output control", + "title": "outControls_obj_title", "type": "object", "properties": { "control": { "type": "string", "propertyOrder": 1, - "title": "Output control topic name", - "description": "Output control in format: device/control", + "title": "outControls_control_title", + "description": "control_description", "pattern": "^[^/+#]+/[^/+#]+$", "_format": "wb-autocomplete", "options": { + "patternmessage": "error_regexp_patternmessage", "wb": { "data": "devices" } @@ -272,21 +179,41 @@ "_minLength_note": "Device name (1) + slash (1) + topic name (1)", "minLength": 3 }, - "actionType": { + "behaviorType": { "type": "string", "propertyOrder": 2, - "title": "Action Type", + "title": "outControls_behaviorType_title", "enum": [ - "setOff", - "setOn", - "toggle" + "toggle", + "setEnable", + "setDisable", + "setValue", + "increaseValueBy", + "decreaseValueBy" ], - "default": "setOff" + "default": "setDisable", + "options": { + "enum_titles": [ + "Toggle (switch)", + "Enable (switch)", + "Disable (switch)", + "Set Value", + "Increase Value By", + "Decrease Value By" + ] + } + }, + "actionValue": { + "type": "number", + "propertyOrder": 3, + "title": "outControls_actionValue_title", + "description": "outControls_actionValue_description", + "default": 0 } }, "required": [ "control", - "actionType" + "behaviorType" ] } } @@ -326,27 +253,59 @@ } }, "translations": { - "en": {}, + "en": { + "enable_title": "Turn on the scenario", + "devicesControl_description": "The script allows you to control one or more devices.
It is suitable for managing lighting, curtains, and other devices.", + "id_prefix_title": "MQTT id of the device", + "id_prefix_description": "Used as part of the topic. Only Latin characters, digits and underscore are allowed. Maximum length is 120.", + "inControls_arr_title": "Triggers", + "inControls_obj_title": "Trigger", + "inControls_arr_description": "Any of the events listed here will trigger the automation.", + "inControls_control_title": "MQTT-topic", + "inControls_behaviorType_title": "Trigger logic", + "outControls_arr_title": "Actions", + "outControls_obj_title": "Action", + "outControls_arr_description": "This list of actions will be executed sequentially when the automation is triggered.", + "outControls_control_title": "MQTT-topic", + "outControls_behaviorType_title": "Action logic", + "outControls_actionValue_title": "Value", + "outControls_actionValue_description": "If necessary", + "control_description": "In format: Device/Control", + "error_regexp_patternmessage": "Value must be valid" + }, "ru": { - "♥ Automation scenarios": "♥ Сценарии автоматизации", + "Automation scenarios": "Сценарии автоматизации", "Scenarios": "Сценарии", "Scenario": "Сценарий", "Scenario name": "Название сценария", - "Link in to out": "Связь входа с выходом", - "Name": "Имя", - "Optional": "Не обязательно", + "enable_title": "Включить сценарий", + "devicesControl_description": "Сценарий позволяет управлять одним или несколькими устройствами.
Применим для управления освещением, шторами и другими устройствами.", + "id_prefix_title": "Идентификатор устройства в MQTT", + "id_prefix_description": "Используется как часть топика в MQTT, только латинские буквы, цифры и нижнее подчёркивание. Не более 120 символов.", + "inControls_arr_title": "События", + "inControls_obj_title": "Событие", + "inControls_arr_description": "Любое из указанных здесь событий будет запускать автоматизацию.", + "inControls_control_title": "MQTT-топик", + "inControls_behaviorType_title": "Логика срабатывания", + "outControls_arr_title": "Действия", + "outControls_obj_title": "Действие", + "outControls_arr_description": "Этот список действий будет выполняться последовательно при запуске автоматизации.", + "outControls_control_title": "MQTT-топик", + "outControls_behaviorType_title": "Логика действия", + "outControls_actionValue_title": "Значение", + "outControls_actionValue_description": "Если необходимо", + "control_description": "В формате: Устройство/Контрол", + "error_regexp_patternmessage": "Значение должно быть корректным", "Devices control": "Управление устройствами", - "Enable": "Активен", - "Input control": "Входной контрол", - "Input controls": "Входные контролы", - "List of input controls with event types": "Список входных контролов и типы событий", - "Input control topic name": "Имя топика входного контрола", - "Event Type": "Тип события", - "Output control": "Выходной контрол", - "Output controls": "Выходные контролы", - "List of output controls with action types": "Список выходных контролов и типы действий", - "Output control topic name": "Имя топика выходного контрола", - "Action Type": "Тип действия" + "When Change": "Когда изменится", + "When Disabled (switch)": "Когда отключилось (для переключателей)", + "When Enabled (switch)": "Когда включилось (для переключателей)", + "Toggle (switch)": "Переключить (для переключателей)", + "Enable (switch)": "Включить (для переключателей)", + "Disable (switch)": "Выключить (для переключателей)", + "Set Value": "Установить значение", + "Increase Value By": "Увеличить значение на", + "Decrease Value By": "Уменьшить значение на" } } }