UNPKG

node-red-contrib-sun-position

Version:
449 lines (424 loc) 19.6 kB
/* * This code is licensed under the Apache License Version 2.0. * * Copyright (c) 2022 Robert Gester * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * */ /******************************************** * delay-until: *********************************************/ 'use strict'; /** --- Type Defs --- * @typedef {import('./types/typedefs.js').runtimeRED} runtimeRED * @typedef {import('./types/typedefs.js').runtimeNode} runtimeNode * @typedef {import('./types/typedefs.js').runtimeNodeConfig} runtimeNodeConfig * @typedef {import("./lib/dateTimeHelper.js").ITimeObject} ITimeObject * @typedef {import("./lib/dateTimeHelper.js").ILimitationsObj} ILimitationsObj * @typedef {import("./10-position-config.js").ITypedValue} ITypedValue * @typedef {import("./10-position-config.js").IValuePropertyType} IValuePropertyType * @typedef {import("./10-position-config.js").ITimePropertyType} ITimePropertyType * @typedef {import("./10-position-config.js").IPositionConfigNode} IPositionConfigNode * @typedef {import("./10-position-config.js").ITimePropertyResult} ITimePropertyResult */ /** * @typedef {Object} IDUPropertyTypeInst * @property {*} compare - valid days * * @typedef {ITypedValue & IDUPropertyTypeInst} IDUPropertyType */ /** * @typedef {Object} IDelayUntilNodeInstance Extensions for the nodeInstance object type * @property {IPositionConfigNode} positionConfig - tbd * * @property {ITimePropertyType} timeData - time definition * @property {('all'|'first'|'last')} queuingBehavior - kind of queue messages * @property {IDUPropertyType} flushMsgs - flush mesage control property definition * @property {IDUPropertyType} dropMsgs - drop mesage control property definition * @property {IDUPropertyType} enqueueMsg - enqueue mesage control property definition * @property {ITypedValue} ctrlProp - control property handling * * @property {number} tsCompare - base time definition * * @property {Array} msgQueue - the message queue * * @property {NodeJS.Timer} delayTimer - the message queue * * @property {ITimePropertyResult} nextTime - next time object * @property {boolean} nextTimeIntermedia - indicator if intermedia node state * @property {boolean} calcByMsg - indicator if time is calculared by message * */ /** * @typedef {IDelayUntilNodeInstance & runtimeNode} IDelayUntilNode Combine nodeInstance with additional, optional functions */ /******************************************************************************************/ /** Export the function that defines the node * @type {runtimeRED} */ module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; /** * standard Node-Red Node handler for the rdgDelayUntilNode * @param {*} config the Node-Red Configuration property of the Node */ function rdgDelayUntilNode(config) { const util = require('util'); const hlp = require('./lib/dateTimeHelper.js'); RED.nodes.createNode(this, config); /** Copy 'this' object in case we need it in context of callbacks of other functions. * @type {IDelayUntilNode} */ // @ts-ignore const node = this; // Retrieve the config node node.positionConfig = RED.nodes.getNode(config.positionConfig); // node.debug('initialize rdgDelayUntilNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); if (!node.positionConfig) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return null; } if (node.positionConfig.checkNode( error => { const text = RED._('node-red-contrib-sun-position/position-config:errors.config-error', { error }); node.error(text); node.status({fill: 'red', shape: 'dot', text }); return true; }, false)) { return null; } node.timeData = { type: config.timeType, value : config.time, offsetType : config.offsetType || 'none', offset : config.offset, multiplier : config.offsetMultiplier || 60000, next: true }; node.calcByMsg = (config.timeType === 'msg' || config.timeType === 'flow' || config.timeType === 'global'); if (node.timeData.type === 'jsonata') { try { node.timeData.expr = node.positionConfig.getJSONataExpression(node, node.timeData.value); } catch (err) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); node.timeData.expr = null; } } node.queuingBehavior = config.queuingBehavior; if (config.flushMsgsType && config.flushMsgsType !== 'none') { node.flushMsgs = { type: config.flushMsgsType, value : config.flushMsgs, compare : RED.util.evaluateNodeProperty(config.flushMsgsValue, config.flushMsgsValueType, node) }; } if (config.dropMsgsType && config.dropMsgsType !== 'none') { node.dropMsgs = { type: config.dropMsgsType, value : config.dropMsgs, compare : RED.util.evaluateNodeProperty(config.dropMsgsValue, config.dropMsgsValueType, node) }; } if (config.enqueueMsgType && config.enqueueMsgType !== 'none') { node.enqueueMsg = { type: config.enqueueMsgType, value : config.enqueueMsg, compare : RED.util.evaluateNodeProperty(config.enqueueMsgValue, config.enqueueMsgValueType, node) }; } if (config.ctrlPropChange === 'true' || config.ctrlPropChange === true) { node.ctrlProp = { type: config.ctrlPropValueType || 'delete', value : config.ctrlPropValue }; } node.tsCompare = parseInt(config.tsCompare) || 0; node.msgQueue = []; node.on('input', (msg, send, done) => { // If this is pre-1.0, 'done' will be undefined done = done || function (text, msg) { if (text) { return node.error(text, msg); } return null; }; send = send || function (...args) { node.send.apply(node, args); }; try { node.debug('--------- delay-until - input'); if (!node.positionConfig) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return null; } // this.debug('starting ' + util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })); // this.debug('self ' + util.inspect(this, { colors: true, compact: 10, breakLength: Infinity })); if (node.flushMsgs) { try { const result = RED.util.evaluateNodeProperty(node.flushMsgs.value, node.flushMsgs.type, node, msg); if (result == node.flushMsgs.compare) { // eslint-disable-line eqeqeq node.debug(`flush queue control property ${node.flushMsgs.value}=${result}`); flushEntireQueue(); clearTimer(); handleEnqueue(msg, done, node.flushMsgs); setStatus(); done(); return null; } } catch(_err) { node.debug(_err); } } if (node.dropMsgs) { try { const result = RED.util.evaluateNodeProperty(node.dropMsgs.value, node.dropMsgs.type, node, msg); if (result == node.dropMsgs.compare) { // eslint-disable-line eqeqeq node.debug(`flush queue control property ${node.dropMsgs.value}=${result}`); dropEntireQueue(); clearTimer(); handleEnqueue(msg, done, node.dropMsgs); setStatus(); done(); return null; } } catch(_err) { node.debug(_err); } } addMsgToQueue(msg, done); setStatus(); return null; } catch (err) { node.log(err.message); node.log(util.inspect(err)); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.error-title') }); done('internal error delay-until until:' + err.message, msg); } return null; }); node.on('close', () => { clearTimer(); }); /** * @typedef {Object} queueObject * @property {*} msg - the message to send * @property {*} done - the done function */ /** * get the Data for compare Date * @param {number} comparetype - type of compare * @param {*} msg - message object * @param {*} node - node object * @returns {*} Date value */ function getIntDate(comparetype, msg, node) { let id = ''; let value = ''; switch (comparetype) { case 1: id = 'msg.ts'; value = msg.ts; break; case 2: id = 'msg.lc'; value = msg.lc; break; case 3: id = 'msg.time'; value = msg.time; break; default: return new Date(); } node.debug('compare time to ' + id + ' = "' + value + '"'); const dto = new Date(value); if (hlp.isValidDate(dto)) { return dto; } node.error('Error can not get a valid timestamp from ' + id + '="' + value + '"! Will use current timestamp!'); return new Date(); } /** * clears all tmers */ function clearTimer() { if (node.delayTimer) { node.debug('clear timer'); clearTimeout(node.delayTimer); delete node.delayTimer; delete node.nextTime; } } /** * send all messages in queue */ function flushEntireQueue() { node.debug('flushEntireQueue - Flush all queued messages'); while (node.msgQueue.length > 0) { const item = node.msgQueue.shift(); node.send(item.msg); item.done(); } } /** * drop all messages from queue */ function dropEntireQueue() { node.debug('dropEntireQueue - Drop all queued messages'); while (node.msgQueue.length > 0) { const item = node.msgQueue.shift(); item.done(); } } /** * get the schedule time * @param {queueObject} qObj - message queue Object * @returns {boolean} returns true if ok */ function recalcTimeOut(qObj) { if (node.delayTimer) { clearTimer(); } node.timeData.now = getIntDate(node.tsCompare, qObj.msg, node); node.nextTime = node.positionConfig.getTimeProp(node, qObj.msg, node.timeData); if (node.nextTime.error) { node.debug('node.nextTime=' + util.inspect(node.nextTime, { colors: true, compact: 10, breakLength: Infinity })); hlp.handleError(node, node.nextTime.error, null, 'could not evaluate time'); return; } let millisec = hlp.getTimeOut(node.timeData.now, node.nextTime.value); // let millisec = node.nextTime.value.valueOf() - node.timeData.now.valueOf(); node.debug(`set timeout to ${node.nextTime.value.valueOf()} - ${node.timeData.now.valueOf()}`); // while (millisec < 1) { // millisec += hlp.TIME_24h; // 24h // } if (millisec > hlp.TIME_4d) { // there is a limitation of nodejs that the maximum setTimeout time // should not more then 2147483647 ms (24.8 days). millisec = Math.min((millisec - hlp.TIME_36h), 2147483646); // node.debug('next inject is far far away, plan a inject time recalc in ' + millisec + ' ms'); node.delayTimer = setTimeout(() => { delete node.delayTimer; recalcTimeOut(qObj); }, millisec); // 1,5 days before node.nextTimeIntermedia = true; } else { node.debug('set timeout to ' + millisec); node.delayTimer = setTimeout(() => { delete node.delayTimer; flushEntireQueue(); setStatus(); }, millisec); } setStatus(); } /** * adds a new message tot he queue * @param {*} msg - message object * @param {*} done - done object */ function addMsgToQueue(msg, done) { if (node.queuingBehavior === 'first' && node.msgQueue.length > 0) { done(); return; } if (node.queuingBehavior === 'last') { dropEntireQueue(); } node.debug('test 3'); const qObj = {msg, done}; node.msgQueue.push(qObj); node.debug('test 4'); if (!node.delayTimer || node.calcByMsg) { recalcTimeOut(qObj); } } /** * adds a new message tot he queue * @param {Object} msg - message object * @param {*} done - done object * @param {Object} ctrlProp - control property object */ function handleEnqueue(msg, done, ctrlProp) { try { if (node.enqueueMsg) { const enqueue = RED.util.evaluateNodeProperty(node.enqueueMsg.value, node.enqueueMsg.type, node, msg); if (enqueue == node.enqueueMsg.compare) { // eslint-disable-line eqeqeq if (node.ctrlProp) { if (node.ctrlProp.type === 'delete') { RED.util.setMessageProperty(msg, ctrlProp.value); RED.util.setMessageProperty(msg, node.enqueueMsg.value); } else { const data = RED.util.evaluateNodeProperty(node.ctrlProp.value, node.ctrlProp.type, node, msg); RED.util.setMessageProperty(msg, ctrlProp.value, data, true); RED.util.setMessageProperty(msg, node.enqueueMsg.value, data, true); } } addMsgToQueue(msg, done); } } } catch(_err) { node.debug(_err); } } /** * adds a new message tot he queue */ function setStatus() { if (node.msgQueue.length > 0) { try { if (node.nextTime) { node.debug('set state ' + util.inspect(node.nextTime, { colors: true, compact: 10, breakLength: Infinity })); if (node.nextTime.error ) { node.status({fill: 'red', shape: 'ring', text: node.nextTime.error }); } else if (node.nextTimeIntermedia) { node.status({ fill: 'yellow', shape: 'dot', text: RED._('delay-until.state.intermedia', { queueLength: node.msgQueue.length, sendTime: node.positionConfig.toDateTimeString(node.nextTime.value) }) }); } else { node.status({ fill: 'green', shape: 'dot', text: RED._('delay-until.state.default', { queueLength: node.msgQueue.length, sendTime: node.positionConfig.toDateTimeString(node.nextTime.value) }) }); } } else { node.status({ fill: 'green', shape: 'dot', text: RED._('delay-until.state.noTime', { queueLength: node.msgQueue.length }) }); } } catch(_err) { node.error(_err.message); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.error-title') }); } } else { node.status({}); } } node.status({}); return null; } RED.nodes.registerType('rdg-delay-until', rdgDelayUntilNode); };