From 64242358a200f1b1131f832ae60cc144259009f2 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 19 Jul 2019 22:47:37 -0700 Subject: [PATCH] feat: JSONata everywhere Added ability to use JSONata expression in a lot of places. Three custom functions have been added to the JSOnata editor: - $entity() will reference the trigger entity - $prevEntity will reference the previous trigger entity state if trigger from an event - $entities() will return all entities, $entities(entity_id) will return a single entity object. --- lib/base-node.js | 54 ++++++++++++++++++- nodes/_static/ifstate.js | 10 ++-- nodes/current-state/current-state.html | 1 + nodes/current-state/current-state.js | 27 ++++++---- .../events-state-changed.html | 1 + .../events-state-changed.js | 27 ++++++---- nodes/get-entities/get-entities.html | 30 +++++++++-- nodes/get-entities/get-entities.js | 52 ++++++++++-------- nodes/poll-state/poll-state.html | 1 + nodes/poll-state/poll-state.js | 25 +++++---- nodes/trigger-state/trigger-state.html | 1 + nodes/wait-until/wait-until.html | 16 ++++-- nodes/wait-until/wait-until.js | 37 +++++++------ 13 files changed, 206 insertions(+), 76 deletions(-) diff --git a/lib/base-node.js b/lib/base-node.js index f99a7a4273..b06e9cfb09 100644 --- a/lib/base-node.js +++ b/lib/base-node.js @@ -323,6 +323,21 @@ class BaseNode { comparatorValue, comparatorValueDatatype === 'entity' ? entity : prevEntity ); + } else if ( + comparatorType !== 'jsonata' && + comparatorValueDatatype === 'jsonata' && + comparatorValue + ) { + try { + cValue = this.evaluateJSONata( + comparatorValue, + message, + entity, + prevEntity + ); + } catch (e) { + throw new Error(`JSONata Error: ${e.message}`); + } } else { if ( comparatorType === 'includes' || @@ -369,14 +384,49 @@ class BaseNode { case 'starts_with': return actualValue.startsWith(cValue); case 'in_group': - const entity = await this.nodeConfig.server.homeAssistant.getStates( + const ent = await this.nodeConfig.server.homeAssistant.getStates( cValue ); const groupEntities = - selectn('attributes.entity_id', entity) || []; + selectn('attributes.entity_id', ent) || []; return groupEntities.includes(actualValue); + case 'jsonata': + if (!cValue) return true; + + try { + return ( + this.evaluateJSONata( + cValue, + message, + entity, + prevEntity + ) === true + ); + } catch (e) { + throw new Error(`JSONata Error: ${e.message}`); + } } } + + evaluateJSONata(expression, message, entity, prevEntity) { + const expr = this.RED.util.prepareJSONataExpression( + expression, + this.node + ); + const serverName = this.utils.toCamelCase(this.nodeConfig.server.name); + + expr.assign('entity', () => entity); + expr.assign('prevEntity', () => prevEntity); + expr.assign('entities', val => { + const homeAssistant = this.node + .context() + .global.get('homeassistant')[serverName]; + if (homeAssistant === undefined) return undefined; + return val ? homeAssistant.states[val] : homeAssistant.states; + }); + + return this.RED.util.evaluateJSONataExpression(expr, message); + } } const _internals = { diff --git a/nodes/_static/ifstate.js b/nodes/_static/ifstate.js index a836ccecc9..1241069f5f 100644 --- a/nodes/_static/ifstate.js +++ b/nodes/_static/ifstate.js @@ -29,6 +29,7 @@ var ifState = (function($) { 'num', 'bool', 're', + 'jsonata', 'msg', 'flow', 'global', @@ -36,7 +37,7 @@ var ifState = (function($) { ]; if (nodeName !== 'currentState') { - defaultTypes.splice(4, 1); + defaultTypes.splice(5, 1); } $input.after( @@ -66,11 +67,14 @@ var ifState = (function($) { case 'lte': case 'gt': case 'gte': - types = ['num'].concat(extraTypes); + types = ['num', 'jsonata'].concat(extraTypes); break; case 'includes': case 'does_not_include': - types = ['str'].concat(extraTypes); + types = ['str', 'jsonata'].concat(extraTypes); + break; + case 'jsonata': + types = ['jsonata']; break; } $input.typedInput('types', types); diff --git a/nodes/current-state/current-state.html b/nodes/current-state/current-state.html index 3efb41f8cc..5ff908da6d 100644 --- a/nodes/current-state/current-state.html +++ b/nodes/current-state/current-state.html @@ -141,6 +141,7 @@ + diff --git a/nodes/current-state/current-state.js b/nodes/current-state/current-state.js index 64eebcf75d..bda65c8ade 100644 --- a/nodes/current-state/current-state.js +++ b/nodes/current-state/current-state.js @@ -108,16 +108,23 @@ module.exports = function(RED) { message ); - const isIfState = await this.getComparatorResult( - config.halt_if_compare, - config.halt_if, - entity.state, - config.halt_if_type, - { - message, - entity - } - ); + let isIfState; + try { + isIfState = await this.getComparatorResult( + config.halt_if_compare, + config.halt_if, + entity.state, + config.halt_if_type, + { + message, + entity + } + ); + } catch (e) { + this.setStatusFailed('Error'); + this.node.error(e.message, message); + return; + } // Handle version 0 'halt if' outputs if (config.version < 1) { diff --git a/nodes/events-state-changed/events-state-changed.html b/nodes/events-state-changed/events-state-changed.html index 405c126e6c..74114345ea 100644 --- a/nodes/events-state-changed/events-state-changed.html +++ b/nodes/events-state-changed/events-state-changed.html @@ -119,6 +119,7 @@ + diff --git a/nodes/events-state-changed/events-state-changed.js b/nodes/events-state-changed/events-state-changed.js index 9a3bf36dfd..3620c35d94 100644 --- a/nodes/events-state-changed/events-state-changed.js +++ b/nodes/events-state-changed/events-state-changed.js @@ -95,16 +95,23 @@ module.exports = function(RED) { } // Check if 'if state' is true - const isIfState = await this.getComparatorResult( - this.nodeConfig.halt_if_compare, - this.nodeConfig.haltIfState, - event.new_state.state, - this.nodeConfig.halt_if_type, - { - entity: event.new_state, - prevEntity: event.old_state - } - ); + let isIfState; + try { + isIfState = await this.getComparatorResult( + this.nodeConfig.halt_if_compare, + this.nodeConfig.haltIfState, + event.new_state.state, + this.nodeConfig.halt_if_type, + { + entity: event.new_state, + prevEntity: event.old_state + } + ); + } catch (e) { + this.setStatusFailed('Error'); + this.node.error(e.message, {}); + return; + } const msg = { topic: entity_id, diff --git a/nodes/get-entities/get-entities.html b/nodes/get-entities/get-entities.html index 3e0144afc5..096d00a28c 100644 --- a/nodes/get-entities/get-entities.html +++ b/nodes/get-entities/get-entities.html @@ -45,7 +45,8 @@ { value: "includes", text: "in" }, { value: "does_not_include", text: "not in" }, { value: "starts_with", text: "starts with" }, - { value: "in_group", text: "in group" } + { value: "in_group", text: "in group" }, + { value: "jsonata", text: "JSONata" } ]; const typeEntity = { value: "entity", label: "entity." }; const defaultTypes = [ @@ -53,6 +54,7 @@ "num", "bool", "re", + "jsonata", "msg", "flow", "global", @@ -108,6 +110,11 @@ .change(function(e) { let types = defaultTypes; + $property.prop( + "disabled", + e.target.value === "jsonata" ? true : false + ); + switch (e.target.value) { case "is": case "is_not": @@ -116,13 +123,30 @@ case "lte": case "gt": case "gte": - types = ["num", "msg", "flow", "global", typeEntity]; + types = [ + "num", + "jsonata", + "msg", + "flow", + "global", + typeEntity + ]; break; case "includes": case "does_not_include": case "starts_with": case "in_group": - types = ["str", "msg", "flow", "global", typeEntity]; + types = [ + "str", + "jsonata", + "msg", + "flow", + "global", + typeEntity + ]; + break; + case "jsonata": + types = ["jsonata"]; break; } $value.typedInput("types", types); diff --git a/nodes/get-entities/get-entities.js b/nodes/get-entities/get-entities.js index 99655b2982..2d64aa6b60 100644 --- a/nodes/get-entities/get-entities.js +++ b/nodes/get-entities/get-entities.js @@ -40,30 +40,40 @@ module.exports = function(RED) { return { payload: {} }; } - let entities = await filter(Object.values(states), async entity => { - const rules = config.rules; - - for (const rule of rules) { - const value = this.utils.selectn(rule.property, entity); - const result = await this.getComparatorResult( - rule.logic, - rule.value, - value, - rule.valueType, - { - message, - entity + let entities; + try { + entities = await filter(Object.values(states), async entity => { + const rules = config.rules; + + for (const rule of rules) { + const value = this.utils.selectn(rule.property, entity); + const result = await this.getComparatorResult( + rule.logic, + rule.value, + value, + rule.valueType, + { + message, + entity + } + ); + if ( + (rule.logic !== 'jsonata' && value === undefined) || + !result + ) { + return false; } - ); - if (value === undefined || !result) { - return false; } - } - entity.timeSinceChangedMs = - Date.now() - new Date(entity.last_changed).getTime(); - return true; - }); + entity.timeSinceChangedMs = + Date.now() - new Date(entity.last_changed).getTime(); + return true; + }); + } catch (e) { + this.setStatusFailed('Error'); + this.node.error(e.message, {}); + return; + } let statusText = `${entities.length} entities`; let payload = {}; diff --git a/nodes/poll-state/poll-state.html b/nodes/poll-state/poll-state.html index e17e152584..4660a63e60 100644 --- a/nodes/poll-state/poll-state.html +++ b/nodes/poll-state/poll-state.html @@ -141,6 +141,7 @@ + diff --git a/nodes/poll-state/poll-state.js b/nodes/poll-state/poll-state.js index 1333e5f1fe..1d442cd7f2 100644 --- a/nodes/poll-state/poll-state.js +++ b/nodes/poll-state/poll-state.js @@ -130,15 +130,22 @@ module.exports = function(RED) { data: pollState }; - const isIfState = await this.getComparatorResult( - this.nodeConfig.halt_if_compare, - this.nodeConfig.halt_if, - pollState.state, - this.nodeConfig.halt_if_type, - { - entity: pollState - } - ); + let isIfState; + try { + isIfState = await this.getComparatorResult( + this.nodeConfig.halt_if_compare, + this.nodeConfig.halt_if, + pollState.state, + this.nodeConfig.halt_if_type, + { + entity: pollState + } + ); + } catch (e) { + this.setStatusFailed('Error'); + this.node.error(e.message, {}); + return; + } // Handle version 0 'halt if' outputs if (this.nodeConfig.version < 1) { diff --git a/nodes/trigger-state/trigger-state.html b/nodes/trigger-state/trigger-state.html index 9764a48f43..7bd16660b6 100644 --- a/nodes/trigger-state/trigger-state.html +++ b/nodes/trigger-state/trigger-state.html @@ -251,6 +251,7 @@ "num", "bool", "re", + "jsonata", { value: "entity", label: "entity." }, { value: "prevEntity", label: "prev entity." } ] diff --git a/nodes/wait-until/wait-until.html b/nodes/wait-until/wait-until.html index 77c4bf0e46..0ff00f974d 100644 --- a/nodes/wait-until/wait-until.html +++ b/nodes/wait-until/wait-until.html @@ -60,6 +60,7 @@ "num", "bool", "re", + "jsonata", "msg", "flow", "global", @@ -71,11 +72,16 @@ types: defaultTypes, typeField: "#node-input-valueType" }) - .typedInput("width", "50%"); + .typedInput("width", "45%"); $("#node-input-comparator").change(function(e) { let types = defaultTypes; + $("#node-input-property").prop( + "disabled", + e.target.value === "jsonata" ? true : false + ); + switch (e.target.value) { case "is": case "is_not": @@ -84,11 +90,14 @@ case "lte": case "gt": case "gte": - types = ["num", "msg", "flow", "global", entityType]; + types = ["num", "jsonata", "msg", "flow", "global", entityType]; break; case "includes": case "does_not_include": - types = ["str", "msg", "flow", "global"]; + types = ["str", "jsonata", "msg", "flow", "global"]; + break; + case "jsonata": + types = ["jsonata"]; break; } $("#node-input-value").typedInput("types", types); @@ -147,6 +156,7 @@ + diff --git a/nodes/wait-until/wait-until.js b/nodes/wait-until/wait-until.js index 5b5026cbf1..bfc0ad04b4 100644 --- a/nodes/wait-until/wait-until.js +++ b/nodes/wait-until/wait-until.js @@ -53,7 +53,8 @@ module.exports = function(RED) { 'gt', 'gte', 'includes', - 'does_not_include' + 'does_not_include', + 'jsonata' ) .label('comparator') } @@ -136,20 +137,26 @@ module.exports = function(RED) { return null; } - const result = await this.getComparatorResult( - this.savedConfig.comparator, - this.savedConfig.value, - this.utils.selectn( - this.savedConfig.property, - event.new_state - ), - this.savedConfig.valueType, - { - message: this.savedMessage, - entity: event.new_state - } - ); - + let result; + try { + result = await this.getComparatorResult( + this.savedConfig.comparator, + this.savedConfig.value, + this.utils.selectn( + this.savedConfig.property, + event.new_state + ), + this.savedConfig.valueType, + { + message: this.savedMessage, + entity: event.new_state + } + ); + } catch (e) { + this.setStatusFailed('Error'); + this.node.error(e.message, {}); + return; + } if (!result) { return null; }