Skip to content

Commit

Permalink
feat(events-state): Add for condition
Browse files Browse the repository at this point in the history
  • Loading branch information
zachowj committed Oct 5, 2020
1 parent 11aa8c3 commit 61021a8
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 101 deletions.
10 changes: 10 additions & 0 deletions docs/node/events-state.md
Expand Up @@ -30,6 +30,16 @@ If the conditional is evaluated as true send the message to the first output oth

- [Conditionals](/guide/conditionals.md)

### For

- Type: `number`

An amount of time an entity's state needs to be in that state before triggering.

::: tip
Output on Connect state changes will not start a timer.
:::

### State Type

- Type: `string`
Expand Down
7 changes: 7 additions & 0 deletions lib/base-node.js
Expand Up @@ -302,6 +302,13 @@ class BaseNode {
}
}

castState(entity, type) {
if (entity) {
entity.original_state = entity.state;
entity.state = this.getCastValue(type, entity.state);
}
}

getContextValue(location, property, message) {
if (message && location === 'msg') {
return this.RED.util.getMessageProperty(message, property);
Expand Down
179 changes: 133 additions & 46 deletions nodes/events-state-changed/events-state-changed.js
@@ -1,7 +1,12 @@
/* eslint-disable camelcase */
const cloneDeep = require('lodash.clonedeep');
const selectn = require('selectn');

const EventsHaNode = require('../../lib/events-ha-node');
const { shouldIncludeEvent } = require('../../lib/utils');
const {
shouldIncludeEvent,
getWaitStatusText,
getTimeInMilliseconds,
} = require('../../lib/utils');

module.exports = function (RED) {
const nodeOptions = {
Expand All @@ -15,13 +20,17 @@ module.exports = function (RED) {
outputinitially: {},
state_type: (nodeDef) => nodeDef.state_type || 'str',
output_only_on_state_change: {},
for: { value: '0' },
forType: { value: 'num' },
forUnits: { value: 'minutes' },
},
};

class ServerStateChangedNode extends EventsHaNode {
constructor(nodeDefinition) {
super(nodeDefinition, RED, nodeOptions);
let eventTopic = 'ha_events:state_changed';
this.topics = [];

if (this.nodeConfig.entityidfiltertype === 'exact') {
eventTopic = this.eventTopic = `ha_events:state_changed:${this.nodeConfig.entityidfilter}`;
Expand All @@ -47,82 +56,158 @@ module.exports = function (RED) {

async onHaEventsStateChanged(evt, runAll) {
const config = this.nodeConfig;
if (this.isEnabled === false || !this.isHomeAssistantRunning) {
return;
}
const { entity_id, event } = cloneDeep(evt);

if (!event.new_state) {
return;
}

event.new_state.timeSinceChangedMs =
Date.now() - new Date(event.new_state.last_changed).getTime();

// Convert and save original state if needed
if (config.state_type !== 'str') {
if (event.old_state) {
event.old_state.original_state = event.old_state.state;
event.old_state.state = this.getCastValue(
config.state_type,
event.old_state.state
);
}
event.new_state.original_state = event.new_state.state;
event.new_state.state = this.getCastValue(
config.state_type,
event.new_state.state
);
}

if (
this.isEnabled === false ||
!this.isHomeAssistantRunning ||
!evt.event.new_state ||
!shouldIncludeEvent(
entity_id,
evt.entity_id,
this.nodeConfig.entityidfilter,
this.nodeConfig.entityidfiltertype
)
) {
return;
}

const eventMessage = cloneDeep(evt);
const oldState = selectn('event.old_state', eventMessage);
const newState = selectn('event.new_state', eventMessage);

// Convert and save original state if needed
this.castState(oldState, config.state_type);
this.castState(newState, config.state_type);

// Output only on state change
if (
runAll === undefined &&
config.output_only_on_state_change === true &&
event.old_state &&

This comment has been minimized.

Copy link
@b3nj1

b3nj1 Nov 30, 2020

hassio-addons/addon-node-red#785

@zachowj , is there a reason you don't check that oldState is valid anymore?

event.old_state.state === event.new_state.state
oldState.state === newState.state
) {
return;
}

// Check if 'if state' is true
// Get if state condition
const isIfState = await this.getComparatorResult(
config.halt_if_compare,
config.haltIfState,
event.new_state.state,
newState.state,
config.halt_if_type,
{
entity: event.new_state,
prevEntity: event.old_state,
entity: newState,
prevEntity: oldState,
}
).catch((e) => {
);

// Track multiple entity ids
this.topics[eventMessage.entity_id] =
this.topics[eventMessage.entity_id] || {};

let timer;
try {
timer = this.getTimerValue();
} catch (e) {
this.node.error(e.message);
this.setStatusFailed('Error');
this.node.error(e.message, {});
return;
}
const validTimer = timer > 0;

// If if state is not used and prev and current state is the same return because timer should already be running
if (validTimer && oldState.state === newState.state) return;

// Don't run timers for on connect updates
if (validTimer && runAll) return;

if (
!validTimer ||
(config.haltIfState && !isIfState) ||
eventMessage.event_type === 'triggered'
) {
this.output(eventMessage, isIfState);
return;
}

const statusText = getWaitStatusText(
timer,
this.nodeConfig.forUnits
);
const timeout = getTimeInMilliseconds(
timer,
this.nodeConfig.forUnits
);

this.node.status({
text: statusText,
});

clearInterval(this.topics[eventMessage.entity_id].id);
this.topics[eventMessage.entity_id].id = setTimeout(
this.output.bind(this, eventMessage, isIfState),
timeout
);
}

getTimerValue() {
if (this.nodeConfig.for === '') return 0;
const timer = this.getTypedInputValue(
this.nodeConfig.for,
this.nodeConfig.forType
);

if (isNaN(timer) || timer < 0) {
throw new Error(`Invalid value for 'for': ${timer}`);
}

return timer;
}

getTypedInputValue(value, valueType, oldEntity, newEntity) {
let val;
switch (valueType) {
case 'flow':
case 'global':
val = this.getContextValue(valueType, value, null);
break;
case 'jsonata':
val = this.evaluateJSONata(
value,
null,
oldEntity,
newEntity
);
break;
case 'num':
default:
val = Number(value);
}
return val;
}

output(eventMessage, condition) {
const config = this.nodeConfig;
const msg = {
topic: entity_id,
payload: event.new_state.state,
data: event,
topic: eventMessage.entity_id,
payload: eventMessage.event.new_state.state,
data: eventMessage,
};

const statusMessage = `${event.new_state.state}${
evt.event_type === 'triggered' ? ` (triggered)` : ''
eventMessage.event.new_state.timeSinceChangedMs =
Date.now() -
new Date(eventMessage.event.new_state.last_changed).getTime();

const statusMessage = `${eventMessage.event.new_state.state}${
eventMessage.event.event_type === 'triggered'
? ` (triggered)`
: ''
}`;

clearInterval(this.topics[eventMessage.entity_id].id);

// Handle version 0 'halt if' outputs. The output were reversed true
// was sent to the second output and false was the first output
// ! Deprecated: Remove before 1.0
if (config.version < 1) {
if (config.haltIfState && isIfState) {
if (config.haltIfState && condition) {
this.setStatusFailed(statusMessage);
this.send([null, msg]);
return;
Expand All @@ -132,7 +217,7 @@ module.exports = function (RED) {
return;
}

if (config.haltIfState && !isIfState) {
if (config.haltIfState && !condition) {
this.setStatusFailed(statusMessage);
this.send([null, msg]);
return;
Expand All @@ -159,6 +244,8 @@ module.exports = function (RED) {
}

onStatesLoaded(entities) {
if (!this.isEnabled) return;

for (const entityId in entities) {
const eventMessage = {
event_type: 'state_changed',
Expand Down

0 comments on commit 61021a8

Please sign in to comment.