├── debian ├── copyright ├── compat ├── source │ └── lintian-overrides ├── wb-rules.maintscript ├── wb-rules-alarms-reloader.service ├── rules ├── control └── wb-rules.service ├── wbrules ├── testrules_empty.js ├── testrules_locations_dis.js.disabled ├── testrules_load_scenario_bad.js ├── testrules_load_scenario.js ├── test-modules │ └── test │ │ ├── multi_init.js │ │ ├── submodule.js │ │ ├── with_require.js │ │ ├── params.js │ │ ├── helloworld.js │ │ └── static.js ├── testrules_locations_syntax_error.js ├── loc1 │ ├── testrules_more.js │ └── testrules_more_changed.js ├── testrules_isolation_2.js ├── testrules_readonly.js ├── testrules_runtime_errors.js ├── testrules_log.js ├── testrules_rule_redefinition.js ├── testrules_cron.js ├── testrules_threading_2.js ├── testrules_cron_changed.js ├── testrules_localbutton.js ├── testrules_topleveltimers.js ├── testrules_read_config.js ├── testrules_locations_faulty.js ├── testrules_isolation_1.js ├── mqtt_tracker.go ├── testrules_modules_2.js ├── testrules_alarm.js ├── testrules_reload_3.js ├── testrules_track_mqtt.js ├── testrules_threading_1.js ├── testrules_reload_1.js ├── testrules_locations.js ├── testrules_locations_changed.js ├── testrules_reload_3_changed.js ├── testrules_cellchanges.js ├── alarms1.conf ├── testrules_defhelper.js ├── rule_isolation_test.go ├── testrules_rule_controls.js ├── testrules_opt.js ├── alarms.conf ├── rule_runtime_errors_test.go ├── testrules_email_commands.js ├── rule_local_button_suite_test.go ├── testrules_tg_commands.js ├── alarms2.conf ├── threading_test.go ├── testrules_reload_2_changed.js ├── eventbuffer.go ├── testrules_persistent_2.js ├── testrules_vcells_storage.js ├── locations.go ├── testrules_sms_commands.js ├── testrules_command.js ├── rule_log_test.go ├── testrules_loopback.js ├── rule_cron_test.go ├── strings.go ├── testrules_reload_2.js ├── scopedcleanup.go ├── rule_notify_email_test.go ├── rule_controls_test.go ├── rule_notify_tg_test.go ├── vcells_storage_test.go ├── testrules_modules.js ├── spawn.go ├── rule_cell_change_test.go ├── rule_track_mqtt_test.go ├── testrules_persistent.js ├── rule_read_config_test.go ├── testrules_controls_api.js ├── rule_loopback_test.go ├── rule_notify_sms_test.go ├── rule_modules_test.go ├── testrules_timers.js ├── persistent_storage_test.go ├── rule_optimization_test.go ├── escontext_test.go ├── rule_timers_test.go ├── testrules.js ├── testrules_meta.js ├── rule_shell_command_test.go ├── editor.go ├── rule_location_test.go ├── rule_controls_api_test.go ├── rule_alarm_test.go ├── rule.go └── rule_basics_test.go ├── rules ├── load_alarms.js ├── rules.js ├── alarms.conf └── alarms-restricted.schema.json ├── wb-rules.wbconfigs ├── Jenkinsfile ├── .gitignore ├── go.mod ├── sample1.js ├── README-readonly.md ├── Makefile ├── LICENSE ├── samplerules.js ├── go.sum └── main.go /debian/copyright: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /wbrules/testrules_empty.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wbrules/testrules_locations_dis.js.disabled: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rules/load_alarms.js: -------------------------------------------------------------------------------- 1 | Alarms.load('/etc/wb-rules/alarms.conf'); 2 | -------------------------------------------------------------------------------- /wb-rules.wbconfigs: -------------------------------------------------------------------------------- 1 | wb_move /etc/wb-rules 2 | wb_move /etc/wb-rules-modules 3 | -------------------------------------------------------------------------------- /debian/source/lintian-overrides: -------------------------------------------------------------------------------- 1 | wb-rules source: source-is-missing *.wbgo.so 2 | -------------------------------------------------------------------------------- /wbrules/testrules_load_scenario_bad.js: -------------------------------------------------------------------------------- 1 | 2 | var x = 2 + 3 3 | } 4 | var y = "aaaa" -------------------------------------------------------------------------------- /wbrules/testrules_load_scenario.js: -------------------------------------------------------------------------------- 1 | var x = 2 + 3; 2 | var y = 'abcd'; 3 | //last comment 4 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/multi_init.js: -------------------------------------------------------------------------------- 1 | exports.value = 42; 2 | 3 | log('Module multi_init init'); 4 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/submodule.js: -------------------------------------------------------------------------------- 1 | require('./with_require'); 2 | 3 | log('Module submodule init'); 4 | -------------------------------------------------------------------------------- /debian/wb-rules.maintscript: -------------------------------------------------------------------------------- 1 | rm_conffile /etc/wb-rules/load_alarms.js 1.6.7 2 | rm_conffile /etc/init.d/wb-rules 2.7.0~ 3 | -------------------------------------------------------------------------------- /rules/rules.js: -------------------------------------------------------------------------------- 1 | // place your rules here or add more .js files in this directory 2 | log('add your rules to /etc/wb-rules/'); 3 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildDebGolangWbgo defaultTargets: 'bullseye-armhf bullseye-arm64', 2 | defaultRunLintian: true 3 | -------------------------------------------------------------------------------- /rules/alarms.conf: -------------------------------------------------------------------------------- 1 | { 2 | "deviceName": "alarms", 3 | "deviceTitle": "Alarms", 4 | "recipients": [], 5 | "alarms": [] 6 | } 7 | -------------------------------------------------------------------------------- /wbrules/testrules_locations_syntax_error.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | if (true) { 4 | if (); // syntax error 5 | } 6 | -------------------------------------------------------------------------------- /wbrules/loc1/testrules_more.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // The location of device "qqq" is testrules_loc1.js:4 4 | defineSomeDevice('qqq'); 5 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/with_require.js: -------------------------------------------------------------------------------- 1 | var m = require('./submodule'); // must be interpreted as "test/submodule" 2 | 3 | log('Module with_require init'); 4 | -------------------------------------------------------------------------------- /wbrules/loc1/testrules_more_changed.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // The location of device "qqq" is testrules_loc1.js:4 4 | defineSomeDevice('qqqNew'); 5 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/params.js: -------------------------------------------------------------------------------- 1 | exports.params = function params() { 2 | return '__filename: ' + __filename + ', module.filename: ' + module.filename; 3 | }; 4 | 5 | log('Module params init'); 6 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/helloworld.js: -------------------------------------------------------------------------------- 1 | exports.hello = 42; 2 | 3 | // export some functions 4 | exports.adder = function (a, b) { 5 | return (a + b) / 2; 6 | }; 7 | 8 | log('Module helloworld init'); 9 | -------------------------------------------------------------------------------- /wbrules/testrules_isolation_2.js: -------------------------------------------------------------------------------- 1 | var v = 42; 2 | 3 | defineRule('isolated_rule', { 4 | whenChanged: ['vdev/someCell'], 5 | then: function () { 6 | log('isolated_rule (testrules_isolation_2.js) ' + v); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /debian/wb-rules-alarms-reloader.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=One-shot service to reload alarms 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=touch /usr/share/wb-rules/load_alarms.js 7 | SyslogIdentifier=wb-rules-alarms-reloader 8 | -------------------------------------------------------------------------------- /wbrules/testrules_readonly.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('roCells', { 2 | title: 'Readonly Cell Test', 3 | cells: { 4 | rocell: { 5 | type: 'switch', 6 | value: false, 7 | readonly: true, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /wbrules/testrules_runtime_errors.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | defineRule('brokenCellChange', { 4 | asSoonAs: function () { 5 | return dev.somedev.foobar; 6 | }, 7 | then: function () { 8 | badvar; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /wbrules/testrules_log.js: -------------------------------------------------------------------------------- 1 | global.__proto__.testLog = function testLog() { 2 | log('log()'); 3 | debug('debug()'); 4 | log.debug('log.debug({})', 42); 5 | log.info('log.info({})', 42); 6 | log.warning('log.warning({})', 42); 7 | log.error('log.error({})', 42); 8 | }; 9 | -------------------------------------------------------------------------------- /wbrules/test-modules/test/static.js: -------------------------------------------------------------------------------- 1 | exports.count = function () { 2 | if (module.static.counter == undefined) { 3 | module.static.counter = 0; 4 | } 5 | 6 | module.static.counter++; 7 | log('Value: {}', module.static.counter); 8 | }; 9 | 10 | log('Module static init'); 11 | -------------------------------------------------------------------------------- /wbrules/testrules_rule_redefinition.js: -------------------------------------------------------------------------------- 1 | defineRule('test', { 2 | whenChanged: 'somedev/bar', 3 | then: function () { 4 | log('bar!'); 5 | }, 6 | }); 7 | 8 | defineRule('test', { 9 | whenChanged: 'somedev/baz', 10 | then: function () { 11 | log('baz!'); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /wbrules/testrules_cron.js: -------------------------------------------------------------------------------- 1 | defineRule('crontest_hourly', { 2 | when: cron('@hourly'), 3 | then: function () { 4 | log('@hourly rule fired'); 5 | }, 6 | }); 7 | 8 | defineRule('crontest_daily', { 9 | when: cron('@daily'), 10 | then: function () { 11 | log('@daily rule fired'); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /wbrules/testrules_threading_2.js: -------------------------------------------------------------------------------- 1 | global.myvar = 84; 2 | 3 | function adder(a, b) { 4 | return a - b; 5 | } 6 | 7 | defineRule({ 8 | whenChanged: 'test/isolation', 9 | then: function () { 10 | log('2: myvar: {}', global.myvar); 11 | log('2: add {} and {}: {}', 2, 3, adder(2, 3)); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /wbrules/testrules_cron_changed.js: -------------------------------------------------------------------------------- 1 | defineRule('crontest_hourly', { 2 | when: cron('@hourly'), 3 | then: function () { 4 | log('@hourly rule fired (new)'); 5 | }, 6 | }); 7 | 8 | defineRule('crontest_daily', { 9 | when: cron('@daily'), 10 | then: function () { 11 | log('@daily rule fired (new)'); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /wbrules/testrules_localbutton.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('buttons', { 2 | title: 'Button Test', 3 | cells: { 4 | somebutton: { 5 | type: 'pushbutton', 6 | }, 7 | }, 8 | }); 9 | 10 | defineRule('buttontest', { 11 | whenChanged: 'buttons/somebutton', 12 | then: function () { 13 | log('button pressed!'); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /wbrules/testrules_topleveltimers.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | log('topleveltimers'); 4 | 5 | var timerId = setTimeout(function () { 6 | log('this one should never fire'); 7 | }, 999); 8 | 9 | clearTimeout(timerId); // remove timeout before the engine is ready 10 | 11 | setTimeout(function () { 12 | log('timer fired'); 13 | }, 1000); 14 | -------------------------------------------------------------------------------- /wbrules/testrules_read_config.js: -------------------------------------------------------------------------------- 1 | defineRule('readSampleConfig', { 2 | whenChanged: 'somedev/readSampleConfig', 3 | then: function (path) { 4 | try { 5 | var conf = readConfig(path); 6 | } catch (e) { 7 | log.error('readConfig error!'); 8 | return; 9 | } 10 | log('config: {}', JSON.stringify(conf)); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /wbrules/testrules_locations_faulty.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // this device must be registered despite script load error 4 | defineSomeDevice('nonFaultyDev'); 5 | 6 | noSuchFunction(); 7 | 8 | // this device isn't created or registered because script execution 9 | // stops at noSuchFunction() call due to an error 10 | defineSomeDevice('faultyDev'); 11 | -------------------------------------------------------------------------------- /wbrules/testrules_isolation_1.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('vdev', { 2 | title: 'VDev', 3 | cells: { 4 | someCell: { 5 | type: 'switch', 6 | value: false, 7 | }, 8 | }, 9 | }); 10 | 11 | var v = 84; 12 | 13 | defineRule('isolated_rule', { 14 | whenChanged: ['vdev/someCell'], 15 | then: function () { 16 | log('isolated_rule (testrules_isolation_1.js) ' + v); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /wbrules/mqtt_tracker.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | type MqttTrackerMap map[uint32]MqttTracker 4 | 5 | type MqttTracker struct { 6 | ID uint32 7 | Topic string 8 | Callback ESCallbackFunc 9 | } 10 | 11 | // NewMqttTracker returns new mqtt tracker instance 12 | func NewMqttTracker(topic string, id uint32) MqttTracker { 13 | return MqttTracker{ 14 | ID: id, 15 | Topic: topic, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /wbrules/testrules_modules_2.js: -------------------------------------------------------------------------------- 1 | defineRule('multiple_require', { 2 | whenChanged: 'test/multifile', 3 | then: function () { 4 | var m = require('test/multi_init'); 5 | log('[2] My value of multi_init:', m.value); 6 | }, 7 | }); 8 | 9 | defineRule('static', { 10 | whenChanged: 'test/static', 11 | then: function () { 12 | var m = require('test/static'); 13 | m.count(); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | MAKEFLAGS += GO=/usr/lib/go-1.21/bin/go 4 | 5 | %: 6 | dh $@ --parallel 7 | 8 | override_dh_installinit: 9 | dh_installinit --noscripts 10 | 11 | override_dh_builddeb: 12 | dh_builddeb -- -Zgzip 13 | 14 | override_dh_installsystemd: 15 | dh_installsystemd --name=wb-rules 16 | dh_installsystemd --name=wb-rules-alarms-reloader --no-start --no-restart-after-upgrade --no-restart-on-upgrade 17 | -------------------------------------------------------------------------------- /wbrules/testrules_alarm.js: -------------------------------------------------------------------------------- 1 | // this stubs out the default alarm object 2 | global.__proto__.Notify = { 3 | sendEmail: function sendEmail(to, subject, text) { 4 | log('EMAIL TO: {} SUBJ: {} TEXT: {}', to, subject, text); 5 | }, 6 | 7 | sendSMS: function sendSMS(to, text) { 8 | log('SMS TO: {} TEXT: {}', to, text); 9 | }, 10 | 11 | sendTelegramMessage: function sendTelegramMessage(token, chatId, text) { 12 | log("TELEGRAM MESSAGE TOKEN: {} CHATID: {} TEXT: {}", token, chatId, text); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: wb-rules 2 | Maintainer: Wiren Board team 3 | Section: misc 4 | Priority: optional 5 | Multi-Arch: foreign 6 | Standards-Version: 4.5.1 7 | Build-Depends: debhelper (>= 9), golang-1.21-go:native 8 | Homepage: https://github.com/wirenboard/wb-rules 9 | 10 | Package: wb-rules 11 | Architecture: any 12 | Depends: libc6 (>= 2.13), ${misc:Depends} 13 | Breaks: wb-mqtt-confed (<< 1.0.2), wb-rules-system (<< 1.6.13), wb-mqtt-dac (<< 1.1.2) 14 | Description: Wiren Board Rule Engine 15 | wb-rules allows you to create control scenarios using JavaScript-based rules. 16 | -------------------------------------------------------------------------------- /wbrules/testrules_reload_3.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('testNulledControl', { 2 | cells: { 3 | pers_text: { 4 | type: 'text', 5 | readonly: false, 6 | forceDefault: true, 7 | value: '', 8 | }, 9 | trigger: { 10 | type: 'pushbutton', 11 | }, 12 | }, 13 | }); 14 | 15 | defineRule({ 16 | whenChanged: 'testNulledControl/trigger', 17 | then: function () { 18 | log.info('before: {}'.format(dev['testNulledControl']['pers_text'])); 19 | dev['testNulledControl']['pers_text'] = 'someTextString'; 20 | log.info('after: {}'.format(dev['testNulledControl']['pers_text'])); 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /debian/wb-rules.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MQTT Rule engine for Wiren Board 3 | Wants=wb-hwconf-manager.service wb-modules.service 4 | After=wb-hwconf-manager.service wb-modules.service mosquitto.service 5 | 6 | [Service] 7 | Type=simple 8 | Restart=on-failure 9 | RestartSec=1 10 | User=root 11 | Environment="WB_RULES_MODULES=/etc/wb-rules-modules:/usr/share/wb-rules-modules" 12 | EnvironmentFile=-/etc/default/wb-rules 13 | ExecStart=/usr/bin/wb-rules $WB_RULES_OPTIONS -http 127.0.0.1:9090 -syslog -editdir '/etc/wb-rules/' '/usr/share/wb-rules-system/rules/' '/etc/wb-rules/' '/usr/share/wb-rules/' 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | .DS_Store 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.deb 24 | *.exe 25 | *.test 26 | *.prof 27 | *.log 28 | 29 | # Tern port file 30 | .tern-port 31 | 32 | /wb-rules 33 | wbgo* 34 | 35 | vendor 36 | 37 | debian/files 38 | 39 | # Temp files 40 | ~* 41 | .*.swp 42 | debian/.debhelper 43 | 44 | .devcontainer 45 | 46 | ### direnv ### 47 | .direnv 48 | .envrc 49 | -------------------------------------------------------------------------------- /wbrules/testrules_track_mqtt.js: -------------------------------------------------------------------------------- 1 | trackMqtt('/wierd/sub/some', function (obj) { 2 | log('1. wierd topic got value'); 3 | log('topic: {}, value: {}'.format(obj.topic, obj.value)); 4 | }); 5 | 6 | trackMqtt('/wierd/+/some', function (obj) { 7 | log('2. wierd topic got value'); 8 | log('topic: {}, value: {}'.format(obj.topic, obj.value)); 9 | }); 10 | 11 | trackMqtt('/wierd/+/another', function (obj) { 12 | log('3. wierd topic got value'); 13 | log('topic: {}, value: {}'.format(obj.topic, obj.value)); 14 | }); 15 | 16 | trackMqtt('/wierd/#', function (obj) { 17 | log('4. wierd topic got value'); 18 | log('topic: {}, value: {}'.format(obj.topic, obj.value)); 19 | }); 20 | -------------------------------------------------------------------------------- /wbrules/testrules_threading_1.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('test', { 2 | cells: { 3 | test: { 4 | type: 'pushbutton', 5 | }, 6 | isolation: { 7 | type: 'pushbutton', 8 | }, 9 | sync: { 10 | type: 'pushbutton', 11 | }, 12 | }, 13 | }); 14 | 15 | defineRule({ 16 | whenChanged: 'test/test', 17 | then: function () { 18 | log('it works!'); 19 | }, 20 | }); 21 | 22 | global.myvar = 42; 23 | 24 | function adder(a, b) { 25 | return a + b; 26 | } 27 | 28 | defineRule({ 29 | whenChanged: 'test/isolation', 30 | then: function () { 31 | log('1: myvar: {}', global.myvar); 32 | log('1: add {} and {}: {}', 2, 3, adder(2, 3)); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /wbrules/testrules_reload_1.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('vdev0', { 2 | title: 'VDev0', 3 | cells: { 4 | someCell: { 5 | type: 'switch', 6 | value: false, 7 | }, 8 | }, 9 | }); 10 | 11 | defineRule('detRun', { 12 | when: function () { 13 | return true; 14 | }, 15 | then: function () { 16 | log('detRun'); 17 | }, 18 | }); 19 | 20 | // create rule indirectly to check dynamic cleanups 21 | setTimeout(function () { 22 | defineRule('checkIndirect', { 23 | when: function () { 24 | return true; 25 | }, 26 | then: function () { 27 | log('checkIndirect'); 28 | }, 29 | }); 30 | log('timeout set'); 31 | }, 0); 32 | 33 | testrules_reload_1_loaded = true; 34 | -------------------------------------------------------------------------------- /wbrules/testrules_locations.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // The location of device "misc" is testrules_locations.js:4 4 | defineSomeDevice('misc'); 5 | 6 | // The location of the rule "whatever" is testrules_locations.js:7 7 | defineSomeRule('whatever'); 8 | 9 | function defBarDev(name) { 10 | defineSomeDevice(name + 'Bar'); 11 | } 12 | 13 | // The location of the rule "fooBar" is testrules_locations.js:14 14 | defineSomeDevice('foo'); 15 | 16 | // The location of the rule "another" is testrules_locations.js:24 (the end of the defineRule call) 17 | defineRule('another', { 18 | asSoonAs: function () { 19 | return !!dev.somedev.another; 20 | }, 21 | then: function () { 22 | log('another!'); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /wbrules/testrules_locations_changed.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // The location of device "misc" is testrules_locations.js:4 4 | defineSomeDevice('miscNew'); 5 | 6 | // The location of the rule "whatever" is testrules_locations.js:7 7 | defineSomeRule('whateverNew'); 8 | 9 | function defBarDev(name) { 10 | defineSomeDevice(name + 'Bar'); 11 | } 12 | 13 | // The location of the rule "fooBar" is testrules_locations.js:14 14 | defineSomeDevice('foo'); 15 | 16 | // The location of the rule "another" is testrules_locations.js:24 (the end of the defineRule call) 17 | defineRule('another', { 18 | asSoonAs: function () { 19 | return !!dev.somedev.another; 20 | }, 21 | then: function () { 22 | log('another!'); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /wbrules/testrules_reload_3_changed.js: -------------------------------------------------------------------------------- 1 | // the same code as in testrules_reload_3.js, 2 | // but it should cause script reloading 3 | 4 | defineVirtualDevice('testNulledControl', { 5 | cells: { 6 | pers_text: { 7 | type: 'text', 8 | readonly: false, 9 | forceDefault: true, 10 | value: '', 11 | }, 12 | trigger: { 13 | type: 'pushbutton', 14 | }, 15 | }, 16 | }); 17 | 18 | defineRule({ 19 | whenChanged: 'testNulledControl/trigger', 20 | then: function () { 21 | log.info('before: {}'.format(dev['testNulledControl']['pers_text'])); 22 | dev['testNulledControl']['pers_text'] = 'someTextString'; 23 | log.info('after: {}'.format(dev['testNulledControl']['pers_text'])); 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /wbrules/testrules_cellchanges.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('cellch', { 2 | title: 'Cell Change Test', 3 | cells: { 4 | sw: { 5 | type: 'switch', 6 | value: false, 7 | }, 8 | misc: { 9 | type: 'text', 10 | value: '0', 11 | }, 12 | button: { 13 | type: 'pushbutton', 14 | }, 15 | }, 16 | }); 17 | 18 | defineRule('startCellChange', { 19 | whenChanged: 'cellch/button', 20 | then: function () { 21 | dev.cellch.sw = !dev.cellch.sw; 22 | dev.cellch.misc = '1'; 23 | log('startCellChange: sw <- {}', dev.cellch.sw); 24 | }, 25 | }); 26 | 27 | defineRule('switchChanged', { 28 | whenChanged: 'cellch/sw', 29 | then: function () { 30 | log('switchChanged: sw={}', dev.cellch.sw); 31 | dev.somedev.sw = true; 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wirenboard/wb-rules 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 7 | github.com/VictoriaMetrics/metrics v1.37.0 8 | github.com/boltdb/bolt v0.0.0-20161223174454-2e25e3bb4285 9 | github.com/robfig/cron/v3 v3.0.1 10 | github.com/stretchr/objx v0.3.0 11 | github.com/stretchr/testify v1.7.0 12 | github.com/wirenboard/go-duktape v0.0.0-20240729075045-b4150233e350 13 | github.com/wirenboard/wbgong v0.6.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/valyala/fastrand v1.1.0 // indirect 20 | github.com/valyala/histogram v1.2.0 // indirect 21 | golang.org/x/sys v0.15.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /wbrules/alarms1.conf: -------------------------------------------------------------------------------- 1 | { 2 | "deviceName": "sampleAlarms", 3 | "deviceTitle": "Sample Alarms", 4 | "recipients": [ 5 | { 6 | "type": "email", 7 | "to": "someone@example.com", 8 | "subject": "alarm!" 9 | }, 10 | { 11 | "type": "email", 12 | "to": "anotherone@example.com", 13 | "subject": "Alarm: {}" 14 | }, 15 | { 16 | "type": "sms", 17 | "to": "+78122128506" 18 | }, 19 | { 20 | "type": "telegram", 21 | "token": "1234567890:AAHG7MAKsUHLs-pBLhpIw1RU07Hmw9LyDac", 22 | "chatId": "123456789" 23 | } 24 | ], 25 | "alarms": [ 26 | { 27 | // not repeated 28 | "name": "unnecessaryDeviceIsOn", 29 | "cell": "somedev/unnecessaryDevicePower", 30 | "expectedValue": 0, 31 | "alarmMessage": "Unnecessary device is on" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /wbrules/testrules_defhelper.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | // When a rule is defined inside a module the editor must use the 4 | // topmost stack frame in the rule file to determine the location of 5 | // the definition, even if some helper functions are used to define 6 | // rules or devices. 7 | 8 | global.__proto__.defineSomeRule = function defineSomeRule(name) { 9 | var ruleName = name + 'Rule'; 10 | defineRule(ruleName, { 11 | asSoonAs: function () { 12 | return !!dev.somedev[name]; 13 | }, 14 | then: function () { 15 | log('{} fired', ruleName); 16 | }, 17 | }); 18 | }; 19 | 20 | global.__proto__.defineSomeDevice = function defineSomeDevice(name) { 21 | defineVirtualDevice(name, { 22 | title: name, 23 | cells: { 24 | sw: { 25 | type: 'switch', 26 | value: false, 27 | }, 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /wbrules/rule_isolation_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "testing" 6 | ) 7 | 8 | type RuleIsolationSuite struct { 9 | RuleSuiteBase 10 | } 11 | 12 | func (s *RuleIsolationSuite) SetupTest() { 13 | s.SetupSkippingDefs("testrules_isolation_1.js", "testrules_isolation_2.js") 14 | } 15 | 16 | func (s *RuleIsolationSuite) TestIsolation() { 17 | s.publish("/devices/vdev/controls/someCell/on", "1", "vdev/someCell") 18 | s.VerifyUnordered( 19 | "tst -> /devices/vdev/controls/someCell/on: [1] (QoS 1)", 20 | "driver -> /devices/vdev/controls/someCell: [1] (QoS 1, retained)", 21 | "[info] isolated_rule (testrules_isolation_1.js) 84", 22 | "[info] isolated_rule (testrules_isolation_2.js) 42", 23 | ) 24 | } 25 | 26 | func TestRuleIsolationSuite(t *testing.T) { 27 | testutils.RunSuites(t, 28 | new(RuleIsolationSuite), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /wbrules/testrules_rule_controls.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('ctrltest', { 2 | cells: { 3 | disable: { 4 | type: 'pushbutton', 5 | }, 6 | enable: { 7 | type: 'pushbutton', 8 | }, 9 | trigger: { 10 | type: 'pushbutton', 11 | }, 12 | run: { 13 | type: 'pushbutton', 14 | }, 15 | }, 16 | }); 17 | 18 | var m = defineRule({ 19 | whenChanged: 'ctrltest/trigger', 20 | then: function () { 21 | log('controllable rule fired'); 22 | }, 23 | }); 24 | 25 | defineRule({ 26 | whenChanged: 'ctrltest/disable', 27 | then: function () { 28 | log('disable'); 29 | disableRule(m); 30 | }, 31 | }); 32 | 33 | defineRule({ 34 | whenChanged: 'ctrltest/enable', 35 | then: function () { 36 | log('enable'); 37 | enableRule(m); 38 | }, 39 | }); 40 | 41 | defineRule({ 42 | whenChanged: 'ctrltest/run', 43 | then: function () { 44 | log('run'); 45 | runRule(m); 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /wbrules/testrules_opt.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | var asSoonAsCount = 0, 4 | whenCount = 0, 5 | runRuleWithoutCells = false; 6 | 7 | defineRule('condCount', { 8 | asSoonAs: function () { 9 | ++asSoonAsCount; 10 | log('condCount: asSoonAs()'); 11 | return dev.somedev.countIt == '42'; 12 | }, 13 | then: function () { 14 | log('condCount fired, count={}', asSoonAsCount); 15 | runRuleWithoutCells = true; 16 | }, 17 | }); 18 | 19 | defineRule('ruleWithoutCells', { 20 | asSoonAs: function () { 21 | return runRuleWithoutCells; 22 | }, 23 | then: function () { 24 | log('ruleWithoutCells fired'); 25 | }, 26 | }); 27 | 28 | defineRule('condCountLT', { 29 | // LT = LevelTriggered 30 | when: function () { 31 | ++whenCount; 32 | log('condCountLT: when()'); 33 | return dev.somedev.countItLT - 0 >= 42; 34 | }, 35 | then: function () { 36 | log('condCountLT fired, count={}', whenCount); 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /wbrules/alarms.conf: -------------------------------------------------------------------------------- 1 | { 2 | "deviceName": "sampleAlarms", 3 | "deviceTitle": "Sample Alarms", 4 | "recipients": [ 5 | { 6 | "type": "email", 7 | "to": "someone@example.com", 8 | "subject": "alarm!" 9 | }, 10 | { 11 | "type": "email", 12 | "to": "anotherone@example.com", 13 | "subject": "Alarm: {}" 14 | }, 15 | { 16 | "type": "sms", 17 | "to": "+78122128506" 18 | }, 19 | { 20 | "type": "telegram", 21 | "token": "1234567890:AAHG7MAKsUHLs-pBLhpIw1RU07Hmw9LyDac", 22 | "chatId": "123456789" 23 | } 24 | ], 25 | "alarms": [ 26 | { 27 | // notification repeated every 200s while active 28 | "name": "importantDeviceIsOff", 29 | "cell": "somedev/importantDevicePower", 30 | "expectedValue": 1, 31 | "alarmMessage": "Important device is off", 32 | "noAlarmMessage": "Important device is back on", 33 | "interval": 200 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /wbrules/rule_runtime_errors_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/wirenboard/wbgong/testutils" 8 | ) 9 | 10 | type RuleRuntimeErrorsSuite struct { 11 | RuleSuiteBase 12 | } 13 | 14 | func (s *RuleRuntimeErrorsSuite) SetupTest() { 15 | s.SetupSkippingDefs("testrules_runtime_errors.js") 16 | } 17 | 18 | func (s *RuleRuntimeErrorsSuite) TestRuntimeErrors() { 19 | s.publish("/devices/somedev/controls/foobar/meta/type", "switch", "somedev/foobar") 20 | s.publish("/devices/somedev/controls/foobar", "1", "somedev/foobar") 21 | s.Verify( 22 | "tst -> /devices/somedev/controls/foobar/meta/type: [switch] (QoS 1, retained)", 23 | "tst -> /devices/somedev/controls/foobar: [1] (QoS 1, retained)", 24 | regexp.MustCompile( 25 | `(?s:ECMAScript error:.*ReferenceError.*testrules_runtime_errors\.js:8.*)`), 26 | ) 27 | s.EnsureGotErrors() 28 | } 29 | 30 | func TestRuleRuntimeErrorsSuite(t *testing.T) { 31 | testutils.RunSuites(t, 32 | new(RuleRuntimeErrorsSuite), 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /wbrules/testrules_email_commands.js: -------------------------------------------------------------------------------- 1 | global.__proto__.runShellCommand = function (command, options) { 2 | log('run command: {}', command); 3 | if (options.input) { 4 | log('input: {}', options.input); 5 | } 6 | if (options.exitCallback) { 7 | options.exitCallback(dev['test_email/exit_code'], 'stdout', 'stderr'); 8 | } 9 | }; 10 | 11 | defineVirtualDevice('test_email', { 12 | cells: { 13 | exit_code: { 14 | type: 'value', 15 | readonly: false, 16 | value: 0, 17 | }, 18 | send: { 19 | type: 'pushbutton', 20 | }, 21 | send_quoted: { 22 | type: 'pushbutton', 23 | }, 24 | }, 25 | }); 26 | 27 | defineRule({ 28 | whenChanged: 'test_email/send', 29 | then: function () { 30 | Notify.sendEmail('me@example.org', 'Test subject', 'Test text'); 31 | }, 32 | }); 33 | 34 | defineRule({ 35 | whenChanged: 'test_email/send_quoted', 36 | then: function () { 37 | Notify.sendEmail('me@example.org', 'Test "subject" \'single\'', 'Test "text" \'single\''); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /wbrules/rule_local_button_suite_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "testing" 6 | ) 7 | 8 | type RuleLocalButtonSuite struct { 9 | RuleSuiteBase 10 | } 11 | 12 | func (s *RuleLocalButtonSuite) SetupTest() { 13 | // s.RuleSuiteBase.SetupTest(false, "testrules_localbutton.js") 14 | s.RuleSuiteBase.SetupSkippingDefs("testrules_localbutton.js") 15 | } 16 | 17 | func (s *RuleLocalButtonSuite) TestLocalButtons() { 18 | for i := 0; i < 3; i++ { 19 | // The change rule must be fired on each button press ('1' .../on value message) 20 | s.publish("/devices/buttons/controls/somebutton/on", "1", "buttons/somebutton") 21 | s.VerifyUnordered( 22 | "tst -> /devices/buttons/controls/somebutton/on: [1] (QoS 1)", 23 | "driver -> /devices/buttons/controls/somebutton: [1] (QoS 1)", // note there's no 'retained' flag 24 | "[info] button pressed!", 25 | ) 26 | } 27 | } 28 | 29 | func TestRuleLocalButtonsSuite(t *testing.T) { 30 | testutils.RunSuites(t, 31 | new(RuleLocalButtonSuite), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /wbrules/testrules_tg_commands.js: -------------------------------------------------------------------------------- 1 | global.__proto__.runShellCommand = function (command, options) { 2 | log('run command: {}', command); 3 | if (options.input) { 4 | log('input: {}', options.input); 5 | } 6 | if (options.exitCallback) { 7 | options.exitCallback(dev['test_tg/exit_code'], '{"ok": true}', 'stderr'); 8 | } 9 | }; 10 | 11 | defineVirtualDevice('test_tg', { 12 | cells: { 13 | exit_code: { 14 | type: 'value', 15 | readonly: false, 16 | value: 0, 17 | }, 18 | send: { 19 | type: 'pushbutton', 20 | }, 21 | send_quoted: { 22 | type: 'pushbutton', 23 | }, 24 | }, 25 | }); 26 | 27 | defineRule({ 28 | whenChanged: 'test_tg/send', 29 | then: function () { 30 | Notify.sendTelegramMessage('1234567890:abcdefghijklmnopqrstuvwxyz123456789', '12345678', 'Test message'); 31 | }, 32 | }); 33 | 34 | defineRule({ 35 | whenChanged: 'test_tg/send_quoted', 36 | then: function () { 37 | Notify.sendTelegramMessage('1234567890:abcdefghijklmnopqrstuvwxyz123456789', '12345678', 'Test "message" \'single\''); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /wbrules/alarms2.conf: -------------------------------------------------------------------------------- 1 | { 2 | "deviceName": "sampleAlarms", 3 | "deviceTitle": "Sample Alarms", 4 | "recipients": [ 5 | { 6 | "type": "email", 7 | "to": "someone@example.com", 8 | "subject": "alarm!" 9 | }, 10 | { 11 | "type": "email", 12 | "to": "anotherone@example.com", 13 | "subject": "Alarm: {}" 14 | }, 15 | { 16 | "type": "sms", 17 | "to": "+78122128506" 18 | }, 19 | { 20 | "type": "telegram", 21 | "token": "1234567890:AAHG7MAKsUHLs-pBLhpIw1RU07Hmw9LyDac", 22 | "chatId": "123456789" 23 | } 24 | ], 25 | "alarms": [ 26 | { 27 | // notification repeated every 10s while active, but no more than 10 times 28 | "name": "temperatureOutOfBounds", 29 | "cell": "somedev/devTemp", 30 | "minValue": 10, // **mintemp** (comment used by test) 31 | "maxValue": 15, // **maxtemp** (comment used by test) 32 | "alarmMessage": "Temperature out of bounds, value = {{dev.somedev.devTemp}}", 33 | "noAlarmMessage": "Temperature is within bounds again, value = {}", 34 | "interval": 10, 35 | "maxCount": 5 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /wbrules/threading_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type JSThreadingTestSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *JSThreadingTestSuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules_threading_1.js", "testrules_threading_2.js") 15 | } 16 | 17 | func (s *JSThreadingTestSuite) TestRuntime() { 18 | s.publish("/devices/test/controls/test/on", "1", "test/test") 19 | s.Verify("tst -> /devices/test/controls/test/on: [1] (QoS 1)") 20 | s.VerifyUnordered( 21 | "driver -> /devices/test/controls/test: [1] (QoS 1)", 22 | "[info] it works!", 23 | ) 24 | } 25 | 26 | func (s *JSThreadingTestSuite) TestThreadsIsolation() { 27 | s.publish("/devices/test/controls/isolation/on", "1", "test/isolation") 28 | 29 | s.VerifyUnordered( 30 | "tst -> /devices/test/controls/isolation/on: [1] (QoS 1)", 31 | "driver -> /devices/test/controls/isolation: [1] (QoS 1)", 32 | "[info] 1: myvar: 42", 33 | "[info] 1: add 2 and 3: 5", 34 | "[info] 2: myvar: 84", 35 | "[info] 2: add 2 and 3: -1", 36 | ) 37 | } 38 | 39 | func TestJSThreading(t *testing.T) { 40 | testutils.RunSuites(t, new(JSThreadingTestSuite)) 41 | } 42 | -------------------------------------------------------------------------------- /wbrules/testrules_reload_2_changed.js: -------------------------------------------------------------------------------- 1 | var devCells = { 2 | someCell: { 3 | type: 'switch', 4 | value: false, 5 | }, 6 | }; 7 | 8 | defineAlias('smc', 'vdev/someCell'); 9 | 10 | try { 11 | defineVirtualDevice('vdev', { 12 | title: 'VDev', 13 | cells: devCells, 14 | }); 15 | } catch (e) { 16 | log(e); 17 | } 18 | 19 | function cellSpec(devName, cellName) { 20 | return devName === undefined ? '(no cell)' : '{}/{}'.format(devName, cellName); 21 | } 22 | 23 | function defChangeRule(name, cell) { 24 | defineRule(name, { 25 | whenChanged: cell, 26 | then: function (newValue, devName, cellName) { 27 | log('{}: {}={}', name, cellSpec(devName, cellName), newValue); 28 | }, 29 | }); 30 | } 31 | 32 | function defDetectRun(name) { 33 | defineRule(name, { 34 | when: function () { 35 | return true; 36 | }, 37 | then: function (newValue, devName, cellName) { 38 | if (smc !== dev.vdev.someCell) throw new Error('cell alias value mismatch!'); 39 | log('{}: {} (s={})', name, cellSpec(devName, cellName), dev.vdev.someCell); 40 | }, 41 | }); 42 | } 43 | 44 | defDetectRun('detectRun'); 45 | defChangeRule('rule1', 'vdev/someCell'); 46 | 47 | global.__proto__.testrules_reload_2_n++; 48 | -------------------------------------------------------------------------------- /sample1.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('relayClicker', { 2 | title: 'Relay Clicker', 3 | cells: { 4 | enabled: { 5 | type: 'switch', 6 | value: false, 7 | }, 8 | }, 9 | }); 10 | 11 | defineRule('startClicking', { 12 | asSoonAs: function () { 13 | return dev.relayClicker.enabled && dev.uchm121rx['Input 0'] == '0'; 14 | }, 15 | then: function () { 16 | startTicker('clickTimer', 1000); 17 | }, 18 | }); 19 | 20 | defineRule('stopClicking', { 21 | asSoonAs: function () { 22 | return !dev.relayClicker.enabled || dev.uchm121rx['Input 0'] != '0'; 23 | }, 24 | then: function () { 25 | timers.clickTimer.stop(); 26 | }, 27 | }); 28 | 29 | defineRule('doClick', { 30 | when: function () { 31 | return timers.clickTimer.firing; 32 | }, 33 | then: function () { 34 | dev.uchm121rx['Relay 0'] = !dev.uchm121rx['Relay 0']; 35 | }, 36 | }); 37 | 38 | defineRule('echo', { 39 | whenChanged: 'wb-w1/00042d40ffff', 40 | then: function (newValue, devName, cellName) { 41 | runShellCommand('echo {}/{} = {}'.format(devName, cellName, newValue), { 42 | captureOutput: true, 43 | exitCallback: function (exitCode, capturedOutput) { 44 | log('cmd output: ' + capturedOutput); 45 | }, 46 | }); 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /wbrules/eventbuffer.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | const ( 8 | EVENT_BUFFER_CAP = 16 9 | EVENT_OBSERVERS_CAP = 1 10 | ) 11 | 12 | type EventBuffer struct { 13 | sync.Mutex 14 | 15 | currentBuffer []*ControlChangeEvent 16 | observer chan struct{} 17 | } 18 | 19 | func NewEventBuffer() *EventBuffer { 20 | return &EventBuffer{ 21 | currentBuffer: make([]*ControlChangeEvent, 0, EVENT_BUFFER_CAP), 22 | observer: make(chan struct{}, 1), 23 | } 24 | } 25 | 26 | func (eb *EventBuffer) Observe() <-chan struct{} { 27 | return eb.observer 28 | } 29 | 30 | func (eb *EventBuffer) PushEvent(e *ControlChangeEvent) { 31 | eb.Lock() 32 | defer eb.Unlock() 33 | 34 | eb.currentBuffer = append(eb.currentBuffer, e) 35 | 36 | // try to notify user if he's not notified already 37 | select { 38 | case eb.observer <- struct{}{}: 39 | default: 40 | } 41 | } 42 | 43 | func (eb *EventBuffer) Retrieve() (e []*ControlChangeEvent) { 44 | eb.Lock() 45 | defer eb.Unlock() 46 | 47 | e = eb.currentBuffer 48 | eb.currentBuffer = make([]*ControlChangeEvent, 0, EVENT_BUFFER_CAP) 49 | return 50 | } 51 | 52 | func (eb *EventBuffer) length() int { 53 | eb.Lock() 54 | defer eb.Unlock() 55 | 56 | return len(eb.currentBuffer) 57 | } 58 | 59 | func (eb *EventBuffer) Close() { 60 | close(eb.observer) 61 | } 62 | -------------------------------------------------------------------------------- /wbrules/testrules_persistent_2.js: -------------------------------------------------------------------------------- 1 | defineRule('testPersistentGlobalRead', { 2 | whenChanged: ['vdev/read'], 3 | then: function () { 4 | var ps = new PersistentStorage('test_storage', { global: true }); 5 | 6 | log( 7 | 'read objects ' + 8 | JSON.stringify(ps['key1']) + 9 | ', ' + 10 | JSON.stringify(ps['key2']) + 11 | ', ' + 12 | JSON.stringify(ps['obj']) 13 | ); 14 | 15 | // modify subobject from ps 16 | var sub = ps.obj.sub; 17 | 18 | sub['hello'] = 'earth'; 19 | log( 20 | 'read objects ' + 21 | JSON.stringify(ps['key1']) + 22 | ', ' + 23 | JSON.stringify(ps['key2']) + 24 | ', ' + 25 | JSON.stringify(ps['obj']) 26 | ); 27 | }, 28 | }); 29 | 30 | defineRule('testPersistentLocalWrite', { 31 | whenChanged: 'vdev/localWrite2', 32 | then: function () { 33 | var ps = new PersistentStorage('test_local'); 34 | ps['key2'] = 'hello_from_2'; 35 | log('file2: write to local PS'); 36 | }, 37 | }); 38 | 39 | defineRule('testPersistentLocalRead', { 40 | whenChanged: 'vdev/localRead2', 41 | then: function () { 42 | var ps = new PersistentStorage('test_local'); 43 | log('file2: read objects ' + JSON.stringify(ps['key1']) + ', ' + JSON.stringify(ps['key2'])); 44 | }, 45 | }); 46 | 47 | log('loaded file 2'); 48 | -------------------------------------------------------------------------------- /wbrules/testrules_vcells_storage.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('test-vdev', { 2 | title: 'Test virtual device', 3 | 4 | cells: { 5 | cell1: { 6 | type: 'switch', 7 | value: false, 8 | }, 9 | cell2: { 10 | type: 'switch', 11 | value: true, 12 | }, 13 | cell3: { 14 | type: 'switch', 15 | value: false, 16 | forceDefault: true, 17 | }, 18 | cellText: { 19 | type: 'text', 20 | readonly: false, 21 | value: 'foo', 22 | }, 23 | }, 24 | }); 25 | 26 | defineVirtualDevice('test-trigger', { 27 | title: 'Trigger device', 28 | 29 | cells: { 30 | echo: { 31 | type: 'pushbutton', 32 | }, 33 | change1: { 34 | type: 'pushbutton', 35 | }, 36 | }, 37 | }); 38 | 39 | defineRule('testChange1', { 40 | whenChanged: ['test-trigger/change1'], 41 | then: function () { 42 | dev['test-vdev/cell1'] = true; 43 | dev['test-vdev/cell3'] = true; 44 | dev['test-vdev/cellText'] = 'bar'; 45 | }, 46 | }); 47 | 48 | defineRule('testEcho', { 49 | whenChanged: ['test-trigger/echo'], 50 | then: function () { 51 | log( 52 | 'vdev ' + 53 | dev['test-vdev/cell1'] + 54 | ', ' + 55 | dev['test-vdev/cell2'] + 56 | ', ' + 57 | dev['test-vdev/cell3'] + 58 | ', ' + 59 | dev['test-vdev/cellText'] 60 | ); 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /wbrules/locations.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | // LocItem represents a device or rule location in the source file 4 | type LocItem struct { 5 | Line int `json:"line"` 6 | Name string `json:"name"` 7 | } 8 | 9 | // LocFileEntry represents a source file 10 | type LocFileEntry struct { 11 | Enabled bool `json:"enabled"` 12 | Error *ScriptError `json:"error,omitempty"` 13 | VirtualPath string `json:"virtualPath"` 14 | Rules []LocItem `json:"rules"` 15 | Devices []LocItem `json:"devices"` 16 | Timers []LocItem `json:"timers"` 17 | 18 | PhysicalPath string `json:"-"` 19 | Context *ESContext `json:"-"` 20 | } 21 | 22 | // LocFileManager interface provides a way to access a list of source 23 | // files 24 | type LocFileManager interface { 25 | ScriptDir() string 26 | ListSourceFiles() ([]LocFileEntry, error) 27 | LiveWriteScript(virtualPath, content string) error 28 | } 29 | 30 | // ScriptError denotes an error that was caused by JavaScript code. 31 | // Files with such errors are partially loaded. 32 | type ScriptError struct { 33 | Message string `json:"message"` 34 | Traceback []LocItem `json:"traceback"` 35 | } 36 | 37 | func NewScriptError(message string, traceback []LocItem) ScriptError { 38 | return ScriptError{message, traceback} 39 | } 40 | 41 | func (err ScriptError) Error() string { 42 | return err.Message 43 | } 44 | -------------------------------------------------------------------------------- /wbrules/testrules_sms_commands.js: -------------------------------------------------------------------------------- 1 | var exitCodes = []; 2 | 3 | global.__proto__.runShellCommand = function (command, options) { 4 | if (exitCodes.length == 0) { 5 | exitCodes = [dev['test_sms/exit_code_2'], dev['test_sms/exit_code_1']]; 6 | } 7 | 8 | log('run command: {}', command); 9 | if (options.input) { 10 | log('input: {}', options.input); 11 | } 12 | if (options.exitCallback) { 13 | options.exitCallback(exitCodes.pop(), 'stdout', 'stderr'); 14 | } 15 | }; 16 | 17 | defineVirtualDevice('test_sms', { 18 | cells: { 19 | exit_code_1: { 20 | type: 'value', 21 | readonly: false, 22 | value: 0, 23 | }, 24 | exit_code_2: { 25 | type: 'value', 26 | readonly: false, 27 | value: 0, 28 | }, 29 | send: { 30 | type: 'pushbutton', 31 | }, 32 | send_quoted: { 33 | type: 'pushbutton', 34 | }, 35 | }, 36 | }); 37 | 38 | defineRule({ 39 | whenChanged: 'test_sms/send', 40 | then: function () { 41 | Notify.sendSMS('88005553535', 'test value'); 42 | }, 43 | }); 44 | 45 | defineRule({ 46 | whenChanged: 'test_sms/send_quoted', 47 | then: function () { 48 | // can't send messages with all types of quotes via mmcli, 49 | // see https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/275 50 | Notify.sendSMS('88005553535', 'test "value" \'single\''); 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /wbrules/testrules_command.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | defineRule('runCommand', { 4 | whenChanged: 'somedev/cmd', 5 | then: function (cmd, devName, cellName) { 6 | log('cmd: ' + cmd); 7 | if (dev.somedev.cmdNoCallback) { 8 | runShellCommand(cmd); 9 | log('(no callback)'); // make sure the rule didn't fail before here 10 | } else { 11 | runShellCommand(cmd, function (exitCode) { 12 | log('exit({}): {}', exitCode, cmd); 13 | }); 14 | } 15 | }, 16 | }); 17 | 18 | function displayOutput(prefix, out) { 19 | out.split('\n').forEach(function (line) { 20 | if (line) log(prefix + line); 21 | }); 22 | } 23 | 24 | defineRule('runCommandWithOutput', { 25 | whenChanged: 'somedev/cmdWithOutput', 26 | then: function (cmd, devName, cellName) { 27 | var options = { 28 | captureOutput: true, 29 | captureErrorOutput: true, 30 | exitCallback: function (exitCode, capturedOutput, capturedErrorOutput) { 31 | log('exit({}): {}', exitCode, cmd); 32 | displayOutput('output: ', capturedOutput); 33 | if (exitCode != 0) displayOutput('error: ', capturedErrorOutput); 34 | }, 35 | }; 36 | var p = cmd.indexOf('!'); 37 | if (p >= 0) { 38 | options.input = cmd.substring(0, p); 39 | cmd = cmd.substring(p + 1); 40 | } 41 | log('cmdWithOutput: ' + cmd); 42 | runShellCommand(cmd, options); 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /wbrules/rule_log_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "testing" 6 | ) 7 | 8 | type LogSuite struct { 9 | RuleSuiteBase 10 | } 11 | 12 | func (s *LogSuite) SetupTest() { 13 | s.SetupSkippingDefs("testrules_log.js") 14 | } 15 | 16 | func (s *LogSuite) TestLog() { 17 | s.publish("/devices/wbrules/controls/Rule debugging/on", "0", "wbrules/Rule debugging") 18 | s.Verify( 19 | "tst -> /devices/wbrules/controls/Rule debugging/on: [0] (QoS 1)", 20 | "driver -> /devices/wbrules/controls/Rule debugging: [0] (QoS 1, retained)", 21 | ) 22 | s.engine.EvalScript("testLog()") 23 | s.Verify( 24 | "[info] log()", 25 | "[info] log.info(42)", 26 | "[warning] log.warning(42)", 27 | "[error] log.error(42)", 28 | ) 29 | s.EnsureGotErrors() 30 | s.EnsureGotWarnings() 31 | s.publish("/devices/wbrules/controls/Rule debugging/on", "1", "wbrules/Rule debugging") 32 | s.Verify( 33 | "tst -> /devices/wbrules/controls/Rule debugging/on: [1] (QoS 1)", 34 | "driver -> /devices/wbrules/controls/Rule debugging: [1] (QoS 1, retained)", 35 | ) 36 | s.engine.EvalScript("testLog()") 37 | s.Verify( 38 | "[info] log()", 39 | "[debug] debug()", 40 | "[debug] log.debug(42)", 41 | "[info] log.info(42)", 42 | "[warning] log.warning(42)", 43 | "[error] log.error(42)", 44 | ) 45 | s.EnsureGotErrors() 46 | s.EnsureGotWarnings() 47 | } 48 | 49 | func TestLogSuite(t *testing.T) { 50 | testutils.RunSuites(t, 51 | new(LogSuite), 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /wbrules/testrules_loopback.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('loopback', { 2 | cells: { 3 | gauge: { 4 | type: 'value', 5 | value: 0, 6 | }, 7 | set_loud: { 8 | type: 'pushbutton', 9 | }, 10 | set_silent: { 11 | type: 'pushbutton', 12 | }, 13 | 14 | relay_main: { 15 | type: 'switch', 16 | value: false, 17 | }, 18 | relay_silent: { 19 | type: 'switch', 20 | value: false, 21 | }, 22 | }, 23 | }); 24 | 25 | defineRule({ 26 | whenChanged: 'loopback/gauge', 27 | then: function (newValue) { 28 | log('gauge set to ' + newValue); 29 | }, 30 | }); 31 | 32 | defineRule({ 33 | whenChanged: 'loopback/set_loud', 34 | then: function () { 35 | log('set_loud button pressed'); 36 | getControl('loopback/gauge').setValue(42); 37 | }, 38 | }); 39 | 40 | defineRule({ 41 | whenChanged: 'loopback/set_silent', 42 | then: function () { 43 | log('set_silent button pressed'); 44 | getControl('loopback/gauge').setValue({ 45 | value: 84, 46 | notify: false, 47 | }); 48 | }, 49 | }); 50 | 51 | defineRule({ 52 | whenChanged: 'loopback/relay_main', 53 | then: function (newValue) { 54 | log('relay_main: ' + newValue); 55 | }, 56 | }); 57 | 58 | defineRule({ 59 | whenChanged: 'loopback/relay_silent', 60 | then: function (newValue) { 61 | log('relay_silent: ' + newValue); 62 | getControl('loopback/relay_main').setValue({ 63 | value: newValue, 64 | notify: false, 65 | }); 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /wbrules/rule_cron_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "testing" 6 | ) 7 | 8 | type RuleCronSuite struct { 9 | RuleSuiteBase 10 | } 11 | 12 | func (s *RuleCronSuite) SetupTest() { 13 | s.SetupSkippingDefs("testrules_cron.js") 14 | } 15 | 16 | func (s *RuleCronSuite) TestCron() { 17 | s.WaitFor(func() bool { 18 | c := make(chan bool) 19 | s.engine.CallSync(func() { 20 | c <- s.cron != nil && s.cron.started 21 | }) 22 | return <-c 23 | }) 24 | 25 | s.cron.invokeEntries("@hourly") 26 | s.cron.invokeEntries("@hourly") 27 | s.cron.invokeEntries("@daily") 28 | s.cron.invokeEntries("@hourly") 29 | 30 | s.Verify( 31 | "[info] @hourly rule fired", 32 | "[info] @hourly rule fired", 33 | "[info] @daily rule fired", 34 | "[info] @hourly rule fired", 35 | ) 36 | 37 | // the new script contains rules with same names as in 38 | // testrules_cron.js that should override the previous rules 39 | s.ReplaceScript("testrules_cron.js", "testrules_cron_changed.js") 40 | s.Verify( 41 | "[changed] testrules_cron.js", 42 | ) 43 | 44 | s.cron.invokeEntries("@hourly") 45 | s.cron.invokeEntries("@hourly") 46 | s.cron.invokeEntries("@daily") 47 | s.cron.invokeEntries("@hourly") 48 | 49 | s.Verify( 50 | "[info] @hourly rule fired (new)", 51 | "[info] @hourly rule fired (new)", 52 | "[info] @daily rule fired (new)", 53 | "[info] @hourly rule fired (new)", 54 | ) 55 | } 56 | 57 | func TestRuleCronSuite(t *testing.T) { 58 | testutils.RunSuites(t, 59 | new(RuleCronSuite), 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /README-readonly.md: -------------------------------------------------------------------------------- 1 | # Флаг readonly/writable в wb-rules 2 | 3 | В версии 2.2 был введён новый флаг `writeable`, который предполагалось использовать для определения возможности редактировать значение контрола снаружи (веб-интерфейс, другие приложения, работающие на контроллере или на другом устройстве). Однако, использование этого флага в дополнение к readonly вызвало дополнительные сложности в понимании и организации логики работы приложений с такими контролами. 4 | 5 | В версии 2.3 решено было отказаться от двух флагов в пользу использования только флага `readonly`. Начиная с версии 2.3.0, при указании флага `writeable` выводится ошибка `writeable flag is deprecated, use readonly instead`, загрузка скрипта прекращается, правила не будут зарегистрированы, устройства не будут созданы. Для решения подобной проблемы необходимо убрать указание флага `writeable` из файлов сценариев и заменить флагом `readonly` равный `!writeable` 6 | 7 | Для примера, если использовалось определение виртуального устройства таким образом: 8 | ```javascript 9 | defineVirtualDevice("someDevice", { 10 | title: "wr-test", 11 | cells: { 12 | "textCell": { 13 | type: "text", 14 | value: "some text", 15 | writeable: true, 16 | }, 17 | } 18 | }) 19 | ``` 20 | 21 | Можно исправить следующим способом: 22 | ```javascript 23 | defineVirtualDevice("someDevice", { 24 | title: "wr-test", 25 | cells: { 26 | "textCell": { 27 | type: "text", 28 | value: "some text", 29 | readonly: false, 30 | }, 31 | } 32 | }) 33 | ``` 34 | -------------------------------------------------------------------------------- /wbrules/strings.go: -------------------------------------------------------------------------------- 1 | // Common string constants definitions 2 | 3 | package wbrules 4 | 5 | const ( 6 | // Virtual devices 7 | VDEV_DESCR_PROP_TITLE = "title" 8 | VDEV_DESCR_PROP_CELLS = "cells" 9 | VDEV_DESCR_PROP_CONTROLS = "controls" 10 | 11 | VDEV_CONTROL_DESCR_PROP_TYPE = "type" 12 | VDEV_CONTROL_DESCR_PROP_FORCEDEFAULT = "forceDefault" 13 | VDEV_CONTROL_DESCR_PROP_LAZYINIT = "lazyInit" 14 | VDEV_CONTROL_DESCR_PROP_VALUE = "value" 15 | VDEV_CONTROL_DESCR_PROP_READONLY = "readonly" 16 | VDEV_CONTROL_DESCR_PROP_WRITEABLE = "writeable" 17 | VDEV_CONTROL_DESCR_PROP_DESCRIPTION = "description" 18 | VDEV_CONTROL_DESCR_PROP_TITLE = "title" 19 | VDEV_CONTROL_DESCR_PROP_ORDER = "order" 20 | VDEV_CONTROL_DESCR_PROP_UNITS = "units" 21 | VDEV_CONTROL_DESCR_PROP_ENUM = "enum" 22 | // FIXME: deprecated 23 | VDEV_CONTROL_DESCR_PROP_MAX = "max" 24 | VDEV_CONTROL_DESCR_PROP_MIN = "min" 25 | VDEV_CONTROL_DESCR_PROP_PRECISION = "precision" 26 | 27 | // default value for 'readonly' 28 | VDEV_CONTROL_READONLY_DEFAULT = true 29 | 30 | // default 'max' value for 'range' type 31 | // FIXME: deprecated 32 | VDEV_CONTROL_RANGE_MAX_DEFAULT = 255.0 33 | VDEV_CONTROL_RANGE_MIN_DEFAULT = 0.0 34 | 35 | JS_DEVPROXY_FUNC_SETVALUE = "setValue" 36 | JS_DEVPROXY_FUNC_SETMETA = "setMeta" 37 | JS_DEVPROXY_FUNC_SETVALUE_KEY = "k" 38 | JS_DEVPROXY_FUNC_SETVALUE_ARG = "v" 39 | JS_DEVPROXY_FUNC_RAWVALUE = "rawValue" 40 | JS_DEVPROXY_FUNC_VALUE = "value" 41 | JS_DEVPROXY_FUNC_VALUE_RET = "v" 42 | JS_DEVPROXY_FUNC_ISCOMPLETE = "isComplete" 43 | JS_DEVPROXY_FUNC_GETMETA = "getMeta" 44 | 45 | JS_CTRLPROXY_FUNC_SETVALUE_VALUE = "value" 46 | JS_CTRLPROXY_FUNC_SETVALUE_NOTIFY = "notify" 47 | ) 48 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean 2 | 3 | PREFIX = /usr 4 | DEB_TARGET_ARCH ?= armhf 5 | WBGO_LOCAL_PATH ?= . 6 | 7 | ifeq ($(DEB_TARGET_ARCH),armhf) 8 | GO_ENV := GOARCH=arm GOARM=6 CC_FOR_TARGET=arm-linux-gnueabihf-gcc CC=$$CC_FOR_TARGET CGO_ENABLED=1 9 | endif 10 | ifeq ($(DEB_TARGET_ARCH),arm64) 11 | GO_ENV := GOARCH=arm64 CC_FOR_TARGET=aarch64-linux-gnu-gcc CC=$$CC_FOR_TARGET CGO_ENABLED=1 12 | endif 13 | ifeq ($(DEB_TARGET_ARCH),amd64) 14 | GO_ENV := GOARCH=amd64 15 | endif 16 | 17 | GO_ENV := GO111MODULE=on $(GO_ENV) 18 | 19 | GO ?= go 20 | GO_FLAGS = -ldflags "-s -w -X main.version=`git describe --tags --always --dirty`" 21 | 22 | all: clean wb-rules 23 | 24 | clean: 25 | rm -rf wb-rules 26 | 27 | amd64: 28 | $(MAKE) DEB_TARGET_ARCH=amd64 29 | 30 | test: 31 | cp $(WBGO_LOCAL_PATH)/amd64.wbgo.so wbrules/wbgo.so 32 | $(GO) test -v -trimpath -ldflags="-s -w" -cover ./wbrules 33 | 34 | wb-rules: main.go wbrules/*.go 35 | $(GO_ENV) $(GO) build -trimpath $(GO_FLAGS) 36 | 37 | install: 38 | mkdir -p $(DESTDIR)$(PREFIX)/share/wb-rules-modules/ $(DESTDIR)/etc/wb-rules-modules/ 39 | install -Dm0755 wb-rules -t $(DESTDIR)$(PREFIX)/bin 40 | install -Dm0644 rules/rules.js -t $(DESTDIR)/etc/wb-rules 41 | install -Dm0644 wb-rules.wbconfigs $(DESTDIR)/etc/wb-configs.d/13wb-rules 42 | 43 | install -Dm0644 scripts/lib.js -t $(DESTDIR)$(PREFIX)/share/wb-rules-system/scripts 44 | install -Dm0644 rules/load_alarms.js -t $(DESTDIR)$(PREFIX)/share/wb-rules 45 | install -Dm0644 $(WBGO_LOCAL_PATH)/$(DEB_TARGET_ARCH).wbgo.so $(DESTDIR)$(PREFIX)/lib/wb-rules/wbgo.so 46 | install -Dm0644 rules/alarms.conf -t $(DESTDIR)/etc/wb-rules 47 | install -Dm0644 rules/alarms.schema.json -t $(DESTDIR)$(PREFIX)/share/wb-mqtt-confed/schemas 48 | 49 | deb: 50 | $(GO_ENV) dpkg-buildpackage -b -a$(DEB_TARGET_ARCH) -us -uc 51 | -------------------------------------------------------------------------------- /wbrules/testrules_reload_2.js: -------------------------------------------------------------------------------- 1 | var devCells = { 2 | someCell: { 3 | type: 'switch', 4 | value: false, 5 | }, 6 | }; 7 | 8 | // removed after reload 9 | devCells.anotherCell = { 10 | type: 'range', 11 | max: 42, 12 | value: 10, 13 | }; 14 | 15 | defineAlias('smc', 'vdev/someCell'); 16 | 17 | defineVirtualDevice('vdev', { 18 | title: 'VDev', 19 | cells: devCells, 20 | }); 21 | 22 | // removed after reload 23 | defineVirtualDevice('vdev1', { 24 | title: 'VDev1', 25 | cells: { 26 | qqq: { 27 | type: 'switch', 28 | value: false, 29 | }, 30 | }, 31 | }); 32 | 33 | function cellSpec(devName, cellName) { 34 | return devName === undefined ? '(no cell)' : '{}/{}'.format(devName, cellName); 35 | } 36 | 37 | function defChangeRule(name, cell) { 38 | defineRule(name, { 39 | whenChanged: cell, 40 | then: function (newValue, devName, cellName) { 41 | log('{}: {}={}', name, cellSpec(devName, cellName), newValue); 42 | }, 43 | }); 44 | } 45 | 46 | function defDetectRun(name) { 47 | defineRule(name, { 48 | when: function () { 49 | return true; 50 | }, 51 | then: function (newValue, devName, cellName) { 52 | if (smc !== dev.vdev.someCell) throw new Error('cell alias value mismatch!'); 53 | log( 54 | '{}: {} (s={}{})', 55 | name, 56 | cellSpec(devName, cellName), 57 | dev.vdev.someCell, 58 | // doesn't log anotherCell value in the altered version 59 | ', a={}'.format(dev.vdev.anotherCell) 60 | ); 61 | }, 62 | }); 63 | } 64 | 65 | defDetectRun('detectRun'); 66 | defDetectRun('detectRun1'); // removed in the altered version 67 | defChangeRule('rule1', 'vdev/someCell'); 68 | defChangeRule('rule2', 'vdev/someCell'); // removed in the altered version 69 | defChangeRule('rule3', 'vdev/anotherCell'); // removed in the altered version 70 | 71 | global.__proto__.testrules_reload_2_n = 0; 72 | -------------------------------------------------------------------------------- /wbrules/scopedcleanup.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import "github.com/wirenboard/wbgong" 4 | 5 | const ( 6 | SCOPE_STACK_CAPACITY = 10 7 | CLEANUP_LIST_CAPACITY = 10 8 | ) 9 | 10 | type CleanupFunc func() 11 | type cleanupMap map[string][]CleanupFunc 12 | 13 | // ScopedCleanup manages a list of cleanup functions 14 | // that must be invoked when some named scope ceases 15 | // to exist. 16 | 17 | type ScopedCleanup struct { 18 | cleanupLists cleanupMap 19 | scopeStack []string 20 | } 21 | 22 | func MakeScopedCleanup() *ScopedCleanup { 23 | return &ScopedCleanup{ 24 | make(cleanupMap), 25 | make([]string, 0, SCOPE_STACK_CAPACITY), 26 | } 27 | } 28 | 29 | func (sc *ScopedCleanup) PushCleanupScope(scope string) string { 30 | if scope == "" { 31 | panic("trying to push an empty scope") 32 | } 33 | sc.scopeStack = append(sc.scopeStack, scope) 34 | return scope 35 | } 36 | 37 | func (sc *ScopedCleanup) PopCleanupScope(scope string) string { 38 | top := len(sc.scopeStack) - 1 39 | if top < 0 || sc.scopeStack[top] != scope { 40 | panic("scoped cleanup stack error") 41 | } 42 | sc.scopeStack = sc.scopeStack[:top] 43 | return scope 44 | } 45 | 46 | func (sc *ScopedCleanup) AddCleanup(cleanupFn CleanupFunc) { 47 | if len(sc.scopeStack) == 0 { 48 | wbgong.Debug.Printf("global scope, cleanup will not run") 49 | return 50 | } 51 | scope := sc.scopeStack[len(sc.scopeStack)-1] 52 | l, found := sc.cleanupLists[scope] 53 | if !found { 54 | l = make([]CleanupFunc, 0, CLEANUP_LIST_CAPACITY) 55 | } 56 | sc.cleanupLists[scope] = append(l, cleanupFn) 57 | } 58 | 59 | func (sc *ScopedCleanup) RunCleanups(scope string) { 60 | l, found := sc.cleanupLists[scope] 61 | if !found { 62 | return 63 | } 64 | defer delete(sc.cleanupLists, scope) 65 | for _, cleanupFn := range l { 66 | cleanupFn() 67 | } 68 | } 69 | 70 | func (sc *ScopedCleanup) RunAllCleanups() { 71 | for scope := range sc.cleanupLists { 72 | sc.RunCleanups(scope) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /wbrules/rule_notify_email_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/wirenboard/wbgong/testutils" 9 | ) 10 | 11 | type RuleNotifyEmailSuite struct { 12 | RuleSuiteBase 13 | } 14 | 15 | func (s *RuleNotifyEmailSuite) SetupTest() { 16 | s.SetupSkippingDefs("testrules_email_commands.js") 17 | } 18 | 19 | func (s *RuleNotifyEmailSuite) setErrorCode(errorCode int) { 20 | s.publish("/devices/test_email/controls/exit_code/on", strconv.Itoa(errorCode), 21 | "test_email/exit_code") 22 | s.VerifyUnordered( 23 | fmt.Sprintf("tst -> /devices/test_email/controls/exit_code/on: [%d] (QoS 1)", errorCode), 24 | fmt.Sprintf("driver -> /devices/test_email/controls/exit_code: [%d] (QoS 1, retained)", errorCode), 25 | ) 26 | } 27 | 28 | func (s *RuleNotifyEmailSuite) TestEmail() { 29 | s.setErrorCode(0) 30 | 31 | s.publish("/devices/test_email/controls/send/on", "1", "test_email/send") 32 | s.VerifyUnordered( 33 | "driver -> /devices/test_email/controls/send: [1] (QoS 1)", 34 | "tst -> /devices/test_email/controls/send/on: [1] (QoS 1)", 35 | "wbrules-log -> /wbrules/log/info: [sending email: Test subject] (QoS 1)", 36 | "wbrules-log -> /wbrules/log/info: [run command: /usr/sbin/sendmail -t] (QoS 1)", 37 | "wbrules-log -> /wbrules/log/info: [input: To: me@example.org\r\nSubject: =?utf-8?B?VGVzdCBzdWJqZWN0?=\r\nContent-Type: text/plain; charset=utf-8\n\nTest text] (QoS 1)", 38 | ) 39 | } 40 | 41 | func (s *RuleNotifyEmailSuite) TestEmailWithQuotes() { 42 | s.setErrorCode(0) 43 | 44 | s.publish("/devices/test_email/controls/send_quoted/on", "1", "test_email/send_quoted") 45 | s.VerifyUnordered( 46 | "driver -> /devices/test_email/controls/send_quoted: [1] (QoS 1)", 47 | "tst -> /devices/test_email/controls/send_quoted/on: [1] (QoS 1)", 48 | "wbrules-log -> /wbrules/log/info: [sending email: Test \"subject\" 'single'] (QoS 1)", 49 | "wbrules-log -> /wbrules/log/info: [run command: /usr/sbin/sendmail -t] (QoS 1)", 50 | "wbrules-log -> /wbrules/log/info: [input: To: me@example.org\r\nSubject: =?utf-8?B?VGVzdCAic3ViamVjdCIgJ3NpbmdsZSc=?=\r\nContent-Type: text/plain; charset=utf-8\n\nTest \"text\" 'single'] (QoS 1)", 51 | ) 52 | } 53 | 54 | func TestNotifyEmailSuite(t *testing.T) { 55 | testutils.RunSuites(t, 56 | new(RuleNotifyEmailSuite), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /wbrules/rule_controls_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleControlsSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleControlsSuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules_rule_controls.js") 15 | } 16 | 17 | func (s *RuleControlsSuite) TestTrigger() { 18 | s.publish("/devices/ctrltest/controls/trigger/on", "1", "ctrltest/trigger") 19 | 20 | s.Verify("tst -> /devices/ctrltest/controls/trigger/on: [1] (QoS 1)") 21 | s.VerifyUnordered( 22 | "driver -> /devices/ctrltest/controls/trigger: [1] (QoS 1)", 23 | "[info] controllable rule fired", 24 | ) 25 | } 26 | 27 | func (s *RuleControlsSuite) TestDisable() { 28 | s.publish("/devices/ctrltest/controls/disable/on", "1", "ctrltest/disable") 29 | 30 | s.Verify("tst -> /devices/ctrltest/controls/disable/on: [1] (QoS 1)") 31 | s.VerifyUnordered( 32 | "driver -> /devices/ctrltest/controls/disable: [1] (QoS 1)", 33 | "[info] disable", 34 | ) 35 | 36 | s.publish("/devices/ctrltest/controls/trigger/on", "1", "ctrltest/trigger") 37 | s.Verify( 38 | "tst -> /devices/ctrltest/controls/trigger/on: [1] (QoS 1)", 39 | "driver -> /devices/ctrltest/controls/trigger: [1] (QoS 1)", 40 | ) 41 | 42 | s.VerifyEmpty() 43 | 44 | s.publish("/devices/ctrltest/controls/enable/on", "1", "ctrltest/enable") 45 | 46 | s.Verify("tst -> /devices/ctrltest/controls/enable/on: [1] (QoS 1)") 47 | s.VerifyUnordered( 48 | "driver -> /devices/ctrltest/controls/enable: [1] (QoS 1)", 49 | "[info] enable", 50 | ) 51 | 52 | s.publish("/devices/ctrltest/controls/trigger/on", "1", "ctrltest/trigger") 53 | 54 | s.Verify("tst -> /devices/ctrltest/controls/trigger/on: [1] (QoS 1)") 55 | s.VerifyUnordered( 56 | "driver -> /devices/ctrltest/controls/trigger: [1] (QoS 1)", 57 | "[info] controllable rule fired", 58 | ) 59 | } 60 | 61 | func (s *RuleControlsSuite) TestRunRule() { 62 | s.publish("/devices/ctrltest/controls/run/on", "1", "ctrltest/run") 63 | 64 | s.Verify("tst -> /devices/ctrltest/controls/run/on: [1] (QoS 1)") 65 | s.VerifyUnordered( 66 | "driver -> /devices/ctrltest/controls/run: [1] (QoS 1)", 67 | "[info] run", 68 | "[info] controllable rule fired", 69 | ) 70 | } 71 | 72 | func TestRuleControls(t *testing.T) { 73 | testutils.RunSuites(t, new(RuleControlsSuite)) 74 | } 75 | -------------------------------------------------------------------------------- /wbrules/rule_notify_tg_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/wirenboard/wbgong/testutils" 9 | ) 10 | 11 | type RuleNotifyTgSuite struct { 12 | RuleSuiteBase 13 | } 14 | 15 | func (s *RuleNotifyTgSuite) SetupTest() { 16 | s.SetupSkippingDefs("testrules_tg_commands.js") 17 | } 18 | 19 | func (s *RuleNotifyTgSuite) setErrorCode(errorCode int) { 20 | s.publish("/devices/test_tg/controls/exit_code/on", strconv.Itoa(errorCode), 21 | "test_tg/exit_code") 22 | s.VerifyUnordered( 23 | fmt.Sprintf("tst -> /devices/test_tg/controls/exit_code/on: [%d] (QoS 1)", errorCode), 24 | fmt.Sprintf("driver -> /devices/test_tg/controls/exit_code: [%d] (QoS 1, retained)", errorCode), 25 | ) 26 | } 27 | 28 | func (s *RuleNotifyTgSuite) TestTg() { 29 | s.setErrorCode(0) 30 | 31 | s.publish("/devices/test_tg/controls/send/on", "1", "test_tg/send") 32 | s.VerifyUnordered( 33 | "driver -> /devices/test_tg/controls/send: [1] (QoS 1)", 34 | "tst -> /devices/test_tg/controls/send/on: [1] (QoS 1)", 35 | "wbrules-log -> /wbrules/log/info: [sending telegram message: Test message] (QoS 1)", 36 | "wbrules-log -> /wbrules/log/info: [run command: curl -s -X POST https://api.telegram.org/bot1234567890:abcdefghijklmnopqrstuvwxyz123456789/sendMessage -H 'Content-Type: application/x-www-form-urlencoded' -d @-] (QoS 1)", 37 | "wbrules-log -> /wbrules/log/info: [input: chat_id=12345678&text=Test%20message] (QoS 1)", 38 | ) 39 | } 40 | 41 | func (s *RuleNotifyTgSuite) TestTgWithQuotes() { 42 | s.setErrorCode(0) 43 | 44 | s.publish("/devices/test_tg/controls/send_quoted/on", "1", "test_tg/send_quoted") 45 | s.VerifyUnordered( 46 | "driver -> /devices/test_tg/controls/send_quoted: [1] (QoS 1)", 47 | "tst -> /devices/test_tg/controls/send_quoted/on: [1] (QoS 1)", 48 | "wbrules-log -> /wbrules/log/info: [sending telegram message: Test \"message\" 'single'] (QoS 1)", 49 | "wbrules-log -> /wbrules/log/info: [run command: curl -s -X POST https://api.telegram.org/bot1234567890:abcdefghijklmnopqrstuvwxyz123456789/sendMessage -H 'Content-Type: application/x-www-form-urlencoded' -d @-] (QoS 1)", 50 | "wbrules-log -> /wbrules/log/info: [input: chat_id=12345678&text=Test%20%22message%22%20'single'] (QoS 1)", 51 | ) 52 | } 53 | 54 | func TestNotifyTgSuite(t *testing.T) { 55 | testutils.RunSuites(t, 56 | new(RuleNotifyTgSuite), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /wbrules/vcells_storage_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/wirenboard/wbgong" 8 | "github.com/wirenboard/wbgong/testutils" 9 | ) 10 | 11 | type VirtualCellsStorageSuite struct { 12 | RuleSuiteBase 13 | tmpDir string 14 | } 15 | 16 | func (s *VirtualCellsStorageSuite) SetupFixture() { 17 | var err error 18 | s.tmpDir, err = os.MkdirTemp("", "wbrulestest") 19 | if err != nil { 20 | s.FailNow("can't create temp directory") 21 | } 22 | wbgong.Debug.Printf("created temp dir %s", s.tmpDir) 23 | } 24 | 25 | func (s *VirtualCellsStorageSuite) TearDownFixture() { 26 | os.RemoveAll(s.tmpDir) 27 | } 28 | 29 | func (s *VirtualCellsStorageSuite) SetupTest() { 30 | s.VdevStorageFile = s.tmpDir + "/test-vcells.db" 31 | s.SetupSkippingDefs("testrules_vcells_storage.js") 32 | } 33 | 34 | func (s *VirtualCellsStorageSuite) TearDownTest() { 35 | s.RuleSuiteBase.TearDownTest() 36 | } 37 | 38 | func (s *VirtualCellsStorageSuite) TestStorage1() { 39 | // s.publish("/devices/test-trigger/controls/echo/on", "1", "test-trigger/echo") 40 | // s.Verify( 41 | // "tst -> /devices/test-trigger/controls/echo/on: [1] (QoS 1)", 42 | // "driver -> /devices/test-trigger/controls/echo: [1] (QoS 1)", 43 | // "[info] vdev false, true, false, foo", 44 | // ) 45 | // s.VerifyEmpty() 46 | 47 | s.publish("/devices/test-trigger/controls/change1/on", "1", "test-trigger/change1", 48 | "test-vdev/cell1", "test-vdev/cell3", "test-vdev/cellText") 49 | s.Verify( 50 | "tst -> /devices/test-trigger/controls/change1/on: [1] (QoS 1)", 51 | "driver -> /devices/test-trigger/controls/change1: [1] (QoS 1)", 52 | "driver -> /devices/test-vdev/controls/cell1: [1] (QoS 1, retained)", 53 | "driver -> /devices/test-vdev/controls/cell3: [1] (QoS 1, retained)", 54 | "driver -> /devices/test-vdev/controls/cellText: [bar] (QoS 1, retained)", 55 | ) 56 | s.VerifyEmpty() 57 | } 58 | 59 | func (s *VirtualCellsStorageSuite) TestStorage2() { 60 | s.publish("/devices/test-trigger/controls/echo/on", "1", "test-trigger/echo") 61 | s.Verify( 62 | "tst -> /devices/test-trigger/controls/echo/on: [1] (QoS 1)", 63 | "driver -> /devices/test-trigger/controls/echo: [1] (QoS 1)", 64 | "[info] vdev true, true, false, bar", 65 | ) 66 | s.VerifyEmpty() 67 | } 68 | 69 | func TestVirtualCellsStorageSuite(t *testing.T) { 70 | s := new(VirtualCellsStorageSuite) 71 | s.SetupFixture() 72 | defer s.TearDownFixture() 73 | testutils.RunSuites(t, s) 74 | } 75 | -------------------------------------------------------------------------------- /wbrules/testrules_modules.js: -------------------------------------------------------------------------------- 1 | defineVirtualDevice('test', { 2 | cells: { 3 | helloworld: { 4 | type: 'switch', 5 | value: false, 6 | }, 7 | multifile: { 8 | type: 'switch', 9 | value: false, 10 | }, 11 | error: { 12 | type: 'switch', 13 | value: false, 14 | }, 15 | cross: { 16 | type: 'switch', 17 | value: false, 18 | }, 19 | params: { 20 | type: 'switch', 21 | value: false, 22 | }, 23 | static: { 24 | type: 'switch', 25 | value: false, 26 | }, 27 | cache: { 28 | type: 'switch', 29 | value: false, 30 | }, 31 | }, 32 | }); 33 | 34 | defineRule('helloworld', { 35 | whenChanged: 'test/helloworld', 36 | then: function () { 37 | var m = require('test/helloworld'); 38 | // var m = {hello: 42}; 39 | log('Required module value:', m.hello); 40 | log('Function test:', m.adder(10, 20)); 41 | }, 42 | }); 43 | 44 | defineRule('error', { 45 | whenChanged: 'test/error', 46 | then: function () { 47 | try { 48 | var m = require('notfound'); 49 | log('ERROR: Found non-existing module'); 50 | } catch (e) { 51 | log('Module not found'); 52 | } 53 | }, 54 | }); 55 | 56 | defineRule('multiple_require', { 57 | whenChanged: 'test/multifile', 58 | then: function () { 59 | var m = require('test/multi_init'); 60 | log('[1] My value of multi_init:', m.value); 61 | }, 62 | }); 63 | 64 | defineRule('cross-dep', { 65 | whenChanged: 'test/cross', 66 | then: function () { 67 | require('test/with_require'); 68 | log('Module loaded'); 69 | }, 70 | }); 71 | 72 | defineRule('params', { 73 | whenChanged: 'test/params', 74 | then: function () { 75 | var m = require('test/params'); 76 | log(m.params()); 77 | }, 78 | }); 79 | 80 | defineRule('static', { 81 | whenChanged: 'test/static', 82 | then: function () { 83 | var m = require('test/static'); 84 | m.count(); 85 | }, 86 | }); 87 | 88 | defineRule('cache1', { 89 | whenChanged: 'test/cache', 90 | then: function () { 91 | var m = require('test/helloworld'); 92 | log('Value: {}', m.hello); 93 | }, 94 | }); 95 | 96 | defineRule('cache2', { 97 | whenChanged: 'test/cache', 98 | then: function () { 99 | var m = require('test/helloworld'); 100 | log('Value: {}', m.hello); 101 | }, 102 | }); 103 | -------------------------------------------------------------------------------- /wbrules/spawn.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "sync" 10 | "syscall" 11 | 12 | wbgong "github.com/wirenboard/wbgong" 13 | ) 14 | 15 | type CommandResult struct { 16 | ExitStatus int 17 | CapturedOutput string 18 | CapturedErrorOutput string 19 | } 20 | 21 | func captureCommandOutput(pipe io.ReadCloser, wg *sync.WaitGroup, result *string, e *error) { 22 | wg.Add(1) 23 | go func() { 24 | var buf bytes.Buffer 25 | if _, err := io.Copy(&buf, pipe); err == nil { 26 | *result = string(buf.Bytes()) 27 | } else { 28 | *e = err 29 | } 30 | wg.Done() 31 | }() 32 | } 33 | 34 | func Spawn(name string, args []string, captureOutput bool, captureErrorOutput bool, input *string) (*CommandResult, error) { 35 | r := &CommandResult{0, "", ""} 36 | var err error 37 | var stdinPipe io.WriteCloser 38 | var stdoutPipe io.ReadCloser 39 | var stderrPipe io.ReadCloser 40 | cmd := exec.Command(name, args...) 41 | if input != nil { 42 | if stdinPipe, err = cmd.StdinPipe(); err != nil { 43 | return nil, fmt.Errorf("cmd.StdinPipe() failed: %s", err) 44 | } 45 | } 46 | if captureOutput { 47 | if stdoutPipe, err = cmd.StdoutPipe(); err != nil { 48 | return nil, fmt.Errorf("cmd.StdoutPipe() failed: %s", err) 49 | } 50 | } 51 | if captureErrorOutput { 52 | if stderrPipe, err = cmd.StderrPipe(); err != nil { 53 | return nil, fmt.Errorf("cmd.StderrPipe() failed: %s", err) 54 | } 55 | } else { 56 | cmd.Stderr = os.Stderr 57 | } 58 | 59 | if err = cmd.Start(); err != nil { 60 | return nil, fmt.Errorf("cmd.Start() failed: %s", err) 61 | } 62 | 63 | if stdinPipe != nil || stdoutPipe != nil || stderrPipe != nil { 64 | var wg sync.WaitGroup 65 | if stdinPipe != nil { 66 | wg.Add(1) 67 | go func() { 68 | io.WriteString(stdinPipe, *input) 69 | stdinPipe.Close() 70 | wg.Done() 71 | }() 72 | } 73 | if stderrPipe != nil { 74 | captureCommandOutput(stderrPipe, &wg, &r.CapturedErrorOutput, &err) 75 | } 76 | if stdoutPipe != nil { 77 | captureCommandOutput(stdoutPipe, &wg, &r.CapturedOutput, &err) 78 | } 79 | wg.Wait() 80 | if err != nil { 81 | return nil, fmt.Errorf("error capturing output: %s", err) 82 | } 83 | } 84 | 85 | if err = cmd.Wait(); err != nil { 86 | if exitErr, ok := err.(*exec.ExitError); ok { 87 | r.ExitStatus = exitErr.Sys().(syscall.WaitStatus).ExitStatus() 88 | wbgong.Debug.Printf("command '%s': error: exit status: %d", cmd.Path, r.ExitStatus) 89 | } else { 90 | return nil, err 91 | } 92 | } 93 | 94 | return r, nil 95 | } 96 | -------------------------------------------------------------------------------- /wbrules/rule_cell_change_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleCellChangesSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleCellChangesSuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules_cellchanges.js") 15 | } 16 | 17 | func (s *RuleCellChangesSuite) TestAssigningSameValueToACellSeveralTimes() { 18 | // There was a problem with 'whenChanged' rules being marked as 19 | // 'rules without cells' which was negatively affecting performance 20 | // (related to SOFT-181). 21 | // The engine prints warnings if a rule gets marked as cell-less, 22 | // but only in case if debugging is enabled, as not to pollute 23 | // logs with too much warnings. 24 | // wbgong.SetDebuggingEnabled(true) 25 | // We don't want to skew other test resuls becuse Engine 26 | // initializes its MQTT debug flag fron wbgong debug flag 27 | // defer wbgong.SetDebuggingEnabled(false) 28 | 29 | s.publish("/devices/cellch/controls/button/on", "1", 30 | "cellch/button", "cellch/sw", "cellch/misc") 31 | s.VerifyUnordered( 32 | "tst -> /devices/cellch/controls/button/on: [1] (QoS 1)", 33 | "driver -> /devices/cellch/controls/button: [1] (QoS 1)", // no 'retained' flag for button 34 | "driver -> /devices/cellch/controls/sw: [1] (QoS 1, retained)", 35 | "driver -> /devices/cellch/controls/misc: [1] (QoS 1, retained)", 36 | "[info] startCellChange: sw <- true", 37 | "[info] switchChanged: sw=true", 38 | ) 39 | s.publish("/devices/somedev/controls/sw", "1", "somedev/sw") 40 | s.VerifyUnordered( 41 | "tst -> /devices/somedev/controls/sw: [1] (QoS 1, retained)", 42 | "driver -> /devices/somedev/controls/sw/on: [1] (QoS 1)", 43 | ) 44 | 45 | s.publish("/devices/cellch/controls/button/on", "1", 46 | "cellch/button", "cellch/sw", "cellch/misc") 47 | s.VerifyUnordered( 48 | "tst -> /devices/cellch/controls/button/on: [1] (QoS 1)", 49 | "driver -> /devices/cellch/controls/button: [1] (QoS 1)", // no 'retained' flag for button 50 | "driver -> /devices/cellch/controls/sw: [0] (QoS 1, retained)", 51 | "driver -> /devices/cellch/controls/misc: [1] (QoS 1, retained)", 52 | "[info] startCellChange: sw <- false", 53 | "[info] switchChanged: sw=false", 54 | ) 55 | 56 | s.publish("/devices/somedev/controls/sw", "1", "somedev/sw") 57 | s.VerifyUnordered( 58 | "tst -> /devices/somedev/controls/sw: [1] (QoS 1, retained)", 59 | "driver -> /devices/somedev/controls/sw/on: [1] (QoS 1)", 60 | ) 61 | // SOFT-181, see comment at the beginning of this test 62 | s.EnsureNoErrorsOrWarnings() 63 | } 64 | 65 | func TestRuleCellChangesSuite(t *testing.T) { 66 | testutils.RunSuites(t, 67 | new(RuleCellChangesSuite), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /wbrules/rule_track_mqtt_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleTrackMqttSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleTrackMqttSuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules_track_mqtt.js") 15 | } 16 | 17 | // TestTracker tests js which contains tracking like this: 18 | // 19 | // trackMqtt("/wierd/sub/some", ... 20 | // trackMqtt("/wierd/+/some", ... 21 | // trackMqtt("/wierd/+/another", ... 22 | // trackMqtt("/wierd/#", ... 23 | func (s *RuleTrackMqttSuite) TestTracker() { 24 | s.publish("/wierd/sub/some", "some-value") 25 | s.VerifyUnordered( 26 | "tst -> /wierd/sub/some: [some-value] (QoS 1, retained)", 27 | "wbrules-log -> /wbrules/log/info: [1. wierd topic got value] (QoS 1)", 28 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub/some, value: some-value] (QoS 1)", 29 | "wbrules-log -> /wbrules/log/info: [2. wierd topic got value] (QoS 1)", 30 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub/some, value: some-value] (QoS 1)", 31 | "wbrules-log -> /wbrules/log/info: [4. wierd topic got value] (QoS 1)", 32 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub/some, value: some-value] (QoS 1)", 33 | ) 34 | 35 | s.publish("/wierd/sub2/some", "some-value") 36 | s.VerifyUnordered( 37 | "tst -> /wierd/sub2/some: [some-value] (QoS 1, retained)", 38 | "wbrules-log -> /wbrules/log/info: [2. wierd topic got value] (QoS 1)", 39 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub2/some, value: some-value] (QoS 1)", 40 | "wbrules-log -> /wbrules/log/info: [4. wierd topic got value] (QoS 1)", 41 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub2/some, value: some-value] (QoS 1)", 42 | ) 43 | 44 | s.publish("/wierd/sub3/another", "another-value") 45 | s.VerifyUnordered( 46 | "tst -> /wierd/sub3/another: [another-value] (QoS 1, retained)", 47 | "wbrules-log -> /wbrules/log/info: [3. wierd topic got value] (QoS 1)", 48 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub3/another, value: another-value] (QoS 1)", 49 | "wbrules-log -> /wbrules/log/info: [4. wierd topic got value] (QoS 1)", 50 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/sub3/another, value: another-value] (QoS 1)", 51 | ) 52 | 53 | s.publish("/wierd/different/long/topic", "random-value") 54 | s.VerifyUnordered( 55 | "tst -> /wierd/different/long/topic: [random-value] (QoS 1, retained)", 56 | "wbrules-log -> /wbrules/log/info: [4. wierd topic got value] (QoS 1)", 57 | "wbrules-log -> /wbrules/log/info: [topic: /wierd/different/long/topic, value: random-value] (QoS 1)", 58 | ) 59 | 60 | s.VerifyEmpty() 61 | } 62 | 63 | func TestTrackMqtt(t *testing.T) { 64 | testutils.RunSuites(t, new(RuleTrackMqttSuite)) 65 | } 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The WB License (MIT-WB) 2 | 3 | Copyright (c) 2013-2023 Contactless Devices, LLC (Wiren Board) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | The Software shall only be used on Wiren Board controllers, that is, hardware 16 | manufactured by Contactless Devices, LLC or its affilates. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | Copyright (c) 2013-2023 ООО Бесконтактные устройства 27 | 28 | Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации (в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно использовать Программное Обеспечение без ограничений, включая неограниченное право на использование, копирование, изменение, слияние, публикацию, распространение, сублицензирование и/или продажу копий Программного Обеспечения, а также лицам, которым предоставляется данное Программное Обеспечение, при соблюдении следующих условий: 29 | 30 | Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части данного Программного Обеспечения. 31 | 32 | Программное Обеспечение должно использоваться только на контроллерах Wiren Board, т.е. 33 | на оборудовании, произведённом компанией ООО Бесконтактные устройства или уполномоченными 34 | компанией ООО Бесконтактные устройства лицами. 35 | 36 | ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. 37 | -------------------------------------------------------------------------------- /wbrules/testrules_persistent.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | defineVirtualDevice('vdev', { 4 | cells: { 5 | write: { 6 | type: 'switch', 7 | value: false, 8 | forceDefault: true, 9 | }, 10 | read: { 11 | type: 'switch', 12 | value: false, 13 | forceDefault: true, 14 | }, 15 | localWrite1: { 16 | type: 'switch', 17 | value: false, 18 | forceDefault: true, 19 | }, 20 | localRead1: { 21 | type: 'switch', 22 | value: false, 23 | forceDefault: true, 24 | }, 25 | localWrite2: { 26 | type: 'switch', 27 | value: false, 28 | forceDefault: true, 29 | }, 30 | localRead2: { 31 | type: 'switch', 32 | value: false, 33 | forceDefault: true, 34 | }, 35 | }, 36 | }); 37 | 38 | defineRule('testPersistentGlobalWrite', { 39 | whenChanged: ['vdev/write'], 40 | then: function () { 41 | var ps = new PersistentStorage('test_storage', { global: true }); 42 | 43 | // try to write a pure object to persistent storage - must get an error 44 | try { 45 | ps['pure'] = { name: 'pure object', foo: 'baz' }; 46 | log('pure object created successfully!'); 47 | } catch (e) { 48 | log('pure object is not created'); 49 | } 50 | 51 | var obj = StorableObject({ name: 'MyObj', foo: 'bar', baz: 126 }); 52 | 53 | ps['key1'] = 42; 54 | ps['key2'] = 'HelloWorld'; 55 | ps['obj'] = obj; 56 | 57 | // post-write value to object 58 | obj.baz = 84; 59 | 60 | // create subobject for object 61 | // also try to create a pure object 62 | try { 63 | obj.pure_sub = { name: 'another pure' }; 64 | log('pure subobject created successfully!'); 65 | } catch (e) { 66 | log('pure subobject is not created'); 67 | } 68 | 69 | // create a correct subobject 70 | obj.sub = StorableObject({ 71 | hello: 'world', 72 | }); 73 | 74 | log( 75 | 'write objects ' + 76 | JSON.stringify(ps['key1']) + 77 | ', ' + 78 | JSON.stringify(ps['key2']) + 79 | ', ' + 80 | JSON.stringify(ps['obj']) 81 | ); 82 | }, 83 | }); 84 | 85 | defineRule('testPersistentLocalWrite', { 86 | whenChanged: 'vdev/localWrite1', 87 | then: function () { 88 | var ps = new PersistentStorage('test_local'); 89 | ps['key1'] = 'hello_from_1'; 90 | log('file1: write to local PS'); 91 | }, 92 | }); 93 | 94 | defineRule('testPersistentLocalRead', { 95 | whenChanged: 'vdev/localRead1', 96 | then: function () { 97 | var ps = new PersistentStorage('test_local'); 98 | log('file1: read objects ' + JSON.stringify(ps['key1']) + ', ' + JSON.stringify(ps['key2'])); 99 | }, 100 | }); 101 | 102 | log('loaded file 1'); 103 | -------------------------------------------------------------------------------- /wbrules/rule_read_config_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/wirenboard/wbgong/testutils" 10 | ) 11 | 12 | type RuleReadConfigSuite struct { 13 | RuleSuiteBase 14 | configDir string 15 | cleanup func() 16 | } 17 | 18 | func (s *RuleReadConfigSuite) SetupTest() { 19 | s.SetupSkippingDefs("testrules_read_config.js") 20 | s.configDir, s.cleanup = testutils.SetupTempDir(s.T()) 21 | s.publish("/devices/somedev/controls/readSampleConfig/meta/type", "text", "somedev/readSampleConfig") 22 | s.Verify("tst -> /devices/somedev/controls/readSampleConfig/meta/type: [text] (QoS 1, retained)") 23 | } 24 | 25 | func (s *RuleReadConfigSuite) TearDownTest() { 26 | if s.cleanup != nil { 27 | s.cleanup() 28 | } 29 | s.RuleSuiteBase.TearDownTest() 30 | } 31 | 32 | func (s *RuleReadConfigSuite) WriteConfig(filename, text string) (configPath string) { 33 | configPath = filepath.Join(s.configDir, "conf.json") 34 | // note that this is JSON config which supports comments, not just json 35 | os.WriteFile(configPath, []byte(text), 0777) 36 | return 37 | } 38 | 39 | func (s *RuleReadConfigSuite) TryReadingConfig(configPath string) { 40 | s.publish("/devices/somedev/controls/readSampleConfig", configPath, "somedev/readSampleConfig") 41 | } 42 | 43 | func (s *RuleReadConfigSuite) verifyReadConfRuleLog(configPath string, msgs ...interface{}) { 44 | msgs = append([]interface{}{ 45 | fmt.Sprintf( 46 | "tst -> /devices/somedev/controls/readSampleConfig: [%s] (QoS 1, retained)", 47 | configPath), 48 | }, msgs...) 49 | s.Verify(msgs...) 50 | } 51 | 52 | func (s *RuleReadConfigSuite) TestReadConfig() { 53 | configPath := s.WriteConfig("conf.json", "{ // whatever! \n/*\nmultiline\ncomment!\n*/\n\"xyz\": 42 }") 54 | s.publish("/devices/somedev/controls/readSampleConfig", "initial_text", "somedev/readSampleConfig") 55 | s.TryReadingConfig(configPath) 56 | s.Verify("tst -> /devices/somedev/controls/readSampleConfig: [initial_text] (QoS 1, retained)") 57 | s.verifyReadConfRuleLog(configPath, "[info] config: {\"xyz\":42}") 58 | } 59 | 60 | func (s *RuleReadConfigSuite) TestReadConfigErrors() { 61 | configPath := filepath.Join(s.configDir, "nosuchconf.json") 62 | s.publish("/devices/somedev/controls/readSampleConfig", "initial_text", "somedev/readSampleConfig") 63 | s.TryReadingConfig(configPath) 64 | s.Verify("tst -> /devices/somedev/controls/readSampleConfig: [initial_text] (QoS 1, retained)") 65 | s.verifyReadConfRuleLog( 66 | configPath, 67 | fmt.Sprintf("[error] failed to open config file: %s", configPath), 68 | "[error] readConfig error!") 69 | s.EnsureGotErrors() 70 | 71 | configPath = s.WriteConfig("badconf.json", "{") 72 | s.TryReadingConfig(configPath) 73 | s.verifyReadConfRuleLog( 74 | configPath, 75 | fmt.Sprintf("[error] failed to parse json: %s", configPath), 76 | "[error] readConfig error!") 77 | s.EnsureGotErrors() 78 | } 79 | 80 | func TestRuleReadConfigSuite(t *testing.T) { 81 | testutils.RunSuites(t, 82 | new(RuleReadConfigSuite), 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /wbrules/testrules_controls_api.js: -------------------------------------------------------------------------------- 1 | var ctrlID = 'wrCtrlID'; 2 | 3 | defineRule({ 4 | whenChanged: ['spawner/spawn'], 5 | then: function (newValue) { 6 | getDevice('spawner') 7 | .controlsList() 8 | .forEach(function (ctrl) { 9 | log('ctrlID: {}, error: {}'.format(ctrl.getId(), ctrl.getError())); 10 | }); 11 | if (getDevice('spawner').isControlExists(ctrlID)) { 12 | getDevice('spawner').removeControl(ctrlID); 13 | } else { 14 | var newControl = { type: 'text', value: 'test-text', readonly: false }; 15 | getDevice('spawner').addControl(ctrlID, newControl); 16 | } 17 | }, 18 | }); 19 | 20 | defineRule({ 21 | whenChanged: ['spawner/check'], 22 | then: function (newValue) { 23 | log('ctrlID: somedev, isVirtual: {}'.format(getDevice('somedev').isVirtual())); 24 | log('ctrlID: spawner, isVirtual: {}'.format(getDevice('spawner').isVirtual())); 25 | 26 | getDevice('spawner') 27 | .controlsList() 28 | .forEach(function (ctrl) { 29 | if (ctrl.getId() === ctrlID) { 30 | log('ctrlID: {}, title: {}'.format(ctrl.getId(), ctrl.getTitle())); 31 | log('ctrlID: {}, error: {}'.format(ctrl.getId(), ctrl.getError())); 32 | log('ctrlID: {}, type: {}'.format(ctrl.getId(), ctrl.getType())); 33 | log('ctrlID: {}, order: {}'.format(ctrl.getId(), ctrl.getOrder())); 34 | log('ctrlID: {}, max: {}'.format(ctrl.getId(), ctrl.getMax())); 35 | log('ctrlID: {}, min: {}'.format(ctrl.getId(), ctrl.getMin())); 36 | log('ctrlID: {}, readonly: {}'.format(ctrl.getId(), ctrl.getReadonly())); 37 | log('ctrlID: {}, units: {}'.format(ctrl.getId(), ctrl.getUnits())); 38 | log('ctrlID: {}, value: {}'.format(ctrl.getId(), ctrl.getValue())); 39 | } 40 | }); 41 | }, 42 | }); 43 | 44 | defineRule({ 45 | whenChanged: ['spawner/change'], 46 | then: function (newValue) { 47 | ctrl = getDevice('spawner').getControl(ctrlID); 48 | if (newValue) { 49 | ctrl.setDescription('true Description'); 50 | ctrl.setTitle('newTitle'); 51 | ctrl.setType('range'); 52 | ctrl.setOrder(5); 53 | ctrl.setMax(255); 54 | ctrl.setMin(5); 55 | ctrl.setReadonly(true); 56 | ctrl.setUnits('meters'); 57 | ctrl.setValue(42); 58 | ctrl.setError('new Error'); 59 | } else { 60 | ctrl.setDescription('new Description'); 61 | ctrl.setTitle('oldTitle'); 62 | ctrl.setError(''); 63 | ctrl.setType('text'); 64 | ctrl.setOrder(4); 65 | ctrl.setMax(0); 66 | ctrl.setMin(0); 67 | ctrl.setReadonly(false); 68 | ctrl.setUnits('chars'); 69 | } 70 | }, 71 | }); 72 | 73 | defineVirtualDevice('spawner', { 74 | title: 'spawner', 75 | cells: { 76 | spawn: { 77 | type: 'switch', 78 | value: false, 79 | readonly: false, 80 | }, 81 | check: { 82 | type: 'switch', 83 | value: false, 84 | readonly: false, 85 | }, 86 | change: { 87 | type: 'switch', 88 | value: false, 89 | readonly: false, 90 | }, 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /wbrules/rule_loopback_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleLoopbackSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleLoopbackSuite) SetupTest() { 14 | s.RuleSuiteBase.SetupSkippingDefs("testrules_loopback.js") 15 | } 16 | 17 | func (s *RuleLoopbackSuite) TestSetGaugeLoud() { 18 | s.publish("/devices/loopback/controls/set_loud/on", "1", "loopback/set_loud", "loopback/gauge") 19 | s.VerifyUnordered( 20 | "tst -> /devices/loopback/controls/set_loud/on: [1] (QoS 1)", 21 | "driver -> /devices/loopback/controls/set_loud: [1] (QoS 1)", // note there's no 'retained' flag 22 | "[info] set_loud button pressed", 23 | "driver -> /devices/loopback/controls/gauge: [42] (QoS 1, retained)", 24 | "[info] gauge set to 42", 25 | ) 26 | } 27 | 28 | func (s *RuleLoopbackSuite) TestSetGaugeSilent() { 29 | s.publish("/devices/loopback/controls/set_silent/on", "1", "loopback/set_silent") // no event for loopback/gauge here 30 | s.VerifyUnordered( 31 | "tst -> /devices/loopback/controls/set_silent/on: [1] (QoS 1)", 32 | "driver -> /devices/loopback/controls/set_silent: [1] (QoS 1)", // note there's no 'retained' flag 33 | "[info] set_silent button pressed", 34 | "driver -> /devices/loopback/controls/gauge: [84] (QoS 1, retained)", 35 | ) 36 | s.VerifyEmpty() // no log entry from gauge rule 37 | } 38 | 39 | func (s *RuleLoopbackSuite) TestStateSync() { 40 | // turn on as usual 41 | s.publish("/devices/loopback/controls/relay_main/on", "1", "loopback/relay_main") 42 | s.VerifyUnordered( 43 | "tst -> /devices/loopback/controls/relay_main/on: [1] (QoS 1)", 44 | "driver -> /devices/loopback/controls/relay_main: [1] (QoS 1, retained)", 45 | "[info] relay_main: true", 46 | ) 47 | 48 | // turn on silently 49 | s.publish("/devices/loopback/controls/relay_silent/on", "1", "loopback/relay_silent") 50 | s.VerifyUnordered( 51 | "tst -> /devices/loopback/controls/relay_silent/on: [1] (QoS 1)", 52 | "driver -> /devices/loopback/controls/relay_silent: [1] (QoS 1, retained)", 53 | "driver -> /devices/loopback/controls/relay_main: [1] (QoS 1, retained)", 54 | "[info] relay_silent: true", 55 | ) 56 | 57 | // turn off silently 58 | s.publish("/devices/loopback/controls/relay_silent/on", "0", "loopback/relay_silent") 59 | s.VerifyUnordered( 60 | "tst -> /devices/loopback/controls/relay_silent/on: [0] (QoS 1)", 61 | "driver -> /devices/loopback/controls/relay_silent: [0] (QoS 1, retained)", 62 | "driver -> /devices/loopback/controls/relay_main: [0] (QoS 1, retained)", 63 | "[info] relay_silent: false", 64 | ) 65 | 66 | // turn on as usual 67 | s.publish("/devices/loopback/controls/relay_main/on", "1", "loopback/relay_main") 68 | s.VerifyUnordered( 69 | "tst -> /devices/loopback/controls/relay_main/on: [1] (QoS 1)", 70 | "driver -> /devices/loopback/controls/relay_main: [1] (QoS 1, retained)", 71 | "[info] relay_main: true", 72 | ) 73 | 74 | s.VerifyEmpty() 75 | } 76 | 77 | func TestRuleLoopbackSuite(t *testing.T) { 78 | testutils.RunSuites(t, 79 | new(RuleLoopbackSuite), 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /samplerules.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | defineVirtualDevice('stabSettings', { 4 | title: 'Stabilization Settings', 5 | cells: { 6 | enabled: { 7 | type: 'switch', 8 | value: false, 9 | }, 10 | lowThreshold: { 11 | type: 'range', 12 | max: 40, 13 | value: 20, 14 | }, 15 | highThreshold: { 16 | type: 'range', 17 | max: 50, 18 | value: 22, 19 | }, 20 | samplebutton: { 21 | type: 'pushbutton', 22 | }, 23 | }, 24 | }); 25 | 26 | defineAlias('stabEnabled', 'stabSettings/enabled'); 27 | defineAlias('roomTemp', 'Weather/Temp 1'); 28 | defineAlias('heaterRelayOn', 'Relays/Relay 1'); 29 | 30 | defineRule('heaterOn', { 31 | asSoonAs: function () { 32 | return stabEnabled && roomTemp < dev.stabSettings.lowThreshold; 33 | }, 34 | then: function () { 35 | log('heaterOn fired'); 36 | heaterRelayOn = true; 37 | startTicker('heating', 3000); 38 | }, 39 | }); 40 | 41 | defineRule('heaterOff', { 42 | when: function () { 43 | return heaterRelayOn && (!stabEnabled || roomTemp >= dev.stabSettings.highThreshold); 44 | }, 45 | then: function () { 46 | log('heaterOff fired'); 47 | heaterRelayOn = false; 48 | timers.heating.stop(); 49 | startTimer('heatingOff', 1000); 50 | }, 51 | }); 52 | 53 | defineRule('ht', { 54 | when: function () { 55 | return timers.heating.firing; 56 | }, 57 | then: function () { 58 | log('heating timer fired'); 59 | }, 60 | }); 61 | 62 | defineRule('htoff', { 63 | when: function () { 64 | return timers.heatingOff.firing; 65 | }, 66 | then: function () { 67 | log('heating-off timer fired'); 68 | }, 69 | }); 70 | 71 | defineRule('tempChange', { 72 | whenChanged: ['Weather/Temp 1', 'Weather/Temp 2'], 73 | then: function (newValue, devName, cellName) { 74 | log('{}/{} = {}', devName, cellName, newValue); 75 | }, 76 | }); 77 | 78 | defineRule('pressureChange', { 79 | whenChanged: 'Weather/Pressure', 80 | then: function (newValue, devName, cellName) { 81 | log('pressure = {}', newValue); 82 | runShellCommand( 83 | "echo -n 'sampleerr' 1>&2; echo -n {}/{}={}".format(devName, cellName, newValue), 84 | { 85 | captureOutput: true, 86 | captureErrorOutput: true, 87 | exitCallback: function (exitCode, capturedOutput, capturedErrorOutput) { 88 | log('cmd exit code: {}', exitCode); 89 | log('cmd output: {}', capturedOutput); 90 | log('cmd error ouput: {}', capturedErrorOutput); 91 | }, 92 | } 93 | ); 94 | }, 95 | }); 96 | 97 | defineRule('buttontest', { 98 | whenChanged: 'stabSettings/samplebutton', 99 | then: function () { 100 | log('samplebutton pressed!'); 101 | }, 102 | }); 103 | 104 | defineRule('crontest', { 105 | when: cron('0,15,30,45 * * * * *'), 106 | then: function () { 107 | log('crontest: {}', new Date()); 108 | }, 109 | }); 110 | 111 | defineRule('crontest1', { 112 | when: cron('3 * * * * *'), 113 | then: function () { 114 | log('crontest1: {}', new Date()); 115 | }, 116 | }); 117 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 h1:AJKJCKcb/psppPl/9CUiQQnTG+Bce0/cIweD5w5Q7aQ= 2 | github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI= 3 | github.com/VictoriaMetrics/metrics v1.37.0 h1:u5Yr+HFofQyn7kgmmkufgkX0nEA6G1oEyK2eaKsVaUM= 4 | github.com/VictoriaMetrics/metrics v1.37.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= 5 | github.com/boltdb/bolt v0.0.0-20161223174454-2e25e3bb4285 h1:EROEheZarnka03RF04n/06GDpsqGKUXkUv4kWP4r8aw= 6 | github.com/boltdb/bolt v0.0.0-20161223174454-2e25e3bb4285/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 13 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 16 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 19 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 21 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 22 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 23 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 24 | github.com/wirenboard/go-duktape v0.0.0-20240729075045-b4150233e350 h1:hjfTrSlSU/Mt2KGZXYMfWhkUJBW8/fmZP5dT4YepCts= 25 | github.com/wirenboard/go-duktape v0.0.0-20240729075045-b4150233e350/go.mod h1:RBaIu9caMBuL6Xk32Ty04qAjl/5cVSfEdCQtbWzvMQ0= 26 | github.com/wirenboard/wbgong v0.6.0 h1:DjUUditytuvQYnRWvXk+LrOkMC4YToLK1+QzZO74ojc= 27 | github.com/wirenboard/wbgong v0.6.0/go.mod h1:FeSukEiXj10SVQ6x4UPoW3O5P/KTRkAWKvEbo9Ftdug= 28 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 29 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /wbrules/rule_notify_sms_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/wirenboard/wbgong/testutils" 9 | ) 10 | 11 | type RuleNotifySmsSuite struct { 12 | RuleSuiteBase 13 | } 14 | 15 | func (s *RuleNotifySmsSuite) SetupTest() { 16 | s.SetupSkippingDefs("testrules_sms_commands.js") 17 | } 18 | 19 | func (s *RuleNotifySmsSuite) setErrorCode(seqNum, errorCode int) { 20 | s.publish(fmt.Sprintf("/devices/test_sms/controls/exit_code_%d/on", seqNum), strconv.Itoa(errorCode), 21 | fmt.Sprintf("test_sms/exit_code_%d", seqNum)) 22 | s.VerifyUnordered( 23 | fmt.Sprintf("tst -> /devices/test_sms/controls/exit_code_%d/on: [%d] (QoS 1)", seqNum, errorCode), 24 | fmt.Sprintf("driver -> /devices/test_sms/controls/exit_code_%d: [%d] (QoS 1, retained)", seqNum, errorCode), 25 | ) 26 | } 27 | 28 | func (s *RuleNotifySmsSuite) TestSmsGammu() { 29 | s.setErrorCode(1, 1) // to make mmcli check OK 30 | s.setErrorCode(2, 0) // to make gammu happy 31 | 32 | s.publish("/devices/test_sms/controls/send/on", "1", "test_sms/send") 33 | s.VerifyUnordered( 34 | "driver -> /devices/test_sms/controls/send: [1] (QoS 1)", 35 | "tst -> /devices/test_sms/controls/send/on: [1] (QoS 1)", 36 | "wbrules-log -> /wbrules/log/info: [run command: wb-gsm should_enable] (QoS 1)", 37 | "wbrules-log -> /wbrules/log/info: [sending sms (gammu-like): test value] (QoS 1)", 38 | "wbrules-log -> /wbrules/log/info: [run command: wb-gsm restart_if_broken && gammu sendsms TEXT '88005553535' -unicode] (QoS 1)", 39 | "wbrules-log -> /wbrules/log/info: [input: test value] (QoS 1)", 40 | ) 41 | } 42 | 43 | func (s *RuleNotifySmsSuite) TestSmsModemManager() { 44 | s.setErrorCode(1, 0) // to make mmcli check OK 45 | s.setErrorCode(2, 0) // to make mmcli call happy 46 | 47 | s.publish("/devices/test_sms/controls/send/on", "1", "test_sms/send") 48 | s.VerifyUnordered( 49 | "driver -> /devices/test_sms/controls/send: [1] (QoS 1)", 50 | "tst -> /devices/test_sms/controls/send/on: [1] (QoS 1)", 51 | "wbrules-log -> /wbrules/log/info: [run command: wb-gsm should_enable] (QoS 1)", 52 | "wbrules-log -> /wbrules/log/info: [sending sms (via ModemManager): test value] (QoS 1)", 53 | "wbrules-log -> /wbrules/log/info: [run command: mmcli -m any --messaging-create-sms=\"number=88005553535,text=\\\"test value\\\"\" | sed -n 's#^Success.*/SMS/\\([0-9]\\+\\).*$#\\1#p' | xargs mmcli --send -s] (QoS 1)", 54 | ) 55 | } 56 | 57 | func (s *RuleNotifySmsSuite) TestSmsModemManagerWithQuotes() { 58 | s.setErrorCode(1, 0) // to make mmcli check OK 59 | s.setErrorCode(2, 0) // to make mmcli call happy 60 | 61 | s.publish("/devices/test_sms/controls/send_quoted/on", "1", "test_sms/send_quoted") 62 | s.VerifyUnordered( 63 | "driver -> /devices/test_sms/controls/send_quoted: [1] (QoS 1)", 64 | "tst -> /devices/test_sms/controls/send_quoted/on: [1] (QoS 1)", 65 | "wbrules-log -> /wbrules/log/info: [run command: wb-gsm should_enable] (QoS 1)", 66 | "wbrules-log -> /wbrules/log/info: [sending sms (via ModemManager): test \"value\" 'single'] (QoS 1)", 67 | "wbrules-log -> /wbrules/log/warning: [ModemManager can't handle SMS with double quotes now, auto replaced with single ones] (QoS 1)", 68 | "wbrules-log -> /wbrules/log/info: [run command: mmcli -m any --messaging-create-sms=\"number=88005553535,text=\\\"test 'value' 'single'\\\"\" | sed -n 's#^Success.*/SMS/\\([0-9]\\+\\).*$#\\1#p' | xargs mmcli --send -s] (QoS 1)", 69 | ) 70 | } 71 | 72 | func TestNotifySmsSuite(t *testing.T) { 73 | testutils.RunSuites(t, 74 | new(RuleNotifySmsSuite), 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /wbrules/rule_modules_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "os" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | type TestModulesSuite struct { 11 | RuleSuiteBase 12 | } 13 | 14 | func (s *TestModulesSuite) SetupTest() { 15 | currentDir, _ := os.Getwd() 16 | s.ModulesPath = currentDir + "/test-modules/" 17 | s.SetupSkippingDefs("testrules_modules.js", "testrules_modules_2.js") 18 | } 19 | 20 | func (s *TestModulesSuite) TestHelloWorld() { 21 | s.publish("/devices/test/controls/helloworld/on", "1", "test/helloworld") 22 | 23 | s.VerifyUnordered( 24 | "tst -> /devices/test/controls/helloworld/on: [1] (QoS 1)", 25 | "driver -> /devices/test/controls/helloworld: [1] (QoS 1, retained)", 26 | "[info] Module helloworld init", 27 | "[info] Required module value: 42", 28 | "[info] Function test: 15", 29 | ) 30 | } 31 | 32 | func (s *TestModulesSuite) TestNotFound() { 33 | s.publish("/devices/test/controls/error/on", "1", "test/error") 34 | 35 | s.VerifyUnordered( 36 | "tst -> /devices/test/controls/error/on: [1] (QoS 1)", 37 | "driver -> /devices/test/controls/error: [1] (QoS 1, retained)", 38 | "[info] Module not found", 39 | ) 40 | 41 | s.EnsureGotErrors() 42 | } 43 | 44 | func (s *TestModulesSuite) TestMultipleRequire() { 45 | s.publish("/devices/test/controls/multifile/on", "1", "test/multifile") 46 | 47 | s.VerifyUnordered( 48 | "tst -> /devices/test/controls/multifile/on: [1] (QoS 1)", 49 | "driver -> /devices/test/controls/multifile: [1] (QoS 1, retained)", 50 | "[info] Module multi_init init", 51 | "[info] Module multi_init init", 52 | "[info] [1] My value of multi_init: 42", 53 | "[info] [2] My value of multi_init: 42", 54 | ) 55 | } 56 | 57 | func (s *TestModulesSuite) TestCrossDependency() { 58 | s.publish("/devices/test/controls/cross/on", "1", "test/cross") 59 | 60 | s.Verify( 61 | "tst -> /devices/test/controls/cross/on: [1] (QoS 1)", 62 | "driver -> /devices/test/controls/cross: [1] (QoS 1, retained)", 63 | "[info] Module submodule init", 64 | "[info] Module with_require init", 65 | "[info] Module loaded", 66 | ) 67 | } 68 | 69 | func (s *TestModulesSuite) TestModuleParams() { 70 | s.publish("/devices/test/controls/params/on", "1", "test/params") 71 | 72 | s.Verify( 73 | "tst -> /devices/test/controls/params/on: [1] (QoS 1)", 74 | "driver -> /devices/test/controls/params: [1] (QoS 1, retained)", 75 | "[info] Module params init", 76 | regexp.MustCompile("\\[__filename: .*/testrules_modules\\.js, module\\.filename: .*/test/params\\.js\\]"), 77 | ) 78 | } 79 | 80 | func (s *TestModulesSuite) TestStaticStorage() { 81 | s.publish("/devices/test/controls/static/on", "1", "test/static") 82 | 83 | s.VerifyUnordered( 84 | "tst -> /devices/test/controls/static/on: [1] (QoS 1)", 85 | "driver -> /devices/test/controls/static: [1] (QoS 1, retained)", 86 | "[info] Module static init", 87 | "[info] Value: 1", 88 | "[info] Module static init", 89 | "[info] Value: 2", 90 | ) 91 | } 92 | 93 | func (s *TestModulesSuite) TestModulesCache() { 94 | s.publish("/devices/test/controls/cache/on", "1", "test/cache") 95 | 96 | s.VerifyUnordered( 97 | "tst -> /devices/test/controls/cache/on: [1] (QoS 1)", 98 | "driver -> /devices/test/controls/cache: [1] (QoS 1, retained)", 99 | "[info] Module helloworld init", 100 | "[info] Value: 42", 101 | "[info] Value: 42", 102 | ) 103 | 104 | } 105 | 106 | func TestModules(t *testing.T) { 107 | testutils.RunSuites(t, 108 | new(TestModulesSuite), 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /wbrules/testrules_timers.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | defineRule('startTimer', { 4 | asSoonAs: function () { 5 | return dev.somedev.foo == 't'; 6 | }, 7 | then: function () { 8 | // make sure it's possible to start more than one timer 9 | // simultaneously 10 | startTimer('sometimer', 500); 11 | startTimer('sometimer1', 500); 12 | }, 13 | }); 14 | 15 | defineRule('startTicker', { 16 | asSoonAs: function () { 17 | return dev.somedev.foo == 'p'; 18 | }, 19 | then: function () { 20 | startTicker('sometimer', 500); 21 | timers.sometimer1.stop(); 22 | }, 23 | }); 24 | 25 | defineRule('stopTimer', { 26 | asSoonAs: function () { 27 | return dev.somedev.foo == 's'; 28 | }, 29 | then: function () { 30 | timers.sometimer.stop(); 31 | timers.sometimer1.stop(); 32 | }, 33 | }); 34 | 35 | defineRule('timer', { 36 | when: function () { 37 | return timers.sometimer.firing; 38 | }, 39 | then: function () { 40 | log('timer fired'); 41 | }, 42 | }); 43 | 44 | defineRule('timer1', { 45 | when: function () { 46 | return timers.sometimer1.firing; 47 | }, 48 | then: function () { 49 | log('timer1 fired'); 50 | }, 51 | }); 52 | 53 | // setTimeout / setInterval based timers 54 | 55 | var timer = null, 56 | timer1 = null; 57 | 58 | defineRule('startTimer1', { 59 | asSoonAs: function () { 60 | return dev.somedev.foo == '+t'; 61 | }, 62 | then: function () { 63 | if (timer) clearTimeout(timer); 64 | if (timer1 != null) clearTimeout(timer1); 65 | timer = setTimeout(function () { 66 | timer = null; 67 | log('timer fired'); 68 | }, 500); 69 | timer1 = setTimeout(function () { 70 | timer1 = null; 71 | log('timer1 fired'); 72 | }, 500); 73 | }, 74 | }); 75 | 76 | defineRule('startTicker1', { 77 | asSoonAs: function () { 78 | return dev.somedev.foo == '+p'; 79 | }, 80 | then: function () { 81 | if (timer) clearTimeout(timer); 82 | if (timer1) { 83 | clearTimeout(timer1); 84 | timer1 = null; 85 | } 86 | timer = setInterval(function () { 87 | log('timer fired'); 88 | }, 500); 89 | }, 90 | }); 91 | 92 | defineRule('stopTimer1', { 93 | asSoonAs: function () { 94 | return dev.somedev.foo == '+s'; 95 | }, 96 | then: function () { 97 | if (timer) { 98 | clearTimeout(timer); 99 | timer = null; 100 | } 101 | if (timer1) { 102 | clearTimeout(timer1); 103 | timer1 = null; 104 | } 105 | }, 106 | }); 107 | 108 | defineRule('shortTimers', { 109 | asSoonAs: function () { 110 | return dev.somedev.foo == 'short'; 111 | }, 112 | then: function () { 113 | setTimeout(function () { 114 | log('timer fired(0)'); 115 | }, 0); 116 | setTimeout(function () { 117 | log('timer fired(-1)'); 118 | }, -1); 119 | setInterval(function () { 120 | log('interval fired(0)'); 121 | }, 0); 122 | setInterval(function () { 123 | log('interval fired(-1)'); 124 | }, -1); 125 | startTimer('sometimer', 0); 126 | startTimer('sometimer1', -1); 127 | startTicker('someticker', 0); 128 | startTicker('someticker1', -1); 129 | }, 130 | }); 131 | 132 | defineRule('cleanupTimers', { 133 | asSoonAs: function () { 134 | return dev.somedev.foo == 'cleanup'; 135 | }, 136 | then: function () { 137 | setTimeout(function () { 138 | log('timer fired(0)'); 139 | }, 500); 140 | setTimeout(function () { 141 | log('timer fired(1)'); 142 | }, 1500); 143 | setInterval(function () { 144 | log('interval fired'); 145 | }, 500); 146 | }, 147 | }); 148 | -------------------------------------------------------------------------------- /wbrules/persistent_storage_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/wirenboard/wbgong" 8 | "github.com/wirenboard/wbgong/testutils" 9 | ) 10 | 11 | type PersistentStorageSuite struct { 12 | RuleSuiteBase 13 | tmpDir string 14 | } 15 | 16 | func (s *PersistentStorageSuite) SetupFixture() { 17 | var err error 18 | 19 | // we need to create separated temp directory because persistent DB file 20 | // should be keeped between tests 21 | s.tmpDir, err = os.MkdirTemp("", "wbrulestest") 22 | if err != nil { 23 | s.FailNow("can't create temp directory") 24 | } 25 | wbgong.Debug.Printf("created temp dir %s", s.tmpDir) 26 | } 27 | 28 | func (s *PersistentStorageSuite) TearDownFixture() { 29 | os.RemoveAll(s.tmpDir) 30 | } 31 | 32 | func (s *PersistentStorageSuite) SetupTest() { 33 | s.PersistentDBFile = s.tmpDir + "/test_persistent.db" 34 | s.VdevStorageFile = s.tmpDir + "/test-vdev.db" 35 | s.SetupSkippingDefs() 36 | s.LiveLoadScriptToDir("testrules_persistent.js", s.tmpDir) 37 | s.SkipTill("[info] loaded file 1") 38 | s.LiveLoadScriptToDir("testrules_persistent_2.js", s.tmpDir) 39 | s.SkipTill("[info] loaded file 2") 40 | } 41 | 42 | func (s *PersistentStorageSuite) TearDownTest() { 43 | s.RuleSuiteBase.TearDownTest() 44 | } 45 | 46 | func (s *PersistentStorageSuite) TestPersistentStorage() { 47 | s.publish("/devices/vdev/controls/write/on", "1", "vdev/write") 48 | 49 | s.VerifyUnordered( 50 | "tst -> /devices/vdev/controls/write/on: [1] (QoS 1)", 51 | "driver -> /devices/vdev/controls/write: [1] (QoS 1, retained)", 52 | "[info] pure object is not created", 53 | "[info] pure subobject is not created", 54 | "[info] write objects 42, \"HelloWorld\", {\"name\":\"MyObj\",\"foo\":\"bar\",\"baz\":84,\"sub\":{\"hello\":\"world\"}}", 55 | ) 56 | 57 | } 58 | 59 | // try to read from persistent storage 60 | func (s *PersistentStorageSuite) TestPersistentStorage2() { 61 | s.publish("/devices/vdev/controls/read/on", "1", "vdev/read") 62 | s.VerifyUnordered( 63 | "tst -> /devices/vdev/controls/read/on: [1] (QoS 1)", 64 | "driver -> /devices/vdev/controls/read: [1] (QoS 1, retained)", 65 | "[info] read objects 42, \"HelloWorld\", {\"name\":\"MyObj\",\"foo\":\"bar\",\"baz\":84,\"sub\":{\"hello\":\"world\"}}", 66 | "[info] read objects 42, \"HelloWorld\", {\"name\":\"MyObj\",\"foo\":\"bar\",\"baz\":84,\"sub\":{\"hello\":\"earth\"}}", 67 | ) 68 | 69 | } 70 | 71 | // test local storages in different files 72 | func (s *PersistentStorageSuite) TestLocalPersistentStorage() { 73 | // write values 74 | s.publish("/devices/vdev/controls/localWrite1/on", "1", "vdev/localWrite1") 75 | s.SkipTill("[info] file1: write to local PS") 76 | 77 | s.publish("/devices/vdev/controls/localWrite2/on", "1", "vdev/localWrite2") 78 | s.SkipTill("[info] file2: write to local PS") 79 | 80 | // now read values 81 | s.publish("/devices/vdev/controls/localRead1/on", "1", "vdev/localRead1") 82 | s.SkipTill("[info] file1: read objects \"hello_from_1\", undefined") 83 | 84 | s.publish("/devices/vdev/controls/localRead2/on", "1", "vdev/localRead2") 85 | s.SkipTill("[info] file2: read objects undefined, \"hello_from_2\"") 86 | } 87 | 88 | func (s *PersistentStorageSuite) TestLocalPersistentStorage2() { 89 | // now read values 90 | s.publish("/devices/vdev/controls/localRead1/on", "1", "vdev/localRead1") 91 | s.SkipTill("[info] file1: read objects \"hello_from_1\", undefined") 92 | 93 | s.publish("/devices/vdev/controls/localRead2/on", "1", "vdev/localRead2") 94 | s.SkipTill("[info] file2: read objects undefined, \"hello_from_2\"") 95 | } 96 | 97 | func TestPersistentStorageSuite(t *testing.T) { 98 | s := new(PersistentStorageSuite) 99 | s.SetupFixture() 100 | defer s.TearDownFixture() 101 | testutils.RunSuites(t, s) 102 | } 103 | -------------------------------------------------------------------------------- /wbrules/rule_optimization_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "github.com/wirenboard/wbgong/testutils" 5 | "testing" 6 | ) 7 | 8 | type RuleOptimizationSuite struct { 9 | RuleSuiteBase 10 | } 11 | 12 | func (s *RuleOptimizationSuite) SetupTest() { 13 | s.SetupSkippingDefs("testrules_opt.js") 14 | } 15 | 16 | func (s *RuleOptimizationSuite) TestRuleCheckOptimization() { 17 | // s.Verify( 18 | // That's the first time when all rules are run. 19 | // somedev/countIt and somedev/countItLT are incomplete here, but 20 | // the engine notes that rules' conditions depend on the cells 21 | // "[info] condCount: asSoonAs()", 22 | // "[info] condCountLT: when()", 23 | // ) 24 | s.publish("/devices/somedev/controls/countIt/meta/type", "text", "somedev/countIt") 25 | s.publish("/devices/somedev/controls/countIt", "0", "somedev/countIt") 26 | s.Verify( 27 | "tst -> /devices/somedev/controls/countIt/meta/type: [text] (QoS 1, retained)", 28 | "[info] condCount: asSoonAs()", 29 | "tst -> /devices/somedev/controls/countIt: [0] (QoS 1, retained)", 30 | // here the value of the cell changes, so the rule is invoked 31 | "[info] condCount: asSoonAs()") 32 | 33 | s.publish("/devices/somedev/controls/temp", "25", "somedev/temp") 34 | s.publish("/devices/somedev/controls/countIt", "42", "somedev/countIt") 35 | s.Verify( 36 | "tst -> /devices/somedev/controls/temp: [25] (QoS 1, retained)", 37 | // changing unrelated cell doesn't cause the rule to be invoked 38 | "tst -> /devices/somedev/controls/countIt: [42] (QoS 1, retained)", 39 | "[info] condCount: asSoonAs()", 40 | // asSoonAs function called during the first run + when countIt 41 | // value changed to 42 42 | "[info] condCount fired, count=4", 43 | // ruleWithoutCells follows condCount rule in testrules.js 44 | // and doesn't utilize any cells. It's run just once when condCount 45 | // rule sets a global variable to true. 46 | "[info] ruleWithoutCells fired") 47 | 48 | s.publish("/devices/somedev/controls/countIt", "0", "somedev/countIt") 49 | s.Verify( 50 | "tst -> /devices/somedev/controls/countIt: [0] (QoS 1, retained)", 51 | "[info] condCount: asSoonAs()") 52 | s.publish("/devices/somedev/controls/countIt", "42", "somedev/countIt") 53 | s.Verify( 54 | "tst -> /devices/somedev/controls/countIt: [42] (QoS 1, retained)", 55 | "[info] condCount: asSoonAs()", 56 | "[info] condCount fired, count=6") 57 | 58 | // now check optimization of level-triggered rules 59 | s.publish("/devices/somedev/controls/countItLT/meta/type", "text", "somedev/countItLT") 60 | s.publish("/devices/somedev/controls/countItLT", "0", "somedev/countItLT") 61 | s.Verify( 62 | "tst -> /devices/somedev/controls/countItLT/meta/type: [text] (QoS 1, retained)", 63 | // here the value of the cell changes, so the rule is invoked 64 | "[info] condCountLT: when()", 65 | "tst -> /devices/somedev/controls/countItLT: [0] (QoS 1, retained)", 66 | "[info] condCountLT: when()") 67 | 68 | s.publish("/devices/somedev/controls/countItLT", "42", "somedev/countItLT") 69 | s.Verify( 70 | "tst -> /devices/somedev/controls/countItLT: [42] (QoS 1, retained)", 71 | "[info] condCountLT: when()", 72 | // when function called during the first run + when countItLT 73 | // value changed to 42 74 | "[info] condCountLT fired, count=4") 75 | 76 | s.publish("/devices/somedev/controls/countItLT", "43", "somedev/countItLT") 77 | s.Verify( 78 | "tst -> /devices/somedev/controls/countItLT: [43] (QoS 1, retained)", 79 | "[info] condCountLT: when()", 80 | "[info] condCountLT fired, count=5") 81 | 82 | s.publish("/devices/somedev/controls/countItLT", "0", "somedev/countItLT") 83 | s.Verify( 84 | "tst -> /devices/somedev/controls/countItLT: [0] (QoS 1, retained)", 85 | "[info] condCountLT: when()") 86 | 87 | s.publish("/devices/somedev/controls/countItLT", "1", "somedev/countItLT") 88 | s.Verify( 89 | "tst -> /devices/somedev/controls/countItLT: [1] (QoS 1, retained)", 90 | "[info] condCountLT: when()") 91 | } 92 | 93 | func TestRuleOptimizationSuite(t *testing.T) { 94 | testutils.RunSuites(t, 95 | new(RuleOptimizationSuite), 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /wbrules/escontext_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/objx" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var objTests = []string{ 13 | `{}`, 14 | `{ 15 | "x": 3, 16 | "y": "abc", 17 | "z": { "rr": 42 }, 18 | "arrKey": [ 1, 2, "x", { "y": "zz" }, null, false, true ] 19 | }`, 20 | } 21 | 22 | func TestJSToObjxAndBack(t *testing.T) { 23 | f := newESContextFactory() 24 | ctx := f.newESContext(nil, "") 25 | for _, jsonStr := range objTests { 26 | if r := ctx.PevalString("(" + jsonStr + ")"); r != 0 { 27 | t.Fatal("failed to evaluate the script") 28 | } 29 | object := ctx.GetJSObject(-1) 30 | ctx.Pop() 31 | var objxMap objx.Map 32 | errUnmarshal := json.Unmarshal([]byte(jsonStr), &objxMap) 33 | if errUnmarshal != nil { 34 | t.Fatalf("Cant unmarshal json: '%s'", errUnmarshal) 35 | } 36 | assert.Equal(t, objxMap, object) 37 | 38 | ctx.PushGlobalObject() 39 | ctx.PushJSObject(object.(objx.Map)) 40 | ctx.PutPropString(-2, "jso") 41 | if r := ctx.PevalString("JSON.stringify(jso)"); r != 0 { 42 | t.Fatal("failed to evaluate the script") 43 | } 44 | jsonStr1 := ctx.SafeToString(-1) 45 | ctx.Pop() 46 | var objxMap1 objx.Map 47 | errUnmarshal1 := json.Unmarshal([]byte(jsonStr1), &objxMap1) 48 | if errUnmarshal1 != nil { 49 | t.Fatalf("Cant unmarshal json: '%s'", errUnmarshal1) 50 | } 51 | assert.Equal(t, objxMap, objxMap1) 52 | } 53 | } 54 | 55 | func TestNumConversions(t *testing.T) { 56 | f := newESContextFactory() 57 | ctx := f.newESContext(nil, "") 58 | ctx.PushJSObject(objx.Map{ 59 | "v_uint8": uint8(0xf0), 60 | "v_uint16": uint16(0xf001), 61 | "v_uint32": uint32(0xf0000001), 62 | "v_uint64": uint64(0xf00000001), 63 | "v_int8": int8(-1), 64 | "v_int16": int16(-2), 65 | "v_int32": int32(-3), 66 | "v_int64": int64(-4), 67 | "v_int": int(-5), 68 | "v_float32": float32(-1.5), 69 | "v_float64": float64(-2.5), 70 | "nan_32": float32(math.NaN()), 71 | "nan_64": float32(math.NaN()), 72 | }) 73 | expected := objx.Map{ 74 | "v_uint8": float64(0xf0), 75 | "v_uint16": float64(0xf001), 76 | "v_uint32": float64(0xf0000001), 77 | "v_uint64": float64(0xf00000001), 78 | "v_int8": float64(-1), 79 | "v_int16": float64(-2), 80 | "v_int32": float64(-3), 81 | "v_int64": float64(-4), 82 | "v_int": float64(-5), 83 | "v_float32": float64(-1.5), 84 | "v_float64": float64(-2.5), 85 | "nan_32": math.NaN(), 86 | "nan_64": math.NaN(), 87 | } 88 | actual := ctx.GetJSObject(-1).(objx.Map) 89 | for k, v := range expected { 90 | f, ok := v.(float64) 91 | switch { 92 | case !ok || !math.IsNaN(f): 93 | assert.Equal(t, v, actual[k], "key: %s", k) 94 | case !math.IsNaN(v.(float64)): 95 | t.Fatalf("%s expected to be NaN but is %v instead", k, v) 96 | } 97 | } 98 | assert.Equal(t, len(expected), len(actual)) 99 | } 100 | 101 | var locTests = []struct { 102 | filename, content string 103 | tracebacks []ESTraceback 104 | }{ 105 | { 106 | "test1.js", 107 | `function aaa () { 108 | storeLoc(); 109 | } 110 | 111 | aaa();`, 112 | []ESTraceback{ 113 | { 114 | {"test1.js", 2}, 115 | {"test1.js", 5}, 116 | }, 117 | }, 118 | }, 119 | { 120 | "test2.js", 121 | `// whatever 122 | storeLoc();`, 123 | []ESTraceback{ 124 | { 125 | {"test2.js", 2}, 126 | }, 127 | }, 128 | }, 129 | } 130 | 131 | func TestCallLocation(t *testing.T) { 132 | f := newESContextFactory() 133 | ctx := f.newESContext(nil, "") 134 | var storedTracebacks []ESTraceback 135 | ctx.PushGlobalObject() 136 | ctx.DefineFunctions(map[string]func(*ESContext) int{ 137 | "storeLoc": func(ctx *ESContext) int { 138 | storedTracebacks = append(storedTracebacks, ctx.GetTraceback()) 139 | return 0 140 | }, 141 | }) 142 | ctx.Pop() 143 | for _, loc := range locTests { 144 | storedTracebacks = make([]ESTraceback, 0, 10) 145 | ctx.LoadScriptFromString(loc.filename, loc.content) 146 | assert.Equal(t, loc.tracebacks, storedTracebacks) 147 | } 148 | } 149 | 150 | func TestLoadScenario(t *testing.T) { 151 | f := newESContextFactory() 152 | ctx := f.newESContext(nil, "") 153 | err := ctx.LoadScenario("testrules_load_scenario.js") 154 | assert.Equal(t, err, nil) 155 | } 156 | 157 | func TestLoadScenarioNeg(t *testing.T) { 158 | f := newESContextFactory() 159 | ctx := f.newESContext(nil, "") 160 | err := ctx.LoadScenario("testrules_load_scenario_bad.js") 161 | if assert.NotEqual(t, err, nil) { 162 | eserror, ok := err.(ESError) 163 | if assert.Equal(t, true, ok) && assert.NotZero(t, len(eserror.Traceback)) { 164 | assert.Equal(t, 5, eserror.Traceback[0].line) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /wbrules/rule_timers_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/wirenboard/wbgong/testutils" 8 | ) 9 | 10 | type RuleTimersSuite struct { 11 | RuleSuiteBase 12 | } 13 | 14 | func (s *RuleTimersSuite) SetupTest() { 15 | s.SetupSkippingDefs("testrules_timers.js") 16 | } 17 | 18 | func (s *RuleTimersSuite) VerifyTimers(prefix string) { 19 | s.publish("/devices/somedev/controls/foo/meta/type", "text", "somedev/foo") 20 | s.publish("/devices/somedev/controls/foo", prefix+"t", "somedev/foo") 21 | s.Verify( 22 | "tst -> /devices/somedev/controls/foo/meta/type: [text] (QoS 1, retained)", 23 | "tst -> /devices/somedev/controls/foo: ["+prefix+"t] (QoS 1, retained)", 24 | "new fake timer: 1, 500", 25 | "new fake timer: 2, 500", 26 | ) 27 | 28 | s.publish("/devices/somedev/controls/foo", prefix+"s", "somedev/foo") 29 | s.VerifyUnordered( 30 | // NOTE: actually, it's only order of timer.Stop() calls which can vary here 31 | "tst -> /devices/somedev/controls/foo: ["+prefix+"s] (QoS 1, retained)", 32 | "timer.Stop(): 1", 33 | "timer.Stop(): 2", 34 | ) 35 | 36 | s.publish("/devices/somedev/controls/foo", prefix+"t", "somedev/foo") 37 | s.Verify( 38 | "tst -> /devices/somedev/controls/foo: ["+prefix+"t] (QoS 1, retained)", 39 | "new fake timer: 3, 500", 40 | "new fake timer: 4, 500", 41 | ) 42 | 43 | ts := s.AdvanceTime(500 * time.Millisecond) 44 | s.FireTimer(3, ts) 45 | s.FireTimer(4, ts) 46 | s.VerifyUnordered( 47 | // the order in which fake timers fire is not strictly defined 48 | // (engine's timer handlers run in parallel) 49 | "timer.fire(): 3", 50 | "timer.fire(): 4", 51 | "[info] timer fired", 52 | "[info] timer1 fired", 53 | ) 54 | 55 | s.publish("/devices/somedev/controls/foo", prefix+"p", "somedev/foo") 56 | s.Verify( 57 | "tst -> /devices/somedev/controls/foo: ["+prefix+"p] (QoS 1, retained)", 58 | "new fake ticker: 5, 500", 59 | ) 60 | 61 | for i := 1; i < 4; i++ { 62 | targetTime := s.AdvanceTime(time.Duration(500*i) * time.Millisecond) 63 | s.FireTimer(5, targetTime) 64 | s.Verify( 65 | "timer.fire(): 5", 66 | "[info] timer fired", 67 | ) 68 | } 69 | 70 | s.publish("/devices/somedev/controls/foo", prefix+"t", "somedev/foo") 71 | s.Verify( 72 | "tst -> /devices/somedev/controls/foo: [" + prefix + "t] (QoS 1, retained)", 73 | ) 74 | s.VerifyUnordered( 75 | "timer.Stop(): 5", 76 | "new fake timer: 6, 500", 77 | "new fake timer: 7, 500", 78 | ) 79 | 80 | ts = s.AdvanceTime(5 * 500 * time.Millisecond) 81 | s.FireTimer(6, ts) 82 | s.FireTimer(7, ts) 83 | s.VerifyUnordered( 84 | "timer.fire(): 6", 85 | "timer.fire(): 7", 86 | "[info] timer fired", 87 | "[info] timer1 fired", 88 | ) 89 | } 90 | 91 | func (s *RuleTimersSuite) TestTimers() { 92 | s.VerifyTimers("") 93 | } 94 | 95 | func (s *RuleTimersSuite) TestDirectTimers() { 96 | s.VerifyTimers("+") 97 | } 98 | 99 | func (s *RuleTimersSuite) TestShortTimers() { 100 | s.publish("/devices/somedev/controls/foo/meta/type", "text", "somedev/foo") 101 | s.publish("/devices/somedev/controls/foo", "short", "somedev/foo") 102 | 103 | s.Verify( 104 | "tst -> /devices/somedev/controls/foo/meta/type: [text] (QoS 1, retained)", 105 | "tst -> /devices/somedev/controls/foo: [short] (QoS 1, retained)", 106 | "new fake timer: 1, 1", 107 | "new fake timer: 2, 1", 108 | "wbrules-log -> /wbrules/log/warning: [_wbStartTimer: 1 ms interval may degrade performance] (QoS 1)", 109 | "new fake ticker: 3, 1", 110 | "wbrules-log -> /wbrules/log/warning: [_wbStartTimer: 1 ms interval may degrade performance] (QoS 1)", 111 | "new fake ticker: 4, 1", 112 | "new fake timer: 5, 1", 113 | "new fake timer: 6, 1", 114 | "wbrules-log -> /wbrules/log/warning: [_wbStartTimer: 1 ms interval may degrade performance] (QoS 1)", 115 | "new fake ticker: 7, 1", 116 | "wbrules-log -> /wbrules/log/warning: [_wbStartTimer: 1 ms interval may degrade performance] (QoS 1)", 117 | "new fake ticker: 8, 1", 118 | ) 119 | s.VerifyEmpty() 120 | } 121 | 122 | func (s *RuleTimersSuite) TestCleanupTimers() { 123 | s.publish("/devices/somedev/controls/foo/meta/type", "text", "somedev/foo") 124 | s.publish("/devices/somedev/controls/foo", "cleanup", "somedev/foo") 125 | 126 | s.Verify( 127 | "tst -> /devices/somedev/controls/foo/meta/type: [text] (QoS 1, retained)", 128 | "tst -> /devices/somedev/controls/foo: [cleanup] (QoS 1, retained)", 129 | "new fake timer: 1, 500", 130 | "new fake timer: 2, 1500", 131 | "new fake ticker: 3, 500", 132 | ) 133 | 134 | s.RemoveScript("testrules_timers.js") 135 | 136 | s.VerifyUnordered( 137 | "timer.Stop(): 1", 138 | "timer.Stop(): 2", 139 | "timer.Stop(): 3", 140 | "[removed] testrules_timers.js", 141 | ) 142 | 143 | s.VerifyEmpty() 144 | } 145 | 146 | func TestRuleTimersSuite(t *testing.T) { 147 | testutils.RunSuites(t, 148 | new(RuleTimersSuite), 149 | ) 150 | } 151 | -------------------------------------------------------------------------------- /wbrules/testrules.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js2-mode -*- 2 | 3 | if ( 4 | (function () { 5 | return this; 6 | })() !== global 7 | ) 8 | throw new Error('global object not defined!'); 9 | 10 | // extra test for format() / xformat() 11 | (function () { 12 | var formatted = '{{}abc {{} {} {{}'.format(1); 13 | if (formatted != '{}abc {} 1 {}') throw new Error('oops! format error: ' + formatted); 14 | 15 | var xformatted = '\\{}abc \\{} {} {} -- {{ (31).toString(16) }} \\{}'.xformat(1, 'zz'); 16 | if (xformatted != '{}abc {} 1 zz -- 1f {}') throw new Error('oops! xformat error: ' + xformatted); 17 | xformatted = "{{ (function(){ throw new Error('zzzerr'); })() }}".xformat(); 18 | if (xformatted != "") 19 | throw new Error('oops! xformat exception handling error: ' + xformatted); 20 | })(); 21 | 22 | function cellSpec(devName, cellName) { 23 | return devName === undefined ? '(no cell)' : '{}/{}'.format(devName, cellName); 24 | } 25 | 26 | defineAlias('stabEnabled', 'stabSettings/enabled'); 27 | defineAlias('temp', 'somedev/temp'); 28 | defineAlias('sw', 'somedev/sw'); 29 | 30 | defineVirtualDevice('stabSettings', { 31 | title: 'Stabilization Settings', 32 | cells: { 33 | enabled: { 34 | type: 'switch', 35 | value: false, 36 | }, 37 | lowThreshold: { 38 | type: 'range', 39 | max: 40, 40 | value: 20, 41 | }, 42 | highThreshold: { 43 | type: 'range', 44 | max: 50, 45 | value: 22, 46 | }, 47 | }, 48 | }); 49 | 50 | defineRule('heaterOn', { 51 | asSoonAs: function () { 52 | if (dev['stabSettings/lowThreshold'] !== dev.stabSettings.lowThreshold) 53 | throw new Error("/-notation in dev name doesn't work"); 54 | return stabEnabled && temp < dev.stabSettings.lowThreshold; 55 | }, 56 | then: function (newValue, devName, cellName) { 57 | log( 58 | 'heaterOn fired, changed: {} -> {}', 59 | cellSpec(devName, cellName), 60 | newValue === undefined ? '(none)' : newValue 61 | ); 62 | sw = true; 63 | }, 64 | }); 65 | 66 | defineRule('heaterOff', { 67 | when: function () { 68 | return sw && (!stabEnabled || temp >= dev.stabSettings.highThreshold); 69 | }, 70 | then: function (newValue, devName, cellName) { 71 | log( 72 | 'heaterOff fired, changed: {} -> {}', 73 | cellSpec(devName, cellName), 74 | newValue === undefined ? '(none)' : newValue 75 | ); 76 | dev['somedev/sw'] = false; 77 | }, 78 | }); 79 | 80 | defineRule('initiallyIncompleteLevelTriggered', { 81 | when: function () { 82 | return dev.somedev.foobar != '0'; 83 | }, 84 | then: function () { 85 | log('initiallyIncompleteLevelTriggered fired'); 86 | }, 87 | }); 88 | 89 | defineRule('sendmqtt', { 90 | asSoonAs: function () { 91 | return dev.somedev.sendit; 92 | }, 93 | then: function () { 94 | publish('/abc/def/ghi', '0', 0); 95 | publish('/misc/whatever', 'abcdef', 1); 96 | publish('/zzz/foo', 'qqq', 2); 97 | publish('/zzz/foo/qwerty', '42', 2, true); 98 | }, 99 | }); 100 | 101 | defineRule('cellChange1', { 102 | whenChanged: 'somedev/foobarbaz', 103 | then: function (newValue, devName, cellName) { 104 | if (arguments.length != 3) throw new Error('invalid arguments for then'); 105 | var v = dev[devName][cellName]; 106 | if (v !== newValue) throw new Error('bad newValue! ' + newValue); 107 | log('cellChange1: {}/{}={} ({})', devName, cellName, v, typeof v); 108 | }, 109 | }); 110 | 111 | defineAlias('tempx', 'somedev/tempx'); 112 | 113 | defineRule('cellChange2', { 114 | whenChanged: ['somedev/foobarbaz', 'tempx' /* an alias */, 'somedev/abutton'], 115 | then: function (newValue, devName, cellName) { 116 | if (arguments.length != 3) throw new Error('invalid arguments for then'); 117 | var v = dev[devName][cellName]; 118 | if (v !== newValue) 119 | throw new Error( 120 | 'bad newValue! ' + newValue + ' ' + typeof newValue + ', expected ' + v + ' ' + typeof v 121 | ); 122 | // just make sure that format works here, too 123 | log('cellChange2: {}/{}={} ({})'.format(devName, cellName, v, typeof v)); 124 | }, 125 | }); 126 | 127 | defineRule('funcValueChange', { 128 | whenChanged: function () { 129 | return dev.somedev.cellforfunc > 3; 130 | }, 131 | then: function (newValue, devName, cellName) { 132 | if (arguments.length != 1) throw new Error('invalid arguments for then'); 133 | log('funcValueChange: {} ({})', newValue, typeof newValue); 134 | }, 135 | }); 136 | 137 | defineRule('funcValueChange2', { 138 | whenChanged: [ 139 | 'somedev/cellforfunc1', 140 | function () { 141 | return dev.somedev.cellforfunc2 > 3; 142 | }, 143 | ], 144 | then: function (newValue, devName, cellName) { 145 | log('funcValueChange2: {}: {} ({})', cellSpec(devName, cellName), newValue, typeof newValue); 146 | }, 147 | }); 148 | 149 | // test anonymous rule 150 | defineRule({ 151 | whenChanged: 'somedev/anon', 152 | then: function () { 153 | log('anonymous rule run'); 154 | }, 155 | }); 156 | -------------------------------------------------------------------------------- /wbrules/testrules_meta.js: -------------------------------------------------------------------------------- 1 | function cellSpec(devName, cellName) { 2 | return devName === undefined ? '(no cell)' : '{}/{}'.format(devName, cellName); 3 | } 4 | 5 | defineVirtualDevice('testDevice', { 6 | title: 'Test Device', 7 | cells: { 8 | switchControl: { 9 | type: 'switch', 10 | value: false, 11 | order: 4, 12 | }, 13 | rangeControl: { 14 | type: 'range', 15 | max: 100, 16 | value: 50, 17 | order: 3, 18 | }, 19 | textControl: { 20 | type: 'text', 21 | value: 'some text', 22 | readonly: false, 23 | enum: { 24 | txt0: { en: 'zero' }, 25 | txt1: { en: 'one' }, 26 | }, 27 | }, 28 | startControl: { 29 | type: 'switch', 30 | value: false, 31 | }, 32 | checkUndefinedControl: { 33 | type: 'switch', 34 | value: false, 35 | }, 36 | vDevWithOrder: { 37 | type: 'switch', 38 | value: false, 39 | }, 40 | createVDevWithControlMetaTitle: { 41 | type: 'switch', 42 | value: false, 43 | }, 44 | createVDevWithControlMetaUnits: { 45 | type: 'switch', 46 | value: false, 47 | }, 48 | }, 49 | }); 50 | 51 | defineRule('onChangeStartControl', { 52 | whenChanged: 'testDevice/startControl', 53 | then: function (newValue, devName, cellName) { 54 | log( 55 | 'got startControl, changed: {} -> {}', 56 | cellSpec(devName, cellName), 57 | newValue === undefined ? '(none)' : newValue 58 | ); 59 | if (newValue) { 60 | dev['testDevice/textControl#error'] = 'error text'; 61 | dev['testDevice/textControl#description'] = 'new description'; 62 | dev['testDevice/textControl#type'] = 'range'; 63 | dev['testDevice/textControl#max'] = '255'; 64 | dev['testDevice/textControl#min'] = '5'; 65 | dev['testDevice/textControl#order'] = '4'; 66 | dev['testDevice/textControl#units'] = 'meters'; 67 | dev['testDevice/textControl#readonly'] = '1'; 68 | } else { 69 | dev['testDevice/textControl#error'] = ''; 70 | dev['testDevice/textControl#description'] = 'old description'; 71 | dev['testDevice/textControl#type'] = 'text'; 72 | dev['testDevice/textControl#max'] = '0'; 73 | dev['testDevice/textControl#min'] = '0'; 74 | dev['testDevice/textControl#order'] = '5'; 75 | dev['testDevice/textControl#units'] = 'chars'; 76 | dev['testDevice/textControl#readonly'] = '0'; 77 | 78 | getDevice('testDevice') 79 | .getControl('textControl') 80 | .setEnumTitles({ str0: { en: 'Off' }, str1: { en: 'On' } }); 81 | } 82 | }, 83 | }); 84 | 85 | defineRule('onChangeSwitchControl', { 86 | whenChanged: 'testDevice/switchControl#error', 87 | then: function (newValue, devName, cellName) { 88 | log( 89 | 'got switchControl, changed: {} -> {}', 90 | cellSpec(devName, cellName), 91 | newValue === undefined ? '(none)' : newValue 92 | ); 93 | }, 94 | }); 95 | 96 | defineRule('onChangeSw', { 97 | whenChanged: 'somedev/sw#error', 98 | then: function (newValue, devName, cellName) { 99 | log( 100 | 'got sw, changed: {} -> {}', 101 | cellSpec(devName, cellName), 102 | newValue === undefined ? '(none)' : newValue 103 | ); 104 | if (newValue !== '') { 105 | dev['testDevice/switchControl'] = true; 106 | } else { 107 | dev['testDevice/switchControl'] = false; 108 | } 109 | }, 110 | }); 111 | 112 | defineRule('asSoonAsExtError', { 113 | asSoonAs: function () { 114 | return dev['somedev/sw#error']; 115 | }, 116 | then: function (newValue, devName, cellName) { 117 | log(devName + '/' + cellName + ' = ' + newValue); 118 | }, 119 | }); 120 | 121 | defineRule('undefinedControlMeta', { 122 | whenChanged: 'testDevice/checkUndefinedControl', 123 | then: function () { 124 | var m = dev['undefined_device/control#type']; 125 | log('Meta: ' + m); 126 | }, 127 | }); 128 | 129 | defineRule('makeVdevWithOrder', { 130 | whenChanged: 'testDevice/vDevWithOrder', 131 | then: function () { 132 | defineVirtualDevice('vDevWithOrder', { 133 | cells: { 134 | test1: { 135 | type: 'text', 136 | value: 'hello', 137 | readonly: true, 138 | order: 4, 139 | }, 140 | test2: { 141 | type: 'text', 142 | value: 'world', 143 | readonly: true, 144 | order: 3, 145 | }, 146 | }, 147 | }); 148 | }, 149 | }); 150 | 151 | defineRule('makeVdevWithControlMetaTitle', { 152 | whenChanged: 'testDevice/createVDevWithControlMetaTitle', 153 | then: function () { 154 | defineVirtualDevice('vDevWithControlMetaTitle', { 155 | cells: { 156 | test1: { 157 | title: 'ControlMetaTitleOne', 158 | type: 'value', 159 | value: 1, 160 | }, 161 | }, 162 | }); 163 | }, 164 | }); 165 | 166 | defineRule('makeVdevWithControlMetaUnits', { 167 | whenChanged: 'testDevice/createVDevWithControlMetaUnits', 168 | then: function () { 169 | defineVirtualDevice('vDevWithControlMetaUnits', { 170 | cells: { 171 | test1: { 172 | units: 'W', 173 | type: 'value', 174 | value: 1, 175 | }, 176 | }, 177 | }); 178 | }, 179 | }); 180 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/VictoriaMetrics/metrics" 15 | "github.com/wirenboard/wb-rules/wbrules" 16 | "github.com/wirenboard/wbgong" 17 | ) 18 | 19 | var version = "unknown" 20 | 21 | const ( 22 | DRIVER_CLIENT_ID = "rules" 23 | DRIVER_CONV_ID = "wb-rules" 24 | ENGINE_CLIENT_ID = "wb-rules-engine" 25 | 26 | RPC_DRIVER_NAME = "wbrules" 27 | 28 | PERSISTENT_DB_FILE = "/var/lib/wirenboard/wbrules-persistent.db" 29 | VIRTUAL_DEVICES_DB_FILE = "/var/lib/wirenboard/wbrules-vdev.db" 30 | WBGO_FILE = "/usr/lib/wb-rules/wbgo.so" 31 | 32 | WBRULES_MODULES_ENV = "WB_RULES_MODULES" 33 | 34 | MOSQUITTO_SOCK_FILE = "/var/run/mosquitto/mosquitto.sock" 35 | DEFAULT_BROKER_URL = "tcp://localhost:1883" 36 | ) 37 | 38 | func isSocket(path string) bool { 39 | info, err := os.Stat(path) 40 | if os.IsNotExist(err) { 41 | return false 42 | } 43 | return info.Mode()&os.ModeSocket != 0 44 | } 45 | 46 | func main() { 47 | 48 | if len(os.Args) > 1 && os.Args[1] == "version" { 49 | fmt.Println(version) 50 | os.Exit(0) 51 | } 52 | 53 | var err error 54 | 55 | brokerAddress := flag.String("broker", DEFAULT_BROKER_URL, "MQTT broker url") 56 | editDir := flag.String("editdir", "", "Editable script directory") 57 | debug := flag.Bool("debug", false, "Enable debugging") 58 | noQueues := flag.Bool("debug-queues", false, "Don't use queues in wbgo driver (debugging)") 59 | useSyslog := flag.Bool("syslog", false, "Use syslog for logging") 60 | mqttDebug := flag.Bool("mqttdebug", false, "Enable MQTT debugging") 61 | precise := flag.Bool("precise", false, "Don't reown devices without driver") 62 | cleanup := flag.Bool("cleanup", false, "Clean up MQTT data on unload") 63 | httpAddr := flag.String("http", "", "Serve metrics and runtime profiling data") 64 | 65 | persistentDbFile := flag.String("pdb", PERSISTENT_DB_FILE, "Persistent storage DB file") 66 | vdevDbFile := flag.String("vdb", VIRTUAL_DEVICES_DB_FILE, "Virtual devices values DB file") 67 | 68 | wbgoso := flag.String("wbgo", WBGO_FILE, "Location to wbgo.so file") 69 | 70 | flag.Parse() 71 | 72 | if flag.NArg() < 1 { 73 | wbgong.Error.Fatal("must specify rule file/directory name(s)") 74 | } 75 | if *useSyslog { 76 | wbgong.UseSyslog() 77 | } 78 | if *debug { 79 | wbgong.SetDebuggingEnabled(true) 80 | } 81 | 82 | if *httpAddr != "" { 83 | http.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) { 84 | metrics.WritePrometheus(w, true) 85 | }) 86 | // debug/pprof handlers are registered in https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/net/http/pprof/pprof.go;l=93 87 | go func() { 88 | log.Println(http.ListenAndServe(*httpAddr, nil)) 89 | }() 90 | } 91 | 92 | errInit := wbgong.Init(*wbgoso) 93 | if errInit != nil { 94 | log.Fatalf("ERROR in init wbgo.so: '%s'", errInit) 95 | } 96 | 97 | if *mqttDebug { 98 | wbgong.EnableMQTTDebugLog(*useSyslog) 99 | } 100 | wbgong.MaybeInitProfiling(nil) 101 | 102 | // prepare exit signal channel 103 | exitCh := make(chan os.Signal, 1) 104 | signal.Notify(exitCh, syscall.SIGINT, syscall.SIGTERM) 105 | 106 | if *brokerAddress == DEFAULT_BROKER_URL && isSocket(MOSQUITTO_SOCK_FILE) { 107 | wbgong.Info.Println("broker URL is default and mosquitto socket detected, trying to connect via it") 108 | *brokerAddress = "unix://" + MOSQUITTO_SOCK_FILE 109 | } 110 | 111 | driverMqttClient := wbgong.NewPahoMQTTClient(*brokerAddress, DRIVER_CLIENT_ID) 112 | driverArgs := wbgong.NewDriverArgs(). 113 | SetId(DRIVER_CONV_ID). 114 | SetMqtt(driverMqttClient). 115 | SetUseStorage(*vdevDbFile != ""). 116 | SetStoragePath(*vdevDbFile). 117 | SetReownUnknownDevices(!*precise) 118 | 119 | if *noQueues { 120 | driverArgs.SetTesting() 121 | } 122 | 123 | driver, err := wbgong.NewDriverBase(driverArgs) 124 | if err != nil { 125 | wbgong.Error.Fatalf("error creating driver: %s", err) 126 | } 127 | 128 | wbgong.Info.Println("driver is created") 129 | 130 | if err := driver.StartLoop(); err != nil { 131 | wbgong.Error.Fatalf("error starting the driver: %s", err) 132 | } 133 | driver.WaitForReady() 134 | 135 | wbgong.Info.Println("driver loop is started") 136 | driver.SetFilter(&wbgong.AllDevicesFilter{}) 137 | 138 | wbgong.Info.Println("wait for driver to become ready") 139 | driver.WaitForReady() 140 | wbgong.Info.Println("driver is ready") 141 | defer driver.Close() 142 | defer driver.StopLoop() 143 | 144 | engineOptions := wbrules.NewESEngineOptions() 145 | engineOptions.SetPersistentDBFile(*persistentDbFile) 146 | engineOptions.SetModulesDirs(strings.Split(os.Getenv(WBRULES_MODULES_ENV), ":")) 147 | engineOptions.SetCleanupOnStop(*cleanup) 148 | 149 | if *noQueues { 150 | engineOptions.SetTesting(true) 151 | } 152 | 153 | engineMqttClient := wbgong.NewPahoMQTTClient(*brokerAddress, ENGINE_CLIENT_ID) 154 | engine, err := wbrules.NewESEngine(driver, engineMqttClient, engineOptions) 155 | if err != nil { 156 | wbgong.Error.Fatalf("error creating engine: %s", err) 157 | } 158 | engine.Start() 159 | defer engine.Stop() 160 | 161 | gotSome := false 162 | watcher := wbgong.NewDirWatcher("\\.js(\\"+wbrules.FILE_DISABLED_SUFFIX+")?$", engine) 163 | if *editDir != "" { 164 | engine.SetSourceRoot(*editDir) 165 | } 166 | for _, path := range flag.Args() { 167 | if err := watcher.Load(path); err != nil { 168 | wbgong.Error.Printf("error loading script file/dir %s: %s", path, err) 169 | } else { 170 | gotSome = true 171 | } 172 | } 173 | if !gotSome { 174 | wbgong.Error.Fatalf("no valid scripts found") 175 | } 176 | wbgong.Info.Println("all rule files are loaded") 177 | 178 | if *editDir != "" { 179 | rpc := wbgong.NewMQTTRPCServer(RPC_DRIVER_NAME, engineMqttClient) 180 | rpc.Register(wbrules.NewEditor(engine)) 181 | rpc.Start() 182 | defer rpc.Stop() 183 | } 184 | 185 | // wait for quit signal 186 | <-exitCh 187 | } 188 | -------------------------------------------------------------------------------- /wbrules/rule_shell_command_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | "time" 8 | 9 | "github.com/wirenboard/wbgong/testutils" 10 | ) 11 | 12 | type RuleShellCommandSuite struct { 13 | RuleSuiteBase 14 | } 15 | 16 | func (s *RuleShellCommandSuite) SetupTest() { 17 | s.SetupSkippingDefs("testrules_command.js") 18 | } 19 | 20 | func (s *RuleShellCommandSuite) fileExists(path string) bool { 21 | if _, err := os.Stat(path); err != nil { 22 | if os.IsNotExist(err) { 23 | return false 24 | } else { 25 | s.Require().Fail("unexpected error when checking for samplefile", "%s", err) 26 | } 27 | } 28 | return true 29 | } 30 | 31 | func (s *RuleShellCommandSuite) verifyFileExists(path string) { 32 | if !s.fileExists(path) { 33 | s.Require().Fail("file does not exist", "%s", path) 34 | } 35 | } 36 | 37 | func (s *RuleShellCommandSuite) TestRunShellCommand() { 38 | dir, cleanup := testutils.SetupTempDir(s.T()) 39 | defer cleanup() 40 | 41 | s.publish("/devices/somedev/controls/cmd/meta/type", "text", "somedev/cmd") 42 | s.publish("/devices/somedev/controls/cmdNoCallback/meta/type", "text", 43 | "somedev/cmdNoCallback") 44 | s.publish("/devices/somedev/controls/cmd", "initial_text", "somedev/cmd") 45 | s.publish("/devices/somedev/controls/cmd", "touch samplefile.txt", "somedev/cmd") 46 | s.Verify( 47 | "tst -> /devices/somedev/controls/cmd/meta/type: [text] (QoS 1, retained)", 48 | "tst -> /devices/somedev/controls/cmdNoCallback/meta/type: [text] (QoS 1, retained)", 49 | "tst -> /devices/somedev/controls/cmd: [initial_text] (QoS 1, retained)", 50 | "tst -> /devices/somedev/controls/cmd: [touch samplefile.txt] (QoS 1, retained)", 51 | "[info] cmd: touch samplefile.txt", 52 | "[info] exit(0): touch samplefile.txt", 53 | ) 54 | 55 | s.verifyFileExists(path.Join(dir, "samplefile.txt")) 56 | 57 | s.publish("/devices/somedev/controls/cmd", "touch nosuchdir/samplefile.txt 2>/dev/null", "somedev/cmd") 58 | s.Verify( 59 | "tst -> /devices/somedev/controls/cmd: [touch nosuchdir/samplefile.txt 2>/dev/null] (QoS 1, retained)", 60 | "[info] cmd: touch nosuchdir/samplefile.txt 2>/dev/null", 61 | "[info] exit(1): touch nosuchdir/samplefile.txt 2>/dev/null", // no such file or directory 62 | ) 63 | 64 | s.publish("/devices/somedev/controls/cmdNoCallback", "1", "somedev/cmdNoCallback") 65 | s.publish("/devices/somedev/controls/cmd", "touch samplefile1.txt", "somedev/cmd") 66 | s.Verify( 67 | "tst -> /devices/somedev/controls/cmdNoCallback: [1] (QoS 1, retained)", 68 | "tst -> /devices/somedev/controls/cmd: [touch samplefile1.txt] (QoS 1, retained)", 69 | "[info] cmd: touch samplefile1.txt", 70 | "[info] (no callback)", 71 | ) 72 | s.WaitFor(func() bool { 73 | return s.fileExists(path.Join(dir, "samplefile1.txt")) 74 | }) 75 | } 76 | 77 | func (s *RuleShellCommandSuite) TestRunShellCommandIO() { 78 | s.publish("/devices/somedev/controls/cmdWithOutput/meta/type", "text", 79 | "somedev/cmdWithOutput") 80 | s.publish("/devices/somedev/controls/cmdWithOutput", "initial_text", 81 | "somedev/cmdWithOutput") 82 | s.publish("/devices/somedev/controls/cmdWithOutput", "echo abc; echo qqq", 83 | "somedev/cmdWithOutput") 84 | s.Verify( 85 | "tst -> /devices/somedev/controls/cmdWithOutput/meta/type: [text] (QoS 1, retained)", 86 | "tst -> /devices/somedev/controls/cmdWithOutput: [initial_text] (QoS 1, retained)", 87 | "tst -> /devices/somedev/controls/cmdWithOutput: [echo abc; echo qqq] (QoS 1, retained)", 88 | "[info] cmdWithOutput: echo abc; echo qqq", 89 | "[info] exit(0): echo abc; echo qqq", 90 | "[info] output: abc", 91 | "[info] output: qqq", 92 | ) 93 | 94 | s.publish("/devices/somedev/controls/cmdWithOutput", "echo abc; echo qqq 1>&2; exit 1", 95 | "somedev/cmdWithOutput") 96 | s.Verify( 97 | "tst -> /devices/somedev/controls/cmdWithOutput: [echo abc; echo qqq 1>&2; exit 1] (QoS 1, retained)", 98 | "[info] cmdWithOutput: echo abc; echo qqq 1>&2; exit 1", 99 | "[info] exit(1): echo abc; echo qqq 1>&2; exit 1", 100 | "[info] output: abc", 101 | "[info] error: qqq", 102 | ) 103 | 104 | s.publish("/devices/somedev/controls/cmdWithOutput", "xxyz!sed s/x/y/g", 105 | "somedev/cmdWithOutput") 106 | s.Verify( 107 | "tst -> /devices/somedev/controls/cmdWithOutput: [xxyz!sed s/x/y/g] (QoS 1, retained)", 108 | "[info] cmdWithOutput: sed s/x/y/g", 109 | "[info] exit(0): sed s/x/y/g", 110 | "[info] output: yyyz", 111 | ) 112 | } 113 | 114 | // This test will fail if exitCallback for request runs on unloaded file 115 | func (s *RuleShellCommandSuite) TestCallbackCleanup() { 116 | _, cleanup := testutils.SetupTempDir(s.T()) 117 | defer cleanup() 118 | 119 | s.publish("/devices/somedev/controls/cmd/meta/type", "text", "somedev/cmd") 120 | s.publish("/devices/somedev/controls/cmdNoCallback/meta/type", "text", 121 | "somedev/cmdNoCallback") 122 | 123 | s.publish("/devices/somedev/controls/cmd", "initial_text", "somedev/cmd") 124 | s.publish("/devices/somedev/controls/cmd", "until [ -f fflag ]; do sleep 0.1; done", "somedev/cmd") 125 | 126 | s.Verify( 127 | "tst -> /devices/somedev/controls/cmd/meta/type: [text] (QoS 1, retained)", 128 | "tst -> /devices/somedev/controls/cmdNoCallback/meta/type: [text] (QoS 1, retained)", 129 | "tst -> /devices/somedev/controls/cmd: [initial_text] (QoS 1, retained)", 130 | "tst -> /devices/somedev/controls/cmd: [until [ -f fflag ]; do sleep 0.1; done] (QoS 1, retained)", 131 | "[info] cmd: until [ -f fflag ]; do sleep 0.1; done", 132 | ) 133 | 134 | // remove script file 135 | s.RemoveScript("testrules_command.js") 136 | 137 | s.Verify("[removed] testrules_command.js") 138 | 139 | // touch file 140 | if _, err := Spawn("touch", []string{"fflag"}, false, false, nil); err != nil { 141 | s.Ck("failed to run command standalone", err) 142 | } 143 | 144 | // load dummy script 145 | s.LiveLoadScript("testrules_empty.js") 146 | 147 | s.Verify("[changed] testrules_empty.js") 148 | 149 | // here we need to wait for script to react on flag file 150 | time.Sleep(500 * time.Millisecond) 151 | s.VerifyEmpty() 152 | } 153 | 154 | func TestRuleShellCommandSuite(t *testing.T) { 155 | testutils.RunSuites(t, 156 | new(RuleShellCommandSuite), 157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /wbrules/editor.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "strings" 7 | 8 | "github.com/wirenboard/wbgong" 9 | ) 10 | 11 | type Editor struct { 12 | locFileManager LocFileManager 13 | } 14 | 15 | type EditorError struct { 16 | code int32 17 | message string 18 | } 19 | 20 | func (err *EditorError) Error() string { 21 | return err.message 22 | } 23 | 24 | func (err *EditorError) ErrorCode() int32 { 25 | return err.code 26 | } 27 | 28 | const ( 29 | // no iota here because these values may be used 30 | // by external software 31 | EDITOR_ERROR_INVALID_PATH = 1000 32 | EDITOR_ERROR_LISTDIR = 1001 33 | EDITOR_ERROR_WRITE = 1002 34 | EDITOR_ERROR_FILE_NOT_FOUND = 1003 35 | EDITOR_ERROR_REMOVE = 1004 36 | EDITOR_ERROR_READ = 1005 37 | EDITOR_ERROR_RENAME = 1006 38 | EDITOR_ERROR_OVERWRITE = 1007 39 | EDITOR_ERROR_INVALID_EXT = 1008 40 | EDITOR_ERROR_INVALID_LEN = 1009 41 | ) 42 | 43 | var invalidExtensionError = &EditorError{EDITOR_ERROR_INVALID_EXT, "File name should ends with .js"} 44 | var invalidLenError = &EditorError{EDITOR_ERROR_INVALID_LEN, "File path should be shorter than or equal to 255 chars"} 45 | var listDirError = &EditorError{EDITOR_ERROR_LISTDIR, "Error listing the directory"} 46 | var readError = &EditorError{EDITOR_ERROR_WRITE, "Error reading the file"} 47 | var writeError = &EditorError{EDITOR_ERROR_WRITE, "Error writing the file"} 48 | var fileNotFoundError = &EditorError{EDITOR_ERROR_FILE_NOT_FOUND, "File not found"} 49 | var rmError = &EditorError{EDITOR_ERROR_REMOVE, "Error removing the file"} 50 | var renameError = &EditorError{EDITOR_ERROR_RENAME, "Error renaming the file"} 51 | var overwriteError = &EditorError{EDITOR_ERROR_OVERWRITE, "New-state file already exists"} 52 | 53 | func NewEditor(locFileManager LocFileManager) *Editor { 54 | return &Editor{locFileManager} 55 | } 56 | 57 | func (editor *Editor) List(args *struct{}, reply *[]LocFileEntry) (err error) { 58 | *reply, err = editor.locFileManager.ListSourceFiles() 59 | return 60 | } 61 | 62 | type EditorSaveArgs struct { 63 | Path string `json:"path"` 64 | Content string `json:"content"` 65 | } 66 | 67 | type EditorSaveResponse struct { 68 | Error interface{} `json:"error,omitempty"` 69 | Path string `json:"path"` 70 | Traceback []LocItem `json:"traceback,omitempty"` 71 | } 72 | 73 | func (editor *Editor) Save(args *EditorSaveArgs, reply *EditorSaveResponse) error { 74 | pth := path.Clean(args.Path) 75 | 76 | for strings.HasPrefix(pth, "/") { 77 | pth = pth[1:] 78 | } 79 | 80 | if !strings.HasSuffix(pth, ".js") { 81 | return invalidExtensionError 82 | } else if len(pth)+len(".js") > 255 { 83 | return invalidLenError 84 | } 85 | 86 | *reply = EditorSaveResponse{nil, pth, nil} 87 | 88 | // check if this file already exists and disabled, so update path 89 | if entry, err := editor.locateFile(pth); err == nil && !entry.Enabled { 90 | pth = pth + FILE_DISABLED_SUFFIX 91 | } 92 | 93 | err := editor.locFileManager.LiveWriteScript(pth, args.Content) 94 | switch err.(type) { 95 | case nil: 96 | return nil 97 | case ScriptError: 98 | reply.Error = err.Error() 99 | reply.Traceback = err.(ScriptError).Traceback 100 | default: 101 | wbgong.Error.Printf("error writing %s: %s", pth, err) 102 | return writeError 103 | } 104 | 105 | return nil 106 | } 107 | 108 | type EditorPathArgs struct { 109 | Path string `json:"path"` 110 | } 111 | 112 | func (editor *Editor) locateFile(virtualPath string) (*LocFileEntry, error) { 113 | entries, err := editor.locFileManager.ListSourceFiles() 114 | if err != nil { 115 | return nil, listDirError 116 | } 117 | 118 | for _, entry := range entries { 119 | if entry.VirtualPath != virtualPath { 120 | continue 121 | } 122 | return &entry, nil 123 | } 124 | 125 | return nil, fileNotFoundError 126 | } 127 | 128 | func (editor *Editor) Remove(args *EditorPathArgs, reply *bool) error { 129 | entry, err := editor.locateFile(args.Path) 130 | if err != nil { 131 | return err 132 | } 133 | if err = os.Remove(entry.PhysicalPath); err != nil { 134 | wbgong.Error.Printf("error removing %s: %s", entry.PhysicalPath, err) 135 | return rmError 136 | } 137 | *reply = true 138 | return nil 139 | } 140 | 141 | type EditorContentResponse struct { 142 | Content string `json:"content"` 143 | Enabled bool `json:"enabled"` 144 | Error *ScriptError `json:"error,omitempty"` 145 | } 146 | 147 | func (editor *Editor) Load(args *EditorPathArgs, reply *EditorContentResponse) error { 148 | entry, err := editor.locateFile(args.Path) 149 | if err != nil { 150 | return err 151 | } 152 | content, err := os.ReadFile(entry.PhysicalPath) 153 | if err != nil { 154 | wbgong.Error.Printf("error reading %s: %s", entry.PhysicalPath, err) 155 | return readError 156 | } 157 | *reply = EditorContentResponse{ 158 | string(content), 159 | entry.Enabled, 160 | entry.Error, 161 | } 162 | return nil 163 | } 164 | 165 | type EditorChangeStateArgs struct { 166 | Path string `json:"path"` 167 | State bool `json:"state"` 168 | } 169 | 170 | func (editor *Editor) ChangeState(args *EditorChangeStateArgs, reply *bool) error { 171 | entry, err := editor.locateFile(args.Path) 172 | 173 | if err != nil { 174 | return err 175 | } 176 | 177 | *reply = false 178 | 179 | // is state is not changed - just say about it 180 | if args.State == entry.Enabled { 181 | return nil 182 | } 183 | 184 | var newPath string 185 | // if we need to enable file, remove suffix 186 | // else add suffix 187 | if args.State { 188 | newPath = entry.PhysicalPath[:len(entry.PhysicalPath)-len(FILE_DISABLED_SUFFIX)] 189 | } else { 190 | newPath = entry.PhysicalPath + FILE_DISABLED_SUFFIX 191 | } 192 | 193 | // check overwrite 194 | if _, err = os.Stat(newPath); !os.IsNotExist(err) { 195 | wbgong.Error.Printf("can't rename %s to %s: looks like second file exists already, deal with this by yourself!", 196 | entry.PhysicalPath, newPath) 197 | return overwriteError 198 | } 199 | 200 | if err = os.Rename(entry.PhysicalPath, newPath); err != nil { 201 | wbgong.Error.Printf("error renaming %s to %s: %s", entry.PhysicalPath, newPath, err) 202 | return renameError 203 | } 204 | 205 | *reply = true 206 | return nil 207 | } 208 | 209 | type EditorRenameArgs struct { 210 | Path string `json:"path"` 211 | NewPath string `json:"new_path"` 212 | } 213 | 214 | func (editor *Editor) Rename(args *EditorRenameArgs, reply *bool) error { 215 | newPath := path.Clean(args.NewPath) 216 | 217 | for strings.HasPrefix(newPath, "/") { 218 | newPath = newPath[1:] 219 | } 220 | 221 | if !strings.HasSuffix(newPath, ".js") { 222 | return invalidExtensionError 223 | } else if len(newPath)+len(".js") > 255 { 224 | return invalidLenError 225 | } 226 | 227 | entry, err := editor.locateFile(args.Path) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | newPath = strings.Replace(entry.PhysicalPath, entry.VirtualPath, newPath, 1) 233 | 234 | if _, err = os.Stat(newPath); !os.IsNotExist(err) { 235 | wbgong.Error.Printf("can't rename %s to %s: looks like second file exists already, deal with this by yourself!", 236 | entry.PhysicalPath, newPath) 237 | return overwriteError 238 | } 239 | 240 | if err = os.Rename(entry.PhysicalPath, newPath); err != nil { 241 | wbgong.Error.Printf("error renaming %s to %s: %s", entry.PhysicalPath, newPath, err) 242 | return renameError 243 | } 244 | 245 | *reply = true 246 | return nil 247 | } 248 | -------------------------------------------------------------------------------- /wbrules/rule_location_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/wirenboard/wbgong/testutils" 8 | ) 9 | 10 | type RuleLocationSuite struct { 11 | RuleSuiteBase 12 | } 13 | 14 | func (s *RuleLocationSuite) SetupTest() { 15 | s.SetupSkippingDefs( 16 | "testrules_defhelper.js", 17 | "testrules_locations.js", 18 | "testrules_locations_dis.js.disabled", 19 | "loc1/testrules_more.js") 20 | 21 | // FIXME: need to wait for the engine to become ready because 22 | // the engine cannot be stopped before it's ready in the 23 | // context of the tests. 24 | ready := false 25 | var mtx sync.Mutex 26 | s.engine.WhenEngineReady(func() { 27 | mtx.Lock() 28 | ready = true 29 | mtx.Unlock() 30 | }) 31 | s.WaitFor(func() bool { 32 | mtx.Lock() 33 | defer mtx.Unlock() 34 | return ready 35 | }) 36 | } 37 | 38 | func (s *RuleLocationSuite) listSourceFiles() (entries []LocFileEntry) { 39 | var err error 40 | entries, err = s.engine.ListSourceFiles() 41 | s.Ck("ListSourceFiles", err) 42 | return 43 | } 44 | 45 | func (s *RuleLocationSuite) TestLocations() { 46 | s.Equal([]LocFileEntry{ 47 | { 48 | VirtualPath: "loc1/testrules_more.js", 49 | PhysicalPath: s.DataFilePath("loc1/testrules_more.js"), 50 | Enabled: true, 51 | Devices: []LocItem{ 52 | {4, "qqq"}, 53 | }, 54 | Rules: []LocItem{}, 55 | Timers: []LocItem{}, 56 | }, 57 | { 58 | VirtualPath: "testrules_defhelper.js", 59 | PhysicalPath: s.DataFilePath("testrules_defhelper.js"), 60 | Enabled: true, 61 | Devices: []LocItem{}, 62 | Rules: []LocItem{}, 63 | Timers: []LocItem{}, 64 | }, 65 | { 66 | VirtualPath: "testrules_locations.js", 67 | PhysicalPath: s.DataFilePath("testrules_locations.js"), 68 | Enabled: true, 69 | Devices: []LocItem{ 70 | {4, "misc"}, 71 | {14, "foo"}, 72 | }, 73 | Rules: []LocItem{ 74 | {7, "whateverRule"}, 75 | // the problem with duktape: the last line of the 76 | // defineRule() call is recorded 77 | {24, "another"}, 78 | }, 79 | Timers: []LocItem{}, 80 | }, 81 | { 82 | VirtualPath: "testrules_locations_dis.js", 83 | PhysicalPath: s.DataFilePath("testrules_locations_dis.js.disabled"), 84 | Enabled: false, 85 | Devices: []LocItem{}, 86 | Rules: []LocItem{}, 87 | Timers: []LocItem{}, 88 | }, 89 | }, s.listSourceFiles()) 90 | } 91 | 92 | func (s *RuleLocationSuite) TestUpdatingLocations() { 93 | s.ReplaceScript("testrules_locations.js", "testrules_locations_changed.js") 94 | s.ReplaceScript("loc1/testrules_more.js", "loc1/testrules_more_changed.js") 95 | s.Equal([]LocFileEntry{ 96 | { 97 | VirtualPath: "loc1/testrules_more.js", 98 | PhysicalPath: s.DataFilePath("loc1/testrules_more.js"), 99 | Enabled: true, 100 | Devices: []LocItem{ 101 | {4, "qqqNew"}, 102 | }, 103 | Rules: []LocItem{}, 104 | Timers: []LocItem{}, 105 | }, 106 | { 107 | VirtualPath: "testrules_defhelper.js", 108 | PhysicalPath: s.DataFilePath("testrules_defhelper.js"), 109 | Enabled: true, 110 | Devices: []LocItem{}, 111 | Rules: []LocItem{}, 112 | Timers: []LocItem{}, 113 | }, 114 | { 115 | VirtualPath: "testrules_locations.js", 116 | PhysicalPath: s.DataFilePath("testrules_locations.js"), 117 | Enabled: true, 118 | Devices: []LocItem{ 119 | {4, "miscNew"}, 120 | {14, "foo"}, 121 | }, 122 | Rules: []LocItem{ 123 | {7, "whateverNewRule"}, 124 | // a problem with duktape: the last line of the 125 | // defineRule() call is recorded 126 | {24, "another"}, 127 | }, 128 | Timers: []LocItem{}, 129 | }, 130 | { 131 | VirtualPath: "testrules_locations_dis.js", 132 | PhysicalPath: s.DataFilePath("testrules_locations_dis.js.disabled"), 133 | Enabled: false, 134 | Devices: []LocItem{}, 135 | Rules: []LocItem{}, 136 | Timers: []LocItem{}, 137 | }, 138 | }, s.listSourceFiles()) 139 | s.SkipTill("[changed] loc1/testrules_more.js") 140 | } 141 | 142 | func (s *RuleLocationSuite) TestRemoval() { 143 | s.RemoveScript("testrules_locations.js") 144 | s.WaitFor(func() bool { 145 | return len(s.listSourceFiles()) == 3 146 | }) 147 | s.Equal([]LocFileEntry{ 148 | { 149 | VirtualPath: "loc1/testrules_more.js", 150 | PhysicalPath: s.DataFilePath("loc1/testrules_more.js"), 151 | Enabled: true, 152 | Devices: []LocItem{ 153 | {4, "qqq"}, 154 | }, 155 | Rules: []LocItem{}, 156 | Timers: []LocItem{}, 157 | }, 158 | { 159 | VirtualPath: "testrules_defhelper.js", 160 | PhysicalPath: s.DataFilePath("testrules_defhelper.js"), 161 | Enabled: true, 162 | Devices: []LocItem{}, 163 | Rules: []LocItem{}, 164 | Timers: []LocItem{}, 165 | }, 166 | { 167 | VirtualPath: "testrules_locations_dis.js", 168 | PhysicalPath: s.DataFilePath("testrules_locations_dis.js.disabled"), 169 | Enabled: false, 170 | Devices: []LocItem{}, 171 | Rules: []LocItem{}, 172 | Timers: []LocItem{}, 173 | }, 174 | }, s.listSourceFiles()) 175 | s.SkipTill("[removed] testrules_locations.js") 176 | 177 | s.RemoveScript("loc1/testrules_more.js") 178 | s.WaitFor(func() bool { 179 | return len(s.listSourceFiles()) == 2 180 | }) 181 | s.Equal([]LocFileEntry{ 182 | { 183 | VirtualPath: "testrules_defhelper.js", 184 | PhysicalPath: s.DataFilePath("testrules_defhelper.js"), 185 | Enabled: true, 186 | Devices: []LocItem{}, 187 | Rules: []LocItem{}, 188 | Timers: []LocItem{}, 189 | }, 190 | { 191 | VirtualPath: "testrules_locations_dis.js", 192 | PhysicalPath: s.DataFilePath("testrules_locations_dis.js.disabled"), 193 | Enabled: false, 194 | Devices: []LocItem{}, 195 | Rules: []LocItem{}, 196 | Timers: []LocItem{}, 197 | }, 198 | }, s.listSourceFiles()) 199 | s.SkipTill("[removed] loc1/testrules_more.js") 200 | } 201 | 202 | func (s *RuleLocationSuite) TestFaultyScript() { 203 | err := s.OverwriteScript("testrules_locations_faulty.js", "testrules_locations_faulty.js") 204 | s.NotNil(err, "error expected") 205 | scriptErr, ok := err.(ScriptError) 206 | s.Require().True(ok, "ScriptError expected") 207 | s.Contains(scriptErr.Message, "ReferenceError") 208 | s.Equal([]LocItem{ 209 | {6, "testrules_locations_faulty.js"}, 210 | }, scriptErr.Traceback) 211 | s.Equal([]LocFileEntry{ 212 | { 213 | VirtualPath: "loc1/testrules_more.js", 214 | PhysicalPath: s.DataFilePath("loc1/testrules_more.js"), 215 | Enabled: true, 216 | Devices: []LocItem{ 217 | {4, "qqq"}, 218 | }, 219 | Rules: []LocItem{}, 220 | Timers: []LocItem{}, 221 | }, 222 | { 223 | VirtualPath: "testrules_defhelper.js", 224 | PhysicalPath: s.DataFilePath("testrules_defhelper.js"), 225 | Enabled: true, 226 | Devices: []LocItem{}, 227 | Rules: []LocItem{}, 228 | Timers: []LocItem{}, 229 | }, 230 | { 231 | VirtualPath: "testrules_locations.js", 232 | PhysicalPath: s.DataFilePath("testrules_locations.js"), 233 | Enabled: true, 234 | Devices: []LocItem{ 235 | {4, "misc"}, 236 | {14, "foo"}, 237 | }, 238 | Rules: []LocItem{ 239 | {7, "whateverRule"}, 240 | // the problem with duktape: the last line of the 241 | // defineRule() call is recorded 242 | {24, "another"}, 243 | }, 244 | Timers: []LocItem{}, 245 | }, 246 | { 247 | VirtualPath: "testrules_locations_dis.js", 248 | PhysicalPath: s.DataFilePath("testrules_locations_dis.js.disabled"), 249 | Enabled: false, 250 | Devices: []LocItem{}, 251 | Rules: []LocItem{}, 252 | Timers: []LocItem{}, 253 | }, 254 | { 255 | VirtualPath: "testrules_locations_faulty.js", 256 | PhysicalPath: s.DataFilePath("testrules_locations_faulty.js"), 257 | Enabled: true, 258 | Devices: []LocItem{ 259 | {4, "nonFaultyDev"}, 260 | }, 261 | Rules: []LocItem{}, 262 | Timers: []LocItem{}, 263 | Error: &scriptErr, 264 | }, 265 | }, s.listSourceFiles()) 266 | 267 | s.SkipTill("[changed] testrules_locations_faulty.js") 268 | } 269 | 270 | func (s *RuleLocationSuite) TestSyntaxError() { 271 | err := s.OverwriteScript( 272 | "testrules_locations_syntax_error.js", 273 | "testrules_locations_syntax_error.js") 274 | s.NotNil(err, "error expected") 275 | scriptErr, ok := err.(ScriptError) 276 | s.Require().True(ok, "ScriptError expected") 277 | s.Contains(scriptErr.Message, "SyntaxError") 278 | s.Equal([]LocItem{ 279 | {4, "testrules_locations_syntax_error.js"}, 280 | }, scriptErr.Traceback) 281 | 282 | s.SkipTill("[changed] testrules_locations_syntax_error.js") 283 | } 284 | 285 | func TestRuleLocationSuite(t *testing.T) { 286 | testutils.RunSuites(t, 287 | new(RuleLocationSuite), 288 | ) 289 | } 290 | -------------------------------------------------------------------------------- /wbrules/rule_controls_api_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleControlsAPISuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleControlsAPISuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules_controls_api.js") 15 | } 16 | 17 | func (s *RuleControlsAPISuite) TestAPI() { 18 | // spawn new control 19 | s.publish("/devices/spawner/controls/spawn/on", "1", "spawner/spawn") 20 | s.VerifyUnordered( 21 | "Subscribe -- driver: /devices/spawner/controls/wrCtrlID/on", 22 | "driver -> /devices/spawner/controls/spawn: [1] (QoS 1, retained)", 23 | "driver -> /devices/spawner/controls/wrCtrlID/meta/order: [4] (QoS 1, retained)", 24 | "driver -> /devices/spawner/controls/wrCtrlID/meta/readonly: [0] (QoS 1, retained)", 25 | "driver -> /devices/spawner/controls/wrCtrlID/meta/type: [text] (QoS 1, retained)", 26 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"order\":4,\"readonly\":false,\"type\":\"text\"}] (QoS 1, retained)", 27 | "driver -> /devices/spawner/controls/wrCtrlID: [test-text] (QoS 1, retained)", 28 | "tst -> /devices/spawner/controls/spawn/on: [1] (QoS 1)", 29 | "wbrules-log -> /wbrules/log/info: [ctrlID: change, error: ] (QoS 1)", 30 | "wbrules-log -> /wbrules/log/info: [ctrlID: check, error: ] (QoS 1)", 31 | "wbrules-log -> /wbrules/log/info: [ctrlID: spawn, error: ] (QoS 1)", 32 | ) 33 | 34 | // change control metadata by API from script 35 | s.publish("/devices/spawner/controls/change/on", "1", "spawner/change", "spawner/wrCtrlID") 36 | s.VerifyUnordered( 37 | "driver -> /devices/spawner/controls/change: [1] (QoS 1, retained)", 38 | "driver -> /devices/spawner/controls/wrCtrlID/meta/description: [true Description] (QoS 1, retained)", 39 | "driver -> /devices/spawner/controls/wrCtrlID/meta/error: [new Error] (QoS 1, retained)", 40 | "driver -> /devices/spawner/controls/wrCtrlID/meta/max: [255] (QoS 1, retained)", 41 | "driver -> /devices/spawner/controls/wrCtrlID/meta/min: [5] (QoS 1, retained)", 42 | "driver -> /devices/spawner/controls/wrCtrlID/meta/order: [5] (QoS 1, retained)", 43 | "driver -> /devices/spawner/controls/wrCtrlID/meta/readonly: [1] (QoS 1, retained)", 44 | "driver -> /devices/spawner/controls/wrCtrlID/meta/type: [range] (QoS 1, retained)", 45 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"error\":\"new Error\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\",\"units\":\"meters\"}] (QoS 1, retained)", 46 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":false,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\"}] (QoS 1, retained)", 47 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\",\"units\":\"meters\"}] (QoS 1, retained)", 48 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\"}] (QoS 1, retained)", 49 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"max\":255,\"order\":5,\"readonly\":false,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\"}] (QoS 1, retained)", 50 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"order\":4,\"readonly\":false,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\"}] (QoS 1, retained)", 51 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"order\":4,\"readonly\":false,\"title\":{\"en\":\"newTitle\"},\"type\":\"text\"}] (QoS 1, retained)", 52 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"order\":4,\"readonly\":false,\"type\":\"text\"}] (QoS 1, retained)", 53 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"true Description\",\"order\":5,\"readonly\":false,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\"}] (QoS 1, retained)", 54 | "driver -> /devices/spawner/controls/wrCtrlID: [42] (QoS 1, retained)", "tst -> /devices/spawner/controls/change/on: [1] (QoS 1)", 55 | ) 56 | 57 | // check getters API inside script 58 | s.publish("/devices/spawner/controls/check/on", "1", "spawner/check") 59 | s.VerifyUnordered( 60 | "driver -> /devices/spawner/controls/check: [1] (QoS 1, retained)", 61 | "tst -> /devices/spawner/controls/check/on: [1] (QoS 1)", 62 | "wbrules-log -> /wbrules/log/info: [ctrlID: somedev, isVirtual: false] (QoS 1)", 63 | "wbrules-log -> /wbrules/log/info: [ctrlID: spawner, isVirtual: true] (QoS 1)", 64 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, error: new Error] (QoS 1)", 65 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, max: 255] (QoS 1)", 66 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, min: 5] (QoS 1)", 67 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, order: 5] (QoS 1)", 68 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, readonly: true] (QoS 1)", 69 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, title: newTitle] (QoS 1)", 70 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, type: range] (QoS 1)", 71 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, units: meters] (QoS 1)", 72 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, value: 42] (QoS 1)", 73 | ) 74 | 75 | // change control metadata by API from script 76 | s.publish("/devices/spawner/controls/change/on", "0", "spawner/change") 77 | s.VerifyUnordered( 78 | "driver -> /devices/spawner/controls/change: [0] (QoS 1, retained)", 79 | "driver -> /devices/spawner/controls/wrCtrlID/meta/description: [new Description] (QoS 1, retained)", 80 | "driver -> /devices/spawner/controls/wrCtrlID/meta/error: [] (QoS 1, retained)", 81 | "driver -> /devices/spawner/controls/wrCtrlID/meta/max: [0] (QoS 1, retained)", 82 | "driver -> /devices/spawner/controls/wrCtrlID/meta/min: [0] (QoS 1, retained)", 83 | "driver -> /devices/spawner/controls/wrCtrlID/meta/order: [4] (QoS 1, retained)", 84 | "driver -> /devices/spawner/controls/wrCtrlID/meta/readonly: [0] (QoS 1, retained)", 85 | "driver -> /devices/spawner/controls/wrCtrlID/meta/type: [text] (QoS 1, retained)", 86 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":4,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"meters\"}] (QoS 1, retained)", 87 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"range\",\"units\":\"meters\"}] (QoS 1, retained)", 88 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":5,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"meters\"}] (QoS 1, retained)", 89 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":4,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"meters\"}] (QoS 1, retained)", 90 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":4,\"readonly\":false,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"chars\"}] (QoS 1, retained)", 91 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":4,\"readonly\":false,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"meters\"}] (QoS 1, retained)", 92 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"\",\"order\":4,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"text\",\"units\":\"meters\"}] (QoS 1, retained)", 93 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"new Error\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"newTitle\"},\"type\":\"range\",\"units\":\"meters\"}] (QoS 1, retained)", 94 | "driver -> /devices/spawner/controls/wrCtrlID/meta: [{\"description\":\"new Description\",\"error\":\"new Error\",\"max\":255,\"min\":5,\"order\":5,\"readonly\":true,\"title\":{\"en\":\"oldTitle\"},\"type\":\"range\",\"units\":\"meters\"}] (QoS 1, retained)", 95 | "tst -> /devices/spawner/controls/change/on: [0] (QoS 1)", 96 | ) 97 | 98 | // check getters API inside script 99 | s.publish("/devices/spawner/controls/check/on", "0", "spawner/check") 100 | s.VerifyUnordered( 101 | "driver -> /devices/spawner/controls/check: [0] (QoS 1, retained)", 102 | "tst -> /devices/spawner/controls/check/on: [0] (QoS 1)", "wbrules-log -> /wbrules/log/info: [ctrlID: somedev, isVirtual: false] (QoS 1)", 103 | "wbrules-log -> /wbrules/log/info: [ctrlID: spawner, isVirtual: true] (QoS 1)", 104 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, error: ] (QoS 1)", "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, max: 0] (QoS 1)", 105 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, min: 0] (QoS 1)", "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, order: 4] (QoS 1)", 106 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, readonly: false] (QoS 1)", "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, title: oldTitle] (QoS 1)", 107 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, type: text] (QoS 1)", "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, units: chars] (QoS 1)", 108 | "wbrules-log -> /wbrules/log/info: [ctrlID: wrCtrlID, value: 42] (QoS 1)", 109 | ) 110 | s.VerifyEmpty() 111 | } 112 | 113 | func TestRuleControlsAPISuite(t *testing.T) { 114 | testutils.RunSuites(t, 115 | new(RuleControlsAPISuite), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /rules/alarms-restricted.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "title": "Alarm Configuration", 5 | "description": "Lists alarms", 6 | "strictProps": false, 7 | "limited": true, 8 | "definitions": { 9 | "emailRecipient": { 10 | "title": "E-mail recipient", 11 | "type": "object", 12 | "properties": { 13 | "type": { 14 | "type": "string", 15 | "title": "Type", 16 | "enum": ["email"], 17 | "default": "email", 18 | "options": { 19 | "hidden": true 20 | }, 21 | "propertyOrder": 1 22 | }, 23 | "to": { 24 | "type": "string", 25 | "title": "E-mail address", 26 | "minLength": 1, 27 | "propertyOrder": 2 28 | }, 29 | "subject": { 30 | "type": "string", 31 | "title": "Subject", 32 | "description": "{} will be replaced with alarm message text", 33 | "propertyOrder": 3 34 | } 35 | }, 36 | "required": ["type", "to"] 37 | }, 38 | "smsRecipient": { 39 | "title": "SMS recipient", 40 | "type": "object", 41 | "properties": { 42 | "type": { 43 | "type": "string", 44 | "title": "Type", 45 | "enum": ["sms"], 46 | "default": "sms", 47 | "options": { 48 | "hidden": true 49 | }, 50 | "propertyOrder": 1 51 | }, 52 | "to": { 53 | "type": "string", 54 | "title": "Phone number", 55 | "minLength": 1, 56 | "propertyOrder": 2 57 | }, 58 | "command": { 59 | "type": "string", 60 | "title": "Command", 61 | "propertyOrder": 3 62 | } 63 | }, 64 | "required": ["type", "to"] 65 | }, 66 | "telegramRecipient": { 67 | "title": "Telegram Bot", 68 | "type": "object", 69 | "properties": { 70 | "type": { 71 | "type": "string", 72 | "title": "Type", 73 | "enum": ["telegram"], 74 | "default": "telegram", 75 | "options": { 76 | "hidden": true 77 | }, 78 | "propertyOrder": 1 79 | }, 80 | "token": { 81 | "type": "string", 82 | "title": "Bot token", 83 | "description": "A token can be obtained from @BotFather", 84 | "minLength": 1, 85 | "propertyOrder": 2 86 | }, 87 | "chatId": { 88 | "type": "string", 89 | "title": "Chat ID", 90 | "description": "The chat ID can be obtained from @getidsbot", 91 | "propertyOrder": 3 92 | } 93 | }, 94 | "required": ["type", "token", "chatId"] 95 | }, 96 | "alarmBase": { 97 | "type": "object", 98 | "properties": { 99 | "name": { 100 | "type": "string", 101 | "title": "Alarm name", 102 | "minLength": 1, 103 | "propertyOrder": 1, 104 | "options": { 105 | "hidden": true 106 | } 107 | }, 108 | "cell": { 109 | "type": "string", 110 | "title": "Cell", 111 | "description": "Use the following format: device/control", 112 | "pattern": "^[^/+#]+/[^/+#]+$", 113 | "minLength": 3, 114 | "propertyOrder": 2, 115 | "options": { 116 | "hidden": true 117 | } 118 | }, 119 | "alarmMessage": { 120 | "type": "string", 121 | "title": "Alarm activation message", 122 | "description": "{} will be replaced by target cell value", 123 | "propertyOrder": 3, 124 | "options": { 125 | "hidden": true 126 | } 127 | }, 128 | "noAlarmMessage": { 129 | "type": "string", 130 | "title": "Alarm deactivation message", 131 | "description": "{} will be replaced by target cell value", 132 | "propertyOrder": 4, 133 | "options": { 134 | "hidden": true 135 | } 136 | }, 137 | "interval": { 138 | "type": "integer", 139 | "title": "Alarm interval in seconds", 140 | "description": "If specified, alarm messages will be repeated while the alarm is active", 141 | "propertyOrder": 5 142 | }, 143 | "maxCount": { 144 | "type": "integer", 145 | "title": "Maximum number of messages", 146 | "description": "Maximum number of messages to send while the alarm is active", 147 | "propertyOrder": 6 148 | } 149 | } 150 | }, 151 | "minValue": { 152 | "type": "number", 153 | "title": "Minimum value", 154 | "description": "Alarm activates when cell value is less than the minimum value" 155 | }, 156 | "maxValue": { 157 | "type": "number", 158 | "title": "Maximum value", 159 | "description": "Alarm activates when cell value is greater than the maximum value" 160 | }, 161 | "expectedValueAlarm": { 162 | "title": "Expected Value Alarm", 163 | "defaultProperties": ["name", "cell", "alarmMessage", "noAlarmMessage", "expectedValue"], 164 | "allOf": [ 165 | { "$ref": "#/definitions/alarmBase" }, 166 | { 167 | "properties": { 168 | "expectedValue": { 169 | "title": "Expected value", 170 | "description": "Alarm activates when cell value differs from the expected value" 171 | } 172 | }, 173 | "required": ["name", "cell", "expectedValue"] 174 | } 175 | ] 176 | }, 177 | "minValueAlarm": { 178 | "title": "Minimum Value Alarm", 179 | "defaultProperties": ["name", "cell", "alarmMessage", "noAlarmMessage", "minValue"], 180 | "allOf": [ 181 | { "$ref": "#/definitions/alarmBase" }, 182 | { 183 | "properties": { 184 | "minValue": { 185 | "$ref": "#/definitions/minValue", 186 | "propertyOrder": 10 187 | } 188 | }, 189 | "required": ["name", "cell", "minValue"] 190 | }, 191 | { 192 | "not": { 193 | "required" : ["maxValue"] 194 | } 195 | } ] 196 | }, 197 | "maxValueAlarm": { 198 | "title": "Maximum Value Alarm", 199 | "defaultProperties": ["name", "cell", "alarmMessage", "noAlarmMessage", "maxValue"], 200 | "allOf": [ 201 | { "$ref": "#/definitions/alarmBase" }, 202 | { 203 | "properties": { 204 | "maxValue": { 205 | "$ref": "#/definitions/maxValue", 206 | "propertyOrder": 10 207 | } 208 | }, 209 | "required": ["name", "cell", "maxValue"] 210 | }, 211 | { 212 | "not": { 213 | "required" : ["minValue"] 214 | } 215 | } 216 | ] 217 | }, 218 | "minMaxValueAlarm": { 219 | "title": "Minimum&Maximum Value Alarm", 220 | "defaultProperties": ["name", "cell", "alarmMessage", "noAlarmMessage", "minValue", "maxValue"], 221 | "allOf": [ 222 | { "$ref": "#/definitions/alarmBase" }, 223 | { 224 | "properties": { 225 | "minValue": { 226 | "$ref": "#/definitions/minValue", 227 | "propertyOrder": 10 228 | }, 229 | "maxValue": { 230 | "$ref": "#/definitions/maxValue", 231 | "propertyOrder": 11 232 | } 233 | }, 234 | "required": ["name", "cell", "minValue", "maxValue"] 235 | }, 236 | 237 | ] 238 | }, 239 | "recipient": { 240 | "options": { 241 | "remove_empty_properties": true, 242 | "keep_oneof_values": false 243 | }, 244 | "title" : "Recipient", 245 | "oneOf": [ 246 | { "$ref": "#/definitions/emailRecipient" }, 247 | { "$ref": "#/definitions/smsRecipient" }, 248 | { "$ref": "#/definitions/telegramRecipient" } 249 | ], 250 | "options": { 251 | "disable_collapse" : true 252 | } 253 | }, 254 | "alarm": { 255 | "headerTemplate": "Alarm{{: |self.name}}", 256 | "oneOf": [ 257 | { "$ref": "#/definitions/expectedValueAlarm" }, 258 | { "$ref": "#/definitions/minValueAlarm" }, 259 | { "$ref": "#/definitions/maxValueAlarm" }, 260 | { "$ref": "#/definitions/minMaxValueAlarm" } 261 | ], 262 | "options": { 263 | "disable_collapse" : true, 264 | "remove_empty_properties": true 265 | } 266 | 267 | } 268 | }, 269 | "properties": { 270 | "deviceName": { 271 | "type": "string", 272 | "title": "Alarm device name", 273 | "description": "Device name to be used in MQTT topics for logging & alarm cells", 274 | "pattern": "^[^\\s/]+$", 275 | "minLength": 1, 276 | "propertyOrder": 6, 277 | "options" : { 278 | "hidden" : true 279 | } 280 | }, 281 | "deviceTitle": { 282 | "type": "string", 283 | "title": "Alarm device title", 284 | "propertyOrder": 7, 285 | "options" : { 286 | "hidden" : true 287 | } 288 | }, 289 | "recipients": { 290 | "type": "array", 291 | "title": "Recipients", 292 | "items": { "$ref": "#/definitions/recipient" }, 293 | "propertyOrder": 2, 294 | "options" : { 295 | "disable_array_reorder" : true 296 | } 297 | }, 298 | "alarms": { 299 | "type": "array", 300 | "title": "Alarms", 301 | "items": { "$ref": "#/definitions/alarm" }, 302 | "propertyOrder": 1, 303 | "options": { 304 | "disable_collapse" : true, 305 | "disable_array_reorder" : true, 306 | "disable_array_delete" : true, 307 | "disable_array_add" : true, 308 | "disable_array_reorder" : true 309 | } 310 | } 311 | }, 312 | "required": ["deviceName", "recipients", "alarms"], 313 | "configFile": { 314 | "path": "/etc/wb-rules/alarms.conf", 315 | "service": "wb-rules-alarms-reloader" 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /wbrules/rule_alarm_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/wirenboard/wbgong/testutils" 13 | ) 14 | 15 | type AlarmSuite struct { 16 | RuleSuiteBase 17 | } 18 | 19 | func (s *AlarmSuite) SetupTest() { 20 | s.SetupSkippingDefs("testrules_alarm.js") 21 | s.publishTestDev() 22 | 23 | } 24 | 25 | func (s *AlarmSuite) loadAlarms(config string, alarm string) { 26 | s.loadAlarmsSkipping(config, "", alarm) 27 | } 28 | 29 | func (s *AlarmSuite) loadAlarmsSkipping(config string, skipLine string, alarm string) { 30 | confPath := s.CopyModifiedDataFileToTempDir(config, config, func(text string) string { 31 | if skipLine == "" { 32 | return text 33 | } 34 | lines := strings.Split(text, "\n") 35 | out := make([]string, 0, len(lines)) 36 | for _, line := range lines { 37 | if !strings.Contains(line, skipLine) { 38 | out = append(out, line) 39 | } 40 | } 41 | return strings.Join(out, "\n") 42 | }) 43 | confPathJS, err := json.Marshal(confPath) 44 | if err != nil { 45 | panic("json.Marshal() failed on string?") 46 | } 47 | // here we simulate loading of alarms into the running engine 48 | s.Ck("failed to init alarms", s.engine.EvalScript(fmt.Sprintf("Alarms.load(%s)", confPathJS))) 49 | s.engine.Refresh() 50 | 51 | s.VerifyUnordered( 52 | "driver -> /devices/sampleAlarms/meta: [{\"driver\":\"wbrules\",\"title\":{\"en\":\"Sample Alarms\"}}] (QoS 1, retained)", 53 | "driver -> /devices/sampleAlarms/meta/name: [Sample Alarms] (QoS 1, retained)", 54 | "driver -> /devices/sampleAlarms/meta/driver: [wbrules] (QoS 1, retained)", 55 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s/meta/type: [alarm] (QoS 1, retained)", alarm), 56 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s/meta/readonly: [1] (QoS 1, retained)", alarm), 57 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s/meta/order: [1] (QoS 1, retained)", alarm), 58 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s/meta: [{\"order\":1,\"readonly\":true,\"type\":\"alarm\"}] (QoS 1, retained)", alarm), 59 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s: [0] (QoS 1, retained)", alarm), 60 | fmt.Sprintf("Subscribe -- driver: /devices/sampleAlarms/controls/alarm_%s/on", alarm), 61 | "driver -> /devices/sampleAlarms/controls/log/meta/type: [text] (QoS 1, retained)", 62 | "driver -> /devices/sampleAlarms/controls/log/meta/readonly: [1] (QoS 1, retained)", 63 | "driver -> /devices/sampleAlarms/controls/log/meta/order: [2] (QoS 1, retained)", 64 | "driver -> /devices/sampleAlarms/controls/log/meta: [{\"order\":2,\"readonly\":true,\"title\":{\"en\":\"Log\",\"ru\":\"Лог\"},\"type\":\"text\"}] (QoS 1, retained)", 65 | "driver -> /devices/sampleAlarms/controls/log: [] (QoS 1, retained)", 66 | "Subscribe -- driver: /devices/sampleAlarms/controls/log/on", 67 | ) 68 | } 69 | 70 | func (s *AlarmSuite) controlRef(dev, ctl string) (controlRef, topicBase string) { 71 | return dev + "/" + ctl, fmt.Sprintf("/devices/%s/controls/%s", dev, ctl) 72 | } 73 | 74 | func (s *AlarmSuite) publishControl(dev, ctl, typ, value string) { 75 | controlRef, topicBase := s.controlRef(dev, ctl) 76 | s.publish(topicBase+"/meta/type", typ, controlRef) 77 | s.publish(topicBase, value, controlRef) 78 | s.Verify( 79 | fmt.Sprintf("tst -> %s/meta/type: [%s] (QoS 1, retained)", topicBase, typ), 80 | fmt.Sprintf("tst -> %s: [%s] (QoS 1, retained)", topicBase, value), 81 | ) 82 | } 83 | 84 | func (s *AlarmSuite) publishTestDev() { 85 | s.publishControl("somedev", "importantDevicePower", "switch", "1") 86 | s.publishControl("somedev", "unnecessaryDevicePower", "switch", "0") 87 | s.publishControl("somedev", "devTemp", "temperature", "11") 88 | } 89 | 90 | func (s *AlarmSuite) publishControlValue(dev, ctl, value string, expectedControlNames ...string) { 91 | controlRef, topicBase := s.controlRef(dev, ctl) 92 | s.publish(topicBase, value, append([]string{controlRef}, expectedControlNames...)...) 93 | s.Verify(fmt.Sprintf("tst -> %s: [%s] (QoS 1, retained)", topicBase, value)) 94 | } 95 | 96 | func (s *AlarmSuite) verifyAlarmControlChange(name string, active bool) { 97 | activeStr := "0" 98 | if active { 99 | activeStr = "1" 100 | } 101 | s.Verify( 102 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s: [%s] (QoS 1, retained)", name, activeStr), 103 | ) 104 | } 105 | 106 | func (s *AlarmSuite) verifyNotificationMsgs(alarm string, text string, stopTimer bool, updateMeta bool) { 107 | if updateMeta { 108 | s.Verify( 109 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/alarm_%s/meta: [{\"order\":1,\"readonly\":true,\"title\":{\"en\":\"%s\"},\"type\":\"alarm\"}] (QoS 1, retained)", alarm, text), 110 | ) 111 | } 112 | if stopTimer { 113 | s.Verify(regexp.MustCompile(`^timer\.Stop\(\): \d+`)) 114 | } 115 | s.Verify( 116 | fmt.Sprintf("driver -> /devices/sampleAlarms/controls/log: [%s] (QoS 1, retained)", text), 117 | fmt.Sprintf("[info] EMAIL TO: someone@example.com SUBJ: alarm! TEXT: %s", text), 118 | fmt.Sprintf("[info] EMAIL TO: anotherone@example.com SUBJ: Alarm: %s TEXT: %s", text, text), 119 | fmt.Sprintf("[info] SMS TO: +78122128506 TEXT: %s", text), 120 | fmt.Sprintf("[info] TELEGRAM MESSAGE TOKEN: 1234567890:AAHG7MAKsUHLs-pBLhpIw1RU07Hmw9LyDac CHATID: 123456789 TEXT: %s", text), 121 | ) 122 | } 123 | 124 | func (s *AlarmSuite) TestRepeatedExpectedValueAlarm() { 125 | s.loadAlarms("alarms.conf", "importantDeviceIsOff") 126 | for i := 0; i < 3; i++ { 127 | s.publishControlValue("somedev", "importantDevicePower", "0", 128 | "sampleAlarms/alarm_importantDeviceIsOff", "sampleAlarms/log") 129 | s.verifyAlarmControlChange("importantDeviceIsOff", true) 130 | s.verifyNotificationMsgs("importantDeviceIsOff", "Important device is off", false, true) 131 | var timerId int 132 | s.Verify(testutils.RegexpCaptureMatcher( 133 | `^new fake ticker: (\d+), 200000`, func(m []string) bool { 134 | var err error 135 | timerId, err = strconv.Atoi(m[1]) 136 | s.Ck("Atoi()", err) 137 | return true 138 | })) 139 | 140 | // no repeated alarm upon the same value 141 | s.publishControlValue("somedev", "importantDevicePower", "0") 142 | s.VerifyEmpty() 143 | 144 | for j := 0; j < 3; j++ { 145 | ts := s.AdvanceTime(200 * time.Second) 146 | s.FireTimer(uint64(timerId), ts) 147 | s.Verify(fmt.Sprintf("timer.fire(): %d", timerId)) 148 | s.expectControlChange("sampleAlarms/log") 149 | s.verifyNotificationMsgs("importantDeviceIsOff", "Important device is off", false, false) 150 | } 151 | 152 | s.publishControlValue("somedev", "importantDevicePower", "1", 153 | "sampleAlarms/alarm_importantDeviceIsOff", "sampleAlarms/log") 154 | s.verifyAlarmControlChange("importantDeviceIsOff", false) 155 | s.verifyNotificationMsgs("importantDeviceIsOff", "Important device is back on", true, true) 156 | 157 | // alarm stays off 158 | s.publishControlValue("somedev", "importantDevicePower", "1") 159 | s.VerifyEmpty() 160 | } 161 | } 162 | 163 | func (s *AlarmSuite) TestNonRepeatedExpectedValueAlarm() { 164 | s.loadAlarms("alarms1.conf", "unnecessaryDeviceIsOn") 165 | for i := 0; i < 3; i++ { 166 | s.publishControlValue("somedev", "unnecessaryDevicePower", "1", 167 | "sampleAlarms/alarm_unnecessaryDeviceIsOn", "sampleAlarms/log") 168 | s.verifyAlarmControlChange("unnecessaryDeviceIsOn", true) 169 | s.verifyNotificationMsgs("unnecessaryDeviceIsOn", "Unnecessary device is on", false, true) 170 | 171 | // no repeated alarm upon the same value 172 | s.publishControlValue("somedev", "unnecessaryDevicePower", "1") 173 | s.VerifyEmpty() 174 | 175 | s.publishControlValue("somedev", "unnecessaryDevicePower", "0", 176 | "sampleAlarms/alarm_unnecessaryDeviceIsOn", "sampleAlarms/log") 177 | s.verifyAlarmControlChange("unnecessaryDeviceIsOn", false) 178 | s.verifyNotificationMsgs("unnecessaryDeviceIsOn", "somedev/unnecessaryDevicePower is back to normal, value = false", false, true) 179 | s.VerifyEmpty() 180 | 181 | s.publishControlValue("somedev", "unnecessaryDevicePower", "0") 182 | s.VerifyEmpty() 183 | } 184 | } 185 | 186 | func (s *AlarmSuite) setOutOfRangeTemp(temp int) { 187 | s.publishControlValue("somedev", "devTemp", strconv.Itoa(temp), 188 | "sampleAlarms/alarm_temperatureOutOfBounds", "sampleAlarms/log") 189 | s.verifyAlarmControlChange("temperatureOutOfBounds", true) 190 | s.verifyNotificationMsgs("temperatureOutOfBounds", fmt.Sprintf("Temperature out of bounds, value = %d", temp), false, true) 191 | s.Verify(regexp.MustCompile(`^new fake ticker: \d+, 10000$`)) 192 | } 193 | 194 | func (s *AlarmSuite) setOkTemp(temp int, stopTimer bool) { 195 | s.publishControlValue("somedev", "devTemp", strconv.Itoa(temp), 196 | "sampleAlarms/alarm_temperatureOutOfBounds", "sampleAlarms/log") 197 | s.verifyAlarmControlChange("temperatureOutOfBounds", false) 198 | s.verifyNotificationMsgs("temperatureOutOfBounds", fmt.Sprintf("Temperature is within bounds again, value = %d", temp), stopTimer, true) 199 | s.VerifyEmpty() 200 | } 201 | 202 | func (s *AlarmSuite) TestRepeatedMinMaxAlarmWithMaxCount() { 203 | s.loadAlarms("alarms2.conf", "temperatureOutOfBounds") 204 | 205 | // go below min 206 | s.setOutOfRangeTemp(9) 207 | 208 | s.publishControlValue("somedev", "devTemp", "8") 209 | s.VerifyEmpty() // still out of bounds, but timer wasn't fired yet 210 | 211 | for i := 0; i < 4; i++ { 212 | ts := s.AdvanceTime(10 * time.Millisecond) 213 | s.FireTimer(1, ts) 214 | s.Verify("timer.fire(): 1") 215 | s.expectControlChange("sampleAlarms/log") 216 | s.verifyNotificationMsgs("temperatureOutOfBounds", "Temperature out of bounds, value = 8", false, false) 217 | } 218 | s.Verify("timer.Stop(): 1") 219 | 220 | s.setOkTemp(10, false) 221 | 222 | // go over max 223 | s.setOutOfRangeTemp(16) 224 | s.setOkTemp(15, true) 225 | } 226 | 227 | func (s *AlarmSuite) TestMinAlarm() { 228 | s.loadAlarmsSkipping("alarms2.conf", "**maxtemp**", "temperatureOutOfBounds") 229 | s.setOutOfRangeTemp(9) 230 | s.setOkTemp(16, true) // maxValue removed, 16 must be ok 231 | } 232 | 233 | func (s *AlarmSuite) TestMaxAlarm() { 234 | s.loadAlarmsSkipping("alarms2.conf", "**mintemp**", "temperatureOutOfBounds") 235 | s.setOutOfRangeTemp(16) 236 | s.setOkTemp(9, true) // minValue removed, 9 must be ok 237 | } 238 | 239 | func TestAlarmSuite(t *testing.T) { 240 | testutils.RunSuites(t, 241 | new(AlarmSuite), 242 | ) 243 | } 244 | -------------------------------------------------------------------------------- /wbrules/rule.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/robfig/cron/v3" 7 | "github.com/stretchr/objx" 8 | wbgong "github.com/wirenboard/wbgong" 9 | ) 10 | 11 | const ( 12 | RULE_OR_COND_CAPACITY = 10 13 | ) 14 | 15 | type CheckMode int8 16 | 17 | const ( 18 | CheckModeNone CheckMode = iota 19 | CheckModeWithEvent 20 | CheckModeIndependent 21 | ) 22 | 23 | type DepTracker interface { 24 | StartTrackingDeps() 25 | StoreRuleControlSpec(rule *Rule, ctrlSpec ControlSpec) 26 | StoreRuleDeps(rule *Rule) 27 | SetUninitializedRule(rule *Rule) 28 | } 29 | 30 | type Cron interface { 31 | AddFunc(spec string, cmd func()) (cron.EntryID, error) 32 | Remove(id cron.EntryID) 33 | Start() 34 | Stop() context.Context 35 | } 36 | 37 | type RuleCondition interface { 38 | // Check checks whether the rule should be run 39 | // and returns a boolean value indicating whether 40 | // it should be run and an optional value 41 | // to be passed as newValue to the rule. In 42 | // case nil is returned as the optional value, 43 | // the value of cell must be used. 44 | RequireInitialization() bool 45 | Check(e *ControlChangeEvent) (bool, interface{}) 46 | GetControlSpecs() []ControlSpec 47 | } 48 | 49 | type RuleConditionBase struct{} 50 | 51 | func (ruleCond *RuleConditionBase) RequireInitialization() bool { 52 | return true 53 | } 54 | 55 | func (ruleCond *RuleConditionBase) Check(e *ControlChangeEvent) (bool, interface{}) { 56 | return false, nil 57 | } 58 | 59 | func (ruleCond *RuleConditionBase) GetControlSpecs() []ControlSpec { 60 | return []ControlSpec{} 61 | } 62 | 63 | type SimpleCallbackCondition struct { 64 | RuleConditionBase 65 | cond func() bool 66 | } 67 | 68 | type LevelTriggeredRuleCondition struct { 69 | SimpleCallbackCondition 70 | } 71 | 72 | func NewLevelTriggeredRuleCondition(cond func() bool) *LevelTriggeredRuleCondition { 73 | return &LevelTriggeredRuleCondition{ 74 | SimpleCallbackCondition: SimpleCallbackCondition{cond: cond}, 75 | } 76 | } 77 | 78 | func (ruleCond *LevelTriggeredRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 79 | return ruleCond.cond(), nil 80 | } 81 | 82 | type DestroyedRuleCondition struct { 83 | RuleConditionBase 84 | } 85 | 86 | func NewDestroyedRuleCondition() *DestroyedRuleCondition { 87 | return &DestroyedRuleCondition{} 88 | } 89 | 90 | func (ruleCond *DestroyedRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 91 | panic("invoking a destroyed rule") 92 | } 93 | 94 | type EdgeTriggeredRuleCondition struct { 95 | SimpleCallbackCondition 96 | prevCondValue bool 97 | firstRun bool 98 | } 99 | 100 | func NewEdgeTriggeredRuleCondition(cond func() bool) *EdgeTriggeredRuleCondition { 101 | return &EdgeTriggeredRuleCondition{ 102 | SimpleCallbackCondition: SimpleCallbackCondition{cond: cond}, 103 | prevCondValue: false, 104 | firstRun: false, 105 | } 106 | } 107 | 108 | func (ruleCond *EdgeTriggeredRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 109 | current := ruleCond.cond() 110 | shouldFire := current && (ruleCond.firstRun || current != ruleCond.prevCondValue) 111 | ruleCond.prevCondValue = current 112 | ruleCond.firstRun = false 113 | return shouldFire, nil 114 | } 115 | 116 | type CellChangedRuleCondition struct { 117 | RuleConditionBase 118 | ctrlSpec ControlSpec 119 | } 120 | 121 | func NewCellChangedRuleCondition(ctrlSpec ControlSpec) (*CellChangedRuleCondition, error) { 122 | return &CellChangedRuleCondition{ 123 | ctrlSpec: ctrlSpec, 124 | }, nil 125 | } 126 | 127 | func (ruleCond *CellChangedRuleCondition) RequireInitialization() bool { 128 | return false 129 | } 130 | 131 | func (ruleCond *CellChangedRuleCondition) GetControlSpecs() []ControlSpec { 132 | return []ControlSpec{ruleCond.ctrlSpec} 133 | } 134 | 135 | func (ruleCond *CellChangedRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 136 | if e == nil || e.Spec != ruleCond.ctrlSpec { 137 | return false, nil 138 | } 139 | 140 | if !e.IsComplete { 141 | wbgong.Debug.Printf("skipping rule due to incomplete cell in whenChanged: %s", e.Spec) 142 | return false, nil 143 | } 144 | 145 | if (e.ControlType != wbgong.CONV_TYPE_PUSHBUTTON && e.PrevValue == nil) || (e.IsRetained && e.PrevValue == e.Value) { 146 | return false, nil 147 | } 148 | 149 | return true, nil 150 | } 151 | 152 | type FuncValueChangedRuleCondition struct { 153 | RuleConditionBase 154 | thunk func() interface{} 155 | oldValue interface{} 156 | } 157 | 158 | func NewFuncValueChangedRuleCondition(f func() interface{}) *FuncValueChangedRuleCondition { 159 | return &FuncValueChangedRuleCondition{ 160 | thunk: f, 161 | oldValue: nil, 162 | } 163 | } 164 | 165 | func (ruleCond *FuncValueChangedRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 166 | v := ruleCond.thunk() 167 | if ruleCond.oldValue == v { 168 | return false, nil 169 | } 170 | ruleCond.oldValue = v 171 | return true, v 172 | } 173 | 174 | type OrRuleCondition struct { 175 | RuleConditionBase 176 | initialized bool 177 | conds []RuleCondition 178 | } 179 | 180 | func NewOrRuleCondition(conds []RuleCondition) *OrRuleCondition { 181 | ret := &OrRuleCondition{initialized: false, conds: conds} 182 | if !ret.RequireInitialization() { 183 | ret.initialized = true 184 | } 185 | return ret 186 | } 187 | 188 | func (ruleCond *OrRuleCondition) RequireInitialization() bool { 189 | for i := range ruleCond.conds { 190 | if ruleCond.conds[i].RequireInitialization() { 191 | return true 192 | } 193 | } 194 | return false 195 | } 196 | 197 | func (ruleCond *OrRuleCondition) GetControlSpecs() []ControlSpec { 198 | r := make([]ControlSpec, 0, RULE_OR_COND_CAPACITY) 199 | for _, cond := range ruleCond.conds { 200 | r = append(r, cond.GetControlSpecs()...) 201 | } 202 | return r 203 | } 204 | 205 | func (ruleCond *OrRuleCondition) Check(e *ControlChangeEvent) (bool, interface{}) { 206 | // if condition is not initialized, we need to check all subconditions to collect deps 207 | // 'Or' condition is initialized by default if no subconditions requires initialization 208 | var res = false 209 | var newValue interface{} 210 | var gotValue = false 211 | 212 | for _, cond := range ruleCond.conds { 213 | if shouldFire, newVal := cond.Check(e); shouldFire { 214 | // this condition is to keep 215 | if !ruleCond.initialized { 216 | if !gotValue { 217 | gotValue = true 218 | newValue = newVal 219 | res = true 220 | } 221 | } else { 222 | return true, newVal 223 | } 224 | } 225 | } 226 | 227 | ruleCond.initialized = true 228 | return res, newValue 229 | } 230 | 231 | type CronRuleCondition struct { 232 | RuleConditionBase 233 | spec string 234 | entryId cron.EntryID 235 | } 236 | 237 | func NewCronRuleCondition(spec string) *CronRuleCondition { 238 | return &CronRuleCondition{spec: spec, entryId: 0} 239 | } 240 | 241 | func (ruleCond *CronRuleCondition) MaybeAddToCron(cron Cron, thunk func()) (err error) { 242 | ruleCond.entryId, err = cron.AddFunc(ruleCond.spec, thunk) 243 | return 244 | } 245 | 246 | // RuleId is returned from defineRule to control rule 247 | type RuleId uint32 248 | 249 | type Rule struct { 250 | tracker DepTracker 251 | id RuleId 252 | context *ESContext 253 | name string // optional, but will be checked for redefinition if set 254 | cond RuleCondition 255 | then ESCallbackFunc 256 | checkMode CheckMode 257 | isIndependent bool 258 | hasDeps bool 259 | enabled bool 260 | } 261 | 262 | func NewRule(tracker DepTracker, id RuleId, name string, cond RuleCondition, then ESCallbackFunc) *Rule { 263 | rule := &Rule{ 264 | tracker: tracker, 265 | id: id, 266 | name: name, 267 | cond: cond, 268 | then: then, 269 | checkMode: CheckModeNone, 270 | isIndependent: false, 271 | hasDeps: false, 272 | enabled: true, 273 | } 274 | rule.StoreInitiallyKnownDeps() 275 | return rule 276 | } 277 | 278 | func (rule *Rule) StoreInitiallyKnownDeps() { 279 | for _, ctrlSpec := range rule.cond.GetControlSpecs() { 280 | rule.tracker.StoreRuleControlSpec(rule, ctrlSpec) 281 | rule.hasDeps = true 282 | } 283 | if rule.cond.RequireInitialization() { 284 | rule.tracker.SetUninitializedRule(rule) 285 | } 286 | } 287 | 288 | func (rule *Rule) SetCheckMode(mode CheckMode) { 289 | rule.checkMode = mode 290 | } 291 | 292 | func (rule *Rule) Check(e *ControlChangeEvent) { 293 | if e != nil && rule.checkMode == CheckModeNone { 294 | // Don't invoke js if no cells mentioned in the 295 | // condition callback changed. If rules are run 296 | // not due to a cell being changed, still need 297 | // to call JS though. 298 | return 299 | } 300 | rule.tracker.StartTrackingDeps() 301 | shouldFire, newValue := rule.cond.Check(e) 302 | var args objx.Map 303 | rule.tracker.StoreRuleDeps(rule) 304 | noArgs := rule.checkMode == CheckModeIndependent 305 | rule.checkMode = CheckModeNone 306 | 307 | if rule.enabled { 308 | switch { 309 | case !shouldFire: 310 | return 311 | case noArgs: 312 | break 313 | case newValue != nil: 314 | args = objx.New(map[string]interface{}{ 315 | "newValue": newValue, 316 | }) 317 | case e != nil: // newValue == nil 318 | args = objx.New(map[string]interface{}{ 319 | "device": e.Spec.DeviceId, 320 | "cell": e.Spec.ControlId, 321 | "newValue": e.Value, 322 | }) 323 | } 324 | if wbgong.DebuggingEnabled() { 325 | wbgong.Debug.Printf("[rule] firing Rule ruleId=%d", rule.id) 326 | } 327 | rule.then(args) 328 | } 329 | } 330 | 331 | func (rule *Rule) MaybeAddToCron(cron Cron) { 332 | if cronCond, ok := rule.cond.(*CronRuleCondition); ok { 333 | var err error 334 | err = cronCond.MaybeAddToCron(cron, func() { 335 | if rule.then != nil { 336 | rule.then(nil) 337 | } 338 | }) 339 | rule.isIndependent = err == nil 340 | if err != nil { 341 | wbgong.Error.Printf("rule %s: invalid cron spec: %s", rule.name, err) 342 | } 343 | } 344 | } 345 | 346 | func (rule *Rule) Destroy() { 347 | rule.then = nil 348 | rule.cond = NewDestroyedRuleCondition() 349 | } 350 | 351 | // IsIndependent() returns true if the rule doesn't use any controls for 352 | // its condition yet shouldn't be invoked upon every RunRules(). 353 | // This is currently used for cron rules. 354 | func (rule *Rule) IsIndependent() bool { 355 | return rule.isIndependent 356 | } 357 | 358 | // HasDeps checks whether the rule has dependencies 359 | func (rule *Rule) HasDeps() bool { 360 | return rule.isIndependent || rule.hasDeps 361 | } 362 | 363 | func (rule *Rule) SetState(state bool, cron Cron) { 364 | rule.enabled = state 365 | 366 | if cronCond, ok := rule.cond.(*CronRuleCondition); ok { 367 | if state { 368 | if cronCond.entryId == 0 { 369 | rule.MaybeAddToCron(cron) 370 | } 371 | } else { 372 | if cronCond.entryId != 0 { 373 | cron.Remove(cronCond.entryId) 374 | cronCond.entryId = 0 375 | } 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /wbrules/rule_basics_test.go: -------------------------------------------------------------------------------- 1 | package wbrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wirenboard/wbgong/testutils" 7 | ) 8 | 9 | type RuleBasicsSuite struct { 10 | RuleSuiteBase 11 | } 12 | 13 | func (s *RuleBasicsSuite) SetupTest() { 14 | s.SetupSkippingDefs("testrules.js") 15 | } 16 | 17 | func (s *RuleBasicsSuite) TestRules() { 18 | s.SetCellValue("stabSettings", "enabled", true) 19 | s.Verify( 20 | "driver -> /devices/stabSettings/controls/enabled: [1] (QoS 1, retained)", 21 | "[info] heaterOn fired, changed: stabSettings/enabled -> true", 22 | ) 23 | s.publish("/devices/somedev/controls/sw", "1", "somedev/sw") 24 | s.VerifyUnordered( 25 | "tst -> /devices/somedev/controls/sw: [1] (QoS 1, retained)", 26 | "driver -> /devices/somedev/controls/sw/on: [1] (QoS 1)", 27 | ) 28 | 29 | s.publish("/devices/somedev/controls/temp", "21", "somedev/temp") 30 | s.Verify( 31 | "tst -> /devices/somedev/controls/temp: [21] (QoS 1, retained)", 32 | ) 33 | 34 | s.publish("/devices/somedev/controls/temp", "22", "somedev/temp") 35 | s.Verify( 36 | "tst -> /devices/somedev/controls/temp: [22] (QoS 1, retained)", 37 | "[info] heaterOff fired, changed: somedev/temp -> 22", 38 | ) 39 | s.publish("/devices/somedev/controls/sw", "0", "somedev/sw") 40 | s.VerifyUnordered( 41 | "driver -> /devices/somedev/controls/sw/on: [0] (QoS 1)", 42 | "tst -> /devices/somedev/controls/sw: [0] (QoS 1, retained)", 43 | ) 44 | 45 | s.publish("/devices/somedev/controls/temp", "18", "somedev/temp") 46 | s.Verify( 47 | "tst -> /devices/somedev/controls/temp: [18] (QoS 1, retained)", 48 | "[info] heaterOn fired, changed: somedev/temp -> 18", 49 | ) 50 | s.publish("/devices/somedev/controls/sw", "1", "somedev/sw") 51 | s.VerifyUnordered( 52 | "driver -> /devices/somedev/controls/sw/on: [1] (QoS 1)", 53 | "tst -> /devices/somedev/controls/sw: [1] (QoS 1, retained)", 54 | ) 55 | 56 | // edge-triggered rule doesn't fire 57 | s.publish("/devices/somedev/controls/temp", "19", "somedev/temp") 58 | s.Verify( 59 | "tst -> /devices/somedev/controls/temp: [19] (QoS 1, retained)", 60 | ) 61 | 62 | s.SetCellValue("stabSettings", "enabled", false) 63 | s.Verify( 64 | "driver -> /devices/stabSettings/controls/enabled: [0] (QoS 1, retained)", 65 | "[info] heaterOff fired, changed: stabSettings/enabled -> false", 66 | ) 67 | s.publish("/devices/somedev/controls/sw", "0", "somedev/sw") 68 | s.VerifyUnordered( 69 | "tst -> /devices/somedev/controls/sw: [0] (QoS 1, retained)", 70 | "driver -> /devices/somedev/controls/sw/on: [0] (QoS 1)", 71 | ) 72 | 73 | s.publish("/devices/somedev/controls/foobar", "1", "somedev/foobar") 74 | s.publish("/devices/somedev/controls/foobar/meta/type", "text", "somedev/foobar") 75 | s.Verify( 76 | "tst -> /devices/somedev/controls/foobar: [1] (QoS 1, retained)", 77 | "tst -> /devices/somedev/controls/foobar/meta/type: [text] (QoS 1, retained)", 78 | "[info] initiallyIncompleteLevelTriggered fired", 79 | ) 80 | 81 | // level-triggered rule fires again here 82 | s.publish("/devices/somedev/controls/foobar", "2", "somedev/foobar") 83 | s.Verify( 84 | "tst -> /devices/somedev/controls/foobar: [2] (QoS 1, retained)", 85 | "[info] initiallyIncompleteLevelTriggered fired", 86 | ) 87 | } 88 | 89 | func (s *RuleBasicsSuite) TestDirectMQTTMessages() { 90 | s.publish("/devices/somedev/controls/sendit/meta/type", "switch", "somedev/sendit") 91 | s.publish("/devices/somedev/controls/sendit", "1", "somedev/sendit") 92 | s.Verify( 93 | "tst -> /devices/somedev/controls/sendit/meta/type: [switch] (QoS 1, retained)", 94 | "tst -> /devices/somedev/controls/sendit: [1] (QoS 1, retained)", 95 | "wbrules-log -> /abc/def/ghi: [0] (QoS 0)", 96 | "wbrules-log -> /misc/whatever: [abcdef] (QoS 1)", 97 | "wbrules-log -> /zzz/foo: [qqq] (QoS 2)", 98 | "wbrules-log -> /zzz/foo/qwerty: [42] (QoS 2, retained)", 99 | ) 100 | } 101 | 102 | func (s *RuleBasicsSuite) TestCellChange() { 103 | s.publish("/devices/somedev/controls/foobarbaz/meta/type", "text", "somedev/foobarbaz") 104 | s.publish("/devices/somedev/controls/foobarbaz", "initial_text", "somedev/foobarbaz") 105 | s.publish("/devices/somedev/controls/foobarbaz", "abc", "somedev/foobarbaz") 106 | s.Verify( 107 | "tst -> /devices/somedev/controls/foobarbaz/meta/type: [text] (QoS 1, retained)", 108 | "tst -> /devices/somedev/controls/foobarbaz: [initial_text] (QoS 1, retained)", 109 | "tst -> /devices/somedev/controls/foobarbaz: [abc] (QoS 1, retained)", 110 | "[info] cellChange1: somedev/foobarbaz=abc (string)", 111 | "[info] cellChange2: somedev/foobarbaz=abc (string)", 112 | ) 113 | 114 | s.publish("/devices/somedev/controls/tempx/meta/type", "temperature", "somedev/tempx") 115 | s.publish("/devices/somedev/controls/tempx", "0", "somedev/tempx") 116 | s.publish("/devices/somedev/controls/tempx", "42", "somedev/tempx") 117 | s.Verify( 118 | "tst -> /devices/somedev/controls/tempx/meta/type: [temperature] (QoS 1, retained)", 119 | "tst -> /devices/somedev/controls/tempx: [0] (QoS 1, retained)", 120 | "tst -> /devices/somedev/controls/tempx: [42] (QoS 1, retained)", 121 | "[info] cellChange2: somedev/tempx=42 (number)", 122 | ) 123 | // no change 124 | s.publish("/devices/somedev/controls/tempx", "42", "somedev/tempx") 125 | s.publish("/devices/somedev/controls/tempx", "42", "somedev/tempx") 126 | s.Verify( 127 | "tst -> /devices/somedev/controls/tempx: [42] (QoS 1, retained)", 128 | "tst -> /devices/somedev/controls/tempx: [42] (QoS 1, retained)", 129 | ) 130 | } 131 | 132 | func (s *RuleBasicsSuite) TestRemoteButtons() { 133 | // FIXME: handling remote buttons, i.e. buttons that 134 | // are defined for external devices and not via defineVirtualDevice(), 135 | // needs more work. We need to handle /on messages for these 136 | // instead of value messages. As of now, the code will work 137 | // unless the remote driver retains button value, in which 138 | // case extra change events will be received on startup 139 | // The change rule must be fired on each button press ('1' value message) 140 | s.publish("/devices/somedev/controls/abutton/meta/type", "pushbutton", "somedev/abutton") 141 | s.publish("/devices/somedev/controls/abutton", "1", "somedev/abutton") 142 | s.Verify( 143 | "tst -> /devices/somedev/controls/abutton/meta/type: [pushbutton] (QoS 1, retained)", 144 | "tst -> /devices/somedev/controls/abutton: [1] (QoS 1, retained)", 145 | "[info] cellChange2: somedev/abutton=true (boolean)", 146 | ) 147 | s.publish("/devices/somedev/controls/abutton", "1", "somedev/abutton") 148 | s.Verify( 149 | "tst -> /devices/somedev/controls/abutton: [1] (QoS 1, retained)", 150 | "[info] cellChange2: somedev/abutton=true (boolean)", 151 | ) 152 | } 153 | 154 | func (s *RuleBasicsSuite) TestFuncValueChange() { 155 | s.publish("/devices/somedev/controls/cellforfunc", "2", "somedev/cellforfunc") 156 | s.Verify( 157 | // the cell is incomplete here 158 | "tst -> /devices/somedev/controls/cellforfunc: [2] (QoS 1, retained)", 159 | ) 160 | 161 | s.publish("/devices/somedev/controls/cellforfunc/meta/type", "temperature", "somedev/cellforfunc") 162 | s.Verify( 163 | "tst -> /devices/somedev/controls/cellforfunc/meta/type: [temperature] (QoS 1, retained)", 164 | "[info] funcValueChange: false (boolean)", 165 | ) 166 | 167 | s.publish("/devices/somedev/controls/cellforfunc", "5", "somedev/cellforfunc") 168 | s.Verify( 169 | "tst -> /devices/somedev/controls/cellforfunc: [5] (QoS 1, retained)", 170 | "[info] funcValueChange: true (boolean)", 171 | ) 172 | 173 | s.publish("/devices/somedev/controls/cellforfunc", "7", "somedev/cellforfunc") 174 | s.Verify( 175 | // expression value not changed 176 | "tst -> /devices/somedev/controls/cellforfunc: [7] (QoS 1, retained)", 177 | ) 178 | 179 | s.publish("/devices/somedev/controls/cellforfunc", "1", "somedev/cellforfunc") 180 | s.Verify( 181 | "tst -> /devices/somedev/controls/cellforfunc: [1] (QoS 1, retained)", 182 | "[info] funcValueChange: false (boolean)", 183 | ) 184 | 185 | s.publish("/devices/somedev/controls/cellforfunc", "0", "somedev/cellforfunc") 186 | s.Verify( 187 | // expression value not changed 188 | "tst -> /devices/somedev/controls/cellforfunc: [0] (QoS 1, retained)", 189 | ) 190 | 191 | // somedev/cellforfunc1 is listed by name 192 | s.publish("/devices/somedev/controls/cellforfunc1", "2", "somedev/cellforfunc1") 193 | s.publish("/devices/somedev/controls/cellforfunc2", "2", "somedev/cellforfunc2") 194 | s.Verify( 195 | // the cell is incomplete here 196 | "tst -> /devices/somedev/controls/cellforfunc1: [2] (QoS 1, retained)", 197 | "tst -> /devices/somedev/controls/cellforfunc2: [2] (QoS 1, retained)", 198 | ) 199 | 200 | s.publish("/devices/somedev/controls/cellforfunc1/meta/type", "temperature", "somedev/cellforfunc1") 201 | s.Verify( 202 | "tst -> /devices/somedev/controls/cellforfunc1/meta/type: [temperature] (QoS 1, retained)", 203 | ) 204 | 205 | // previously (latest ab4767e7) funcValueChange2 had to be triggered by 206 | // setting meta/type in previous publish in this test. 207 | // Now whenChanged logic has changed because of "setValue without notify" feature, 208 | // so this completeness check is pretty hard to implement. Also it is 209 | // not obvious why the rule should change in this case after all. 210 | // So this test was changed to fit these new quirks. Have fun. 211 | s.publish("/devices/somedev/controls/cellforfunc1", "3", "somedev/cellforfunc1") 212 | s.Verify( 213 | "tst -> /devices/somedev/controls/cellforfunc1: [3] (QoS 1, retained)", 214 | "[info] funcValueChange2: somedev/cellforfunc1: 3 (number)", 215 | ) 216 | 217 | s.publish("/devices/somedev/controls/cellforfunc2/meta/type", "temperature", "somedev/cellforfunc2") 218 | s.Verify( 219 | "tst -> /devices/somedev/controls/cellforfunc2/meta/type: [temperature] (QoS 1, retained)", 220 | "[info] funcValueChange2: (no cell): false (boolean)", 221 | ) 222 | 223 | s.publish("/devices/somedev/controls/cellforfunc2", "5", "somedev/cellforfunc2") 224 | s.Verify( 225 | "tst -> /devices/somedev/controls/cellforfunc2: [5] (QoS 1, retained)", 226 | "[info] funcValueChange2: (no cell): true (boolean)", 227 | ) 228 | } 229 | 230 | func (s *RuleBasicsSuite) TestAnonymousRule() { 231 | s.publish("/devices/somedev/controls/anon/meta/type", "pushbutton", "somedev/anon") 232 | s.publish("/devices/somedev/controls/anon", "1", "somedev/anon") 233 | 234 | s.Verify("tst -> /devices/somedev/controls/anon/meta/type: [pushbutton] (QoS 1, retained)") 235 | s.VerifyUnordered( 236 | "tst -> /devices/somedev/controls/anon: [1] (QoS 1, retained)", 237 | "[info] anonymous rule run", 238 | ) 239 | } 240 | 241 | func (s *RuleBasicsSuite) TestRuleRedefinition() { 242 | s.LiveLoadScript("testrules_rule_redefinition.js") 243 | s.EnsureGotErrors() 244 | s.SkipTill("[error] defineRule error: named rule redefinition: test") 245 | s.Verify(testutils.RegexpCaptureMatcher( 246 | `.*Error: error error \(rc -100\).*`, func(m []string) bool { 247 | return true 248 | })) 249 | s.Verify("[changed] testrules_rule_redefinition.js") 250 | } 251 | 252 | func TestRuleBasicsSuite(t *testing.T) { 253 | testutils.RunSuites(t, 254 | new(RuleBasicsSuite), 255 | ) 256 | } 257 | --------------------------------------------------------------------------------