Skip to content

Commit

Permalink
feat: JSONata everywhere
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zachowj committed Jul 20, 2019
1 parent 5f437d4 commit 6424235
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 76 deletions.
54 changes: 52 additions & 2 deletions lib/base-node.js
Expand Up @@ -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' ||
Expand Down Expand Up @@ -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 = {
Expand Down
10 changes: 7 additions & 3 deletions nodes/_static/ifstate.js
Expand Up @@ -29,14 +29,15 @@ var ifState = (function($) {
'num',
'bool',
're',
'jsonata',
'msg',
'flow',
'global',
entityType
];

if (nodeName !== 'currentState') {
defaultTypes.splice(4, 1);
defaultTypes.splice(5, 1);
}

$input.after(
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions nodes/current-state/current-state.html
Expand Up @@ -141,6 +141,7 @@
<option value="gte">&gt;=</option>
<option value="includes">in</option>
<option value="does_not_include">not in</option>
<option value="jsonata">JSONata</option>
</select>
<input type="text" id="node-input-halt_if" />
</div>
Expand Down
27 changes: 17 additions & 10 deletions nodes/current-state/current-state.js
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions nodes/events-state-changed/events-state-changed.html
Expand Up @@ -119,6 +119,7 @@
<option value="gte">&gt;=</option>
<option value="includes">in</option>
<option value="does_not_include">not in</option>
<option value="jsonata">JSONata</option>
</select>
<input type="text" id="node-input-haltifstate" />
</div>
Expand Down
27 changes: 17 additions & 10 deletions nodes/events-state-changed/events-state-changed.js
Expand Up @@ -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,
Expand Down
30 changes: 27 additions & 3 deletions nodes/get-entities/get-entities.html
Expand Up @@ -45,14 +45,16 @@
{ 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 = [
"str",
"num",
"bool",
"re",
"jsonata",
"msg",
"flow",
"global",
Expand Down Expand Up @@ -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":
Expand All @@ -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);
Expand Down
52 changes: 31 additions & 21 deletions nodes/get-entities/get-entities.js
Expand Up @@ -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 = {};
Expand Down
1 change: 1 addition & 0 deletions nodes/poll-state/poll-state.html
Expand Up @@ -141,6 +141,7 @@
<option value="gte">&gt;=</option>
<option value="includes">in</option>
<option value="does_not_include">not in</option>
<option value="jsonata">JSONata</option>
</select>
<input type="text" id="node-input-halt_if" />
</div>
Expand Down
25 changes: 16 additions & 9 deletions nodes/poll-state/poll-state.js
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions nodes/trigger-state/trigger-state.html
Expand Up @@ -251,6 +251,7 @@
"num",
"bool",
"re",
"jsonata",
{ value: "entity", label: "entity." },
{ value: "prevEntity", label: "prev entity." }
]
Expand Down

0 comments on commit 6424235

Please sign in to comment.