Движок правил wb-rules 1.7: различия между версиями

Материал из Wiren Board
 
(не показано 6 промежуточных версий 3 участников)
Строка 1: Строка 1:
В обновлённом движке правил wb-rules присутствует ряд важных нововведений, касающихся логики написания сценариев.
<languages/>
<translate>


= Сценарии =
== Изоляция сценариев == <!--T:3-->
== Постоянное хранилище данных ==


В wb-rules 1.7 добавлена поддержка постоянных хранилищ. По сути, это объекты, значения в которых будут сохраняться
<!--T:4-->
даже при потере питания контроллера. Такие хранилища удобно использовать для хранения состояний или конфигурации.
Начиная с версии wb-rules 1.7, локальные переменные и функции, обьявленные в файле сценария не видны в других сценариях.
Таким образом, каждый сценарий может определять свои функции и переменные без риска изменить поведение других сценариев.


<syntaxhighlight lang="js">
=== Пример === <!--T:5-->
var ps = new PersistentStorage("my-storage", { global: true });


ps.key = "Hello World";
<!--T:6-->
log(ps.key);
В качестве примера приведём два сценария, одновременно запускаемых в движке правил.
</syntaxhighlight>
Каждый сценарий определяет переменные и функции.


Поддерживаются только глобальные хранилища, т.е. видимые по одному и тому же имени из всех файлов сценариев.
<!--T:7-->
В предыдущих версиях wb-rules обращение к переменной, изменяемой в нескольких
файлах сценариев, может привести к неопределённому поведению. В версиях, начиная с 1.7,
поведение строго определено и такое же, как будто сценарий единственный в системе.


== Виртуальные устройства ==
<!--T:8-->
В комментариях указан вывод команд log для ранних версий и для актуальной версии. Обратите внимание на var перед переменными.


В предыдущих версиях wb-rules значения контролов виртуальных устройств хранились только в MQTT retained, что не очень надёжно (в случае
<!--T:9-->
потери питания данные могли быть легко утеряны). Начиная с версии 2.0, эти значения сохраняются также в специальное хранилище в постоянной
'''Сценарий 1 (rules1.js)'''
памяти и восстанавливаются при загрузке сценария.
<syntaxhighlight lang="js">
var test1 = 42;


Если необходимо каждый раз при перезагрузке скрипта восстанавливать строго определённое значение (т.е. не восстанавливать предыдущее сохранённое),
<!--T:10-->
можно добавить в описание контрола поле forceDefault:
setInterval(function myFuncOne() {
    log("myFuncOne called");
    log("test1: {}", test1);
    log("test2: {}", test2);
}, 5000);
</syntaxhighlight>


<!--T:11-->
'''Сценарий 2 (rules2.js)'''
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
defineVirtualDevice("vdev", {
var test1 = 84;
    ...
var test2 = "Hello";
    cells: {
        ...
        mycell: {
            type: "value",
            value: "10",
            forceDefault: true // при каждой загрузке сценария поле mycell будет получать значение 10
        }
    }
});
</syntaxhighlight>


= Модули =
<!--T:12-->
Начиная с версии 1.7, в движке правил wb-rules появилась поддержка подключаемых JS-модулей (похожая по поведению на
setInterval(function myFuncTwo() {
аналогичную в Node.js, но с некоторыми особенностями).
    log("myFuncTwo called");
 
    log("test1: {}, test2: {}", test1, test2);
== Расположение ==
}, 5000);
Поиск модулей происходит по следующим путям (в заданном порядке):
 
* /etc/wb-rules/modules
* /usr/share/wb-rules/modules
 
Таким образом, пользовательские модули удобно складывать в /etc/wb-rules.
 
Добавить свои пути можно редактированием /etc/default/wb-rules добавлением путей
к переменной WB_RULES_MODULES через разделитель (:):
 
<syntaxhighlight lang="bash">
...
WB_RULES_MODULES="/etc/wb-rules/modules:/usr/share/wb-rules/modules"
...
</syntaxhighlight>
</syntaxhighlight>


== Подключение модуля к сценарию ==
<!--T:13-->
Было:
<pre>
2019-01-05 17:29:50myFuncTwo called
2019-01-05 17:29:50test1: 84, test2: Hello
2019-01-05 17:29:50myFuncOne called
2019-01-05 17:29:50test1: 84
2019-01-05 17:29:50test2: Hello


Подключение модуля происходит с помощью функции require(). Она возвращает объект, экспортированный
<!--T:14-->
модулем (exports).
</pre>


<syntaxhighlight lang="js">
<!--T:15-->
...
Стало:
var myModule = require("myModule");
<pre>
...
2019-01-05 17:28:42myFuncTwo called
</syntaxhighlight>
2019-01-05 17:28:42test1: 84, test2: Hello
 
