node-red-contrib-boolean-logic-ultimate
Version:
A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, debouncer, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.
198 lines (170 loc) • 5.39 kB
JavaScript
;
module.exports = function (RED) {
const helpers = require('./lib/node-helpers.js');
function HysteresisUltimate(config) {
RED.nodes.createNode(this, config);
const node = this;
const REDUtil = RED.util;
const setNodeStatus = helpers.createStatus(node);
const controlTopic = config.controlTopic || 'hysteresis';
const payloadPropName = config.payloadPropName || 'payload';
const mode = (config.mode || 'high').toLowerCase(); // high | low
const emitOnlyOnChange = config.emitOnlyOnChange !== false;
let onThreshold = Number(config.onThreshold);
let offThreshold = Number(config.offThreshold);
let state = Boolean(config.initialState);
if (!Number.isFinite(onThreshold)) onThreshold = 70;
if (!Number.isFinite(offThreshold)) offThreshold = 65;
function toNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'boolean') {
return value ? 1 : 0;
}
if (typeof value === 'string') {
const v = Number(value.trim());
return Number.isFinite(v) ? v : undefined;
}
return undefined;
}
function evaluateTyped(typedValue, typedType, baseMsg, fallback) {
try {
return REDUtil.evaluateNodeProperty(typedValue, typedType || 'bool', node, baseMsg);
} catch (error) {
return fallback;
}
}
function updateStatus(lastValue) {
const direction = mode === 'low' ? 'LOW' : 'HIGH';
const valueText = lastValue === undefined ? '-' : Number(lastValue).toFixed(2);
setNodeStatus({
fill: state ? 'green' : 'grey',
shape: state ? 'dot' : 'ring',
text: `${direction} ${valueText} on:${onThreshold} off:${offThreshold}`,
});
}
function evaluateState(value) {
let nextState = state;
if (mode === 'low') {
if (value <= onThreshold) {
nextState = true;
} else if (value >= offThreshold) {
nextState = false;
}
} else {
if (value >= onThreshold) {
nextState = true;
} else if (value <= offThreshold) {
nextState = false;
}
}
return nextState;
}
function emitDiagnostics(baseMsg, payload) {
const msg = baseMsg ? REDUtil.cloneMessage(baseMsg) : {};
msg.topic = `${controlTopic}/event`;
msg.payload = payload;
node.send([null, msg]);
}
function emitState(baseMsg, changed, inputValue) {
if (!changed && emitOnlyOnChange) {
updateStatus(inputValue);
return;
}
const msg = baseMsg ? REDUtil.cloneMessage(baseMsg) : {};
msg.payload = state
? evaluateTyped(config.onPayload, config.onPayloadType, baseMsg, true)
: evaluateTyped(config.offPayload, config.offPayloadType, baseMsg, false);
msg.hysteresis = {
state,
changed,
mode,
value: inputValue,
onThreshold,
offThreshold,
};
msg.event = changed ? 'state_changed' : 'state_confirmed';
node.send([msg, null]);
updateStatus(inputValue);
}
function handleControl(msg) {
let consumed = false;
if (msg.reset === true) {
state = Boolean(config.initialState);
emitDiagnostics(msg, {
event: 'reset',
state,
onThreshold,
offThreshold,
mode,
});
consumed = true;
}
if (Object.prototype.hasOwnProperty.call(msg, 'onThreshold')) {
const next = Number(msg.onThreshold);
if (Number.isFinite(next)) {
onThreshold = next;
consumed = true;
}
}
if (Object.prototype.hasOwnProperty.call(msg, 'offThreshold')) {
const next = Number(msg.offThreshold);
if (Number.isFinite(next)) {
offThreshold = next;
consumed = true;
}
}
if (Object.prototype.hasOwnProperty.call(msg, 'state')) {
state = Boolean(msg.state);
emitState(msg, true, undefined);
consumed = true;
}
if (msg.status === true) {
emitDiagnostics(msg, {
event: 'status',
state,
mode,
onThreshold,
offThreshold,
});
consumed = true;
}
if (consumed) {
updateStatus();
}
return consumed;
}
node.on('input', (msg) => {
if (msg.topic === controlTopic && handleControl(msg)) {
return;
}
const resolved = helpers.resolveInput(msg, payloadPropName, config.translatorConfig, RED);
const inputValue = toNumber(resolved.value);
if (inputValue === undefined) {
emitDiagnostics(msg, {
event: 'invalid_input',
value: resolved.value,
property: payloadPropName,
});
return;
}
const nextState = evaluateState(inputValue);
const changed = nextState !== state;
state = nextState;
emitState(msg, changed, inputValue);
if (changed) {
emitDiagnostics(msg, {
event: 'state_changed',
state,
value: inputValue,
mode,
onThreshold,
offThreshold,
});
}
});
updateStatus();
}
RED.nodes.registerType('HysteresisUltimate', HysteresisUltimate);
};