Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

filtrex expressions #32

Merged
merged 2 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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