2019-01-05 17:28:42myFuncOne called
При этом движок правил будет искать файл myModule.js по очереди в директориях поиска (см. Расположение).
2019-01-05 17:28:42test1: 42
 
2019-01-05 17:28:42ECMAScript error: ReferenceError: identifier 'test2' undefined
Также допустим поиск файла модуля по поддиректориям в директориях поиска, тогда вызов будет выглядеть так:
duk_js_var.c:1232
myFuncOne /etc/wb-rules/rules1.js:6 preventsyield
</pre>


=== Примечание === <!--T:16-->


<!--T:17-->
В предыдущих версиях wb-rules для изоляции правил рекомендовалось использовать замыкание, т.е.
оборачивание кода сценария в конструкцию:
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
...
(function() {
var myModule = require("path/to/myModule");
    // код сценария идёт здесь
...
})();
</syntaxhighlight>
</syntaxhighlight>


После того, как файл будет найден, его содержимое будет выполнено, и из файла будет передан объект exports.
<!--T:18-->
Начиная с версии 1.7, в подобной конструкции обычно нет необходимости. Тем не менее, старые сценарии,
использующие эту конструкцию, продолжат работу без изменений в поведении.


'''Примечание 1:''' если модуль был подключен в одном сценарии несколько раз (несколько вызовов require("myModule")),
=== Обходные пути === <!--T:19-->
содержимое файла модуля будет выполнено только в первый раз, а при повторных вызовах будет возвращаться сохранённый
объект exports.


'''Примечание 2:''' если модуль подключается в разных сценариях, для каждого сценария будет создан свой объект модуля
<!--T:20-->
и заново выполнен весь код модуля. Если модулю требуется использовать данные, общие для всех файлов сценариев,
Если в вашей системе использовалось общее глобальное пространство для хранения общих данных и функций,
для хранения данных следует использовать объект module.static.
есть несколько способов реализации такого поведения:


== Создание модуля ==
==== Постоянное хранилище ==== <!--T:21-->


Для создания модуля достаточно создать файл с именем, соответсвующим имени модуля (с расширением .js) в директории
<!--T:22-->
/etc/wb-rules/modules.
Для обмена данными также можно использовать глобальные постоянные хранилища (PersistentStorage).


В этом файле будут доступны все стандартные функции wb-rules, а также набор специальных объектов, с помощью
<!--T:23-->
которого можно реализовать необходимый функционал модуля.
'''Внимание:''' при использовании глобальных постоянных хранилищ может произойти совпадение имён, в
этом случае возможно труднообнаруживаемое нарушение поведения.


=== Объект exports ===
<!--T:24-->
<syntaxhighlight lang="js">
var ps = new PersistentStorage("my-global-storage", {global: true});


С помощью объекта exports можно передавать пользовательскому сценарию параметры и методы.
<!--T:25-->
/// ...


==== Пример ====
<!--T:26-->
ps.myvar = "value"; // это значение доступно для всех пользователей хранилища с именем "my-global-storage"
</syntaxhighlight>


Файл модуля /etc/wb-rules/modules/myModule.js
==== Прототип глобального объекта ==== <!--T:27-->
<syntaxhighlight lang="js">
exports.hello = function(text) {
    log("Hello from module, {}", text);
};


exports.answer = 42;
<!--T:28-->
</syntaxhighlight>
'''ВНИМАНИЕ:''' метод считается "грязным", т.к. все переменные и функции, опубликованные таким образом,
становятся доступными всем сценариям в системе. Старайтесь избегать этого способа. За неопределённое
поведение при использовании этого метода несёт ответственность сам программист.


Файл сценария scenario.js
<!--T:29-->
<syntaxhighlight lang="js">
Глобальные объекты всех сценариев имеют общий объект-''прототип'', в котором определены стандартные функции
var m = require("myModule");
wb-rules (такие, как defineRule, setTimeout и т.д.). Через него можно передавать переменные или функции
m.hello("world"); // выведет в лог "Hello from module, world"
в общую область видимости.
log("The answer is {}", m.answer); // выведет в лог "The answer is 42"
</syntaxhighlight>
 
'''Будьте внимательны:''' объект exports можно '''только дополнять значениями''', но не переопределять.
Иначе значения экспортированы не будут!


<!--T:30-->
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
exports = function(text) {
global.__proto__.myVar = 42; // теперь myVar - общая переменная для всех сценариев
    log("Hello from module, {}", text);
};


// Ожидание:
<!--T:31-->
var m = require("my-module");
// из других сценариев к переменной можно обращаться так
m("world"); // не работает
log("shared myVar: {}", myVar);


