Skip to content

Commit

Permalink
Merge pull request #32 from bartbutenaers/master
Browse files Browse the repository at this point in the history
filtrex expressions
  • Loading branch information
macinspak authored Jul 18, 2022
2 parents 53687c8 + dea04fe commit 1b55e3a
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 9 deletions.
65 changes: 57 additions & 8 deletions alarm/sensor.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
name: { value : "", required: true },
panel: { type: 'AnamicoAlarmPanel', required: true },
alarmStates: { value : "1", required: true, validate:RED.validators.regex(/.{1,}/) },
triggerType: { value : null, required: false }
triggerType: { value : null, required: false }, // Legacy property
triggerCondition: { value : "", required: false, validate:function(v) { return !this.triggerConditionError; } },
// Use the triggerConditionError property to let Node-RED know whether the trigger expression contains a syntax error or not.
// This way the flow editor knows whether a red triangle should be drawn for this node or not.
// The triggerConditionError property will be filled in the oneditsave.
// This way we can avoid running the expression check every time the validator is called, since the expression won't change meanwhile anyway...
triggerConditionError: { value : false},
},
inputs: 1,
outputs: 0,
Expand All @@ -18,8 +24,47 @@
paletteLabel: 'Alarm Sensor',
oneditprepare: function() {
//called at launch time

var node = this;

// Check the expression syntax (at server side), for every change made in the expression input
$('#node-input-triggerCondition').keyup(function() {
var triggerExpression = this.value;

$.ajax({
method: 'POST',
url: 'anamico-alarm-sensor/check/',
data: {expression: btoa(triggerExpression)},
success: function(response){
// Draw a red border around the expression input field if necessary (similar how the validation functions do it)
if (response.result === "error") {
$('#node-input-triggerCondition').addClass("input-error");
node.triggerConditionError = true;
}
else { // "ok"
$('#node-input-triggerCondition').removeClass("input-error");
node.triggerConditionError = false;
}
}
});
});


// For older nodes (version 1.2.5 and below) there was a dropdown triggerType, but no triggerCondition expression.
// When the config screen of this node is being opened, the triggerType should be migrated to a corresponding triggerCondition expression.
if (!this.triggerCondition) {
if (this.triggerType == "1") {
$('#node-input-triggerCondition').val("msg.payload.open == true");
}
else {
// "Any message" means no trigger condition
$('#node-input-triggerCondition').val("");
}
}
else {
// Make sure the condition is checked when the config screen is opened
$('#node-input-triggerCondition').val(this.triggerCondition);
$('#node-input-triggerCondition').keyup();
}

$.get('allow2/paired/' + configId, function( data, textStatus, jqXHR ) {
//write access token value back out to the form
Expand Down Expand Up @@ -145,6 +190,13 @@
});
});
},
oneditsave: function() {
// Store the result of the last (server side) trigger expression check
this.triggerConditionError = false;
if ($('#node-input-triggerCondition').hasClass("input-error")) {
this.triggerConditionError = true;
}
},
exportable: false
});
</script>
Expand Down Expand Up @@ -172,13 +224,10 @@

<div class="form-row"></div>
<div class="form-row">
<label for="node-input-triggerType"><i class="fa fa-filter"></i> Trigger</label>
<select id="node-input-triggerType">
<option value="">Any Message</option>
<option value="1">msg.payload.open == true</option>
</select>
<label for="node-input-triggerCondition"><i class="fa fa-filter"></i> Trigger</label>
<input type="text" id="node-input-triggerCondition" placeholder="Logical (boolean) expression">
</div>
<div class="form-tips">Tip: Having no triggers defined will make this sensor trigger on ANY message. Otherwise, define a condition under which the sensor is considered "tripped".</div>
<div class="form-tips">Tip: Having no trigger condition defined will make this sensor trigger on ANY message. Otherwise, define a condition under which the sensor is considered "tripped".</div>
</script>

<script type="text/x-red" data-help-name="AnamicoAlarmSensor">
Expand Down
77 changes: 77 additions & 0 deletions alarm/sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
//const async = require('async');

module.exports = function(RED) {
const { compileExpression, useDotAccessOperatorAndOptionalChaining } = require("filtrex");

function compileFiltrexExpression(expression) {
const options = {
// Allow boolean values in the Filtrex expressions. For example "msg.payload == true".
// See https://github.com/m93a/filtrex/issues/46#issuecomment-922105858
constants: {
true: true,
false: false
},
// Allow dot operators in the Filtex expressions. For example "msg.payload".
// See https://github.com/m93a/filtrex/issues/44#issuecomment-925914796
customProp: useDotAccessOperatorAndOptionalChaining
}

return compileExpression(expression, options);
}

function AnamicoAlarmSensor(config) {
RED.nodes.createNode(this, config);
Expand All @@ -12,10 +29,51 @@ module.exports = function(RED) {
this.alarmStates = JSON.parse("[" + config.alarmStates + "]");

this.resetTimer = null;

var triggerCondition = config.triggerCondition;

// For older nodes (version 1.2.5 and below) there was a dropdown triggerType, but no triggerCondition expression.
// When such a node is started, the triggerType should be migrated to a corresponding triggerCondition expression.
if (!triggerCondition) {
if (config.triggerType == "1") {
triggerCondition = "msg.payload.open == true";
}
else {
triggerCondition = "";
}
}

// No trigger condition should be converted to a trigger expression that matches all messages
if (triggerCondition.trim() == "") {
triggerCondition = "true == true";
}

// For performance reasons, the trigger expression will be compiled once at the start.
// Under the hood a trigger function is being generated.
if (triggerCondition) {
try {
node.triggerFunction = compileFiltrexExpression(triggerCondition);
}
catch(err) {
node.error("Invalid trigger condition expression: " + err);
}
}

node.on('input', function(msg) {

//node.log(node.alarmStates);

if (!node.triggerFunction) {
node.warn("Invalid trigger condition expression cannot evaluate message");
return;
}

var trigger = node.triggerFunction({msg: msg});

if (trigger !== true && trigger !== false) {
// Invalid input (e.g. "5" instead of 5) will result in trigger containing "TypeError: expected ..."
trigger = false;
}

node.status({ fill:"blue", shape:"dot", text:"trigger" });

Expand Down Expand Up @@ -69,4 +127,23 @@ module.exports = function(RED) {

}
RED.nodes.registerType("AnamicoAlarmSensor", AnamicoAlarmSensor);

// Make the expression syntax check available to the config screen in the Node-RED editor
RED.httpAdmin.post('/anamico-alarm-sensor/check', function(req, res){
// Decode the base64-encoded trigger condition in the body of the post request
var buff = new Buffer(req.body.expression, 'base64');
var triggerExpression = buff.toString('ascii');

// Try to compile the trigger expression
try {
// When no trigger condition is available, that will be considered as a valid expression also
if (triggerExpression.trim() != "") {
compileFiltrexExpression(triggerExpression);
res.json({result: "ok"});
}
}
catch(err) {
res.json({result: "error", error: err});
}
});
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
],
"dependencies": {
"debug": "^4.3.1",
"async": "^2.6.1"
"async": "^2.6.1",
"filtrex": "^3.0.0-rc11"
},
"description": "Nodes to build your own home alarm system. Designed to work easily with (but does not require) homekit.",
"devDependencies": {},
Expand Down

0 comments on commit 1b55e3a

Please sign in to comment.