// На практике m будет пустым объектом.
<!--T:32-->
// Та же проблема произойдёт при использовании такой конструкции:
// или вот так, что чуть более аккуратно, т.к. однозначно показывает, где определена переменная
exports = {
log("shared myVar: {}", global.__proto__.myVar);
    hello: function(text) {
        log("Hello from module, {}", world);
    },
    answer: 42
};
</syntaxhighlight>
</syntaxhighlight>


=== Объект module ===
<!--T:33-->
Правило поиска переменной в первом случае будет выглядеть так:


Объект module содержит параметры, относящиеся непосредственно к файлу модуля.
<!--T:34-->
# Проверяем, есть ли myVar среди локальных переменных (определённой как var myVar = ...).
# Если нет, проверяем, есть ли myVar в глобальном объекте (определённой как myVar = ...).
# Если нет, проверяем, есть ли myVar в ''прототипе'' глобального объекта (определённой как global.__proto__.myVar).


==== module.filename ====
<!--T:35-->
Содержит полный путь до файла ''модуля''. Например, для модуля, сохранённого в /etc/wb-rules/modules/myModule.js:
Поиск останавливается, как только переменная найдена.


<syntaxhighlight lang="js">
<!--T:36-->
log(module.filename); // выведет /etc/wb-rules/modules/myModule.js
Таким образом, первый способ обращения будет работать только в том случае, если myVar не определена в верхних областях видимости.
</syntaxhighlight>


==== module.static ====
== Постоянное хранилище данных == <!--T:37-->
Объект, хранящий данные, общие для всех экземпляров данного модуля. Его следует использовать для тех данных,
которые должны быть доступны сразу во всех сценариях, использующих данный модуль.


Файл /etc/wb-rules/modules/myModule.js
<!--T:38-->
<syntaxhighlight lang="js">
В wb-rules 1.7 добавлена поддержка постоянных хранилищ. По сути, это объекты, значения в которых будут сохраняться
exports.counter = function() {
даже при потере питания контроллера. Такие хранилища удобно использовать для хранения состояний или конфигурации.
    if (module.static.count === undefined) {
        module.static.count = 1;
    }
    log("Number of calls: {}", module.static.count);
    module.static.count++;
};
</syntaxhighlight>


Файл сценария scenario1.js
<!--T:39-->
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
var m = require("myModule");
var ps = new PersistentStorage("my-storage", { global: true });
m.counter();
m.counter();
</syntaxhighlight>


Файл сценария scenario2.js
<!--T:40-->
<syntaxhighlight lang="js">
ps.key = "Hello World";
var m = require("myModule");
log(ps.key);
m.counter();
m.counter();
m.counter();
</syntaxhighlight>
</syntaxhighlight>


В результате работы двух скриптов в логе окажется 5 сообщений:
<!--T:41-->
Поддерживаются только глобальные хранилища, т.е. видимые по одному и тому же имени из всех файлов сценариев.


<syntaxhighlight>
== Виртуальные устройства == <!--T:42-->
Number of calls: 1
Number of calls: 2
Number of calls: 3
Number of calls: 4
Number of calls: 5
</syntaxhighlight>


=== __filename ===
<!--T:43-->
В предыдущих версиях wb-rules значения контролов виртуальных устройств хранились только в MQTT retained, что не очень надёжно (в случае
потери питания данные могли быть легко утеряны). Начиная с версии 2.0, эти значения сохраняются также в специальное хранилище в постоянной
памяти и восстанавливаются при загрузке сценария.


Переменная __filename берётся из глобального объекта сценария, к которому подключается модуль, и содержит имя файла
<!--T:44-->
сценария.
Если необходимо каждый раз при перезагрузке скрипта восстанавливать строго определённое значение (т.е. не восстанавливать предыдущее сохранённое),
 
можно добавить в описание контрола поле forceDefault:
В случае, если модуль подключается в другом модуле, переменная __filename, тем не менее, будет содержать именно
имя файла сценария - вершины дерева зависимостей.


Файл /etc/wb-rules/modules/myModule.js
<!--T:45-->
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
exports.hello = function() {
defineVirtualDevice("vdev", {
     log(__filename);
     ...
};
    cells: {
</syntaxhighlight>
        ...
 
        mycell: {
Файл сценария /etc/wb-rules/scenario1.js
            type: "value",
<syntaxhighlight lang="js">
            value: "10",
var m = require("myModule");
            forceDefault: true // при каждой загрузке сценария поле mycell будет получать значение 10
m.hello(); // выведет scenario1.js
        }
    }
});
</syntaxhighlight>
</syntaxhighlight>
</translate>

Текущая версия на 12:28, 17 марта 2021

Другие языки:

Изоляция сценариев

Начиная с версии wb-rules 1.7, локальные переменные и функции, обьявленные в файле сценария не видны в других сценариях. Таким образом, каждый сценарий может определять свои функции и переменные без риска изменить поведение других сценариев.

Пример

В качестве примера приведём два сценария, одновременно запускаемых в движке правил. Каждый сценарий определяет переменные и функции.

В предыдущих версиях wb-rules обращение к переменной, изменяемой в нескольких файлах сценариев, может привести к неопределённому поведению. В версиях, начиная с 1.7, поведение строго определено и такое же, как будто сценарий единственный в системе.

В комментариях указан вывод команд log для ранних версий и для актуальной версии. Обратите внимание на var перед переменными.

Сценарий 1 (rules1.js)

var test1 = 42;

setInterval(function myFuncOne() {
    log("myFuncOne called");
    log("test1: {}", test1);
    log("test2: {}", test2);
}, 5000);

Сценарий 2 (rules2.js)

var test1 = 84;
var test2 = "Hello";

setInterval(function myFuncTwo() {
    log("myFuncTwo called");
    log("test1: {}, test2: {}", test1, test2);
}, 5000);

Было:

2019-01-05 17:29:50myFuncTwo called
2019-01-05 17:29:50test1: 84, test2: Hello
2019-01-05 17:29:50myFuncOne called
2019-01-05 17:29:50test1: 84
2019-01-05 17:29:50test2: Hello

Стало:

2019-01-05 17:28:42myFuncTwo called
2019-01-05 17:28:42test1: 84, test2: Hello
2019-01-05 17:28:42myFuncOne called
2019-01-05 17:28:42test1: 42
2019-01-05 17:28:42ECMAScript error: ReferenceError: identifier 'test2' undefined
duk_js_var.c:1232
myFuncOne /etc/wb-rules/rules1.js:6 preventsyield

Примечание

В предыдущих версиях wb-rules для изоляции правил рекомендовалось использовать замыкание, т.е. оборачивание кода сценария в конструкцию:

(function() {
    // код сценария идёт здесь
})();

Начиная с версии 1.7, в подобной конструкции обычно нет необходимости. Тем не менее, старые сценарии, использующие эту конструкцию, продолжат работу без изменений в поведении.

Обходные пути

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

Постоянное хранилище

Для обмена данными также можно использовать глобальные постоянные хранилища (PersistentStorage).

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

var ps = new PersistentStorage("my-global-storage", {global: true});

/// ...

ps.myvar = "value"; // это значение доступно для всех пользователей хранилища с именем "my-global-storage"

Прототип глобального объекта

ВНИМАНИЕ: метод считается "грязным", т.к. все переменные и функции, опубликованные таким образом, становятся доступными всем сценариям в системе. Старайтесь избегать этого способа. За неопределённое поведение при использовании этого метода несёт ответственность сам программист.

Глобальные объекты всех сценариев имеют общий объект-прототип, в котором определены стандартные функции wb-rules (такие, как defineRule, setTimeout и т.д.). Через него можно передавать переменные или функции в общую область видимости.

global.__proto__.myVar = 42; // теперь myVar - общая переменная для всех сценариев

// из других сценариев к переменной можно обращаться так
log("shared myVar: {}", myVar);

// или вот так, что чуть более аккуратно, т.к. однозначно показывает, где определена переменная
log("shared myVar: {}", global.__proto__.myVar);

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

  1. Проверяем, есть ли myVar среди локальных переменных (определённой как var myVar = ...).
  2. Если нет, проверяем, есть ли myVar в глобальном объекте (определённой как myVar = ...).
  3. Если нет, проверяем, есть ли myVar в прототипе глобального объекта (определённой как global.__proto__.myVar).

Поиск останавливается, как только переменная найдена.

Таким образом, первый способ обращения будет работать только в том случае, если myVar не определена в верхних областях видимости.

Постоянное хранилище данных

В wb-rules 1.7 добавлена поддержка постоянных хранилищ. По сути, это объекты, значения в которых будут сохраняться даже при потере питания контроллера. Такие хранилища удобно использовать для хранения состояний или конфигурации.

var ps = new PersistentStorage("my-storage", { global: true });

ps.key = "Hello World";
log(ps.key);

Поддерживаются только глобальные хранилища, т.е. видимые по одному и тому же имени из всех файлов сценариев.

Виртуальные устройства

В предыдущих версиях wb-rules значения контролов виртуальных устройств хранились только в MQTT retained, что не очень надёжно (в случае потери питания данные могли быть легко утеряны). Начиная с версии 2.0, эти значения сохраняются также в специальное хранилище в постоянной памяти и восстанавливаются при загрузке сценария.

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

defineVirtualDevice("vdev", {
    ...
    cells: {
        ...
        mycell: {
            type: "value",
            value: "10",
            forceDefault: true // при каждой загрузке сценария поле mycell будет получать значение 10
        }
    }
});