From 3122fc8f16b5b0fd9ded3f418da9a6c319432ff3 Mon Sep 17 00:00:00 2001 From: Hypnos Date: Wed, 10 Nov 2021 11:32:34 +0100 Subject: [PATCH 01/44] initial --- nodes/blind-control.html | 6 +- nodes/blind-control.js | 87 ++++++++++------ nodes/clock-timer.js | 20 ++-- nodes/lib/timeControlHelper.js | 185 ++++++++++++++++++++++++++++++--- package-lock.json | 2 +- 5 files changed, 235 insertions(+), 65 deletions(-) diff --git a/nodes/blind-control.html b/nodes/blind-control.html index d90a76c..90a9fd9 100644 --- a/nodes/blind-control.html +++ b/nodes/blind-control.html @@ -9,7 +9,7 @@ const cNBC_RULE_TYPE_UNTIL = 0; const cNBC_RULE_TYPE_FROM = 1; const cNBC_RULE_OP = { - levelAbsolute : 0, + absolute : 0, levelMinOversteer : 1, // ⭳❗ minimum (oversteer) levelMaxOversteer : 2, // ⭱️❗ maximum (oversteer) slatOversteer : 5, @@ -1091,7 +1091,7 @@ } result += '
'; if (data.level) { - if (data.level.operator <= cNBC_RULE_OP.levelAbsolute || data.levelType === types.LevelND.value) { + if (data.level.operator <= cNBC_RULE_OP.absolute || data.levelType === types.LevelND.value) { if (typeof data.level.type !== 'undefined' && data.level.type !== types.LevelND.value) { result += '
'; result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleBlindLevel'); @@ -2736,7 +2736,7 @@ }).change(() => { $dialogLevelOperator.change(); }); $dialogLevel.typedInput('width', '40%'); const $dialogLevelOperator = $dialog.find('#dlg-ip-btctl-rule-level-operator'); - $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelAbsolute ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelAbs'))); + $dialogLevelOperator.append($('').val(cNBC_RULE_OP.absolute ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelAbs'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelMinOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelMin'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelMaxOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelMax'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.slatOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleSlatOversteer'))); diff --git a/nodes/blind-control.js b/nodes/blind-control.js index 3a40239..91c2436 100644 --- a/nodes/blind-control.js +++ b/nodes/blind-control.js @@ -9,22 +9,11 @@ const util = require('util'); const clonedeep = require('lodash.clonedeep'); const isEqual = require('lodash.isequal'); -const cRuleUntil = 0; -const cRuleFrom = 1; -const cRule = { - levelAbsolute : 0, - levelMinOversteer : 1, // ⭳❗ minimum (oversteer) - levelMaxOversteer : 2, // ⭱️❗ maximum (oversteer) - slatOversteer : 5, - topicOversteer : 8, - off : 9 -}; const cautoTriggerTimeBeforeSun = 10 * 60000; // 10 min const cautoTriggerTimeSun = 5 * 60000; // 5 min const cWinterMode = 1; const cMinimizeMode = 3; const cSummerMode = 16; -const cRuleDefault = -1; /******************************************************************************************/ /** * get the absolute level from percentage level @@ -511,9 +500,8 @@ module.exports = function (RED) { */ function checkRules(node, msg, oNow, tempData) { // node.debug('checkRules --------------------'); - const livingRuleData = {}; ctrlLib.prepareRules(node, msg, tempData, oNow.dNow); - // node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); + //node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); let ruleSel = null; let ruleSlatOvs = null; @@ -524,53 +512,53 @@ module.exports = function (RED) { // node.debug('first loop count:' + node.rules.count + ' lastuntil:' + node.rules.lastUntil); for (let i = 0; i <= node.rules.lastUntil; ++i) { const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleFrom) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === cRuleFrom) { continue; } + if (rule.time && rule.time.operator === ctrlLib.cRuleTime.from) { continue; } // const res = fktCheck(rule, r => (r >= nowNr)); let res = null; - if (!rule.time || rule.time.operator === cRuleFrom) { + if (!rule.time || rule.time.operator === ctrlLib.cRuleTime.from) { res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); } else { + //node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== ctrlLib.cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); } if (res) { - // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); - if (res.level.operator === cRule.slatOversteer) { + //node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === ctrlLib.cRuleType.slatOversteer) { ruleSlatOvs = res; - } else if (res.level.operator === cRule.topicOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.topicOversteer) { ruleTopicOvs = res; - } else if (res.level.operator === cRule.levelMinOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.levelMinOversteer) { ruleSelMin = res; - } else if (res.level.operator === cRule.levelMaxOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.levelMaxOversteer) { ruleSelMax = res; } else { ruleSel = res; ruleindex = i; - if (rule.time && rule.time.operator !== cRuleFrom) { + if (rule.time && rule.time.operator !== ctrlLib.cRuleTime.from) { break; } } } } - if (!ruleSel || (ruleSel.time && ruleSel.time.operator === cRuleFrom) ) { - // node.debug('--------- starting second loop ' + node.rules.count); + if (!ruleSel || (ruleSel.time && ruleSel.time.operator === ctrlLib.cRuleTime.from) ) { + //node.debug('--------- starting second loop ' + node.rules.count); for (let i = (node.rules.count - 1); i >= 0; --i) { const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleUntil) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + //node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== ctrlLib.cRuleTime.until) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === cRuleUntil) { continue; } // - From: timeOp === cRuleFrom + if (rule.time && rule.time.operator === ctrlLib.cRuleTime.until) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from const res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); if (res) { - // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); - if (res.level.operator === cRule.slatOversteer) { + //node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === ctrlLib.cRuleType.slatOversteer) { ruleSlatOvs = res; - } else if (res.level.operator === cRule.topicOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.topicOversteer) { ruleTopicOvs = res; - } else if (res.level.operator === cRule.levelMinOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.levelMinOversteer) { ruleSelMin = res; - } else if (res.level.operator === cRule.levelMaxOversteer) { + } else if (res.level.operator === ctrlLib.cRuleType.levelMaxOversteer) { ruleSelMax = res; } else { ruleSel = res; @@ -580,6 +568,37 @@ module.exports = function (RED) { } } + const rule = ctrlLib.checkRules(node, msg, oNow, tempData); + if (ruleSel != rule.ruleSel) { + node.error('not equal result ruleSel!'); + node.debug('ruleSel ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + } else { + node.debug('same rule selected ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('same rule selected ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); + } + if (ruleSlatOvs != rule.ruleSlatOvs) { + node.error('not equal result ruleSlatOvs!'); + node.debug('ruleSlatOvs ' + util.inspect(ruleSlatOvs, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + } + if (ruleTopicOvs != rule.ruleTopicOvs) { + node.error('not equal result ruleTopicOvs!'); + node.debug('ruleTopicOvs ' + util.inspect(ruleTopicOvs, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + } + if (ruleSelMin != rule.ruleSelMin) { + node.error('not equal result ruleSelMin!'); + node.debug('ruleSelMin ' + util.inspect(ruleSelMin, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + } + if (ruleSelMax != rule.ruleSelMax) { + node.error('not equal result ruleSelMax!'); + node.debug('ruleSelMax ' + util.inspect(ruleSelMax, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + } + const livingRuleData = {}; + livingRuleData.importance = 0; livingRuleData.resetOverwrite = false; if (ruleTopicOvs) { @@ -717,9 +736,9 @@ module.exports = function (RED) { livingRuleData.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.'+name, data); // node.debug(`checkRules end livingRuleData=${util.inspect(livingRuleData, { colors: true, compact: 10, breakLength: Infinity })}`); - if (ruleSel.level.operator === cRule.off) { + if (ruleSel.level.operator === ctrlLib.cRuleType.off) { livingRuleData.isOff = true; - } else if (ruleSel.level.operator === cRule.levelAbsolute) { // absolute rule + } else if (ruleSel.level.operator === ctrlLib.cRuleType.absolute) { // absolute rule livingRuleData.level = getBlindPosFromTI(node, msg, ruleSel.level.type, ruleSel.level.value, -1); livingRuleData.slat = node.positionConfig.getPropValue(node, msg, ruleSel.slat, false, oNow.dNow); livingRuleData.active = (livingRuleData.level > -1); @@ -731,7 +750,7 @@ module.exports = function (RED) { return livingRuleData; } livingRuleData.active = false; - livingRuleData.id = cRuleDefault; + livingRuleData.id = ctrlLib.cRuleDefault; livingRuleData.importance = 0; livingRuleData.resetOverwrite = false; livingRuleData.level = node.nodeData.levelDefault; diff --git a/nodes/clock-timer.js b/nodes/clock-timer.js index 5a6a7e1..5947a52 100644 --- a/nodes/clock-timer.js +++ b/nodes/clock-timer.js @@ -9,10 +9,6 @@ const util = require('util'); const clonedeep = require('lodash.clonedeep'); const isEqual = require('lodash.isequal'); -const cRuleUntil = 0; -const cRuleFrom = 1; -const cRuleDefault = -1; - /******************************************************************************************/ module.exports = function (RED) { 'use strict'; @@ -162,12 +158,12 @@ module.exports = function (RED) { // node.debug('first loop count:' + node.rules.count + ' lastuntil:' + node.rules.lastUntil); for (let i = 0; i <= node.rules.lastUntil; ++i) { const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleFrom) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== ctrlLib.cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === cRuleFrom) { continue; } + if (rule.time && rule.time.operator === ctrlLib.cRuleTime.from) { continue; } // const res = fktCheck(rule, r => (r >= nowNr)); let res = null; - if (!rule.time || rule.time.operator === cRuleFrom) { + if (!rule.time || rule.time.operator === ctrlLib.cRuleTime.from) { res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); } else { res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); @@ -176,19 +172,19 @@ module.exports = function (RED) { // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); ruleSel = res; ruleindex = i; - if (rule.time && rule.time.operator !== cRuleFrom) { + if (rule.time && rule.time.operator !== ctrlLib.cRuleTime.from) { break; } } } - if (!ruleSel || (ruleSel.time && ruleSel.time.operator === cRuleFrom) ) { + if (!ruleSel || (ruleSel.time && ruleSel.time.operator === ctrlLib.cRuleTime.from) ) { // node.debug('--------- starting second loop ' + node.rules.count); for (let i = (node.rules.count - 1); i >= 0; --i) { const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleUntil) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== ctrlLib.cRuleTime.until) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === cRuleUntil) { continue; } // - From: timeOp === cRuleFrom + if (rule.time && rule.time.operator === ctrlLib.cRuleTime.until) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from const res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); if (res) { // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); @@ -272,7 +268,7 @@ module.exports = function (RED) { return livingRuleData; } livingRuleData.active = false; - livingRuleData.id = cRuleDefault; + livingRuleData.id = ctrlLib.cRuleDefault; livingRuleData.importance = 0; livingRuleData.resetOverwrite = false; livingRuleData.payloadData = { diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index bbded38..d7d3112 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -7,6 +7,25 @@ const path = require('path'); const util = require('util'); const hlp = require( path.resolve( __dirname, './dateTimeHelper.js') ); +const cRuleTime = { + until: 0, + from: 1, + at: 2 +} +const cRuleType = { + absolute : 0, + levelMinOversteer : 1, // ⭳❗ minimum (oversteer) + levelMaxOversteer : 2, // ⭱️❗ maximum (oversteer) + slatOversteer : 5, + topicOversteer : 8, + off : 9 +}; + +const cRuleDefault = -1; +const cRuleLogOperatorAnd = 2; +const cRuleLogOperatorOr = 1; + + module.exports = { evalTempData, posOverwriteReset, @@ -17,19 +36,17 @@ module.exports = { getRuleTimeData, validPosition, compareRules, - initializeCtrl + checkRules, + initializeCtrl, + cRuleTime, + cRuleType, + cRuleDefault, + cRuleLogOperatorAnd, + cRuleLogOperatorOr }; -const cRuleUntil = 0; -const cRuleFrom = 1; -const cRuleAbsolute = 0; -// const cRuleNone = 0; -// const cRuleMinOversteer = 1; // ⭳❗ minimum (oversteer) -// const cRuleMaxOversteer = 2; // ⭱️❗ maximum (oversteer) -const cRuleLogOperatorAnd = 2; -const cRuleLogOperatorOr = 1; let RED = null; - +/******************************************************************************************/ /** * evaluate temporary Data * @param {*} node node Data @@ -395,6 +412,138 @@ function compareRules(node, msg, rule, cmp, data) { } return null; } +/******************************************************************************************/ +/** + * check all rules and determinate the active rule + * @param {Object} node node data + * @param {Object} msg the message object + * @param {ITimeObject} oNow the *current* date Object + * @param {Object} tempData the object storing the temporary caching data + * @returns the active rule or null + */ +function checkRules(node, msg, oNow, tempData) { + // node.debug('checkRules --------------------'); + const livingRuleData = {}; + prepareRules(node, msg, tempData, oNow.dNow); + // node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); + const result = { + ruleindex : -1, + ruleSel : null + } + + // node.debug('first loop count:' + node.rules.count + ' lastAt:' + node.rules.lastAt + ' lastuntil:' + node.rules.lastUntil); + if (node.rules.atCount > 0) { + for (let i = 0; i <= node.rules.lastAt; ++i) { + const rule = node.rules.data[i]; + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + if (!rule.enabled) { continue; } + if (rule.time && rule.time.operator !== cRuleTime.at) { continue; } + // const res = fktCheck(rule, r => (r >= nowNr)); + const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater than rule time + if (res) { + // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === cRuleType.slatOversteer) { + result.ruleSlatOvs = res; + } else if (res.level.operator === cRuleType.topicOversteer) { + result.ruleTopicOvs = res; + } else if (res.level.operator === cRuleType.levelMinOversteer) { + result.ruleSelMin = res; + } else if (res.level.operator === cRuleType.levelMaxOversteer) { + result.ruleSelMax = res; + } else { + result.ruleSel = res; + result.ruleindex = i; + } + } else if (rule.time) { + // break on the first at rule, where the time does not match + break; + } + } + } + + if (!result.ruleSel) { + for (let i = 0; i <= node.rules.lastUntil; ++i) { + const rule = node.rules.data[i]; + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + if (!rule.enabled) { continue; } + if (rule.time && rule.time.operator !== cRuleTime.until) { continue; } + // const res = fktCheck(rule, r => (r >= nowNr)); + const res = compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); // now is less time + if (res) { + // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === cRuleType.slatOversteer) { + result.ruleSlatOvs = res; + } else if (res.level.operator === cRuleType.topicOversteer) { + result.ruleTopicOvs = res; + } else if (res.level.operator === cRuleType.levelMinOversteer) { + result.ruleSelMin = res; + } else if (res.level.operator === cRuleType.levelMaxOversteer) { + result.ruleSelMax = res; + } else { + result.ruleSel = res; + result.ruleindex = i; + break; + } + } + } + } + + if (!result.ruleSel) { + for (let i = 0; i < node.rules.count; ++i) { + const rule = node.rules.data[i]; + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + if (!rule.enabled) { continue; } + if (rule.time && rule.time.operator !== cRuleTime.from) { continue; } + // const res = fktCheck(rule, r => (r >= nowNr)); + const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater than rule time + if (res) { + // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === cRuleType.slatOversteer) { + result.ruleSlatOvs = res; + } else if (res.level.operator === cRuleType.topicOversteer) { + result.ruleTopicOvs = res; + } else if (res.level.operator === cRuleType.levelMinOversteer) { + result.ruleSelMin = res; + } else if (res.level.operator === cRuleType.levelMaxOversteer) { + result.ruleSelMax = res; + } else { + result.ruleSel = res; + result.ruleindex = i; + } + } else if (rule.time) { + // break on the first at rule, where the time does not match + break; + } + } + // node.debug('--------- starting second loop ' + node.rules.count); + /* + for (let i = (node.rules.count - 1); i >= 0; --i) { + const rule = node.rules.data[i]; + // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.until) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + if (!rule.enabled) { continue; } + if (rule.time && rule.time.operator !== cRuleTime.from) { continue; } // - From: timeOp === cRuleTime.from + const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater time + if (res) { + // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === cRuleType.slatOversteer) { + result.ruleSlatOvs = res; + } else if (res.level.operator === cRuleType.topicOversteer) { + result.ruleTopicOvs = res; + } else if (res.level.operator === cRuleType.levelMinOversteer) { + result.ruleSelMin = res; + } else if (res.level.operator === cRuleType.levelMaxOversteer) { + result.ruleSelMax = res; + } else { + result.ruleSel = res; + break; + } + } + } + */ + } + + return result; +} /*************************************************************************************************************************/ /** * check if a level has a valid value @@ -510,6 +659,8 @@ function initializeCtrl(REDLib, node, config) { node.rules.count = node.rules.data.length; node.rules.lastUntil = node.rules.count -1; node.rules.firstFrom = node.rules.lastUntil; + node.rules.lastAt = node.rules.count -1; + node.rules.atCount = 0; node.rules.firstTimeLimited = node.rules.count; node.rules.maxImportance = 0; node.rules.canResetOverwrite = false; @@ -566,7 +717,7 @@ function initializeCtrl(REDLib, node, config) { rule.time = { type : rule.timeType, value : (rule.timeValue || ''), - operator : (parseInt(rule.timeOp) || cRuleUntil), + operator : (parseInt(rule.timeOp) || cRuleTime.until), offsetType : (rule.offsetType || 'none'), offset : (rule.offsetValue || 1), multiplier : (parseInt(rule.multiplier) || 60000), @@ -632,7 +783,7 @@ function initializeCtrl(REDLib, node, config) { rule.level = { type : (rule.levelType || 'levelFixed'), value : (rule.levelValue || 'closed (min)'), - operator : (parseInt(rule.levelOp) || cRuleAbsolute), + operator : (parseInt(rule.levelOp) || cRuleType.absolute), operatorText : rule.levelOpText || RED._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelAbs') }; } @@ -677,7 +828,7 @@ function initializeCtrl(REDLib, node, config) { rule.name = rule.name || 'rule ' + rule.pos; rule.enabled = !(rule.enabled === false || rule.enabled === 'false'); rule.resetOverwrite = hlp.isTrue(rule.resetOverwrite === true) ? true : false; - if (rule.payload || (rule.level && (rule.level.operator === cRuleAbsolute))) { + if (rule.payload || (rule.level && (rule.level.operator === cRuleType.absolute))) { rule.importance = Number(rule.importance) || 0; node.rules.maxImportance = Math.max(node.rules.maxImportance, rule.importance); node.rules.canResetOverwrite = node.rules.canResetOverwrite || rule.resetOverwrite; @@ -688,10 +839,14 @@ function initializeCtrl(REDLib, node, config) { if (rule.timeMax) { rule.timeMax.next = false; } if (rule.timeMin) { rule.timeMin.next = false; } node.rules.firstTimeLimited = Math.min(i, node.rules.firstTimeLimited); - if (rule.time.operator === cRuleUntil) { + if (rule.time.operator === cRuleTime.at) { + node.rules.atCount++; + node.rules.lastAt = i; + } + if (rule.time.operator === cRuleTime.until) { node.rules.lastUntil = i; } - if (rule.time.operator === cRuleFrom) { + if (rule.time.operator === cRuleTime.from) { node.rules.firstFrom = Math.min(i,node.rules.firstFrom); } if (!rule.time.days || rule.time.days === '*') { diff --git a/package-lock.json b/package-lock.json index aa97bbd..927694f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-sun-position", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { From d2ddc52e8a664e340f865385455ceb4bf1847b24 Mon Sep 17 00:00:00 2001 From: Hypnos Date: Thu, 11 Nov 2021 17:27:54 +0100 Subject: [PATCH 02/44] dev --- nodes/lib/timeControlHelper.js | 426 ++++++++++++++++++--------------- 1 file changed, 229 insertions(+), 197 deletions(-) diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index d7d3112..faa41cd 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -2,16 +2,13 @@ * dateTimeHelper.js: *********************************************/ 'use strict'; +const { time } = require('console'); +const { endsWith } = require('lodash'); const path = require('path'); const util = require('util'); const hlp = require( path.resolve( __dirname, './dateTimeHelper.js') ); -const cRuleTime = { - until: 0, - from: 1, - at: 2 -} const cRuleType = { absolute : 0, levelMinOversteer : 1, // ⭳❗ minimum (oversteer) @@ -24,7 +21,8 @@ const cRuleType = { const cRuleDefault = -1; const cRuleLogOperatorAnd = 2; const cRuleLogOperatorOr = 1; - +const cNBC_RULE_TYPE_UNTIL = 0; +const cNBC_RULE_TYPE_FROM = 1; module.exports = { evalTempData, @@ -38,7 +36,6 @@ module.exports = { compareRules, checkRules, initializeCtrl, - cRuleTime, cRuleType, cRuleDefault, cRuleLogOperatorAnd, @@ -273,9 +270,10 @@ function prepareRules(node, msg, tempData, dNow) { * @param {*} rule the rule data * @return {number} timestamp of the rule */ -function getRuleTimeData(node, msg, rule, dNow) { +function getRuleTimeData(node, msg, rule, timep, dNow) { rule.time.dNow = dNow; - rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time); + rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time[timep]); + if (rule.timeData.error) { hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeData.error }), undefined, rule.timeData.error); return -1; @@ -372,41 +370,79 @@ function compareRules(node, msg, rule, cmp, data) { if (!rule.time) { return rule; } - if (rule.time.days && !rule.time.days.includes(data.dayNr)) { - return null; - } - if (rule.time.months && !rule.time.months.includes(data.monthNr)) { - return null; - } - if (rule.time.onlyOddDays && (data.dateNr % 2 === 0)) { // even - return null; - } - if (rule.time.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd - return null; - } - if (rule.time.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even - return null; - } - if (rule.time.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd - return null; - } - if (rule.time.dateStart || rule.time.dateEnd) { - rule.time.dateStart.setFullYear(data.yearNr); - rule.time.dateEnd.setFullYear(data.yearNr); - if (rule.time.dateEnd > rule.time.dateStart) { - // in the current year - if (data.dNow < rule.time.dateStart || data.dNow > rule.time.dateEnd) { - return null; + if (rule.time.start) { + if (rule.time.start.days && !rule.time.start.days.includes(data.dayNr)) { + return null; + } + if (rule.time.start.months && !rule.time.start.months.includes(data.monthNr)) { + return null; + } + if (rule.time.start.onlyOddDays && (data.dateNr % 2 === 0)) { // even + return null; + } + if (rule.time.start.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd + return null; + } + if (rule.time.start.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even + return null; + } + if (rule.time.start.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd + return null; + } + if (rule.time.start.dateStart || rule.time.start.dateEnd) { + rule.time.start.dateStart.setFullYear(data.yearNr); + rule.time.start.dateEnd.setFullYear(data.yearNr); + if (rule.time.start.dateEnd > rule.time.start.dateStart) { + // in the current year + if (data.dNow < rule.time.start.dateStart || data.dNow > rule.time.start.dateEnd) { + return null; + } + } else { + // switch between year from end to start + if (data.dNow < rule.time.start.dateStart && data.dNow > rule.time.start.dateEnd) { + return null; + } } - } else { - // switch between year from end to start - if (data.dNow < rule.time.dateStart && data.dNow > rule.time.dateEnd) { - return null; + } + } else if (rule.time.end) { + if (rule.time.end.days && !rule.time.end.days.includes(data.dayNr)) { + return null; + } + if (rule.time.end.months && !rule.time.end.months.includes(data.monthNr)) { + return null; + } + if (rule.time.end.onlyOddDays && (data.dateNr % 2 === 0)) { // even + return null; + } + if (rule.time.end.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd + return null; + } + if (rule.time.end.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even + return null; + } + if (rule.time.end.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd + return null; + } + if (rule.time.end.dateStart || rule.time.end.dateEnd) { + rule.time.end.dateStart.setFullYear(data.yearNr); + rule.time.end.dateEnd.setFullYear(data.yearNr); + if (rule.time.end.dateEnd > rule.time.end.dateStart) { + // in the current year + if (data.dNow < rule.time.end.dateStart || data.dNow > rule.time.end.dateEnd) { + return null; + } + } else { + // switch between year from end to start + if (data.dNow < rule.time.end.dateStart && data.dNow > rule.time.end.dateEnd) { + return null; + } } } + } else { + return null; } const num = getRuleTimeData(node, msg, rule, data.dNow); - // node.debug(`pos=${rule.pos} type=${rule.time.operatorText} - ${rule.time.value} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`); + // node.debug(`pos=${rule.pos} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`); if (data.dayId === rule.timeData.dayId && num >=0 && (cmp(num) === true)) { return rule; } @@ -429,99 +465,40 @@ function checkRules(node, msg, oNow, tempData) { const result = { ruleindex : -1, ruleSel : null - } + }; - // node.debug('first loop count:' + node.rules.count + ' lastAt:' + node.rules.lastAt + ' lastuntil:' + node.rules.lastUntil); - if (node.rules.atCount > 0) { - for (let i = 0; i <= node.rules.lastAt; ++i) { - const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator !== cRuleTime.at) { continue; } - // const res = fktCheck(rule, r => (r >= nowNr)); - const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater than rule time - if (res) { - // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); - if (res.level.operator === cRuleType.slatOversteer) { - result.ruleSlatOvs = res; - } else if (res.level.operator === cRuleType.topicOversteer) { - result.ruleTopicOvs = res; - } else if (res.level.operator === cRuleType.levelMinOversteer) { - result.ruleSelMin = res; - } else if (res.level.operator === cRuleType.levelMaxOversteer) { - result.ruleSelMax = res; - } else { - result.ruleSel = res; - result.ruleindex = i; - } - } else if (rule.time) { - // break on the first at rule, where the time does not match + for (let i = 0; i <= node.rules.lastUntil; ++i) { + const rule = node.rules.data[i]; + // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + if (!rule.enabled) { continue; } + if (rule.time && !rule.time.end) { continue; } + // const res = fktCheck(rule, r => (r >= nowNr)); + const res = compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); // now is less time + if (res) { + // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + if (res.level.operator === cRuleType.slatOversteer) { + result.ruleSlatOvs = res; + } else if (res.level.operator === cRuleType.topicOversteer) { + result.ruleTopicOvs = res; + } else if (res.level.operator === cRuleType.levelMinOversteer) { + result.ruleSelMin = res; + } else if (res.level.operator === cRuleType.levelMaxOversteer) { + result.ruleSelMax = res; + } else { + result.ruleSel = res; + result.ruleindex = i; break; } } } if (!result.ruleSel) { - for (let i = 0; i <= node.rules.lastUntil; ++i) { - const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator !== cRuleTime.until) { continue; } - // const res = fktCheck(rule, r => (r >= nowNr)); - const res = compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); // now is less time - if (res) { - // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); - if (res.level.operator === cRuleType.slatOversteer) { - result.ruleSlatOvs = res; - } else if (res.level.operator === cRuleType.topicOversteer) { - result.ruleTopicOvs = res; - } else if (res.level.operator === cRuleType.levelMinOversteer) { - result.ruleSelMin = res; - } else if (res.level.operator === cRuleType.levelMaxOversteer) { - result.ruleSelMax = res; - } else { - result.ruleSel = res; - result.ruleindex = i; - break; - } - } - } - } - - if (!result.ruleSel) { - for (let i = 0; i < node.rules.count; ++i) { - const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator !== cRuleTime.from) { continue; } - // const res = fktCheck(rule, r => (r >= nowNr)); - const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater than rule time - if (res) { - // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); - if (res.level.operator === cRuleType.slatOversteer) { - result.ruleSlatOvs = res; - } else if (res.level.operator === cRuleType.topicOversteer) { - result.ruleTopicOvs = res; - } else if (res.level.operator === cRuleType.levelMinOversteer) { - result.ruleSelMin = res; - } else if (res.level.operator === cRuleType.levelMaxOversteer) { - result.ruleSelMax = res; - } else { - result.ruleSel = res; - result.ruleindex = i; - } - } else if (rule.time) { - // break on the first at rule, where the time does not match - break; - } - } // node.debug('--------- starting second loop ' + node.rules.count); - /* for (let i = (node.rules.count - 1); i >= 0; --i) { const rule = node.rules.data[i]; - // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleTime.until) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator !== cRuleTime.from) { continue; } // - From: timeOp === cRuleTime.from + if (rule.time && !rule.time.start) { continue; } const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater time if (res) { // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); @@ -539,7 +516,6 @@ function checkRules(node, msg, oNow, tempData) { } } } - */ } return result; @@ -658,9 +634,6 @@ function initializeCtrl(REDLib, node, config) { // Prepare Rules node.rules.count = node.rules.data.length; node.rules.lastUntil = node.rules.count -1; - node.rules.firstFrom = node.rules.lastUntil; - node.rules.lastAt = node.rules.count -1; - node.rules.atCount = 0; node.rules.firstTimeLimited = node.rules.count; node.rules.maxImportance = 0; node.rules.canResetOverwrite = false; @@ -714,43 +687,44 @@ function initializeCtrl(REDLib, node, config) { } if(rule.timeType) { if (!rule.time && rule.timeType !== 'none') { - rule.time = { + const operator = (parseInt(rule.timeOp) || cNBC_RULE_TYPE_UNTIL); + rule.time = { }; + let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL + if (operator === cNBC_RULE_TYPE_FROM) { + rule.time.start = {}; + ttype = 'start'; + } + rule.time[ttype] = { type : rule.timeType, value : (rule.timeValue || ''), - operator : (parseInt(rule.timeOp) || cRuleTime.until), offsetType : (rule.offsetType || 'none'), offset : (rule.offsetValue || 1), - multiplier : (parseInt(rule.multiplier) || 60000), - days : (rule.timeDays || '*'), - months : (rule.timeMonths || '*') + multiplier : (parseInt(rule.multiplier) || 60000) }; - if (rule.timeOnlyOddDays) rule.time.onlyOddDays = rule.timeOnlyOddDays; - if (rule.timeOnlyEvenDays) rule.time.onlyEvenDays = rule.timeOnlyEvenDays; - if (rule.timeDateStart) rule.time.dateStart = rule.timeDateStart; - if (rule.timeDateEnd) rule.time.dateEnd = rule.timeDateEnd; - if (rule.timeOpText) rule.time.operatorText = rule.timeOpText; - if (rule.timeMinType) { - if (!rule.timeMin && rule.timeMinType !== 'none') { - rule.timeMin = { - type : rule.timeMinType, - value : (rule.timeMinValue || ''), - offsetType : (rule.offsetMinType || 'none'), - offset : (rule.offsetMinValue || 1), - multiplier : (parseInt(rule.multiplierMin) || 60000) - }; - } + if (rule.timeMinType && rule.timeMinType !== 'none') { + rule.time[ttype].min = { + type : rule.timeMinType, + value : (rule.timeMinValue || ''), + offsetType : (rule.offsetMinType || 'none'), + offset : (rule.offsetMinValue || 1), + multiplier : (parseInt(rule.multiplierMin) || 60000) + }; } - if (rule.timeMaxType) { - if (!rule.timeMax && rule.timeMaxType !== 'none') { - rule.timeMax = { - type : rule.timeMaxType, - value : (rule.timeMaxValue || ''), - offsetType : (rule.offsetMaxType || 'none'), - offset : (rule.offsetMaxValue || 1), - multiplier : (parseInt(rule.multiplierMax) || 60000) - }; - } + if (rule.timeMaxType && rule.timeMaxType !== 'none') { + rule.time[ttype].max = { + type : rule.timeMaxType, + value : (rule.timeMaxValue || ''), + offsetType : (rule.offsetMaxType || 'none'), + offset : (rule.offsetMaxValue || 1), + multiplier : (parseInt(rule.multiplierMax) || 60000) + }; } + if (rule.timeDays && rule.timeDays !== '*') rule.time[ttype].days = rule.timeDays; + if (rule.timeMonths && rule.timeMonths !== '*') rule.time[ttype].months = rule.timeMonths; + if (rule.timeOnlyOddDays) rule.time[ttype].onlyOddDays = rule.timeOnlyOddDays; + if (rule.timeOnlyEvenDays) rule.time[ttype].onlyEvenDays = rule.timeOnlyEvenDays; + if (rule.timeDateStart) rule.time[ttype].dateStart = rule.timeDateStart; + if (rule.timeDateEnd) rule.time[ttype].dateEnd = rule.timeDateEnd; } delete rule.timeType; delete rule.timeValue; @@ -778,6 +752,56 @@ function initializeCtrl(REDLib, node, config) { delete rule.multiplierMax; delete rule.timeMaxOp; } + if (rule.time && rule.time.operator) { + let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL + if (rule.time.operator === cNBC_RULE_TYPE_FROM) { + ttype = 'start'; + } + rule.time[ttype] = { + type : rule.time.type, + value : rule.time.value, + offsetType : rule.time.offsetType, + offset : rule.time.offset, + multiplier : rule.time.multiplier + }; + if (rule.timeMin && rule.timeMin.type !== 'none' ) { + rule.time[ttype].min = { + type : rule.timeMin.type, + value : (rule.timeMin.value || ''), + offsetType : (rule.timeMin.offsetType || 'none'), + offset : (rule.timeMin.offset || 1), + multiplier : (parseInt(rule.timeMin.multiplier) || 60000) + }; + } + if (rule.timeMax && rule.timeMax.type !== 'none' ) { + rule.time[ttype].max = { + type : rule.timeMax.type, + value : (rule.timeMax.value || ''), + offsetType : (rule.timeMax.offsetType || 'none'), + offset : (rule.timeMax.offset || 1), + multiplier : (parseInt(rule.timeMax.multiplier) || 60000) + }; + } + if (rule.time.days && rule.time.days !== '*') rule.time[ttype].days = rule.time.days; + if (rule.time.months && rule.time.months !== '*') rule.time[ttype].months = rule.time.months; + if (rule.time.onlyOddDays) rule.time[ttype].onlyOddDays = rule.time.onlyOddDays; + if (rule.time.onlyEvenDays) rule.time[ttype].onlyEvenDays = rule.time.onlyEvenDays; + if (rule.time.dateStart) rule.time[ttype].dateStart = rule.time.dateStart; + if (rule.time.dateEnd) rule.time[ttype].dateEnd = rule.time.dateEnd; + delete rule.time.type; + delete rule.time.value; + delete rule.time.offsetType; + delete rule.time.offset; + delete rule.time.multiplier; + delete rule.time.days; + delete rule.time.months; + delete rule.time.onlyOddDays; + delete rule.time.onlyEvenDays; + delete rule.time.dateStart; + delete rule.time.dateEnd; + delete rule.timeMin; + delete rule.timeMax; + } if (rule.levelType) { if (!rule.level) { rule.level = { @@ -835,53 +859,61 @@ function initializeCtrl(REDLib, node, config) { } /// readout timesettings if (rule.time) { + const checkTimeR = id => { + if (rule.time[id].max) { rule.time[id].max.next = false; } + if (rule.time[id].min) { rule.time[id].min.next = false; } + if (!rule.time[id].days || rule.time[id].days === '*') { + delete rule.time[id].days; + } else { + rule.time[id].days = rule.time[id].days.split(','); + rule.time[id].days = rule.time[id].days.map( e => parseInt(e) ); + } + if (!rule.time[id].months || rule.time[id].months === '*') { + delete rule.time[id].months; + } else { + rule.time[id].months = rule.time[id].months.split(','); + rule.time[id].months = rule.time[id].months.map( e => parseInt(e) ); + } + if (rule.time[id].onlyOddDays && rule.time[id].onlyEvenDays) { + delete rule.time[id].onlyOddDays; + delete rule.time[id].onlyEvenDays; + } + if (rule.time[id].onlyOddWeeks && rule.time[id].onlyEvenWeeks) { + delete rule.time[id].onlyOddWeeks; + delete rule.time[id].onlyEvenWeeks; + } + if (rule.time[id].dateStart || rule.time[id].dateEnd) { + if (rule.time[id].dateStart) { + rule.time[id].dateStart = new Date(rule.time[id].dateStart); + rule.time[id].dateStart.setHours(0, 0, 0, 1); + } else { + rule.time[id].dateStart = new Date(2000,0,1,0, 0, 0, 1); + } + if (rule.time[id].dateEnd) { + rule.time[id].dateEnd = new Date(rule.time[id].dateEnd); + rule.time[id].dateEnd.setHours(23, 59, 59, 999); + } else { + rule.time[id].dateEnd = new Date(2000,11,31, 23, 59, 59, 999); + } + } + }; rule.time.next = false; - if (rule.timeMax) { rule.timeMax.next = false; } - if (rule.timeMin) { rule.timeMin.next = false; } node.rules.firstTimeLimited = Math.min(i, node.rules.firstTimeLimited); - if (rule.time.operator === cRuleTime.at) { - node.rules.atCount++; - node.rules.lastAt = i; + if (rule.time.start) { // cNBC_RULE_TYPE_FROM + checkTimeR('start'); } - if (rule.time.operator === cRuleTime.until) { + if (rule.time.end) { // cNBC_RULE_TYPE_UNTIL node.rules.lastUntil = i; + checkTimeR('end'); } - if (rule.time.operator === cRuleTime.from) { - node.rules.firstFrom = Math.min(i,node.rules.firstFrom); - } - if (!rule.time.days || rule.time.days === '*') { - delete rule.time.days; - } else { - rule.time.days = rule.time.days.split(','); - rule.time.days = rule.time.days.map( e => parseInt(e) ); - } - if (!rule.time.months || rule.time.months === '*') { - delete rule.time.months; - } else { - rule.time.months = rule.time.months.split(','); - rule.time.months = rule.time.months.map( e => parseInt(e) ); - } - if (rule.time.onlyOddDays && rule.time.onlyEvenDays) { - delete rule.time.onlyOddDays; - delete rule.time.onlyEvenDays; - } - if (rule.time.onlyOddWeeks && rule.time.onlyEvenWeeks) { - delete rule.time.onlyOddWeeks; - delete rule.time.onlyEvenWeeks; - } - if (rule.time.dateStart || rule.time.dateEnd) { - if (rule.time.dateStart) { - rule.time.dateStart = new Date(rule.time.dateStart); - rule.time.dateStart.setHours(0, 0, 0, 1); - } else { - rule.time.dateStart = new Date(2000,0,1,0, 0, 0, 1); - } - if (rule.time.dateEnd) { - rule.time.dateEnd = new Date(rule.time.dateEnd); - rule.time.dateEnd.setHours(23, 59, 59, 999); - } else { - rule.time.dateEnd = new Date(2000,11,31, 23, 59, 59, 999); - } + if (rule.time.start && rule.time.end) { + // if both are defined, only use limitations on start + delete rule.time.end.days; + delete rule.time.end.months; + delete rule.time.end.onlyOddDays; + delete rule.time.end.onlyEvenDays; + delete rule.time.end.dateStart; + delete rule.time.end.dateEnd; } } rule.conditions.forEach(cond => { From d71247d975908a8b2699d52994af1204684216db Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Thu, 11 Nov 2021 20:46:28 +0100 Subject: [PATCH 03/44] dev --- nodes/blind-control.html | 879 +++++++++++++++++------ nodes/locales/en-US/blind-control.json | 3 +- nodes/locales/en-US/position-config.json | 2 + 3 files changed, 674 insertions(+), 210 deletions(-) diff --git a/nodes/blind-control.html b/nodes/blind-control.html index 90a9fd9..fa56360 100644 --- a/nodes/blind-control.html +++ b/nodes/blind-control.html @@ -9,7 +9,7 @@ const cNBC_RULE_TYPE_UNTIL = 0; const cNBC_RULE_TYPE_FROM = 1; const cNBC_RULE_OP = { - absolute : 0, + levelAbsolute : 0, levelMinOversteer : 1, // ⭳❗ minimum (oversteer) levelMaxOversteer : 2, // ⭱️❗ maximum (oversteer) slatOversteer : 5, @@ -609,6 +609,24 @@ label: node._('node-red-contrib-sun-position/position-config:common.types.ctrlObj'), hasValue: false }; + types.OutputCtrlObj = { + value: 'ctrlObj', + label: node._('node-red-contrib-sun-position/position-config:common.types.ctrlObj'), + hasValue: false + }; + types.OutputCtrlObj = { + value: 'ctrlObj', + label: node._('node-red-contrib-sun-position/position-config:common.types.ctrlObj'), + hasValue: false + }; + + types.UndefinedTimeFrom = Object.assign({}, types.Undefined, { + label: node._('node-red-contrib-sun-position/position-config:common.types.undefinedTimeFrom') + }); + types.UndefinedTimeUntil = Object.assign({}, types.Undefined, { + label: node._('node-red-contrib-sun-position/position-config:common.types.undefinedTimeUntil') + }); + const selFields = getSelectFields(); /** * save rules @@ -1091,7 +1109,7 @@ } result += '
'; if (data.level) { - if (data.level.operator <= cNBC_RULE_OP.absolute || data.levelType === types.LevelND.value) { + if (data.level.operator <= cNBC_RULE_OP.levelAbsolute || data.levelType === types.LevelND.value) { if (typeof data.level.type !== 'undefined' && data.level.type !== types.LevelND.value) { result += '
'; result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleBlindLevel'); @@ -1464,6 +1482,8 @@ data.index = i; // data.conditional = ((data.conditions.length > 0) ? 1 : 0); // data.timeLimited = ((data.time && data.time.type !== 'none') ? 1 : 0); + // TODO: node-input-rule-timeReg does not exists + // WARNING: node-input-rule-timeReg does not exists data.timeData = (parseFloat($(this).find('.node-input-rule-timeReg').attr('timedata')) || 0); }); /** @@ -2529,16 +2549,156 @@ }); // condition-container $dialog.find('#dlg-ip-btctl-rule-condition-text').html(node._('node-red-contrib-sun-position/position-config:ruleCtrl.text.ruleCondition')); // #endregion dialogConditions - // #region dialogTimeReg - const $dialogTimeRegOperator = $dialog.find('#dlg-ip-btctl-rule-timeReg-operator'); - $dialogTimeRegOperator.append($('').val(cNBC_RULE_TYPE_UNTIL).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeUntil'))); - $dialogTimeRegOperator.append($('').val(cNBC_RULE_TYPE_FROM).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeFrom'))); + // #region dialogTimeReg Start + const $dialogTimeStartRegInput = $dialog.find('#dlg-ip-btctl-rule-timeStartReg'); + $dialogTimeStartRegInput.typedInput({ + default: types.Undefined.value, + types: [ + types.UndefinedTimeFrom, + types.TimeEntered, + types.TimeSun, + types.TimeMoon, + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.SunTimeByAzimuth, + types.SunTimeByAzimuthRad + ] + }); + $dialogTimeStartRegInput.typedInput('width', '40%'); + + const $dialogTimeStartRegMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeStartReg'); + appendOptions(node, $dialogTimeStartRegMultiplier, 'multiplier', data => (data.id > 0)); + + const $dialogTimeStartRegOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeStartReg'); + $dialogTimeStartRegOffset.typedInput({ + default: types.Undefined.value, + types: [ + types.Undefined, + 'num', + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.randmNumCachedDay, + types.randmNumCachedWeek + ] + }).change(() => { + const type = $dialogTimeStartRegOffset.typedInput('type'); + if (type === types.Undefined.value) { + $dialogTimeStartRegMultiplier.prop('disabled', true); + } else { + $dialogTimeStartRegMultiplier.prop('disabled', false); + } + $dialogTimeStartRegInput.change(); + }); + $dialogTimeStartRegOffset.typedInput('width', '40%'); + // #endregion dialogTimeReg Start + // #region dialogTimeMin Start + const $dialogTimeStartMinInput = $dialog.find('#dlg-ip-btctl-rule-timeStartMin'); + $dialogTimeStartMinInput.typedInput({ + default: types.Undefined.value, + types: [ + types.Undefined, + types.TimeEntered, + types.TimeSun, + types.TimeMoon, + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.SunTimeByAzimuth, + types.SunTimeByAzimuthRad + ] + }); + $dialogTimeStartMinInput.typedInput('width', '40%'); + + const $dialogTimeStartMinMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeStartMin'); + appendOptions(node, $dialogTimeStartMinMultiplier, 'multiplier', data => (data.id > 0)); + + const $dialogTimeStartMinOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeStartMin'); + $dialogTimeStartMinOffset.typedInput({ + default: types.Undefined.value, + types: [ + types.Undefined, + 'num', + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.randmNumCachedDay, + types.randmNumCachedWeek + ] + }).change(() => { + const type = $dialogTimeStartMinOffset.typedInput('type'); + if (type === types.Undefined.value) { + $dialogTimeStartMinMultiplier.prop('disabled', true); + } else { + $dialogTimeStartMinMultiplier.prop('disabled', false); + } + $dialogTimeStartMinInput.change(); + }); + $dialogTimeStartMinOffset.typedInput('width', '40%'); + // #endregion dialogTimeMin Start + // #region dialogTimeMax Start + const $dialogTimeStartMaxInput = $dialog.find('#dlg-ip-btctl-rule-timeStartMax'); + $dialogTimeStartMaxInput.typedInput({ + default: types.Undefined.value, + types: [ + types.Undefined, + types.TimeEntered, + types.TimeSun, + types.TimeMoon, + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.SunTimeByAzimuth, + types.SunTimeByAzimuthRad + ] + }); + $dialogTimeStartMaxInput.typedInput('width', '40%'); - const $dialogTimeRegInput = $dialog.find('#dlg-ip-btctl-rule-timeReg'); - $dialogTimeRegInput.typedInput({ + const $dialogTimeStartMaxMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeStartMax'); + appendOptions(node, $dialogTimeStartMaxMultiplier, 'multiplier', data => (data.id > 0)); + + const $dialogTimeStartMaxOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeStartMax'); + $dialogTimeStartMaxOffset.typedInput({ default: types.Undefined.value, types: [ types.Undefined, + 'num', + 'msg', + 'flow', + 'global', + 'env', + 'jsonata', + types.randmNumCachedDay, + types.randmNumCachedWeek + ] + }).change(() => { + const type = $dialogTimeStartMaxOffset.typedInput('type'); + if (type === types.Undefined.value) { + $dialogTimeStartMaxMultiplier.prop('disabled', true); + } else { + $dialogTimeStartMaxMultiplier.prop('disabled', false); + } + $dialogTimeStartMaxInput.change(); + }); + // #endregion dialogTimeMax Start + + // #region dialogTimeReg End + const $dialogTimeEndRegInput = $dialog.find('#dlg-ip-btctl-rule-timeEndReg'); + $dialogTimeEndRegInput.typedInput({ + default: types.Undefined.value, + types: [ + types.UndefinedTimeUntil, types.TimeEntered, types.TimeSun, types.TimeMoon, @@ -2551,13 +2711,13 @@ types.SunTimeByAzimuthRad ] }); - $dialogTimeRegInput.typedInput('width', '40%'); + $dialogTimeEndRegInput.typedInput('width', '40%'); - const $dialogTimeRegMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeReg'); - appendOptions(node, $dialogTimeRegMultiplier, 'multiplier', data => (data.id > 0)); + const $dialogTimeEndRegMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeEndReg'); + appendOptions(node, $dialogTimeEndRegMultiplier, 'multiplier', data => (data.id > 0)); - const $dialogTimeRegOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeReg'); - $dialogTimeRegOffset.typedInput({ + const $dialogTimeEndRegOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeEndReg'); + $dialogTimeEndRegOffset.typedInput({ default: types.Undefined.value, types: [ types.Undefined, @@ -2571,19 +2731,19 @@ types.randmNumCachedWeek ] }).change(() => { - const type = $dialogTimeRegOffset.typedInput('type'); + const type = $dialogTimeEndRegOffset.typedInput('type'); if (type === types.Undefined.value) { - $dialogTimeRegMultiplier.prop('disabled', true); + $dialogTimeEndRegMultiplier.prop('disabled', true); } else { - $dialogTimeRegMultiplier.prop('disabled', false); + $dialogTimeEndRegMultiplier.prop('disabled', false); } - $dialogTimeRegInput.change(); + $dialogTimeEndRegInput.change(); }); - $dialogTimeRegOffset.typedInput('width', '40%'); - // #endregion dialogTimeReg - // #region dialogTimeMin - const $dialogTimeMinInput = $dialog.find('#dlg-ip-btctl-rule-timeMin'); - $dialogTimeMinInput.typedInput({ + $dialogTimeEndRegOffset.typedInput('width', '40%'); + // #endregion dialogTimeReg End + // #region dialogTimeMin End + const $dialogTimeEndMinInput = $dialog.find('#dlg-ip-btctl-rule-timeEndMin'); + $dialogTimeEndMinInput.typedInput({ default: types.Undefined.value, types: [ types.Undefined, @@ -2599,13 +2759,13 @@ types.SunTimeByAzimuthRad ] }); - $dialogTimeMinInput.typedInput('width', '40%'); + $dialogTimeEndMinInput.typedInput('width', '40%'); - const $dialogTimeMinMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeMin'); - appendOptions(node, $dialogTimeMinMultiplier, 'multiplier', data => (data.id > 0)); + const $dialogTimeEndMinMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeEndMin'); + appendOptions(node, $dialogTimeEndMinMultiplier, 'multiplier', data => (data.id > 0)); - const $dialogTimeMinOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeMin'); - $dialogTimeMinOffset.typedInput({ + const $dialogTimeEndMinOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeEndMin'); + $dialogTimeEndMinOffset.typedInput({ default: types.Undefined.value, types: [ types.Undefined, @@ -2619,19 +2779,19 @@ types.randmNumCachedWeek ] }).change(() => { - const type = $dialogTimeMinOffset.typedInput('type'); + const type = $dialogTimeEndMinOffset.typedInput('type'); if (type === types.Undefined.value) { - $dialogTimeMinMultiplier.prop('disabled', true); + $dialogTimeEndMinMultiplier.prop('disabled', true); } else { - $dialogTimeMinMultiplier.prop('disabled', false); + $dialogTimeEndMinMultiplier.prop('disabled', false); } - $dialogTimeMinInput.change(); + $dialogTimeEndMinInput.change(); }); - $dialogTimeMinOffset.typedInput('width', '40%'); - // #endregion dialogTimeMin - // #region dialogTimeMax - const $dialogTimeMaxInput = $dialog.find('#dlg-ip-btctl-rule-timeMax'); - $dialogTimeMaxInput.typedInput({ + $dialogTimeEndMinOffset.typedInput('width', '40%'); + // #endregion dialogTimeMin End + // #region dialogTimeMax End + const $dialogTimeEndMaxInput = $dialog.find('#dlg-ip-btctl-rule-timeEndMax'); + $dialogTimeEndMaxInput.typedInput({ default: types.Undefined.value, types: [ types.Undefined, @@ -2647,13 +2807,13 @@ types.SunTimeByAzimuthRad ] }); - $dialogTimeMaxInput.typedInput('width', '40%'); + $dialogTimeEndMaxInput.typedInput('width', '40%'); - const $dialogTimeMaxMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeMax'); - appendOptions(node, $dialogTimeMaxMultiplier, 'multiplier', data => (data.id > 0)); + const $dialogTimeEndMaxMultiplier = $dialog.find('#dlg-ip-btctl-rule-multiplier-timeEndMax'); + appendOptions(node, $dialogTimeEndMaxMultiplier, 'multiplier', data => (data.id > 0)); - const $dialogTimeMaxOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeMax'); - $dialogTimeMaxOffset.typedInput({ + const $dialogTimeEndMaxOffset = $dialog.find('#dlg-ip-btctl-rule-offset-timeEndMax'); + $dialogTimeEndMaxOffset.typedInput({ default: types.Undefined.value, types: [ types.Undefined, @@ -2667,16 +2827,16 @@ types.randmNumCachedWeek ] }).change(() => { - const type = $dialogTimeMaxOffset.typedInput('type'); + const type = $dialogTimeEndMaxOffset.typedInput('type'); if (type === types.Undefined.value) { - $dialogTimeMaxMultiplier.prop('disabled', true); + $dialogTimeEndMaxMultiplier.prop('disabled', true); } else { - $dialogTimeMaxMultiplier.prop('disabled', false); + $dialogTimeEndMaxMultiplier.prop('disabled', false); } - $dialogTimeMaxInput.change(); + $dialogTimeEndMaxInput.change(); }); - $dialogTimeMaxOffset.typedInput('width', '40%'); - // #endregion dialogTimeMax + // #endregion dialogTimeMax End + // #region dialogTimeLimit $dialog.find('#dialog-row-timeConstraints-show').click(() => { $dialog.find('#dialog-row-timeConstraintsPlaceholder').hide(); @@ -2736,7 +2896,7 @@ }).change(() => { $dialogLevelOperator.change(); }); $dialogLevel.typedInput('width', '40%'); const $dialogLevelOperator = $dialog.find('#dlg-ip-btctl-rule-level-operator'); - $dialogLevelOperator.append($('').val(cNBC_RULE_OP.absolute ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelAbs'))); + $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelAbsolute ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelAbs'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelMinOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelMin'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.levelMaxOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleLevelMax'))); $dialogLevelOperator.append($('').val(cNBC_RULE_OP.slatOversteer ).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleSlatOversteer'))); @@ -2831,43 +2991,240 @@ $dialogTopic.change(() => { _dialogGenHelpText(); }); // #endregion topic // #region dialogChange - $dialogTimeRegInput.change(() => { + $dialogTimeStartRegInput.change(() => { if (dialogDataOnLoading) { return; } - const opType = $dialogTimeRegInput.typedInput('type'); + const opType = $dialogTimeStartRegInput.typedInput('type'); if (opType === types.Undefined.value) { node.dialogAddData.timeReg = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeReg').attr('title', node.dialogAddData.timeReg.text); - $dialogTimeRegInput.attr('timedata', node.dialogAddData.timeReg.ts); - $dialogTimeRegOperator.hide(); - $('#dialog-row-offset-timeReg').hide(); - $('#dialog-row-timeReg').attr('title', opType); + $('#dialog-row-timeStartReg').attr('title', node.dialogAddData.timeReg.text); + $dialogTimeStartRegInput.attr('timedata', node.dialogAddData.timeReg.ts); + $('#dialog-row-offset-timeStartReg').hide(); + $('#dialog-row-timeStartReg').attr('title', opType); - $('#dialog-row-timeMin').hide(); - $('#dialog-row-timeMax').hide(); - $('#dialog-row-offset-timeMin').hide(); - $('#dialog-row-offset-timeMax').hide(); + $('#dialog-row-timeStartMin').hide(); + $('#dialog-row-timeStartMax').hide(); + $('#dialog-row-offset-timeStartMin').hide(); + $('#dialog-row-offset-timeStartMax').hide(); + $('.dialog-row-timeStartLimits').hide(); + $('.dialog-row-timeStartLimits-ph').hide(); + if ($dialogTimeStartMaxInput.typedInput('type') !== types.Undefined.value) { + $dialogTimeStartMaxInput.typedInput('type', types.Undefined.value); + } + if ($dialogTimeStartMaxInput.typedInput('type') !== types.Undefined.value) { + $dialogTimeStartMaxInput.typedInput('type', types.Undefined.value); + } + _dialogGenHelpText(); + return; + } + if (opType === types.TimeEntered.value || + opType === types.TimeSun.value || + opType === types.TimeMoon.value) { + $('#dialog-row-offset-timeStartReg').show(); + } else { + $('#dialog-row-offset-timeStartReg').hide(); + } + + const noLimit = getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'), 7) === '*' && + getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) === '*' && + $dialogTimeLimitOnlyEvenDays.is(':checked') !== true && + $dialogTimeLimitOnlyOddDays.is(':checked') !== true && + $dialogTimeLimitOnlyEvenWeeks.is(':checked') !== true && + $dialogTimeLimitOnlyOddWeeks.is(':checked') !== true && + $dialogTimeLimitDateStart.val() === '' && + $dialogTimeLimitDateEnd.val() === ''; + if (noLimit) { $('.dialog-row-timeLimits').hide(); + $('.dialog-row-timeLimits-ph').show(); + } else { + $('.dialog-row-timeLimits').show(); $('.dialog-row-timeLimits-ph').hide(); - if ($dialogTimeMaxInput.typedInput('type') !== types.Undefined.value) { - $dialogTimeMaxInput.typedInput('type', types.Undefined.value); + } + getBackendData(d => { + if (d.value && d.value !== 'none') { + const dv = new Date(d.value); + node.dialogAddData.timeReg = { + ts : dv.getTime(), + data : d, + text : bdDateToTime(dv) + }; + } else { + node.dialogAddData.timeReg = { + ts : 0, + data : null, + text : bdDateToTime(d) + }; + } + $('#dialog-row-timeStartReg').attr('title', node.dialogAddData.timeReg.text); + $dialogTimeStartRegInput.attr('timedata', node.dialogAddData.timeReg.ts); + _dialogGenHelpText(); + }, { + nodeId: node.id, + kind: 'getTimeData', + config: $nodeConfig.val(), + type: opType, + value: $dialogTimeStartRegInput.typedInput('value'), + offsetType: $dialogTimeStartRegOffset.typedInput('type'), + offset: $dialogTimeStartRegOffset.typedInput('value'), + multiplier: parseInt($dialogTimeStartRegMultiplier.val()), + noOffsetError: true + }); + $dialogTimeStartMinInput.change(); + $dialogTimeStartMaxInput.change(); + }); + + $dialogTimeStartMinInput.change(() => { + if (dialogDataOnLoading) { return; } + const opAType = $dialogTimeStartRegInput.typedInput('type'); + if (opAType && opAType !== types.Undefined.value) { + $('#dialog-row-timeStartMin').show(); + const opType = $dialogTimeStartMinInput.typedInput('type'); + if (opType === types.Undefined.value) { + $('#dialog-row-offset-timeStartMin').hide(); + node.dialogAddData.timeMin = { + ts : 0, + data : null, + text : 'N/A' + }; + $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.timeMin.text); + $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + _dialogGenHelpText(); + return; + } else if (opType === types.TimeEntered.value || + opType === types.TimeSun.value || + opType === types.TimeMoon.value) { + $('#dialog-row-offset-timeStartMin').show(); + } else { + $('#dialog-row-offset-timeStartMin').hide(); + } + getBackendData(d => { + if (d.value && d.value !== 'none') { + const dv = new Date(d.value); + node.dialogAddData.timeMin = { + ts : dv.getTime(), + data : d, + text : bdDateToTime(dv) + }; + } else { + node.dialogAddData.timeMin = { + ts : 0, + data : null, + text : bdDateToTime(d) + }; + } + $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.timeMin.text); + $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + _dialogGenHelpText(); + }, { + nodeId: node.id, + kind: 'getTimeData', + config: $nodeConfig.val(), + type: opType, + value: $dialogTimeStartMinInput.typedInput('value'), + offsetType: $dialogTimeStartMinOffset.typedInput('type'), + offset: $dialogTimeStartMinOffset.typedInput('value'), + multiplier: parseInt($dialogTimeStartMinMultiplier.val()), + noOffsetError: true + }); + } + _dialogGenHelpText(); + }); + + $dialogTimeStartMaxInput.change(() => { + if (dialogDataOnLoading) { return; } + const opAType = $dialogTimeStartRegInput.typedInput('type'); + if (opAType && opAType !== types.Undefined.value) { + $('#dialog-row-timeStartMax').show(); + const opType = $dialogTimeStartMaxInput.typedInput('type'); + if (opType === types.Undefined.value) { + $('#dialog-row-offset-timeStartMax').hide(); + node.dialogAddData.timeMax = { + ts : 0, + data : null, + text : 'N/A' + }; + $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.timeMax.text); + $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + _dialogGenHelpText(); + return; + } else if (opType === types.TimeEntered.value || + opType === types.TimeSun.value || + opType === types.TimeMoon.value) { + $('#dialog-row-offset-timeStartMax').show(); + } else { + $('#dialog-row-offset-timeStartMax').hide(); + } + getBackendData(d => { + if (d.value && d.value !== 'none') { + const dv = new Date(d.value); + node.dialogAddData.timeMax = { + ts : dv.getTime(), + data : d, + text : bdDateToTime(dv) + }; + } else { + node.dialogAddData.timeMax = { + ts : 0, + data : null, + text : bdDateToTime(d) + }; + } + $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.timeMax.text); + $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + _dialogGenHelpText(); + }, { + nodeId: node.id, + kind: 'getTimeData', + config: $nodeConfig.val(), + type: opType, + value: $dialogTimeStartMaxInput.typedInput('value'), + offsetType: $dialogTimeStartMaxOffset.typedInput('type'), + offset: $dialogTimeStartMaxOffset.typedInput('value'), + multiplier: parseInt($dialogTimeStartMaxMultiplier.val()), + noOffsetError: true + }); + } + _dialogGenHelpText(); + }); + + $dialogTimeEndRegInput.change(() => { + if (dialogDataOnLoading) { return; } + const opType = $dialogTimeEndRegInput.typedInput('type'); + if (opType === types.Undefined.value) { + node.dialogAddData.timeReg = { + ts : 0, + data : null, + text : 'N/A' + }; + $('#dialog-row-timeEndReg').attr('title', node.dialogAddData.timeReg.text); + $dialogTimeEndRegInput.attr('timedata', node.dialogAddData.timeReg.ts); + $('#dialog-row-timeEndReg-offset').hide(); + $('#dialog-row-timeEndReg').attr('title', opType); + + $('#dialog-row-timeEndMin').hide(); + $('#dialog-row-timeEndMax').hide(); + $('#dialog-row-timeEndMin-offset').hide(); + $('#dialog-row-timeEndMax-offset').hide(); + $('.dialog-row-timeEndLimits').hide(); + $('.dialog-row-timeEndLimits-ph').hide(); + if ($dialogTimeEndMaxInput.typedInput('type') !== types.Undefined.value) { + $dialogTimeEndMaxInput.typedInput('type', types.Undefined.value); } - if ($dialogTimeMaxInput.typedInput('type') !== types.Undefined.value) { - $dialogTimeMaxInput.typedInput('type', types.Undefined.value); + if ($dialogTimeEndMaxInput.typedInput('type') !== types.Undefined.value) { + $dialogTimeEndMaxInput.typedInput('type', types.Undefined.value); } _dialogGenHelpText(); return; } - $dialogTimeRegOperator.show(); if (opType === types.TimeEntered.value || opType === types.TimeSun.value || opType === types.TimeMoon.value) { - $('#dialog-row-offset-timeReg').show(); + $('#dialog-row-timeEndReg-offset').show(); } else { - $('#dialog-row-offset-timeReg').hide(); + $('#dialog-row-timeEndReg-offset').hide(); } const noLimit = getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'), 7) === '*' && @@ -2900,47 +3257,47 @@ text : bdDateToTime(d) }; } - $('#dialog-row-timeReg').attr('title', node.dialogAddData.timeReg.text); - $dialogTimeRegInput.attr('timedata', node.dialogAddData.timeReg.ts); + $('#dialog-row-timeEndReg').attr('title', node.dialogAddData.timeReg.text); + $dialogTimeEndRegInput.attr('timedata', node.dialogAddData.timeReg.ts); _dialogGenHelpText(); }, { nodeId: node.id, kind: 'getTimeData', config: $nodeConfig.val(), type: opType, - value: $dialogTimeRegInput.typedInput('value'), - offsetType: $dialogTimeRegOffset.typedInput('type'), - offset: $dialogTimeRegOffset.typedInput('value'), - multiplier: parseInt($dialogTimeRegMultiplier.val()), + value: $dialogTimeEndRegInput.typedInput('value'), + offsetType: $dialogTimeEndRegOffset.typedInput('type'), + offset: $dialogTimeEndRegOffset.typedInput('value'), + multiplier: parseInt($dialogTimeEndRegMultiplier.val()), noOffsetError: true }); - $dialogTimeMinInput.change(); - $dialogTimeMaxInput.change(); + $dialogTimeEndMinInput.change(); + $dialogTimeEndMaxInput.change(); }); - $dialogTimeMinInput.change(() => { + $dialogTimeEndMinInput.change(() => { if (dialogDataOnLoading) { return; } - const opAType = $dialogTimeRegInput.typedInput('type'); + const opAType = $dialogTimeEndRegInput.typedInput('type'); if (opAType && opAType !== types.Undefined.value) { - $('#dialog-row-timeMin').show(); - const opType = $dialogTimeMinInput.typedInput('type'); + $('#dialog-row-timeEndMin').show(); + const opType = $dialogTimeEndMinInput.typedInput('type'); if (opType === types.Undefined.value) { - $('#dialog-row-offset-timeMin').hide(); + $('#dialog-row-timeEndMin-offset').hide(); node.dialogAddData.timeMin = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.timeMin.text); + $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.timeMin.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || opType === types.TimeSun.value || opType === types.TimeMoon.value) { - $('#dialog-row-offset-timeMin').show(); + $('#dialog-row-timeEndMin-offset').show(); } else { - $('#dialog-row-offset-timeMin').hide(); + $('#dialog-row-timeEndMin-offset').hide(); } getBackendData(d => { if (d.value && d.value !== 'none') { @@ -2957,47 +3314,47 @@ text : bdDateToTime(d) }; } - $('#dialog-row-timeMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.timeMin.text); + $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.timeMin.ts); _dialogGenHelpText(); }, { nodeId: node.id, kind: 'getTimeData', config: $nodeConfig.val(), type: opType, - value: $dialogTimeMinInput.typedInput('value'), - offsetType: $dialogTimeMinOffset.typedInput('type'), - offset: $dialogTimeMinOffset.typedInput('value'), - multiplier: parseInt($dialogTimeMinMultiplier.val()), + value: $dialogTimeEndMinInput.typedInput('value'), + offsetType: $dialogTimeEndMinOffset.typedInput('type'), + offset: $dialogTimeEndMinOffset.typedInput('value'), + multiplier: parseInt($dialogTimeEndMinMultiplier.val()), noOffsetError: true }); } _dialogGenHelpText(); }); - $dialogTimeMaxInput.change(() => { + $dialogTimeEndMaxInput.change(() => { if (dialogDataOnLoading) { return; } - const opAType = $dialogTimeRegInput.typedInput('type'); + const opAType = $dialogTimeEndRegInput.typedInput('type'); if (opAType && opAType !== types.Undefined.value) { - $('#dialog-row-timeMax').show(); - const opType = $dialogTimeMaxInput.typedInput('type'); + $('#dialog-row-timeEndMax').show(); + const opType = $dialogTimeEndMaxInput.typedInput('type'); if (opType === types.Undefined.value) { - $('#dialog-row-offset-timeMax').hide(); + $('#dialog-row-timeEndMax-offset').hide(); node.dialogAddData.timeMax = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.timeMax.text); + $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || opType === types.TimeSun.value || opType === types.TimeMoon.value) { - $('#dialog-row-offset-timeMax').show(); + $('#dialog-row-timeEndMax-offset').show(); } else { - $('#dialog-row-offset-timeMax').hide(); + $('#dialog-row-timeEndMax-offset').hide(); } getBackendData(d => { if (d.value && d.value !== 'none') { @@ -3014,28 +3371,30 @@ text : bdDateToTime(d) }; } - $('#dialog-row-timeMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.timeMax.text); + $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); _dialogGenHelpText(); }, { nodeId: node.id, kind: 'getTimeData', config: $nodeConfig.val(), type: opType, - value: $dialogTimeMaxInput.typedInput('value'), - offsetType: $dialogTimeMaxOffset.typedInput('type'), - offset: $dialogTimeMaxOffset.typedInput('value'), - multiplier: parseInt($dialogTimeMaxMultiplier.val()), + value: $dialogTimeEndMaxInput.typedInput('value'), + offsetType: $dialogTimeEndMaxOffset.typedInput('type'), + offset: $dialogTimeEndMaxOffset.typedInput('value'), + multiplier: parseInt($dialogTimeEndMaxMultiplier.val()), noOffsetError: true }); } _dialogGenHelpText(); }); + $('.dialog-row-timeLimits :input').change(() => { if (node.dialogAddData.timeout2) { clearTimeout(node.dialogAddData.timeout2); } node.dialogAddData.timeout2 = setTimeout(() => { delete node.dialogAddData.timeout2; - $dialogTimeRegInput.change(); + $dialogTimeStartRegInput.change(); + $dialogTimeEndRegInput.change(); },1000); }); // #endregion dialogChange @@ -3067,11 +3426,69 @@ for (let i = 0; i < data.conditions.length; i++) { $dialogConditionContainer.editableList('addItem', data.conditions[i]); } + $dialogTimeStartRegMultiplier.val(60000); + $dialogTimeEndRegMultiplier.val(60000); + setTInputValue($dialogTimeStartRegInput, '', types.Undefined.value); + setTInputValue($dialogTimeStartRegOffset, 1, types.Undefined.value); + setTInputValue($dialogTimeEndRegInput, '', types.Undefined.value); + setTInputValue($dialogTimeEndRegOffset, 1, types.Undefined.value); + initCheckboxesBlock('#dlg-ip-btctl-rule-timeDays', '*'); + initCheckboxesBlock('#dlg-ip-btctl-rule-timeMonths', '*'); + $dialogTimeLimitOnlyEvenDays.prop('checked', false); + $dialogTimeLimitOnlyOddDays.prop('checked', false); + $dialogTimeLimitOnlyEvenWeeks.prop('checked', false); + $dialogTimeLimitOnlyOddWeeks.prop('checked', false); + $dialogTimeLimitDateStart.val(''); + $dialogTimeLimitDateEnd.val(''); + + setTInputValue($dialogTimeEndMinInput, '', types.Undefined.value); + setTInputValue($dialogTimeEndMinOffset, 1, types.Undefined.value); + $dialogTimeEndMinMultiplier.val(60000); + setTInputValue($dialogTimeEndMaxInput, '', types.Undefined.value); + setTInputValue($dialogTimeEndMaxOffset, 1, types.Undefined.value); + $dialogTimeEndMaxMultiplier.val(60000); + + setTInputValue($dialogTimeStartMinInput, '', types.Undefined.value); + setTInputValue($dialogTimeStartMinOffset, 1, types.Undefined.value); + $dialogTimeStartMinMultiplier.val(60000); + setTInputValue($dialogTimeStartMaxInput, '', types.Undefined.value); + setTInputValue($dialogTimeStartMaxOffset, 1, types.Undefined.value); + $dialogTimeStartMaxMultiplier.val(60000); + if (data.time) { - $dialogTimeRegOperator.val(data.time.operator); - $dialogTimeRegMultiplier.val(data.time.multiplier); - setTInputValue($dialogTimeRegInput, data.time.value, data.time.type); - setTInputValue($dialogTimeRegOffset, data.time.offset, data.time.offsetType); + if (data.time.start) { + $dialogTimeStartRegMultiplier.val(data.time.multiplier); + setTInputValue($dialogTimeStartRegInput, data.time.value, data.time.type); + setTInputValue($dialogTimeStartRegOffset, data.time.offset, data.time.offsetType); + + if (data.time.start.min) { + $dialogTimeStartMinMultiplier.val(data.timeMin.multiplier || 60000); + setTInputValue($dialogTimeStartMinInput, data.timeMin.value, data.timeMin.type); + setTInputValue($dialogTimeStartMinOffset, data.timeMin.offset, data.timeMin.offsetType); + } + if (data.time.start.max) { + $dialogTimeStartMaxMultiplier.val(data.timeMax.multiplier || 60000); + setTInputValue($dialogTimeStartMaxInput, data.timeMax.value, data.timeMax.type); + setTInputValue($dialogTimeStartMaxOffset, data.timeMax.offset, data.timeMax.offsetType); + } + } + if (data.time.end) { + $dialogTimeEndRegMultiplier.val(data.time.multiplier); + setTInputValue($dialogTimeEndRegInput, data.time.value, data.time.type); + setTInputValue($dialogTimeEndRegOffset, data.time.offset, data.time.offsetType); + + if (data.time.end.min) { + $dialogTimeEndMinMultiplier.val(data.timeMin.multiplier || 60000); + setTInputValue($dialogTimeEndMinInput, data.timeMin.value, data.timeMin.type); + setTInputValue($dialogTimeEndMinOffset, data.timeMin.offset, data.timeMin.offsetType); + } + if (data.time.end.max) { + $dialogTimeEndMaxMultiplier.val(data.timeMax.multiplier || 60000); + setTInputValue($dialogTimeEndMaxInput, data.timeMax.value, data.timeMax.type); + setTInputValue($dialogTimeEndMaxOffset, data.timeMax.offset, data.timeMax.offsetType); + } + } + initCheckboxesBlock('#dlg-ip-btctl-rule-timeDays', data.time.days); initCheckboxesBlock('#dlg-ip-btctl-rule-timeMonths', data.time.months); if (!!data.time.onlyEvenDays && !!data.time.onlyOddDays) { @@ -3116,37 +3533,6 @@ } $dialogTimeLimitDateStart.change(); $dialogTimeLimitDateEnd.change(); - } else { - $dialogTimeRegOperator.val(0); - $dialogTimeRegMultiplier.val(60000); - setTInputValue($dialogTimeRegInput, '', types.Undefined.value); - setTInputValue($dialogTimeRegOffset, 1, types.Undefined.value); - initCheckboxesBlock('#dlg-ip-btctl-rule-timeDays', '*'); - initCheckboxesBlock('#dlg-ip-btctl-rule-timeMonths', '*'); - $dialogTimeLimitOnlyEvenDays.prop('checked', false); - $dialogTimeLimitOnlyOddDays.prop('checked', false); - $dialogTimeLimitOnlyEvenWeeks.prop('checked', false); - $dialogTimeLimitOnlyOddWeeks.prop('checked', false); - $dialogTimeLimitDateStart.val(''); - $dialogTimeLimitDateEnd.val(''); - } - if (data.timeMin) { - $dialogTimeMinMultiplier.val(data.timeMin.multiplier || 60000); - setTInputValue($dialogTimeMinInput, data.timeMin.value, data.timeMin.type); - setTInputValue($dialogTimeMinOffset, data.timeMin.offset, data.timeMin.offsetType); - } else { - setTInputValue($dialogTimeMinInput, '', types.Undefined.value); - setTInputValue($dialogTimeMinOffset, 1, types.Undefined.value); - $dialogTimeMinMultiplier.val(60000); - } - if (data.timeMax) { - $dialogTimeMaxMultiplier.val(data.timeMax.multiplier || 60000); - setTInputValue($dialogTimeMaxInput, data.timeMax.value, data.timeMax.type); - setTInputValue($dialogTimeMaxOffset, data.timeMax.offset, data.timeMax.offsetType); - } else { - setTInputValue($dialogTimeMaxInput, '', types.Undefined.value); - setTInputValue($dialogTimeMaxOffset, 1, types.Undefined.value); - $dialogTimeMaxMultiplier.val(60000); } if (data.level) { $dialogLevelOperator.val(data.level.operator || 0); @@ -3166,7 +3552,8 @@ _dialogGenHelpText(); dialogDataOnLoading = false; - $dialogTimeRegInput.change(); + $dialogTimeStartRegInput.change(); + $dialogTimeEndRegInput.change(); $dialogLevelOperator.change(); } catch (err) { console.log(err.message); // eslint-disable-line no-console @@ -3261,20 +3648,94 @@ result.valid.conditions.push(isCValid); result.isValid = result.isValid && isCValid.value && isCValid.threshold; }); - const timeType = $dialogTimeRegInput.typedInput('type'); - result.valid.time = $dialogTimeRegInput.typedInput('validate'); - if (timeType !== 'none') { - result.time = { - type : timeType, - value : $dialogTimeRegInput.typedInput('value'), - operator : parseInt($dialogTimeRegOperator.val()), - operatorText : $dialogTimeRegOperator.find('option:selected').text(), - offsetType : $dialogTimeRegOffset.typedInput('type'), - offset : $dialogTimeRegOffset.typedInput('value'), - multiplier : parseInt($dialogTimeRegMultiplier.val()), - days : getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7), - months : getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) - }; + const timeTypeStart = $dialogTimeStartRegInput.typedInput('type'); + const timeTypeEnd = $dialogTimeEndRegInput.typedInput('type'); + result.valid.timeStart = $dialogTimeStartRegInput.typedInput('validate'); + result.valid.timeEnd = $dialogTimeEndRegInput.typedInput('validate'); + + if (timeTypeStart !== 'none' || timeTypeEnd !== 'none') { + result.time = { }; + if (timeTypeStart !== 'none') { + result.time.start = { + type : timeTypeStart, + value : $dialogTimeStartRegInput.typedInput('value'), + offsetType : $dialogTimeStartRegOffset.typedInput('type'), + offset : $dialogTimeStartRegOffset.typedInput('value'), + multiplier : parseInt($dialogTimeStartRegMultiplier.val()), + days : getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7), + months : getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) + }; + result.valid.timeStart = result.valid.timeStart && $dialogTimeStartRegOffset.typedInput('validate') && result.time.multiplier; + const minType = $dialogTimeStartMinInput.typedInput('type'); + result.valid.timeStartMin = $dialogTimeStartMinInput.typedInput('validate'); + if (minType !== 'none') { + result.time.start.min = { + type : minType, + value : $dialogTimeStartMinInput.typedInput('value'), + offsetType : $dialogTimeStartMinOffset.typedInput('type'), + offset : $dialogTimeStartMinOffset.typedInput('value'), + multiplier : parseInt($dialogTimeStartMinMultiplier.val()) + }; + result.valid.timeStartMin = result.valid.timeStartMin && + $dialogTimeStartMinOffset.typedInput('validate') && + result.timeMin.multiplier; + } + const maxType = $dialogTimeStartMaxInput.typedInput('type'); + result.valid.timeStartMax = $dialogTimeStartMaxInput.typedInput('validate'); + if (maxType !== 'none') { + result.time.start.max = { + type : maxType, + value : $dialogTimeStartMaxInput.typedInput('value'), + offsetType : $dialogTimeStartMaxOffset.typedInput('type'), + offset : $dialogTimeStartMaxOffset.typedInput('value'), + multiplier : parseInt($dialogTimeStartMaxMultiplier.val()) + }; + result.valid.timeStartMax =result.valid.timeStartMax && + result.timeMax.multiplier; + } + result.isValid = result.isValid && result.valid.timeStart && result.valid.timeStartMin && result.valid.timeStartMax; + } + if (timeTypeEnd !== 'none') { + result.time.end = { + type : timeTypeEnd, + value : $dialogTimeEndRegInput.typedInput('value'), + offsetType : $dialogTimeEndRegOffset.typedInput('type'), + offset : $dialogTimeEndRegOffset.typedInput('value'), + multiplier : parseInt($dialogTimeEndRegMultiplier.val()), + days : getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7), + months : getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) + }; + result.valid.timeEnd = result.valid.timeEnd && $dialogTimeEndRegOffset.typedInput('validate') && result.time.multiplier; + const minType = $dialogTimeEndMinInput.typedInput('type'); + result.valid.timeEndMin = $dialogTimeEndMinInput.typedInput('validate'); + if (minType !== 'none') { + result.time.end.min = { + type : minType, + value : $dialogTimeEndMinInput.typedInput('value'), + offsetType : $dialogTimeEndMinOffset.typedInput('type'), + offset : $dialogTimeEndMinOffset.typedInput('value'), + multiplier : parseInt($dialogTimeEndMinMultiplier.val()) + }; + result.valid.timeEndMin = result.valid.timeEndMin && + $dialogTimeEndMinOffset.typedInput('validate') && + result.timeMin.multiplier; + } + const maxType = $dialogTimeEndMaxInput.typedInput('type'); + result.valid.timeEndMax = $dialogTimeEndMaxInput.typedInput('validate'); + if (maxType !== 'none') { + result.time.end.max = { + type : maxType, + value : $dialogTimeEndMaxInput.typedInput('value'), + offsetType : $dialogTimeEndMaxOffset.typedInput('type'), + offset : $dialogTimeEndMaxOffset.typedInput('value'), + multiplier : parseInt($dialogTimeEndMaxMultiplier.val()) + }; + result.valid.timeEndMax =result.valid.timeEndMax && + result.timeMax.multiplier; + } + result.isValid = result.isValid && result.valid.timeEnd && result.valid.timeEndMin && result.valid.timeEndMax; + } + result.valid.timeLimit = true; setval(result.time,'onlyEvenDays', $dialogTimeLimitOnlyEvenDays.is(':checked')); setval(result.time,'onlyOddDays', $dialogTimeLimitOnlyOddDays.is(':checked')); setval(result.time,'onlyEvenWeeks', $dialogTimeLimitOnlyEvenWeeks.is(':checked')); @@ -3282,7 +3743,6 @@ setval(result.time,'dateStart', $dialogTimeLimitDateStart.val()); setval(result.time,'dateEnd', $dialogTimeLimitDateEnd.val()); - result.valid.time = result.valid.time && $dialogTimeRegOffset.typedInput('validate') && result.time.multiplier; if (result.time.onlyEvenDays && result.time.onlyOddDays) { delete result.time.onlyEvenDays; delete result.time.onlyOddDays; @@ -3292,36 +3752,9 @@ delete result.time.onlyOddWeeks; } if (!result.time.days || !result.time.months) { - result.valid.time = false; - } - const minType = $dialogTimeMinInput.typedInput('type'); - result.valid.timeMin = $dialogTimeMinInput.typedInput('validate'); - if (minType !== 'none') { - result.timeMin = { - type : minType, - value : $dialogTimeMinInput.typedInput('value'), - offsetType : $dialogTimeMinOffset.typedInput('type'), - offset : $dialogTimeMinOffset.typedInput('value'), - multiplier : parseInt($dialogTimeMinMultiplier.val()) - }; - result.valid.timeMin = result.valid.timeMin && - $dialogTimeMinOffset.typedInput('validate') && - result.timeMin.multiplier; - } - const maxType = $dialogTimeMaxInput.typedInput('type'); - result.valid.timeMax = $dialogTimeMaxInput.typedInput('validate'); - if (maxType !== 'none') { - result.timeMax = { - type : maxType, - value : $dialogTimeMaxInput.typedInput('value'), - offsetType : $dialogTimeMaxOffset.typedInput('type'), - offset : $dialogTimeMaxOffset.typedInput('value'), - multiplier : parseInt($dialogTimeMaxMultiplier.val()) - }; - result.valid.timeMax =result.valid.timeMax && - result.timeMax.multiplier; + result.valid.timeLimit = false; } - result.isValid = result.isValid && result.valid.time && result.valid.timeMin && result.valid.timeMax; + result.isValid = result.isValid && result.valid.timeLimit; } result.valid.level = (result.level.operator === cNBC_RULE_OP.off) || (result.level.operator === cNBC_RULE_OP.slatOversteer) || @@ -3866,44 +4299,72 @@

-
- - +
+
+ + + +
+
+ + +
+
+ + + -
-
- - - +
+
+ + + +
+
+ + +
+
+ + +
-
- - +
+ +
-
- - - +
-
- - +
+ +
-
- - - +
-
+
-
+
diff --git a/nodes/locales/en-US/blind-control.json b/nodes/locales/en-US/blind-control.json index 2b45de1..0a4c4b0 100644 --- a/nodes/locales/en-US/blind-control.json +++ b/nodes/locales/en-US/blind-control.json @@ -28,7 +28,8 @@ "sunFloorLength": "length on the floor", "sunMinDelta": "min delta", "sunTopic": "Topic 🌞/⛄", - "time": "time", + "timeStart": "time Start", + "timeEnd": "time End", "name": "name", "oversteerValue": "oversteer", "oversteerThreshold": "Threshold", diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/position-config.json index 928c920..941889b 100644 --- a/nodes/locales/en-US/position-config.json +++ b/nodes/locales/en-US/position-config.json @@ -37,6 +37,8 @@ "types": { "unlimited":"no limitation", "undefined":"not used", + "undefinedTimeFrom":"from midnight or previous rule", + "undefinedTimeUntil":"until midnight or the next rule", "datespecific":"timestamp enhanced", "nodeId":"node name", "nodeName":"node ID", From 94e209a63d6d4bf7e205f7b2a875748819a847a4 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sat, 13 Nov 2021 13:20:23 +0100 Subject: [PATCH 04/44] dev --- nodes/clock-timer.js | 5 +++++ nodes/lib/timeControlHelper.js | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/nodes/clock-timer.js b/nodes/clock-timer.js index 5947a52..0e8658c 100644 --- a/nodes/clock-timer.js +++ b/nodes/clock-timer.js @@ -545,6 +545,11 @@ module.exports = function (RED) { topic = hlp.topicReplace(topic, replaceAttrs); } + node.debug('output'); + node.debug(util.inspect(node.previousData.payloadValue, Object.getOwnPropertyNames(err))); + node.debug(util.inspect(node.payload.current, Object.getOwnPropertyNames(err))); + node.debug(`topic alt=${node.payload.topic} 0 topic neu=${node.previousData.topic}`); + node.payload.current; if ((typeof node.payload.current !== 'undefined') && (node.payload.current !== 'none') && (node.payload.current !== null) && diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index faa41cd..0efa1b4 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -9,6 +9,12 @@ const path = require('path'); const util = require('util'); const hlp = require( path.resolve( __dirname, './dateTimeHelper.js') ); +const cRuleTime = { // deprecated + until : 0, + from : 1 +}; + + const cRuleType = { absolute : 0, levelMinOversteer : 1, // ⭳❗ minimum (oversteer) @@ -39,7 +45,8 @@ module.exports = { cRuleType, cRuleDefault, cRuleLogOperatorAnd, - cRuleLogOperatorOr + cRuleLogOperatorOr, + cRuleTime // deprecated }; let RED = null; @@ -268,10 +275,12 @@ function prepareRules(node, msg, tempData, dNow) { * @param {*} node node data * @param {*} msg the message object * @param {*} rule the rule data + * @param {string} timep rule type + * @param {number} dNow base timestamp * @return {number} timestamp of the rule */ function getRuleTimeData(node, msg, rule, timep, dNow) { - rule.time.dNow = dNow; + rule.time[timep].dNow = dNow; rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time[timep]); if (rule.timeData.error) { @@ -895,7 +904,7 @@ function initializeCtrl(REDLib, node, config) { } else { rule.time[id].dateEnd = new Date(2000,11,31, 23, 59, 59, 999); } - } + } }; rule.time.next = false; node.rules.firstTimeLimited = Math.min(i, node.rules.firstTimeLimited); From 0bf33071a6fe22bda000b83f3eedb7ee0c7d7ac7 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sat, 27 Nov 2021 22:05:03 +0100 Subject: [PATCH 05/44] fixed within-time --- CHANGELOG.md | 4 +++ nodes/blind-control.html | 9 ++++-- nodes/locales/de/position-config.json | 7 +++- nodes/locales/en-US/position-config.json | 1 + nodes/locales/en-US/within-time-switch.json | 3 -- nodes/position-config.js | 4 ++- nodes/static/htmlglobal.js | 7 +++- nodes/within-time-switch.html | 36 +++++++++++++++------ nodes/within-time-switch.js | 12 ++++--- 9 files changed, 59 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62cf42b..242da0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Install of a specific Version in Redmatic (on a Homematic): This can be also used to go back to an older Version. +### 2.0.4: flow library fix + +- no changes, republished 2.0.3 because of node-red flow library missing data + ### 2.0.3: small fix + enhancement - general diff --git a/nodes/blind-control.html b/nodes/blind-control.html index fa56360..f7d5677 100644 --- a/nodes/blind-control.html +++ b/nodes/blind-control.html @@ -2125,7 +2125,7 @@ .css('width', 'calc(60% - 30px)') .appendTo($row1) .typedInput({ - default: 'input', + default: types.OutputLevel.value, types: [ types.OutputLevel, types.OutputLevelInverse, @@ -2159,9 +2159,12 @@ // const plNType = $propertyName.typedInput('type'); const plVType = $propertyValue.typedInput('type'); const plVValue = $propertyValue.typedInput('value'); - if (plVValue === 'input') { + if (plVValue === types.OutputLevel.value || + plVValue === types.OutputLevelInverse.value || + plVValue === types.OutputSlat.value || + plVValue === types.OutputTopic.value || + plVValue === types.OutputCtrlObj.value) { $containerRow.attr('title', $('#node-input-input').attr('title')); - } else { getBackendData(d => { $containerRow.attr('title', bdDateToTime(d)); diff --git a/nodes/locales/de/position-config.json b/nodes/locales/de/position-config.json index 13a18ff..bfe9ac8 100644 --- a/nodes/locales/de/position-config.json +++ b/nodes/locales/de/position-config.json @@ -3,6 +3,8 @@ "label": { "positionConfig": "Konfiguration", "binary": "binär", + "infoText":"Die Konfiguration muss erstellt und deployed werden damit die Einstellungen angezeigt werden können.", + "loadingInfo":"lade Daten...", "json": "json", "jsonata": "jsonata Ausdr.", "timestamp": "Zeitpunkt", @@ -35,6 +37,9 @@ "types": { "unlimited": "keine Limitierung", "undefined": "nicht verwendet", + "undefinedTimeFrom":"von Mitternacht oder vorhergehender Regel", + "undefinedTimeUntil":"bis Mitternacht oder nächster Regel", + "msgInput": "eingangs nachricht", "datespecific": "Zeitpunkt (erweitert)", "nodeId":"Node Name", "nodeName":"Node ID", @@ -89,7 +94,7 @@ "slat":"Lamellenposition", "topic":"topic", "ctrlObj":"Status object", - "strPlaceholder": "string mit Platzhatern" + "strPlaceholder": "string mit Platzhaltern" }, "typeOptions": { "moonRise": "Mondaufgang", diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/position-config.json index 941889b..aa07c6e 100644 --- a/nodes/locales/en-US/position-config.json +++ b/nodes/locales/en-US/position-config.json @@ -39,6 +39,7 @@ "undefined":"not used", "undefinedTimeFrom":"from midnight or previous rule", "undefinedTimeUntil":"until midnight or the next rule", + "msgInput": "input message", "datespecific":"timestamp enhanced", "nodeId":"node name", "nodeName":"node ID", diff --git a/nodes/locales/en-US/within-time-switch.json b/nodes/locales/en-US/within-time-switch.json index 9b9105a..38d70b2 100644 --- a/nodes/locales/en-US/within-time-switch.json +++ b/nodes/locales/en-US/within-time-switch.json @@ -36,9 +36,6 @@ "withinTimeValue": "in time payload", "outOfTimeValue": "out of time payload" }, - "typeLabel":{ - "input": "msg.payload of the input message" - }, "placeholder": { "propertyStart": "Property for alternate start time", "propertyStartThreshold": "threshold", diff --git a/nodes/position-config.js b/nodes/position-config.js index 0ee6a13..1bac335 100644 --- a/nodes/position-config.js +++ b/nodes/position-config.js @@ -750,7 +750,9 @@ module.exports = function (RED) { */ setMessageProp(_srcNode, msg, type, value, data) { // _srcNode.debug(`setMessageProp type=${type} value=${value} msg=${util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })} data=${util.inspect(data, { colors: true, compact: 10, breakLength: Infinity })}`); - if (type === 'msgPayload') { + if (type === 'msgInput') { + return; + } else if (type === 'msgPayload') { msg.payload = data; } else if (type === 'msgTopic') { msg.topic = data; diff --git a/nodes/static/htmlglobal.js b/nodes/static/htmlglobal.js index b1a8fab..f4fe3cb 100644 --- a/nodes/static/htmlglobal.js +++ b/nodes/static/htmlglobal.js @@ -183,7 +183,12 @@ function getTypes(node) { // eslint-disable-line no-unused-vars }, DateSpecific: { value: 'dateSpecific', - label: node._('node-red-contrib-sun-position/position-config:common.types.datespecific','timestamp enhanced'), + label: node._('node-red-contrib-sun-position/position-config:common.types.datespecific'), + hasValue: false + }, + MsgInput: { + value: 'msgInput', + label: node._('node-red-contrib-sun-position/position-config:common.types.msgInput'), hasValue: false }, MsgPayload: { diff --git a/nodes/within-time-switch.html b/nodes/within-time-switch.html index cc7fafc..df577ba 100644 --- a/nodes/within-time-switch.html +++ b/nodes/within-time-switch.html @@ -265,19 +265,21 @@ }, withinTimeValue: { value: 'true', - required: true, + required: false, validate: RED.validators.typedInput('withinTimeValueType') }, withinTimeValueType: { - value: 'input' + value: 'msgInput', + required: true }, outOfTimeValue: { value: 'false', - required: true, + required: false, validate: RED.validators.typedInput('outOfTimeValueType') }, outOfTimeValueType: { - value: 'input' + value: 'msgInput', + required: true }, tsCompare: { value: '0' @@ -839,14 +841,17 @@ }); // #endregion timeAltEnd // #region Output Values + if (node.withinTimeValueType === 'input') { + node.withinTimeValueType = types.MsgInput.value; + } setupTInput(node, { typeProp: 'withinTimeValueType', valueProp: 'withinTimeValue', width: 'calc(100% - 110px)', - defaultType: 'input', - defaultValue: 'true' , + defaultType: node.withinTimeValueType || types.MsgInput.value, + defaultValue: node.withinTimeValue, types: [ - { value: 'input', label: node._('within-time-switch.typeLabel.input'), hasValue: false }, + types.MsgInput, 'str', 'num', 'bool', @@ -883,14 +888,17 @@ types.nodeName ] }); + if (node.outOfTimeValueType === 'input') { + node.outOfTimeValueType = types.MsgInput.value; + } setupTInput(node, { typeProp: 'outOfTimeValueType', valueProp: 'outOfTimeValue', width: 'calc(100% - 110px)', - defaultType: 'input', - defaultValue: 'true' , + defaultType: node.outOfTimeValueType || types.MsgInput.value, + defaultValue: node.outOfTimeValue, types: [ - { value: 'input', label: node._('within-time-switch.typeLabel.input'), hasValue: false }, + types.MsgInput, 'str', 'num', 'bool', @@ -989,6 +997,14 @@ this.timeDays = getCheckboxesStr($('#within-time-switch-timeDays input[type=checkbox]:checked'), 7); this.timeMonths = getCheckboxesStr($('#within-time-switch-timeMonths input[type=checkbox]:checked'), 12); + + this.withinTimeValueType = $('#node-input-withinTimeValue').typedInput('type'); + this.outOfTimeValueType = $('#node-input-outOfTimeValue').typedInput('type'); + console.log(this.withinTimeValueType); + console.log(this.outOfTimeValueType); + console.log($('#node-input-outOfTimeValue').typedInput('value')); + console.log($('#node-input-outOfTimeValue').typedInput('value')); + } }); })(); diff --git a/nodes/within-time-switch.js b/nodes/within-time-switch.js index 047224a..1707dec 100644 --- a/nodes/within-time-switch.js +++ b/nodes/within-time-switch.js @@ -418,12 +418,14 @@ module.exports = function (RED) { } this.withinTimeValue = { value : config.withinTimeValue ? config.withinTimeValue : 'true', - type : config.withinTimeValueType ? config.withinTimeValueType : 'input' + type : config.withinTimeValueType ? config.withinTimeValueType : 'msgInput' }; + if (this.withinTimeValue.type === 'input') { this.withinTimeValue.type = 'msgInput'; } this.outOfTimeValue = { value : config.outOfTimeValue ? config.outOfTimeValue : 'false', - type : config.outOfTimeValueType ? config.outOfTimeValueType : 'input' + type : config.outOfTimeValueType ? config.outOfTimeValueType : 'msgInput' }; + if (this.outOfTimeValueType.type === 'input') { this.outOfTimeValueType.type = 'msgInput'; } this.timeOutObj = null; this.lastMsgObj = null; @@ -458,7 +460,7 @@ module.exports = function (RED) { msg.withinTime = true; this.debug('in time [1] - send msg to first output ' + result.startSuffix + node.positionConfig.toDateTimeString(dNow) + result.endSuffix + ' (' + msg.withinTimeStart.id + ' - ' + cmpNow + ' - ' + msg.withinTimeEnd.id + ')'); - if (node.withinTimeValue.type === 'input') { + if (node.withinTimeValue.type === 'msgInput') { send([msg, null]); // within time } else { const resultMsg = RED.util.cloneMessage(msg); @@ -472,7 +474,7 @@ module.exports = function (RED) { msg.withinTime = true; this.debug('in time [2] - send msg to first output ' + result.startSuffix + node.positionConfig.toDateTimeString(dNow) + result.endSuffix + ' (' + msg.withinTimeStart.id + ' - ' + cmpNow + ' - ' + msg.withinTimeEnd.id + ')'); - if (node.withinTimeValue.type === 'input') { + if (node.withinTimeValue.type === 'msgInput') { send([msg, null]); // within time } else { const resultMsg = RED.util.cloneMessage(msg); @@ -487,7 +489,7 @@ module.exports = function (RED) { } msg.withinTime = false; this.debug('out of time - send msg to second output ' + result.startSuffix + node.positionConfig.toDateTimeString(dNow) + result.endSuffix); - if (node.outOfTimeValue.type === 'input') { + if (node.outOfTimeValue.type === 'msgInput') { send([null, msg]); // out of time } else { const resultMsg = RED.util.cloneMessage(msg); From 6067b275440180e553eb4e2e2be98d966ed3f49e Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sun, 23 Jan 2022 16:59:32 +0100 Subject: [PATCH 06/44] dev --- nodes/blind-control.js | 41 +++++++++++++++++++++------------- nodes/lib/timeControlHelper.js | 31 ++++++++++++++++--------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/nodes/blind-control.js b/nodes/blind-control.js index adc1736..3e6fabb 100644 --- a/nodes/blind-control.js +++ b/nodes/blind-control.js @@ -502,7 +502,6 @@ module.exports = function (RED) { */ function checkRules(node, msg, oNow, tempData) { // node.debug('checkRules --------------------'); - const livingRuleData = {}; ctrlLib.prepareRules(node, msg, tempData, oNow.now); // node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); @@ -517,7 +516,7 @@ module.exports = function (RED) { const rule = node.rules.data[i]; // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noFrom=${rule.time.operator !== cRuleFrom} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === ctrlLib.cRuleTime.from) { continue; } + if (rule.time && !rule.time.start) { continue; } const res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); /* if (!rule.time || rule.time.operator === ctrlLib.cRuleTime.from) { @@ -547,12 +546,12 @@ module.exports = function (RED) { } if (!ruleSel || (ruleSel.time && ruleSel.time.operator === ctrlLib.cRuleTime.from) ) { - //node.debug('--------- starting second loop ' + node.rules.count); + // node.debug('--------- starting second loop ' + node.rules.count); for (let i = (node.rules.count - 1); i >= 0; --i) { const rule = node.rules.data[i]; // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noUntil=${rule.time.operator !== cRuleUntil} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`); if (!rule.enabled) { continue; } - if (rule.time && rule.time.operator === ctrlLib.cRuleTime.until) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from + if (rule.time && !rule.time.end) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from const res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); if (res) { // node.debug(`2. ruleSel ${rule.name} (${rule.pos}) data=${ util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }) }`); @@ -573,7 +572,7 @@ module.exports = function (RED) { } const rule = ctrlLib.checkRules(node, msg, oNow, tempData); - if (ruleSel != rule.ruleSel) { + if (ruleSel !== rule.ruleSel) { node.error('not equal result ruleSel!'); node.debug('ruleSel ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); @@ -581,22 +580,22 @@ module.exports = function (RED) { node.debug('same rule selected ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); node.debug('same rule selected ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); } - if (ruleSlatOvs != rule.ruleSlatOvs) { + if (ruleSlatOvs !== rule.ruleSlatOvs) { node.error('not equal result ruleSlatOvs!'); node.debug('ruleSlatOvs ' + util.inspect(ruleSlatOvs, { colors: true, compact: 10, breakLength: Infinity })); node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); } - if (ruleTopicOvs != rule.ruleTopicOvs) { + if (ruleTopicOvs !== rule.ruleTopicOvs) { node.error('not equal result ruleTopicOvs!'); node.debug('ruleTopicOvs ' + util.inspect(ruleTopicOvs, { colors: true, compact: 10, breakLength: Infinity })); node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); } - if (ruleSelMin != rule.ruleSelMin) { + if (ruleSelMin !== rule.ruleSelMin) { node.error('not equal result ruleSelMin!'); node.debug('ruleSelMin ' + util.inspect(ruleSelMin, { colors: true, compact: 10, breakLength: Infinity })); node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); } - if (ruleSelMax != rule.ruleSelMax) { + if (ruleSelMax !== rule.ruleSelMax) { node.error('not equal result ruleSelMax!'); node.debug('ruleSelMax ' + util.inspect(ruleSelMax, { colors: true, compact: 10, breakLength: Infinity })); node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); @@ -675,12 +674,24 @@ module.exports = function (RED) { if (!rule.time) { return; } - const num = ctrlLib.getRuleTimeData(node, msg, rule, oNow); - if (num > oNow.nowNr) { - node.debug('autoTrigger set to rule ' + rule.pos); - const diff = num - oNow.nowNr; - node.autoTrigger.time = Math.min(node.autoTrigger.time, diff); - node.autoTrigger.type = 2; // next rule + if (rule.time.start) { + const numStart = ctrlLib.getRuleTimeData(node, msg, rule, 'start', oNow); + if (numStart > oNow.nowNr) { + node.debug('autoTrigger set to rule ' + rule.pos + ' (start)'); + const diff = numStart - oNow.nowNr; + node.autoTrigger.time = Math.min(node.autoTrigger.time, diff); + node.autoTrigger.type = 2; // next rule + return; + } + } + if (rule.time.end) { + const numEnd = ctrlLib.getRuleTimeData(node, msg, rule, 'end', oNow); + if (numEnd > oNow.nowNr) { + node.debug('autoTrigger set to rule ' + rule.pos + ' (end)'); + const diff = numEnd - oNow.nowNr; + node.autoTrigger.time = Math.min(node.autoTrigger.time, diff); + node.autoTrigger.type = 2; // next rule + } } }; if (ruleSel) { diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index b25a6dc..9d610f2 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -2,8 +2,6 @@ * dateTimeHelper.js: *********************************************/ 'use strict'; -const { time } = require('console'); -const { endsWith } = require('lodash'); const path = require('path'); const util = require('util'); @@ -280,6 +278,9 @@ function prepareRules(node, msg, tempData, dNow) { * @return {number} timestamp of the rule */ function getRuleTimeData(node, msg, rule, timep, dNow) { + if (!rule.time[timep]) { + return -1; + } rule.time[timep].now = dNow; rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time[timep]); @@ -293,9 +294,9 @@ function getRuleTimeData(node, msg, rule, timep, dNow) { rule.timeData.ts = rule.timeData.value.getTime(); // node.debug(`time=${rule.timeData.value} -> ${new Date(rule.timeData.value)}`); rule.timeData.dayId = hlp.getDayId(rule.timeData.value); - if (rule.timeMin) { - rule.timeMin.now = dNow; - rule.timeDataMin = node.positionConfig.getTimeProp(node, msg, rule.timeMin); + if (rule.time[timep].min) { + rule.time[timep].min.now = dNow; + rule.timeDataMin = node.positionConfig.getTimeProp(node, msg, rule.time[timep].min); const numMin = rule.timeDataMin.value.getTime(); rule.timeDataMin.source = 'Min'; if (rule.timeDataMin.error) { @@ -312,9 +313,9 @@ function getRuleTimeData(node, msg, rule, timep, dNow) { } } } - if (rule.timeMax) { - rule.timeMax.now = dNow; - rule.timeDataMax = node.positionConfig.getTimeProp(node, msg, rule.timeMax); + if (rule.time[timep].max) { + rule.time[timep].max.now = dNow; + rule.timeDataMax = node.positionConfig.getTimeProp(node, msg, rule.time[timep].max); const numMax = rule.timeDataMax.value.getTime(); rule.timeDataMax.source = 'Max'; if (rule.timeDataMax.error) { @@ -381,6 +382,9 @@ function compareRules(node, msg, rule, cmp, data) { if (!rule.time) { return rule; } + let numStart = Number.MIN_VALUE; + let numEnd = Number.MAX_VALUE; + if (rule.time.start) { if (rule.time.start.days && !rule.time.start.days.includes(data.dayNr)) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid days`); @@ -423,6 +427,7 @@ function compareRules(node, msg, rule, cmp, data) { } } } + numStart = getRuleTimeData(node, msg, rule, 'start', data.now); } else if (rule.time.end) { if (rule.time.end.days && !rule.time.end.days.includes(data.dayNr)) { return null; @@ -464,12 +469,17 @@ function compareRules(node, msg, rule, cmp, data) { } } } + numEnd = getRuleTimeData(node, msg, rule, 'end', data.now); } else { return null; } - const num = getRuleTimeData(node, msg, rule, data.now); + if (numStart > numEnd) { + if (data.dayId === rule.timeData.dayId && numStart >=0 && numStart <= data.nowNr && numEnd > data.nowNr) { + return rule; + } + } // node.debug(`compareRules ${rule.name} (${rule.pos}) type=${rule.time.operatorText} - ${rule.time.value} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`); - if (data.dayId === rule.timeData.dayId && num >=0 && (cmp(num) === true)) { + if (data.dayId === rule.timeData.dayId && numStart >=0 && numStart <= data.nowNr && numEnd > data.nowNr) { return rule; } // node.debug(`compareRules rule ${rule.name} (${rule.pos}) dayId=${data.dayId} rule-DayID=${rule.timeData.dayId} num=${num} cmp=${cmp(num)} invalid time`); @@ -486,7 +496,6 @@ function compareRules(node, msg, rule, cmp, data) { */ function checkRules(node, msg, oNow, tempData) { // node.debug('checkRules --------------------'); - const livingRuleData = {}; prepareRules(node, msg, tempData, oNow.dNow); // node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); const result = { From 5eef75fecb09d72e15412dad015376ef3ea9aed4 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sun, 30 Jan 2022 14:26:26 +0100 Subject: [PATCH 07/44] development intermediate state --- nodes/blind-control.html | 747 ++++++++++++++--------- nodes/blind-control.js | 100 +-- nodes/clock-timer.html | 9 +- nodes/lib/dateTimeHelper.js | 2 +- nodes/lib/timeControlHelper.js | 405 ++++++------ nodes/locales/de/position-config.json | 3 +- nodes/locales/en-US/blind-control.json | 5 - nodes/locales/en-US/position-config.json | 19 +- nodes/position-config.html | 2 + nodes/position-config.js | 12 +- nodes/static/htmlglobal.js | 5 + nodes/time-comp.html | 3 +- nodes/time-inject.html | 3 +- nodes/time-span.html | 3 +- nodes/within-time-switch.html | 6 +- nodes/within-time-switch.js | 4 +- 16 files changed, 774 insertions(+), 554 deletions(-) diff --git a/nodes/blind-control.html b/nodes/blind-control.html index 676ced7..10d3f34 100644 --- a/nodes/blind-control.html +++ b/nodes/blind-control.html @@ -6,6 +6,11 @@ const cNBC_MODE_MAXIMIZE = 1; const cNBC_MODE_MINIMIZE = 3; const cNBC_MODE_SUN_CONTROL = 16; + const cNBC_RULE_EXEC = { + auto: 0, + first:1, + last:2 + }; const cNBC_RULE_TYPE_UNTIL = 0; const cNBC_RULE_TYPE_FROM = 1; const cNBC_RULE_OP = { @@ -533,7 +538,7 @@ $('#node-input-sunControlMode').val(cNBC_MODE_SUN_CONTROL); } - node.dialogAddData = {}; + node.dialogAddData = { time:{ start:{ min:{}, max:{} }, end:{ min:{}, max:{} } }, timeReg:{} }; const setup = function(node) { /* global getTypes getSelectFields setupTInput appendOptions addLabel getBackendData bdDateToTime initCheckboxesBlock getCheckboxesStr setTInputValue */ // #region initialize @@ -829,7 +834,7 @@ }); // #endregion blind // #region rules - const ruleVersion = 3; + const ruleVersion = 4; /** * get a time in millisecond as string of the format hh:mm, hh:mm:ss or hh:mm:ss.mss * @param {number} s - milisecond @@ -924,17 +929,30 @@ let result = ''; const length = (enh) ? 60 : 30; try { - if (typeof data.isValid === 'undefined' || typeof data.version === 'undefined' || (!data.isValid && (typeof data.valid === 'undefined' || Object.keys(data.valid).length === 0))) { + if (typeof data.isValid === 'undefined' || typeof data.version === 'undefined' || (!data.isValid && !data.validationError && typeof data.validation === 'undefined')) { result += '
'; result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.outDated'); result += '
'; } else if (!data.isValid) { result += '
'; result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.isInValid'); + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleInValidText'); + result += data.validationError; result += '
'; - if (data.valid && Object.keys(data.valid).length > 0) { + if (data.validation && Object.keys(data.validation).length > 0) { + const lines = JSON.stringify(data.validation, + (key, value) => { + if (value === true || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0)) + return undefined; + if (value === false) + return node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleInValidElement'); + return value; + }, '\t').split('\n'); + lines.splice(0,1); // remove first line + lines.pop(); // remove last line result += '
';
-                                    result += JSON.stringify(data.valid, null, '\t');
+                                    result += lines.join('\n');
                                     result += '
'; } } @@ -986,154 +1004,159 @@ result += '
'; } if (data.time) { - if (data.time.type && data.timeType !== 'none') { - result += '
'; - result += ' '; - result += data.time.operatorText; - result += ' ' + RED.nodes.getType('position-config').getValueLabel(node, data.time.type, data.time.value, null, length) + ''; - result += _getOffsetText(node, data.time.offsetType, data.time.offset, data.time.multiplier); - if (enh && enh.timeReg && enh.timeReg.text) { - result += ' [ ' + enh.timeReg.text + ']'; - } + const gettime = (ttype, text) => { + if (data.time[ttype] && data.time[ttype].type && data.timeType !== 'none') { + result += '
'; + result += ' '; + result += text; + result += ' ' + RED.nodes.getType('position-config').getValueLabel(node, data.time[ttype].type, data.time[ttype].value, null, length) + ''; + result += _getOffsetText(node, data.time[ttype].offsetType, data.time[ttype].offset, data.time[ttype].multiplier); + if (enh && enh.timeReg && enh.timeReg.text) { + result += ' [ ' + enh.timeReg.text + ']'; + } - if (data.timeMin && data.timeMin.type && data.timeMin.type !== 'none') { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMin'); - result += ' '; - result += RED.nodes.getType('position-config').getValueLabel(node, data.timeMin.type, data.timeMin.value, null, length); - result += ''; - result += _getOffsetText(node, data.timeMin.offsetType, data.timeMin.offset, data.timeMin.multiplier); - if (enh && enh.timeMin && enh.timeMin.text) { - result += ' [ ' + enh.timeMin.text + ']'; + if (data.time[ttype].min && data.time[ttype].min.type && data.time[ttype].min.type !== 'none') { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMin'); + result += ' '; + result += RED.nodes.getType('position-config').getValueLabel(node, data.time[ttype].min.type, data.time[ttype].min.value, null, length); + result += ''; + result += _getOffsetText(node, data.time[ttype].min.offsetType, data.time[ttype].min.offset, data.time[ttype].min.multiplier); + if (enh && enh.time[ttype].min && enh.time[ttype].min.text) { + result += ' [ ' + enh.time[ttype].min.text + ']'; + } + result += '
'; } - result += '
'; - } - if (data.timeMax && data.timeMax.type && data.timeMax.type !== 'none') { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMax'); - result += ' '; - result += RED.nodes.getType('position-config').getValueLabel(node, data.timeMax.type, data.timeMax.value, null, length); - result += ''; - result += _getOffsetText(node, data.timeMax.offsetType, data.timeMax.offset, data.timeMax.multiplier); - if (enh && enh.timeMax && enh.timeMax.text) { - result += ' [ ' + enh.timeMax.text + ']'; + if (data.time[ttype].max && data.time[ttype].max.type && data.time[ttype].max.type !== 'none') { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMax'); + result += ' '; + result += RED.nodes.getType('position-config').getValueLabel(node, data.time[ttype].max.type, data.time[ttype].max.value, null, length); + result += ''; + result += _getOffsetText(node, data.time[ttype].max.offsetType, data.time[ttype].max.offset, data.time[ttype].max.multiplier); + if (enh && enh.time[ttype].max && enh.time[ttype].max.text) { + result += ' [ ' + enh.time[ttype].max.text + ']'; + } + result += '
'; } result += '
'; } - if (data.time.days && data.time.days !== '*') { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeDays'); - result += ' '; - const daysarr = data.time.days.split(','); - if (daysarr.length === 1) { - result += (node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[0]) + 7))); - } else if (data.time.days === '5,6,0') { - result += node._('node-red-contrib-sun-position/position-config:common.days.12'); - result += '-'; - result += node._('node-red-contrib-sun-position/position-config:common.days.7'); - } else if (daysarr.length === 2) { - result += node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[0]) + 7)); - result += ' '; - result += node._('node-red-contrib-sun-position/position-config:common.label.and'); - result += ' '; - result += node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[1]) + 7)); + }; + gettime('start',node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeStart')); + gettime('end',node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeEnd')); + result += '
'; + if (data.time.days && data.time.days !== '*') { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeDays'); + result += ' '; + const daysarr = data.time.days.split(','); + if (daysarr.length === 1) { + result += (node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[0]) + 7))); + } else if (data.time.days === '5,6,0') { + result += node._('node-red-contrib-sun-position/position-config:common.days.12'); + result += '-'; + result += node._('node-red-contrib-sun-position/position-config:common.days.7'); + } else if (daysarr.length === 2) { + result += node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[0]) + 7)); + result += ' '; + result += node._('node-red-contrib-sun-position/position-config:common.label.and'); + result += ' '; + result += node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(daysarr[1]) + 7)); + } else { + daysarr.sort((a, b) => parseInt(a) - parseInt(b)); + let isok = true; + let elF = parseInt(daysarr[0]); + daysarr[0] = node._('node-red-contrib-sun-position/position-config:common.days.'+(elF + 7)); + for (let index = 1; index < daysarr.length; index++) { + const element = parseInt(daysarr[index]); + isok = isok && (element === elF+1); + elF = element; + daysarr[index] = node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(element) + 7)); + } + if (isok) { + result += (daysarr[0] + '-' + daysarr[daysarr.length-1]); } else { - daysarr.sort((a, b) => parseInt(a) - parseInt(b)); - let isok = true; - let elF = parseInt(daysarr[0]); - daysarr[0] = node._('node-red-contrib-sun-position/position-config:common.days.'+(elF + 7)); - for (let index = 1; index < daysarr.length; index++) { - const element = parseInt(daysarr[index]); - isok = isok && (element === elF+1); - elF = element; - daysarr[index] = node._('node-red-contrib-sun-position/position-config:common.days.'+(parseInt(element) + 7)); - } - if (isok) { - result += (daysarr[0] + '-' + daysarr[daysarr.length-1]); - } else { - result += (daysarr.join(',')); - } + result += (daysarr.join(',')); } - result += '
'; } - if (data.time.onlyEvenDays || data.time.onlyOddDays || data.time.onlyEvenWeeks || data.time.onlyOddWeeks) { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimit'); - result += ' '; - const resultArr = []; - if (data.time.onlyEvenDays) { - resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyEvenDays')); - } - if (data.time.onlyOddDays) { - resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyOddDays')); - } - if (data.time.onlyEvenWeeks) { - resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyEvenWeeks')); - } - if (data.time.onlyOddWeeks) { - resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyOddWeeks')); - } - result += resultArr.join(' ' + node._('node-red-contrib-sun-position/position-config:common.label.and') + ' '); - result += '
'; + result += '
'; + } + if (data.time.onlyEvenDays || data.time.onlyOddDays || data.time.onlyEvenWeeks || data.time.onlyOddWeeks) { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimit'); + result += ' '; + const resultArr = []; + if (data.time.onlyEvenDays) { + resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyEvenDays')); } - if (data.time.months && data.time.months !== '*') { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMonths'); - result += ' '; - const montharr = data.time.months.split(','); - if (montharr.length === 2) { - result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[0]) + 12)); - result += ' '; - result += node._('node-red-contrib-sun-position/position-config:common.label.and'); - result += ' '; - result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[1]) + 12)); - } else if (montharr.length === 1) { - result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[0]) + 12)); - } else { - montharr.sort((a, b) => parseInt(a) - parseInt(b)); - let isok = true; - let elF = parseInt(montharr[0]); - montharr[0] = node._('node-red-contrib-sun-position/position-config:common.months.'+(elF + 12)); - for (let index = 1; index < montharr.length; index++) { - const element = parseInt(montharr[index]); - isok = isok && (element === elF+1); - elF = element; - montharr[index] = node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(element) + 12)); - } - if (isok) { - result += montharr[0]; - result += '-'; - result += montharr[montharr.length-1]; - } else { - result += montharr.join(','); - } - } - result += '
'; + if (data.time.onlyOddDays) { + resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyOddDays')); } - - if (data.time.dateStart || data.time.dateEnd) { - result += '
'; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimitStart'); - result += ' '; - if (enh) { - const year = (new Date()).getFullYear(); - result += data.time.dateStart || year + '-01-01'; - } else { - result += (getDateShort(data.time.dateStart) || '01-01'); + if (data.time.onlyEvenWeeks) { + resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyEvenWeeks')); + } + if (data.time.onlyOddWeeks) { + resultArr.push(node._('node-red-contrib-sun-position/position-config:common.label.onlyOddWeeks')); + } + result += resultArr.join(' ' + node._('node-red-contrib-sun-position/position-config:common.label.and') + ' '); + result += '
'; + } + if (data.time.months && data.time.months !== '*') { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeMonths'); + result += ' '; + const montharr = data.time.months.split(','); + if (montharr.length === 2) { + result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[0]) + 12)); + result += ' '; + result += node._('node-red-contrib-sun-position/position-config:common.label.and'); + result += ' '; + result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[1]) + 12)); + } else if (montharr.length === 1) { + result += node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(montharr[0]) + 12)); + } else { + montharr.sort((a, b) => parseInt(a) - parseInt(b)); + let isok = true; + let elF = parseInt(montharr[0]); + montharr[0] = node._('node-red-contrib-sun-position/position-config:common.months.'+(elF + 12)); + for (let index = 1; index < montharr.length; index++) { + const element = parseInt(montharr[index]); + isok = isok && (element === elF+1); + elF = element; + montharr[index] = node._('node-red-contrib-sun-position/position-config:common.months.'+(parseInt(element) + 12)); } - result += ' '; - result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimitEnd'); - result += ' '; - if (enh) { - const year = (new Date()).getFullYear(); - result += data.time.dateEnd || year + '-12-31'; + if (isok) { + result += montharr[0]; + result += '-'; + result += montharr[montharr.length-1]; } else { - result += (getDateShort(data.time.dateEnd) || '12-31'); + result += montharr.join(','); } - result += '
'; } - result += '
'; + result += '
'; } + if (data.time.dateStart || data.time.dateEnd) { + result += '
'; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimitStart'); + result += ' '; + if (enh) { + const year = (new Date()).getFullYear(); + result += data.time.dateStart || year + '-01-01'; + } else { + result += (getDateShort(data.time.dateStart) || '01-01'); + } + result += ' '; + result += node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleTimeLimitEnd'); + result += ' '; + if (enh) { + const year = (new Date()).getFullYear(); + result += data.time.dateEnd || year + '-12-31'; + } else { + result += (getDateShort(data.time.dateEnd) || '12-31'); + } + result += '
'; + } + result += '
'; } result += '
'; if (data.level) { @@ -1235,13 +1258,13 @@ if (data === {}) { data.description = 'please edit rule'; data.name = 'empty'; + data.exec = cNBC_RULE_EXEC.auto; data.version = ruleVersion; data.enabled = true; } data.index = containerIndex; data.importance = 0; if (!data.conditions) { - data.valid = {}; data.conditions = []; if (data.validOperandAType && data.validOperandAType !== types.Unlimited.value) { data.conditions.push({ @@ -1282,7 +1305,7 @@ delete data.valid2OperandBValue; delete data.valid2OperandBType; } - if(data.timeBType) { + if (data.timeBType) { if (data.timeBType === types.TimeSun.value || data.timeBType === types.TimeMoon.value) { data.timeMinType = data.timeType; data.timeMinValue = data.timeValue; @@ -1296,54 +1319,18 @@ delete data.timeBValue; delete data.timeBOp; delete data.timeBOpText; - data.isValid = false; + delete data.isValid; // need retrigger of validation, because rule is outdated data.description = node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleIsOutdated'); } - if(data.timeType) { - if (!data.time && data.timeType !== types.Undefined.value) { - data.time = { - type : data.timeType, - value : (data.timeValue || ''), - operator : (parseInt(data.timeOp) || 0), - offsetType : (data.offsetType || types.Undefined.value), - offset : (data.offsetValue || '1'), - multiplier : (parseInt(data.multiplier) || 60000), - days : (data.timeDays || '*'), - months : (data.timeMonths || '*') - }; - if (data.timeOnlyOddDays) data.time.onlyOddDays = data.timeOnlyOddDays; - if (data.timeOnlyEvenDays) data.time.onlyEvenDays = data.timeOnlyEvenDays; - if (data.timeDateStart) data.time.dateStart = data.timeDateStart; - if (data.timeDateEnd) data.time.dateEnd = data.timeDateEnd; - if (data.timeOpText) data.time.operatorText = data.timeOpText; - } - delete data.timeType; - delete data.timeValue; - delete data.timeOp; - delete data.timeOpText; - delete data.offsetType; - delete data.offsetValue; - delete data.multiplier; - delete data.timeDays; - delete data.timeMonths; - delete data.timeOnlyOddDays; - delete data.timeOnlyEvenDays; - delete data.timeDateStart; - delete data.timeDateEnd; - if (data.time && data.time.onlyEvenDays && data.time.onlyOddDays) { - delete data.time.onlyEvenDays; - delete data.time.onlyOddDays; - } - } if (data.timeMinType) { if (!data.timeMin && data.timeMinType !== types.Undefined.value) { - data.timeMin = { + data.timeMin = Object.assign({ type : data.timeMinType, value : (data.timeMinValue || ''), offsetType : (data.offsetMinType || types.Undefined.value), offset : (data.offsetMinValue || '1'), multiplier : (parseInt(data.multiplierMin) || 60000) - }; + }, data.timeMin); } delete data.timeMinType; delete data.timeMinValue; @@ -1354,13 +1341,13 @@ } if (data.timeMaxType) { if (!data.timeMax && data.timeMaxType !== types.Undefined.value) { - data.timeMax = { + data.timeMax = Object.assign({ type : data.timeMaxType, value : (data.timeMaxValue || ''), offsetType : (data.offsetMaxType || types.Undefined.value), offset : (data.offsetMaxValue || '1'), multiplier : (parseInt(data.multiplierMax) || 60000) - }; + }, data.timeMax); } delete data.timeMaxType; delete data.timeMaxValue; @@ -1369,6 +1356,128 @@ delete data.multiplierMax; delete data.timeMaxOp; } + if (data.timeType) { + if (!data.time && data.timeType !== types.Undefined.value) { + const operator = (parseInt(data.timeOp) || cNBC_RULE_TYPE_UNTIL); + data.time = { }; + let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL + data.exec = cNBC_RULE_EXEC.auto; + if (operator === cNBC_RULE_TYPE_FROM) { + data.time.start = {}; + ttype = 'start'; + } + data.time[ttype] = { + type : data.timeType, + value : (data.timeValue || ''), + offsetType : (data.offsetType || 'none'), + offset : (data.offsetValue || 1), + multiplier : (parseInt(data.multiplier) || 60000) + }; + + if (data.timeMinType && data.timeMinType !== 'none') { + data.time[ttype].min = { + type : data.timeMinType, + value : (data.timeMinValue || ''), + offsetType : (data.offsetMinType || 'none'), + offset : (data.offsetMinValue || 1), + multiplier : (parseInt(data.multiplierMin) || 60000) + }; + } + if (data.timeMin) { + data.time[ttype].min = { + type : data.timeMin.type, + value : data.timeMin.value, + offsetType : data.timeMin.offsetType, + offset : data.timeMin.offset, + multiplier : data.timeMin.multiplier + }; + delete data.timeMin; + } + if (data.timeMaxType && data.timeMaxType !== 'none') { + data.time[ttype].max = { + type : data.timeMaxType, + value : (data.timeMaxValue || ''), + offsetType : (data.offsetMaxType || 'none'), + offset : (data.offsetMaxValue || 1), + multiplier : (parseInt(data.multiplierMax) || 60000) + }; + } + if (data.timeMax) { + data.time[ttype].max = { + type : data.timeMax.type, + value : data.timeMax.value, + offsetType : data.timeMax.offsetType, + offset : data.timeMax.offset, + multiplier : data.timeMax.multiplier + }; + delete data.timeMax; + } + if (data.timeDays && data.timeDays !== '*') data.time.days = data.timeDays; + if (data.timeMonths && data.timeMonths !== '*') data.time.months = data.timeMonths; + if (data.timeOnlyOddDays) data.time.onlyOddDays = data.timeOnlyOddDays; + if (data.timeOnlyEvenDays) data.time.onlyEvenDays = data.timeOnlyEvenDays; + if (data.timeDateStart) data.time.dateStart = data.timeDateStart; + if (data.timeDateEnd) data.time.dateEnd = data.timeDateEnd; + } + delete data.timeType; + delete data.timeValue; + delete data.timeOp; + delete data.timeOpText; + delete data.offsetType; + delete data.offsetValue; + delete data.multiplier; + delete data.timeDays; + delete data.timeMonths; + delete data.timeOnlyOddDays; + delete data.timeOnlyEvenDays; + delete data.timeDateStart; + delete data.timeDateEnd; + if (data.time && data.time.onlyEvenDays && data.time.onlyOddDays) { + delete data.time.onlyEvenDays; + delete data.time.onlyOddDays; + } + } + if (data.time && (typeof data.time.operator !== 'undefined')) { + let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL + data.exec = cNBC_RULE_EXEC.auto; + if (data.time.operator === cNBC_RULE_TYPE_FROM) { + ttype = 'start'; + } + data.time[ttype] = Object.assign({ + type : data.time.type, + value : data.time.value, + offsetType : data.time.offsetType, + offset : data.time.offset, + multiplier : data.time.multiplier + }, data.time[ttype]); + if (data.timeMin && data.timeMin.type !== 'none' ) { + data.time[ttype].min = Object.assign({ + type : data.timeMin.type, + value : (data.timeMin.value || ''), + offsetType : (data.timeMin.offsetType || 'none'), + offset : (data.timeMin.offset || 1), + multiplier : (parseInt(data.timeMin.multiplier) || 60000) + }, data.time[ttype].min); + } + if (data.timeMax && data.timeMax.type !== 'none' ) { + data.time[ttype].max = Object.assign({ + type : data.timeMax.type, + value : (data.timeMax.value || ''), + offsetType : (data.timeMax.offsetType || 'none'), + offset : (data.timeMax.offset || 1), + multiplier : (parseInt(data.timeMax.multiplier) || 60000) + }, data.time[ttype].max); + } + delete data.time.operator; + delete data.time.operatorText; + delete data.time.type; + delete data.time.value; + delete data.time.offsetType; + delete data.time.offset; + delete data.time.multiplier; + delete data.timeMin; + delete data.timeMax; + } if (data.levelType) { if (!data.level) { data.level = { @@ -1403,6 +1512,9 @@ data.description = getRuleDescription(node, selFields, data); data.version = ruleVersion; } + if (typeof data.exec === 'undefined') { + data.exec = cNBC_RULE_EXEC.auto; + } $containerRow.data('ruleData', JSON.stringify(data)); $containerRow.css({ overflow: 'hidden'}); // , whiteSpace: 'nowrap' @@ -1412,6 +1524,14 @@ const $div = $('
', { class: 'rdgtimer-rule-mainblock' }).appendTo($row0); + let exec = cNBC_RULE_EXEC.first; + if (data.exec === cNBC_RULE_EXEC.last || (data.time && data.time.start && !data.time.end)) { + exec = cNBC_RULE_EXEC.last; + } + $('', { + id: 'rule-exec-'+containerIndex, + class: 'rdgtimer-rule-exec-span' + }).append(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleExecShort.'+exec)).appendTo( $div ); $('', { id: 'rule-name-'+containerIndex, class: 'rdgtimer-rule-name-span' @@ -2193,7 +2313,8 @@ types.randmNumCachedDay, types.randmNumCachedWeek, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($propertyValue, prop.v, prop.vt); @@ -2346,6 +2467,12 @@ }); const $dialogName = $dialog.find('#dlg-ip-btctl-rule-name'); + + const $dialogExec = $dialog.find('#dlg-ip-btctl-rule-exec'); + $dialogExec.append($('').val(cNBC_RULE_EXEC.auto).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleExecAuto'))); + $dialogExec.append($('').val(cNBC_RULE_EXEC.first).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleExecFirst'))); + $dialogExec.append($('').val(cNBC_RULE_EXEC.last).text(node._('node-red-contrib-sun-position/position-config:ruleCtrl.label.ruleExecLast'))); + let $dialogDataRow = null; let dialogDataOnLoading = false; let dialogDataIndex = -1; @@ -2441,7 +2568,8 @@ types.DayOfYearEven, types.SunControlModeType, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($dialogCondOperand, data.value, data.valueType); @@ -3132,13 +3260,13 @@ const opType = $dialogTimeStartMinInput.typedInput('type'); if (opType === types.Undefined.value) { $('#dialog-row-offset-timeStartMin').hide(); - node.dialogAddData.timeMin = { + node.dialogAddData.time.start.min = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.time.start.min.text); + $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.time.start.min.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || @@ -3151,20 +3279,20 @@ getBackendData(d => { if (d.value && d.value !== 'none') { const dv = new Date(d.value); - node.dialogAddData.timeMin = { + node.dialogAddData.time.start.min = { ts : dv.getTime(), data : d, text : bdDateToTime(dv) }; } else { - node.dialogAddData.timeMin = { + node.dialogAddData.time.start.min = { ts : 0, data : null, text : bdDateToTime(d) }; } - $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeStartMin').attr('title', node.dialogAddData.time.start.min.text); + $dialogTimeStartMinInput.attr('timedata', node.dialogAddData.time.start.min.ts); _dialogGenHelpText(); }, { nodeId: node.id, @@ -3189,13 +3317,13 @@ const opType = $dialogTimeStartMaxInput.typedInput('type'); if (opType === types.Undefined.value) { $('#dialog-row-offset-timeStartMax').hide(); - node.dialogAddData.timeMax = { + node.dialogAddData.time.start.max = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.time.start.max.text); + $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.time.start.max.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || @@ -3208,20 +3336,20 @@ getBackendData(d => { if (d.value && d.value !== 'none') { const dv = new Date(d.value); - node.dialogAddData.timeMax = { + node.dialogAddData.time.start.max = { ts : dv.getTime(), data : d, text : bdDateToTime(dv) }; } else { - node.dialogAddData.timeMax = { + node.dialogAddData.time.start.max = { ts : 0, data : null, text : bdDateToTime(d) }; } - $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeStartMax').attr('title', node.dialogAddData.time.start.max.text); + $dialogTimeStartMaxInput.attr('timedata', node.dialogAddData.time.start.max.ts); _dialogGenHelpText(); }, { nodeId: node.id, @@ -3331,13 +3459,13 @@ const opType = $dialogTimeEndMinInput.typedInput('type'); if (opType === types.Undefined.value) { $('#dialog-row-timeEndMin-offset').hide(); - node.dialogAddData.timeMin = { + node.dialogAddData.time.end.min = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.time.end.min.text); + $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.time.end.min.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || @@ -3350,20 +3478,20 @@ getBackendData(d => { if (d.value && d.value !== 'none') { const dv = new Date(d.value); - node.dialogAddData.timeMin = { + node.dialogAddData.time.end.min = { ts : dv.getTime(), data : d, text : bdDateToTime(dv) }; } else { - node.dialogAddData.timeMin = { + node.dialogAddData.time.end.min = { ts : 0, data : null, text : bdDateToTime(d) }; } - $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.timeMin.text); - $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.timeMin.ts); + $('#dialog-row-timeEndMin').attr('title', node.dialogAddData.time.end.min.text); + $dialogTimeEndMinInput.attr('timedata', node.dialogAddData.time.end.min.ts); _dialogGenHelpText(); }, { nodeId: node.id, @@ -3388,13 +3516,13 @@ const opType = $dialogTimeEndMaxInput.typedInput('type'); if (opType === types.Undefined.value) { $('#dialog-row-timeEndMax-offset').hide(); - node.dialogAddData.timeMax = { + node.dialogAddData.time.end.max = { ts : 0, data : null, text : 'N/A' }; - $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.time.end.max.text); + $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.time.end.max.ts); _dialogGenHelpText(); return; } else if (opType === types.TimeEntered.value || @@ -3407,20 +3535,20 @@ getBackendData(d => { if (d.value && d.value !== 'none') { const dv = new Date(d.value); - node.dialogAddData.timeMax = { + node.dialogAddData.time.end.max = { ts : dv.getTime(), data : d, text : bdDateToTime(dv) }; } else { - node.dialogAddData.timeMax = { + node.dialogAddData.time.end.max = { ts : 0, data : null, text : bdDateToTime(d) }; } - $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.timeMax.text); - $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.timeMax.ts); + $('#dialog-row-timeEndMax').attr('title', node.dialogAddData.time.end.max.text); + $dialogTimeEndMaxInput.attr('timedata', node.dialogAddData.time.end.max.ts); _dialogGenHelpText(); }, { nodeId: node.id, @@ -3467,6 +3595,8 @@ dialogDataIndex = containerIndex; const data = JSON.parse($dialogDataRow.data('ruleData')); $dialogName.val(data.name); + $dialogExec.val(data.exec); + $dialogRuleEnableBtn.attr('is_enabled', data.enabled); $dialogRuleEnableBtn.trigger('setRuleEnabledState'); dialogDataOnLoading = true; @@ -3505,35 +3635,35 @@ if (data.time) { if (data.time.start) { - $dialogTimeStartRegMultiplier.val(data.time.multiplier); - setTInputValue($dialogTimeStartRegInput, data.time.value, data.time.type); - setTInputValue($dialogTimeStartRegOffset, data.time.offset, data.time.offsetType); + $dialogTimeStartRegMultiplier.val(data.time.start.multiplier); + setTInputValue($dialogTimeStartRegInput, data.time.start.value, data.time.start.type); + setTInputValue($dialogTimeStartRegOffset, data.time.start.offset, data.time.start.offsetType); if (data.time.start.min) { - $dialogTimeStartMinMultiplier.val(data.timeMin.multiplier || 60000); - setTInputValue($dialogTimeStartMinInput, data.timeMin.value, data.timeMin.type); - setTInputValue($dialogTimeStartMinOffset, data.timeMin.offset, data.timeMin.offsetType); + $dialogTimeStartMinMultiplier.val(data.time.start.min.multiplier || 60000); + setTInputValue($dialogTimeStartMinInput, data.time.start.min.value, data.time.start.min.type); + setTInputValue($dialogTimeStartMinOffset, data.time.start.min.offset, data.time.start.min.offsetType); } if (data.time.start.max) { - $dialogTimeStartMaxMultiplier.val(data.timeMax.multiplier || 60000); - setTInputValue($dialogTimeStartMaxInput, data.timeMax.value, data.timeMax.type); - setTInputValue($dialogTimeStartMaxOffset, data.timeMax.offset, data.timeMax.offsetType); + $dialogTimeStartMaxMultiplier.val(data.time.start.max.multiplier || 60000); + setTInputValue($dialogTimeStartMaxInput, data.time.start.max.value, data.time.start.max.type); + setTInputValue($dialogTimeStartMaxOffset, data.time.start.max.offset, data.time.start.max.offsetType); } } if (data.time.end) { - $dialogTimeEndRegMultiplier.val(data.time.multiplier); - setTInputValue($dialogTimeEndRegInput, data.time.value, data.time.type); - setTInputValue($dialogTimeEndRegOffset, data.time.offset, data.time.offsetType); + $dialogTimeEndRegMultiplier.val(data.time.end.multiplier); + setTInputValue($dialogTimeEndRegInput, data.time.end.value, data.time.end.type); + setTInputValue($dialogTimeEndRegOffset, data.time.end.offset, data.time.end.offsetType); if (data.time.end.min) { - $dialogTimeEndMinMultiplier.val(data.timeMin.multiplier || 60000); - setTInputValue($dialogTimeEndMinInput, data.timeMin.value, data.timeMin.type); - setTInputValue($dialogTimeEndMinOffset, data.timeMin.offset, data.timeMin.offsetType); + $dialogTimeEndMinMultiplier.val(data.time.end.min.multiplier || 60000); + setTInputValue($dialogTimeEndMinInput, data.time.end.min.value, data.time.end.min.type); + setTInputValue($dialogTimeEndMinOffset, data.time.end.min.offset, data.time.end.min.offsetType); } if (data.time.end.max) { - $dialogTimeEndMaxMultiplier.val(data.timeMax.multiplier || 60000); - setTInputValue($dialogTimeEndMaxInput, data.timeMax.value, data.timeMax.type); - setTInputValue($dialogTimeEndMaxOffset, data.timeMax.offset, data.timeMax.offsetType); + $dialogTimeEndMaxMultiplier.val(data.time.end.max.multiplier || 60000); + setTInputValue($dialogTimeEndMaxInput, data.time.end.max.value, data.time.end.max.type); + setTInputValue($dialogTimeEndMaxOffset, data.time.end.max.offset, data.time.end.max.offsetType); } } @@ -3611,6 +3741,22 @@ $dialog.dialog('open'); } + /** + * checks if all properties of an object are true + */ + function checkObj(obj, path) { + for (const [key, value] of Object.entries(obj)) { + if (value === true || key === 'index') continue; // return path; + if (typeof value === 'object') { + const val = checkObj(value, path + key + '.'); + if (val !== '') return val; + continue; + } + return path + key; + } + return ''; + } + /** * Get the data object for a rule */ @@ -3618,10 +3764,12 @@ const result = { index: dialogDataIndex, name:$dialogName.val(), + exec: $dialogExec.val(), version: ruleVersion, enabled: !($dialogRuleEnableBtn.attr('is_enabled') === false || $dialogRuleEnableBtn.attr('is_enabled') === 'false'), - isValid: true, - valid:{ + isValid: false, + validationError: '', + validation:{ conditions : [] }, conditions: [], @@ -3639,6 +3787,7 @@ resetOverwrite: $dialogResetOverwrite.is(':checked'), importance: parseInt($dialogImportance.val()) }; + try { const setval = (param, name, val) => { if (val) param[name] = val; }; if (result.level.type === types.LevelND.value) { @@ -3688,34 +3837,38 @@ delete p.conditionText; } result.conditions.push(p); - const isCValid = { + result.validation.conditions.push({ index : _i, value : $cond.find('.dlg-ip-btctl-rule-condOperand').typedInput('validate'), threshold : $cond.find('.dlg-ip-btctl-rule-condThreshold').typedInput('validate') - }; - result.valid.conditions.push(isCValid); - result.isValid = result.isValid && isCValid.value && isCValid.threshold; + }); }); const timeTypeStart = $dialogTimeStartRegInput.typedInput('type'); const timeTypeEnd = $dialogTimeEndRegInput.typedInput('type'); - result.valid.timeStart = $dialogTimeStartRegInput.typedInput('validate'); - result.valid.timeEnd = $dialogTimeEndRegInput.typedInput('validate'); if (timeTypeStart !== 'none' || timeTypeEnd !== 'none') { result.time = { }; + result.validation.time = { + start : { + value : $dialogTimeStartRegInput.typedInput('validate') + }, + end : { + value : $dialogTimeEndRegInput.typedInput('validate') + } + }; + if (timeTypeStart !== 'none') { result.time.start = { type : timeTypeStart, value : $dialogTimeStartRegInput.typedInput('value'), offsetType : $dialogTimeStartRegOffset.typedInput('type'), offset : $dialogTimeStartRegOffset.typedInput('value'), - multiplier : parseInt($dialogTimeStartRegMultiplier.val()), - days : getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7), - months : getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) + multiplier : parseInt($dialogTimeStartRegMultiplier.val()) }; - result.valid.timeStart = result.valid.timeStart && $dialogTimeStartRegOffset.typedInput('validate') && result.time.multiplier; + result.validation.time.start.offset = $dialogTimeStartRegOffset.typedInput('validate'); + result.validation.time.start.multiplier = !isNaN(result.time.start.multiplier); const minType = $dialogTimeStartMinInput.typedInput('type'); - result.valid.timeStartMin = $dialogTimeStartMinInput.typedInput('validate'); + result.validation.time.start.min = $dialogTimeStartMinInput.typedInput('validate'); if (minType !== 'none') { result.time.start.min = { type : minType, @@ -3724,12 +3877,11 @@ offset : $dialogTimeStartMinOffset.typedInput('value'), multiplier : parseInt($dialogTimeStartMinMultiplier.val()) }; - result.valid.timeStartMin = result.valid.timeStartMin && - $dialogTimeStartMinOffset.typedInput('validate') && - result.timeMin.multiplier; + result.validation.time.start.minOffset = $dialogTimeStartMinOffset.typedInput('validate'); + result.validation.time.start.minMultiplier = !isNaN(result.time.start.min.multiplier); } const maxType = $dialogTimeStartMaxInput.typedInput('type'); - result.valid.timeStartMax = $dialogTimeStartMaxInput.typedInput('validate'); + result.validation.time.start.max = $dialogTimeStartMaxInput.typedInput('validate'); if (maxType !== 'none') { result.time.start.max = { type : maxType, @@ -3738,10 +3890,9 @@ offset : $dialogTimeStartMaxOffset.typedInput('value'), multiplier : parseInt($dialogTimeStartMaxMultiplier.val()) }; - result.valid.timeStartMax =result.valid.timeStartMax && - result.timeMax.multiplier; + result.validation.time.start.maxOffset = $dialogTimeStartMaxOffset.typedInput('validate'); + result.validation.time.start.maxMultiplier = !isNaN(result.time.start.max.multiplier); } - result.isValid = result.isValid && result.valid.timeStart && result.valid.timeStartMin && result.valid.timeStartMax; } if (timeTypeEnd !== 'none') { result.time.end = { @@ -3749,13 +3900,12 @@ value : $dialogTimeEndRegInput.typedInput('value'), offsetType : $dialogTimeEndRegOffset.typedInput('type'), offset : $dialogTimeEndRegOffset.typedInput('value'), - multiplier : parseInt($dialogTimeEndRegMultiplier.val()), - days : getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7), - months : getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12) + multiplier : parseInt($dialogTimeEndRegMultiplier.val()) }; - result.valid.timeEnd = result.valid.timeEnd && $dialogTimeEndRegOffset.typedInput('validate') && result.time.multiplier; + result.validation.time.end.offset = $dialogTimeEndRegOffset.typedInput('validate'); + result.validation.time.end.multiplier = !isNaN(result.time.end.multiplier); const minType = $dialogTimeEndMinInput.typedInput('type'); - result.valid.timeEndMin = $dialogTimeEndMinInput.typedInput('validate'); + result.validation.time.end.min = $dialogTimeEndMinInput.typedInput('validate'); if (minType !== 'none') { result.time.end.min = { type : minType, @@ -3764,12 +3914,11 @@ offset : $dialogTimeEndMinOffset.typedInput('value'), multiplier : parseInt($dialogTimeEndMinMultiplier.val()) }; - result.valid.timeEndMin = result.valid.timeEndMin && - $dialogTimeEndMinOffset.typedInput('validate') && - result.timeMin.multiplier; + result.validation.time.end.minOffset = $dialogTimeEndMinOffset.typedInput('validate'); + result.validation.time.end.minMultiplier = !isNaN(result.time.end.min.multiplier); } const maxType = $dialogTimeEndMaxInput.typedInput('type'); - result.valid.timeEndMax = $dialogTimeEndMaxInput.typedInput('validate'); + result.validation.time.end.max = $dialogTimeEndMaxInput.typedInput('validate'); if (maxType !== 'none') { result.time.end.max = { type : maxType, @@ -3778,12 +3927,13 @@ offset : $dialogTimeEndMaxOffset.typedInput('value'), multiplier : parseInt($dialogTimeEndMaxMultiplier.val()) }; - result.valid.timeEndMax =result.valid.timeEndMax && - result.timeMax.multiplier; + result.validation.time.end.maxOffset = $dialogTimeEndMaxOffset.typedInput('validate'); + result.validation.time.end.maxMultiplier = !isNaN(result.time.end.max.multiplier); } - result.isValid = result.isValid && result.valid.timeEnd && result.valid.timeEndMin && result.valid.timeEndMax; } - result.valid.timeLimit = true; + + result.time.days = getCheckboxesStr($('#dlg-ip-btctl-rule-timeDays input[type=checkbox]:checked'),7); + result.time.months = getCheckboxesStr($('#dlg-ip-btctl-rule-timeMonths input[type=checkbox]:checked'), 12); setval(result.time,'onlyEvenDays', $dialogTimeLimitOnlyEvenDays.is(':checked')); setval(result.time,'onlyOddDays', $dialogTimeLimitOnlyOddDays.is(':checked')); setval(result.time,'onlyEvenWeeks', $dialogTimeLimitOnlyEvenWeeks.is(':checked')); @@ -3794,36 +3944,53 @@ if (result.time.onlyEvenDays && result.time.onlyOddDays) { delete result.time.onlyEvenDays; delete result.time.onlyOddDays; + result.validation.time.noEvenAndOddDaysAllowed = false; } if (result.time.onlyEvenWeeks && result.time.onlyOddWeeks) { delete result.time.onlyEvenWeeks; delete result.time.onlyOddWeeks; + result.validation.time.noEvenAndOddWeeksAllowed = false; + } + if (!result.time.days || !result.time.months) { + result.validation.time.noDaysAllowed = false; } if (!result.time.days || !result.time.months) { - result.valid.timeLimit = false; + result.validation.time.noMonthAllowed = false; } - result.isValid = result.isValid && result.valid.timeLimit; } - result.valid.level = (result.level.operator === cNBC_RULE_OP.off) || - (result.level.operator === cNBC_RULE_OP.slatOversteer) || - (result.level.operator === cNBC_RULE_OP.topicOversteer) || - $dialogLevel.typedInput('validate'); - result.valid.slat = (result.level.operator === cNBC_RULE_OP.off) || - (result.level.operator === cNBC_RULE_OP.topicOversteer) || - (result.level.operator === cNBC_RULE_OP.levelMaxOversteer) || - (result.level.operator === cNBC_RULE_OP.levelMinOversteer) || - $dialogSlat.typedInput('validate'); - result.isValid = result.isValid && result.valid.level && result.valid.slat; + switch (result.level.operator) { + case cNBC_RULE_OP.off: + break; + case cNBC_RULE_OP.topicOversteer: + result.validation.topicIsMissing = (result.topic !== ''); + break; + case cNBC_RULE_OP.levelAbsolute: + case cNBC_RULE_OP.levelMinOversteer: + case cNBC_RULE_OP.levelMaxOversteer: + result.validation.level = $dialogLevel.typedInput('validate'); + result.validation.slat = $dialogSlat.typedInput('validate'); + break; + case cNBC_RULE_OP.slatOversteer: + result.validation.slat = $dialogSlat.typedInput('validate'); + break; + default: + result.validation.ruleOperator = false; + break; + } + result.validationError = checkObj(result.validation, ''); + result.isValid = (result.validationError === ''); } catch (err) { console.log(err.message); // eslint-disable-line no-console console.log(err.stack); // eslint-disable-line no-console result.isValid = false; - result.valid.error = err.message; + result.validationError = err.message; } if (!result.isValid) { - console.log('rule validation:',result.isValid, result.valid); // eslint-disable-line + console.warn('Rule validation failed! Wrong element:', result.validationError, result.validation); // eslint-disable-line + console.log('Rule Data:', result); // eslint-disable-line } else { - delete result.valid; // if valid, not needed + delete result.validation; // if valid, not needed + // delete result.validationError; } return result; } @@ -4365,11 +4532,11 @@

- +
- + @@ -4379,7 +4546,7 @@
- + @@ -4389,17 +4556,17 @@
- +
- +
- + @@ -4409,7 +4576,7 @@
- + @@ -4419,7 +4586,7 @@
- + @@ -4502,6 +4669,11 @@

+
+ + +
@@ -4740,6 +4912,11 @@ width: 30%; /* width: 120px; */ float: left; } + .rdgtimer-rule-exec-span { + margin-right: 5px; + float: left; + box-shadow: 1px 1px 1px 1px rgb(0 0 0 / 20%); + } .rdgtimer-rule-description-span { font-size: 10px; margin-left: 30%; diff --git a/nodes/blind-control.js b/nodes/blind-control.js index 3e6fabb..d2f7cb0 100644 --- a/nodes/blind-control.js +++ b/nodes/blind-control.js @@ -236,9 +236,9 @@ module.exports = function (RED) { node.level.current = newPos; node.level.currentInverse = newPos; node.level.topic = msg.topic; - if (typeof msg.slat !== undefined && msg.slat !== null) { + if (typeof msg.slat !== 'undefined' && msg.slat !== null) { node.level.slat = msg.slat; - } else if (typeof msg.blindSlat !== undefined && msg.blindSlat !== null) { + } else if (typeof msg.blindSlat !== 'undefined' && msg.blindSlat !== null) { node.level.slat = msg.blindSlat; } else if (typeof msg.topic === 'string' && msg.topic.includes('slatOverwrite')) { node.level.slat = msg.payload; @@ -516,15 +516,8 @@ module.exports = function (RED) { const rule = node.rules.data[i]; // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noFrom=${rule.time.operator !== cRuleFrom} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`); if (!rule.enabled) { continue; } - if (rule.time && !rule.time.start) { continue; } + if (rule.time && !rule.time.end) { continue; } const res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); - /* - if (!rule.time || rule.time.operator === ctrlLib.cRuleTime.from) { - res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); - } else { - //node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== ctrlLib.cRuleTime.from) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); - } */ if (res) { // node.debug(`1. ruleSel ${rule.name} (${rule.pos}) data=${ util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }) }`); if (res.level.operator === ctrlLib.cRuleType.slatOversteer) { @@ -551,7 +544,7 @@ module.exports = function (RED) { const rule = node.rules.data[i]; // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noUntil=${rule.time.operator !== cRuleUntil} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`); if (!rule.enabled) { continue; } - if (rule.time && !rule.time.end) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from + if (rule.time && !rule.time.start) { continue; } // - From: timeOp === ctrlLib.cRuleTime.from const res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); if (res) { // node.debug(`2. ruleSel ${rule.name} (${rule.pos}) data=${ util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }) }`); @@ -574,31 +567,32 @@ module.exports = function (RED) { const rule = ctrlLib.checkRules(node, msg, oNow, tempData); if (ruleSel !== rule.ruleSel) { node.error('not equal result ruleSel!'); - node.debug('ruleSel ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('ruleOld ' + util.inspect(ruleSel, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); + node.debug('ruleNew ' + util.inspect(rule, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); } else { - node.debug('same rule selected ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('same rule selected ' + util.inspect(ruleSel, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('same rule selected '); + // node.debug('same rule selected ' + util.inspect(rule, { colors: true, compact: 10, depth: 4,, maxStringLength: 1000 breakLength: Infinity })); + // node.debug('same rule selected ' + util.inspect(ruleSel, { colors: true, compact: 10, depth: 4,, maxStringLength: 1000 breakLength: Infinity })); } - if (ruleSlatOvs !== rule.ruleSlatOvs) { + if (ruleSlatOvs !== rule.ruleSlatOvs && (ctrlLib.isNullOrUndefined(ruleSlatOvs) !== ctrlLib.isNullOrUndefined(rule.ruleSlatOvs))) { node.error('not equal result ruleSlatOvs!'); - node.debug('ruleSlatOvs ' + util.inspect(ruleSlatOvs, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('ruleSlatOvs ' + util.inspect(ruleSlatOvs, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule.ruleSlatOvs, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); } - if (ruleTopicOvs !== rule.ruleTopicOvs) { + if (ruleTopicOvs !== rule.ruleTopicOvs && (ctrlLib.isNullOrUndefined(ruleTopicOvs) !== ctrlLib.isNullOrUndefined(rule.ruleTopicOvs))) { node.error('not equal result ruleTopicOvs!'); - node.debug('ruleTopicOvs ' + util.inspect(ruleTopicOvs, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('ruleTopicOvs ' + util.inspect(ruleTopicOvs, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule.ruleTopicOvs, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); } - if (ruleSelMin !== rule.ruleSelMin) { + if (ruleSelMin !== rule.ruleSelMin && (ctrlLib.isNullOrUndefined(ruleSelMin) !== ctrlLib.isNullOrUndefined(rule.ruleSelMin))) { node.error('not equal result ruleSelMin!'); - node.debug('ruleSelMin ' + util.inspect(ruleSelMin, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('ruleSelMin ' + util.inspect(ruleSelMin, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule.ruleSelMin, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); } - if (ruleSelMax !== rule.ruleSelMax) { + if (ruleSelMax !== rule.ruleSelMax && (ctrlLib.isNullOrUndefined(ruleSelMax) !== ctrlLib.isNullOrUndefined(rule.ruleSelMax))) { node.error('not equal result ruleSelMax!'); - node.debug('ruleSelMax ' + util.inspect(ruleSelMax, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('rule ' + util.inspect(rule, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('ruleSelMax ' + util.inspect(ruleSelMax, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule.ruleSelMax, { colors: true, compact: 10, depth: 4, maxStringLength: 1000, breakLength: Infinity })); } const livingRuleData = {}; @@ -674,21 +668,30 @@ module.exports = function (RED) { if (!rule.time) { return; } + rule.timeData = { + start:{ + ts: Number.MIN_VALUE + }, + end:{ + ts: Number.MAX_VALUE + }, + now: oNow + }; if (rule.time.start) { - const numStart = ctrlLib.getRuleTimeData(node, msg, rule, 'start', oNow); - if (numStart > oNow.nowNr) { + ctrlLib.getRuleTimeData(node, msg, rule, 'start', oNow); + if (rule.timeData.start.ts > oNow.nowNr) { node.debug('autoTrigger set to rule ' + rule.pos + ' (start)'); - const diff = numStart - oNow.nowNr; + const diff = rule.timeData.start.ts - oNow.nowNr; node.autoTrigger.time = Math.min(node.autoTrigger.time, diff); node.autoTrigger.type = 2; // next rule return; } } if (rule.time.end) { - const numEnd = ctrlLib.getRuleTimeData(node, msg, rule, 'end', oNow); - if (numEnd > oNow.nowNr) { + ctrlLib.getRuleTimeData(node, msg, rule, 'end', oNow); + if (rule.timeData.end.ts > oNow.nowNr) { node.debug('autoTrigger set to rule ' + rule.pos + ' (end)'); - const diff = numEnd - oNow.nowNr; + const diff = rule.timeData.end.ts - oNow.nowNr; node.autoTrigger.time = Math.min(node.autoTrigger.time, diff); node.autoTrigger.type = 2; // next rule } @@ -736,15 +739,23 @@ module.exports = function (RED) { data.textShort = ruleSel.conditon.textShort; name = 'ruleCond'; } - if (ruleSel.time) { + if (ruleSel.time && ruleSel.timeData) { livingRuleData.time = ruleSel.timeData; - livingRuleData.time.timeLocal = node.positionConfig.toTimeString(ruleSel.timeData.value); - livingRuleData.time.timeLocalDate = node.positionConfig.toDateString(ruleSel.timeData.value); - livingRuleData.time.dateISO= ruleSel.timeData.value.toISOString(); - livingRuleData.time.dateUTC= ruleSel.timeData.value.toUTCString(); - data.timeOp = ruleSel.time.operatorText; - data.timeLocal = livingRuleData.time.timeLocal; - data.time = livingRuleData.time.dateISO; + if (livingRuleData.time.start) { + livingRuleData.time.start.timeLocal = node.positionConfig.toTimeString(ruleSel.timeData.start.value); + livingRuleData.time.start.timeLocalDate = node.positionConfig.toDateString(ruleSel.timeData.start.value); + livingRuleData.time.start.dateISO= ruleSel.timeData.start.value.toISOString(); + livingRuleData.time.start.dateUTC= ruleSel.timeData.start.value.toUTCString(); + } + if (livingRuleData.time.end) { + livingRuleData.time.end.timeLocal = node.positionConfig.toTimeString(ruleSel.timeData.end.value); + livingRuleData.time.end.timeLocalDate = node.positionConfig.toDateString(ruleSel.timeData.end.value); + livingRuleData.time.end.dateISO= ruleSel.timeData.end.value.toISOString(); + livingRuleData.time.end.dateUTC= ruleSel.timeData.end.value.toUTCString(); + } + // data.timeOp = ruleSel.time.operatorText; + // data.timeLocal = livingRuleData.time.timeLocal; + // data.time = livingRuleData.time.dateISO; name = (ruleSel.conditional) ? 'ruleTimeCond' : 'ruleTime'; } livingRuleData.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.'+name, data); @@ -905,14 +916,17 @@ module.exports = function (RED) { node.nodeData.overwrite.expireDuration = parseFloat(hlp.chkValueFilled(config.overwriteExpire, NaN)); if (node.nodeData.levelTop < node.nodeData.levelBottom) { + [node.nodeData.levelTop, node.nodeData.levelBottom] = [node.nodeData.levelBottom, node.nodeData.levelTop]; + [node.nodeData.levelTopOffset, node.nodeData.levelBottomOffset] = [node.nodeData.levelBottomOffset, node.nodeData.levelTopOffset]; + node.levelReverse = true; + /* let tmp = node.nodeData.levelBottom; node.nodeData.levelBottom = node.nodeData.levelTop; node.nodeData.levelTop = tmp; - tmp = node.nodeData.levelBottomOffset; node.nodeData.levelBottomOffset = node.nodeData.levelTopOffset; node.nodeData.levelTopOffset = tmp; - node.levelReverse = true; + */ } node.nodeData.levelBottomSun = node.nodeData.levelBottom + node.nodeData.levelBottomOffset; if (node.nodeData.levelBottomSun < node.nodeData.levelBottom || node.nodeData.levelBottomSun > node.nodeData.levelTop) { diff --git a/nodes/clock-timer.html b/nodes/clock-timer.html index ac58b92..23daea7 100644 --- a/nodes/clock-timer.html +++ b/nodes/clock-timer.html @@ -144,7 +144,8 @@ env: 'Environment variable', jsonata: 'result of jsonata expression', nodeId: 'additional node ID', - nodeName: this.name + nodeName: this.name, + nodePath: this._path }; // if only payload and topic - display payload type @@ -1280,7 +1281,8 @@ types.randmNumCachedDay, types.randmNumCachedWeek, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($propertyValue, prop.v, prop.vt); @@ -1524,7 +1526,8 @@ types.DayOfYear, types.DayOfYearEven, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($dialogCondOperand, data.value, data.valueType); diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index 109083d..b664e18 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -176,7 +176,7 @@ function angleNormRad(angle) { * @returns {string} id of the given node */ function getNodeId(node) { - return '[' + node.type + ((node.name) ? '/' + node.name + ':' : ':') + node.id + ']'; + return '[' + node.type + ((node.name) ? '/' + node.name + ':' : ':') + (node._path || node.id) + ']'; } /*******************************************************************************************************/ /** diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index 9d610f2..87a5d6c 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -27,8 +27,14 @@ const cRuleLogOperatorAnd = 2; const cRuleLogOperatorOr = 1; const cNBC_RULE_TYPE_UNTIL = 0; const cNBC_RULE_TYPE_FROM = 1; +const cNBC_RULE_EXEC = { + auto: 0, + first:1, + last:2 +}; module.exports = { + isNullOrUndefined, evalTempData, posOverwriteReset, setExpiringOverwrite, @@ -49,6 +55,15 @@ module.exports = { let RED = null; /******************************************************************************************/ +/** + * Returns true if the given object is null or undefined. Otherwise, returns false. + * @param {*} object object to check + * @returns {boolean} true if the given object is null or undefined. Otherwise, returns false. + */ +function isNullOrUndefined(object) { + return (object === null || typeof object === 'undefined'); // isNullOrUndefined(object) +} + /** * evaluate temporary Data * @param {*} node node Data @@ -60,7 +75,7 @@ let RED = null; function evalTempData(node, type, value, data, tempData) { // node.debug(`evalTempData type=${type} value=${value} data=${data}`); const name = `${type}.${value}`; - if (data === null || typeof data === 'undefined') { + if (isNullOrUndefined(data)) { if (typeof tempData[name] !== 'undefined') { if (type !== 'PlT') { node.log(RED._('node-red-contrib-sun-position/position-config:errors.usingTempValue', { type, value, usedValue: tempData[name] })); @@ -216,6 +231,9 @@ function setOverwriteReason(node) { function prepareRules(node, msg, tempData, dNow) { for (let i = 0; i < node.rules.count; ++i) { const rule = node.rules.data[i]; + if (rule.time) { + delete rule.timeData; + } if (rule.conditional) { rule.conditon = { result : false @@ -270,69 +288,78 @@ function prepareRules(node, msg, tempData, dNow) { /** * get time constrainty of a rule - * @param {*} node node data - * @param {*} msg the message object - * @param {*} rule the rule data + * @param {Object} node node data + * @param {Object} msg the message object + * @param {Object} rule the rule data * @param {string} timep rule type * @param {number} dNow base timestamp * @return {number} timestamp of the rule */ function getRuleTimeData(node, msg, rule, timep, dNow) { - if (!rule.time[timep]) { - return -1; + if (!rule.time[timep] || rule.timeData[timep]) { + return; } rule.time[timep].now = dNow; - rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time[timep]); - if (rule.timeData.error) { - hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeData.error }), undefined, rule.timeData.error); - return -1; - } else if (!rule.timeData.value) { + rule.timeData[timep] = node.positionConfig.getTimeProp(node, msg, rule.time[timep]); + + if (rule.timeData[timep].error) { + hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeData[timep].error }), undefined, rule.timeData[timep].error); + node.debug('rule data complete'); + node.debug(util.inspect(rule, { colors: true, compact: 10, depth: 10, breakLength: Infinity })); + return; + } else if (!rule.timeData[timep].value) { throw new Error('Error can not calc time!'); } - rule.timeData.source = 'Default'; - rule.timeData.ts = rule.timeData.value.getTime(); - // node.debug(`time=${rule.timeData.value} -> ${new Date(rule.timeData.value)}`); - rule.timeData.dayId = hlp.getDayId(rule.timeData.value); + rule.timeData[timep].source = 'Default'; + rule.timeData[timep].ts = rule.timeData[timep].value.getTime(); + // node.debug(`time=${rule.timeData[timep].value} -> ${new Date(rule.timeData[timep].value)}`); if (rule.time[timep].min) { rule.time[timep].min.now = dNow; - rule.timeDataMin = node.positionConfig.getTimeProp(node, msg, rule.time[timep].min); - const numMin = rule.timeDataMin.value.getTime(); - rule.timeDataMin.source = 'Min'; - if (rule.timeDataMin.error) { - hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeDataMin.error }), undefined, rule.timeDataAlt.error); - } else if (!rule.timeDataMin.value) { + if (!rule.timeDataMin) rule.timeDataMin = { start:{}, end:{} }; + rule.timeDataMin[timep] = node.positionConfig.getTimeProp(node, msg, rule.time[timep].min); + rule.timeDataMin[timep].ts = rule.timeDataMin[timep].value.getTime(); + rule.timeDataMin[timep].source = 'Min'; + if (rule.timeDataMin[timep].error) { + hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeDataMin[timep].error }), undefined, rule.timeDataAlt.error); + } else if (!rule.timeDataMin[timep].value) { throw new Error('Error can not calc Alt time!'); } else { - if (numMin > rule.timeData.ts) { - const tmp = rule.timeData; - rule.timeData = rule.timeDataMin; - rule.timeDataMin = tmp; - rule.timeData.ts = numMin; - rule.timeData.dayId = hlp.getDayId(rule.timeDataMin.value); + if (rule.timeDataMin[timep].ts > rule.timeData[timep].ts) { + [rule.timeData[timep], rule.timeDataMin[timep]] = [rule.timeDataMin[timep], rule.timeData[timep]]; + /* + const tmp = rule.timeData[timep]; + rule.timeData[timep] = rule.timeDataMin[timep]; + rule.timeDataMin[timep] = tmp; + // rule.timeData[timep].dayId = hlp.getDayId(rule.timeDataMin[timep].value); + */ } } } if (rule.time[timep].max) { rule.time[timep].max.now = dNow; - rule.timeDataMax = node.positionConfig.getTimeProp(node, msg, rule.time[timep].max); - const numMax = rule.timeDataMax.value.getTime(); - rule.timeDataMax.source = 'Max'; - if (rule.timeDataMax.error) { - hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeDataMax.error }), undefined, rule.timeDataAlt.error); - } else if (!rule.timeDataMax.value) { + if (!rule.timeDataMax) rule.timeDataMax = { start:{}, end:{} }; + rule.timeDataMax[timep] = node.positionConfig.getTimeProp(node, msg, rule.time[timep].max); + rule.timeDataMax[timep].ts = rule.timeDataMax[timep].value.getTime(); + rule.timeDataMax[timep].source = 'Max'; + if (rule.timeDataMax[timep].error) { + hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeDataMax[timep].error }), undefined, rule.timeDataAlt.error); + } else if (!rule.timeDataMax[timep].value) { throw new Error('Error can not calc Alt time!'); } else { - if (numMax < rule.timeData.ts) { - const tmp = rule.timeData; - rule.timeData = rule.timeDataMax; - rule.timeDataMax = tmp; - rule.timeData.ts = numMax; - rule.timeData.dayId = hlp.getDayId(rule.timeDataMax.value); + if (rule.timeDataMax[timep].ts < rule.timeData[timep].ts) { + [rule.timeData[timep], rule.timeDataMax[timep]] = [rule.timeDataMax[timep], rule.timeData[timep]]; + /* + const tmp = rule.timeData[timep]; + rule.timeData[timep] = rule.timeDataMax[timep]; + rule.timeDataMax[timep] = tmp; + // rule.timeData[timep].dayId = hlp.getDayId(rule.timeDataMax[timep].value); + */ } } } - return rule.timeData.ts; + rule.timeData[timep].dayId = hlp.getDayId(rule.timeData[timep].value); + return; } /*************************************************************************************************************************/ @@ -382,107 +409,92 @@ function compareRules(node, msg, rule, cmp, data) { if (!rule.time) { return rule; } - let numStart = Number.MIN_VALUE; - let numEnd = Number.MAX_VALUE; - if (rule.time.start) { - if (rule.time.start.days && !rule.time.start.days.includes(data.dayNr)) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid days`); - return null; - } - if (rule.time.start.months && !rule.time.start.months.includes(data.monthNr)) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid month`); - return null; - } - if (rule.time.start.onlyOddDays && (data.dateNr % 2 === 0)) { // even - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even days`); - return null; - } - if (rule.time.start.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd days`); - return null; - } - if (rule.time.start.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even week`); - return null; - } - if (rule.time.start.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd week`); - return null; + if (rule.time.days && !rule.time.days.includes(data.dayNr)) { + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid days`); + return null; + } + if (rule.time.months && !rule.time.months.includes(data.monthNr)) { + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid month`); + return null; + } + if (rule.time.onlyOddDays && (data.dateNr % 2 === 0)) { // even + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even days`); + return null; + } + if (rule.time.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd days`); + return null; + } + if (rule.time.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even week`); + return null; + } + if (rule.time.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd week`); + return null; + } + if (rule.time.dateStart || rule.time.dateEnd) { + rule.time.dateStart.setFullYear(data.yearNr); + rule.time.dateEnd.setFullYear(data.yearNr); + if (rule.time.dateEnd > rule.time.dateStart) { + // in the current year + if (data.now < rule.time.dateStart || data.now > rule.time.dateEnd) { + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range within year`); + return null; + } + } else { + // switch between year from end to start + if (data.now < rule.time.dateStart && data.now > rule.time.dateEnd) { + node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range over year`); + return null; + } } - if (rule.time.start.dateStart || rule.time.start.dateEnd) { - rule.time.start.dateStart.setFullYear(data.yearNr); - rule.time.start.dateEnd.setFullYear(data.yearNr); - if (rule.time.start.dateEnd > rule.time.start.dateStart) { - // in the current year - if (data.now < rule.time.start.dateStart || data.now > rule.time.start.dateEnd) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range within year`); - return null; + } + + rule.timeData = { + start:{ + ts: Number.MIN_VALUE + }, + end:{ + ts: Number.MAX_VALUE + }, + now: data.now + }; + + if (rule.time.start) { + getRuleTimeData(node, msg, rule, 'start', data.now, Number.MIN_VALUE); + if (rule.time.end) { + getRuleTimeData(node, msg, rule, 'end', data.now, Number.MAX_VALUE); + if (rule.timeData.start.ts > rule.timeData.end.ts) { + if (data.dayId === rule.timeData.start.dayId && + rule.timeData.start.ts <= data.nowNr) { + return rule; } - } else { - // switch between year from end to start - if (data.now < rule.time.start.dateStart && data.now > rule.time.start.dateEnd) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range over year`); - return null; + if (data.dayId === rule.timeData.end.dayId && + rule.timeData.end.ts > data.nowNr) { + return rule; } + return null; + } + if (data.dayId !== rule.timeData.end.dayId) { + return null; } } - numStart = getRuleTimeData(node, msg, rule, 'start', data.now); - } else if (rule.time.end) { - if (rule.time.end.days && !rule.time.end.days.includes(data.dayNr)) { - return null; - } - if (rule.time.end.months && !rule.time.end.months.includes(data.monthNr)) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid month`); - return null; - } - if (rule.time.end.onlyOddDays && (data.dateNr % 2 === 0)) { // even - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even days`); - return null; - } - if (rule.time.end.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd days`); - return null; - } - if (rule.time.end.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even week`); + if (data.dayId !== rule.timeData.start.dayId) { return null; } - if (rule.time.end.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd week`); + } else if (rule.time.end) { + getRuleTimeData(node, msg, rule, 'end', data.now, Number.MAX_VALUE); + if (data.dayId !== rule.timeData.end.dayId) { return null; } - if (rule.time.end.dateStart || rule.time.end.dateEnd) { - rule.time.end.dateStart.setFullYear(data.yearNr); - rule.time.end.dateEnd.setFullYear(data.yearNr); - if (rule.time.end.dateEnd > rule.time.end.dateStart) { - // in the current year - if (data.now < rule.time.end.dateStart || data.now > rule.time.end.dateEnd) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range within year`); - return null; - } - } else { - // switch between year from end to start - if (data.now < rule.time.end.dateStart && data.now > rule.time.end.dateEnd) { - node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range over year`); - return null; - } - } - } - numEnd = getRuleTimeData(node, msg, rule, 'end', data.now); - } else { - return null; - } - if (numStart > numEnd) { - if (data.dayId === rule.timeData.dayId && numStart >=0 && numStart <= data.nowNr && numEnd > data.nowNr) { - return rule; - } } - // node.debug(`compareRules ${rule.name} (${rule.pos}) type=${rule.time.operatorText} - ${rule.time.value} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`); - if (data.dayId === rule.timeData.dayId && numStart >=0 && numStart <= data.nowNr && numEnd > data.nowNr) { + if (rule.timeData.start.ts <= data.nowNr && + rule.timeData.end.ts > data.nowNr) { return rule; } - // node.debug(`compareRules rule ${rule.name} (${rule.pos}) dayId=${data.dayId} rule-DayID=${rule.timeData.dayId} num=${num} cmp=${cmp(num)} invalid time`); + // node.debug(`compareRules rule ${rule.name} (${rule.pos}) dayId=${data.dayId} rule-DayID=${rule.timeData[timep].dayId} num=${num} cmp=${cmp(num)} invalid time`); return null; } /******************************************************************************************/ @@ -506,9 +518,7 @@ function checkRules(node, msg, oNow, tempData) { for (let i = 0; i <= node.rules.lastUntil; ++i) { const rule = node.rules.data[i]; // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - if (!rule.enabled) { continue; } - if (rule.time && !rule.time.end) { continue; } - // const res = fktCheck(rule, r => (r >= nowNr)); + if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.last) { continue; } const res = compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); // now is less time if (res) { // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); @@ -533,8 +543,7 @@ function checkRules(node, msg, oNow, tempData) { for (let i = (node.rules.count - 1); i >= 0; --i) { const rule = node.rules.data[i]; // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); - if (!rule.enabled) { continue; } - if (rule.time && !rule.time.start) { continue; } + if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.first) { continue; } const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater time if (res) { // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); @@ -670,13 +679,16 @@ function initializeCtrl(REDLib, node, config) { // Prepare Rules node.rules.count = node.rules.data.length; node.rules.lastUntil = node.rules.count -1; + node.rules.firstFrom = node.rules.lastUntil; node.rules.firstTimeLimited = node.rules.count; node.rules.maxImportance = 0; node.rules.canResetOverwrite = false; - + node.debug('all node.rules before convert'); + node.debug(util.inspect(node.rules, { colors: true, compact: 10, depth: 10, breakLength: Infinity })); for (let i = 0; i < node.rules.count; ++i) { const rule = node.rules.data[i]; rule.pos = i + 1; + rule.exec = rule.exec || cNBC_RULE_EXEC.auto; // Backward compatibility if (!rule.conditions) { rule.conditions = []; @@ -755,13 +767,13 @@ function initializeCtrl(REDLib, node, config) { multiplier : (parseInt(rule.multiplierMax) || 60000) }; } - if (rule.timeDays && rule.timeDays !== '*') rule.time[ttype].days = rule.timeDays; - if (rule.timeMonths && rule.timeMonths !== '*') rule.time[ttype].months = rule.timeMonths; - if (rule.timeOnlyOddDays) rule.time[ttype].onlyOddDays = rule.timeOnlyOddDays; - if (rule.timeOnlyEvenDays) rule.time[ttype].onlyEvenDays = rule.timeOnlyEvenDays; - if (rule.timeDateStart) rule.time[ttype].dateStart = rule.timeDateStart; - if (rule.timeDateEnd) rule.time[ttype].dateEnd = rule.timeDateEnd; } + if (rule.timeDays && rule.timeDays !== '*') rule.time.days = rule.timeDays; + if (rule.timeMonths && rule.timeMonths !== '*') rule.time.months = rule.timeMonths; + if (rule.timeOnlyOddDays) rule.time.onlyOddDays = rule.timeOnlyOddDays; + if (rule.timeOnlyEvenDays) rule.time.onlyEvenDays = rule.timeOnlyEvenDays; + if (rule.timeDateStart) rule.time.dateStart = rule.timeDateStart; + if (rule.timeDateEnd) rule.time.dateEnd = rule.timeDateEnd; delete rule.timeType; delete rule.timeValue; delete rule.timeOp; @@ -788,53 +800,43 @@ function initializeCtrl(REDLib, node, config) { delete rule.multiplierMax; delete rule.timeMaxOp; } - if (rule.time && rule.time.operator) { + if (rule.time && (typeof rule.time.operator !== 'undefined')) { let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL if (rule.time.operator === cNBC_RULE_TYPE_FROM) { ttype = 'start'; } - rule.time[ttype] = { + rule.time[ttype] = Object.assign({ type : rule.time.type, value : rule.time.value, offsetType : rule.time.offsetType, offset : rule.time.offset, multiplier : rule.time.multiplier - }; + }, rule.time[ttype]); if (rule.timeMin && rule.timeMin.type !== 'none' ) { - rule.time[ttype].min = { + rule.time[ttype].min = Object.assign({ type : rule.timeMin.type, value : (rule.timeMin.value || ''), offsetType : (rule.timeMin.offsetType || 'none'), offset : (rule.timeMin.offset || 1), multiplier : (parseInt(rule.timeMin.multiplier) || 60000) - }; + }, rule.time[ttype].min); } if (rule.timeMax && rule.timeMax.type !== 'none' ) { - rule.time[ttype].max = { + rule.time[ttype].max = Object.assign({ type : rule.timeMax.type, value : (rule.timeMax.value || ''), offsetType : (rule.timeMax.offsetType || 'none'), offset : (rule.timeMax.offset || 1), multiplier : (parseInt(rule.timeMax.multiplier) || 60000) - }; + }, rule.time[ttype].max); } - if (rule.time.days && rule.time.days !== '*') rule.time[ttype].days = rule.time.days; - if (rule.time.months && rule.time.months !== '*') rule.time[ttype].months = rule.time.months; - if (rule.time.onlyOddDays) rule.time[ttype].onlyOddDays = rule.time.onlyOddDays; - if (rule.time.onlyEvenDays) rule.time[ttype].onlyEvenDays = rule.time.onlyEvenDays; - if (rule.time.dateStart) rule.time[ttype].dateStart = rule.time.dateStart; - if (rule.time.dateEnd) rule.time[ttype].dateEnd = rule.time.dateEnd; + delete rule.time.operator; + delete rule.time.operatorText; delete rule.time.type; delete rule.time.value; delete rule.time.offsetType; delete rule.time.offset; delete rule.time.multiplier; - delete rule.time.days; - delete rule.time.months; - delete rule.time.onlyOddDays; - delete rule.time.onlyEvenDays; - delete rule.time.dateStart; - delete rule.time.dateEnd; delete rule.timeMin; delete rule.timeMax; } @@ -884,6 +886,11 @@ function initializeCtrl(REDLib, node, config) { if (rule.payload && !('next' in rule)) { rule.payload.next = true; } + rule.execUse = cNBC_RULE_EXEC.first; + if (rule.exec === cNBC_RULE_EXEC.last || (rule.time && rule.time.start && !rule.time.end)) { + rule.execUse = cNBC_RULE_EXEC.last; + } + /// check generic rule settings rule.name = rule.name || 'rule ' + rule.pos; rule.enabled = !(rule.enabled === false || rule.enabled === 'false'); @@ -898,58 +905,50 @@ function initializeCtrl(REDLib, node, config) { const checkTimeR = id => { if (rule.time[id].max) { rule.time[id].max.next = false; } if (rule.time[id].min) { rule.time[id].min.next = false; } - if (!rule.time[id].days || rule.time[id].days === '*') { - delete rule.time[id].days; - } else { - rule.time[id].days = rule.time[id].days.split(','); - rule.time[id].days = rule.time[id].days.map( e => parseInt(e) ); - } - if (!rule.time[id].months || rule.time[id].months === '*') { - delete rule.time[id].months; - } else { - rule.time[id].months = rule.time[id].months.split(','); - rule.time[id].months = rule.time[id].months.map( e => parseInt(e) ); - } - if (rule.time[id].onlyOddDays && rule.time[id].onlyEvenDays) { - delete rule.time[id].onlyOddDays; - delete rule.time[id].onlyEvenDays; - } - if (rule.time[id].onlyOddWeeks && rule.time[id].onlyEvenWeeks) { - delete rule.time[id].onlyOddWeeks; - delete rule.time[id].onlyEvenWeeks; - } - if (rule.time[id].dateStart || rule.time[id].dateEnd) { - if (rule.time[id].dateStart) { - rule.time[id].dateStart = new Date(rule.time[id].dateStart); - rule.time[id].dateStart.setHours(0, 0, 0, 1); - } else { - rule.time[id].dateStart = new Date(2000,0,1,0, 0, 0, 1); - } - if (rule.time[id].dateEnd) { - rule.time[id].dateEnd = new Date(rule.time[id].dateEnd); - rule.time[id].dateEnd.setHours(23, 59, 59, 999); - } else { - rule.time[id].dateEnd = new Date(2000,11,31, 23, 59, 59, 999); - } - } + rule.time[id].next = false; }; - rule.time.next = false; node.rules.firstTimeLimited = Math.min(i, node.rules.firstTimeLimited); if (rule.time.start) { // cNBC_RULE_TYPE_FROM + node.rules.firstFrom = Math.min(i, node.rules.firstFrom); checkTimeR('start'); } if (rule.time.end) { // cNBC_RULE_TYPE_UNTIL node.rules.lastUntil = i; checkTimeR('end'); } - if (rule.time.start && rule.time.end) { - // if both are defined, only use limitations on start - delete rule.time.end.days; - delete rule.time.end.months; - delete rule.time.end.onlyOddDays; - delete rule.time.end.onlyEvenDays; - delete rule.time.end.dateStart; - delete rule.time.end.dateEnd; + if (!rule.time.days || rule.time.days === '*') { + delete rule.time.days; + } else { + rule.time.days = rule.time.days.split(','); + rule.time.days = rule.time.days.map( e => parseInt(e) ); + } + if (!rule.time.months || rule.time.months === '*') { + delete rule.time.months; + } else { + rule.time.months = rule.time.months.split(','); + rule.time.months = rule.time.months.map( e => parseInt(e) ); + } + if (rule.time.onlyOddDays && rule.time.onlyEvenDays) { + delete rule.time.onlyOddDays; + delete rule.time.onlyEvenDays; + } + if (rule.time.onlyOddWeeks && rule.time.onlyEvenWeeks) { + delete rule.time.onlyOddWeeks; + delete rule.time.onlyEvenWeeks; + } + if (rule.time.dateStart || rule.time.dateEnd) { + if (rule.time.dateStart) { + rule.time.dateStart = new Date(rule.time.dateStart); + rule.time.dateStart.setHours(0, 0, 0, 1); + } else { + rule.time.dateStart = new Date(2000,0,1,0, 0, 0, 1); + } + if (rule.time.dateEnd) { + rule.time.dateEnd = new Date(rule.time.dateEnd); + rule.time.dateEnd.setHours(23, 59, 59, 999); + } else { + rule.time.dateEnd = new Date(2000,11,31, 23, 59, 59, 999); + } } } rule.conditions.forEach(cond => { @@ -985,6 +984,8 @@ function initializeCtrl(REDLib, node, config) { }); rule.conditional = rule.conditions.length > 0; } + node.debug('all node.rules after convert'); + node.debug(util.inspect(node.rules, { colors: true, compact: 10, depth: 10, breakLength: Infinity })); if (node.autoTrigger || (parseFloat(config.startDelayTime) > 9)) { let delay = parseFloat(config.startDelayTime) || (300 + Math.floor(Math.random() * 700)); // default = 300ms - 1s diff --git a/nodes/locales/de/position-config.json b/nodes/locales/de/position-config.json index d4eac16..3b970c5 100644 --- a/nodes/locales/de/position-config.json +++ b/nodes/locales/de/position-config.json @@ -43,6 +43,7 @@ "datespecific": "Zeitpunkt (erweitert)", "nodeId":"Node Name", "nodeName":"Node ID", + "nodePath":"Node Path", "timeentered": "Uhrzeit (nächste)", "dateentered": "Datum (spezieller Zeitpunkt)", "timepredefined": "Uhrzeit speziell", @@ -375,8 +376,6 @@ "editRule": "Regel editieren", "duplicateRule": "Regel duplizieren", "ruleState": "aktiviert oder deaktiviert die Regel", - "ruleTimeFrom": "↧ von", - "ruleTimeUntil": "↥ bis", "btnAdd": "hinzufügen", "btnSort": "sortieren", "btnClear": "leeren", diff --git a/nodes/locales/en-US/blind-control.json b/nodes/locales/en-US/blind-control.json index 85bd3b4..ec837d8 100644 --- a/nodes/locales/en-US/blind-control.json +++ b/nodes/locales/en-US/blind-control.json @@ -30,8 +30,6 @@ "sunFloorLength": "length on the floor", "sunMinDelta": "min delta", "sunTopic": "Topic 🌞/⛄", - "timeStart": "time Start", - "timeEnd": "time End", "name": "name", "oversteerValue": "oversteer", "oversteerThreshold": "Threshold", @@ -41,9 +39,6 @@ "oversteerSunInWindow": "only when sun shines in the window", "smoothTime": "smoothTime", "showEnhSettings": "Enhanced settings", - "offset": "time offset", - "offset-timeMin": "earliest (min) offset", - "offset-timeMax": "latest (max) offset", "dialogtitle": "Edit Rule", "autoTrigger": "auto trigger", "autoTrigger2": "dynamic, but at least every", diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/position-config.json index 309edb2..7dffd91 100644 --- a/nodes/locales/en-US/position-config.json +++ b/nodes/locales/en-US/position-config.json @@ -43,6 +43,7 @@ "datespecific":"timestamp enhanced", "nodeId":"node name", "nodeName":"node ID", + "nodePath":"node path", "timeentered":"time (next)", "dateentered":"date", "timepredefined":"fixed times", @@ -385,13 +386,27 @@ "ruleLevelMax": "⭱️❗ maximum (oversteer)", "ruleSlatOversteer": "Slat (oversteer)", "ruleTopicOversteer": "Topic (oversteer)", + "ruleTimeStart": "↧ from", + "ruleTimeEnd": "↥ until", + "ruleTimeMinOffset": "earliest (min) offset", + "ruleTimeMaxOffset": "latest (max) offset", + "ruleTimeOffset": "time offset", "ruleOff": "✋ send nothing", "ruleLevelND": "use default level (e.g. sun control)", "editRule": "edit the rule", "duplicateRule": "duplicate the rule", "ruleState": "enables or disables the rule", - "ruleTimeFrom": "↧ from", - "ruleTimeUntil": "↥ until", + "ruleExec":"order of evaluation", + "ruleExecAuto": "auto - rules with only '↧ from', last matching; all other first matching", + "ruleExecFirst": "⭳ first matching (evaluation first to last [1])", + "ruleExecLast": "⭱️ last matching (evaluation if no first matching, last to first)", + "ruleExecShort": [ + "", + "¹⭳", + "²⭱️" + ], + "ruleInValidElement":"invalid", + "ruleInValidText":"invalid Element", "btnAdd": "add", "btnSort": "sort", "btnClear": "clear", diff --git a/nodes/position-config.html b/nodes/position-config.html index 6a1c6b8..ccdf30c 100644 --- a/nodes/position-config.html +++ b/nodes/position-config.html @@ -421,6 +421,8 @@ return node._('node-red-contrib-sun-position/position-config:common.types.nodeId') + suffix; case 'nodeName': return node._('node-red-contrib-sun-position/position-config:common.types.nodeName') + suffix; + case 'nodePath': + return node._('node-red-contrib-sun-position/position-config:common.types.nodePath') + suffix; case 'PlT': return node._('node-red-contrib-sun-position/position-config:common.typeOptions.PlTRes',{topic: RED.nodes.getType('position-config').clipValueLength(v, l), suffix} ); case 'msgPayload': diff --git a/nodes/position-config.js b/nodes/position-config.js index 1bac335..f2b1816 100644 --- a/nodes/position-config.js +++ b/nodes/position-config.js @@ -512,7 +512,7 @@ module.exports = function (RED) { * @returns {string} formated Date object */ toDateTimeString(dt) { - return (this.toDateString(dt) + ' ' + this.toTimeString(dt)).trim(); + return (dt && this.toDateString(dt) + ' ' + this.toTimeString(dt)).trim(); } /** @@ -521,7 +521,7 @@ module.exports = function (RED) { * @returns {string} formated Date object */ toTimeString(dt) { - if (!this.tzOffset && this.stateTimeFormat === '3') { + if (dt && !this.tzOffset && this.stateTimeFormat === '3') { return dt.toLocaleTimeString(); } return hlp.getFormattedDateOut(dt, this.stateTimeFormat, (this.tzOffset === 0), this.tzOffset); @@ -533,7 +533,7 @@ module.exports = function (RED) { * @returns {string} formated Date object */ toDateString(dt) { - if (!this.tzOffset && this.stateDateFormat === '12') { + if (dt && !this.tzOffset && this.stateDateFormat === '12') { return dt.toLocaleDateString(); } return hlp.getFormattedDateOut(dt, this.stateDateFormat, (this.tzOffset === 0), this.tzOffset); @@ -810,7 +810,9 @@ module.exports = function (RED) { if (!hlp.isValidDate(dNow)) { dNow = new Date(); _srcNode.debug('getTimeProp: Date parameter not given or date Parameter ' + data.now + ' is invalid!!');} try { if (data.type === '' || data.type === 'none' || data.type === null || typeof data.type === 'undefined') { - result.error = 'wrong type "' + data.type + '"="' + data.value+'"'; + result.error = 'internal error - time-type is not defined (type="' + String(data.type) + '" value="' + String(data.value) + '")'; + _srcNode.error(result.error); + _srcNode.debug(util.inspect(data, {colors:true, compact:10})); } else if (data.type === 'date') { result.value = dNow; if (this.tzOffset) { @@ -1122,6 +1124,8 @@ module.exports = function (RED) { return _srcNode.addId || _srcNode.id; } else if (data.type === 'nodeName') { return _srcNode.name || _srcNode.id; // if empty fallback to node ID + } else if (data.type === 'nodePath') { + return _srcNode._path || _srcNode.id; // if empty fallback to node ID } else if (data.type === 'randmNumCachedDay') { const val = data.value.split(/((?:[1-9]|-0\.|0\.|-)\d*(?:\.\d+)?)/); return this.getCachedRandomDayNumber(_srcNode, parseFloat(val[1]), parseFloat(val[3]), dNow); diff --git a/nodes/static/htmlglobal.js b/nodes/static/htmlglobal.js index 0ef0bd4..ca415df 100644 --- a/nodes/static/htmlglobal.js +++ b/nodes/static/htmlglobal.js @@ -244,6 +244,11 @@ function getTypes(node) { // eslint-disable-line no-unused-vars label: node._('node-red-contrib-sun-position/position-config:common.types.nodeName','node name'), hasValue: false }, + nodePath: { + value: 'nodePath', + label: node._('node-red-contrib-sun-position/position-config:common.types.nodePath','node name'), + hasValue: false + }, TimeEntered: { value: 'entered', label: node._('node-red-contrib-sun-position/position-config:common.types.timeentered','time (next)'), diff --git a/nodes/time-comp.html b/nodes/time-comp.html index 275be15..1f264bd 100644 --- a/nodes/time-comp.html +++ b/nodes/time-comp.html @@ -623,7 +623,8 @@ types.randmNumCachedDay, types.randmNumCachedWeek, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($propertyValue, prop.v, prop.vt); diff --git a/nodes/time-inject.html b/nodes/time-inject.html index 866b1e4..c96d423 100644 --- a/nodes/time-inject.html +++ b/nodes/time-inject.html @@ -858,7 +858,8 @@ types.randmNumCachedDay, types.randmNumCachedWeek, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($propertyValue, prop.v, prop.vt); diff --git a/nodes/time-span.html b/nodes/time-span.html index 4001e5b..b75f763 100644 --- a/nodes/time-span.html +++ b/nodes/time-span.html @@ -600,7 +600,8 @@ types.randmNumCachedDay, types.randmNumCachedWeek, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); setTInputValue($propertyValue, prop.v, prop.vt); diff --git a/nodes/within-time-switch.html b/nodes/within-time-switch.html index e239e30..69c08f7 100644 --- a/nodes/within-time-switch.html +++ b/nodes/within-time-switch.html @@ -885,7 +885,8 @@ types.DayOfYear, types.DayOfYearEven, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); if (node.outOfTimeValueType === 'input') { @@ -932,7 +933,8 @@ types.DayOfYear, types.DayOfYearEven, types.nodeId, - types.nodeName + types.nodeName, + types.nodePath ] }); // #endregion Output Values diff --git a/nodes/within-time-switch.js b/nodes/within-time-switch.js index 67b6b03..b3aef0b 100644 --- a/nodes/within-time-switch.js +++ b/nodes/within-time-switch.js @@ -389,10 +389,10 @@ module.exports = function (RED) { this.timeOnlyOddWeeks = false; } - if (typeof config.timedatestart !== undefined && config.timedatestart !== '') { + if (typeof config.timedatestart !== 'undefined' && config.timedatestart !== '') { this.timeStartDate = new Date(config.timedatestart); } - if (typeof config.timedateend !== undefined && config.timedateend !== '') { + if (typeof config.timedateend !== 'undefined' && config.timedateend !== '') { this.timeEndDate = new Date(config.timedateend); } From 2f84b32af316b668722506937c84d40c85a07da2 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Mon, 31 Jan 2022 21:21:11 +0100 Subject: [PATCH 08/44] reworked topicReplace to textReplace --- nodes/blind-control.js | 22 +++++-------- nodes/clock-timer.js | 4 +-- nodes/lib/dateTimeHelper.js | 60 ++++++++++++++++++++++++++-------- nodes/lib/timeControlHelper.js | 6 ++-- nodes/position-config.js | 16 ++++++++- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/nodes/blind-control.js b/nodes/blind-control.js index d2f7cb0..8a1e545 100644 --- a/nodes/blind-control.js +++ b/nodes/blind-control.js @@ -531,9 +531,7 @@ module.exports = function (RED) { } else { ruleSel = res; ruleindex = i; - if (rule.time && rule.time.operator !== ctrlLib.cRuleTime.from) { - break; - } + break; } } } @@ -919,14 +917,6 @@ module.exports = function (RED) { [node.nodeData.levelTop, node.nodeData.levelBottom] = [node.nodeData.levelBottom, node.nodeData.levelTop]; [node.nodeData.levelTopOffset, node.nodeData.levelBottomOffset] = [node.nodeData.levelBottomOffset, node.nodeData.levelTopOffset]; node.levelReverse = true; - /* - let tmp = node.nodeData.levelBottom; - node.nodeData.levelBottom = node.nodeData.levelTop; - node.nodeData.levelTop = tmp; - tmp = node.nodeData.levelBottomOffset; - node.nodeData.levelBottomOffset = node.nodeData.levelTopOffset; - node.nodeData.levelTopOffset = tmp; - */ } node.nodeData.levelBottomSun = node.nodeData.levelBottom + node.nodeData.levelBottomOffset; if (node.nodeData.levelBottomSun < node.nodeData.levelBottom || node.nodeData.levelBottomSun > node.nodeData.levelTop) { @@ -1291,7 +1281,9 @@ module.exports = function (RED) { autoTrigger : node.autoTrigger, lastEvaluated: node.previousData.last, name: node.name || node.id, - id: node.addId || node.id + id: node.addId || node.id, + srcId: node.id, + path: node._path || node.id }; let ruleId = NaN; @@ -1405,6 +1397,8 @@ module.exports = function (RED) { const replaceAttrs = { name: blindCtrl.name, id: blindCtrl.id, + srcId: blindCtrl.srcId, + path: blindCtrl.path, level: blindCtrl.level, levelInverse: blindCtrl.levelInverse, slat: blindCtrl.slat, @@ -1417,7 +1411,7 @@ module.exports = function (RED) { payload: msg.payload }; if (topic) { - topic = hlp.topicReplace(topic, replaceAttrs); + topic = hlp.textReplace(topic, replaceAttrs, RED, msg); } if ((!node.startDelayTimeOut) && (!isNaN(node.level.current)) && @@ -1440,7 +1434,7 @@ module.exports = function (RED) { } else if (prop.type === 'ctrlObj') { resultObj = blindCtrl; } else if (prop.type === 'strPlaceholder') { - resultObj = hlp.topicReplace(''+prop.value, replaceAttrs); + resultObj = hlp.textReplace(''+prop.value, replaceAttrs, RED, msg); } else { resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.now); } diff --git a/nodes/clock-timer.js b/nodes/clock-timer.js index 902be88..ed58169 100644 --- a/nodes/clock-timer.js +++ b/nodes/clock-timer.js @@ -557,7 +557,7 @@ module.exports = function (RED) { payload: msg.payload }; if (topic) { - topic = hlp.topicReplace(topic, replaceAttrs); + topic = hlp.textReplace(topic, replaceAttrs, RED, msg); } if ((!node.startDelayTimeOut) && @@ -577,7 +577,7 @@ module.exports = function (RED) { } else if (prop.type === 'ctrlObj') { resultObj = timeCtrl; } else if (prop.type === 'strPlaceholder') { - resultObj = hlp.topicReplace(''+prop.value, replaceAttrs); + resultObj = hlp.textReplace(''+prop.value, replaceAttrs, RED, msg); } else { resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.now); } diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index b664e18..fc98790 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -49,7 +49,7 @@ module.exports = { initializeParser, getFormattedDateOut, parseDateFromFormat, - topicReplace, + textReplace, getNowTimeStamp, getNowObject }; @@ -2069,31 +2069,63 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u return res; } +/** + * get a value by a path + * @param {object} obj object to get path from + * @param {string} path path to property + * @returns the value of the property + */ +function getDeepValue(obj, path){ + for (var i=0, path=path.split('.'), len=path.length; i { - topicAttrsLower[k.toLowerCase()] = topicAttrs[k]; + const textAttrsLower = {}; + Object.keys(textAttrs).forEach(k => { + textAttrsLower[k.toLowerCase()] = textAttrs[k]; }); - const match = topic.match(/\${[^}]+}/g); + const match = text.match(/\${[^}]+}/g); if (match) { match.forEach(v => { const key = v.substr(2, v.length - 3); const rx = new RegExp('\\${' + key + '}', 'g'); const rkey = key.toLowerCase(); - topic = topic.replace(rx, topicAttrsLower[rkey] || ''); + if (rkey === 'now' || rkey === 'datetime') { + const d = new Date(); + text = text.replace(rx, d.toLocaleString()); + } else if (rkey === 'date') { + const d = new Date(); + text = text.replace(rx, d.toLocaleDateString()); + } else if (rkey === 'time') { + const d = new Date(); + text = text.replace(rx, d.toLocaleTimeString()); + } else if (rkey === 'isodate') { + const d = new Date(); + text = text.replace(rx, d.toISOString()); + } else if (msg && (rkey.includes('.') || rkey.includes('['))) { + try { + text = text.replace(rx, RED.util.getMessageProperty(msg, rkey)); + } catch (ex) { + text = text.replace(rx, ex.message); + } + } else { + text = text.replace(rx, textAttrsLower[rkey] || ''); + } }); } - - return topic; + return text; } \ No newline at end of file diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index 87a5d6c..f152c1e 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -517,11 +517,11 @@ function checkRules(node, msg, oNow, tempData) { for (let i = 0; i <= node.rules.lastUntil; ++i) { const rule = node.rules.data[i]; - // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); + node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.last) { continue; } const res = compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow); // now is less time if (res) { - // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); if (res.level.operator === cRuleType.slatOversteer) { result.ruleSlatOvs = res; } else if (res.level.operator === cRuleType.topicOversteer) { @@ -546,7 +546,7 @@ function checkRules(node, msg, oNow, tempData) { if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.first) { continue; } const res = compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow); // now is greater time if (res) { - // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); + node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity })); if (res.level.operator === cRuleType.slatOversteer) { result.ruleSlatOvs = res; } else if (res.level.operator === cRuleType.topicOversteer) { diff --git a/nodes/position-config.js b/nodes/position-config.js index f2b1816..1592d6d 100644 --- a/nodes/position-config.js +++ b/nodes/position-config.js @@ -1102,8 +1102,22 @@ module.exports = function (RED) { result = hlp.angleNorm(Number(data.value)); } else if (data.type === 'numAzimuthRad' || data.type === 'numAltitudeRad') { data = hlp.angleNormRad(Number(data.value)); - } else if (data.type === 'str' || data.type === 'strPlaceholder') { + } else if (data.type === 'str') { result = ''+data.value; + } else if (data.type === 'strPlaceholder') { + const replaceAttrs = { + name: _srcNode.name, + id: _srcNode.id, + path: _srcNode._path || _srcNode.id, + topic: msg.topic + }; + if (typeof msg.payload === 'object') { + replaceAttrs.payload = JSON.stringify(msg.payload); + Object.assign(replaceAttrs,msg.payload); + } else { + replaceAttrs.payload = msg.payload; + } + result = hlp.textReplace(''+data.value, replaceAttrs, RED, msg); } else if (data.type === 'bool') { result = /^true$/i.test(data.value); } else if (data.type === 'date') { From 70c5995d81ecfd47ee5f7f6394f8cee3411461ab Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sun, 6 Feb 2022 20:04:46 +0100 Subject: [PATCH 09/44] first develpment release --- LICENSE.txt | 201 +++++++ nodes/blind-control.html | 70 ++- nodes/blind-control.js | 30 +- nodes/clock-timer.html | 70 ++- nodes/clock-timer.js | 30 +- nodes/delay-until.html | 521 ++++++++++++++++++ nodes/delay-until.js | 374 +++++++++++++ nodes/icons/hourglass-black.svg | 1 + nodes/icons/hourglass-white.svg | 1 + nodes/icons/inputTypeSunTimeElevationRise.svg | 128 +++++ .../inputTypeSunTimeElevationRiseRad.svg | 139 +++++ nodes/icons/inputTypeSunTimeElevationSet.svg | 128 +++++ .../icons/inputTypeSunTimeElevationSetRad.svg | 139 +++++ nodes/lib/dateTimeHelper.js | 22 + nodes/lib/suncalc.js | 2 + nodes/lib/timeControlHelper.js | 22 + nodes/locales/de/position-config.json | 14 +- nodes/locales/en-US/delay-until.json | 43 ++ nodes/locales/en-US/position-config.json | 24 +- nodes/moon-position.html | 24 +- nodes/moon-position.js | 37 +- nodes/position-config.html | 107 +++- nodes/position-config.js | 121 +++- nodes/static/htmlglobal.js | 136 +++-- nodes/sun-position.html | 62 ++- nodes/sun-position.js | 37 +- nodes/time-comp.html | 37 +- nodes/time-comp.js | 33 ++ nodes/time-inject.html | 98 +++- nodes/time-inject.js | 52 +- nodes/time-span.html | 43 +- nodes/time-span.js | 35 +- nodes/within-time-switch.html | 126 ++++- nodes/within-time-switch.js | 59 +- package.json | 7 +- 35 files changed, 2730 insertions(+), 243 deletions(-) create mode 100644 LICENSE.txt create mode 100644 nodes/delay-until.html create mode 100644 nodes/delay-until.js create mode 100644 nodes/icons/hourglass-black.svg create mode 100644 nodes/icons/hourglass-white.svg create mode 100644 nodes/icons/inputTypeSunTimeElevationRise.svg create mode 100644 nodes/icons/inputTypeSunTimeElevationRiseRad.svg create mode 100644 nodes/icons/inputTypeSunTimeElevationSet.svg create mode 100644 nodes/icons/inputTypeSunTimeElevationSetRad.svg create mode 100644 nodes/locales/en-US/delay-until.json diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/nodes/blind-control.html b/nodes/blind-control.html index 8ef5cd4..e91a36e 100644 --- a/nodes/blind-control.html +++ b/nodes/blind-control.html @@ -1,4 +1,25 @@ + + + + + \ No newline at end of file diff --git a/nodes/delay-until.js b/nodes/delay-until.js new file mode 100644 index 0000000..807ff8c --- /dev/null +++ b/nodes/delay-until.js @@ -0,0 +1,374 @@ +/* + * 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'; + +const util = require('util'); +const path = require('path'); + +const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + +module.exports = function(RED) { + 'use strict'; + /** + * withinTimeSwitchNode + * @param {*} config - configuration + */ + function rdgDelayUntilNode(config) { + RED.nodes.createNode(this, config); + this.locale = require('os-locale').sync(); + // Retrieve the config node + this.positionConfig = RED.nodes.getNode(config.positionConfig); + // this.debug('initialize rdgDelayUntilNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); + if (!this.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 (this.positionConfig.checkNode(error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { + return null; + } + this.timeData = { + type: config.timeType, + value : config.time, + offsetType : config.offsetType, + offset : config.offset, + multiplier : config.offsetMultiplier, + next: true, + calcByMsg: (config.timeType === 'msg' || + config.timeType === 'flow' || + config.timeType === 'global') + }; + if (this.timeData.type === 'jsonata') { + try { + this.timeData.expr = this.positionConfig.getJSONataExpression(this, this.timeData.value); + } catch (err) { + this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + this.timeData.expr = null; + } + } + this.queuingBehavior = config.queuingBehavior; + this.flushMsgs = { + type: config.flushMsgsType || 'none', + value : config.flushMsgs + }; + this.flushMsgsValue = config.flushMsgsValue; + this.dropMsgs = { + type: config.dropMsgsType || 'none', + value : config.dropMsgs + }; + this.dropMsgsValue = config.dropMsgsValue; + this.enqueueMsg = { + type: config.enqueueMsgType || 'none', + value : config.enqueueMsg + }; + this.enqueueMsgValue = config.enqueueMsgValue; + this.ctrlPropSet = config.ctrlPropSet; + this.ctrlPropValue = config.ctrlPropValue; + this.tsCompare = config.tsCompare; + const node = this; + + 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 })); + node.debug('test 00'); + if (!node.flushMsgs.type !== 'none') { + try { + const result = RED.util.getMessageProperty(msg, node.flushMsgs.value); + if (result == node.flushMsgsValue) { // eslint-disable-line eqeqeq + node.debug(`flush queue control property ${node.flushMsgs.value}=${result}`); + flushEntireQueue(); + clearTimer(); + const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); + if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq + if (node.ctrlPropSet) { + RED.util.setMessageProperty(msg, node.flushMsgs.value, node.ctrlPropValue, false); + } + addMsgToQueue(msg, done); + return null; + } + setStatus(); + done(); + return null; + } + } catch(_err) { + node.debug(_err); + } + } + node.debug('test 05'); + if (!node.dropMsgs.type !== 'none') { + node.debug('test 06'); + try { + const result = RED.util.getMessageProperty(msg, node.dropMsgs.value); + if (result == node.dropMsgsValue) { // eslint-disable-line eqeqeq + node.debug(`flush queue control property ${node.dropMsgs.value}=${result}`); + dropEntireQueue(); + clearTimer(); + const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); + if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq + if (node.ctrlPropSet) { + RED.util.setMessageProperty(msg, node.dropMsgs.value, node.ctrlPropValue, false); + } + addMsgToQueue(msg, done); + return null; + } + setStatus(); + done(); + return null; + } + } catch(_err) { + node.debug(_err); + } + } + node.debug('test 1'); + addMsgToQueue(msg, done); + return null; + } catch (err) { + node.debug('test catch'); + node.log(err.message); + node.log(util.inspect(err, Object.getOwnPropertyNames(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(); + } + const dNow = getIntDate(node.tsCompare, qObj.msg, node); + node.nextTime = node.positionConfig.getTimeProp(node, qObj.msg, node.timeData, dNow); + 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 = node.nextTime.value.valueOf() - dNow.valueOf(); + node.debug(`set timeout to ${node.nextTime.value.valueOf()} - ${dNow.valueOf()}`); + while (millisec < 1) { + millisec += 86400000; // 24h + } + if (millisec > 345600000) { + // there is a limitation of nodejs that the maximum setTimeout time + // should not more then 2147483647 ms (24.8 days). + millisec = Math.min((millisec - 129600000), 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.nextTime.intermedia = 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.timeData.calcByMsg) { + recalcTimeOut(qObj); + } + setStatus(); + } + + /** + * adds a new message tot he queue + * @param {*} msg - message object + * @param {*} done - done object + */ + 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.nextTime.intermedia) { + 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); +}; \ No newline at end of file diff --git a/nodes/icons/hourglass-black.svg b/nodes/icons/hourglass-black.svg new file mode 100644 index 0000000..a986689 --- /dev/null +++ b/nodes/icons/hourglass-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nodes/icons/hourglass-white.svg b/nodes/icons/hourglass-white.svg new file mode 100644 index 0000000..a258837 --- /dev/null +++ b/nodes/icons/hourglass-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nodes/icons/inputTypeSunTimeElevationRise.svg b/nodes/icons/inputTypeSunTimeElevationRise.svg new file mode 100644 index 0000000..7188962 --- /dev/null +++ b/nodes/icons/inputTypeSunTimeElevationRise.svg @@ -0,0 +1,128 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + E + + + + diff --git a/nodes/icons/inputTypeSunTimeElevationRiseRad.svg b/nodes/icons/inputTypeSunTimeElevationRiseRad.svg new file mode 100644 index 0000000..9e666c2 --- /dev/null +++ b/nodes/icons/inputTypeSunTimeElevationRiseRad.svg @@ -0,0 +1,139 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + E + + + R + + diff --git a/nodes/icons/inputTypeSunTimeElevationSet.svg b/nodes/icons/inputTypeSunTimeElevationSet.svg new file mode 100644 index 0000000..9a27ad0 --- /dev/null +++ b/nodes/icons/inputTypeSunTimeElevationSet.svg @@ -0,0 +1,128 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + E + + + + diff --git a/nodes/icons/inputTypeSunTimeElevationSetRad.svg b/nodes/icons/inputTypeSunTimeElevationSetRad.svg new file mode 100644 index 0000000..3269193 --- /dev/null +++ b/nodes/icons/inputTypeSunTimeElevationSetRad.svg @@ -0,0 +1,139 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + E + + + R + + diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index b664e18..2425d09 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -1,3 +1,25 @@ +/* + * 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. + * + */ + /******************************************** * dateTimeHelper.js: *********************************************/ diff --git a/nodes/lib/suncalc.js b/nodes/lib/suncalc.js index 3c14257..4ad4e04 100644 --- a/nodes/lib/suncalc.js +++ b/nodes/lib/suncalc.js @@ -2,6 +2,8 @@ (c) 2011-2015, Vladimir Agafonkin SunCalc is a JavaScript library for calculating sun/moon position and light phases. https://github.com/mourner/suncalc + + Reworked and enhanced by Robert Gester */ 'use strict'; const util = require('util'); // eslint-disable-line no-unused-vars diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index 02605b6..2f7d8ef 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -1,3 +1,25 @@ +/* + * 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. + * + */ + /******************************************** * dateTimeHelper.js: *********************************************/ diff --git a/nodes/locales/de/position-config.json b/nodes/locales/de/position-config.json index 2507fcb..d853542 100644 --- a/nodes/locales/de/position-config.json +++ b/nodes/locales/de/position-config.json @@ -64,10 +64,16 @@ "numAltitude":"Sonnenhöhenwinkel", "numAzimuthRad":"Sonnenrichtung (rad)", "numAltitudeRad":"Sonnenhöhenwinkel (rad)", - "sunTimeByElevation":"Zeit durch Sonnenhöhenwinkel", - "sunTimeByAzimuth":"Zeit durch Sonnenrichtung", - "sunTimeByElevationRad":"Zeit durch Sonnenhöhenwinkel (rad)", - "sunTimeByAzimuthRad":"Zeit durch Sonnenrichtung (rad)", + "SunTimeByAzimuth":"Zeit durch Sonnenrichtung", + "SunTimeByAzimuthRad":"Zeit durch Sonnenrichtung (rad)", + "SunTimeByElevationObj":"Zeit Auf/Untergang Sonne durch Höhenwinkel", + "SunTimeByElevationObjRad":"Zeit Auf/Untergang Sonne durch Höhenwinkel (rad)", + "SunTimeByElevationNext":"Zeit Auf/Untergang Sonne durch Höhenwinkel", + "SunTimeByElevationNextRad":"Zeit Auf/Untergang Sonne durch Höhenwinkel (rad)", + "SunTimeByElevationRise":"Zeit Aufgang Sonne durch Höhenwinkel", + "SunTimeByElevationRiseRad":"Zeit Aufgang Sonne durch Höhenwinkel (rad)", + "SunTimeByElevationSet":"Zeit Untergang Sonne durch Höhenwinkel", + "SunTimeByElevationSetRad":"Zeit Untergang Sonne durch Höhenwinkel (rad)", "suntime":"Sonnenzeit", "suntimes":"Sonnenzeiten", "mooncalc": "Mondposition", diff --git a/nodes/locales/en-US/delay-until.json b/nodes/locales/en-US/delay-until.json new file mode 100644 index 0000000..9456c63 --- /dev/null +++ b/nodes/locales/en-US/delay-until.json @@ -0,0 +1,43 @@ +{ + "delay-until": { + "label": { + "name": "delay until", + "outputPort": "message", + "time":"time", + "offset":"Offset", + "flushMsgs":"flush messages with", + "dropMsgs":"drop message with", + "equals": "equal", + "compareTime":"base time", + "now":"date.now", + "msgts":"msg.ts", + "msglc":"msg.lc", + "msgtime":"msg.time", + "showEnhSettings":"Enhanced settings", + "enqueueMsg":"enqueue message with", + "ctrlPropSet":"set control properties", + "ctrlPropSetB":"to value", + "queuingBehavior":"message treatment", + "allMsgs":"enqueue all messages", + "firstMsgs":"enqueue only first message", + "lastMsgs":"enqueue only last (latest) message" + }, + "placeholder": { + "time":"time", + "offset":"Offset time", + "flushMsgs":"flush all waiting messages if property is equal value", + "dropMsgs":"drop all waiting messages if property is equal value", + "ctrlPropSet":"set a control propertie to the speciefied value" + }, + "tips": { + "documentation": "Documentation and examples", + "controlMsgProp":"Here can be defined special properties of the input message to flush or drop queued messages.", + "controlMsgPropDefined": "If one of the control propeties are equal to the value, all messages will be dropped or flushed from the queue. If the message contains additional the property msg.enqueue the message will be stored. Wit the following the control property can be set in that case to another value." + }, + "state": { + "default": "(__queueLength__) flush __sendTime__", + "intermedia": "(__queueLength__) retrigger __sendTime__", + "noTime": "(__queueLength__) will not send" + } + } +} \ No newline at end of file diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/position-config.json index abe332b..f5dd567 100644 --- a/nodes/locales/en-US/position-config.json +++ b/nodes/locales/en-US/position-config.json @@ -32,7 +32,9 @@ "ok":"Ok", "randNum":"random till", "randNumCachedDay":"random (daily) till", - "randNumCachedWeek":"random (weekly) till" + "randNumCachedWeek":"random (weekly) till", + "inputPort": "input message", + "outputPort": "output message" }, "types": { "unlimited":"no limitation", @@ -64,10 +66,16 @@ "numAltitude":"Elevation angle", "numAzimuthRad":"Azimuth angle (rad)", "numAltitudeRad":"Elevation angle (rad)", - "sunTimeByElevation":"time by elevation", - "sunTimeByAzimuth":"time by azimuth", - "sunTimeByElevationRad":"time by elevation (rad)", - "sunTimeByAzimuthRad":"time by azimuth (rad)", + "SunTimeByAzimuth":"time by azimuth", + "SunTimeByAzimuthRad":"time by azimuth (rad)", + "SunTimeByElevationObj":"time by elevation (set or rise)", + "SunTimeByElevationObjRad":"time by elevation (set or rise in rad)", + "SunTimeByElevationNext":"next time by elevation (set or rise)", + "SunTimeByElevationNextRad":"next time by elevation (set or rise in rad)", + "SunTimeByElevationRise":"next rise time by elevation", + "SunTimeByElevationRiseRad":"next rise time by elevation (rad)", + "SunTimeByElevationSet":"next set time by elevation", + "SunTimeByElevationSetRad":"next set time by elevation (rad)", "suntime":"sun time", "suntimes":"sun times", "mooncalc":"moon calculation", @@ -473,8 +481,8 @@ "error-title": "internal error", "error-init": "error '__message__', retry in __time__min", "warn-init": "warning '__message__', retry in __time__min", - "pos-config": "Node not properly configured!! Missing or wrong position configuration!", - "pos-config-state": "Node not properly configured!!", + "config-missing": "Node not properly configured!! Configuration is missing (no configuration node is selected).", + "config-missing-state": "No configuration node is selected.", "unknownPropertyOperator": "error, the used property operator __propertyOp__=\"__propertyOpText__\" is unknown!", "unknownCompareOperator": "error, the used compare operator \"__operator__\" is unknown! (\"__opTypeA__.__opValueA__\" compare to \"__opTypeB__.__opValueB__\")", "notEvaluableProperty":"Error: could not evaluate __type__.__value__!", @@ -521,7 +529,7 @@ "errors": { "latitude-missing": "Latitude is missing or wrong!", "longitude-missing": "Longitude is missing or wrong!", - "coordinates-missing": "Latitude and Longitude is wrong!" + "coordinates-missing": "Coordinates Latitude and Longitude is missing in the configuration node!" } } } \ No newline at end of file diff --git a/nodes/moon-position.html b/nodes/moon-position.html index 3d68ddb..f0e8785 100644 --- a/nodes/moon-position.html +++ b/nodes/moon-position.html @@ -1,4 +1,25 @@ + @@ -41,7 +62,8 @@ const rule = this.rules[index - 1]; if (rule) { - return RED.nodes.getType('position-config').getValueLabel(rule.valueLowType, rule.valueLow) + ' & ' + RED.nodes.getType('position-config').getValueLabel(rule.valueHighType, rule.valueHigh); + return RED.nodes.getType('position-config').getRDGNodeValLbl(this, rule.valueLowType, rule.valueLow) + ' & ' + + RED.nodes.getType('position-config').getRDGNodeValLbl(this, rule.valueHighType, rule.valueHigh); } return undefined; }, diff --git a/nodes/moon-position.js b/nodes/moon-position.js index ecdcdfd..a7775e6 100644 --- a/nodes/moon-position.js +++ b/nodes/moon-position.js @@ -1,3 +1,25 @@ +/* + * 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. + * + */ + /******************************************** * moon-position: *********************************************/ @@ -22,11 +44,8 @@ module.exports = function (RED) { this.azimuthPos = {}; const node = this; if (!this.positionConfig) { - node.status({ - fill: 'red', - shape: 'dot', - text: 'Node not properly configured!!' - }); + 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; } @@ -40,12 +59,8 @@ module.exports = function (RED) { const dNow = hlp.getNowTimeStamp(this, msg); if (!this.positionConfig) { - node.status({ - fill: 'red', - shape: 'dot', - text: RED._('node-red-contrib-sun-position/position-config:errors.pos-config-state') - }); - done(RED._('node-red-contrib-sun-position/position-config:errors.pos-config'), msg); + 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; } const ports = new Array(this.rules.length); diff --git a/nodes/position-config.html b/nodes/position-config.html index ccdf30c..967d7dc 100644 --- a/nodes/position-config.html +++ b/nodes/position-config.html @@ -1,4 +1,25 @@ + + \ No newline at end of file diff --git a/nodes/22-delay-until.js b/nodes/22-delay-until.js new file mode 100644 index 0000000..283ce33 --- /dev/null +++ b/nodes/22-delay-until.js @@ -0,0 +1,374 @@ +/* + * 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'; + +module.exports = function(RED) { + 'use strict'; + /** + * withinTimeSwitchNode + * @param {*} config - configuration + */ + function rdgDelayUntilNode(config) { + const util = require('util'); + const path = require('path'); + + const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + + RED.nodes.createNode(this, config); + this.locale = require('os-locale').sync(); + // Retrieve the config node + this.positionConfig = RED.nodes.getNode(config.positionConfig); + // this.debug('initialize rdgDelayUntilNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); + if (!this.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 (this.positionConfig.checkNode(error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { + return null; + } + this.timeData = { + type: config.timeType, + value : config.time, + offsetType : config.offsetType, + offset : config.offset, + multiplier : config.offsetMultiplier, + next: true, + calcByMsg: (config.timeType === 'msg' || + config.timeType === 'flow' || + config.timeType === 'global') + }; + if (this.timeData.type === 'jsonata') { + try { + this.timeData.expr = this.positionConfig.getJSONataExpression(this, this.timeData.value); + } catch (err) { + this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + this.timeData.expr = null; + } + } + this.queuingBehavior = config.queuingBehavior; + this.flushMsgs = { + type: config.flushMsgsType || 'none', + value : config.flushMsgs + }; + this.flushMsgsValue = config.flushMsgsValue; + this.dropMsgs = { + type: config.dropMsgsType || 'none', + value : config.dropMsgs + }; + this.dropMsgsValue = config.dropMsgsValue; + this.enqueueMsg = { + type: config.enqueueMsgType || 'none', + value : config.enqueueMsg + }; + this.enqueueMsgValue = config.enqueueMsgValue; + this.ctrlPropSet = config.ctrlPropSet; + this.ctrlPropValue = config.ctrlPropValue; + this.tsCompare = config.tsCompare; + const node = this; + + 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 })); + node.debug('test 00'); + if (!node.flushMsgs.type !== 'none') { + try { + const result = RED.util.getMessageProperty(msg, node.flushMsgs.value); + if (result == node.flushMsgsValue) { // eslint-disable-line eqeqeq + node.debug(`flush queue control property ${node.flushMsgs.value}=${result}`); + flushEntireQueue(); + clearTimer(); + const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); + if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq + if (node.ctrlPropSet) { + RED.util.setMessageProperty(msg, node.flushMsgs.value, node.ctrlPropValue, false); + } + addMsgToQueue(msg, done); + return null; + } + setStatus(); + done(); + return null; + } + } catch(_err) { + node.debug(_err); + } + } + node.debug('test 05'); + if (!node.dropMsgs.type !== 'none') { + node.debug('test 06'); + try { + const result = RED.util.getMessageProperty(msg, node.dropMsgs.value); + if (result == node.dropMsgsValue) { // eslint-disable-line eqeqeq + node.debug(`flush queue control property ${node.dropMsgs.value}=${result}`); + dropEntireQueue(); + clearTimer(); + const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); + if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq + if (node.ctrlPropSet) { + RED.util.setMessageProperty(msg, node.dropMsgs.value, node.ctrlPropValue, false); + } + addMsgToQueue(msg, done); + return null; + } + setStatus(); + done(); + return null; + } + } catch(_err) { + node.debug(_err); + } + } + node.debug('test 1'); + addMsgToQueue(msg, done); + return null; + } catch (err) { + node.debug('test catch'); + node.log(err.message); + node.log(util.inspect(err, Object.getOwnPropertyNames(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(); + } + const dNow = getIntDate(node.tsCompare, qObj.msg, node); + node.nextTime = node.positionConfig.getTimeProp(node, qObj.msg, node.timeData, dNow); + 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 = node.nextTime.value.valueOf() - dNow.valueOf(); + node.debug(`set timeout to ${node.nextTime.value.valueOf()} - ${dNow.valueOf()}`); + while (millisec < 1) { + millisec += 86400000; // 24h + } + if (millisec > 345600000) { + // there is a limitation of nodejs that the maximum setTimeout time + // should not more then 2147483647 ms (24.8 days). + millisec = Math.min((millisec - 129600000), 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.nextTime.intermedia = 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.timeData.calcByMsg) { + recalcTimeOut(qObj); + } + setStatus(); + } + + /** + * adds a new message tot he queue + * @param {*} msg - message object + * @param {*} done - done object + */ + 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.nextTime.intermedia) { + 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); +}; \ No newline at end of file diff --git a/nodes/locales/de/position-config.json b/nodes/locales/de/10-position-config.json similarity index 100% rename from nodes/locales/de/position-config.json rename to nodes/locales/de/10-position-config.json diff --git a/nodes/locales/de/time-inject.json b/nodes/locales/de/20-time-inject.json similarity index 100% rename from nodes/locales/de/time-inject.json rename to nodes/locales/de/20-time-inject.json diff --git a/nodes/locales/de/within-time-switch.json b/nodes/locales/de/21-within-time-switch.json similarity index 100% rename from nodes/locales/de/within-time-switch.json rename to nodes/locales/de/21-within-time-switch.json diff --git a/nodes/locales/de/sun-position.json b/nodes/locales/de/30-sun-position.json similarity index 100% rename from nodes/locales/de/sun-position.json rename to nodes/locales/de/30-sun-position.json diff --git a/nodes/locales/de/moon-position.json b/nodes/locales/de/31-moon-position.json similarity index 100% rename from nodes/locales/de/moon-position.json rename to nodes/locales/de/31-moon-position.json diff --git a/nodes/locales/de/time-comp.json b/nodes/locales/de/60-time-comp.json similarity index 100% rename from nodes/locales/de/time-comp.json rename to nodes/locales/de/60-time-comp.json diff --git a/nodes/locales/de/time-span.json b/nodes/locales/de/61-time-span.json similarity index 100% rename from nodes/locales/de/time-span.json rename to nodes/locales/de/61-time-span.json diff --git a/nodes/locales/de/blind-control.json b/nodes/locales/de/80-blind-control.json similarity index 100% rename from nodes/locales/de/blind-control.json rename to nodes/locales/de/80-blind-control.json diff --git a/nodes/locales/de/clock-timer.json b/nodes/locales/de/81-clock-timer.json similarity index 100% rename from nodes/locales/de/clock-timer.json rename to nodes/locales/de/81-clock-timer.json diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/10-position-config.json similarity index 100% rename from nodes/locales/en-US/position-config.json rename to nodes/locales/en-US/10-position-config.json diff --git a/nodes/locales/en-US/time-inject.html b/nodes/locales/en-US/20-time-inject.html similarity index 100% rename from nodes/locales/en-US/time-inject.html rename to nodes/locales/en-US/20-time-inject.html diff --git a/nodes/locales/en-US/time-inject.json b/nodes/locales/en-US/20-time-inject.json similarity index 100% rename from nodes/locales/en-US/time-inject.json rename to nodes/locales/en-US/20-time-inject.json diff --git a/nodes/locales/en-US/within-time-switch.html b/nodes/locales/en-US/21-within-time-switch.html similarity index 100% rename from nodes/locales/en-US/within-time-switch.html rename to nodes/locales/en-US/21-within-time-switch.html diff --git a/nodes/locales/en-US/within-time-switch.json b/nodes/locales/en-US/21-within-time-switch.json similarity index 100% rename from nodes/locales/en-US/within-time-switch.json rename to nodes/locales/en-US/21-within-time-switch.json diff --git a/nodes/locales/en-US/sun-position.html b/nodes/locales/en-US/30-sun-position.html similarity index 100% rename from nodes/locales/en-US/sun-position.html rename to nodes/locales/en-US/30-sun-position.html diff --git a/nodes/locales/en-US/sun-position.json b/nodes/locales/en-US/30-sun-position.json similarity index 100% rename from nodes/locales/en-US/sun-position.json rename to nodes/locales/en-US/30-sun-position.json diff --git a/nodes/locales/en-US/moon-position.html b/nodes/locales/en-US/31-moon-position.html similarity index 100% rename from nodes/locales/en-US/moon-position.html rename to nodes/locales/en-US/31-moon-position.html diff --git a/nodes/locales/en-US/moon-position.json b/nodes/locales/en-US/31-moon-position.json similarity index 100% rename from nodes/locales/en-US/moon-position.json rename to nodes/locales/en-US/31-moon-position.json diff --git a/nodes/locales/en-US/time-comp.html b/nodes/locales/en-US/60-time-comp.html similarity index 100% rename from nodes/locales/en-US/time-comp.html rename to nodes/locales/en-US/60-time-comp.html diff --git a/nodes/locales/en-US/time-comp.json b/nodes/locales/en-US/60-time-comp.json similarity index 100% rename from nodes/locales/en-US/time-comp.json rename to nodes/locales/en-US/60-time-comp.json diff --git a/nodes/locales/en-US/time-span.html b/nodes/locales/en-US/61-time-span.html similarity index 100% rename from nodes/locales/en-US/time-span.html rename to nodes/locales/en-US/61-time-span.html diff --git a/nodes/locales/en-US/time-span.json b/nodes/locales/en-US/61-time-span.json similarity index 100% rename from nodes/locales/en-US/time-span.json rename to nodes/locales/en-US/61-time-span.json diff --git a/nodes/locales/en-US/blind-control.html b/nodes/locales/en-US/80-blind-control.html similarity index 100% rename from nodes/locales/en-US/blind-control.html rename to nodes/locales/en-US/80-blind-control.html diff --git a/nodes/locales/en-US/blind-control.json b/nodes/locales/en-US/80-blind-control.json similarity index 100% rename from nodes/locales/en-US/blind-control.json rename to nodes/locales/en-US/80-blind-control.json diff --git a/nodes/locales/en-US/clock-timer.json b/nodes/locales/en-US/81-clock-timer.json similarity index 100% rename from nodes/locales/en-US/clock-timer.json rename to nodes/locales/en-US/81-clock-timer.json diff --git a/nodes/locales/en-US/81-delay-until.json b/nodes/locales/en-US/81-delay-until.json new file mode 100644 index 0000000..9456c63 --- /dev/null +++ b/nodes/locales/en-US/81-delay-until.json @@ -0,0 +1,43 @@ +{ + "delay-until": { + "label": { + "name": "delay until", + "outputPort": "message", + "time":"time", + "offset":"Offset", + "flushMsgs":"flush messages with", + "dropMsgs":"drop message with", + "equals": "equal", + "compareTime":"base time", + "now":"date.now", + "msgts":"msg.ts", + "msglc":"msg.lc", + "msgtime":"msg.time", + "showEnhSettings":"Enhanced settings", + "enqueueMsg":"enqueue message with", + "ctrlPropSet":"set control properties", + "ctrlPropSetB":"to value", + "queuingBehavior":"message treatment", + "allMsgs":"enqueue all messages", + "firstMsgs":"enqueue only first message", + "lastMsgs":"enqueue only last (latest) message" + }, + "placeholder": { + "time":"time", + "offset":"Offset time", + "flushMsgs":"flush all waiting messages if property is equal value", + "dropMsgs":"drop all waiting messages if property is equal value", + "ctrlPropSet":"set a control propertie to the speciefied value" + }, + "tips": { + "documentation": "Documentation and examples", + "controlMsgProp":"Here can be defined special properties of the input message to flush or drop queued messages.", + "controlMsgPropDefined": "If one of the control propeties are equal to the value, all messages will be dropped or flushed from the queue. If the message contains additional the property msg.enqueue the message will be stored. Wit the following the control property can be set in that case to another value." + }, + "state": { + "default": "(__queueLength__) flush __sendTime__", + "intermedia": "(__queueLength__) retrigger __sendTime__", + "noTime": "(__queueLength__) will not send" + } + } +} \ No newline at end of file From 5e52926135fe736d4fb8f5d28d0625641e4270a9 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Mon, 21 Feb 2022 15:42:04 +0100 Subject: [PATCH 13/44] fixed errors --- nodes/20-time-inject.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/nodes/20-time-inject.js b/nodes/20-time-inject.js index 3198b9a..5174b9f 100644 --- a/nodes/20-time-inject.js +++ b/nodes/20-time-inject.js @@ -105,6 +105,7 @@ module.exports = function (RED) { if (node.positionConfig.checkNode(error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); + delete node.positionConfig; }, false)) { return; } @@ -1073,23 +1074,6 @@ module.exports = function (RED) { }); try { - 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; - } - if (node.positionConfig.checkNode(error => { - node.error(error); - node.status({fill: 'red', shape: 'dot', text: error }); - }, false)) { - return; - } - 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; - } - if (!isNaN(node.onceDelay)) { node.status({ fill: 'yellow', From 11429985476c672433cee586c2572da3c9a3b34f Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 22 Feb 2022 02:26:07 +0100 Subject: [PATCH 14/44] added test for time inject node (first implementation) - added test for time inject node - enhanced error handling in time inject and position config node --- nodes/10-position-config.html | 28 +- nodes/10-position-config.js | 27 +- nodes/20-time-inject.js | 16 +- nodes/21-within-time-switch.js | 10 +- nodes/22-delay-until.js | 9 +- nodes/60-time-comp.js | 9 +- nodes/61-time-span.js | 9 +- nodes/80-blind-control.js | 15 +- nodes/81-clock-timer.js | 14 +- nodes/lib/dateTimeHelper.js | 109 +- nodes/locales/en-US/10-position-config.json | 4 +- test/20_time-inject_spec.js | 1659 +++++++++++++++++++ 12 files changed, 1799 insertions(+), 110 deletions(-) create mode 100644 test/20_time-inject_spec.js diff --git a/nodes/10-position-config.html b/nodes/10-position-config.html index 967d7dc..cd5ac2b 100644 --- a/nodes/10-position-config.html +++ b/nodes/10-position-config.html @@ -57,14 +57,8 @@ return (v === true) || (v === 'true'); } }, - longitude: { - value: '', - required: false - }, - latitude: { - value: '', - required: false - }, + longitude: {value: '' }, + latitude: {value: ''}, angleType: { value: 'deg', required: true, @@ -131,9 +125,6 @@ $('#node-config-input-isValide').val(false); const $lat = $('#node-config-input-posLatitude'); const $lon = $('#node-config-input-posLongitude'); - - const latOldVal = parseFloat($('#node-config-input-latitude').val()); - const lonOldVal = parseFloat($('#node-config-input-longitude').val()); let latVal = parseFloat($lat.val()); let lonVal = parseFloat($lon.val()); @@ -171,8 +162,8 @@ }); if ($lat.val() === '' || isNaN(latVal)) { - if (latOldVal !== '' && !isNaN(latOldVal) && latOldVal !== 0) { - $lat.val(latOldVal); + if (node.longitude !== '' && !isNaN(node.longitude) && node.longitude !== 0) { + $lat.val(node.longitude); } else { $lat.val(''); } @@ -180,8 +171,8 @@ } if ($lon.val() === '' || isNaN(lonVal)) { - if (lonOldVal !== '' && !isNaN(lonOldVal) && lonOldVal !== 0) { - $lon.val(lonOldVal); + if (node.longitude !== '' && !isNaN(node.longitude) && node.longitude !== 0) { + $lon.val(node.longitude); } else { $lon.val(''); } @@ -243,10 +234,9 @@ } else { $('#node-config-input-posLatitude').val(latitude); } - $('#node-config-input-latitude').val(0); /* */ - $('#node-config-input-latitude').val(0); - $('#node-config-input-longitude').val(0); + delete this.longitude; + delete this.latitude; }, /** @@ -555,13 +545,11 @@
-
-
diff --git a/nodes/10-position-config.js b/nodes/10-position-config.js index f42f29e..68762cd 100644 --- a/nodes/10-position-config.js +++ b/nodes/10-position-config.js @@ -51,8 +51,15 @@ module.exports = function (RED) { try { this.name = config.name; - this.latitude = parseFloat(this.credentials.posLatitude || config.latitude); - this.longitude = parseFloat(this.credentials.posLongitude || config.longitude); + this.valid = true; + this.latitude = parseFloat(Object.prototype.hasOwnProperty.call(this.credentials, 'posLatitude') ? this.credentials.posLatitude : config.latitude); + this.longitude = parseFloat(Object.prototype.hasOwnProperty.call(this.credentials, 'posLongitude') ? this.credentials.posLongitude : config.longitude); + this.checkNode( + error => { + this.error(error); + this.status({fill: 'red', shape: 'dot', text: error }); + this.valid = false; + }); this.angleType = config.angleType; this.tzOffset = parseInt(config.timeZoneOffset || 99); this.tzDST = parseInt(config.timeZoneDST || 0); @@ -112,8 +119,8 @@ module.exports = function (RED) { * register a node as child * @param {*} node node to register as child node */ - register(node) { - this.users[node.id] = node; + register(srcnode) { + this.users[srcnode.id] = srcnode; } /** @@ -122,11 +129,10 @@ module.exports = function (RED) { * @param {function} done function which should be executed after deregister * @returns {*} result of the function */ - deregister(node, done) { - delete node.users[node.id]; + deregister(srcnode, done) { + delete srcnode.users[srcnode.id]; return done(); } - /*******************************************************************************************************/ /** * This callback type is called `requestCallback` and is displayed as a global symbol. @@ -143,8 +149,7 @@ module.exports = function (RED) { * @return {boolean|string} returns the result of onrror if an error occurs, otherwise onOK */ checkNode(onError, onOk) { - if ((Number.isNaN(this.latitude) && Number.isNaN(this.longitude)) || - ((this.latitude === 0) && (this.longitude === 0))) { + if ((Number.isNaN(this.latitude) && Number.isNaN(this.longitude))) { return onError(RED._('position-config.errors.coordinates-missing')); } if (isNaN(this.latitude) || (this.latitude < -90) || (this.latitude > 90)) { @@ -1614,7 +1619,7 @@ module.exports = function (RED) { } /**************************************************************************************************************/ _sunTimesRefresh(todayValue, dayId) { - this.checkNode(error => { throw new Error(error); }); + if (!this.valid) { return; } if (this.cache.sunTimesToday.dayId === (dayId + 1)) { this.cache.sunTimesToday.times = this.cache.sunTimesTomorrow.times; this.cache.sunTimesToday.sunPosAtSolarNoon = this.cache.sunTimesTomorrow.sunPosAtSolarNoon; @@ -1642,7 +1647,7 @@ module.exports = function (RED) { } _moonTimesRefresh(todayValue, dayId) { - this.checkNode(error => { throw new Error(error); }); + if (!this.valid) { return; } if (this.cache.moonTimesToday.dayId === (dayId + 1)) { this.cache.moonTimesTomorrow.dayId = this.cache.moonTimes2Days.dayId; diff --git a/nodes/20-time-inject.js b/nodes/20-time-inject.js index 5174b9f..83a11a1 100644 --- a/nodes/20-time-inject.js +++ b/nodes/20-time-inject.js @@ -101,12 +101,11 @@ module.exports = function (RED) { 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; - } - if (node.positionConfig.checkNode(error => { - node.error(error); - node.status({fill: 'red', shape: 'dot', text: error }); - delete node.positionConfig; - }, false)) { + } else if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return; } // node.debug('initialize timeInjectNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); @@ -759,13 +758,14 @@ module.exports = function (RED) { */ node.prepOutMsg = msg => { // node.debug(`prepOutMsg node.msg=${util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })}`); - const dNow = new Date(); + const dNow = msg.__ts__input_date || new Date(); let props = node.props; if (msg.__user_inject_props__ && msg.__user_inject_props__.props && Array.isArray(msg.__user_inject_props__.props)) { props = prepareProps(node, msg.__user_inject_props__.props); } delete msg.__user_inject_props__; + delete msg.__ts__input_date; // node.debug(`prepOutMsg props=${util.inspect(props, { colors: true, compact: 10, breakLength: Infinity })}`); for (let i = 0; i < props.length; i++) { @@ -1048,7 +1048,7 @@ module.exports = function (RED) { try { node.debug('--------- time-inject - input msg='+ util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })); if (!node.positionConfig) { - throw new Error('configuration missing!'); + throw new Error('Configuration missing or wrong!'); } if (node.injType === tInj.timer) { node.doCreateStartTimeout(node); diff --git a/nodes/21-within-time-switch.js b/nodes/21-within-time-switch.js index 7d951a2..7e90152 100644 --- a/nodes/21-within-time-switch.js +++ b/nodes/21-within-time-switch.js @@ -310,11 +310,11 @@ module.exports = function (RED) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); setstate(node, { error: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return; - } - if (this.positionConfig.checkNode(error => { - node.error(error); - setstate(node, { error }); - }, false)) { + } else if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return; } diff --git a/nodes/22-delay-until.js b/nodes/22-delay-until.js index 283ce33..1b998e2 100644 --- a/nodes/22-delay-until.js +++ b/nodes/22-delay-until.js @@ -47,10 +47,11 @@ module.exports = function(RED) { node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return null; } - if (this.positionConfig.checkNode(error => { - node.error(error); - node.status({fill: 'red', shape: 'dot', text: error }); - }, false)) { + if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return null; } this.timeData = { diff --git a/nodes/60-time-comp.js b/nodes/60-time-comp.js index 078e800..a69676f 100644 --- a/nodes/60-time-comp.js +++ b/nodes/60-time-comp.js @@ -45,10 +45,11 @@ module.exports = function (RED) { node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return; } - if (this.positionConfig.checkNode(error => { - node.error(error); - node.status({fill: 'red', shape: 'dot', text: error }); - }, false)) { + if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return; } this.input = { diff --git a/nodes/61-time-span.js b/nodes/61-time-span.js index 723d039..a858fa2 100644 --- a/nodes/61-time-span.js +++ b/nodes/61-time-span.js @@ -291,10 +291,11 @@ module.exports = function (RED) { node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.config-missing') }); return; } - if (this.positionConfig.checkNode(error => { - node.error(error); - node.status({fill: 'red', shape: 'dot', text: error }); - }, false)) { + if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return; } this.operand1 = { diff --git a/nodes/80-blind-control.js b/nodes/80-blind-control.js index 218c9dc..3bc25bb 100644 --- a/nodes/80-blind-control.js +++ b/nodes/80-blind-control.js @@ -780,13 +780,18 @@ module.exports = function (RED) { this.positionConfig = RED.nodes.getNode(config.positionConfig); const node = this; if (!this.positionConfig) { - node.status({ - fill: 'red', - shape: 'dot', - text: 'Node not properly configured!!' - }); + 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') }); return; } + if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { + return; + } + if (!Array.isArray(config.results)) { config.results = [{ p: '', diff --git a/nodes/81-clock-timer.js b/nodes/81-clock-timer.js index e67efe6..9386eb6 100644 --- a/nodes/81-clock-timer.js +++ b/nodes/81-clock-timer.js @@ -329,11 +329,15 @@ module.exports = function (RED) { this.outputs = Number(config.outputs || 1); const node = this; if (!this.positionConfig) { - node.status({ - fill: 'red', - shape: 'dot', - text: 'Node not properly configured!!' - }); + 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') }); + return; + } + if (this.positionConfig.checkNode( + error => { + node.error(error); + node.status({fill: 'red', shape: 'dot', text: error }); + }, false)) { return; } diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index f6c14b2..c114e12 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -1089,10 +1089,12 @@ function getTimeOfText(t, date, utc, timeZoneOffset) { * parses a string which contains a date or only a time to a Date object * @param {any} dt number or text which contains a date or a time * @param {boolean} preferMonthFirst if true, Dates with moth first should be preferd, otherwise month last (european) - * @param {boolean} [utc] define if the time should be in utc + * @param {boolean} [utc] indicates if the date should be in utc + * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @return {Date} the parsed date object, throws an error if can not parsed */ -function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset) { +function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { // console.debug('getDateOfText dt=' + util.inspect(dt, { colors: true, compact: 10, breakLength: Infinity })); // eslint-disable-line if (dt === null || typeof dt === 'undefined') { throw new Error('Could not evaluate as a valid Date or time. Value is null or undefined!'); @@ -1128,11 +1130,11 @@ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset) { } if (typeof dt === 'string') { - let res = _parseDateTime(dt, preferMonthFirst, utc, timeZoneOffset); + let res = _parseDateTime(dt, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - res = _parseDate(dt, preferMonthFirst, utc, timeZoneOffset); + res = _parseDate(dt, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - res = _parseArray(dt, _dateFormat.parseTimes, utc, timeZoneOffset); + res = _parseArray(dt, _dateFormat.parseTimes, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } if (utc || timeZoneOffset === 0) { if (!dt.includes('UTC') && !dt.includes('utc') && !dt.includes('+') && !dt.includes('-')) { @@ -1666,10 +1668,11 @@ function _getInt(str, i, minlength, maxlength) { * @param {string} format format of the value * @param {boolean} [utc] indicates if the date should be in utc * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @returns {object} a Date object with value:{Date} or error:{String} if pattern does not match. */ -function _getDateFromFormat(val, format, utc, timeZoneOffset) { - // console.debug(`getDateFromFormat val=${val} format=${format} timeZoneOffset=${timeZoneOffset}`); // eslint-disable-line +function _getDateFromFormat(val, format, utc, timeZoneOffset, dNow) { + // console.debug(`getDateFromFormat val=${val} format=${format} utc=${utc} timeZoneOffset=${timeZoneOffset}`); // eslint-disable-line val = String(val); if (format.slice(0, 4) === 'UTC:' || format.slice(0, 4) === 'utc:') { @@ -1683,17 +1686,27 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset) { val = String(val); format = String(format); - const dNow = new Date(); let i_val = 0; let i_format = 0; let x; let y; - let year = dNow.getFullYear(); - let month = dNow.getMonth() + 1; - let date = dNow.getDate(); - let hour = dNow.getHours(); - let min = dNow.getMinutes(); - let sec = dNow.getSeconds(); - let misec = dNow.getMilliseconds(); + + let year = 0; + let month = 0; + let date = 0; + let hour = 0; + let min = 0; + let sec = 0; + let misec = 0; + + if (dNow) { + year = dNow.getFullYear(); + month = dNow.getMonth() + 1; + date = dNow.getDate(); + hour = dNow.getHours(); + min = dNow.getMinutes(); + sec = dNow.getSeconds(); + misec = dNow.getMilliseconds(); + } let ampm = ''; while (i_format < format.length) { @@ -1895,11 +1908,14 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset) { * parses an array of different formats * @param {string} val date string to parse * @param {Array.} listToCheck a list of strings with different formats to check + * @param {boolean} [utc] indicates if the date should be in utc + * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @returns {Date|null} a Date object or **null** if no patterns match. */ -function _parseArray(val, listToCheck, utc, timeZoneOffset) { +function _parseArray(val, listToCheck, utc, timeZoneOffset, dNow) { for (let i = 0, n = listToCheck.length; i < n; i++) { - const res = _getDateFromFormat(val, listToCheck[i], utc, timeZoneOffset); + const res = _getDateFromFormat(val, listToCheck[i], utc, timeZoneOffset, dNow); if (res.value !== null && typeof res.value !== 'undefined') { return res.value; } @@ -1926,15 +1942,18 @@ function _isTimestamp(str) { * d/M/y d-M-y d.M.y d-MMM d/M d-M * @param {string} val date string to parse * @param {boolean} [preferMonthFirst] if **true** the method to search first for formats like M/d/y (e.g. American format) before d/M/y (e.g. European). + * @param {boolean} [utc] indicates if the date should be in utc + * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @returns {Date|null} a Date object or **null** if no patterns match. */ -function _parseDate(val, preferMonthFirst, utc, timeZoneOffset) { +function _parseDate(val, preferMonthFirst, utc, timeZoneOffset, dNow) { // console.debug('_parseDate val=' + val + ' - preferMonthFirst=' + preferMonthFirst); // eslint-disable-line - let res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.monthFirst : _dateFormat.parseDates.dateFirst, utc, timeZoneOffset); + let res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.monthFirst : _dateFormat.parseDates.dateFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.dateFirst : _dateFormat.parseDates.monthFirst, utc, timeZoneOffset); + res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.dateFirst : _dateFormat.parseDates.monthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - return _parseArray(val, _dateFormat.parseDates.general, utc, timeZoneOffset); + return _parseArray(val, _dateFormat.parseDates.general, utc, timeZoneOffset, dNow); } /** @@ -1942,9 +1961,12 @@ function _parseDate(val, preferMonthFirst, utc, timeZoneOffset) { * number of possible date formats to get the value. * @param {string} val date string to parse * @param {boolean} [preferMonthFirst] if **true** the method to search first for formats like M/d/y (e.g. American format) before d/M/y (e.g. European). + * @param {boolean} [utc] indicates if the date should be in utc + * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @returns {Date|null} a Date object or **null** if no patterns match. */ -function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset) { +function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset, dNow) { // console.debug('_parseDateTime val=' + val + ' - preferMonthFirst=' + preferMonthFirst); // eslint-disable-line /** mixes two lists */ function mix(lst1, lst2, result) { @@ -1965,7 +1987,7 @@ function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset) { checkList = mix(_dateFormat.parseDates.monthFirst, _dateFormat.parseTimes, checkList); } checkList = mix(_dateFormat.parseDates.general, _dateFormat.parseTimes, checkList); - return _parseArray(val, checkList, utc, timeZoneOffset); + return _parseArray(val, checkList, utc, timeZoneOffset, dNow); } /** @@ -1975,9 +1997,12 @@ function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset) { * @param {Array.} [dayNames] list of day names * @param {Array.} [monthNames] list of month names * @param {Array.} [dayDiffNames] list of day diff names + * @param {boolean} [utc] indicates if the date should be in utc + * @param {number} [timeZoneOffset] timezone offset in minutes of the input date + * @param {Date} [dNow] base Date, if defined missing parts will be used from this Date object * @returns {Date} a Date object or throws an error if no patterns match. */ -function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, utc, timeZoneOffset) { +function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, utc, timeZoneOffset, dNow) { // console.debug('parseDateFromFormat date=' + util.inspect(date, { colors: true, compact: 10, breakLength: Infinity }) + ' - format=' + util.inspect(format, { colors: true, compact: 10, breakLength: Infinity }) + ' [' + dayNames + '] - [' + monthNames + '] [' + dayDiffNames + ']'); // eslint-disable-line if (dayNames) { _dateFormat.i18n.dayNames = dayNames; @@ -1999,7 +2024,7 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u let res = null; if (isNaN(format)) { // timeparse_TextOther - res = _getDateFromFormat(date, format, utc, timeZoneOffset); + res = _getDateFromFormat(date, format, utc, timeZoneOffset, dNow); if (res.error) { throw new Error('could not evaluate format of ' + date + ' (' + format + ') - ' + res.error); } @@ -2007,11 +2032,11 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u } else { const tryparse = (val, preferMonthFirst) => { // console.debug('try parse ' + util.inspect(val, { colors: true, compact: 10, breakLength: Infinity }) + ' preferMonthFirst=' + preferMonthFirst); // eslint-disable-line - let res = _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset); + let res = _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - res = _parseDate(val, preferMonthFirst, utc, timeZoneOffset); + res = _parseDate(val, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } - res = _parseArray(val, _dateFormat.parseTimes, utc, timeZoneOffset); + res = _parseArray(val, _dateFormat.parseTimes, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } res = Date.parse(val); if (!isNaN(res)) { @@ -2093,29 +2118,29 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u /** * replaces placeholder in a string - * @param {string} topic - the topic - * @param {object} topicAttrs - an object with different propertys who are allowed as placeholders - * @returns {string} the topic with replaced placeholders + * @param {string} str - the string to replace placeholders + * @param {object} strAttrs - an object with different propertys who are allowed as placeholders + * @returns {string} the string with replaced placeholders */ -function topicReplace(topic, topicAttrs) { - if (!topic || typeof topicAttrs !== 'object') { - return topic; +function topicReplace(str, strAttrs) { + if (!str || typeof strAttrs !== 'object') { + return str; } - const topicAttrsLower = {}; - Object.keys(topicAttrs).forEach(k => { - topicAttrsLower[k.toLowerCase()] = topicAttrs[k]; + const strAttrsLower = {}; + Object.keys(strAttrs).forEach(k => { + strAttrsLower[k.toLowerCase()] = strAttrs[k]; }); - const match = topic.match(/\${[^}]+}/g); + const match = str.match(/[\$#]{[^}]+}/g); if (match) { match.forEach(v => { const key = v.substr(2, v.length - 3); - const rx = new RegExp('\\${' + key + '}', 'g'); + const rx = new RegExp('\[\$#]{' + key + '}', 'g'); const rkey = key.toLowerCase(); - const res = Object.prototype.hasOwnProperty.call(topicAttrsLower, rkey) ? topicAttrsLower[rkey] : ''; - topic = topic.replace(rx, res); + const res = Object.prototype.hasOwnProperty.call(strAttrsLower, rkey) ? strAttrsLower[rkey] : ''; + str = str.replace(rx, res); }); } - return topic; + return str; } \ No newline at end of file diff --git a/nodes/locales/en-US/10-position-config.json b/nodes/locales/en-US/10-position-config.json index f5dd567..5582d8c 100644 --- a/nodes/locales/en-US/10-position-config.json +++ b/nodes/locales/en-US/10-position-config.json @@ -481,8 +481,8 @@ "error-title": "internal error", "error-init": "error '__message__', retry in __time__min", "warn-init": "warning '__message__', retry in __time__min", - "config-missing": "Node not properly configured!! Configuration is missing (no configuration node is selected).", - "config-missing-state": "No configuration node is selected.", + "config-missing": "Node not properly configured!! Configuration is missing or wrong (no valid configuration is selected)!", + "config-missing-state": "No valid configuration is selected!", "unknownPropertyOperator": "error, the used property operator __propertyOp__=\"__propertyOpText__\" is unknown!", "unknownCompareOperator": "error, the used compare operator \"__operator__\" is unknown! (\"__opTypeA__.__opValueA__\" compare to \"__opTypeB__.__opValueB__\")", "notEvaluableProperty":"Error: could not evaluate __type__.__value__!", diff --git a/test/20_time-inject_spec.js b/test/20_time-inject_spec.js new file mode 100644 index 0000000..b2b7248 --- /dev/null +++ b/test/20_time-inject_spec.js @@ -0,0 +1,1659 @@ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable require-jsdoc */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-unused-vars */ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +/* global Context context describe beforeEach before afterEach after it */ + +const should = require('should'); +const sinon = require("sinon"); +require("should-sinon"); + +const helper = require('node-red-node-test-helper'); + +// Nodes +// const functionNode = require('@node-red/nodes/core/core/80-function.js'); +const nodeConfig = require('../nodes/10-position-config.js'); +const nodeTimeInject = require('../nodes/20-time-inject.js'); + +const credentials = {'nc1': {'posLatitude': '51.16406769771653', 'posLongitude': '10.447609909242438'}}; +const cfgNode = { + id: 'nc1', + type: 'position-config', + name: 'Mitte von Deutschland', + isValide: 'true', + angleType: 'deg', + timeZoneOffset: '99', + timeZoneDST: '0', + stateTimeFormat: '3', + stateDateFormat: '12', + contextStore: '' +}; +const tabNode = {id: 'flow', type: 'tab', label: 'FLOW' }; +const groupNode = {id: 'g0', type: 'group', name: 'GROUP' }; +const hlpNode = {id: 'n2', type: 'helper'}; + +/* +const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + g: "g0", + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + { + p: '', + pt: 'msgPayload', + v: '${NR_NODE_NAME}', + vt: 'str', + o: '', + oT: 'none', + oM: '60000', + f: 0, + fS: 0, + fI: '0', + next: true, + days: '*', + months: '*', + onlyOddDays: false, + onlyEvenDays: false, + onlyOddWeeks: false, + onlyEvenWeeks: false + }, + { + p: '', + pt: 'msgTopic', + v: 'test-topic', + vt: 'str', + o: '', + oT: 'none', + oM: '60000', + f: 0, + fS: 0, + fI: '0', + next: false, + days: '*', + months: '*', + onlyOddDays: false, + onlyEvenDays: false, + onlyOddWeeks: false, + onlyEvenWeeks: false + } + ], + injectTypeSelect: 'none', + intervalCount: 1, + intervalCountType: 'num', + intervalCountMultiplier: 60000, + time: '', + timeType: 'entered', + offset: 0, + offsetType: 'none', + offsetMultiplier: 60000, + timeEnd: '', + timeEndType: 'entered', + timeEndOffset: 0, + timeEndOffsetType: 'none', + timeEndOffsetMultiplier: 60000, + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + property: '', + propertyType: 'none', + propertyCompare: 'true', + propertyThreshold: '', + propertyThresholdType: 'num', + timeAlt: '', + timeAltType: 'entered', + timeAltDays: '*', + timeAltOnlyOddDays: false, + timeAltOnlyEvenDays: false, + timeAltOnlyOddWeeks: false, + timeAltOnlyEvenWeeks: false, + timeAltMonths: '*', + timeAltOffset: 0, + timeAltOffsetType: 'none', + timeAltOffsetMultiplier: 60000, + once: false, + onceDelay: 0.1, + recalcTime: 2, + wires: [['n2']] + }, cfgNode, hlpNode]; +*/ + +describe('time inject node', () => { + beforeEach(function (done) { + helper.startServer(done); + }); + + afterEach(function (done) { + helper.unload().then(function () { + helper.stopServer(done); + }); + }); + + describe('test configuration', function() { + it('fail if missing configuration ', done => { + const flow = [ + { + id: 'n1', + type: 'time-inject', + name: 'NAME', + nameInt: 'test', + positionConfig: '', + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n1 = helper.getNode('n1'); + try { + n1.status.should.be.calledOnce(); + n1.error.should.be.calledOnce().and.calledWith('node-red-contrib-sun-position/position-config:errors.config-missing'); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('fail if latitude missing ', done => { + const flow = [ + { + id: "n1", + type: "time-inject", + name: "", + nameInt: "Zeitpunkt", + positionConfig: "nc1", + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: "none", + once: false, + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('fail if longitude missing ', done => { + const flow = [ + { + id: "n1", + type: "time-inject", + name: "", + nameInt: "Zeitpunkt", + positionConfig: "nc1", + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: "none", + once: false, + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '5'}}; + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('fail if latitude invalid ', done => { + const flow = [ + { + id: "n1", + type: "time-inject", + name: "", + nameInt: "Zeitpunkt", + positionConfig: "nc1", + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: "none", + once: false, + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '90.1', 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('fail if longitude invalid ', done => { + const flow = [ + { + id: "n1", + type: "time-inject", + name: "", + nameInt: "Zeitpunkt", + positionConfig: "nc1", + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: "none", + once: false, + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '51', 'posLongitude': '180.1'}}; + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch (err) { + done(err); + } + }); + }); + }); + + describe('basic tests', function() { + function basicTest(type, val, rval, topic = 't1', adFkt = null) { + it('inject value ('+type+')', function(done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: val, vt: type }, + {p: '', pt: 'msgTopic', v: topic, vt: 'str'} + ], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n1 = helper.getNode('n1'); // inject node + // const nc1 = helper.getNode('nc1'); // inject node + const n2 = helper.getNode('n2'); // helper node + try { + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'injectNodeName'); + + n2.on('input', function (msg) { + try { + msg.should.have.property('topic', topic); + if (rval) { + msg.should.have.property('payload'); + // console.log(msg); + // console.log(rval); + should.deepEqual(msg.payload, rval); + } else { + msg.should.have.property('payload', val); + } + done(); + } catch (err) { + done(err); + } + }); + if (adFkt) { + adFkt(); + } + n1.receive({}); + } catch (err) { + done(err); + } + }); + }); + } + + basicTest('num', 10); + basicTest('str', '10'); + basicTest('bool', true); + // 'date', + const val_json = '{ "x":"vx", "y":"vy", "z":"vz" }'; + basicTest('json', val_json, JSON.parse(val_json)); + const val_buf = '[1,2,3,4,5]'; + basicTest('bin', val_buf, Buffer.from(JSON.parse(val_buf))); + basicTest('env', 'NR_TEST', 'foo', 'env test', () => { process.env.NR_TEST = 'foo'; }); + basicTest('strPlaceholder', '#{id}', 'n1', 'placeholder test'); + basicTest('strPlaceholder', '#{path}', 'flow/n1', 'placeholder test'); + // TODO: maybe add test for 'flow' + // TODO: maybe add test for 'global' + + it('inject value (date) ', function(done) { + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds()); + + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + should(msg.payload).be.greaterThan(timestamp.getTime()); + should(msg.payload).be.lessThan(timestamp.getTime() + 80); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject value (dateSpecific) UTC ', function(done) { + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds()); + + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', + o: "", oT: "none", oM: "60000", + f: 1, fS: 1, fT: "UTC Datum und Zeit", fI: "1", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + let d = new Date(msg.payload); + should(d.getTime()).be.greaterThan(timestamp.getTime() - 1000); + should(d.getTime()).be.lessThan(timestamp.getTime() + 1000); + msg.payload.substring(0, msg.payload.length - 6).should.equal(timestamp.toUTCString().substring(0, timestamp.toUTCString().length - 6)); + + done(); + } catch(err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject value (dateSpecific) UNIX-time ', function(done) { + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds()); + + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fT: "Millisekunden UNIX-Zeit", fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + should(msg.payload).be.greaterThan(timestamp.getTime()); + should(msg.payload).be.lessThan(timestamp.getTime() + 80); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject value (entered) ', function(done) { + const timestamp = new Date(); + timestamp.setSeconds(0); + timestamp.setHours(10); + timestamp.setMinutes(0); + timestamp.setMilliseconds(0); + + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '10:00', vt: 'entered', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(timestamp.getTime()); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject value (dateEntered) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '1.1.2020 10:00', vt: 'dateEntered', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(1577869200000); // 1577869200000 - 1.1.2020 10:00 + done(); + } catch(err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject value (pdsTime-astronomicalDawn) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'astronomicalDawn', vt: 'pdsTime', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(1577856059986); // Wed Jan 01 2020 05:20:59 GMT+0000 + done(); + } catch(err) { + done(err); + } + }); + n1.receive({ __ts__input_date: new Date(1577851200000)}); // 1577851200000 - 1.1.2020 5:00 + }); + }); + + it('inject value (pdsTime-civilDawn) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'civilDawn', vt: 'pdsTime', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(1577861034863); // Wed Jan 01 2020 06:43:54 GMT+0000 + done(); + } catch(err) { + done(err); + } + }); + n1.receive({ __ts__input_date: new Date(1577851200000)}); // 1577851200000 - 1.1.2020 5:00 + }); + }); + + it('inject value (pdmTime-rise) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'rise', vt: 'pdmTime', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(1577875980344); // Wed Jan 01 2020 10:53:00 GMT+0000 + done(); + } catch(err) { + done(err); + } + }); + n1.receive({ __ts__input_date: new Date(1577851200000)}); // 1577851200000 - 1.1.2020 5:00 + }); + }); + + it('inject value (dayOfMonth) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'fMon', vt: 'dayOfMonth', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.equal(1578265200000); // Sun Jan 05 2020 23:00:00 GMT+0000 + done(); + } catch(err) { + done(err); + } + }); + n1.receive({ __ts__input_date: new Date(1577851200000)}); // 1577851200000 - 1.1.2020 5:00 + }); + }); + + it('inject value (pdsTimeNow) ', function(done) { + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'myNode', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'pdsTimeNow', + o: "", oT: "none", oM: "60000", + f: 0, fS: 0, fI: "0", + next: true, + days: "*", months: "*", + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n1.status.should.be.calledOnce(); + n1.should.have.property('name', 'myNode'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'tx'); + msg.should.have.property('payload'); + msg.payload.should.have.keys('next', 'last'); + msg.payload.next.should.have.keys('value', 'name', 'pos', 'valid', 'elevation'); + msg.payload.last.should.have.keys('value', 'name', 'pos', 'valid', 'elevation'); + msg.payload.next.name.should.equal('astronomicalDawn'); + msg.payload.last.name.should.equal('nadir'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({ __ts__input_date: new Date(1577851200000)}); // 1577851200000 - 1.1.2020 5:00 + }); + }); + + /* + // TODO: add test for ... + types.SunCalc, + types.SunInSky, + types.MoonCalc, + types.MoonPhase, + types.SunAzimuth, + types.SunElevation, + types.SunAzimuthRad, + types.SunElevationRad, + types.SunTimeByAzimuth, + types.SunTimeByElevationObj, + types.SunTimeByAzimuthRad, + types.SunTimeByElevationObjRad, + types.isDST, + types.WeekOfYear, + types.WeekOfYearEven, + types.DayOfYear, + types.DayOfYearEven, + types.numPercent, + types.randomNumber, + types.randmNumCachedDay, + types.randmNumCachedWeek, + types.nodeId, + types.nodeName, + types.nodePath + */ + + // basicTest('flow', 'flowValue', 'changeMe', 'env test topic', () => { n1.context().flow.set("flowValue", "changeMe"); }); + it('should inject multiple properties ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't1', vt: 'str'}, + {p:'x', pt: 'msg', v: 10, 'vt':'num'}, + {p:'y', pt: 'msg', v: 'x+2', 'vt':'jsonata'} + ], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('topic', 't1'); + msg.should.have.property('payload', 'foo'); + msg.should.have.property('x', 10); + msg.should.have.property('y', 12); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + + it('should inject custom properties in message', function (done) { + // n1: inject node with { topic:"static", payload:"static", bool1:true, str1:"1" } + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'static', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 'static', vt: 'str'}, + {p:'bool1', pt: 'msg', v:'true', vt:'bool'}, + {p:'str1', pt: 'msg', v:'1', vt:'str'} + ], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.not.have.property('payload'); // payload removed + msg.should.have.property('topic', 't_override'); // changed value to t_override + msg.should.have.property('str1', 1);// changed type from str to num + msg.should.have.property('num1', 1);// new prop + msg.should.have.property('bool1', false);// changed value to false + done(); + } catch (err) { + done(err); + } + }); + n1.receive({ __user_inject_props__: { + props: [ + {p:'topic', pt: 'msgTopic', v:'t_override', vt:'str'}, // change value to t_override + {p:'str1', pt: 'msg', v:'1', vt:'num'}, // change type + {p:'num1', pt: 'msg', v:'1', vt:'num'}, // new prop + {p:'bool1', pt: 'msg', v:'false', vt:'bool'} // change value to false + ]} + }); + }); + }); + + it('should report invalid JSONata expression', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '@', vt: 'jsonata'}, + {p: '', pt: 'msgTopic', v: 't1', vt: 'str'} + ], + injectTypeSelect: 'none', + + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + let count = 0; + n2.on('input', function (msg) { + try { + msg.should.have.property('topic', 't1'); + msg.should.not.have.property('payload'); + count++; + if (count === 2) { + done(); + } + } catch (err) { + done(err); + } + }); + n1.on('call:error', function(_err) { + count++; + if (count === 2) { + done(); + } + }); + n1.receive({}); + }); + }); + + }); /* basic tests */ + + describe('environment variable', function() { + it('inject name of node as environment variable ', done => { + const flow = [ + { + id: 'n1', + type: 'time-inject', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_NODE_NAME', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', msg => { + try { + msg.should.have.property('payload', 'NAME'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of node as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_NODE_ID', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'n1'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject path of node as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_NODE_PATH', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'flow/n1'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject name of flow as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_FLOW_NAME', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'FLOW'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of flow as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_FLOW_ID', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'flow'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject name of group as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_GROUP_NAME', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, groupNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'GROUP'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of group as environment variable ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'NR_GROUP_ID', vt: 'env'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, groupNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'g0'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + + it('inject name of node as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_NODE_NAME}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'NAME'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of node as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_NODE_ID}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'n1'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject path of node as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_NODE_PATH}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'flow/n1'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + + it('inject name of flow as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_FLOW_NAME}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'FLOW'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of flow as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_FLOW_ID}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'flow'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject name of group as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_GROUP_NAME}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, groupNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'GROUP'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of group as environment variable by substitution ', function (done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + g: 'g0', + name: 'NAME', + nameInt: 'test', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: '${NR_GROUP_ID}', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, groupNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function () { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + n2.on('input', function (msg) { + try { + msg.should.have.property('payload', 'g0'); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + }); /* environment variable */ + + describe('inject once', function() { + it('should inject once with default delay property', function(done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [], + injectTypeSelect: 'none', + once: true, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n1 = helper.getNode('n1'); + n1.should.have.property('onceDelay', 0.1); + done(); + }); + }); + + it('should inject once with default delay', function(done) { + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds() + 1); + + const flow1 = [ + { + id: 'n1', + type: 'time-inject', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 't1', vt: 'str'}], + injectTypeSelect: 'none', + once: true, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + const n2 = helper.getNode('n2'); + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 't1'); + msg.should.have.property('type', 'once/startup'); + msg.should.have.property('payload'); + should(msg.payload).be.lessThan(timestamp.getTime()); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + it('should inject once with 500 msec. delay property', function(done) { + this.timeout(2700); // have to wait for the inject with delay of two seconds + const flow = [ + { + id: 'n1', + type: 'time-inject', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 't1', vt: 'str'}], + injectTypeSelect: 'none', + once: true, + onceDelay: 0.5, + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n1 = helper.getNode('n1'); + n1.should.have.property('onceDelay', 0.5); + done(); + }); + }); + + it('should inject once with delay of two seconds', function(done) { + this.timeout(2700); // have to wait for the inject with delay of two seconds + + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds() + 1); + + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: 'test', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 't1', vt: 'str'}], + injectTypeSelect: 'none', + once: true, + onceDelay: 2, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n2 = helper.getNode('n2'); + n2.on('input', function(msg) { + msg.should.have.property('topic', 't1'); + msg.should.have.property('type', 'once/startup'); + msg.should.have.property('payload'); + should(msg.payload).be.greaterThan(timestamp.getTime()); + done(); + }); + }); + }); + }); /* inject once */ + + describe('inject repeatedly', function() { + it('should inject repeatedly', function(done) { + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '"payload" ↻0.2s', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'payload', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't2', vt: 'str'}], + injectTypeSelect: 'interval', + intervalCount: '0.2', + intervalCountType: 'num', + intervalCountMultiplier: 1000, + once: false, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n2 = helper.getNode('n2'); + let count = 0; + n2.on('input', function(msg) { + msg.should.have.property('topic', 't2'); + msg.should.have.property('payload', 'payload'); + count += 1; + if (count > 2) { + helper.clearFlows().then(function() { + done(); + }); + } + }); + }); + }); + + it('should inject once with delay of two seconds and repeatedly', function(done) { + this.timeout(2700); // have to wait for the inject with delay of two seconds + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds() + 1); + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '"payload" ↻0.2s', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 'tx2', vt: 'str'}], + injectTypeSelect: 'interval', + intervalCount: '0.2', + intervalCountType: 'num', + intervalCountMultiplier: 1000, + once: true, + onceDelay: 1.2, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n2 = helper.getNode('n2'); + let count = 0; + n2.on('input', function(msg) { + msg.should.have.property('topic', 'tx2'); + should(msg.payload).be.greaterThan(timestamp.getTime()); + count += 1; + if (count > 2) { + helper.clearFlows().then(function() { + done(); + }); + } + }); + }); + }); + }); /* inject repeatedly */ + + // TODO: implement inject by time + + /* + it('should inject with cron', function(done) { + helper.load(nodeTimeInject, [{id:'n1', type:'inject', + payloadType:'date', topic: 't3', + crontab: '* * * * * *', wires:[['n3']] }, + {id:'n3', type:'helper'}], + function() { + const n3 = helper.getNode('n3'); + n3.on('input', function(msg) { + msg.should.have.property('topic', 't3'); + msg.should.have.property('payload').be.a.Number(); + helper.clearFlows().then(function() { + done(); + }); + }); + }); + }); /* should inject with cron */ + + describe('post', function() { + it('should inject message', function(done) { + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '"payload" ↻0.2s', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'hello', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't4', vt: 'str'}], + injectTypeSelect: 'interval', + intervalCount: '0.2', + intervalCountType: 'num', + intervalCountMultiplier: 1000, + once: false, + wires: [['n4']] + }, cfgNode, {id: 'n4', type: 'helper'}, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n4 = helper.getNode('n4'); + n4.on('input', function(msg) { + msg.should.have.property('topic', 't4'); + msg.should.have.property('payload', 'hello'); + helper.clearFlows().then(function() { + done(); + }); + }); + try { + helper.request() + .post('/time-inject/n1') + .expect(200).end(async function(err) { + if (err) { + // eslint-disable-next-line no-console + console.log(err); + await helper.clearFlows(); + done(err); + } + }); + } catch(err) { + done(err); + } + }); + }); + + it('should inject custom properties in posted message', function(done) { + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '"payload" ↻0.2s', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'hello', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't4', vt: 'str'}], + injectTypeSelect: 'interval', + intervalCount: '0.2', + intervalCountType: 'num', + intervalCountMultiplier: 1000, + once: false, + wires: [['n4']] + }, cfgNode, {id: 'n4', type: 'helper'}, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n4 = helper.getNode('n4'); + n4.on('input', function(msg) { + msg.should.not.have.property('payload'); // payload removed + msg.should.have.property('topic', 't_override'); // changed value to t_override + msg.should.have.property('str1', '1'); // injected prop + msg.should.have.property('num1', 1); // injected prop + msg.should.have.property('bool1', true); // injected prop + + helper.clearFlows().then(function() { + done(); + }); + }); + try { + helper.request() + .post('/time-inject/n1') + .send({ __user_inject_props__: { + props: [ + {p:'topic', pt: 'msgTopic', v:'t_override', vt:'str'}, // change value to t_override + {p:'str1', pt: 'msg', v:'1', vt:'str'}, // new prop + {p:'num1', pt: 'msg', v:'1', vt:'num'}, // new prop + {p:'bool1', pt: 'msg', v:'true', vt:'bool'} // new prop + ]} + }) + .expect(200).end(async function(err) { + if (err) { + // eslint-disable-next-line no-console + console.log(err); + await helper.clearFlows(); + done(err); + } + }); + } catch(err) { + done(err); + } + }); + }); + + it('should fail for invalid node', function(done) { + helper.request().post('/time-inject/invalid').expect(404).end(done); + }); + }); /* post */ +}); \ No newline at end of file From a5d9b1fb733ebe0ec9d90c9c59ca2eeb05c7f96c Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 22 Feb 2022 17:22:35 +0100 Subject: [PATCH 15/44] fixed type names --- nodes/20-time-inject.html | 25 ++++++++--------- nodes/21-within-time-switch.html | 48 ++++++++++++++++---------------- nodes/30-sun-position.html | 24 ++++++++-------- nodes/80-blind-control.html | 36 ++++++++++++------------ nodes/81-clock-timer.html | 36 ++++++++++++------------ 5 files changed, 84 insertions(+), 85 deletions(-) diff --git a/nodes/20-time-inject.html b/nodes/20-time-inject.html index b424210..05de998 100644 --- a/nodes/20-time-inject.html +++ b/nodes/20-time-inject.html @@ -1385,12 +1385,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onChange(_type, _value) { const injectType = $('#node-input-injectTypeSelect').val(); @@ -1443,7 +1443,6 @@ }); } }); // time - node.offsetMultiplier = multiplierUpdate(node.offsetMultiplier, 'offsetMultiplier', () => $('#node-input-time').change()); setupTInput(node, { typeProp: 'offsetType', @@ -1479,12 +1478,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onChange(_type, _value) { const injectType = $('#node-input-injectTypeSelect').val(); diff --git a/nodes/21-within-time-switch.html b/nodes/21-within-time-switch.html index 2de14fc..3404b1c 100644 --- a/nodes/21-within-time-switch.html +++ b/nodes/21-within-time-switch.html @@ -416,12 +416,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onFocus(_type, _value) { const plVType = $('#node-input-startTime').typedInput('type'); @@ -563,12 +563,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onFocus(_type, _value) { const plVType = $('#node-input-endTime').typedInput('type'); @@ -724,12 +724,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onFocus(_type, _value) { if (($('#node-input-startTimeAlt').typedInput('type') === 'none') || @@ -880,12 +880,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onFocus(_type, _value) { if (($('#node-input-endTimeAlt').typedInput('type') === 'none') || diff --git a/nodes/30-sun-position.html b/nodes/30-sun-position.html index 5daa012..083ab82 100644 --- a/nodes/30-sun-position.html +++ b/nodes/30-sun-position.html @@ -203,12 +203,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onChange(_type, _value) { if ($('#node-input-start').typedInput('type') === types.Undefined.value) { @@ -258,12 +258,12 @@ 'env', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ], onChange(_type, _value) { if ($('#node-input-end').typedInput('type') === types.Undefined.value) { diff --git a/nodes/80-blind-control.html b/nodes/80-blind-control.html index e91a36e..092d696 100644 --- a/nodes/80-blind-control.html +++ b/nodes/80-blind-control.html @@ -2619,12 +2619,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeRegInput.typedInput('width', '40%'); @@ -2674,12 +2674,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeMinInput.typedInput('width', '40%'); @@ -2729,12 +2729,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeMaxInput.typedInput('width', '40%'); diff --git a/nodes/81-clock-timer.html b/nodes/81-clock-timer.html index 1e6a803..ab53515 100644 --- a/nodes/81-clock-timer.html +++ b/nodes/81-clock-timer.html @@ -1724,12 +1724,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeRegInput.typedInput('width', '40%'); @@ -1779,12 +1779,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeMinInput.typedInput('width', '40%'); @@ -1834,12 +1834,12 @@ 'jsonata', types.SunTimeByAzimuth, types.SunTimeByAzimuthRad, - types.pdsTimeByElevationNext, - types.pdsTimeByElevationNextRad, - types.pdsTimeByElevationRise, - types.pdsTimeByElevationRiseRad, - types.pdsTimeByElevationSet, - types.pdsTimeByElevationSetRad + types.SunTimeByElevationNext, + types.SunTimeByElevationNextRad, + types.SunTimeByElevationRise, + types.SunTimeByElevationRiseRad, + types.SunTimeByElevationSet, + types.SunTimeByElevationSetRad ] }); $dialogTimeMaxInput.typedInput('width', '40%'); From 00e0f09ca73b3f5d1c2e2923dcc50c1ed3b24b77 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:03:46 +0100 Subject: [PATCH 16/44] added test for inject node --- nodes/20-time-inject.js | 66 +++++----- test/20_time-inject_spec.js | 237 +++++++++++++++++++++--------------- 2 files changed, 168 insertions(+), 135 deletions(-) diff --git a/nodes/20-time-inject.js b/nodes/20-time-inject.js index 83a11a1..cf787d0 100644 --- a/nodes/20-time-inject.js +++ b/nodes/20-time-inject.js @@ -430,16 +430,13 @@ module.exports = function (RED) { /** * get the schedule time * @param {Date} time - time to schedule - * @param {number} [limit] - minimal time limit to schedule * @returns {number} milliseconds until the defined Date */ - node.tsGetNextScheduleTime = (time, limit) => { + node.tsGetNextScheduleTime = time => { const dNow = new Date(); - let millisec = time.valueOf() - dNow.valueOf(); - if (limit) { - while (millisec < limit) { - millisec += 86400000; // 24h - } + let millisec = (time.valueOf() - dNow.valueOf()) - 5; + while (millisec < 10) { + millisec += 86400000; // 24h } return millisec; }; @@ -563,21 +560,21 @@ module.exports = function (RED) { case tInj.interval: if (doEmit === true) { node.emit('input', { - type: 'once/startup' + _inject_type: 'once/startup' }); // will create timeout } // node.debug('initialize - absolute Intervall'); - node.send(node.prepOutMsg({ type: 'interval-start' })); + node.send(node.prepOutMsg({ _inject_type: 'interval-start' })); node.createNextInterval(); break; case tInj.timer: // node.debug('initialize - timer'); if (doEmit === true) { node.emit('input', { - type: 'once/startup' + _inject_type: 'once/startup' }); // will create timeout } else { - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'initial'); } break; case tInj.intervalBtwStartEnd: @@ -585,11 +582,11 @@ module.exports = function (RED) { // node.debug('initialize - Intervall timer/amount/fromStart'); if (doEmit === true) { node.emit('input', { - type: 'once/startup' + _inject_type: 'once/startup' }); // will create timeout } if (!node.initializeStartTimer(node)) { - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'initial'); } break; default: @@ -597,7 +594,7 @@ module.exports = function (RED) { node.doSetStatus(node, 'green'); if (doEmit === true) { node.emit('input', { - type: 'once/startup' + _inject_type: 'once/startup' }); // will create timeout } } @@ -628,7 +625,7 @@ module.exports = function (RED) { let millisecEnd = 1000 * 60 * 60 * 24; // 24h if ((node.nextEndTime !== null) && (typeof node.nextEndTime !== 'undefined')) { // node.debug('timeout ' + node.nextEndTime + ' is in ' + millisec + 'ms'); - millisecEnd = node.tsGetNextScheduleTime(node.nextEndTime, 10); + millisecEnd = node.tsGetNextScheduleTime(node.nextEndTime); } return millisecEnd; }; @@ -655,7 +652,7 @@ module.exports = function (RED) { node.timeOutEndObj = null; clearInterval(node.intervalObj); node.intervalObj = null; - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'timeOutEnd'); }, millisecEnd); }; // doCreateEndTimeout @@ -665,7 +662,7 @@ module.exports = function (RED) { node.doRecalcStartTimeOut = () => { try { node.debug('performing a recalc of the next inject time'); - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'timeOutRecalc'); } catch (err) { node.error(err.message); node.log(util.inspect(err, Object.getOwnPropertyNames(err))); @@ -724,7 +721,7 @@ module.exports = function (RED) { if (node.intervalTime < intervalMax) { node.debug(`interval is less than max=${intervalMax}ms, create absolute interval of ${node.intervalTime}ms`); node.intervalObj = setInterval(() => { - node.send(node.prepOutMsg({ type: 'interval' })); + node.send(node.prepOutMsg({ _inject_type: 'interval' })); }, node.intervalTime); if (node.intervalTime > 43200000) { // 12h node.status({ @@ -746,7 +743,7 @@ module.exports = function (RED) { } else { node.createNextInterval(); } - node.send(node.prepOutMsg({ type: 'interval' })); + node.send(node.prepOutMsg({ _inject_type: 'interval' })); }, millisec); node.status({ text: node.positionConfig.toTimeString(new Date(tsStart)) + ' ↻' + node.intervalText @@ -808,13 +805,13 @@ module.exports = function (RED) { node.doCreateEndTimeout(node); clearInterval(node.intervalObj); node.doSetStatus(node, 'green'); - node.send(node.prepOutMsg({ type: 'interval-time-start' })); + node.send(node.prepOutMsg({ _inject_type: 'interval-time-start' })); node.intervalObj = setInterval(() => { node.IntervalCountCurrent++; if (node.injType !== node.intervalAmount) { - node.send(node.prepOutMsg({ type: 'interval-time' })); + node.send(node.prepOutMsg({ _inject_type: 'interval-time' })); } else if (node.IntervalCountCurrent < node.IntervalCountMax) { - node.send(node.prepOutMsg({ type: 'interval-amount' })); + node.send(node.prepOutMsg({ _inject_type: 'interval-amount' })); } }, node.intervalTime); }; @@ -825,8 +822,8 @@ module.exports = function (RED) { * @param {boolean} [_onInit] - _true_ if is in initialisation * @returns {object} state or error */ - node.doCreateStartTimeout = node => { - // node.debug(`doCreateStartTimeout node.timeStartData=${util.inspect(node.timeStartData, { colors: true, compact: 10, breakLength: Infinity })}`); + node.doCreateStartTimeout = (node, reason) => { + // node.debug(`doCreateStartTimeout ${reason} node.timeStartData=${util.inspect(node.timeStartData, { colors: true, compact: 10, breakLength: Infinity })}`); if (node.injType === tInj.none || node.injType === tInj.interval) { return; @@ -865,7 +862,7 @@ module.exports = function (RED) { const nextStartTimeData = node.positionConfig.getTimeProp(node, {}, node.timeStartData); if (nextStartTimeData.error) { node.debug('node.nextStartTimeData=' + util.inspect(nextStartTimeData, { colors: true, compact: 10, breakLength: Infinity })); - hlp.handleError(node, nextStartTimeData.error, null, 'could not evaluate time'); + hlp.handleError(node, nextStartTimeData.error, null, 'could not evaluate time (' + reason +')'); return; } node.nextStartTime = nextStartTimeData.value; @@ -880,13 +877,13 @@ module.exports = function (RED) { if (nextTimeAltData.error) { isFixedTime = false; // node.debug('nextTimeAltData=' + util.inspect(nextTimeAltData, { colors: true, compact: 10, breakLength: Infinity })); - hlp.handleError(node, nextTimeAltData.error, null, 'could not evaluate alternate time'); + hlp.handleError(node, nextTimeAltData.error, null, 'could not evaluate alternate time (' + reason +')'); return; } node.nextStartTimeAlt = nextTimeAltData.value; isFixedTime = isFixedTime && nextTimeAltData.fix; if (!hlp.isValidDate(node.nextStartTimeAlt)) { - hlp.handleError(this, 'Invalid time format of alternate time "' + node.nextStartTimeAlt + '"', undefined, 'internal error!'); + hlp.handleError(this, 'Invalid time format of alternate time "' + node.nextStartTimeAlt + '"', undefined, 'internal error! (' + reason +')'); } else { node.timeStartData.isAltAvailable = true; } @@ -907,10 +904,10 @@ module.exports = function (RED) { return; } - let millisec = node.tsGetNextScheduleTime(node.nextStartTime, 10); + let millisec = node.tsGetNextScheduleTime(node.nextStartTime); if (node.timeStartData.isAltAvailable) { shape = 'ring'; - const millisecAlt = node.tsGetNextScheduleTime(node.nextStartTimeAlt, 10); + const millisecAlt = node.tsGetNextScheduleTime(node.nextStartTimeAlt); if (millisecAlt < millisec) { millisec = millisecAlt; node.timeStartData.isAltFirst = true; @@ -948,10 +945,7 @@ module.exports = function (RED) { // node.debug(`timeOutStartObj isAlt=${isAlt} isAltFirst=${node.timeStartData.isAltFirst}`); const msg = { - type: 'start', - timeData: {} - // settingData: node.timeStartData, - // settingDataAlt: node.timeStartAltData + _inject_type: 'time' }; node.timeOutStartObj = null; let useAlternateTime = false; @@ -982,7 +976,7 @@ module.exports = function (RED) { if (!isFixedTime && !node.intervalObj) { node.intervalObj = setInterval(() => { node.debug('retriggered'); - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'retriggered'); }, node.recalcTime); } else if (isFixedTime && node.intervalObj) { clearInterval(node.intervalObj); @@ -1051,7 +1045,7 @@ module.exports = function (RED) { throw new Error('Configuration missing or wrong!'); } if (node.injType === tInj.timer) { - node.doCreateStartTimeout(node); + node.doCreateStartTimeout(node, 'on Input'); } send(node.prepOutMsg(msg)); if (msg.payload && msg.payload.error) { @@ -1109,7 +1103,7 @@ module.exports = function (RED) { text: RED._('node-red-contrib-sun-position/position-config:errors.error-title') }); } - }, 400 + Math.floor(Math.random() * 600)); + }, 100 + Math.floor(Math.random() * 500)); } catch (err) { node.error(err.message); node.log(util.inspect(err, Object.getOwnPropertyNames(err))); diff --git a/test/20_time-inject_spec.js b/test/20_time-inject_spec.js index b2b7248..41b5aa1 100644 --- a/test/20_time-inject_spec.js +++ b/test/20_time-inject_spec.js @@ -26,7 +26,6 @@ require("should-sinon"); const helper = require('node-red-node-test-helper'); // Nodes -// const functionNode = require('@node-red/nodes/core/core/80-function.js'); const nodeConfig = require('../nodes/10-position-config.js'); const nodeTimeInject = require('../nodes/20-time-inject.js'); @@ -47,101 +46,6 @@ const tabNode = {id: 'flow', type: 'tab', label: 'FLOW' }; const groupNode = {id: 'g0', type: 'group', name: 'GROUP' }; const hlpNode = {id: 'n2', type: 'helper'}; -/* -const flow = [ - { - id: 'n1', - type: 'time-inject', - z: 'flow', - g: "g0", - name: 'NAME', - nameInt: 'test', - positionConfig: 'nc1', - props: [ - { - p: '', - pt: 'msgPayload', - v: '${NR_NODE_NAME}', - vt: 'str', - o: '', - oT: 'none', - oM: '60000', - f: 0, - fS: 0, - fI: '0', - next: true, - days: '*', - months: '*', - onlyOddDays: false, - onlyEvenDays: false, - onlyOddWeeks: false, - onlyEvenWeeks: false - }, - { - p: '', - pt: 'msgTopic', - v: 'test-topic', - vt: 'str', - o: '', - oT: 'none', - oM: '60000', - f: 0, - fS: 0, - fI: '0', - next: false, - days: '*', - months: '*', - onlyOddDays: false, - onlyEvenDays: false, - onlyOddWeeks: false, - onlyEvenWeeks: false - } - ], - injectTypeSelect: 'none', - intervalCount: 1, - intervalCountType: 'num', - intervalCountMultiplier: 60000, - time: '', - timeType: 'entered', - offset: 0, - offsetType: 'none', - offsetMultiplier: 60000, - timeEnd: '', - timeEndType: 'entered', - timeEndOffset: 0, - timeEndOffsetType: 'none', - timeEndOffsetMultiplier: 60000, - timeDays: '*', - timeOnlyOddDays: false, - timeOnlyEvenDays: false, - timeOnlyOddWeeks: false, - timeOnlyEvenWeeks: false, - timeMonths: '*', - timedatestart: '', - timedateend: '', - property: '', - propertyType: 'none', - propertyCompare: 'true', - propertyThreshold: '', - propertyThresholdType: 'num', - timeAlt: '', - timeAltType: 'entered', - timeAltDays: '*', - timeAltOnlyOddDays: false, - timeAltOnlyEvenDays: false, - timeAltOnlyOddWeeks: false, - timeAltOnlyEvenWeeks: false, - timeAltMonths: '*', - timeAltOffset: 0, - timeAltOffsetType: 'none', - timeAltOffsetMultiplier: 60000, - once: false, - onceDelay: 0.1, - recalcTime: 2, - wires: [['n2']] - }, cfgNode, hlpNode]; -*/ - describe('time inject node', () => { beforeEach(function (done) { helper.startServer(done); @@ -505,7 +409,7 @@ describe('time inject node', () => { {p: '', pt: 'msgPayload', v: '10:00', vt: 'entered', o: "", oT: "none", oM: "60000", f: 0, fS: 0, fI: "0", - next: true, + next: false, days: "*", months: "*", onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], @@ -1385,7 +1289,7 @@ describe('time inject node', () => { n2.on('input', function(msg) { try { msg.should.have.property('topic', 't1'); - msg.should.have.property('type', 'once/startup'); + msg.should.have.property('_inject_type', 'once/startup'); msg.should.have.property('payload'); should(msg.payload).be.lessThan(timestamp.getTime()); done(); @@ -1446,8 +1350,8 @@ describe('time inject node', () => { const n2 = helper.getNode('n2'); n2.on('input', function(msg) { msg.should.have.property('topic', 't1'); - msg.should.have.property('type', 'once/startup'); msg.should.have.property('payload'); + msg.should.have.property('_inject_type', 'once/startup'); should(msg.payload).be.greaterThan(timestamp.getTime()); done(); }); @@ -1531,6 +1435,141 @@ describe('time inject node', () => { }); }); /* inject repeatedly */ + /* + describe('inject repeatedly between times', function() { + + }); /* inject repeatedly between times */ + + /* + describe('inject fixed count between times', function() { + + }); /* inject fixed count between times */ + + + describe('inject at fixed timestamp ', function() { + it('should inject at fixed timestamp', function(done) { + this.timeout(10100); // have to wait for the inject with delay of two seconds + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds() + 5); + timestamp.setMilliseconds(0); + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '⏲ fixed timestamp', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'payload', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't2', vt: 'str'}], + injectTypeSelect: 'time', + time: timestamp.toLocaleTimeString(), // timestamp.getHours() + ":" + timestamp.getHours() + ":" + timestamp.getMinutes(), + timeType: "entered", + offset: 0, + offsetType: "none", + offsetMultiplier: 60000, + timeDays: "*", + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: "*", + timedatestart: "", + timedateend: "", + property: "", + propertyType: "none", + propertyCompare: "true", + propertyThreshold: "", + propertyThresholdType: "num", + once: false, + onceDelay: 0.01, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n2 = helper.getNode('n2'); + n2.on('input', function(msg) { + try { + const d = (new Date()).getTime(); + const ts = timestamp.getTime(); + msg.should.have.property('topic', 't2'); + msg.should.have.property('_inject_type', 'time'); + msg.should.have.property('payload'); + should(d).be.greaterThan(ts); + should(d).be.lessThan(ts + 100); + done(); + } catch(err) { + console.log(ts); + console.log(d); + console.log(msg); + done(err); + } + }); + }); + }); + + it('should inject at fixed timestamp and offset (num)', function(done) { + this.timeout(10100); // have to wait for the inject with delay of two seconds + const timestamp = new Date(); + timestamp.setSeconds(timestamp.getSeconds() + 5); + timestamp.setMilliseconds(0); + const flow = [{ + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '⏲ fixed timestamp', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: 'payload', vt: 'str'}, + {p: '', pt: 'msgTopic', v: 't2', vt: 'str'}], + injectTypeSelect: 'time', + time: timestamp.toLocaleTimeString(), // timestamp.getHours() + ":" + timestamp.getHours() + ":" + timestamp.getMinutes(), + timeType: "entered", + offset: 2, + offsetType: "num", + offsetMultiplier: 1000, + timeDays: "*", + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: "*", + timedatestart: "", + timedateend: "", + property: "", + propertyType: "none", + propertyCompare: "true", + propertyThreshold: "", + propertyThresholdType: "num", + once: false, + onceDelay: 0.01, + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + + helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + const n2 = helper.getNode('n2'); + n2.on('input', function(msg) { + try { + const d = (new Date()).getTime(); + const ts = timestamp.getTime(); + msg.should.have.property('topic', 't2'); + msg.should.have.property('_inject_type', 'time'); + msg.should.have.property('payload'); + should(d).be.greaterThan(ts + 2000); + should(d).be.lessThan(ts + 2100); + done(); + } catch(err) { + console.log(ts); + console.log(d); + console.log(msg); + done(err); + } + }); + }); + }); + }); /* inject at fixed timestamp */ + // TODO: implement inject by time /* From c5bad525616286dd4f8a00b86a2865d984fcfc7d Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:04:12 +0100 Subject: [PATCH 17/44] smaller changes in preparation for tests --- nodes/21-within-time-switch.js | 129 +++++++++++++++++---------------- nodes/22-delay-until.js | 51 +++++++------ 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/nodes/21-within-time-switch.js b/nodes/21-within-time-switch.js index 7e90152..6d1fbc3 100644 --- a/nodes/21-within-time-switch.js +++ b/nodes/21-within-time-switch.js @@ -43,19 +43,19 @@ module.exports = function (RED) { let id = ''; let value = ''; switch (comparetype) { - case '1': + case 1: id = 'msg.ts'; value = msg.ts; break; - case '2': + case 2: id = 'msg.lc'; value = msg.lc; break; - case '3': + case 3: id = 'msg.time'; value = msg.time; break; - case '4': + case 4: id = 'msg.value'; value = msg.value; break; @@ -303,14 +303,15 @@ module.exports = function (RED) { */ function withinTimeSwitchNode(config) { RED.nodes.createNode(this, config); + const node = this; // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - // this.debug('initialize withinTimeSwitchNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); - if (!this.positionConfig) { + node.positionConfig = RED.nodes.getNode(config.positionConfig); + // node.debug('initialize withinTimeSwitchNode ' + 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')); setstate(node, { error: RED._('node-red-contrib-sun-position/position-config:errors.config-missing-state') }); return; - } else if (this.positionConfig.checkNode( + } else if (node.positionConfig.checkNode( error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); @@ -318,7 +319,7 @@ module.exports = function (RED) { return; } - this.timeStart = { + node.timeStart = { type: config.startTimeType, value : config.startTime, offsetType : config.startOffsetType, @@ -326,7 +327,7 @@ module.exports = function (RED) { multiplier : config.startOffsetMultiplier }; - this.timeEnd = { + node.timeEnd = { type: config.endTimeType, value : config.endTime, offsetType : config.endOffsetType, @@ -334,7 +335,7 @@ module.exports = function (RED) { multiplier : config.endOffsetMultiplier }; - this.timeStartAlt = { + node.timeStartAlt = { type: config.startTimeAltType || 'none', value : config.startTimeAlt, offsetType : config.startOffsetAltType, @@ -342,29 +343,29 @@ module.exports = function (RED) { multiplier : config.startOffsetAltMultiplier }; - this.propertyStartOperator = config.propertyStartCompare || 'true'; - this.propertyStart = { + node.propertyStartOperator = config.propertyStartCompare || 'true'; + node.propertyStart = { type : config.propertyStartType || 'none', value : config.propertyStart || '' }; - this.propertyStartThreshold = { + node.propertyStartThreshold = { type : config.propertyStartThresholdType || 'none', value : config.propertyStartThreshold || '' }; - if (this.positionConfig && this.propertyStart.type === 'jsonata') { + if (node.positionConfig && node.propertyStart.type === 'jsonata') { try { - this.propertyStart.expr = this.positionConfig.getJSONataExpression(this, this.propertyStart.value); + node.propertyStart.expr = node.positionConfig.getJSONataExpression(node, node.propertyStart.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - this.propertyStart.expr = null; + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.propertyStart.expr = null; } } - if (this.propertyStart.type === 'none' || this.timeStartAlt.type === 'none') { - this.propertyStart.type = 'none'; - delete this.timeStartAlt; + if (node.propertyStart.type === 'none' || node.timeStartAlt.type === 'none') { + node.propertyStart.type = 'none'; + delete node.timeStartAlt; } - this.timeEndAlt = { + node.timeEndAlt = { type: config.endTimeAltType || 'none', value : config.endTimeAlt, offsetType : config.endOffsetAltType, @@ -372,97 +373,97 @@ module.exports = function (RED) { multiplier : config.endOffsetAltMultiplier }; - this.propertyEndOperator = config.propertyEndCompare || 'true'; - this.propertyEnd = { + node.propertyEndOperator = config.propertyEndCompare || 'true'; + node.propertyEnd = { type : config.propertyEndType || 'none', value : config.propertyEnd || '' }; - this.propertyEndThreshold = { + node.propertyEndThreshold = { type : config.propertyEndThresholdType || 'none', value : config.propertyEndThreshold || '' }; - if (this.positionConfig && this.propertyEnd.type === 'jsonata') { + if (node.positionConfig && node.propertyEnd.type === 'jsonata') { try { - this.propertyEnd.expr = this.positionConfig.getJSONataExpression(this, this.propertyEnd.value); + node.propertyEnd.expr = node.positionConfig.getJSONataExpression(node, node.propertyEnd.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - this.propertyEnd.expr = null; + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.propertyEnd.expr = null; } } - if (this.propertyEnd.type === 'none' || this.timeEndAlt.type === 'none') { - this.propertyEnd.type = 'none'; - delete this.timeEndAlt; + if (node.propertyEnd.type === 'none' || node.timeEndAlt.type === 'none') { + node.propertyEnd.type = 'none'; + delete node.timeEndAlt; } - this.timeRestrictions = { + node.timeRestrictions = { type: config.timeRestrictionsType || 'none', value : config.timeRestrictions }; - if (this.timeRestrictions.type === 'jsonata') { + if (node.timeRestrictions.type === 'jsonata') { try { - this.timeRestrictions.expr = this.positionConfig.getJSONataExpression(this, this.timeRestrictions.value); + node.timeRestrictions.expr = node.positionConfig.getJSONataExpression(node, node.timeRestrictions.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - this.timeRestrictions.expr = null; + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.timeRestrictions.expr = null; } } - if (this.timeRestrictions.type === 'none') { // none means limitations would defined internal! - this.timeOnlyEvenDays = hlp.isTrue(config.timeOnlyEvenDays); - this.timeOnlyOddDays = hlp.isTrue(config.timeOnlyOddDays); - this.timeOnlyEvenWeeks = hlp.isTrue(config.timeOnlyEvenWeeks); - this.timeOnlyOddWeeks = hlp.isTrue(config.timeOnlyOddWeeks); + if (node.timeRestrictions.type === 'none') { // none means limitations would defined internal! + node.timeOnlyEvenDays = hlp.isTrue(config.timeOnlyEvenDays); + node.timeOnlyOddDays = hlp.isTrue(config.timeOnlyOddDays); + node.timeOnlyEvenWeeks = hlp.isTrue(config.timeOnlyEvenWeeks); + node.timeOnlyOddWeeks = hlp.isTrue(config.timeOnlyOddWeeks); - if (this.timeOnlyEvenDays && this.timeOnlyOddDays) { - this.timeOnlyEvenDays = false; - this.timeOnlyOddDays = false; + if (node.timeOnlyEvenDays && node.timeOnlyOddDays) { + node.timeOnlyEvenDays = false; + node.timeOnlyOddDays = false; } - if (this.timeOnlyEvenWeeks && this.timeOnlyOddWeeks) { - this.timeOnlyEvenWeeks = false; - this.timeOnlyOddWeeks = false; + if (node.timeOnlyEvenWeeks && node.timeOnlyOddWeeks) { + node.timeOnlyEvenWeeks = false; + node.timeOnlyOddWeeks = false; } if (typeof config.timedatestart !== 'undefined' && config.timedatestart !== '') { - this.timeStartDate = new Date(config.timedatestart); + node.timeStartDate = new Date(config.timedatestart); } if (typeof config.timedateend !== 'undefined' && config.timedateend !== '') { - this.timeEndDate = new Date(config.timedateend); + node.timeEndDate = new Date(config.timedateend); } if (config.timeDays === '') { throw new Error('No valid days given! Please check settings!'); } else if (!config.timeDays || config.timeDays === '*') { // config.timeDays = null; - delete this.timeDays; + delete node.timeDays; } else { - this.timeDays = config.timeDays.split(','); - this.timeDays = this.timeDays.map( e => parseInt(e) ); + node.timeDays = config.timeDays.split(','); + node.timeDays = node.timeDays.map( e => parseInt(e) ); } if (config.timeMonths === '') { throw new Error('No valid month given! Please check settings!'); } else if (!config.timeMonths || config.timeMonths === '*') { // config.timeMonths = null; - delete this.timeMonths; + delete node.timeMonths; } else { - this.timeMonths = config.timeMonths.split(','); - this.timeMonths = this.timeMonths.map( e => parseInt(e) ); + node.timeMonths = config.timeMonths.split(','); + node.timeMonths = node.timeMonths.map( e => parseInt(e) ); } } - this.withinTimeValue = { + node.withinTimeValue = { value : config.withinTimeValue ? config.withinTimeValue : 'true', type : config.withinTimeValueType ? config.withinTimeValueType : 'msgInput' }; - if (this.withinTimeValue.type === 'input') { this.withinTimeValue.type = 'msgInput'; } - this.outOfTimeValue = { + if (node.withinTimeValue.type === 'input') { node.withinTimeValue.type = 'msgInput'; } + node.outOfTimeValue = { value : config.outOfTimeValue ? config.outOfTimeValue : 'false', type : config.outOfTimeValueType ? config.outOfTimeValueType : 'msgInput' }; - if (this.outOfTimeValue.type === 'input') { this.outOfTimeValue.type = 'msgInput'; } + if (node.outOfTimeValue.type === 'input') { node.outOfTimeValue.type = 'msgInput'; } - this.timeOutObj = null; - this.lastMsgObj = null; - const node = this; + node.timeOutObj = null; + node.lastMsgObj = null; + node.tsCompare = parseInt(config.tsCompare) || 0; node.on('input', function (msg, send, done) { // If this is pre-1.0, 'done' will be undefined @@ -478,7 +479,7 @@ module.exports = function (RED) { } // this.debug('starting ' + util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })); // this.debug('self ' + util.inspect(this, { colors: true, compact: 10, breakLength: Infinity })); - const dNow = getIntDate(config.tsCompare, msg, node); + const dNow = getIntDate(node.tsCompare, msg, node); const result = calcWithinTimes(this, msg, dNow); if (result.valid && result.start.value && result.end.value) { diff --git a/nodes/22-delay-until.js b/nodes/22-delay-until.js index 1b998e2..86c8f42 100644 --- a/nodes/22-delay-until.js +++ b/nodes/22-delay-until.js @@ -36,25 +36,25 @@ module.exports = function(RED) { const path = require('path'); const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); - - RED.nodes.createNode(this, config); - this.locale = require('os-locale').sync(); + const node = this; + RED.nodes.createNode(node, config); + node.locale = require('os-locale').sync(); // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - // this.debug('initialize rdgDelayUntilNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); - if (!this.positionConfig) { + 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 (this.positionConfig.checkNode( + if (node.positionConfig.checkNode( error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); }, false)) { return null; } - this.timeData = { + node.timeData = { type: config.timeType, value : config.time, offsetType : config.offsetType, @@ -65,34 +65,33 @@ module.exports = function(RED) { config.timeType === 'flow' || config.timeType === 'global') }; - if (this.timeData.type === 'jsonata') { + if (node.timeData.type === 'jsonata') { try { - this.timeData.expr = this.positionConfig.getJSONataExpression(this, this.timeData.value); + node.timeData.expr = node.positionConfig.getJSONataExpression(node, node.timeData.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - this.timeData.expr = null; + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.timeData.expr = null; } } - this.queuingBehavior = config.queuingBehavior; - this.flushMsgs = { + node.queuingBehavior = config.queuingBehavior; + node.flushMsgs = { type: config.flushMsgsType || 'none', value : config.flushMsgs }; - this.flushMsgsValue = config.flushMsgsValue; - this.dropMsgs = { + node.flushMsgsValue = config.flushMsgsValue; + node.dropMsgs = { type: config.dropMsgsType || 'none', value : config.dropMsgs }; - this.dropMsgsValue = config.dropMsgsValue; - this.enqueueMsg = { + node.dropMsgsValue = config.dropMsgsValue; + node.enqueueMsg = { type: config.enqueueMsgType || 'none', value : config.enqueueMsg }; - this.enqueueMsgValue = config.enqueueMsgValue; - this.ctrlPropSet = config.ctrlPropSet; - this.ctrlPropValue = config.ctrlPropValue; - this.tsCompare = config.tsCompare; - const node = this; + node.enqueueMsgValue = config.enqueueMsgValue; + node.ctrlPropSet = config.ctrlPropSet; + node.ctrlPropValue = config.ctrlPropValue; + node.tsCompare = parseInt(config.tsCompare) || 0; node.msgQueue = []; @@ -194,15 +193,15 @@ module.exports = function(RED) { let id = ''; let value = ''; switch (comparetype) { - case '1': + case 1: id = 'msg.ts'; value = msg.ts; break; - case '2': + case 2: id = 'msg.lc'; value = msg.lc; break; - case '3': + case 3: id = 'msg.time'; value = msg.time; break; From 4f2d0fa84e097569ff14c21f757a14e2a5d2e4f5 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Mon, 28 Feb 2022 19:42:32 +0100 Subject: [PATCH 18/44] added cron to time-inject; minor revisions --- CHANGELOG.md | 13 +- nodes/20-time-inject.html | 49 ++- nodes/20-time-inject.js | 71 +++- nodes/icons/inputTypeCRON.svg | 129 ++++++ nodes/lib/dateTimeHelper.js | 61 ++- nodes/locales/de/10-position-config.json | 1 + nodes/locales/de/20-time-inject.json | 1 + nodes/locales/en-US/10-position-config.json | 1 + nodes/locales/en-US/20-time-inject.json | 5 +- nodes/static/htmlglobal.js | 9 +- package-lock.json | 3 +- package.json | 9 +- test/20_time-inject_spec.js | 414 +++++++++++--------- 13 files changed, 522 insertions(+), 244 deletions(-) create mode 100644 nodes/icons/inputTypeCRON.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 8001664..6db8797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ Install of a specific Version in Redmatic (on a Homematic): - `npm install --save --no-package-lock --global-style --save-prefix="~" --production node-red-contrib-sun-position@2.0.0` This can be also used to go back to an older Version. +### ?.?.?: enhancement + +- general + - first implementation of tests with moca and some changes due to this + +- inject enhance (time-inject) + - added repletely inject with CRON expression + - added automatic test case + - basic inject + - time based inject + ### 2.1.1: bug fixes - clock-timer @@ -23,7 +34,7 @@ This can be also used to go back to an older Version. - blind-control - fix bug of handling not time constrained rules be first to last evaluated - + ### 2.0.13: bug fixes - general diff --git a/nodes/20-time-inject.html b/nodes/20-time-inject.html index 05de998..4d0daa7 100644 --- a/nodes/20-time-inject.html +++ b/nodes/20-time-inject.html @@ -225,6 +225,7 @@ (typeof this.injectTypeSelect === 'undefined') || (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'time') || + (this.injectTypeSelect === 'cron') || RED.validators.typedInput('intervalCountType')(v) || (v > 0) ); @@ -242,6 +243,7 @@ (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'time') || (this.injectTypeSelect === 'interval-amount') || + (this.injectTypeSelect === 'cron') || RED.validators.number()(v) ); } @@ -258,6 +260,17 @@ ); } }, + cron: { + value: '', + required: false, + validate(v) { + return (this.injectTypeSelect !== 'cron') || + RED.validators.typedInput('cronType')(v); + } + }, + cronType: { + value: 'cronexpr' + }, time: { value: '', required: false, @@ -281,6 +294,7 @@ (typeof this.offsetType === 'undefined') || (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'interval') || + (this.injectTypeSelect === 'cron') || RED.validators.typedInput('offsetType')(v) ); } @@ -297,6 +311,7 @@ RED.validators.number()(v) || (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'interval') || + (this.injectTypeSelect === 'cron') || $('#node-input-offsetType').typedInput('type') === 'none' || this.offsetType === 'none' ); @@ -377,6 +392,7 @@ typeof v === 'undefined' || (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'interval') || + (this.injectTypeSelect === 'cron') || RED.validators.regex(/^$|^\d{4}-\d{1,2}-\d{1,2}$|^\d{2}-\d{1,2}-\d{1,2}$/)(v) ); } @@ -389,6 +405,7 @@ typeof v === 'undefined' || (this.injectTypeSelect === 'none') || (this.injectTypeSelect === 'interval') || + (this.injectTypeSelect === 'cron') || RED.validators.regex(/^$|^\d{4}-\d{1,2}-\d{1,2}$|^\d{2}-\d{1,2}-\d{1,2}$/)(v) ); } @@ -1328,10 +1345,12 @@ $('.time-inject-row-timeOnly').show(); $('#node-input-intervalCountMultiplier').hide(); break; + case 'cron': + $('#time-inject-row-cron').show(); + break; default: break; } - // $('.time-inject-row-all').hide(); $('#node-input-offset').change(); // includes time + property change $('#node-input-timeEndOffset').change(); // also timeEnd change $('#node-input-timeAltOffset').change(); // includes time + property change @@ -1395,7 +1414,7 @@ onChange(_type, _value) { const injectType = $('#node-input-injectTypeSelect').val(); // console.log('time onChange -> ' + injectType + ' / type = ' + type); - if (injectType === 'none' || injectType === 'interval') { + if (injectType === 'none' || injectType === 'interval' || injectType === 'cron') { $('.time-inject-row-timeStartOffset').hide(); $('.time-inject-row-timeLimits').hide(); $('.time-inject-row-altStart').hide(); @@ -1765,6 +1784,21 @@ } }); // #endregion timeAlt + // #region cron + setupTInput(node, { + typeProp: 'cronType', + valueProp: 'cron', + width: 'calc(100% - 110px)', + defaultType: types.cronExpr.value, + defaultValue: '', + types: [ + types.cronExpr + ], + onChange(_type, _value) { + $('#node-input-time').change(); + } + }); // cron + // # endregion cron // console.log('Loading done, trigegr changes'); $('#node-input-injectTypeSelect').change(); setTimeout(() => { @@ -2047,6 +2081,10 @@ if ($('#node-input-intervalCount').typedInput('type') === 'num') { this.nameInt += $('#node-input-intervalCount').typedInput('value'); } + } else if ($('#node-input-injectTypeSelect').val() === 'cron') { + this.nameInt += $('#node-input-cron').typedInput('value'); + this.nameInt += ' = '; + this.nameInt += propData.payloadLbl; } else { this.nameInt += '⏲ '; if ($('#node-input-time').typedInput('type') === propData.payloadType) { @@ -2128,6 +2166,7 @@ +
@@ -2144,7 +2183,11 @@
- +
+ + + +
diff --git a/nodes/20-time-inject.js b/nodes/20-time-inject.js index cf787d0..64e9ef3 100644 --- a/nodes/20-time-inject.js +++ b/nodes/20-time-inject.js @@ -29,6 +29,7 @@ module.exports = function (RED) { const util = require('util'); const path = require('path'); + const {scheduleTask} = require('cronosjs'); const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); @@ -90,7 +91,8 @@ module.exports = function (RED) { timer : 1, interval : 2, intervalBtwStartEnd : 4, - intervalAmount : 5 + intervalAmount : 5, + cron : 6 }; const intervalMax = 24*60*60*1000 * 3; // 3 Tage const node = this; @@ -101,7 +103,7 @@ module.exports = function (RED) { 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; - } else if (this.positionConfig.checkNode( + } else if (node.positionConfig.checkNode( error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); @@ -117,6 +119,8 @@ module.exports = function (RED) { node.injType = tInj.timer; } else if (config.injectTypeSelect === 'interval-amount') { node.injType = tInj.intervalAmount; + } else if (config.injectTypeSelect === 'cron') { + node.injType = tInj.cron; } else { node.injType = tInj.none; } @@ -265,7 +269,10 @@ module.exports = function (RED) { node.timeEndData.onlyOddWeeks = false; } } // timeEndData - + node.cronjob = null; + if (node.injType === tInj.cron) { + node.cronExpr = config.cron || ''; + } // cron /* Handle legacy */ if(!Array.isArray(config.props)){ config.props = []; @@ -427,20 +434,6 @@ module.exports = function (RED) { node.timedateend = new Date(config.timedateend); } - /** - * get the schedule time - * @param {Date} time - time to schedule - * @returns {number} milliseconds until the defined Date - */ - node.tsGetNextScheduleTime = time => { - const dNow = new Date(); - let millisec = (time.valueOf() - dNow.valueOf()) - 5; - while (millisec < 10) { - millisec += 86400000; // 24h - } - return millisec; - }; - /** * get the limitation for time */ @@ -589,6 +582,15 @@ module.exports = function (RED) { node.doCreateStartTimeout(node, 'initial'); } break; + case tInj.cron: + // node.debug('initialize - Intervall cron'); + if (doEmit === true) { + node.emit('input', { + _inject_type: 'once/startup' + }); // will create timeout + } + node.doCreateCRONSetup(node); + break; default: // node.debug('initialize - default'); node.doSetStatus(node, 'green'); @@ -625,7 +627,7 @@ module.exports = function (RED) { let millisecEnd = 1000 * 60 * 60 * 24; // 24h if ((node.nextEndTime !== null) && (typeof node.nextEndTime !== 'undefined')) { // node.debug('timeout ' + node.nextEndTime + ' is in ' + millisec + 'ms'); - millisecEnd = node.tsGetNextScheduleTime(node.nextEndTime); + millisecEnd = hlp.getTimeOut(new Date(), node.nextEndTime); } return millisecEnd; }; @@ -816,6 +818,14 @@ module.exports = function (RED) { }, node.intervalTime); }; + node.doCreateCRONSetup = function () { + node.cronjob = scheduleTask(node.cronExpr,() => { + node.emit('input', { _inject_type: 'cron' }); + node.doSetStatus(node); + }); + node.doSetStatus(node); + }; + /** * creates the timeout * @param {*} node - the node representation @@ -904,10 +914,10 @@ module.exports = function (RED) { return; } - let millisec = node.tsGetNextScheduleTime(node.nextStartTime); + let millisec = hlp.getTimeOut(node.timeStartData.now, node.nextStartTime); if (node.timeStartData.isAltAvailable) { shape = 'ring'; - const millisecAlt = node.tsGetNextScheduleTime(node.nextStartTimeAlt); + const millisecAlt = hlp.getTimeOut(node.timeStartData.now, node.nextStartTimeAlt); if (millisecAlt < millisec) { millisec = millisecAlt; node.timeStartData.isAltFirst = true; @@ -988,7 +998,22 @@ module.exports = function (RED) { }; node.doSetStatus = (node, fill, shape) => { - if (node.nextStartTimeAlt && node.timeOutStartObj) { + if (node.cronjob) { + const d = node.cronjob.nextRun; + if (d) { + node.status({ + fill: fill || 'green', + shape, + text: node.cronExpr + ' = ' + node.positionConfig.toDateTimeString(d) + }); + } else { + node.status({ + fill: fill || 'red', + shape, + text: node.cronExpr + }); + } + } else if (node.nextStartTimeAlt && node.timeOutStartObj) { if (node.timeStartData.isAltFirst) { node.status({ fill, @@ -1141,6 +1166,10 @@ module.exports = function (RED) { this.timeOutEndObj = null; if (RED.settings.verbose) { this.log(RED._('inject.stopped')); } } + if (this.cronjob !== null) { + this.cronjob.stop(); + delete this.cronjob; + } }; RED.httpAdmin.post('/time-inject/:id', RED.auth.needsPermission('time-inject.write'), (req, res) => { diff --git a/nodes/icons/inputTypeCRON.svg b/nodes/icons/inputTypeCRON.svg new file mode 100644 index 0000000..9dc5b13 --- /dev/null +++ b/nodes/icons/inputTypeCRON.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + CRON + diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index c114e12..9da0adc 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -25,8 +25,18 @@ *********************************************/ 'use strict'; const util = require('util'); +const TIME_WEEK = 604800000; +const TIME_24h = 86400000; +const TIME_1h = 3600000; +const TIME_1min = 60000; +const TIME_1s = 1000; module.exports = { + TIME_WEEK, + TIME_24h, + TIME_1h, + TIME_1min, + TIME_1s, isBool, isTrue, isFalse, @@ -67,6 +77,7 @@ module.exports = { getUTCDayId, getDayId, getTimeNumberUTC, + getTimeOut, getNodeId, initializeParser, getFormattedDateOut, @@ -92,7 +103,7 @@ Date.prototype.getWeek = function() { // January 4 is always in week 1. const week1 = new Date(date.getFullYear(), 0, 4); // Adjust to Thursday in week 1 and count number of weeks from date to week1. - return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + return 1 + Math.round(((date.getTime() - week1.getTime()) / TIME_24h - 3 + (week1.getDay() + 6) % 7) / 7); }; @@ -414,7 +425,7 @@ function getWeekOfYear(date) { // Get first day of year const yearStart = new Date(Date.UTC(date.getUTCFullYear(),0,1)); // Calculate full weeks to nearest Thursday - const weekNo = Math.ceil(( ( (date - yearStart) / 86400000) + 1)/7); + const weekNo = Math.ceil(( ( (date - yearStart) / TIME_24h) + 1)/7); // Return array of year and week number return [date.getUTCFullYear(), weekNo]; } @@ -426,9 +437,8 @@ function getWeekOfYear(date) { */ function getDayOfYear(date) { const start = new Date(date.getFullYear(), 0, 0); - const diff = (date - start) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000); - const oneDay = 1000 * 60 * 60 * 24; - return [date.getUTCFullYear(), Math.floor(diff / oneDay)]; + const diff = (date - start) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * TIME_1min); + return [date.getUTCFullYear(), Math.floor(diff / TIME_24h)]; } /** @@ -515,7 +525,21 @@ function getNowObject(node, msg) { * @return {number} numeric representation of the time part of the date */ function getTimeNumberUTC(date) { - return date.getUTCMilliseconds() + date.getUTCSeconds() * 1000 + date.getUTCMinutes() * 60000 + date.getUTCHours() * 3600000; + return date.getUTCMilliseconds() + date.getUTCSeconds() * TIME_1s + date.getUTCMinutes() * TIME_1min + date.getUTCHours() * TIME_1h; +} +/*******************************************************************************************************/ +/** + * get the timeout time + * @param {Date} base - base time (tyically new Date()) + * @param {Date} time - time to schedule the timeout + * @returns {number} milliseconds until the defined Date + */ +function getTimeOut(base, time) { + let millisec = (time.valueOf() - base.valueOf()); // - 5; + while (millisec < 10) { + millisec += TIME_24h; // 24h + } + return millisec; } /*******************************************************************************************************/ /** @@ -744,9 +768,9 @@ function isDSTObserved(d) { */ function convertDateTimeZone(date, timeZoneOffset) { const localTime = date.getTime(); - const localOffset = date.getTimezoneOffset() * 60000; + const localOffset = date.getTimezoneOffset() * TIME_1min; const utc = localTime + localOffset; - const destTime = utc + (60000*timeZoneOffset); + const destTime = utc + (TIME_1min * timeZoneOffset); return new Date(destTime); } @@ -847,8 +871,7 @@ const parseDate = dateString => { * @returns {DATE} Date round to next full Hour */ function roundToHour(date) { - const p = 60 * 60 * 1000; // milliseconds in an hour - return new Date(Math.round(date.getTime() / p ) * p); + return new Date(Math.round(date.getTime() / TIME_1h ) * TIME_1h); } /*******************************************************************************************************/ @@ -1037,18 +1060,18 @@ function limitDate(limit, d) { * @return {Date|null} the parsed date object or **null** if can not parsed */ function getTimeOfText(t, date, utc, timeZoneOffset) { - // console.debug('getTimeOfText t=' + t + ' date=' + date); // eslint-disable-line - const d = date || new Date(); + // console.debug(`getTimeOfText t=${ t } date=${ date.toISOString() } utc=${ utc } timeZoneOffset=${ timeZoneOffset }`); // eslint-disable-line + const d = date ? new Date(date) : new Date(); if (t && (!t.includes('.')) && (!t.includes('-'))) { t = t.toLocaleLowerCase(); // const matches = t.match(/(0\d|1\d|2[0-3]|\d)(?::([0-5]\d|\d))(?::([0-5]\d|\d))?\s*(p?)/); if (t.includes('utc')) { utc = true; - t = t.replace('utc',''); + t = t.replace('utc','').trim(); } if (t.includes('local')) { utc = false; - t = t.replace('local',''); + t = t.replace('local','').trim(); } const matches = t.match(/(0\d|1\d|2[0-3]|\d)(?::([0-5]\d|\d))(?::([0-5]\d|\d))?\s*(p?)(?:a|am|m|\s)?\s*(?:(\+|-)(\d\d):([0-5]\d)|(\+|-)(\d\d\d\d?))?/); if (matches) { @@ -1453,11 +1476,11 @@ function getFormattedDateOut(date, format, utc, timeZoneOffset) { case 6: // timeformat_ms return date.getTime() - (new Date()).getTime(); case 7: // timeformat_sec - return Math.round((date.getTime() - (new Date()).getTime()) / 1000); + return Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s); case 8: // timeformat_min - return (Math.round((date.getTime() - (new Date()).getTime()) / 1000) / 60); + return (Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s) / 60); case 9: // timeformat_hour - return (Math.round((date.getTime() - (new Date()).getTime()) / 1000) / 3600); + return (Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s) / 3600); case 10: // timeformat_YYYYMMDDHHMMSS if (utc) { return Number(date.getUTCFullYear() + @@ -1539,7 +1562,7 @@ function getFormattedDateOut(date, format, utc, timeZoneOffset) { timeUTCStr: date.toUTCString(), timeISOStr: date.toISOString(), delay, - delaySec: Math.round(delay / 1000), + delaySec: Math.round(delay / TIME_1s), lc: dNow.getTime(), ofYear: { year : dayOfYear[0], @@ -2132,6 +2155,7 @@ function topicReplace(str, strAttrs) { strAttrsLower[k.toLowerCase()] = strAttrs[k]; }); + /* eslint-disable no-useless-escape */ const match = str.match(/[\$#]{[^}]+}/g); if (match) { match.forEach(v => { @@ -2142,5 +2166,6 @@ function topicReplace(str, strAttrs) { str = str.replace(rx, res); }); } + /* eslint-enable no-useless-escape */ return str; } \ No newline at end of file diff --git a/nodes/locales/de/10-position-config.json b/nodes/locales/de/10-position-config.json index d853542..b9fc547 100644 --- a/nodes/locales/de/10-position-config.json +++ b/nodes/locales/de/10-position-config.json @@ -44,6 +44,7 @@ "nodePath":"Node Pfad", "timeentered": "Uhrzeit (nächste)", "dateentered": "Datum (spezieller Zeitpunkt)", + "cronexpr":"CRON-Ausdruck", "timepredefined": "Uhrzeit speziell", "dayofmonth": "Monatstag", "dayofweek": "Wochentag", diff --git a/nodes/locales/de/20-time-inject.json b/nodes/locales/de/20-time-inject.json index 7c23e2b..545c3ec 100644 --- a/nodes/locales/de/20-time-inject.json +++ b/nodes/locales/de/20-time-inject.json @@ -19,6 +19,7 @@ "fewDays": "ein paar Tage", "fewMonths": "ein paar Monate", "interval-amount":"feste Anzahl zwischen den Zeitpunkten", + "cron":"CRON-Ausdruck", "count":"Anzahl", "intervalStart":"beginnend von" }, diff --git a/nodes/locales/en-US/10-position-config.json b/nodes/locales/en-US/10-position-config.json index 5582d8c..fc586df 100644 --- a/nodes/locales/en-US/10-position-config.json +++ b/nodes/locales/en-US/10-position-config.json @@ -46,6 +46,7 @@ "nodePath":"node path", "timeentered":"time (next)", "dateentered":"date", + "cronexpr":"CRON expression", "timepredefined":"fixed times", "dayofmonth":"day of month", "dayofweek":"day of week", diff --git a/nodes/locales/en-US/20-time-inject.json b/nodes/locales/en-US/20-time-inject.json index 35a47d0..8b31e27 100644 --- a/nodes/locales/en-US/20-time-inject.json +++ b/nodes/locales/en-US/20-time-inject.json @@ -20,6 +20,7 @@ "fewDays": "few days", "fewMonths": "few months", "interval-amount":"fixed number between time", + "cron":"CRON expression", "count":"Count", "intervalStart":"start from" }, @@ -31,7 +32,6 @@ "propertyThreshold": "Threshold", "payload": "payload data of the send message", "intervalCount":"count", - "intervalStart":"YYYY-MM-DDTHH:mm:ss", "time": "time for inject", "timeOffset": "offset of time", "timeAlt": "alternate time", @@ -41,8 +41,9 @@ "months":"select months wherefore it should be valid", "recalcTime": "Interval of a recalculation of the time for send a message.", "once": "define if the node should emit a message independent of the time a message on flow start", + "intervalStart":"YYYY-MM-DDTHH:mm:ss", "start":"tt.mm", - "end":"tt.mm" + "end":"tt.mm" }, "tips": { "addTimes": "Alternative time: If the specified property has the value 'true', the trigger is triggered at the specified alternative time instead of the normal specified time. This is useful for triggering a flow at a different time on public holidays or other special days.", diff --git a/nodes/static/htmlglobal.js b/nodes/static/htmlglobal.js index aa49e61..a8f36e4 100644 --- a/nodes/static/htmlglobal.js +++ b/nodes/static/htmlglobal.js @@ -277,7 +277,14 @@ function getTypes(node) { // eslint-disable-line no-unused-vars label: node._('node-red-contrib-sun-position/position-config:common.types.timeentered','time (next)'), icon: 'icons/node-red-contrib-sun-position/inputTypeTime.svg', hasValue: true, - validate: /^(0\d|\d|1\d|2[0-3])(?::([0-5]\d|\d))?(?::([0-5]\d|\d))?\s*(pm|p|PM|P|utc|UTC|local|LOCAL)?$/ + validate: /^(0\d|\d|1\d|2[0-3])(?::([0-5]\d|\d))?(?::([0-5]\d|\d))?\s*(pm|p|PM|P|am|a|AM|A)?\s*(utc|UTC|local|LOCAL|Local)?$/ + }, + cronExpr: { + value: 'cronexpr', + label: node._('node-red-contrib-sun-position/position-config:common.types.cronexpr','time (next)'), + icon: 'icons/node-red-contrib-sun-position/inputTypeCRON.svg', + hasValue: true, + validate: /^(@(yearly|annually|monthly|weekly|daily|midnight|hourly))|(?:(?:(?:(?:\d+,)+\d+|(?:\d+(?:\/|-|#)\d+)|\d+L?|\*(?:\/\d+)?|L(?:-\d+)?|\?|[A-Z]{3}(?:-[A-Z]{3})?) ?){5,7})$/ }, DateEntered: { value: 'dateEntered', diff --git a/package-lock.json b/package-lock.json index 0d99297..12e78bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2178,8 +2178,7 @@ "cronosjs": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/cronosjs/-/cronosjs-1.7.1.tgz", - "integrity": "sha512-d6S6+ep7dJxsAG8OQQCdKuByI/S/AV64d9OF5mtmcykOyPu92cAkAnF3Tbc9s5oOaLQBYYQmTNvjqYRkPJ/u5Q==", - "dev": true + "integrity": "sha512-d6S6+ep7dJxsAG8OQQCdKuByI/S/AV64d9OF5mtmcykOyPu92cAkAnF3Tbc9s5oOaLQBYYQmTNvjqYRkPJ/u5Q==" }, "cross-spawn": { "version": "7.0.3", diff --git a/package.json b/package.json index 26bfd46..74b257b 100644 --- a/package.json +++ b/package.json @@ -110,16 +110,17 @@ }, "dependencies": { "lodash.clonedeep": "^4.5.0", - "lodash.isequal": "^4.5.0" + "lodash.isequal": "^4.5.0", + "cronosjs": "^1.7.1" }, "devDependencies": { - "eslint": ">=8.9.0", + "eslint": ">=8.10.0", "eslint-plugin-html": ">=6.2.0", - "eslint-plugin-jsdoc": ">=37.9.4", + "eslint-plugin-jsdoc": ">=37.9.5", "eslint-plugin-json": ">=3.1.0", "eslint-plugin-node": ">=11.1.0", "jsonata": "^1.8.6", - "mocha": "^8.3.0", + "mocha": "^8.4.0", "node-red": ">=2.0.0", "node-red-dev": "^0.1.5", "node-red-node-test-helper": "^0.2.7", diff --git a/test/20_time-inject_spec.js b/test/20_time-inject_spec.js index 41b5aa1..05fe8b7 100644 --- a/test/20_time-inject_spec.js +++ b/test/20_time-inject_spec.js @@ -19,9 +19,17 @@ **/ /* global Context context describe beforeEach before afterEach after it */ +/** + * Test cases for time inect node + * + * @example: + * to run all tests: npm test + * to run all node tests: npm run testnode + * to run single test: mocha -g "time inject node" + */ const should = require('should'); -const sinon = require("sinon"); -require("should-sinon"); +const sinon = require('sinon'); +require('should-sinon'); const helper = require('node-red-node-test-helper'); @@ -57,7 +65,7 @@ describe('time inject node', () => { }); }); - describe('test configuration', function() { + describe('test configuration', () => { it('fail if missing configuration ', done => { const flow = [ { @@ -71,7 +79,7 @@ describe('time inject node', () => { once: false, wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n1 = helper.getNode('n1'); try { n1.status.should.be.calledOnce(); @@ -85,19 +93,19 @@ describe('time inject node', () => { it('fail if latitude missing ', done => { const flow = [ - { - id: "n1", - type: "time-inject", - name: "", - nameInt: "Zeitpunkt", - positionConfig: "nc1", - props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], - injectTypeSelect: "none", - once: false, - wires: [[]] - }, cfgNode]; + { + id: 'n1', + type: 'time-inject', + name: '', + nameInt: 'Zeitpunkt', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [[]] + }, cfgNode]; const invalidCreds = {'nc1': { 'posLongitude': '10'}}; - helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, () => { const n1 = helper.getNode('n1'); const nc1 = helper.getNode('nc1'); try { @@ -113,19 +121,19 @@ describe('time inject node', () => { it('fail if longitude missing ', done => { const flow = [ - { - id: "n1", - type: "time-inject", - name: "", - nameInt: "Zeitpunkt", - positionConfig: "nc1", - props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], - injectTypeSelect: "none", - once: false, - wires: [[]] - }, cfgNode]; + { + id: 'n1', + type: 'time-inject', + name: '', + nameInt: 'Zeitpunkt', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [[]] + }, cfgNode]; const invalidCreds = {'nc1': { 'posLatitude': '5'}}; - helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, () => { const n1 = helper.getNode('n1'); const nc1 = helper.getNode('nc1'); try { @@ -141,19 +149,19 @@ describe('time inject node', () => { it('fail if latitude invalid ', done => { const flow = [ - { - id: "n1", - type: "time-inject", - name: "", - nameInt: "Zeitpunkt", - positionConfig: "nc1", - props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], - injectTypeSelect: "none", - once: false, - wires: [[]] - }, cfgNode]; + { + id: 'n1', + type: 'time-inject', + name: '', + nameInt: 'Zeitpunkt', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [[]] + }, cfgNode]; const invalidCreds = {'nc1': { 'posLatitude': '90.1', 'posLongitude': '10'}}; - helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, () => { const n1 = helper.getNode('n1'); const nc1 = helper.getNode('nc1'); try { @@ -169,19 +177,19 @@ describe('time inject node', () => { it('fail if longitude invalid ', done => { const flow = [ - { - id: "n1", - type: "time-inject", - name: "", - nameInt: "Zeitpunkt", - positionConfig: "nc1", - props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], - injectTypeSelect: "none", - once: false, - wires: [[]] - }, cfgNode]; + { + id: 'n1', + type: 'time-inject', + name: '', + nameInt: 'Zeitpunkt', + positionConfig: 'nc1', + props: [{p: '', pt: 'msgPayload', v: 'foo', vt: 'str'}], + injectTypeSelect: 'none', + once: false, + wires: [[]] + }, cfgNode]; const invalidCreds = {'nc1': { 'posLatitude': '51', 'posLongitude': '180.1'}}; - helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, function() { + helper.load([nodeConfig, nodeTimeInject], flow, invalidCreds, () => { const n1 = helper.getNode('n1'); const nc1 = helper.getNode('nc1'); try { @@ -196,7 +204,7 @@ describe('time inject node', () => { }); }); - describe('basic tests', function() { + describe('basic tests', () => { function basicTest(type, val, rval, topic = 't1', adFkt = null) { it('inject value ('+type+')', function(done) { const flow = [ @@ -215,7 +223,7 @@ describe('time inject node', () => { once: false, wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n1 = helper.getNode('n1'); // inject node // const nc1 = helper.getNode('nc1'); // inject node const n2 = helper.getNode('n2'); // helper node @@ -282,7 +290,7 @@ describe('time inject node', () => { once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -314,18 +322,21 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', - o: "", oT: "none", oM: "60000", - f: 1, fS: 1, fT: "UTC Datum und Zeit", fI: "1", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, - {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], + { + p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', + o: '', oT: 'none', oM: '60000', + f: 1, fS: 1, fT: 'UTC Datum und Zeit', fI: '1', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false + }, + {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'} + ], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -334,7 +345,7 @@ describe('time inject node', () => { try { msg.should.have.property('topic', 'tx'); msg.should.have.property('payload'); - let d = new Date(msg.payload); + const d = new Date(msg.payload); should(d.getTime()).be.greaterThan(timestamp.getTime() - 1000); should(d.getTime()).be.lessThan(timestamp.getTime() + 1000); msg.payload.substring(0, msg.payload.length - 6).should.equal(timestamp.toUTCString().substring(0, timestamp.toUTCString().length - 6)); @@ -360,18 +371,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fT: "Millisekunden UNIX-Zeit", fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: '', vt: 'dateSpecific', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fT: 'Millisekunden UNIX-Zeit', fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -406,18 +418,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: '10:00', vt: 'entered', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: false, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: '10:00', vt: 'entered', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: false, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -445,18 +458,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: '1.1.2020 10:00', vt: 'dateEntered', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: '1.1.2020 10:00', vt: 'dateEntered', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -484,18 +498,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: 'astronomicalDawn', vt: 'pdsTime', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: 'astronomicalDawn', vt: 'pdsTime', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -523,18 +538,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: 'civilDawn', vt: 'pdsTime', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: 'civilDawn', vt: 'pdsTime', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -562,18 +578,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: 'rise', vt: 'pdmTime', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: 'rise', vt: 'pdmTime', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -601,18 +618,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: 'fMon', vt: 'dayOfMonth', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: 'fMon', vt: 'dayOfMonth', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -640,18 +658,19 @@ describe('time inject node', () => { nameInt: 'test', positionConfig: 'nc1', props: [ - {p: '', pt: 'msgPayload', v: '', vt: 'pdsTimeNow', - o: "", oT: "none", oM: "60000", - f: 0, fS: 0, fI: "0", - next: true, - days: "*", months: "*", - onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, + { + p: '', pt: 'msgPayload', v: '', vt: 'pdsTimeNow', + o: '', oT: 'none', oM: '60000', + f: 0, fS: 0, fI: '0', + next: true, + days: '*', months: '*', + onlyOddDays: false, onlyEvenDays: false, onlyOddWeeks: false, onlyEvenWeeks: false }, {p: '', pt: 'msgTopic', v: 'tx', vt: 'str'}], injectTypeSelect: 'none', once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n1.status.should.be.calledOnce(); @@ -836,7 +855,7 @@ describe('time inject node', () => { }); /* basic tests */ - describe('environment variable', function() { + describe('environment variable', () => { it('inject name of node as environment variable ', done => { const flow = [ { @@ -850,7 +869,7 @@ describe('time inject node', () => { once: false, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n1 = helper.getNode('n1'); const n2 = helper.getNode('n2'); n2.on('input', msg => { @@ -1245,7 +1264,7 @@ describe('time inject node', () => { }); }); /* environment variable */ - describe('inject once', function() { + describe('inject once', () => { it('should inject once with default delay property', function(done) { const flow = [ { @@ -1259,7 +1278,7 @@ describe('time inject node', () => { once: true, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n1 = helper.getNode('n1'); n1.should.have.property('onceDelay', 0.1); done(); @@ -1284,7 +1303,7 @@ describe('time inject node', () => { once: true, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow1, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow1, credentials, () => { const n2 = helper.getNode('n2'); n2.on('input', function(msg) { try { @@ -1317,7 +1336,7 @@ describe('time inject node', () => { onceDelay: 0.5, wires: [['n2']] }, cfgNode, hlpNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n1 = helper.getNode('n1'); n1.should.have.property('onceDelay', 0.5); done(); @@ -1346,7 +1365,7 @@ describe('time inject node', () => { onceDelay: 2, wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n2 = helper.getNode('n2'); n2.on('input', function(msg) { msg.should.have.property('topic', 't1'); @@ -1359,7 +1378,7 @@ describe('time inject node', () => { }); }); /* inject once */ - describe('inject repeatedly', function() { + describe('inject repeatedly', () => { it('should inject repeatedly', function(done) { const flow = [{ id: 'n1', @@ -1379,7 +1398,7 @@ describe('time inject node', () => { wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n2 = helper.getNode('n2'); let count = 0; n2.on('input', function(msg) { @@ -1387,7 +1406,7 @@ describe('time inject node', () => { msg.should.have.property('payload', 'payload'); count += 1; if (count > 2) { - helper.clearFlows().then(function() { + helper.clearFlows().then(() => { done(); }); } @@ -1418,7 +1437,7 @@ describe('time inject node', () => { wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n2 = helper.getNode('n2'); let count = 0; n2.on('input', function(msg) { @@ -1426,7 +1445,7 @@ describe('time inject node', () => { should(msg.payload).be.greaterThan(timestamp.getTime()); count += 1; if (count > 2) { - helper.clearFlows().then(function() { + helper.clearFlows().then(() => { done(); }); } @@ -1435,18 +1454,19 @@ describe('time inject node', () => { }); }); /* inject repeatedly */ - /* - describe('inject repeatedly between times', function() { - + describe('inject repeatedly between times', () => { + it('test needs to be implemented', function() { + this.skip(); + }); }); /* inject repeatedly between times */ - /* - describe('inject fixed count between times', function() { - + describe('inject fixed count between times', () => { + it('test needs to be implemented', function() { + this.skip(); + }); }); /* inject fixed count between times */ - - describe('inject at fixed timestamp ', function() { + describe('inject at fixed timestamp ', () => { it('should inject at fixed timestamp', function(done) { this.timeout(10100); // have to wait for the inject with delay of two seconds const timestamp = new Date(); @@ -1464,29 +1484,29 @@ describe('time inject node', () => { {p: '', pt: 'msgTopic', v: 't2', vt: 'str'}], injectTypeSelect: 'time', time: timestamp.toLocaleTimeString(), // timestamp.getHours() + ":" + timestamp.getHours() + ":" + timestamp.getMinutes(), - timeType: "entered", + timeType: 'entered', offset: 0, - offsetType: "none", + offsetType: 'none', offsetMultiplier: 60000, - timeDays: "*", + timeDays: '*', timeOnlyOddDays: false, timeOnlyEvenDays: false, timeOnlyOddWeeks: false, timeOnlyEvenWeeks: false, - timeMonths: "*", - timedatestart: "", - timedateend: "", - property: "", - propertyType: "none", - propertyCompare: "true", - propertyThreshold: "", - propertyThresholdType: "num", + timeMonths: '*', + timedatestart: '', + timedateend: '', + property: '', + propertyType: 'none', + propertyCompare: 'true', + propertyThreshold: '', + propertyThresholdType: 'num', once: false, onceDelay: 0.01, wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n2 = helper.getNode('n2'); n2.on('input', function(msg) { try { @@ -1499,9 +1519,7 @@ describe('time inject node', () => { should(d).be.lessThan(ts + 100); done(); } catch(err) { - console.log(ts); - console.log(d); - console.log(msg); + console.log(msg); // eslint-disable-line no-console done(err); } }); @@ -1525,29 +1543,29 @@ describe('time inject node', () => { {p: '', pt: 'msgTopic', v: 't2', vt: 'str'}], injectTypeSelect: 'time', time: timestamp.toLocaleTimeString(), // timestamp.getHours() + ":" + timestamp.getHours() + ":" + timestamp.getMinutes(), - timeType: "entered", + timeType: 'entered', offset: 2, - offsetType: "num", + offsetType: 'num', offsetMultiplier: 1000, - timeDays: "*", + timeDays: '*', timeOnlyOddDays: false, timeOnlyEvenDays: false, timeOnlyOddWeeks: false, timeOnlyEvenWeeks: false, - timeMonths: "*", - timedatestart: "", - timedateend: "", - property: "", - propertyType: "none", - propertyCompare: "true", - propertyThreshold: "", - propertyThresholdType: "num", + timeMonths: '*', + timedatestart: '', + timedateend: '', + property: '', + propertyType: 'none', + propertyCompare: 'true', + propertyThreshold: '', + propertyThresholdType: 'num', once: false, onceDelay: 0.01, wires: [['n2']] }, cfgNode, hlpNode, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n2 = helper.getNode('n2'); n2.on('input', function(msg) { try { @@ -1560,9 +1578,7 @@ describe('time inject node', () => { should(d).be.lessThan(ts + 2100); done(); } catch(err) { - console.log(ts); - console.log(d); - console.log(msg); + console.log(msg); // eslint-disable-line no-console done(err); } }); @@ -1570,27 +1586,41 @@ describe('time inject node', () => { }); }); /* inject at fixed timestamp */ - // TODO: implement inject by time - - /* - it('should inject with cron', function(done) { - helper.load(nodeTimeInject, [{id:'n1', type:'inject', - payloadType:'date', topic: 't3', - crontab: '* * * * * *', wires:[['n3']] }, - {id:'n3', type:'helper'}], - function() { - const n3 = helper.getNode('n3'); - n3.on('input', function(msg) { - msg.should.have.property('topic', 't3'); - msg.should.have.property('payload').be.a.Number(); - helper.clearFlows().then(function() { - done(); + describe('inject with cron ', () => { + it('should inject with cron', function(done) { + const flow = [ + { + id: 'n1', + type: 'time-inject', + z: 'flow', + name: 'injectNodeName', + nameInt: '* * * * * * = timestamp', + positionConfig: 'nc1', + props: [ + {p: '', pt: 'msgPayload', v: '', vt: 'date'}, + {p: '', pt: 'msgTopic', v: 't3', vt: 'str'}], + injectTypeSelect: 'cron', + cron: '* * * * * *', + cronType: 'cronexpr', + once: false, + onceDelay: 0.1, + recalcTime: 2, + wires: [['n3']] + }, cfgNode, {id:'n3', type:'helper'}, tabNode]; + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { + const n3 = helper.getNode('n3'); + n3.on('input', function(msg) { + msg.should.have.property('topic', 't3'); + msg.should.have.property('payload').be.a.Number(); + helper.clearFlows().then(function() { + done(); + }); }); }); - }); - }); /* should inject with cron */ + }); /* should inject with cron */ + }); /* inject with cron */ - describe('post', function() { + describe('post', () => { it('should inject message', function(done) { const flow = [{ id: 'n1', @@ -1609,12 +1639,12 @@ describe('time inject node', () => { once: false, wires: [['n4']] }, cfgNode, {id: 'n4', type: 'helper'}, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n4 = helper.getNode('n4'); n4.on('input', function(msg) { msg.should.have.property('topic', 't4'); msg.should.have.property('payload', 'hello'); - helper.clearFlows().then(function() { + helper.clearFlows().then(() => { done(); }); }); @@ -1653,7 +1683,7 @@ describe('time inject node', () => { once: false, wires: [['n4']] }, cfgNode, {id: 'n4', type: 'helper'}, tabNode]; - helper.load([nodeConfig, nodeTimeInject], flow, credentials, function() { + helper.load([nodeConfig, nodeTimeInject], flow, credentials, () => { const n4 = helper.getNode('n4'); n4.on('input', function(msg) { msg.should.not.have.property('payload'); // payload removed @@ -1662,7 +1692,7 @@ describe('time inject node', () => { msg.should.have.property('num1', 1); // injected prop msg.should.have.property('bool1', true); // injected prop - helper.clearFlows().then(function() { + helper.clearFlows().then(() => { done(); }); }); From d8be07316f536c95f1e4d18cac4981f6baacf774 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Mon, 28 Feb 2022 19:56:33 +0100 Subject: [PATCH 19/44] rename --- CHANGELOG.md | 3 +- nodes/locales/en-US/81-delay-until.json | 43 ------------------------- 2 files changed, 2 insertions(+), 44 deletions(-) delete mode 100644 nodes/locales/en-US/81-delay-until.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db8797..8033772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ This can be also used to go back to an older Version. ### ?.?.?: enhancement - general - - first implementation of tests with moca and some changes due to this + - first implementation of tests with `mocha` and some changes due to the test implementation + - revised error handling and output messages if the configuration of nodes is not correct (missing config node, missing latitude/longitude). - inject enhance (time-inject) - added repletely inject with CRON expression diff --git a/nodes/locales/en-US/81-delay-until.json b/nodes/locales/en-US/81-delay-until.json deleted file mode 100644 index 9456c63..0000000 --- a/nodes/locales/en-US/81-delay-until.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "delay-until": { - "label": { - "name": "delay until", - "outputPort": "message", - "time":"time", - "offset":"Offset", - "flushMsgs":"flush messages with", - "dropMsgs":"drop message with", - "equals": "equal", - "compareTime":"base time", - "now":"date.now", - "msgts":"msg.ts", - "msglc":"msg.lc", - "msgtime":"msg.time", - "showEnhSettings":"Enhanced settings", - "enqueueMsg":"enqueue message with", - "ctrlPropSet":"set control properties", - "ctrlPropSetB":"to value", - "queuingBehavior":"message treatment", - "allMsgs":"enqueue all messages", - "firstMsgs":"enqueue only first message", - "lastMsgs":"enqueue only last (latest) message" - }, - "placeholder": { - "time":"time", - "offset":"Offset time", - "flushMsgs":"flush all waiting messages if property is equal value", - "dropMsgs":"drop all waiting messages if property is equal value", - "ctrlPropSet":"set a control propertie to the speciefied value" - }, - "tips": { - "documentation": "Documentation and examples", - "controlMsgProp":"Here can be defined special properties of the input message to flush or drop queued messages.", - "controlMsgPropDefined": "If one of the control propeties are equal to the value, all messages will be dropped or flushed from the queue. If the message contains additional the property msg.enqueue the message will be stored. Wit the following the control property can be set in that case to another value." - }, - "state": { - "default": "(__queueLength__) flush __sendTime__", - "intermedia": "(__queueLength__) retrigger __sendTime__", - "noTime": "(__queueLength__) will not send" - } - } -} \ No newline at end of file From 215ba0e15cf9614a1533c7532696e05b104780f0 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:49:40 +0100 Subject: [PATCH 20/44] added new delay-until node --- CHANGELOG.md | 2 + nodes/22-delay-until.html | 225 +++- nodes/22-delay-until.js | 126 ++- nodes/locales/de/10-position-config.json | 5 +- nodes/locales/de/22-delay-until.json | 38 + nodes/locales/en-US/10-position-config.json | 1 + nodes/locales/en-US/22-delay-until.json | 43 + nodes/static/htmlglobal.js | 5 + test/22-delay-until_spec.js | 1123 +++++++++++++++++++ 9 files changed, 1458 insertions(+), 110 deletions(-) create mode 100644 nodes/locales/de/22-delay-until.json create mode 100644 nodes/locales/en-US/22-delay-until.json create mode 100644 test/22-delay-until_spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8033772..5968beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ This can be also used to go back to an older Version. - basic inject - time based inject +- new Node. delay-until which allows to delay messages until defined time + ### 2.1.1: bug fixes - clock-timer diff --git a/nodes/22-delay-until.html b/nodes/22-delay-until.html index d02f85d..caa662b 100644 --- a/nodes/22-delay-until.html +++ b/nodes/22-delay-until.html @@ -69,51 +69,57 @@ }, flushMsgsType: { value: 'none' }, flushMsgsValue: { - value: '', + value: 'true', validate(_v) { return ( - (_v !== '') || (this.flushMsgsType === 'none') || - ($('#node-input-flushMsgs').typedInput('type') === 'none') + ($('#node-input-flushMsgs').typedInput('type') === 'none') || + RED.validators.typedInput('flushMsgsValueType') ); } }, + flushMsgsValueType: { value: 'bool' }, dropMsgs: { value: '', validate: RED.validators.typedInput('flushMsgsType') }, dropMsgsType: { value: 'none' }, dropMsgsValue: { - value: '', + value: 'true', validate(_v) { return ( - (_v !== '') || (this.dropMsgsType === 'none') || - ($('#node-input-dropMsgs').typedInput('type') === 'none') + ($('#node-input-dropMsgs').typedInput('type') === 'none') || + RED.validators.typedInput('dropMsgsValueType') ); } }, + dropMsgsValueType: { value: 'bool' }, enqueueMsg: { value: '', validate: RED.validators.typedInput('enqueueMsgType') }, enqueueMsgType: { value: 'none' }, enqueueMsgValue: { - value: '', + value: 'true', validate(_v) { return ( (_v !== '') || (this.dropMsgsType === 'none') || - ($('#node-input-enqueueMsg').typedInput('type') === 'none') + ($('#node-input-enqueueMsg').typedInput('type') === 'none') || + RED.validators.typedInput('enqueueMsgValueType') ); } }, - ctrlPropSet: { + enqueueMsgValueType: { value: 'bool' }, + ctrlPropChange: { value: false }, ctrlPropValue: { - value: '' + value: '', + validate: RED.validators.typedInput('ctrlPropValueType') }, + ctrlPropValueType: { value: 'delete' }, tsCompare: { value: '0' } @@ -174,6 +180,8 @@ } // #endregion initialize + // #region time + setupTInput(node, { typeProp: 'timeType', valueProp: 'time', @@ -246,33 +254,19 @@ } }); node.offsetMultiplier = multiplierUpdate(node.offsetMultiplier, 'offsetMultiplier', () => $('#node-input-time').change()); + // #endregion time - $('#node-input-ctrlPropSet').on('change', (_type, _value) => { - if ( onInit) { return; } - const typef = $('#node-input-flushMsgs').typedInput('type'); - const typed = $('#node-input-dropMsgs').typedInput('type'); - if (typef === types.Undefined.value && - typed === types.Undefined.value) { - $('.row-controlMsgPropDefined').hide(); - $('.row-ctrlPropSet').hide(); - } else { - $('.row-controlMsgPropDefined').show(); - const typee = $('#node-input-enqueueMsg').typedInput('type'); - if (typee === types.Undefined.value) { - $('#node-input-enqueueMsgValue').prop('disabled', true); - $('.row-ctrlPropSet').hide(); - } else { - $('#node-input-enqueueMsgValue').prop('disabled', false); - $('.row-ctrlPropSet').show(); - $('#node-input-ctrlPropValue').prop('disabled', !$('#node-input-ctrlPropSet').is(':checked')); - } - } + $('#node-input-queuingBehavior').on('change', () => { + $('#node-input-flushMsgs').change(); + $('#node-input-dropMsgs').change(); + $('#node-input-enqueueMsg').change(); }); + // #region controlMsgProps setupTInput(node, { typeProp: 'flushMsgsType', valueProp: 'flushMsgs', - width: 'calc(100% - 255px)', + width: 'calc(100% - 305px)', defaultType: types.Undefined.value, defaultValue: 0, types: [ @@ -283,20 +277,36 @@ if ( onInit) { return; } const type = $('#node-input-flushMsgs').typedInput('type'); if (type === types.Undefined.value) { - $('#node-input-flushMsgsValue').prop('disabled', true); - $('.row-ctrlPropSet').hide(); + $('#node-input-flushMsgsCmpText').hide(); + $('#node-input-flushMsgsValue').typedInput('hide'); } else { - $('#node-input-flushMsgsValue').prop('disabled', false); + $('#node-input-flushMsgsCmpText').show(); + $('#node-input-flushMsgsValue').typedInput('show'); } $('#node-input-flushMsgsValue').change(); - $('#node-input-ctrlPropSet').change(); + $('#node-input-enqueueMsg').change(); } }); + setupTInput(node, { + typeProp: 'flushMsgsValueType', + valueProp: 'flushMsgsValue', + width: '120px', + defaultType: 'bool', + defaultValue: 'true', + types: [ + 'bool', + 'num', + 'str', + 'json', + 'env' + ] + }); + setupTInput(node, { typeProp: 'dropMsgsType', valueProp: 'dropMsgs', - width: 'calc(100% - 255px)', + width: 'calc(100% - 305px)', defaultType: types.Undefined.value, defaultValue: 0, types: [ @@ -307,19 +317,36 @@ if ( onInit) { return; } const type = $('#node-input-dropMsgs').typedInput('type'); if (type === types.Undefined.value) { - $('#node-input-dropMsgsValue').prop('disabled', true); + $('#node-input-dropMsgsCmpText').hide(); + $('#node-input-dropMsgsValue').typedInput('hide'); } else { - $('#node-input-dropMsgsValue').prop('disabled', false); + $('#node-input-dropMsgsCmpText').show(); + $('#node-input-dropMsgsValue').typedInput('show'); } $('#node-input-dropMsgsValue').change(); - $('#node-input-ctrlPropSet').change(); + $('#node-input-enqueueMsg').change(); } }); + setupTInput(node, { + typeProp: 'dropMsgsValueType', + valueProp: 'dropMsgsValue', + width: '120px', + defaultType: 'bool', + defaultValue: 'true', + types: [ + 'bool', + 'num', + 'str', + 'json', + 'env' + ] + }); + setupTInput(node, { typeProp: 'enqueueMsgType', valueProp: 'enqueueMsg', - width: 'calc(100% - 255px)', + width: 'calc(100% - 305px)', defaultType: types.Undefined.value, defaultValue: 0, types: [ @@ -327,10 +354,83 @@ 'msg' ], onChange(_type, _value) { - $('#node-input-ctrlPropSet').change(); + if ( onInit) { return; } + const type = $('#node-input-enqueueMsg').typedInput('type'); + if (type === types.Undefined.value) { + $('#node-input-enqueueMsgCmpText').hide(); + $('#node-input-enqueueMsgValue').typedInput('hide'); + } else { + $('#node-input-enqueueMsgCmpText').show(); + $('#node-input-enqueueMsgValue').typedInput('show'); + } + $('#node-input-enqueueMsgValue').change(); + $('#node-input-ctrlPropChange').change(); } }); + setupTInput(node, { + typeProp: 'enqueueMsgValueType', + valueProp: 'enqueueMsgValue', + width: '120px', + defaultType: 'bool', + defaultValue: 'true', + types: [ + 'bool', + 'num', + 'str', + 'json', + 'env' + ] + }); + + $('#node-input-ctrlPropChange').on('change', (_type, _value) => { + if ( onInit) { return; } + const typef = $('#node-input-flushMsgs').typedInput('type'); + const typed = $('#node-input-dropMsgs').typedInput('type'); + if (typef === types.Undefined.value && + typed === types.Undefined.value) { + $('.row-controlMsgPropDefined').hide(); + $('.row-ctrlPropChange').hide(); + } else { + $('.row-controlMsgPropDefined').show(); + const typee = $('#node-input-enqueueMsg').typedInput('type'); + if (typee === types.Undefined.value) { + $('.row-ctrlPropChange').hide(); + } else { + $('.row-ctrlPropChange').show(); + // $('#node-input-ctrlPropValue').prop('disabled', !$('#node-input-ctrlPropChange').is(':checked')); + if ($('#node-input-ctrlPropChange').is(':checked')) { + $('#node-input-ctrlPropValue').typedInput('enable'); + } else { + $('#node-input-ctrlPropValue').typedInput('disable'); + } + } + } + }); + + setupTInput(node, { + typeProp: 'ctrlPropValueType', + valueProp: 'ctrlPropValue', + width: '90px', + defaultType: types.Delete.value, + defaultValue: '', + types: [ + types.Delete, + 'date', + 'bool', + 'num', + 'str', + 'json', + 'env', + 'msg', + 'flow', + 'global', + 'bin', + 'jsonata' + ] + }); + // #endregion controlMsgProps + // #region Enhanced settings initializeValue(node, 'tsCompare', 0); const chkVal = (name, val) => { @@ -406,36 +506,42 @@
-
-
+
+
+
- - + + + +
-
+
- - + + +
-
+
-
+
- - + + +
-
- - - - +
+ + + + +

@@ -510,12 +616,15 @@ max-width: 125px; margin-left: 5px; } + .node-input-queuingBehavior { + width: 70% !important; + } .node-input-addpropvalue { width: 90px; max-width: 90px; margin-left: 5px; } - #node-input-ctrlPropSet { + #node-input-ctrlPropChange { width: auto !important; } \ No newline at end of file diff --git a/nodes/22-delay-until.js b/nodes/22-delay-until.js index 86c8f42..fe33cf1 100644 --- a/nodes/22-delay-until.js +++ b/nodes/22-delay-until.js @@ -57,9 +57,9 @@ module.exports = function(RED) { node.timeData = { type: config.timeType, value : config.time, - offsetType : config.offsetType, + offsetType : config.offsetType || 'none', offset : config.offset, - multiplier : config.offsetMultiplier, + multiplier : config.offsetMultiplier || 60000, next: true, calcByMsg: (config.timeType === 'msg' || config.timeType === 'flow' || @@ -74,23 +74,33 @@ module.exports = function(RED) { } } node.queuingBehavior = config.queuingBehavior; - node.flushMsgs = { - type: config.flushMsgsType || 'none', - value : config.flushMsgs - }; - node.flushMsgsValue = config.flushMsgsValue; - node.dropMsgs = { - type: config.dropMsgsType || 'none', - value : config.dropMsgs - }; - node.dropMsgsValue = config.dropMsgsValue; - node.enqueueMsg = { - type: config.enqueueMsgType || 'none', - value : config.enqueueMsg - }; - node.enqueueMsgValue = config.enqueueMsgValue; - node.ctrlPropSet = config.ctrlPropSet; - node.ctrlPropValue = config.ctrlPropValue; + 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 = []; @@ -110,22 +120,14 @@ module.exports = function(RED) { // this.debug('starting ' + util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })); // this.debug('self ' + util.inspect(this, { colors: true, compact: 10, breakLength: Infinity })); - node.debug('test 00'); - if (!node.flushMsgs.type !== 'none') { + if (node.flushMsgs) { try { - const result = RED.util.getMessageProperty(msg, node.flushMsgs.value); - if (result == node.flushMsgsValue) { // eslint-disable-line eqeqeq + 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(); - const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); - if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq - if (node.ctrlPropSet) { - RED.util.setMessageProperty(msg, node.flushMsgs.value, node.ctrlPropValue, false); - } - addMsgToQueue(msg, done); - return null; - } + handleEnqueue(msg, done, node.flushMsgs); setStatus(); done(); return null; @@ -134,23 +136,14 @@ module.exports = function(RED) { node.debug(_err); } } - node.debug('test 05'); - if (!node.dropMsgs.type !== 'none') { - node.debug('test 06'); + if (node.dropMsgs) { try { - const result = RED.util.getMessageProperty(msg, node.dropMsgs.value); - if (result == node.dropMsgsValue) { // eslint-disable-line eqeqeq + 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(); - const enqueue = RED.util.getMessageProperty(msg, node.enqueueMsg.value); - if (enqueue == node.enqueueMsgValue) { // eslint-disable-line eqeqeq - if (node.ctrlPropSet) { - RED.util.setMessageProperty(msg, node.dropMsgs.value, node.ctrlPropValue, false); - } - addMsgToQueue(msg, done); - return null; - } + handleEnqueue(msg, done, node.dropMsgs); setStatus(); done(); return null; @@ -159,11 +152,10 @@ module.exports = function(RED) { node.debug(_err); } } - node.debug('test 1'); addMsgToQueue(msg, done); + setStatus(); return null; } catch (err) { - node.debug('test catch'); node.log(err.message); node.log(util.inspect(err, Object.getOwnPropertyNames(err))); node.status({fill: 'red', shape: 'dot', text: RED._('node-red-contrib-sun-position/position-config:errors.error-title') }); @@ -263,17 +255,20 @@ module.exports = function(RED) { clearTimer(); } const dNow = getIntDate(node.tsCompare, qObj.msg, node); + node.nextTime = node.positionConfig.getTimeProp(node, qObj.msg, node.timeData, dNow); 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 = node.nextTime.value.valueOf() - dNow.valueOf(); + let millisec = hlp.getTimeOut(dNow, node.nextTime.value); + // let millisec = node.nextTime.value.valueOf() - dNow.valueOf(); node.debug(`set timeout to ${node.nextTime.value.valueOf()} - ${dNow.valueOf()}`); - while (millisec < 1) { - millisec += 86400000; // 24h - } + // while (millisec < 1) { + // millisec += 86400000; // 24h + // } if (millisec > 345600000) { // there is a limitation of nodejs that the maximum setTimeout time // should not more then 2147483647 ms (24.8 days). @@ -315,9 +310,38 @@ module.exports = function(RED) { if (!node.delayTimer || node.timeData.calcByMsg) { recalcTimeOut(qObj); } - setStatus(); } + /** + * 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 * @param {*} msg - message object diff --git a/nodes/locales/de/10-position-config.json b/nodes/locales/de/10-position-config.json index b9fc547..a165d06 100644 --- a/nodes/locales/de/10-position-config.json +++ b/nodes/locales/de/10-position-config.json @@ -32,11 +32,14 @@ "ok":"Ok", "randNum":"zufällig bis", "randNumCachedDay":"zufällig (täglich) bis", - "randNumCachedWeek":"zufällig (wöchentlich) bis" + "randNumCachedWeek":"zufällig (wöchentlich) bis", + "inputPort": "Eingangs- Nachricht", + "outputPort": "Ausgangs- Nachricht" }, "types": { "unlimited": "keine Limitierung", "undefined": "nicht verwendet", + "delete":"entfernen", "msgInput": "eingangs Nachricht", "datespecific": "Zeitpunkt (erweitert)", "nodeId":"Node Name", diff --git a/nodes/locales/de/22-delay-until.json b/nodes/locales/de/22-delay-until.json new file mode 100644 index 0000000..af29d30 --- /dev/null +++ b/nodes/locales/de/22-delay-until.json @@ -0,0 +1,38 @@ +{ + "delay-until": { + "label": { + "name": "delay until", + "outputPort": "message", + "time":"Zeit", + "offset":"Offset", + "flushMsgs":"messages senden mit", + "dropMsgs":"messages verwerfen mit", + "equals": "gleich", + "compareTime":"Basis Zeit", + "now":"date.now", + "msgts":"msg.ts", + "msglc":"msg.lc", + "msgtime":"msg.time", + "showEnhSettings":"Erweiterte Einstellungen", + "enqueueMsg":"Nachricht in Warteschlange aufnehmen mit", + "ctrlPropChange":"Setze Steuer Eigenschaft", + "ctrlPropChangeB":"auf Wert", + "queuingBehavior":"Nachrichten Behandlung", + "allMsgs":"alle Nachrichten in die Warteschlange stellen", + "firstMsgs":"nur die erste Nachricht in die Warteschlange stellen", + "lastMsgs":"nur die letzte Nachricht in die Warteschlange stellen" + }, + "placeholder": { + "time":"Zeit", + "offset":"Offset time", + "flushMsgs": "alle wartenden Nachrichten senden, wenn die Eigenschaft den angegebebnen Wert hat", + "dropMsgs": "alle wartenden Nachrichten löschen, wenn die Eigenschaft den angegebebnen Wert hat", + "ctrlPropChange": "setzt die Kontrolleigenschaft auf den angegebenen Wert" + }, + "tips": { + "documentation": "Dokumentation und Beispiele", + "controlMsgProp":"Hier können spezielle Eigenschaften mit Werten der Eingangsnachricht definiert werden, womit die Nachrichten in der Warteschlange sofort gesendet werden sollen oder womit die Warteschlange geleert werden soll (und die Nachrichten zu verwerfen).", + "controlMsgPropDefined": "Wenn eine der Kontrolleigenschaften gleich dem Wert ist, werden alle Nachrichten aus der Warteschlange entfernt oder gelöscht. Wenn die Nachricht zusätzlich die Eigenschaft zum wiedereinstellen enthält, wird die Nachricht in die Warteschlange aufgenommen. In diesem Fall kann man noch festlegen ob die Steuereigenschaften der Nachricht auf einen anderen Wert gesetzt oder entfernt werden sollen." + } + } +} \ No newline at end of file diff --git a/nodes/locales/en-US/10-position-config.json b/nodes/locales/en-US/10-position-config.json index fc586df..e2ea9c0 100644 --- a/nodes/locales/en-US/10-position-config.json +++ b/nodes/locales/en-US/10-position-config.json @@ -39,6 +39,7 @@ "types": { "unlimited":"no limitation", "undefined":"not used", + "delete":"delete", "msgInput": "input message", "datespecific":"timestamp enhanced", "nodeId":"node name", diff --git a/nodes/locales/en-US/22-delay-until.json b/nodes/locales/en-US/22-delay-until.json new file mode 100644 index 0000000..d84624b --- /dev/null +++ b/nodes/locales/en-US/22-delay-until.json @@ -0,0 +1,43 @@ +{ + "delay-until": { + "label": { + "name": "delay until", + "outputPort": "message", + "time":"time", + "offset":"Offset", + "flushMsgs":"flush messages with", + "dropMsgs":"drop message with", + "equals": "equals", + "compareTime":"base time", + "now":"date.now", + "msgts":"msg.ts", + "msglc":"msg.lc", + "msgtime":"msg.time", + "showEnhSettings":"Enhanced settings", + "enqueueMsg":"enqueue message with", + "ctrlPropChange":"set control properties", + "ctrlPropChangeB":"to value", + "queuingBehavior":"message treatment", + "allMsgs":"enqueue all messages", + "firstMsgs":"enqueue only first message", + "lastMsgs":"enqueue only last (latest) message" + }, + "placeholder": { + "time":"time", + "offset":"Offset time", + "flushMsgs":"flush all waiting messages if property is equal value", + "dropMsgs":"drop all waiting messages if property is equal value", + "ctrlPropChange":"set a control propertie to the speciefied value" + }, + "tips": { + "documentation": "Documentation and examples", + "controlMsgProp":"Here can be defined special properties of the input message to flush or drop queued messages.", + "controlMsgPropDefined": "If one of the control propeties are equal to the value, all messages will be dropped or flushed from the queue. If the message contains additional the property and value for enqueue the message will be stored. With the following the control property can be set in that case to another value." + }, + "state": { + "default": "(__queueLength__) flush __sendTime__", + "intermedia": "(__queueLength__) retrigger __sendTime__", + "noTime": "(__queueLength__) will not send" + } + } +} \ No newline at end of file diff --git a/nodes/static/htmlglobal.js b/nodes/static/htmlglobal.js index a8f36e4..2221029 100644 --- a/nodes/static/htmlglobal.js +++ b/nodes/static/htmlglobal.js @@ -203,6 +203,11 @@ function getTypes(node) { // eslint-disable-line no-unused-vars label: node._('node-red-contrib-sun-position/position-config:common.types.undefined'), hasValue: false }, + Delete: { + value: 'delete', + label: node._('node-red-contrib-sun-position/position-config:common.types.delete'), + hasValue: false + }, DateSpecific: { value: 'dateSpecific', label: node._('node-red-contrib-sun-position/position-config:common.types.datespecific'), diff --git a/test/22-delay-until_spec.js b/test/22-delay-until_spec.js new file mode 100644 index 0000000..30e888d --- /dev/null +++ b/test/22-delay-until_spec.js @@ -0,0 +1,1123 @@ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable require-jsdoc */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-unused-vars */ +/* + * 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. + * + */ +/* global Context context describe beforeEach before afterEach after it */ + +/** + * Test cases for delay until node + * + * @example: + * to run all tests: npm test + * to run only testcases: npm run testnode + * to run single test: mocha -g "test configuration" + */ + +const should = require('should'); +const sinon = require('sinon'); +require('should-sinon'); + +const helper = require('node-red-node-test-helper'); + +// Nodes +const nodeConfig = require('../nodes/10-position-config.js'); +const nodeDelayUntil = require('../nodes/22-delay-until.js'); + +helper.init(require.resolve('node-red')); + +const credentials = {'nc1': {'posLatitude': '51.16406769771653', 'posLongitude': '10.447609909242438'}}; +const cfgNode = { + id: 'nc1', + type: 'position-config', + name: 'Mitte von Deutschland', + isValide: 'true', + angleType: 'deg', + timeZoneOffset: '99', + timeZoneDST: '0', + stateTimeFormat: '3', + stateDateFormat: '12', + contextStore: '' +}; +const tabNode = {id: 'flow', type: 'tab', label: 'FLOW' }; +const hlpNode = {id: 'n2', type: 'helper'}; + +describe('delay until node', function() { + beforeEach(function (done) { + helper.startServer(done); + }); + + afterEach(function (done) { + helper.unload().then(function () { + helper.stopServer(done); + }); + }); + + describe('test configuration (delay until)', function() { + it('fail if missing configuration ', done => { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: '', + time: '10:00', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + const n1 = helper.getNode('n1'); // delay until node + const n2 = helper.getNode('n2'); // helper node + try { + n1.status.should.be.calledOnce(); + n1.error.should.be.calledOnce().and.calledWith('node-red-contrib-sun-position/position-config:errors.config-missing'); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if latitude missing ', done => { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeDelayUntil], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if longitude missing ', done => { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '5'}}; + helper.load([nodeConfig, nodeDelayUntil], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if latitude invalid ', done => { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '90.1', 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeDelayUntil], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if longitude invalid ', done => { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '51', 'posLongitude': '180.1'}}; + helper.load([nodeConfig, nodeDelayUntil], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('test if correctly loaded', function (done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + offset: 0, + offsetType: 'none', + offsetMultiplier: 60000, + queuingBehavior: 'all', + flushMsgs: '', + flushMsgsType: 'none', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + dropMsgs: '', + dropMsgsType: 'none', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + enqueueMsg: '', + enqueueMsgType: 'none', + enqueueMsgValue: 'true', + enqueueMsgValueType: 'bool', + ctrlPropChange: false, + ctrlPropValue: '', + ctrlPropValueType: 'delete', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + const n1 = helper.getNode('n1'); + try { + n1.status.should.be.calledOnce(); + // should.not.exist(err); + // should.exist(n1); + n1.should.have.property('name', 'delayuntil'); + n1.should.have.property('type', 'rdg-delay-until'); + n1.should.have.property('positionConfig'); + n1.should.have.property('timeData'); + n1.timeData.should.have.property('type', 'entered'); + n1.timeData.should.have.property('value', '10:00'); + n1.timeData.should.have.property('offsetType', 'none'); + n1.timeData.should.have.property('offset', 0); + n1.timeData.should.have.property('multiplier', 60000); + n1.timeData.should.have.property('next', true); + // msg.payload.should.have.keys('next', 'last'); + n1.should.have.property('queuingBehavior', 'all'); + n1.should.not.have.property('flushMsgs'); + n1.should.not.have.property('dropMsgs'); + n1.should.not.have.property('enqueueMsg'); + n1.should.not.have.property('ctrlProp'); + n1.should.have.property('tsCompare', 0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('test if correctly loaded enhanced', function (done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + name: 'delayuntil', + positionConfig: 'nc1', + time: '10:00', + timeType: 'entered', + offset: 0, + offsetType: 'none', + offsetMultiplier: 60000, + queuingBehavior: 'all', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'true', + enqueueMsgValueType: 'bool', + ctrlPropChange: true, + ctrlPropValue: '', + ctrlPropValueType: 'delete', + tsCompare: '0', + wires: [[]] + }, cfgNode]; + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + const n1 = helper.getNode('n1'); + try { + n1.status.should.be.calledOnce(); + // should.not.exist(err); + // should.exist(n1); + n1.should.have.property('name', 'delayuntil'); + n1.should.have.property('type', 'rdg-delay-until'); + n1.should.have.property('positionConfig'); + n1.should.have.property('timeData'); + n1.timeData.should.have.property('type', 'entered'); + n1.timeData.should.have.property('value', '10:00'); + n1.timeData.should.have.property('offsetType', 'none'); + n1.timeData.should.have.property('offset', 0); + n1.timeData.should.have.property('multiplier', 60000); + n1.timeData.should.have.property('next', true); + // msg.payload.should.have.keys('next', 'last'); + n1.should.have.property('queuingBehavior', 'all'); + n1.should.have.property('flushMsgs'); + n1.flushMsgs.should.have.property('type', 'msg'); + n1.flushMsgs.should.have.property('value', 'flush'); + n1.flushMsgs.should.have.property('compare', true); + n1.should.have.property('dropMsgs'); + n1.dropMsgs.should.have.property('type', 'msg'); + n1.dropMsgs.should.have.property('value', 'drop'); + n1.dropMsgs.should.have.property('compare', true); + n1.should.have.property('enqueueMsg'); + n1.enqueueMsg.should.have.property('type', 'msg'); + n1.enqueueMsg.should.have.property('value', 'enqueue'); + n1.enqueueMsg.should.have.property('compare', true); + n1.should.have.property('ctrlProp'); + n1.ctrlProp.should.have.property('type', 'delete'); + n1.ctrlProp.should.have.property('value', ''); + n1.should.have.property('tsCompare', 0); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe('test message queue (delay until)', function() { + let clock = null; + + beforeEach(function() { + clock = sinon.useFakeTimers({toFake: ['Date', 'setTimeout', 'clearTimeout']}); + // now:1577833200, + }); + + afterEach(function() { + helper.unload(); + sinon.restore(); + }); + + it('should delay message until specific time', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg) { + try { + msg.should.have.property('payload', 42); + msg.should.have.property('topic', 'test'); + done(); + } catch(err) { + console.log(msg); // eslint-disable-line no-console + done(err); + } + }); + + n1.receive({payload: 42, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should trigger at specified time with offset', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + offset: 1, + offsetType: 'num', + offsetMultiplier: 60000, + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg) { + try { + msg.should.have.property('payload', 'test with offset'); + msg.should.have.property('topic', 'test'); + done(); + } catch(err) { + console.log(msg); // eslint-disable-line no-console + done(err); + } + }); + + n1.receive({payload: 'test with offset', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 120000); + n1.msgQueue.should.have.length(1); + clock.tick(120000); // advance clock by 2 mins + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should drop messages', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({drop: true}); + n1.msgQueue.should.have.length(0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('should drop messages, if only first is stored', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'first', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({drop: true}); + n1.msgQueue.should.have.length(0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('should drop messages, if only last is stored', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'last', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({drop: true}); + n1.msgQueue.should.have.length(0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('should drop messages and re-enqueue', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'true', + enqueueMsgValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg) { + try { + msg.should.have.property('topic', 'test'); + msg.should.have.property('drop', true); + msg.should.have.property('enqueue', true); + done(); + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({drop: true, enqueue: true, topic: 'test'}); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should drop messagess and re-enqueue not matching', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'yes', + enqueueMsgValueType: 'str', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({drop: true, enqueue: 'no'}); + n1.msgQueue.should.have.length(0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('should delete control properties for drop message', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + dropMsgs: 'drop', + dropMsgsType: 'msg', + dropMsgsValue: 'true', + dropMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'yes', + enqueueMsgValueType: 'str', + offsetType: 'none', + queuingBehavior: 'all', + ctrlPropChange: true, + ctrlPropValue: '', + ctrlPropValueType: 'delete', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg) { + try { + msg.should.not.have.property('drop'); + msg.should.not.have.property('enqueue'); + // msg.should.have.property('drop', 'false'); + // msg.should.have.property('enqueue', 'false'); + done(); + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({drop: true, enqueue: 'yes'}); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should flush messages', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + let msgCount = 0; + + n2.on('input', function(msg){ + try { + msgCount++; + msg.should.have.property('topic', 'test'); + msg.should.have.property('payload', msgCount); + if (msgCount === 2) { + done(); + } + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 1, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 2, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({flush: true}); + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should flush messages and re-enqueue', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'true', + enqueueMsgValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + let msgCount = 0; + + n2.on('input', function(msg) { + try { + msgCount++; + msg.should.have.property('topic', 'test'); + if (msgCount < 3) { + msg.should.have.property('payload', 'test' + msgCount); + } else { + msg.should.have.property('flush', true); + msg.should.have.property('enqueue', true); + done(); + } + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({flush: true, enqueue: true, topic: 'test'}); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should flush messagess and re-enqueue not matching', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'yes', + enqueueMsgValueType: 'str', + offsetType: 'none', + queuingBehavior: 'all', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + let msgCount = 0; + + n2.on('input', function(msg) { + try { + msgCount++; + msg.should.have.property('topic', 'test'); + if (msgCount < 3) { + msg.should.have.property('payload', 'test' + msgCount); + } else { + msg.should.have.property('flush', true); + msg.should.have.property('enqueue', true); + done(); + } + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({flush: true, enqueue: 'no', topic: 'test'}); + n1.msgQueue.should.have.length(0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('should delete control properties for flush message', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + enqueueMsg: 'enqueue', + enqueueMsgType: 'msg', + enqueueMsgValue: 'yes', + enqueueMsgValueType: 'str', + offsetType: 'none', + queuingBehavior: 'all', + ctrlPropChange: true, + ctrlPropValue: '', + ctrlPropValueType: 'delete', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + let msgCount = 0; + + n2.on('input', function(msg) { + try { + msgCount++; + msg.should.have.property('topic', 'test'); + msg.should.have.property('payload', 'test' + msgCount); + msg.should.not.have.property('flush'); + msg.should.not.have.property('enqueue'); + if (msgCount >= 3) { + done(); + } + } catch(err) { + done(err); + } + }); + n1.receive({payload: 'test1', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 'test2', topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(2); + n1.receive({flush: true, enqueue: 'yes', topic: 'test', payload: 'test3'}); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should delay only last message', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + offsetType: 'none', + queuingBehavior: 'last', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg){ + try { + msg.should.have.property('topic', 'test'); + msg.should.have.property('payload', 3); + done(); + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 1, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 2, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 3, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should flush last message', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + flushMsgs: 'flush', + flushMsgsType: 'msg', + flushMsgsValue: 'true', + flushMsgsValueType: 'bool', + offsetType: 'none', + queuingBehavior: 'last', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg){ + try { + msg.should.have.property('topic', 'test'); + msg.should.have.property('payload', 2); + done(); + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 1, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 2, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({flush: true}); + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + + it('should delay only first message', function(done) { + const flow = [ + { + id: 'n1', + type: 'rdg-delay-until', + z: 'flow', + name: 'delayuntil', + positionConfig: 'nc1', + time: '00:01 utc', + timeType: 'entered', + offsetType: 'none', + queuingBehavior: 'first', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode, tabNode]; + sinon.spy(clock, 'setTimeout'); + + helper.load([nodeConfig, nodeDelayUntil], flow, credentials, function() { + try { + const n1 = helper.getNode('n1'); + const n2 = helper.getNode('n2'); + + n2.on('input', function(msg){ + try { + msg.should.have.property('topic', 'test'); + msg.should.have.property('payload', 1); + done(); + } catch(err) { + done(err); + } + }); + + n1.receive({payload: 1, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 2, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + n1.receive({payload: 3, topic: 'test'}); + clock.setTimeout.should.be.calledWith(sinon.match.any, 60000); + n1.msgQueue.should.have.length(1); + clock.tick(60000); // advance clock by 1 min + n1.msgQueue.should.have.length(0); + } catch(err) { + done(err); + } + }); + }); + }); +}); \ No newline at end of file From 237336845620f234e73858bc3807d437143e130f Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:18:01 +0100 Subject: [PATCH 21/44] fix missing map marker --- nodes/10-position-config.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nodes/10-position-config.html b/nodes/10-position-config.html index cd5ac2b..67a724d 100644 --- a/nodes/10-position-config.html +++ b/nodes/10-position-config.html @@ -139,11 +139,7 @@ $('#node-config-map-row').show(); const clat = parseFloat(lat); const clon = parseFloat(lon); - $('#node-config-map-content').attr('src', `https://www.openstreetmap.org/export/embed.html?bbox=${(clon-0.004).toFixed(14)}%2C${(clat-0.004).toFixed(14)}%2C${(clon+0.004).toFixed(14)}%2C${(clat+0.004).toFixed(14)}&layer=mapnik&marker=${lat}%2C${lon}`); - // src="https://www.openstreetmap.org/export/embed.html?bbox=13.630465865135195%2C51.088954687366886%2C13.633700609207155%2C51.09019968648441&layer=mapnik&marker=51.089577967896076%2C13.632082700696628 - // src="https://www.openstreetmap.org/export/embed.html?bbox=13.61883000000000%2C51.08226000000000%2C13.62883000000000%2C51.09226000000000&layer=mapnik&marker=51.08726%2C13.62383" - // https://www.openstreetmap.org/export/embed.html?bbox=13.62281000000000%2C51.08621 %2C13.62481 %2C51.08821 &layer=mapnik&marker=51.08721%2C13.62381 - // src="https://www.openstreetmap.org/export/embed.html?bbox=51.07726%2C13.61383%2C51.087260.01%2C13.623830.01&layer=mapnik + $('#node-config-map-content').attr('src', `https://www.openstreetmap.org/export/embed.html?bbox=${(clon-0.004).toFixed(14)}%2C${(clat-0.004).toFixed(14)}%2C${(clon+0.004).toFixed(14)}%2C${(clat+0.004).toFixed(14)}&layer=mapnik&marker=${clat.toFixed(14)}%2C${clon.toFixed(14)}`); $('#node-config-map-link').html(`Openstreetmap`); } else { $('#node-config-map-row').hide(); From 1017a108634ddf9b94d53428fc455032de778d99 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:08:34 +0100 Subject: [PATCH 22/44] changed state output in case time span is over midnight, because the shown times could be not correct #416 - within-time-switch - changed state output in case time span is over midnight, because the shown times could be not correct #416 - added automatic test case - basic inject --- CHANGELOG.md | 5 + nodes/21-within-time-switch.js | 93 +++-- test/21-within-time-switch_spec.js | 648 +++++++++++++++++++++++++++++ test/22-delay-until_spec.js | 2 +- 4 files changed, 709 insertions(+), 39 deletions(-) create mode 100644 test/21-within-time-switch_spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5968beb..16dbe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ This can be also used to go back to an older Version. - basic inject - time based inject +- within-time-switch + - changed state output in case time span is over midnight, because the shown times could be not correct #416 + - added automatic test case + - basic inject + - new Node. delay-until which allows to delay messages until defined time ### 2.1.1: bug fixes diff --git a/nodes/21-within-time-switch.js b/nodes/21-within-time-switch.js index 6d1fbc3..44a3625 100644 --- a/nodes/21-within-time-switch.js +++ b/nodes/21-within-time-switch.js @@ -77,7 +77,7 @@ module.exports = function (RED) { * @param {*} data - the state data * @returns {boolean} */ - function setstate(node, data) { + function setstate(node, data, reverse) { if (data.error) { node.status({ fill: 'red', @@ -99,11 +99,19 @@ module.exports = function (RED) { } else if (data.end && data.end.error) { hlp.handleError(node, RED._('within-time-switch.errors.error-end-time', { message : data.end.error}), undefined, data.end.error); } else if (data.start && data.start.value && data.end && data.end.value) { - node.status({ - fill: 'blue', - shape: 'dot', - text: '⏵' + node.positionConfig.toTimeString(data.start.value) + data.startSuffix + ' - ⏴' + node.positionConfig.toTimeString(data.end.value) + data.endSuffix - }); + if (reverse) { + node.status({ + fill: 'blue', + shape: 'dot', + text: node.positionConfig.toTimeString(data.start.value) + data.startSuffix + '⏵ ~ 🌃 ~ ⏴' + node.positionConfig.toTimeString(data.end.value) + data.endSuffix + }); + } else { + node.status({ + fill: 'blue', + shape: 'dot', + text: '⏵' + node.positionConfig.toTimeString(data.start.value) + data.startSuffix + ' - ⏴' + node.positionConfig.toTimeString(data.end.value) + data.endSuffix + }); + } } return false; } @@ -324,7 +332,8 @@ module.exports = function (RED) { value : config.startTime, offsetType : config.startOffsetType, offset : config.startOffset, - multiplier : config.startOffsetMultiplier + multiplier : config.startOffsetMultiplier, + next : false }; node.timeEnd = { @@ -332,7 +341,8 @@ module.exports = function (RED) { value : config.endTime, offsetType : config.endOffsetType, offset : config.endOffset, - multiplier : config.endOffsetMultiplier + multiplier : config.endOffsetMultiplier, + next : false }; node.timeStartAlt = { @@ -340,29 +350,31 @@ module.exports = function (RED) { value : config.startTimeAlt, offsetType : config.startOffsetAltType, offset : config.startOffsetAlt, - multiplier : config.startOffsetAltMultiplier + multiplier : config.startOffsetAltMultiplier, + next : false }; - node.propertyStartOperator = config.propertyStartCompare || 'true'; node.propertyStart = { type : config.propertyStartType || 'none', value : config.propertyStart || '' }; - node.propertyStartThreshold = { - type : config.propertyStartThresholdType || 'none', - value : config.propertyStartThreshold || '' - }; - if (node.positionConfig && node.propertyStart.type === 'jsonata') { - try { - node.propertyStart.expr = node.positionConfig.getJSONataExpression(node, node.propertyStart.value); - } catch (err) { - node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - node.propertyStart.expr = null; - } - } if (node.propertyStart.type === 'none' || node.timeStartAlt.type === 'none') { node.propertyStart.type = 'none'; delete node.timeStartAlt; + } else { + node.propertyStartOperator = config.propertyStartCompare || 'true'; + node.propertyStartThreshold = { + type : config.propertyStartThresholdType || 'none', + value : config.propertyStartThreshold || '' + }; + if (node.positionConfig && node.propertyStart.type === 'jsonata') { + try { + node.propertyStart.expr = node.positionConfig.getJSONataExpression(node, node.propertyStart.value); + } catch (err) { + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.propertyStart.expr = null; + } + } } node.timeEndAlt = { @@ -370,29 +382,31 @@ module.exports = function (RED) { value : config.endTimeAlt, offsetType : config.endOffsetAltType, offset : config.endOffsetAlt, - multiplier : config.endOffsetAltMultiplier + multiplier : config.endOffsetAltMultiplier, + next : false }; - node.propertyEndOperator = config.propertyEndCompare || 'true'; node.propertyEnd = { type : config.propertyEndType || 'none', value : config.propertyEnd || '' }; - node.propertyEndThreshold = { - type : config.propertyEndThresholdType || 'none', - value : config.propertyEndThreshold || '' - }; - if (node.positionConfig && node.propertyEnd.type === 'jsonata') { - try { - node.propertyEnd.expr = node.positionConfig.getJSONataExpression(node, node.propertyEnd.value); - } catch (err) { - node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); - node.propertyEnd.expr = null; - } - } if (node.propertyEnd.type === 'none' || node.timeEndAlt.type === 'none') { node.propertyEnd.type = 'none'; delete node.timeEndAlt; + } else { + node.propertyEndOperator = config.propertyEndCompare || 'true'; + node.propertyEndThreshold = { + type : config.propertyEndThresholdType || 'none', + value : config.propertyEndThreshold || '' + }; + if (node.positionConfig && node.propertyEnd.type === 'jsonata') { + try { + node.propertyEnd.expr = node.positionConfig.getJSONataExpression(node, node.propertyEnd.value); + } catch (err) { + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); + node.propertyEnd.expr = null; + } + } } node.timeRestrictions = { @@ -488,8 +502,8 @@ module.exports = function (RED) { msg.withinTimeStart.id = hlp.getTimeNumberUTC(result.start.value); msg.withinTimeEnd.id = hlp.getTimeNumberUTC(result.end.value); const cmpNow = hlp.getTimeNumberUTC(dNow); - setstate(node, result); if (msg.withinTimeStart.id < msg.withinTimeEnd.id) { + setstate(node, result, false); if (cmpNow >= msg.withinTimeStart.id && cmpNow < msg.withinTimeEnd.id) { msg.withinTime = true; this.debug('in time [1] - send msg to first output ' + result.startSuffix + @@ -505,6 +519,7 @@ module.exports = function (RED) { return null; } } else if (!(cmpNow >= msg.withinTimeEnd.id && cmpNow < msg.withinTimeStart.id)) { + setstate(node, result, true); msg.withinTime = true; this.debug('in time [2] - send msg to first output ' + result.startSuffix + node.positionConfig.toDateTimeString(dNow) + result.endSuffix + ' (' + msg.withinTimeStart.id + ' - ' + cmpNow + ' - ' + msg.withinTimeEnd.id + ')'); @@ -517,9 +532,11 @@ module.exports = function (RED) { } done(); return null; + } else { + setstate(node, result, true); } } else { - setstate(node, result); + setstate(node, result, false); } msg.withinTime = false; this.debug('out of time - send msg to second output ' + result.startSuffix + node.positionConfig.toDateTimeString(dNow) + result.endSuffix); diff --git a/test/21-within-time-switch_spec.js b/test/21-within-time-switch_spec.js new file mode 100644 index 0000000..4eb9bc1 --- /dev/null +++ b/test/21-within-time-switch_spec.js @@ -0,0 +1,648 @@ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable require-jsdoc */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable no-unused-vars */ +/* + * 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. + * + */ +/* global Context context describe beforeEach before afterEach after it */ + +/** + * Test cases for delay until node + * + * @example: + * to run all tests: npm test + * to run only testcases: npm run testnode + * to run single test: mocha -g "within time switch" + */ + +const should = require('should'); +const sinon = require('sinon'); +require('should-sinon'); + +const helper = require('node-red-node-test-helper'); + +// Nodes +const nodeConfig = require('../nodes/10-position-config.js'); +const nodeWithinTimeSwitch = require('../nodes/21-within-time-switch.js'); + +helper.init(require.resolve('node-red')); + +const credentials = {'nc1': {'posLatitude': '51.16406769771653', 'posLongitude': '10.447609909242438'}}; +const cfgNode = { + id: 'nc1', + type: 'position-config', + name: 'Mitte von Deutschland', + isValide: 'true', + angleType: 'deg', + timeZoneOffset: '99', + timeZoneDST: '0', + stateTimeFormat: '3', + stateDateFormat: '12', + contextStore: '' +}; +const tabNode = {id: 'flow', type: 'tab', label: 'FLOW' }; +const hlpNode = {id: 'n2', type: 'helper'}; + +describe('within time switch node', function() { + beforeEach(function (done) { + helper.startServer(done); + }); + + afterEach(function (done) { + helper.unload().then(function () { + helper.stopServer(done); + }); + }); + + describe('test configuration (within time switch)', function() { + it('fail if missing configuration ', done => { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: '', + nameInt: '', + positionConfig: '', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode, hlpNode]; + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, credentials, function() { + const n1 = helper.getNode('n1'); // within time switch node + const n2 = helper.getNode('n2'); // helper node + try { + n1.status.should.be.calledOnce(); + n1.error.should.be.calledOnce().and.calledWith('node-red-contrib-sun-position/position-config:errors.config-missing'); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if latitude missing ', done => { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: '', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if longitude missing ', done => { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: '', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '5'}}; + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if latitude invalid ', done => { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: '', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '90.1', 'posLongitude': '10'}}; + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.latitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('fail if longitude invalid ', done => { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: '', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + const invalidCreds = {'nc1': { 'posLatitude': '51', 'posLongitude': '180.1'}}; + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, invalidCreds, function() { + const n1 = helper.getNode('n1'); + const nc1 = helper.getNode('nc1'); + try { + n1.status.should.be.called(); + n1.error.should.be.called().and.calledWith('position-config.errors.longitude-missing'); + nc1.error.should.be.called(); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('test if correctly loaded', function (done) { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: 'time switch', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: '', + propertyStartType: 'none', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: '', + propertyEndType: 'none', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, credentials, function() { + const n1 = helper.getNode('n1'); + try { + n1.status.should.be.calledOnce(); + // should.not.exist(err); + // should.exist(n1); + n1.should.have.property('name', 'time switch'); + n1.should.have.property('type', 'within-time-switch'); + n1.should.have.property('positionConfig'); + + n1.should.have.property('timeStart'); + n1.timeStart.should.have.property('type', 'entered'); + n1.timeStart.should.have.property('value', '10:00'); + n1.timeStart.should.have.property('offsetType', 'none'); + n1.timeStart.should.have.property('offset', 0); + n1.timeStart.should.have.property('multiplier', 60000); + n1.timeStart.should.have.property('next', false); + + n1.should.have.property('timeEnd'); + n1.timeEnd.should.have.property('type', 'entered'); + n1.timeEnd.should.have.property('value', '12:00'); + n1.timeEnd.should.have.property('offsetType', 'none'); + n1.timeEnd.should.have.property('offset', 0); + n1.timeEnd.should.have.property('multiplier', 60000); + n1.timeEnd.should.have.property('next', false); + + n1.should.not.have.property('timeStartAlt'); + n1.should.not.have.property('timeEndAlt'); + + n1.should.have.property('propertyStart'); + n1.propertyStart.should.have.property('type', 'none'); + n1.should.not.have.property('propertyStartThreshold'); + n1.should.have.property('propertyEnd'); + n1.propertyEnd.should.have.property('type', 'none'); + n1.should.not.have.property('propertyEndThreshold'); + // msg.payload.should.have.keys('next', 'last'); + n1.should.have.property('tsCompare', 0); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('test if correctly loaded (enhanced)', function (done) { + const flow = [ + { + id: 'n1', + type: 'within-time-switch', + name: 'time switch', + nameInt: '', + positionConfig: 'nc1', + startTime: '10:00', + startTimeType: 'entered', + startOffset: 0, + startOffsetType: 'none', + startOffsetMultiplier: 60000, + endTime: '12:00', + endTimeType: 'entered', + endOffset: 0, + endOffsetType: 'none', + endOffsetMultiplier: 60000, + timeRestrictions: 0, + timeRestrictionsType: 'none', + timeDays: '*', + timeOnlyOddDays: false, + timeOnlyEvenDays: false, + timeOnlyOddWeeks: false, + timeOnlyEvenWeeks: false, + timeMonths: '*', + timedatestart: '', + timedateend: '', + propertyStart: 'altStart', + propertyStartType: 'msg', + propertyStartCompare: 'true', + propertyStartThreshold: '', + propertyStartThresholdType: 'num', + startTimeAlt: '11:00', + startTimeAltType: 'entered', + startOffsetAlt: 0, + startOffsetAltType: 'none', + startOffsetAltMultiplier: 60000, + propertyEnd: 'altEnd', + propertyEndType: 'msg', + propertyEndCompare: 'true', + propertyEndThreshold: '', + propertyEndThresholdType: 'num', + endTimeAlt: '13:00', + endTimeAltType: 'entered', + endOffsetAlt: 0, + endOffsetAltType: 'none', + endOffsetAltMultiplier: 60000, + withinTimeValue: 'true', + withinTimeValueType: 'msgInput', + outOfTimeValue: 'false', + outOfTimeValueType: 'msgInput', + tsCompare: '0', + wires: [['n2']] + }, cfgNode]; + + helper.load([nodeConfig, nodeWithinTimeSwitch], flow, credentials, function() { + const n1 = helper.getNode('n1'); + try { + n1.status.should.be.calledOnce(); + // should.not.exist(err); + // should.exist(n1); + n1.should.have.property('name', 'time switch'); + n1.should.have.property('type', 'within-time-switch'); + n1.should.have.property('positionConfig'); + + n1.should.have.property('timeStart'); + n1.timeStart.should.have.property('type', 'entered'); + n1.timeStart.should.have.property('value', '10:00'); + n1.timeStart.should.have.property('offsetType', 'none'); + n1.timeStart.should.have.property('offset', 0); + n1.timeStart.should.have.property('multiplier', 60000); + n1.timeStart.should.have.property('next', false); + + n1.should.have.property('timeEnd'); + n1.timeEnd.should.have.property('type', 'entered'); + n1.timeEnd.should.have.property('value', '12:00'); + n1.timeEnd.should.have.property('offsetType', 'none'); + n1.timeEnd.should.have.property('offset', 0); + n1.timeEnd.should.have.property('multiplier', 60000); + n1.timeEnd.should.have.property('next', false); + + n1.should.have.property('timeStartAlt'); + n1.timeStartAlt.should.have.property('type', 'entered'); + n1.timeStartAlt.should.have.property('value', '11:00'); + n1.should.have.property('timeEndAlt'); + n1.timeEndAlt.should.have.property('type', 'entered'); + n1.timeEndAlt.should.have.property('value', '13:00'); + + n1.should.have.property('propertyStart'); + n1.propertyStart.should.have.property('type', 'msg'); + n1.propertyStart.should.have.property('value', 'altStart'); + n1.should.have.property('propertyStartThreshold'); + n1.should.have.property('propertyStartOperator'); + + n1.should.have.property('propertyEnd'); + n1.propertyEnd.should.have.property('type', 'msg'); + n1.propertyEnd.should.have.property('value', 'altEnd'); + n1.should.have.property('propertyEndThreshold'); + n1.should.have.property('propertyEndOperator'); + // msg.payload.should.have.keys('next', 'last'); + n1.should.have.property('tsCompare', 0); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe('test message passing', function() { + it('test needs to be implemented', function() { + this.skip(); + }); + + it('should pass to first output in within time', function() { + this.skip(); + }); + + it('should pass to second output in case out of time', function() { + this.skip(); + }); + }); +}); \ No newline at end of file diff --git a/test/22-delay-until_spec.js b/test/22-delay-until_spec.js index 30e888d..da2714c 100644 --- a/test/22-delay-until_spec.js +++ b/test/22-delay-until_spec.js @@ -31,7 +31,7 @@ * @example: * to run all tests: npm test * to run only testcases: npm run testnode - * to run single test: mocha -g "test configuration" + * to run single test: mocha -g "delay until" */ const should = require('should'); From 3386b3c5ec70ca442110c6380dc4578247750237 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Fri, 4 Mar 2022 17:55:50 +0100 Subject: [PATCH 23/44] added type definition, enabled type checking --- jsconfig.json | 15 + nodes/10-position-config.js | 568 ++++++++++++++-------- nodes/20-time-inject.js | 292 ++++++++--- nodes/21-within-time-switch.js | 82 +++- nodes/22-delay-until.js | 83 +++- nodes/30-sun-position.js | 111 +++-- nodes/31-moon-position.js | 82 +++- nodes/60-time-comp.js | 77 ++- nodes/61-time-span.js | 86 +++- nodes/80-blind-control.js | 62 ++- nodes/81-clock-timer.js | 61 ++- nodes/lib/dateTimeHelper.js | 438 +++++++++++------ nodes/lib/suncalc.js | 366 ++++++++------ nodes/lib/timeControlHelper.js | 105 ++-- nodes/locales/de/80-blind-control.json | 4 +- nodes/locales/en-US/80-blind-control.json | 4 +- nodes/types/typedefs.js | 361 ++++++++++++++ 17 files changed, 2014 insertions(+), 783 deletions(-) create mode 100644 jsconfig.json create mode 100644 nodes/types/typedefs.js diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..2e1495f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "commonJS", + "target": "es6", + "lib": [ + "es2019" + ], + "moduleResolution": "node", + "sourceMap": true, + "alwaysStrict": true, + "resolveJsonModule": true + }, + "exclude": ["node_modules", "**/node_modules/*", "temp", "**/temp/*", "backup", "**/backup/*", "examples", "**/examples/*"] + } \ No newline at end of file diff --git a/nodes/10-position-config.js b/nodes/10-position-config.js index cc585e5..8cab72c 100644 --- a/nodes/10-position-config.js +++ b/nodes/10-position-config.js @@ -24,23 +24,255 @@ * position-config: *********************************************/ '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").ITimeObject} ITimeObject + * @typedef {import("./lib/dateTimeHelper").ILimitationsObj} ILimitationsObj + * @typedef {import("./lib/suncalc.js").ISunTimeSingle} ISunTimeSingle + */ -// (function() { -/*******************************************************************************************************/ +// IPositionConfigNodeInstance +/** + * @typedef {Object} IPositionConfigNodeInstance Extensions for the nodeInstance object type + * @property {Object} positionConfig - tbd + * @property {string} addId internal used additional id + * @property {Object} nodeData get/set generic Data of the node + * @property {Object} sunData - the sun data Object + * @property {Object} windowSettings - the window settings Object + * @property {number} smoothTime smoothTime + * @property {Object} reason - tbd + * @property {boolean} levelReverse - indicator if the Level is in reverse order + * @property {string} contextStore - used context store + * @property {Array.} oversteers - tbd + * @property {Object} oversteer - tbd + * @property {Object} rules - tbd + * @property {Object} level - tbd + * @property {Object} previousData - tbd + * @property {Array.} results - tbd + * + * @property {Object} autoTrigger autotrigger options + * @property {NodeJS.Timeout} autoTriggerObj autotrigger options + * + * @property {Object} startDelayTimeOut - tbd + * @property {NodeJS.Timeout} startDelayTimeOutObj - tbd + + * @property {function} setState function for settign the state of the node + * @property {function} register register a node as child + * @property {function} deregister remove a previous registered node as child + * @property {FktCheckNode} checkNode checks the node configuration + * @property {FktFormatDate} toDateTimeString Formate a Date Object to a Date and Time String + * @property {FktFormatDate} toTimeString Formate a Date Object to a Time String + * @property {FktFormatDate} toDateString Formate a Date Object to a Date String + * @property {FktGetFloatProp} getFloatProp get a float value from a type input in Node-Red + * @property {FktFormatOutDate} formatOutDate get an formated date prepared for output + * @property {FktGetOutDataProp} getOutDataProp get the time Data prepared for output + * @property {FktSetMessageProp} setMessageProp Creates a out object, based on input data + * @property {FktGetTimeProp} getTimeProp get the time Data from a typed input + * @property {FktGetPropValue} getPropValue get a property value from a type input in Node-Red + * @property {FktComparePropValue} comparePropValue compared two property's + * @property {function} getSunCalc + * @property {function} getMoonCalc + * @property {FktGetJSONataExpression} getJSONataExpression get a prepared JSONATA Expression + * ... obviously there are more ... + */ -module.exports = function (RED) { - 'use strict'; +/** + * check this node for configuration errors + * @typedef {function} FktCheckNode + * @property {onErrorCallback} [onError] - if an error occurs this function will be called + * @property {any} [onOk] - the return value in case of ok + * @return {boolean|string} returns the result of onrror if an error occurs, otherwise onOK + */ - const path = require('path'); +/** + * Formate a Date Object + * @typedef {function} FktFormatDate + * @param {Date} dt Date to format to Date and Time string + * @returns {string} formated Date object + */ - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); - const util = require('util'); +/** + * get a float value from a type input in Node-Red + * @typedef {function} FktGetFloatProp + * @param {*} _srcNode - source node information + * @param {*} msg - message object + * @param {string} type - type name of the type input + * @param {*} value - value of the type input + * @param {*} [def] - default value if can not get float value + * @param {*} [callback] - callback function for getting getPropValue + * @param {boolean} [noError] - true if no error should be given in GUI + * @param {Date} [dNow] base Date to use for Date time functions + * @returns {number} float property + */ + +/** + * get an formated date prepared for output + * @typedef {function} FktFormatOutDate + * @param {*} _srcNode - source node for logging + * @param {*} msg - the message object + * @param {Date} dateValue - the source date object which should be formated + * @param {ITimePropertyType} data - additional formating and control data + * @param {Date} [dNow] base Date to use for Date time functions + */ + +/** + * get the time Data prepared for output + * @typedef {function} FktGetOutDataProp + * @param {*} _srcNode - source node for logging + * @param {*} msg - the message object + * @param {ITimePropertyType} data - a Data object + * @param {Date} [dNow] base Date to use for Date time functions + * @param {boolean} [noError] - true if no error shoudl be given in GUI + * @returns {*} output Data + */ + +/** + * Creates a out object, based on input data + * @typedef {function} FktSetMessageProp + * @param {*} _srcNode The base node + * @param {*} msg The Message Object to set the Data + * @param {*} type type of the property to set + * @param {*} value value of the property to set + * @param {*} data Data object to set to the property + */ + +/** + * get the time Data from a typed input + * @typedef {function} FktGetTimeProp + * @param {*} _srcNode - source node for logging + * @param {*} msg - the message object + * @param {ITimePropertyType} data - a Data object + * @param {*} [dNow] base Date to use for Date time functions + * @returns {ITimePropertyResult} value of the type input + */ + +/** + * get a prepared JSONATA Expression + * @typedef {function} FktGetJSONataExpression + * @param {*} _srcNode - source node information + * @param {string} value - get an expression for a value + * @returns {function} JSONataExpression + */ + +/** + * get a property value from a type input in Node-Red + * @typedef {function} FktGetPropValue + * @param {*} _srcNode - source node information + * @param {*} msg - message object + * @param {IValuePropertyType} data - data object with more information + * @param {boolean} [noError] - true if no error shoudl be given in GUI + * @param {Date} [dNow] base Date to use for Date time functions + * @returns {*} value of the type input, return of the callback function if defined or __null__ if value could not resolved +*/ + +/** + * compared two property's + * @typedef {function} FktComparePropValue + * @param {*} _srcNode - source node information + * @param {*} msg - message object + * @property {IValuePropertyType} operandA - first operand + * @property {string} compare - compare between the both operands + * @property {IValuePropertyType} operandB - second operand + * @param {boolean} [noError] - true if no error shoudl be given in GUI + * @param {Date} [dNow] base Date to use for Date time functions + * @returns {*} value of the type input, return of the callback function if defined or __null__ if value could not resolved +*/ + +/** + * @typedef {IPositionConfigNodeInstance & runtimeNode} IPositionConfigNode Combine nodeInstance with additional, optional functions + */ + +/** + * @typedef {Object} ITimeResult + * @property {Date} value - a Date object of the neesed date/time + * @property {number} ts - The time as unix timestamp + * @property {number} pos - The position of the sun on the time + * @property {number} angle - Angle of the sun on the time + * @property {number} julian - The time as julian calendar + * @property {boolean} valid - indicates if the time is valid or not + * @property {string} [error] - string of an error message if an error occurs + */ - const sunCalc = require(path.join(__dirname, '/lib/suncalc.js')); +/** + * @typedef {Object} ISunTimeDefRed + * @property {string} name - The Name of the time + * @property {Date} value - Date object with the calculated sun-time + * @property {number} pos - The position of the sun on the time + * @property {number} elevation - The elevation angle + * @property {boolean} valid - indicates if the time is valid or not + */ + +/** + * @typedef {Object} ISunTimeDefNextLast + * @property {ISunTimeDefRed} next - next sun time + * @property {ISunTimeDefRed} last - previous sun time + */ + +/** + * @typedef {Object} IMoonTime + * @property {Date|NaN} value - a Date object of the neesed date/time + * @property {string} [error] - string of an error message if an error occurs + */ + +/** + * @typedef {Object} ITypedValue + * @property {string} type - type of the value + * @property {*} value - value + */ + +/** + * @typedef {Object} IOffsetData + * @property {string} [offset] - value of the offset + * @property {string} [offsetType] - type name of the offset + * @property {function} [offsetCallback] - callback function for getting getPropValue + * @property {boolean} [noOffsetError] - true if no error should be given in GUI + * @property {number} [multiplier] - multiplier to the time + */ + +/** + * @typedef {Object} ITimePropertyTypeInt + * @property {string} [format] - format of the input + * @property {string} [days] - valid days + * @property {string} [months] - valid monthss + * @property {Date} [now] - base date, current time as default + * @property {number} [latitude] - base date, current time as default + * @property {number} [longitude] - base date, current time as default + * @property {*} [expr] - optional prepared Jsonata expression + * + * @typedef {ITimePropertyTypeInt & ILimitationsObj & ITypedValue & IOffsetData} ITimePropertyType + */ + +/** + * @typedef {Object} ITimePropertyResult + * @property {Date} value - the Date value + * @property {string} error - error message if an error has occured + * @property {boolean} fix - indicator if the given time value is a fix date + */ + +/** + * @typedef {Object} IValuePropertyTypeInt + * @property {*} [expr] - optional prepared Jsonata expression + * @property {function} [callback] - function which should be called after value was recived + * + * @typedef {ITypedValue & IValuePropertyTypeInt} IValuePropertyType + */ +/*******************************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { + 'use strict'; + + const hlp = require('./lib/dateTimeHelper.js'); + const util = require('util'); + const sunCalc = require('./lib/suncalc.js'); const dayMs = 86400000; // 1000 * 60 * 60 * 24; - /** generic configuration Node */ + /** generic configuration Node + * @extends runtimeNode + */ class positionConfigurationNode { /** * creates a new instance of the settings node and initializes them @@ -52,11 +284,15 @@ module.exports = function (RED) { try { this.name = config.name; this.valid = true; + // @ts-ignore this.latitude = parseFloat(Object.prototype.hasOwnProperty.call(this.credentials, 'posLatitude') ? this.credentials.posLatitude : config.latitude); + // @ts-ignore this.longitude = parseFloat(Object.prototype.hasOwnProperty.call(this.credentials, 'posLongitude') ? this.credentials.posLongitude : config.longitude); this.checkNode( error => { + // @ts-ignore this.error(error); + // @ts-ignore this.status({fill: 'red', shape: 'dot', text: error }); this.valid = false; }); @@ -87,6 +323,7 @@ module.exports = function (RED) { if (this.tzOffset !== 99) { this.tzOffset += this.tzDST; this.tzOffset = (this.tzOffset * -60); + // @ts-ignore this.debug('tzOffset is set to ' + this.tzOffset + ' tzDST=' + this.tzDST); } else { this.tzOffset = null; @@ -105,7 +342,9 @@ module.exports = function (RED) { this._moonTimesRefresh(today.valueOf(), dayId); hlp.initializeParser(RED._('common.days', { returnObjects: true}), RED._('common.months', { returnObjects: true}), RED._('common.dayDiffNames', { returnObjects: true})); } catch (err) { - this.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + // @ts-ignore + this.debug(util.inspect(err)); + // @ts-ignore this.status({ fill: 'red', shape: 'ring', @@ -117,15 +356,16 @@ module.exports = function (RED) { /** * register a node as child - * @param {*} node node to register as child node + * @param {*} srcnode node to register as child node */ register(srcnode) { + // @ts-ignore this.users[srcnode.id] = srcnode; } /** * remove a previous registered node as child - * @param {*} node node to remove + * @param {*} srcnode node to remove * @param {function} done function which should be executed after deregister * @returns {*} result of the function */ @@ -161,47 +401,19 @@ module.exports = function (RED) { return onOk; } /*******************************************************************************************************/ - /** - * @typedef {Object} limitationsObj - * @property {number} [next] if greater than 0 the number of days in the future - * @property {string} [days] days for which should be calculated the sun time - * @property {string} [months] months for which should be calculated the sun time - * @property {boolean} [onlyOddDays] - if true only odd days will be used - * @property {boolean} [onlyEvenDays] - if true only even days will be used - * @property {boolean} [onlyOddWeeks] - if true only odd weeks will be used - * @property {boolean} [onlyEvenWeeks] - if true only even weeks will be used - */ - // * @property {string} [meteoSeason] -only valid meteorological season - // * @property {string} [astroSeason] -only valid astronomical season - - /** - * @typedef {Object} timeresult - * @property {Date} value - a Date object of the neesed date/time - * @property {number} ts - The time as unix timestamp - * @property {number} pos - The position of the sun on the time - * @property {number} angle - Angle of the sun on the time - * @property {number} julian - The time as julian calendar - * @property {boolean} valid - indicates if the time is valid or not - */ - - /** - * @typedef {Object} erroresult - * @property {string} error - string of an error message if an error occurs - */ - /** * gets sun time by Name * @param {Date} dNow current time * @param {string} value name of the sun time * @param {number} [offset] the offset (positive or negative) which should be added to the date. If no multiplier is given, the offset must be in milliseconds. * @param {number} [multiplier] additional multiplier for the offset. Should be a positive Number. Special value -1 if offset is in month and -2 if offset is in years - * @param {limitationsObj} [limit] additional limitations for the calculation + * @param {ILimitationsObj} [limit] additional limitations for the calculation * @param {number} [latitude] latitude * @param {number} [longitude] longitude - * @return {timeresult|erroresult} result object of sunTime + * @return {ITimeResult} result object of sunTime */ - getSunTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude) { - // this.debug('getSunTimeByName dNow=' + dNow + ' limit=' + util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })); + _getSunTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude) { + // this.debug('_getSunTimeByName dNow=' + dNow + ' limit=' + util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })); let result; const dayId = hlp.getDayId(dNow); // this._getUTCDayId(dNow); if (latitude && longitude) { @@ -210,7 +422,7 @@ module.exports = function (RED) { latitude = this.latitude; longitude = this.longitude; this._sunTimesCheck(); // refresh if needed, get dayId - // this.debug(`getSunTimeByName value=${value} offset=${offset} multiplier=${multiplier} dNow=${dNow} dayId=${dayId} limit=${util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })}`); + // this.debug(`_getSunTimeByName value=${value} offset=${offset} multiplier=${multiplier} dNow=${dNow} dayId=${dayId} limit=${util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })}`); if (dayId === this.cache.sunTimesToday.dayId) { result = Object.assign({}, this.cache.sunTimesToday.times[value]); // needed for a object copy } else if (dayId === this.cache.sunTimesTomorrow.dayId) { @@ -259,19 +471,19 @@ module.exports = function (RED) { result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } - // this.debug('getSunTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); + // this.debug('_getSunTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); return result; } /** - * gets sun time by Name + * gets sun time by Elevation * @param {Date} dNow current time * @param {number} elevationAngle name of the sun time - * @param {timePropType} [tprop] additional limitations for the calculation + * @param {ITimePropertyType} [tprop] additional limitations for the calculation * @param {string} [prop=both] property (set, rise or both) to return - * @return {timeresult|erroresult} result object of sunTime + * @return {ISunTimeSingle} result object of sunTime */ - getSunTimeElevation(dNow, elevationAngle, degree, tprop, prop) { + _getSunTimeByElevation(dNow, elevationAngle, degree, tprop, prop) { if (!hlp.isValidDate(dNow)) { const dto = new Date(dNow); if (hlp.isValidDate(dto)) { @@ -280,7 +492,7 @@ module.exports = function (RED) { dNow = new Date(); } } - // this.debug('getSunTimeByName dNow=' + dNow + ' tprop=' + util.inspect(tprop, { colors: true, compact: 10, breakLength: Infinity })); + // this.debug('_getSunTimeByElevation dNow=' + dNow + ' tprop=' + util.inspect(tprop, { colors: true, compact: 10, breakLength: Infinity })); const latitude = (tprop.latitude || this.latitude); const longitude = (tprop.longitude || this.longitude); const result = Object.assign({},sunCalc.getSunTime(dNow.valueOf(), latitude, longitude, elevationAngle, degree)); @@ -311,10 +523,10 @@ module.exports = function (RED) { }; if (prop==='rise') { calc(result.rise, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, degree).rise); - return result.rise; + return result; } else if (prop==='set') { calc(result.set, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, degree).set); - return result.set; + return result; } calc(result.rise, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, degree).rise); calc(result.set, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, degree).set); @@ -322,15 +534,15 @@ module.exports = function (RED) { } /** - * gets sun time by Name + * gets sun time by Azimuth * @param {Date} dNow current time * @param {number} azimuthAngle angle of the sun - * @param {timePropType} [tprop] additional limitations for the calculation + * @param {ITimePropertyType} [tprop] additional limitations for the calculation * @param {number} [latitude] latitude * @param {number} [longitude] longitude - * @return {timeresult|erroresult} result object of sunTime + * @return {Date} result object of sunTime */ - getSunTimeAzimuth(dNow, azimuthAngle, degree, tprop, latitude, longitude) { + _getSunTimeByAzimuth(dNow, azimuthAngle, degree, tprop, latitude, longitude) { if (!hlp.isValidDate(dNow)) { const dto = new Date(dNow); if (hlp.isValidDate(dto)) { @@ -339,7 +551,7 @@ module.exports = function (RED) { dNow = new Date(); } } - // this.debug('getSunTimeByName dNow=' + dNow + ' tprop=' + util.inspect(tprop, { colors: true, compact: 10, breakLength: Infinity })); + // this.debug('_getSunTimeByAzimuth dNow=' + dNow + ' tprop=' + util.inspect(tprop, { colors: true, compact: 10, breakLength: Infinity })); latitude = (latitude || tprop.latitude || this.latitude); longitude = (longitude || tprop.longitude || this.longitude); let result = sunCalc.getSunTimeByAzimuth(dNow, latitude, longitude, azimuthAngle, degree); @@ -356,7 +568,7 @@ module.exports = function (RED) { } const r = hlp.limitDate(tprop, result); if (r.error) { - result = NaN; + result = null; } else { result = r.date; } @@ -373,13 +585,13 @@ module.exports = function (RED) { /** * gets previous and next sun time * @param {Date} dNow current time - * @return {array} result object of sunTime + * @return {ISunTimeDefNextLast} result object of sunTime */ - getSunTimePrevNext(dNow) { + _getSunTimePrevNext(dNow) { let dayId = hlp.getDayId(dNow); // this._getUTCDayId(dNow); this._sunTimesCheck(); // refresh if needed, get dayId let result; - // this.debug(`getSunTimePrevNext dNow=${dNow} dayId=${dayId} today=${util.inspect(today, { colors: true, compact: 10, breakLength: Infinity })}`); + // this.debug(`_getSunTimePrevNext dNow=${dNow} dayId=${dayId} today=${util.inspect(today, { colors: true, compact: 10, breakLength: Infinity })}`); if (dayId === this.cache.sunTimesToday.dayId) { result = this.cache.sunTimesToday.times; } else if (dayId === this.cache.sunTimesTomorrow.dayId) { @@ -389,6 +601,7 @@ module.exports = function (RED) { } else if (dayId === this.cache.sunTimesAdd2.dayId) { result = this.cache.sunTimesAdd2.times; } else { + // @ts-ignore this.debug('sun-time not in cache - calc time (2)'); this.cache.sunTimesAdd2 = { dayId: this.cache.sunTimesAdd1.dayId, @@ -410,7 +623,7 @@ module.exports = function (RED) { return a.ts - b.ts; }); const dNowTs = dNow.getTime() + 300; // offset to get really next - // this.debug(`getSunTimePrevNext dNowTs=${dNowTs} sortable=${util.inspect(sortable, { colors: true, compact: 10, breakLength: Infinity })}`); + // this.debug(`_getSunTimePrevNext dNowTs=${dNowTs} sortable=${util.inspect(sortable, { colors: true, compact: 10, breakLength: Infinity })}`); let last = sortable[0]; if (last.ts >= dNowTs) { @@ -492,21 +705,18 @@ module.exports = function (RED) { }; } /*******************************************************************************************************/ - /** - * @typedef {Object} moontime - * @property {Date|NaN} value - a Date object of the neesed date/time - */ - /** * gets moon time * @param {Date} dNow current time * @param {string} value name of the moon time * @param {number} [offset] the offset (positive or negative) which should be added to the date. If no multiplier is given, the offset must be in milliseconds. * @param {number} [multiplier] additional multiplier for the offset. Should be a positive Number. Special value -1 if offset is in month and -2 if offset is in years - * @param {limitationsObj} [limit] additional limitations for the calculation - * @return {moontime|erroresult} result object of moon time + * @param {ILimitationsObj} [limit] additional limitations for the calculation + * @param {number} [latitude] optional latitude angle + * @param {number} [longitude] optional longitude angle + * @return {IMoonTime} result object of moon time */ - getMoonTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude) { + _getMoonTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude) { const result = {}; const dateBase = new Date(dNow); const dayId = hlp.getDayId(dNow); // this._getUTCDayId(dNow); @@ -517,7 +727,7 @@ module.exports = function (RED) { longitude = this.longitude; this._moonTimesCheck(); // refresh if needed, get dayId - // this.debug(`getMoonTimeByName value=${value} offset=${offset} multiplier=${multiplier} dNow=${dNow} dayId=${dayId} limit=${util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })}`); + // this.debug(`_getMoonTimeByName value=${value} offset=${offset} multiplier=${multiplier} dNow=${dNow} dayId=${dayId} limit=${util.inspect(limit, { colors: true, compact: 10, breakLength: Infinity })}`); if (dayId === this.cache.moonTimesToday.dayId) { result.value = this.cache.moonTimesToday.times[value]; // needed for a object copy @@ -565,7 +775,7 @@ module.exports = function (RED) { result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } - // this.debug('getMoonTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); + // this.debug('_getMoonTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); return result; } /*******************************************************************************************************/ @@ -605,10 +815,11 @@ module.exports = function (RED) { /** * get the random number cache for a node * @param {*} _srcNode - source node information - * @param {Date} [dNow] base Date to use for Date time functions + * @param {Date} dNow base Date to use for Date time functions + * @param {string} store used context store * @returns {*} random number cache */ - getNodeNumberCache(_srcNode, dNow, store) { + _getNodeNumberCache(_srcNode, dNow, store) { const cache = _srcNode.context().get('randomNumberCache', store) || {}; if (!cache.day || cache.day.v !== dNow.getDate()) { cache.day = { @@ -631,8 +842,8 @@ module.exports = function (RED) { * @param {Date} [dNow] base Date to use for Date time functions * @returns {number} random number */ - getCachedRandomDayNumber(_srcNode, limit1, limit2, dNow) { - // _srcNode.debug(`getCachedRandomDayNumber limit1=${String(limit1)} limit2=${String(limit2)} dNow=${dNow}`); + _getCachedRandomDayNumber(_srcNode, limit1, limit2, dNow) { + // _srcNode.debug(`_getCachedRandomDayNumber limit1=${String(limit1)} limit2=${String(limit2)} dNow=${dNow}`); if (isNaN(limit1)) { _srcNode.error(`the value for random number limit is wrong limit1=${String(limit1)} limit2=${String(limit2)}, using 60`); limit1 = 60; @@ -641,7 +852,7 @@ module.exports = function (RED) { const high = Math.max(limit1, isNaN(limit2) ? 0 : limit2); const name = 'min_'+low+'_max_'+high; const store = _srcNode.contextStore || this.contextStore; - const cache = this.getNodeNumberCache(_srcNode, dNow, store); + const cache = this._getNodeNumberCache(_srcNode, dNow, store); if (isNaN(cache.day[name])) { cache.day[name] = low + (Math.random() * (high - low)); _srcNode.context().set('randomNumberCache', cache, store); @@ -656,7 +867,7 @@ module.exports = function (RED) { * @param {Date} [dNow] base Date to use for Date time functions * @returns {number} random number */ - getCachedRandomWeekNumber(_srcNode, limit1, limit2, dNow) { + _getCachedRandomWeekNumber(_srcNode, limit1, limit2, dNow) { // _srcNode.debug(`getCachedRandomWeekNumber limit1=${limit1} limit2=${limit2} dNow=${dNow}`); if (isNaN(limit1)) { _srcNode.error(`the value for random number limit is wrong limit1=${limit1} limit2=${limit2}, using 60`); @@ -666,7 +877,7 @@ module.exports = function (RED) { const high = Math.max(limit1, isNaN(limit2) ? 0 : limit2); const name = 'min_'+low+'_max_'+high; const store = _srcNode.contextStore || this.contextStore; - const cache = this.getNodeNumberCache(_srcNode, dNow, store); + const cache = this._getNodeNumberCache(_srcNode, dNow, store); if (isNaN(cache.week[name])) { cache.week[name] = low + (Math.random() * (high - low)); _srcNode.context().set('randomNumberCache', cache, store); @@ -681,7 +892,7 @@ module.exports = function (RED) { * @param {*} value - value of the type input * @param {*} [def] - default value if can not get float value * @param {*} [callback] - callback function for getting getPropValue - * @param {boolean} [noError] - true if no error shoudl be given in GUI + * @param {boolean} [noError] - true if no error should be given in GUI * @param {Date} [dNow] base Date to use for Date time functions * @returns {number} float property */ @@ -716,24 +927,12 @@ module.exports = function (RED) { return data; } /*******************************************************************************************************/ - /** - * @typedef {Object} outPropType - * @property {string} type - type name of the type input - * @property {string} value - value of the type input - * @property {string|number} format - format of the input - * @property {string} [offset] - value of the offset type input - * @property {string} [offsetType] - type name of the offset type input - * @property {number} [multiplier] - multiplier to the time - * @property {boolean} [next] - if __true__ the next date will be delivered starting from now, otherwise the matching date of the date from now - * @property {string} [days] - valid days - */ - /** * get an formated date prepared for output * @param {*} _srcNode - source node for logging - * @param {*} [msg] - the message object + * @param {*} msg - the message object * @param {Date} dateValue - the source date object which should be formated - * @param {outPropType} data - additional formating and control data + * @param {ITimePropertyType} data - additional formating and control data * @param {Date} [dNow] base Date to use for Date time functions */ formatOutDate(_srcNode, msg, dateValue, data, dNow) { @@ -745,11 +944,10 @@ module.exports = function (RED) { /** * get the time Data prepared for output * @param {*} _srcNode - source node for logging - * @param {*} [msg] - the message object - * @param {outPropType} data - a Data object + * @param {*} msg - the message object + * @param {ITimePropertyType} data - a Data object * @param {Date} [dNow] base Date to use for Date time functions * @param {boolean} [noError] - true if no error shoudl be given in GUI - * @param {Object} [replaceObject] an object if in the result parameters should be replaced * @returns {*} output Data */ getOutDataProp(_srcNode, msg, data, dNow, noError) { @@ -761,7 +959,7 @@ module.exports = function (RED) { return null; } else if (data.type === 'date') { if (this.tzOffset) { - return hlp.convertDateTimeZone(Date.now(), this.tzOffset); + return hlp.convertDateTimeZone(dNow, this.tzOffset); } return Date.now(); } else if (data.type === 'dateSpecific') { @@ -769,23 +967,25 @@ module.exports = function (RED) { } else if ((data.type === 'pdsTime') || (data.type === 'pdmTime')) { if (data.type === 'pdsTime') { // sun const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); - result = this.getSunTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); + result = this._getSunTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); } else if (data.type === 'pdmTime') { // moon const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); - result = this.getMoonTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); - _srcNode.debug(`getMoonTimeByName data=${util.inspect(result, { colors: true, compact: 10, breakLength: Infinity }) }`); + result = this._getMoonTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); + _srcNode.debug(`_getMoonTimeByName data=${util.inspect(result, { colors: true, compact: 10, breakLength: Infinity }) }`); } if (result && result.value && !result.error) { return hlp.getFormattedDateOut(result.value, data.format, (this.tzOffset === 0), this.tzOffset); } return null; } else if (data.type === 'pdsTimeNow') { - result = Object.assign({}, this.getSunTimePrevNext(dNow)); + result = Object.assign({}, this._getSunTimePrevNext(dNow)); const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); result.last.value = hlp.normalizeDate(result.last.value, offsetX, data.multiplier, data); result.next.value = hlp.normalizeDate(result.next.value, offsetX, data.multiplier, data); if (this.tzOffset) { + // @ts-ignore result.last.value = hlp.convertDateTimeZone(result.last.value, this.tzOffset).getTime(); + // @ts-ignore result.next.value = hlp.convertDateTimeZone(result.next.value, this.tzOffset).getTime(); } return result; @@ -833,40 +1033,17 @@ module.exports = function (RED) { } } /*******************************************************************************************************/ - /** - * @typedef {Object} timePropType - * @property {string} type - type name of the type input - * @property {string} value - value of the type input - * @property {string|number} [format] - format of the input - * @property {string} [offset] - value of the offset type input - * @property {string} [offsetType] - type name of the offset type input - * @property {number} [multiplier] - multiplier to the time - * @property {boolean} [next] - if __true__ the next date will be delivered starting from now, otherwise the matching date of the date from now - * @property {string} [days] - valid days - * @property {string} [months] - valid monthss - * @property {Date} [now] - base date, current time as default - * @property {number} [latitude] - base date, current time as default - * @property {number} [longitude] - base date, current time as default - */ - - /** - * @typedef {Object} timePropResultType - * @property {Date} value - the Date value - * @property {string} error - error message if an error has occured - * @property {boolean} fix - indicator if the given time value is a fix date - */ - /** * get the time Data from a typed input * @param {*} _srcNode - source node for logging - * @param {*} [msg] - the message object - * @param {timePropType} data - a Data object + * @param {*} msg - the message object + * @param {ITimePropertyType} data - a Data object * @param {*} [dNow] base Date to use for Date time functions - * @returns {timePropResultType} value of the type input + * @returns {ITimePropertyResult} value of the type input */ getTimeProp(_srcNode, msg, data, dNow) { // _srcNode.debug(`getTimeProp data=${util.inspect(data, { colors: true, compact: 10, breakLength: Infinity })} tzOffset=${this.tzOffset}`); - let result = { + const result = { value: null, error: null, fix: true @@ -888,7 +1065,7 @@ module.exports = function (RED) { const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); result.value = hlp.normalizeDate(dNow, offsetX, data.multiplier, data); if (this.tzOffset) { - result.value = hlp.convertDateTimeZone(result.value); + result.value = hlp.convertDateTimeZone(result.value, this.tzOffset); } result.fix = true; } else if (data.type === 'dayOfMonth') { @@ -896,7 +1073,7 @@ module.exports = function (RED) { const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); result.value = hlp.normalizeDate(result.value, offsetX, data.multiplier, data); if (this.tzOffset) { - result.value = hlp.convertDateTimeZone(result.value); + result.value = hlp.convertDateTimeZone(result.value, this.tzOffset); } } else if (data.type === 'entered') { result.value = hlp.getTimeOfText(String(data.value), dNow, (this.tzOffset === 0), this.tzOffset); @@ -915,7 +1092,7 @@ module.exports = function (RED) { } else if (data.type === 'pdsTime') { // sun const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); - result = this.getSunTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); + Object.assign(result, this._getSunTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude)); if (this.tzOffset) { result.value = hlp.convertDateTimeZone(result.value, this.tzOffset); } @@ -923,16 +1100,16 @@ module.exports = function (RED) { } else if (data.type === 'pdmTime') { // moon const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); - result = this.getMoonTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude); + Object.assign(result, this._getMoonTimeByName(dNow, data.value, offsetX, data.multiplier, data, data.latitude, data.longitude)); if (this.tzOffset) { result.value = hlp.convertDateTimeZone(result.value, this.tzOffset); } result.fix = true; } else if (data.type === 'pdsTimeNow') { - result = this.getSunTimePrevNext(dNow).next; + Object.assign(result, this._getSunTimePrevNext(dNow).next); result.fix = true; const offsetX = this.getFloatProp(_srcNode, msg, data.offsetType, data.offset, 0, data.offsetCallback, data.noOffsetError, dNow); - result.value = hlp.addOffset(new Date(result.value), offsetX, data.multiplier, data.next); + result.value = hlp.addOffset(new Date(result.value), offsetX, data.multiplier); } else if (data.type === 'pdsTimeByAzimuth' || data.type === 'pdsTimeByAzimuthRad') { result.value = this.getPropValue(_srcNode, msg, data, true, dNow); @@ -969,8 +1146,9 @@ module.exports = function (RED) { } } } catch (err) { - _srcNode.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + _srcNode.debug(util.inspect(err)); const e = new Error(RED._('errors.notEvaluablePropertyAdd', {type:data.type, value: data.value, err:err.message})); + // @ts-ignore e.original = err; e.stack = e.stack.split('\n').slice(0,2).join('\n')+'\n'+err.stack; throw e; @@ -986,22 +1164,14 @@ module.exports = function (RED) { } /*******************************************************************************************************/ /** - * @typedef {Object} propValueType - * @property {string} type - type name of the type input - * @property {string} value - value of the type input - * @property {string} [expr] - optional prepared Jsonata expression - * @property {function} [callback] - function which should be called after value was recived - */ - - /** - * get a property value from a type input in Node-Red + * get a prepared JSONATA Expression * @param {*} _srcNode - source node information - * @param {propValueType} data - data object with more information - * @returns {*} JSONataExpression + * @param {string} value - get an expression for a value + * @returns {function} JSONataExpression */ - getJSONataExpression(_srcNode, data) { + getJSONataExpression(_srcNode, value) { // _srcNode.debug(`getJSONataExpression - data= ${util.inspect(data, { colors: true, compact: 10, breakLength: Infinity })}`); - const expr = RED.util.prepareJSONataExpression(data, _srcNode); + const expr = RED.util.prepareJSONataExpression(value, _srcNode); // expr.assign('sunTimes', function (val, store) { // return node.context().global.get(val, store); // }); @@ -1054,7 +1224,7 @@ module.exports = function (RED) { return hlp.isDSTObserved(new Date(date)); }, '<(osn)?:b>'); expr.registerFunction('addOffsetToDate', (date, offset, multiplier) => { - return hlp.addOffsetToDate(new Date(date), offset, multiplier); + return hlp.addOffset(new Date(date), offset, multiplier); }, '<(osn)?nn:b>'); expr.registerFunction('getFormattedDateOut', (date, format, utc, timeZoneOffset) => { return hlp.getFormattedDateOut(date, format, utc, timeZoneOffset); @@ -1089,7 +1259,7 @@ module.exports = function (RED) { onlyOddWeeks, onlyEvenWeeks }; - return this.getSunTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude).value; + return this._getSunTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude).value; }, ''); expr.registerFunction('getSunTimePrevNext', dNow => { if (!hlp.isValidDate(dNow)) { @@ -1100,7 +1270,7 @@ module.exports = function (RED) { dNow = new Date(); } } - return this.getSunTimePrevNext(dNow); + return this._getSunTimePrevNext(dNow); }, '<(osn)?:(ol)>'); expr.registerFunction('getSunTimeByElevationNext', (elevation, dNow) => { if (!hlp.isValidDate(dNow)) { @@ -1111,7 +1281,7 @@ module.exports = function (RED) { dNow = new Date(); } } - return this.getSunTime(dNow, parseFloat(elevation), {}); + return this._getSunTimeByElevation(dNow, parseFloat(elevation), {}); }, ''); expr.registerFunction('getMoonTimeByName', (value, offset, multiplier, dNow) => { if (!hlp.isValidDate(dNow)) { @@ -1122,22 +1292,22 @@ module.exports = function (RED) { dNow = new Date(); } } - return this.getMoonTimeByName(dNow, value, offset, multiplier).value; + return this._getMoonTimeByName(dNow, value, offset, multiplier).value; }, ''); expr.registerFunction('getSunCalc', (date, calcTimes, sunInSky) => { return this.getSunCalc(date, calcTimes, sunInSky); }, '<(osn)?b?b?:(ol)>'); expr.registerFunction('getSunInSky', date => { - return this.getSunInSky(date); + return this._getSunInSky(date); }, '<(osn)?:n>'); expr.registerFunction('getMoonCalc', (date, calcTimes, moonInSky) => { return this.getMoonCalc(new Date(date), calcTimes, moonInSky); }, '<(osn)?b?:(ol)>'); expr.registerFunction('getMoonIllumination', date => { - return this.getMoonIllumination(date); + return this._getMoonIllumination(date); }, '<(osn)?:(ol)>'); expr.registerFunction('getMoonPhase', date => { - return this.getMoonPhase(date); + return this._getMoonIllumination(date).phase; }, '<(osn)?:(ol)>'); return expr; } @@ -1146,7 +1316,7 @@ module.exports = function (RED) { * get a property value from a type input in Node-Red * @param {*} _srcNode - source node information * @param {*} msg - message object - * @param {propValueType} data - data object with more information + * @param {IValuePropertyType} data - data object with more information * @param {boolean} [noError] - true if no error shoudl be given in GUI * @param {Date} [dNow] base Date to use for Date time functions * @returns {*} value of the type input, return of the callback function if defined or __null__ if value could not resolved @@ -1166,7 +1336,7 @@ module.exports = function (RED) { } else if (data.type === 'numAzimuth' || data.type === 'numAltitude') { result = hlp.angleNorm(Number(data.value)); } else if (data.type === 'numAzimuthRad' || data.type === 'numAltitudeRad') { - data = hlp.angleNormRad(Number(data.value)); + result = hlp.angleNormRad(Number(data.value)); } else if (data.type === 'str') { result = ''+data.value; } else if (data.type === 'strPlaceholder') { @@ -1201,11 +1371,11 @@ module.exports = function (RED) { return _srcNode._path || _srcNode.id; // if empty fallback to node ID } else if (data.type === 'randmNumCachedDay') { const val = data.value.split(/((?:[1-9]|-0\.|0\.|-)\d*(?:\.\d+)?)/); - return this.getCachedRandomDayNumber(_srcNode, parseFloat(val[1]), parseFloat(val[3]), dNow); + return this._getCachedRandomDayNumber(_srcNode, parseFloat(val[1]), parseFloat(val[3]), dNow); // return (isNaN(rv) ? 0 : rv); } else if (data.type === 'randmNumCachedWeek') { const val = data.value.split(/((?:[1-9]|-0\.|0\.|-)\d*(?:\.\d+)?)/); - return this.getCachedRandomWeekNumber(_srcNode, parseFloat(val[1]), parseFloat(val[3]), dNow); + return this._getCachedRandomWeekNumber(_srcNode, parseFloat(val[1]), parseFloat(val[3]), dNow); // return (isNaN(rv) ? 0 : rv); } else if (data.type === 'randomNum') { const val = data.value.split(/((?:[1-9]|-0\.|0\.|-)\d*(?:\.\d+)?)/); @@ -1227,7 +1397,7 @@ module.exports = function (RED) { } else if (data.type === 'pdsCalcData') { result = this.getSunCalc(dNow, true, false); } else if (data.type === 'pdsCalcPercent') { - result = this.getSunInSky(dNow); + result = this._getSunInSky(dNow); } else if (data.type === 'pdsCalcAzimuth') { result = this.getSunCalc(dNow, false, false).azimuthDegrees; } else if (data.type === 'pdsCalcElevation') { @@ -1237,40 +1407,40 @@ module.exports = function (RED) { } else if (data.type === 'pdsCalcElevationRad') { result = this.getSunCalc(dNow, false, false).altitudeRadians; } else if (data.type === 'pdsTimeByElevation') { // gives an object back - result = this.getSunTimeElevation(dNow, parseFloat(data.value), true, data); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), true, data); } else if (data.type === 'pdsTimeByElevationRad') { // gives an object back - result = this.getSunTimeElevation(dNow, parseFloat(data.value), false, data); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), false, data); } else if (data.type === 'pdsTimeByAzimuth') { - result = this.getSunTimeAzimuth(dNow, parseFloat(data.value), true, data); + result = this._getSunTimeByAzimuth(dNow, parseFloat(data.value), true, data); } else if (data.type === 'pdsTimeByAzimuthRad') { - result = this.getSunTimeAzimuth(dNow, parseFloat(data.value), false, data); + result = this._getSunTimeByAzimuth(dNow, parseFloat(data.value), false, data); } else if (data.type === 'pdsTimeByElevationNext') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), true, data); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), true, data); if (result.set.value.getTime() < result.rise.value.getTime()) { return result.set; } return result.rise; } else if (data.type === 'pdsTimeByElevationNextRad') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), false, data); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), false, data); if (result.set.value.getTime() < result.rise.value.getTime()) { return result.set; } return result.rise; } else if (data.type === 'pdsTimeByElevationRise') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), true, data, 'rise'); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), true, data, 'rise').rise; } else if (data.type === 'pdsTimeByElevationRiseRad') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), false, data, 'rise'); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), false, data, 'rise').rise; } else if (data.type === 'pdsTimeByElevationSet') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), true, data, 'set'); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), true, data, 'set').set; } else if (data.type === 'pdsTimeByElevationSetRad') { - result = this.getSunTimeElevation(dNow, parseFloat(data.value), false, data, 'set'); + result = this._getSunTimeByElevation(dNow, parseFloat(data.value), false, data, 'set').set; } else if (data.type === 'pdmCalcData') { result = this.getMoonCalc(dNow, true, false); } else if (data.type === 'pdmPhase') { - result = this.getMoonPhase(dNow); + result = this._getMoonIllumination(dNow).phase; } else if (data.type === 'pdmPhaseCheck') { - const phase = this.getMoonPhase(dNow); - result = (phase === data.value); + const phase = this._getMoonIllumination(dNow).phase; + result = (phase.id === data.value); } else if (data.type === 'entered' || data.type === 'dateEntered') { result = hlp.getDateOfText(String(data.value), true, (this.tzOffset === 0), this.tzOffset); } else if (data.type === 'pdbIsDST') { @@ -1290,13 +1460,13 @@ module.exports = function (RED) { } result = RED.util.evaluateJSONataExpression(data.expr, msg); } catch (err) { - _srcNode.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + _srcNode.debug(util.inspect(err)); } } else { try { result = RED.util.evaluateNodeProperty(data.value, data.type, _srcNode, msg); } catch (err) { - _srcNode.info(util.inspect(err, Object.getOwnPropertyNames(err))); + _srcNode.info(util.inspect(err)); } } if (typeof data.callback === 'function') { @@ -1313,12 +1483,12 @@ module.exports = function (RED) { } /*******************************************************************************************************/ /** - * get a property value from a type input in Node-Red + * compared two property's * @param {*} _srcNode - source node information * @param {*} msg - message object - * @property {propValueType} operandA - first operand + * @property {IValuePropertyType} operandA - first operand * @property {string} compare - compare between the both operands - * @property {propValueType} operandB - second operand + * @property {IValuePropertyType} operandB - second operand * @param {boolean} [noError] - true if no error shoudl be given in GUI * @param {Date} [dNow] base Date to use for Date time functions * @returns {*} value of the type input, return of the callback function if defined or __null__ if value could not resolved @@ -1416,6 +1586,7 @@ module.exports = function (RED) { const cacheEnabled = isNaN(specLatitude) && isNaN(specLongitude); if (cacheEnabled && (Math.abs(date.getTime() - this.cache.lastSunCalc.ts) < 3000)) { + // @ts-ignore this.log('getSunCalc, time difference since last output to low, do no calculation'); return this.cache.lastSunCalc; } @@ -1482,12 +1653,12 @@ module.exports = function (RED) { } /**************************************************************************************************************/ - getSunInSky(date) { + _getSunInSky(date) { const result = this.getSunCalc(date, true, true); return result.altitudePercent; } /**************************************************************************************************************/ - getMoonIllumination(date) { + _getMoonIllumination(date) { if (!hlp.isValidDate(date)) { const dto = new Date(date); if (hlp.isValidDate(dto)) { @@ -1515,10 +1686,6 @@ module.exports = function (RED) { return result; } - getMoonPhase(date) { - return this.getMoonIllumination(date, false).phase; - } - getMoonCalc(date, calcTimes, moonInSky, specLatitude, specLongitude) { if (!hlp.isValidDate(date)) { const dto = new Date(date); @@ -1531,13 +1698,14 @@ module.exports = function (RED) { const cacheEnabled = isNaN(specLatitude) && isNaN(specLongitude); if (cacheEnabled && (Math.abs(date.getTime() - this.cache.lastMoonCalc.ts) < 3000)) { + // @ts-ignore this.log('getMoonCalc, time difference since last output to low, do no calculation'); return this.cache.lastMoonCalc; } specLatitude = specLatitude || this.latitude; specLongitude = specLongitude || this.longitude; - const moonPos = sunCalc.getMoonPosition(date.valueOf(), specLatitude, specLongitude); + const moonData = sunCalc.getMoonData(date.valueOf(), specLatitude, specLongitude); const result = { ts: date.getTime(), @@ -1551,17 +1719,17 @@ module.exports = function (RED) { latitude: specLatitude, longitude: specLongitude, angleType: this.angleType, - azimuth: (this.angleType === 'deg') ? moonPos.azimuthDegrees : moonPos.azimuth, - altitude: (this.angleType === 'deg') ? moonPos.altitudeDegrees : moonPos.altitude, // elevation = altitude - altitudeDegrees: moonPos.altitudeDegrees, - azimuthDegrees: moonPos.azimuthDegrees, - altitudeRadians: moonPos.altitude, - azimuthRadians: moonPos.azimuth, - distance: moonPos.distance, - parallacticAngle: (this.angleType === 'deg') ? moonPos.parallacticAngleDegrees : moonPos.parallacticAngle, - illumination: this.getMoonIllumination(date.valueOf()) + azimuth: (this.angleType === 'deg') ? moonData.azimuthDegrees : moonData.azimuth, + altitude: (this.angleType === 'deg') ? moonData.altitudeDegrees : moonData.altitude, // elevation = altitude + altitudeDegrees: moonData.altitudeDegrees, + azimuthDegrees: moonData.azimuthDegrees, + altitudeRadians: moonData.altitude, + azimuthRadians: moonData.azimuth, + distance: moonData.distance, + parallacticAngle: (this.angleType === 'deg') ? moonData.parallacticAngleDegrees : moonData.parallacticAngle, + illumination: moonData.illumination, + zenithAngle: (this.angleType === 'deg') ? 180 / Math.PI * moonData.zenithAngle : moonData.zenithAngle }; - result.illumination.zenithAngle = (this.angleType === 'deg') ? 180 / Math.PI * (result.illumination.angle - moonPos.parallacticAngle) : result.illumination.angle - moonPos.parallacticAngle; if (!calcTimes) { return result; } @@ -1723,11 +1891,11 @@ module.exports = function (RED) { } catch(err) { console.log('RED.httpAdmin.get data ERR'); // eslint-disable-line no-console console.log(err); // eslint-disable-line no-console - obj.value = NaN; + delete obj.value; obj.useful = false; obj.error = err.message; obj.errorStack= err.stack; - scrNode.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + scrNode.debug(util.inspect(err)); scrNode.error(err.message); res.status(200).send(JSON.stringify(obj, Object.getOwnPropertyNames(obj))); } diff --git a/nodes/20-time-inject.js b/nodes/20-time-inject.js index 64e9ef3..55de558 100644 --- a/nodes/20-time-inject.js +++ b/nodes/20-time-inject.js @@ -24,24 +24,125 @@ * time-inject: *********************************************/ 'use strict'; -module.exports = function (RED) { +/** --- 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").IOffsetData} IOffsetData + */ + +/** + * @typedef {Object} ITIPropertyTypeInt + * @property {string} outType - valid days + * @property {*} outValue - valid days + * @property {string} outType - valid days + * @property {string} [expr] - optional prepared Jsonata expression + * + * @typedef {ILimitationsObj & ITypedValue & IOffsetData & ITIPropertyTypeInt} ITIPropertyType + */ + +/** + * @typedef {Object} ITimeInjectNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * @property {string} addId internal used additional id + * + * @property {number} injType type of the inject node + * @property {number} intervalCount - count of the interval + * @property {string} intervalCountType - ?? + * @property {number} intervalCountMultiplier - ?? + * @property {Date} intervalStart - ?? + * @property {number} intervalAmount - ?? + * @property {number} intervalCountCurrent - ?? + * @property {number} intervalCountMax - ?? + * @property {string} intervalText - the text of the interval + * + * @property {ITimePropertyType} timeStartData - ?? + * @property {IValuePropertyType} property - ?? + * @property {ITypedValue} propertyThreshold - ?? + * @property {string} propertyOperator - ?? + * @property {ITimePropertyType} timeStartAltData - ?? + * @property {ITimePropertyType} timeEndData - ?? + * @property {*} cronJobObj - ?? + * @property {string} cronExpr - ?? + * + * @property {Array.} props - output data + * + * @property {number} recalcTime - ?? + * + * @property {NodeJS.Timeout} timeOutStartObj - ?? + * @property {NodeJS.Timeout} timeOutEndObj - ?? + * @property {NodeJS.Timer} intervalObj - ?? + * @property {NodeJS.Timeout} onceTimeOutObj - ?? + * + * @property {number} intervalTime - ?? + * @property {Date} nextStartTime - ?? + * @property {Date} nextStartTimeAlt - ?? + * @property {Date} nextEndTime - ?? + * @property {number} onceDelay - ?? + * @property {Date} timedatestart - ?? + * @property {Date} timedateend - ?? + * @property {boolean} isAltAvailable - ?? + * @property {boolean} isAltFirst - ?? + * + * + * @property {number} cacheYear - ?? + * @property {Date} cacheStart - ?? + * @property {Date} cacheEnd - ?? + * + * @property {function} getTimeLimitation - get the limitation for time + * @property {function} initializeStartTimer - initializes the start timer + * @property {function} initialize - initializes the node itself + * @property {IGetTimeAsMillisecond} getMillisecEnd - get the end time in millisecond + * @property {function} doCreateStartTimeout - creates the start timeout + * @property {function} doCreateEndTimeout - creates the end timeout + * @property {function} doCreateCRONSetup - creates the CRON interval + * @property {function} doRecalcStartTimeOut - Recalculate the Start timeout + * @property {function} doStartInterval - start an Intervall + * @property {function} getIntervalText - creates the text for an interval + * @property {function} createNextInterval - Recalculate the Interval + * @property {function} prepOutMsg - Prepares a message object for sending + * @property {function} getIntervalTime - get and validate a given interval + * @property {function} doSetStatus - get and validate a given interval + * ... obviously there are more ... + */ + +/** + * Description of the function + * @typedef {function} IGetTimeAsMillisecond + * @param {ITIPropertyType} node the node Data + * @return {number} the time in millisecond +*/ + +/** + * @typedef {ITimeInjectNodeInstance & runtimeNode} ITimeInjectNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const util = require('util'); - const path = require('path'); const {scheduleTask} = require('cronosjs'); - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + const hlp = require('./lib/dateTimeHelper.js'); + /******************************************************************************************/ /** - * timeInjectNode - * @param {*} config - configuration + * standard Node-Red Node handler for the timeInjectNode + * @param {*} config the Node-Red Configuration property of the Node */ function timeInjectNode(config) { /** * get the output properies * @param {object} props - the property array - * @returns {object} the nw property object + * @returns {Array.} the nw property object */ function prepareProps(node, props) { // node.debug('prepareProps ' + util.inspect(props, { colors: true, compact: 10, breakLength: Infinity })); @@ -95,8 +196,14 @@ module.exports = function (RED) { cron : 6 }; const intervalMax = 24*60*60*1000 * 3; // 3 Tage + + RED.nodes.createNode(this, config); + + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {ITimeInjectNode} + */ + // @ts-ignore const node = this; - RED.nodes.createNode(node, config); // Retrieve the config node node.positionConfig = RED.nodes.getNode(config.positionConfig); if (!node.positionConfig) { @@ -162,6 +269,7 @@ module.exports = function (RED) { }; if (!node.timeStartData.offsetType) { + // @ts-ignore node.timeStartData.offsetType = ((node.timeStartData.offset === 0) ? 'none' : 'num'); } if (node.timeStartData.days === '') { @@ -217,6 +325,7 @@ module.exports = function (RED) { onlyEvenWeeks: hlp.isTrue(config.timeAltOnlyEvenWeeks), onlyOddWeeks : hlp.isTrue(config.timeAltOnlyOddWeeks) }; + // @ts-ignore if (!node.timeStartAltData.offsetType) { node.timeStartAltData.offsetType = ((node.timeStartAltData.offset === 0) ? 'none' : 'num'); } if (node.timeStartAltData.days === '') { @@ -252,6 +361,7 @@ module.exports = function (RED) { onlyOddWeeks : hlp.isTrue(config.timeOnlyOddWeeks) }; if (!node.timeEndData.offsetType) { + // @ts-ignore node.timeEndData.offsetType = ((node.timeEndData.offset === 0) ? 'none' : 'num'); } if (node.timeEndData.days === '') { @@ -269,7 +379,7 @@ module.exports = function (RED) { node.timeEndData.onlyOddWeeks = false; } } // timeEndData - node.cronjob = null; + node.cronJobObj = null; if (node.injType === tInj.cron) { node.cronExpr = config.cron || ''; } // cron @@ -436,6 +546,7 @@ module.exports = function (RED) { /** * get the limitation for time + * @param {Date} dNow base Date */ node.getTimeLimitation = dNow => { const result = { @@ -483,7 +594,10 @@ module.exports = function (RED) { return result; }; - node.initializeStartTimer = node => { + /** + * initializes the start timer + */ + node.initializeStartTimer = () => { // node.debug(`initializeStartTimer`); if (!node.timeStartData) { // node.debug('initializeStartTimer - no start time data'); @@ -540,15 +654,19 @@ module.exports = function (RED) { node.getIntervalTime(); node.doStartInterval(); // starte Interval } else if (node.injType === tInj.intervalAmount) { - node.IntervalCountMax = node.positionConfig.getFloatProp(node, null, node.intervalCountType, node.intervalCount, 0); - node.intervalTime = Math.floor((millisecEnd - millisecStart) / node.IntervalCountMax); - node.IntervalCountCurrent = 0; + node.intervalCountMax = node.positionConfig.getFloatProp(node, null, node.intervalCountType, node.intervalCount, 0); + node.intervalTime = Math.floor((millisecEnd - millisecStart) / node.intervalCountMax); + node.intervalCountCurrent = 0; node.doStartInterval(); // starte Interval } return true; }; - node.initialize = (node, doEmit) => { + /** + * get the end time in millisecond + * @param {boolean} doEmit if true directly send Data + */ + node.initialize = doEmit => { switch (node.injType) { case tInj.interval: if (doEmit === true) { @@ -567,7 +685,7 @@ module.exports = function (RED) { _inject_type: 'once/startup' }); // will create timeout } else { - node.doCreateStartTimeout(node, 'initial'); + node.doCreateStartTimeout('initial'); } break; case tInj.intervalBtwStartEnd: @@ -578,8 +696,8 @@ module.exports = function (RED) { _inject_type: 'once/startup' }); // will create timeout } - if (!node.initializeStartTimer(node)) { - node.doCreateStartTimeout(node, 'initial'); + if (!node.initializeStartTimer()) { + node.doCreateStartTimeout('initial'); } break; case tInj.cron: @@ -589,11 +707,11 @@ module.exports = function (RED) { _inject_type: 'once/startup' }); // will create timeout } - node.doCreateCRONSetup(node); + node.doCreateCRONSetup(); break; default: // node.debug('initialize - default'); - node.doSetStatus(node, 'green'); + node.doSetStatus('green'); if (doEmit === true) { node.emit('input', { _inject_type: 'once/startup' @@ -604,9 +722,9 @@ module.exports = function (RED) { /** * get the end time in millisecond - * @param {*} node the node Data + * @return {number} the time in millisecond */ - node.getMillisecEnd = node => { + node.getMillisecEnd = () => { if (!node.timeEndData) { return null; } @@ -634,11 +752,11 @@ module.exports = function (RED) { /** * creates the end timeout - * @param {*} node - the node representation + * @param {number} [millisecEnd] - the end timestamp * @returns {object} state or error */ - node.doCreateEndTimeout = (node, millisecEnd) => { - millisecEnd = millisecEnd || node.getMillisecEnd(node); + node.doCreateEndTimeout = millisecEnd => { + millisecEnd = millisecEnd || node.getMillisecEnd(); if (millisecEnd === null) { return; } @@ -646,7 +764,7 @@ module.exports = function (RED) { if (millisecEnd > 345600000) { millisecEnd = Math.min((millisecEnd - 129600000), 2147483646); node.timeOutEndObj = setTimeout(() => { - node.doCreateEndTimeout(node); + node.doCreateEndTimeout(); }, millisecEnd); // 1,5 days before return; } @@ -654,7 +772,7 @@ module.exports = function (RED) { node.timeOutEndObj = null; clearInterval(node.intervalObj); node.intervalObj = null; - node.doCreateStartTimeout(node, 'timeOutEnd'); + node.doCreateStartTimeout('timeOutEnd'); }, millisecEnd); }; // doCreateEndTimeout @@ -664,10 +782,10 @@ module.exports = function (RED) { node.doRecalcStartTimeOut = () => { try { node.debug('performing a recalc of the next inject time'); - node.doCreateStartTimeout(node, 'timeOutRecalc'); + node.doCreateStartTimeout('timeOutRecalc'); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'dot', @@ -676,6 +794,12 @@ module.exports = function (RED) { } }; + /** + * creates the text for an interval + * @param {number} mstime - time stamp + * @param {number} val - value + * @returns {string} the text of the interval + */ node.getIntervalText = (mstime, val) => { if (mstime === 604800000) { if (val === 1) { @@ -754,6 +878,7 @@ module.exports = function (RED) { /** * Prepares a message object for sending + * @param {*} msg - the message object */ node.prepOutMsg = msg => { // node.debug(`prepOutMsg node.msg=${util.inspect(msg, { colors: true, compact: 10, breakLength: Infinity })}`); @@ -804,35 +929,35 @@ module.exports = function (RED) { */ node.doStartInterval = () => { node.timeOutStartObj = null; - node.doCreateEndTimeout(node); + node.doCreateEndTimeout(); clearInterval(node.intervalObj); - node.doSetStatus(node, 'green'); + node.doSetStatus('green'); node.send(node.prepOutMsg({ _inject_type: 'interval-time-start' })); node.intervalObj = setInterval(() => { - node.IntervalCountCurrent++; + node.intervalCountCurrent++; if (node.injType !== node.intervalAmount) { node.send(node.prepOutMsg({ _inject_type: 'interval-time' })); - } else if (node.IntervalCountCurrent < node.IntervalCountMax) { + } else if (node.intervalCountCurrent < node.intervalCountMax) { node.send(node.prepOutMsg({ _inject_type: 'interval-amount' })); } }, node.intervalTime); }; + /** creates the CRON interval */ node.doCreateCRONSetup = function () { - node.cronjob = scheduleTask(node.cronExpr,() => { + node.cronJobObj = scheduleTask(node.cronExpr, () => { node.emit('input', { _inject_type: 'cron' }); - node.doSetStatus(node); - }); - node.doSetStatus(node); + node.doSetStatus(); + }, {}); + node.doSetStatus(); }; /** * creates the timeout - * @param {*} node - the node representation - * @param {boolean} [_onInit] - _true_ if is in initialisation + * @param {string} [reason] - name of the reason * @returns {object} state or error */ - node.doCreateStartTimeout = (node, reason) => { + node.doCreateStartTimeout = reason => { // node.debug(`doCreateStartTimeout ${reason} node.timeStartData=${util.inspect(node.timeStartData, { colors: true, compact: 10, breakLength: Infinity })}`); if (node.injType === tInj.none || node.injType === tInj.interval) { @@ -849,20 +974,20 @@ module.exports = function (RED) { delete node.intervalTime; if (node.injType === tInj.intervalBtwStartEnd) { - node.IntervalCountMax = 0; + node.intervalCountMax = 0; node.getIntervalTime(); } if (node.injType === tInj.intervalAmount) { - node.IntervalCountMax = node.positionConfig.getFloatProp(node, null, node.intervalCountType, node.intervalCount, 0); + node.intervalCountMax = node.positionConfig.getFloatProp(node, null, node.intervalCountType, node.intervalCount, 0); delete node.intervalTime; } let fill = 'yellow'; let shape = 'dot'; - node.timeStartData.isAltAvailable = false; - node.timeStartData.isAltFirst = false; + node.isAltAvailable = false; + node.isAltFirst = false; let isFixedTime = true; node.timeStartData.now = new Date(); @@ -895,7 +1020,7 @@ module.exports = function (RED) { if (!hlp.isValidDate(node.nextStartTimeAlt)) { hlp.handleError(this, 'Invalid time format of alternate time "' + node.nextStartTimeAlt + '"', undefined, 'internal error! (' + reason +')'); } else { - node.timeStartData.isAltAvailable = true; + node.isAltAvailable = true; } } } else { @@ -915,12 +1040,12 @@ module.exports = function (RED) { } let millisec = hlp.getTimeOut(node.timeStartData.now, node.nextStartTime); - if (node.timeStartData.isAltAvailable) { + if (node.isAltAvailable) { shape = 'ring'; const millisecAlt = hlp.getTimeOut(node.timeStartData.now, node.nextStartTimeAlt); if (millisecAlt < millisec) { millisec = millisecAlt; - node.timeStartData.isAltFirst = true; + node.isAltFirst = true; } } @@ -939,35 +1064,37 @@ module.exports = function (RED) { }, millisec); // 1,5 days before fill = 'blue'; } else if (node.injType === tInj.intervalBtwStartEnd) { - // node.debug('intervalTime - timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.timeStartData.isAltAvailable + ' isAltFirst=' + node.timeStartData.isAltFirst + ')'); + // node.debug('intervalTime - timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.isAltAvailable + ' isAltFirst=' + node.isAltFirst + ')'); + // @ts-ignore node.timeOutStartObj = setTimeout(node.doStartInterval, millisec); fill = 'grey'; } else if (node.injType === tInj.intervalAmount) { - // node.debug('intervalAmount - timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.timeStartData.isAltAvailable + ' isAltFirst=' + node.timeStartData.isAltFirst + ')'); - const millisecEnd = node.getMillisecEnd(node); - node.intervalTime = Math.floor((millisecEnd - millisec) / node.IntervalCountMax); - node.IntervalCountCurrent = 0; + // node.debug('intervalAmount - timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.isAltAvailable + ' isAltFirst=' + node.isAltFirst + ')'); + const millisecEnd = node.getMillisecEnd(); + node.intervalTime = Math.floor((millisecEnd - millisec) / node.intervalCountMax); + node.intervalCountCurrent = 0; + // @ts-ignore node.timeOutStartObj = setTimeout(node.doStartInterval, millisec); fill = 'grey'; } else { // node.injType === tInj.timer - // node.debug('timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.timeStartData.isAltAvailable + ' isAltFirst=' + node.timeStartData.isAltFirst + ')'); + // node.debug('timeout ' + node.nextStartTime + ' is in ' + millisec + 'ms (isAlt=' + node.isAltAvailable + ' isAltFirst=' + node.isAltFirst + ')'); node.timeOutStartObj = setTimeout(() => { - // node.debug(`timeOutStartObj isAlt=${isAlt} isAltFirst=${node.timeStartData.isAltFirst}`); + // node.debug(`timeOutStartObj isAlt=${isAlt} isAltFirst=${node.isAltFirst}`); const msg = { _inject_type: 'time' }; node.timeOutStartObj = null; let useAlternateTime = false; - if (node.timeStartData.isAltAvailable) { + if (node.isAltAvailable) { let needsRecalc = false; try { useAlternateTime = node.positionConfig.comparePropValue(node, msg, node.property, node.propertyOperator, node.propertyThreshold); - needsRecalc = (node.timeStartData.isAltFirst && !useAlternateTime) || (!node.timeStartData.isAltFirst && useAlternateTime); - // node.debug(`timeOutStartObj isAltAvailable=${node.timeStartData.isAltAvailable} isAltFirst=${node.timeStartData.isAltFirst} needsRecalc=${needsRecalc}`); + needsRecalc = (node.isAltFirst && !useAlternateTime) || (!node.isAltFirst && useAlternateTime); + // node.debug(`timeOutStartObj isAltAvailable=${node.isAltAvailable} isAltFirst=${node.isAltFirst} needsRecalc=${needsRecalc}`); } catch (err) { - needsRecalc = node.timeStartData.isAltFirst; + needsRecalc = node.isAltFirst; hlp.handleError(node, RED._('time-inject.errors.invalid-property-type', node.property), err); } @@ -986,7 +1113,7 @@ module.exports = function (RED) { if (!isFixedTime && !node.intervalObj) { node.intervalObj = setInterval(() => { node.debug('retriggered'); - node.doCreateStartTimeout(node, 'retriggered'); + node.doCreateStartTimeout('retriggered'); }, node.recalcTime); } else if (isFixedTime && node.intervalObj) { clearInterval(node.intervalObj); @@ -994,12 +1121,16 @@ module.exports = function (RED) { } } } - node.doSetStatus(node, fill, shape); + node.doSetStatus(fill, shape); }; - node.doSetStatus = (node, fill, shape) => { - if (node.cronjob) { - const d = node.cronjob.nextRun; + /** sets the node status + * @param {string} [fill] - fill of the state icon + * @param {string} [shape] - sape of the state icon + */ + node.doSetStatus = (fill, shape) => { + if (node.cronJobObj) { + const d = node.cronJobObj.nextRun; if (d) { node.status({ fill: fill || 'green', @@ -1014,7 +1145,7 @@ module.exports = function (RED) { }); } } else if (node.nextStartTimeAlt && node.timeOutStartObj) { - if (node.timeStartData.isAltFirst) { + if (node.isAltFirst) { node.status({ fill, shape, @@ -1070,7 +1201,7 @@ module.exports = function (RED) { throw new Error('Configuration missing or wrong!'); } if (node.injType === tInj.timer) { - node.doCreateStartTimeout(node, 'on Input'); + node.doCreateStartTimeout('on Input'); } send(node.prepOutMsg(msg)); if (msg.payload && msg.payload.error) { @@ -1081,7 +1212,7 @@ module.exports = function (RED) { return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -1099,12 +1230,12 @@ module.exports = function (RED) { shape: 'ring', text: RED._('time-inject.message.onceDelay', { seconds: (node.onceDelay)}) }); - node.onceTimeout = setTimeout(() => { + node.onceTimeOutObj = setTimeout(() => { try { - node.initialize(node, true); + node.initialize(true); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -1116,12 +1247,12 @@ module.exports = function (RED) { } node.status({}); - node.onceTimeout = setTimeout(() => { + node.onceTimeOutObj = setTimeout(() => { try { - node.initialize(node, false); + node.initialize(false); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -1131,7 +1262,7 @@ module.exports = function (RED) { }, 100 + Math.floor(Math.random() * 500)); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -1143,32 +1274,31 @@ module.exports = function (RED) { RED.nodes.registerType('time-inject', timeInjectNode); timeInjectNode.prototype.close = function () { - if (this.onceTimeout) { - clearTimeout(this.onceTimeout); - this.onceTimeout = null; - } - if (this.onceTimeout2) { - clearTimeout(this.onceTimeout2); - this.onceTimeout2 = null; + if (this.onceTimeOutObj) { + clearTimeout(this.onceTimeOutObj); + this.onceTimeOutObj = null; } if (this.timeOutStartObj) { clearTimeout(this.timeOutStartObj); this.timeOutStartObj = null; + // @ts-ignore if (RED.settings.verbose) { this.log(RED._('inject.stopped')); } } if (this.intervalObj) { clearInterval(this.intervalObj); this.intervalObj = null; + // @ts-ignore if (RED.settings.verbose) { this.log(RED._('inject.stopped')); } } if (this.timeOutEndObj) { clearTimeout(this.timeOutEndObj); this.timeOutEndObj = null; + // @ts-ignore if (RED.settings.verbose) { this.log(RED._('inject.stopped')); } } - if (this.cronjob !== null) { - this.cronjob.stop(); - delete this.cronjob; + if (this.cronJobObj !== null) { + this.cronJobObj.stop(); + delete this.cronJobObj; } }; diff --git a/nodes/21-within-time-switch.js b/nodes/21-within-time-switch.js index 44a3625..333f080 100644 --- a/nodes/21-within-time-switch.js +++ b/nodes/21-within-time-switch.js @@ -24,8 +24,61 @@ * within-time-switch: *********************************************/ '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 {Object} IWithinTimeNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * + * @property {ITimePropertyType} timeStart - ?? + * @property {ITimePropertyType} timeStartAlt - ?? + * @property {IValuePropertyType} propertyStart - ?? + * @property {string} propertyStartOperator - ?? + * @property {IValuePropertyType} propertyStartThreshold - ?? + * + * @property {ITimePropertyType} timeEnd - ?? + * @property {ITimePropertyType} timeEndAlt - ?? + * @property {IValuePropertyType} propertyEnd - ?? + * @property {string} propertyEndOperator - ?? + * @property {IValuePropertyType} propertyEndThreshold - ?? + * + * @property {IValuePropertyType} timeRestrictions - ?? + * @property {ITypedValue} withinTimeValue - ?? + * @property {ITypedValue} outOfTimeValue - ?? + * + * @property {boolean} timeOnlyEvenDays - ?? + * @property {boolean} timeOnlyOddDays - ?? + * @property {boolean} timeOnlyEvenWeeks - ?? + * @property {boolean} timeOnlyOddWeeks - ?? + * @property {Date} timeStartDate - ?? + * @property {Date} timeEndDate - ?? + * @property {Array.} timeDays - ?? + * @property {Array.} timeMonths - ?? + * + * + * @property {NodeJS.Timer} timeOutObj - ?? + * @property {*} lastMsgObj - ?? + * + * @property {number} tsCompare - ?? + */ -module.exports = function (RED) { +/** + * @typedef {IWithinTimeNodeInstance & runtimeNode} IWithinTimeNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const util = require('util'); @@ -190,17 +243,17 @@ module.exports = function (RED) { } } } catch (err) { - node.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + node.debug(util.inspect(err)); node.error(err); } } if ((typeof node.timeDays !== 'undefined') && !node.timeDays.includes(dNow.getDay())) { - node.debug('invalid Day config. today=' + dNow.getDay() + ' timeDays=' + util.inspect(node.timeDays, Object.getOwnPropertyNames(node.timeDays))); + node.debug('invalid Day config. today=' + dNow.getDay() + ' timeDays=' + util.inspect(node.timeDays)); result.warn = RED._('within-time-switch.errors.invalid-day'); return result; } if ((typeof node.timeMonths !== 'undefined') && !node.timeMonths.includes(dNow.getMonth())) { - node.debug('invalid Month config. today=' + dNow.getMonth() + ' timeMonths=' + util.inspect(node.timeMonths, Object.getOwnPropertyNames(node.timeMonths))); + node.debug('invalid Month config. today=' + dNow.getMonth() + ' timeMonths=' + util.inspect(node.timeMonths)); result.warn = RED._('within-time-switch.errors.invalid-month'); return result; } @@ -267,7 +320,7 @@ module.exports = function (RED) { } catch (err) { result.altStartTime = false; hlp.handleError(node, RED._('within-time-switch.errors.invalid-propertyStart-type', node.propertyStart), err); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); } } @@ -305,12 +358,17 @@ module.exports = function (RED) { return result; } + /******************************************************************************************/ /** - * withinTimeSwitchNode - * @param {*} config - configuration + * standard Node-Red Node handler for the withinTimeSwitchNode + * @param {*} config the Node-Red Configuration property of the Node */ function withinTimeSwitchNode(config) { RED.nodes.createNode(this, config); + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {IWithinTimeNode} + */ + // @ts-ignore const node = this; // Retrieve the config node node.positionConfig = RED.nodes.getNode(config.positionConfig); @@ -450,8 +508,8 @@ module.exports = function (RED) { // config.timeDays = null; delete node.timeDays; } else { - node.timeDays = config.timeDays.split(','); - node.timeDays = node.timeDays.map( e => parseInt(e) ); + const tmp = config.timeDays.split(','); + node.timeDays = tmp.map( e => parseInt(e) ); } if (config.timeMonths === '') { @@ -460,8 +518,8 @@ module.exports = function (RED) { // config.timeMonths = null; delete node.timeMonths; } else { - node.timeMonths = config.timeMonths.split(','); - node.timeMonths = node.timeMonths.map( e => parseInt(e) ); + const tmp = config.timeMonths.split(','); + node.timeMonths = tmp.map( e => parseInt(e) ); } } node.withinTimeValue = { @@ -551,7 +609,7 @@ module.exports = function (RED) { return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); setstate(node, { error: RED._('node-red-contrib-sun-position/position-config:errors.error-title') }); done('internal error within-time-switch:' + err.message, msg); } diff --git a/nodes/22-delay-until.js b/nodes/22-delay-until.js index fe33cf1..3f320f1 100644 --- a/nodes/22-delay-until.js +++ b/nodes/22-delay-until.js @@ -24,21 +24,72 @@ * 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 + */ -module.exports = function(RED) { +/** + * @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'; /** - * withinTimeSwitchNode - * @param {*} config - configuration + * 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 path = require('path'); - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + 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; - RED.nodes.createNode(node, config); - node.locale = require('os-locale').sync(); // Retrieve the config node node.positionConfig = RED.nodes.getNode(config.positionConfig); // node.debug('initialize rdgDelayUntilNode ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); @@ -60,11 +111,11 @@ module.exports = function(RED) { offsetType : config.offsetType || 'none', offset : config.offset, multiplier : config.offsetMultiplier || 60000, - next: true, - calcByMsg: (config.timeType === 'msg' || - config.timeType === 'flow' || - config.timeType === 'global') + 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); @@ -157,7 +208,7 @@ module.exports = function(RED) { return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + 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); } @@ -278,7 +329,7 @@ module.exports = function(RED) { delete node.delayTimer; recalcTimeOut(qObj); }, millisec); // 1,5 days before - node.nextTime.intermedia = true; + node.nextTimeIntermedia = true; } else { node.debug('set timeout to ' + millisec); node.delayTimer = setTimeout(() => { @@ -307,7 +358,7 @@ module.exports = function(RED) { const qObj = {msg, done}; node.msgQueue.push(qObj); node.debug('test 4'); - if (!node.delayTimer || node.timeData.calcByMsg) { + if (!node.delayTimer || node.calcByMsg) { recalcTimeOut(qObj); } } @@ -344,8 +395,6 @@ module.exports = function(RED) { /** * adds a new message tot he queue - * @param {*} msg - message object - * @param {*} done - done object */ function setStatus() { if (node.msgQueue.length > 0) { @@ -354,7 +403,7 @@ module.exports = function(RED) { 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.nextTime.intermedia) { + } else if (node.nextTimeIntermedia) { node.status({ fill: 'yellow', shape: 'dot', diff --git a/nodes/30-sun-position.js b/nodes/30-sun-position.js index 439de2f..af2b4b1 100644 --- a/nodes/30-sun-position.js +++ b/nodes/30-sun-position.js @@ -23,42 +23,83 @@ /******************************************** * sun-position: *********************************************/ -module.exports = function (RED) { - 'use strict'; - const path = require('path'); +'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("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + */ + +/** + * @typedef {Object} ISunPositionNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * + * @property {string} topic output topic + * @property {Array} rules output topic + * + * @property {*} start type of start value + * @property {string} startType start value + * @property {*} startOffset start offset value + * @property {string} startOffsetType type of the start offset value + * @property {number} startOffsetMultiplier start offset multipier + * + * @property {*} end type of end value + * @property {string} endType end value + * @property {*} endOffset end offset value + * @property {string} endOffsetType type of the end offset value + * @property {number} endOffsetMultiplier end offset multipier + * + * @property {*} azimuthPos end offset multipier + */ - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + +/** + * @typedef {ISunPositionNodeInstance & runtimeNode} ISunPositionNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { + 'use strict'; + const hlp = require('./lib/dateTimeHelper.js'); const util = require('util'); + /******************************************************************************************/ /** - * sunPositionNode - * @param {*} config - configuration + * standard Node-Red Node handler for the sunPositionNode + * @param {*} config the Node-Red Configuration property of the Node */ function sunPositionNode(config) { RED.nodes.createNode(this, config); - // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - this.topic = config.topic || ''; - this.rules = config.rules || []; - this.azimuthPos = {}; - this.start = config.start; - this.startType = config.startType || 'none'; - this.startOffset = config.startOffset || 0; - this.startOffsetType = config.startOffsetType || 'none'; - this.startOffsetMultiplier = config.startOffsetMultiplier || 60; - this.end = config.end; - this.endType = config.endType || 'none'; - this.endOffset = config.endOffset || 0; - this.endOffsetType = config.endOffsetType || 'none'; - this.endOffsetMultiplier = config.endOffsetMultiplier || 60; + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {ISunPositionNode} + */ + // @ts-ignore const node = this; - if (!this.positionConfig) { + // Retrieve the config node + node.positionConfig = RED.nodes.getNode(config.positionConfig); + node.topic = config.topic || ''; + node.rules = config.rules || []; + node.azimuthPos = {}; + node.start = config.start; + node.startType = config.startType || 'none'; + node.startOffset = config.startOffset || 0; + node.startOffsetType = config.startOffsetType || 'none'; + node.startOffsetMultiplier = config.startOffsetMultiplier || 60; + node.end = config.end; + node.endType = config.endType || 'none'; + node.endOffset = config.endOffset || 0; + node.endOffsetType = config.endOffsetType || 'none'; + node.endOffsetMultiplier = config.endOffsetMultiplier || 60; + + 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; } - this.on('input', function (msg, send, done) { + node.on('input', function (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); }; @@ -67,19 +108,19 @@ module.exports = function (RED) { let errorStatus = ''; const dNow = hlp.getNowTimeStamp(this, msg); - if (!this.positionConfig) { + 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; } - const ports = new Array(this.rules.length); + const ports = new Array(node.rules.length); ports[0] = RED.util.cloneMessage(msg); - ports[0].payload = this.positionConfig.getSunCalc(dNow, true, false, msg.latitude || msg.lat, msg.longitude || msg.lon); - ports[0].topic = this.topic; + ports[0].payload = node.positionConfig.getSunCalc(dNow, true, false, msg.latitude || msg.lat, msg.longitude || msg.lon); + ports[0].topic = node.topic; if (!ports[0].payload.azimuth) { - // this.error('Azimuth could not calculated!'); - send(ports); // this.send(ports); + // node.error('Azimuth could not calculated!'); + send(ports); // node.send(ports); done('Azimuth could not calculated!', msg); return null; } @@ -137,8 +178,8 @@ module.exports = function (RED) { ports[0].payload.sunInSky = nowMillis > ports[0].payload.startTime && nowMillis < ports[0].payload.endTime; } - for (let i = 0; i < this.rules.length; i += 1) { - const rule = this.rules[i]; + for (let i = 0; i < node.rules.length; i += 1) { + const rule = node.rules[i]; const low = getNumProp(node, msg, rule.valueLowType, rule.valueLow); const high = getNumProp(node, msg, rule.valueHighType, rule.valueHigh); const chk = hlp.checkLimits(ports[0].payload.azimuth, low, high); @@ -155,7 +196,7 @@ module.exports = function (RED) { node.azimuthPos = ports[0].payload.pos; if (errorStatus) { - this.status({ + node.status({ fill: 'red', shape: 'dot', text: errorStatus @@ -186,18 +227,18 @@ module.exports = function (RED) { text = azimuth + '/' + altitude + ' - ' + node.positionConfig.toDateTimeString(ports[0].payload.lastUpdate); fill = 'grey'; } - this.status({ + node.status({ fill, shape: 'dot', text }); } - send(ports); // this.send(ports); + send(ports); // node.send(ports); done(); return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/31-moon-position.js b/nodes/31-moon-position.js index b8f07e7..5f16bcb 100644 --- a/nodes/31-moon-position.js +++ b/nodes/31-moon-position.js @@ -24,8 +24,42 @@ * moon-position: *********************************************/ '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("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + */ -module.exports = function (RED) { +/** + * @typedef {Object} IMoonPositionNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * + * @property {string} topic output topic + * @property {Array} rules output topic + * + * @property {*} start type of start value + * @property {string} startType start value + * @property {*} startOffset start offset value + * @property {string} startOffsetType type of the start offset value + * @property {number} startOffsetMultiplier start offset multipier + * + * @property {*} end type of end value + * @property {string} endType end value + * @property {*} endOffset end offset value + * @property {string} endOffsetType type of the end offset value + * @property {number} endOffsetMultiplier end offset multipier + * + * @property {*} azimuthPos end offset multipier + */ + +/** + * @typedef {IMoonPositionNodeInstance & runtimeNode} IMoonPositionNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const path = require('path'); @@ -33,24 +67,28 @@ module.exports = function (RED) { const util = require('util'); /** - * moonPositionNode - * @param {*} config - configuration + * standard Node-Red Node handler for the moonPositionNode + * @param {*} config the Node-Red Configuration property of the Node */ function moonPositionNode(config) { RED.nodes.createNode(this, config); - // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - this.topic = config.topic || ''; - this.rules = config.rules || []; - this.azimuthPos = {}; + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {IMoonPositionNode} + */ + // @ts-ignore const node = this; - if (!this.positionConfig) { + // Retrieve the config node + node.positionConfig = RED.nodes.getNode(config.positionConfig); + node.topic = config.topic || ''; + node.rules = config.rules || []; + node.azimuthPos = {}; + 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; } - this.on('input', function (msg, send, done) { + node.on('input', function (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); }; @@ -59,28 +97,28 @@ module.exports = function (RED) { const errorStatus = ''; const dNow = hlp.getNowTimeStamp(this, msg); - if (!this.positionConfig) { + 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; } - const ports = new Array(this.rules.length); + const ports = new Array(node.rules.length); ports[0] = RED.util.cloneMessage(msg); - ports[0].payload = this.positionConfig.getMoonCalc(dNow, true, true, msg.latitude || msg.lat, msg.longitude || msg.lon); + ports[0].payload = node.positionConfig.getMoonCalc(dNow, true, true, msg.latitude || msg.lat, msg.longitude || msg.lon); - ports[0].topic = this.topic; + ports[0].topic = node.topic; if (!ports[0].payload.azimuth) { - // this.error('Azimuth could not calculated!'); - send(ports); // this.send(ports); + // node.error('Azimuth could not calculated!'); + send(ports); // node.send(ports); done('Azimuth could not calculated!', msg); return null; } ports[0].payload.pos = []; ports[0].payload.posChanged = false; - for (let i = 0; i < this.rules.length; i += 1) { - const rule = this.rules[i]; + for (let i = 0; i < node.rules.length; i += 1) { + const rule = node.rules[i]; const low = getNumProp(node, msg, rule.valueLowType, rule.valueLow); const high = getNumProp(node, msg, rule.valueHighType, rule.valueHigh); const chk = hlp.checkLimits(ports[0].payload.azimuth, low, high); @@ -99,7 +137,7 @@ module.exports = function (RED) { node.azimuthPos = ports[0].payload.pos; if (errorStatus) { - this.status({ + node.status({ fill: 'red', shape: 'dot', text: errorStatus @@ -114,18 +152,18 @@ module.exports = function (RED) { text = azimuth + '/' + altitude + ' - ' + node.positionConfig.toDateTimeString(ports[0].payload.lastUpdate); fill = 'grey'; } - this.status({ + node.status({ fill, shape: 'dot', text }); } - send(ports); // this.send(ports); + send(ports); // node.send(ports); done(); return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/60-time-comp.js b/nodes/60-time-comp.js index a69676f..84d26d4 100644 --- a/nodes/60-time-comp.js +++ b/nodes/60-time-comp.js @@ -24,35 +24,64 @@ * time-comp: *********************************************/ '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("./10-position-config.js").ITimePropertyType} ITimePropertyType + * @typedef {import("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + */ -module.exports = function (RED) { + +/** + * @typedef {Object} ITimeCompareNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * + * @property {ITimePropertyType} input - input data + * @property {Array} results - output data + * @property {Array} rules - input data + * @property {boolean|string} checkall - define if check all rules + * + */ + +/** + * @typedef {ITimeCompareNodeInstance & runtimeNode} ITimeCompareNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const util = require('util'); - const path = require('path'); - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + const hlp = require('./lib/dateTimeHelper.js'); /** - * timeCompNode - * @param {*} config - configuration + * standard Node-Red Node handler for the timeCompNode + * @param {*} config the Node-Red Configuration property of the Node */ function timeCompNode(config) { RED.nodes.createNode(this, config); + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {ITimeCompareNode} + */ + // @ts-ignore + const node = this; // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - // this.debug('initialize time Node ' + util.inspect(config, { colors: true, compact: 10, breakLength: Infinity })); - if (!this.positionConfig) { + node.positionConfig = RED.nodes.getNode(config.positionConfig); + // node.debug('initialize time Node ' + 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; } - if (this.positionConfig.checkNode( + if (node.positionConfig.checkNode( error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); }, false)) { return; } - this.input = { + node.input = { type: config.inputType, value: config.input, next: hlp.isTrue(config.inputNext), @@ -92,7 +121,7 @@ module.exports = function (RED) { delete config.result1OffsetMultiplier; } - this.results = []; + node.results = []; config.results.forEach(prop => { const propNew = { outType : prop.pt, @@ -112,21 +141,21 @@ module.exports = function (RED) { onlyOddWeeks : prop.onlyOddWeeks }; - if (this.positionConfig && propNew.type === 'jsonata') { + if (node.positionConfig && propNew.type === 'jsonata') { try { - propNew.expr = this.positionConfig.getJSONataExpression(this, propNew.value); + propNew.expr = node.positionConfig.getJSONataExpression(this, propNew.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error: err.message })); + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error: err.message })); propNew.expr = null; } } - this.results.push(propNew); + node.results.push(propNew); }); - this.rules = config.rules; - this.checkall = config.checkall; - const node = this; - if (!this.positionConfig) { + node.rules = config.rules; + node.checkall = config.checkall; + + if (!node.positionConfig) { node.status({ fill: 'red', shape: 'dot', @@ -135,7 +164,7 @@ module.exports = function (RED) { return; } - this.on('input', (msg, send, done) => { + 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); }; @@ -171,9 +200,9 @@ module.exports = function (RED) { resultObj = node.positionConfig.getOutDataProp(this, msg, prop, dNow); } if (resultObj === null || (typeof resultObj === 'undefined')) { - this.error('Could not evaluate ' + prop.type + '.' + prop.value + '. - Maybe settings outdated (open and save again)!'); + node.error('Could not evaluate ' + prop.type + '.' + prop.value + '. - Maybe settings outdated (open and save again)!'); } else if (resultObj.error) { - this.error('error on getting result: "' + resultObj.error + '"'); + node.error('error on getting result: "' + resultObj.error + '"'); } else { node.positionConfig.setMessageProp(this, msg, prop.outType, prop.outValue, resultObj); } @@ -187,7 +216,7 @@ module.exports = function (RED) { let operatorValid = true; if (rule.propertyType !== 'none') { const res = RED.util.evaluateNodeProperty(rule.propertyValue, rule.propertyType, node, msg); - operatorValid = hlp.toBoolean(res); + operatorValid = hlp.isTrue(res); } if (operatorValid) { @@ -344,7 +373,7 @@ module.exports = function (RED) { return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/61-time-span.js b/nodes/61-time-span.js index a858fa2..1b85a69 100644 --- a/nodes/61-time-span.js +++ b/nodes/61-time-span.js @@ -24,13 +24,40 @@ * time-span: *********************************************/ '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("./10-position-config.js").ITimePropertyType} ITimePropertyType + * @typedef {import("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + */ + + +/** + * @typedef {Object} ITimeSpanNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * + * @property {ITimePropertyType} operand1 - operand1 data + * @property {ITimePropertyType} operand2 - operand2 data + * @property {number} operand - operand + * operand + * @property {Array} results - output data + * @property {Array} rules - input data + * @property {boolean|string} checkall - define if check all rules + * + */ -module.exports = function (RED) { +/** + * @typedef {ITimeSpanNodeInstance & runtimeNode} ITimeSpanNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const util = require('util'); - const path = require('path'); - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); + const hlp = require('./lib/dateTimeHelper.js'); const perSecond = 1000; const perMinute = 60000; @@ -173,6 +200,7 @@ module.exports = function (RED) { tt: H < 12 ? 'am' : 'pm', T: H < 12 ? 'A' : 'P', TT: H < 12 ? 'AM' : 'PM', + // @ts-ignore S: ['th', 'st', 'nd', 'rd'][d % 10 > 3 ? 0 : (d % 100 - d % 10 !== 10) * d % 10] }; @@ -185,13 +213,14 @@ module.exports = function (RED) { * get a formated timespan * @param {Date} date1 - Date 1 * @param {Date} date2 - Date 2 - * @param {string} format - the out format - * @returns {string} the formatet timespan + * @param {number|string} format - the out format + * @returns {number|object} the formatet timespan */ function getFormattedTimeSpanOut(node, date1, date2, format) { const timeSpan = date1.getTime() - date2.getTime(); format = format || 0; + // @ts-ignore if (isNaN(format)) { return formatTS(date1, date2, String(format)); } @@ -279,26 +308,32 @@ module.exports = function (RED) { } /** - * timeSpanNode - * @param {*} config - configuration + * standard Node-Red Node handler for the timeSpanNode + * @param {*} config the Node-Red Configuration property of the Node */ function timeSpanNode(config) { RED.nodes.createNode(this, config); + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {ITimeSpanNode} + */ + // @ts-ignore + const node = this; + // Retrieve the config node - this.positionConfig = RED.nodes.getNode(config.positionConfig); - if (!this.positionConfig) { + node.positionConfig = RED.nodes.getNode(config.positionConfig); + 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') }); return; } - if (this.positionConfig.checkNode( + if (node.positionConfig.checkNode( error => { node.error(error); node.status({fill: 'red', shape: 'dot', text: error }); }, false)) { return; } - this.operand1 = { + node.operand1 = { type: config.operand1Type, value: config.operand1, format: config.operand1Format, @@ -307,7 +342,7 @@ module.exports = function (RED) { multiplier: config.operand1OffsetMultiplier }; - this.operand2 = { + node.operand2 = { type: config.operand2Type, value: config.operand2, format: config.operand2Format, @@ -348,7 +383,7 @@ module.exports = function (RED) { delete config.result1OffsetMultiplier; } - this.results = []; + node.results = []; config.results.forEach(prop => { const propNew = { outType : prop.pt, @@ -369,22 +404,21 @@ module.exports = function (RED) { onlyOddWeeks : prop.onlyOddWeeks }; - if (this.positionConfig && propNew.type === 'jsonata') { + if (node.positionConfig && propNew.type === 'jsonata') { try { - propNew.expr = this.positionConfig.getJSONataExpression(this, propNew.value); + propNew.expr = node.positionConfig.getJSONataExpression(this, propNew.value); } catch (err) { - this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error: err.message })); + node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error: err.message })); propNew.expr = null; } } - this.results.push(propNew); + node.results.push(propNew); }); - this.operand = config.operand; - this.rules = config.rules; - this.checkall = config.checkall; - const node = this; + node.operand = config.operand; + node.rules = config.rules; + node.checkall = config.checkall; - this.on('input', (msg, send, done) => { + 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); }; @@ -438,9 +472,9 @@ module.exports = function (RED) { } if (resultObj === null || (typeof resultObj === 'undefined')) { - this.error('Could not evaluate ' + prop.type + '.' + prop.value + '. - Maybe settings outdated (open and save again)!'); + node.error('Could not evaluate ' + prop.type + '.' + prop.value + '. - Maybe settings outdated (open and save again)!'); } else if (resultObj.error) { - this.error('error on getting result: "' + resultObj.error + '"'); + node.error('error on getting result: "' + resultObj.error + '"'); } else { node.positionConfig.setMessageProp(this, msg, prop.outType, prop.outValue, resultObj); } @@ -491,7 +525,7 @@ module.exports = function (RED) { } } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -515,7 +549,7 @@ module.exports = function (RED) { return null; } catch (err) { node.log(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/80-blind-control.js b/nodes/80-blind-control.js index 402c4f8..2215922 100644 --- a/nodes/80-blind-control.js +++ b/nodes/80-blind-control.js @@ -23,14 +23,43 @@ /******************************************** * blind-control: *********************************************/ +'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").ITimeObject} ITimeObject + * @typedef {import("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + * @typedef {import("./lib/timeControlHelper.js").ITimeControlNodeInstance} ITimeControlNodeInstance + */ + +/** + * @typedef {Object} IBlindControlNodeInstance Extensions for the nodeInstance object type + * @property {Object} nodeData get/set generic Data of the node + * @property {Object} sunData - the sun data Object + * @property {Object} windowSettings - the window settings Object + * @property {number} smoothTime smoothTime + * @property {boolean} levelReverse - indicator if the Level is in reverse order + * @property {Array.} oversteers - tbd + * @property {Object} oversteer - tbd + * @property {Object} level - tbd + * @property {Object} previousData - tbd + * @property {Array.} results - tbd + * ... obviously there are more ... + */ + +/** + * @typedef {ITimeControlNodeInstance & IBlindControlNodeInstance & runtimeNode} IBlindControlNode Combine nodeInstance with additional, optional functions + */ /******************************************************************************************/ -module.exports = function (RED) { +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; - const path = require('path'); - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); - const ctrlLib = require(path.join(__dirname, '/lib/timeControlHelper.js')); + const hlp = require('./lib/dateTimeHelper.js'); + const ctrlLib = require('./lib/timeControlHelper.js'); const util = require('util'); const clonedeep = require('lodash.clonedeep'); const isEqual = require('lodash.isequal'); @@ -44,7 +73,7 @@ module.exports = function (RED) { /** * get the absolute level from percentage level * @param {*} node the node settings - * @param {*} percentPos the level in percentage (0-1) + * @param {*} levelPercent the level in percentage (0-1) */ function posPrcToAbs_(node, levelPercent) { return posRound_(node, ((node.nodeData.levelTop - node.nodeData.levelBottom) * levelPercent) + node.nodeData.levelBottom); @@ -61,7 +90,7 @@ module.exports = function (RED) { /** * get the absolute inverse level * @param {*} node the node settings - * @param {*} levelAbsolute the level absolute + * @param {*} level the level absolute * @return {number} get the inverse level */ function getInversePos_(node, level) { @@ -141,7 +170,7 @@ module.exports = function (RED) { }, false, oNow.now))); } catch (err) { node.error(RED._('blind-control.errors.getOversteerData', err)); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); } // node.debug('node.oversteers=' + util.inspect(node.oversteers, { colors: true, compact: 10, breakLength: Infinity })); return undefined; @@ -168,7 +197,7 @@ module.exports = function (RED) { return node.nodeData.levelBottom; } else if (value.includes('open')) { return node.nodeData.levelTop; - } else if (val === '') { + } else if (value === '') { return def; } } else { @@ -188,7 +217,7 @@ module.exports = function (RED) { return res; } catch (err) { node.error(RED._('blind-control.errors.getBlindPosData', err)); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); } return def; } @@ -502,7 +531,7 @@ module.exports = function (RED) { * @param {Object} node node data * @param {number} [rulePos] the position of the rule which should be changed * @param {string} [ruleName] the name of the rule which should be changed - * @param {Object} ruleData the properties of the rule which should be changed + * @param {Object} [ruleData] the properties of the rule which should be changed */ function changeRules(node, rulePos, ruleName, ruleData) { // node.debug(`changeRules: ${ node.rules.count } ruleData:' ${util.inspect(ruleData, {colors:true, compact:10})}`); @@ -827,6 +856,10 @@ module.exports = function (RED) { function sunBlindControlNode(config) { RED.nodes.createNode(this, config); this.positionConfig = RED.nodes.getNode(config.positionConfig); + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {IBlindControlNode} + */ + // @ts-ignore const node = this; if (!this.positionConfig) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); @@ -1152,7 +1185,7 @@ module.exports = function (RED) { /** * handles the input of a message object to the node */ - this.on('input', function (msg, send, done) { + node.on('input', function (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); }; @@ -1267,7 +1300,6 @@ module.exports = function (RED) { } // initialize - node.nowarn = {}; const tempData = node.context().get('cacheData',node.contextStore) || {}; if (!isNaN(node.level.current)) { node.previousData.level = node.level.current; @@ -1504,7 +1536,7 @@ module.exports = function (RED) { done(); return null; } catch (err) { - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -1515,7 +1547,7 @@ module.exports = function (RED) { return null; }); - this.on('close', () => { + node.on('close', () => { if (node.autoTriggerObj) { clearTimeout(node.autoTriggerObj); delete node.autoTriggerObj; @@ -1531,7 +1563,7 @@ module.exports = function (RED) { ctrlLib.initializeCtrl(RED, node, config); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/81-clock-timer.js b/nodes/81-clock-timer.js index f9fdc9d..79ba339 100644 --- a/nodes/81-clock-timer.js +++ b/nodes/81-clock-timer.js @@ -1,3 +1,4 @@ +// @ts-check /* * This code is licensed under the Apache License Version 2.0. * @@ -23,12 +24,47 @@ /******************************************** * clock-timer: *********************************************/ -module.exports = function (RED) { - 'use strict'; - const path = require('path'); +/** --- 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").ITimeObject} ITimeObject + * @typedef {import("./10-position-config.js").IPositionConfigNode} IPositionConfigNode + * @typedef {import("./lib/timeControlHelper.js").ITimeControlNodeInstance} ITimeControlNodeInstance + */ + +// IClockTimerNodeInstance +/** + * @typedef {Object} IClockTimerNodeInstance Extensions for the nodeInstance object type + * @property {Object} nodeData get/set generic Data of the node + * @property {Object} reason - tbd + * @property {string} contextStore - used context store + * @property {Object} oversteer - tbd + * @property {Object} rules - tbd + * @property {Object} payload - tbd + * @property {Object} previousData - tbd + * @property {Array.} results - tbd + * + * @property {Object} autoTrigger autotrigger options + * @property {NodeJS.Timeout} autoTriggerObj autotrigger options + * + * @property {Object} startDelayTimeOut - tbd + * @property {NodeJS.Timeout} startDelayTimeOutObj - tbd - const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); - const ctrlLib = require(path.join(__dirname, '/lib/timeControlHelper.js')); + * @property {function} setState function for settign the state of the node + * ... obviously there are more ... + */ + +/** + * @typedef {ITimeControlNodeInstance & IClockTimerNodeInstance & runtimeNode} IClockTimerNode Combine nodeInstance with additional, optional functions + */ +/******************************************************************************************/ +/** Export the function that defines the node + * @type {runtimeRED} */ +module.exports = function (/** @type {runtimeRED} */ RED) { + 'use strict'; + const hlp = require('./lib/dateTimeHelper.js'); + const ctrlLib = require('./lib/timeControlHelper.js'); const util = require('util'); const clonedeep = require('lodash.clonedeep'); const isEqual = require('lodash.isequal'); @@ -147,7 +183,7 @@ module.exports = function (RED) { * @param {Object} node node data * @param {number} [rulePos] the position of the rule which should be changed * @param {string} [ruleName] the name of the rule which should be changed - * @param {Object} ruleData the properties of the rule which should be changed + * @param {Object} [ruleData] the properties of the rule which should be changed */ function changeRules(node, rulePos, ruleName, ruleData) { // node.debug(`changeRules: ${ node.rules.count } ruleData:' ${util.inspect(ruleData, {colors:true, compact:10})}`); @@ -321,6 +357,10 @@ module.exports = function (RED) { RED.nodes.createNode(this, config); this.positionConfig = RED.nodes.getNode(config.positionConfig); this.outputs = Number(config.outputs || 1); + /** Copy 'this' object in case we need it in context of callbacks of other functions. + * @type {IClockTimerNode} + */ + // @ts-ignore const node = this; if (!this.positionConfig) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.config-missing')); @@ -433,7 +473,7 @@ module.exports = function (RED) { /** * handles the input of a message object to the node */ - this.on('input', function (msg, send, done) { + node.on('input', function (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); }; @@ -498,7 +538,6 @@ module.exports = function (RED) { } // initialize - node.nowarn = {}; const tempData = node.context().get('cacheData',node.contextStore) || {}; const previousData = node.context().get('lastData', node.contextStore) || { reasonCode: -1, @@ -650,7 +689,7 @@ module.exports = function (RED) { done(); return null; } catch (err) { - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -661,7 +700,7 @@ module.exports = function (RED) { return null; }); - this.on('close', () => { + node.on('close', () => { if (node.autoTriggerObj) { clearTimeout(node.autoTriggerObj); delete node.autoTriggerObj; @@ -677,7 +716,7 @@ module.exports = function (RED) { ctrlLib.initializeCtrl(RED, node, config); } catch (err) { node.error(err.message); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js index b54089c..5735047 100644 --- a/nodes/lib/dateTimeHelper.js +++ b/nodes/lib/dateTimeHelper.js @@ -1,3 +1,4 @@ +// @ts-check /* * This code is licensed under the Apache License Version 2.0. * @@ -24,6 +25,38 @@ * dateTimeHelper.js: *********************************************/ 'use strict'; + +/** +* support timeData +* @typedef {Object} ITimeObject Data +* @param {Date} now +* @param {number} nowNr +* @param {number} dayNr +* @param {number} monthNr +* @param {number} dateNr +* @param {number} yearNr +* @param {number} dayId +*/ + +/** + * @typedef {Object} ILimitationsObj + * @property {boolean} [next] - if __true__ the next date will be delivered starting from now, otherwise the matching date of the date from now + * @property {Array.|string} [days] - days for which should be calculated the sun time + * @property {Array.|string} [months] - months for which should be calculated the sun time + * @property {boolean} [onlyOddDays] - if true only odd days will be used + * @property {boolean} [onlyEvenDays] - if true only even days will be used + * @property {boolean} [onlyOddWeeks] - if true only odd weeks will be used + * @property {boolean} [onlyEvenWeeks] - if true only even weeks will be used + */ + + +/** + * @typedef {Object} ILimitedDate + * @property {Date} date - The limited Date + * @property {boolean} hasChanged - indicator if the input Date has changed + * @property {string} error - if an error occurs the string is not empty + */ + const util = require('util'); const TIME_WEEK = 604800000; const TIME_24h = 86400000; @@ -224,7 +257,7 @@ function pad2(n) { // always returns a string /** * creates a string from a number with leading zeros - * @param {string|number|boolean} n number to format + * @param {any} val number to format * @param {number} [len] length of number (default 2) * @returns {string} number with minimum digits as defined in length */ @@ -249,8 +282,8 @@ function pad(val, len) { * generic function for handle a error in a node * @param {any} node the node where the error occurs * @param {String} messageText the message text - * @param {Error} err the error object - * @param {string} stateText the state text which should be set to the node + * @param {Error} [err] the error object + * @param {string} [stateText] the state text which should be set to the node */ function handleError(node, messageText, err, stateText) { if (!err) { @@ -265,7 +298,7 @@ function handleError(node, messageText, err, stateText) { if (node && messageText) { node.error(messageText); - node.log(util.inspect(err, Object.getOwnPropertyNames(err))); + node.log(util.inspect(err)); node.status({ fill: 'red', shape: 'ring', @@ -274,7 +307,7 @@ function handleError(node, messageText, err, stateText) { } else if (console) { /* eslint-disable no-console */ console.error(messageText); - console.log(util.inspect(err, Object.getOwnPropertyNames(err))); + console.log(util.inspect(err)); console.trace(); // eslint-disable-line /* eslint-enable no-console */ } @@ -358,9 +391,9 @@ function getLastDayOfMonth(year, month, dayOfWeek) { /** * get a date for the special day in the given month - * @param {string} year year to check + * @param {number} year year to check * @param {number} month month to check - * @param {number} dayName Name of the special day + * @param {string} dayName Name of the special day * @returns {Date|null} last day of given month or null */ function getSpecialDayOfMonth(year, month, dayName) { @@ -426,7 +459,7 @@ function getWeekOfYear(date) { // Get first day of year const yearStart = new Date(Date.UTC(date.getUTCFullYear(),0,1)); // Calculate full weeks to nearest Thursday - const weekNo = Math.ceil(( ( (date - yearStart) / TIME_24h) + 1)/7); + const weekNo = Math.ceil(( ( (date.getTime() - yearStart.getTime()) / TIME_24h) + 1)/7); // Return array of year and week number return [date.getUTCFullYear(), weekNo]; } @@ -438,7 +471,7 @@ function getWeekOfYear(date) { */ function getDayOfYear(date) { const start = new Date(date.getFullYear(), 0, 0); - const diff = (date - start) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * TIME_1min); + const diff = (date.getTime() - start.getTime()) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * TIME_1min); return [date.getUTCFullYear(), Math.floor(diff / TIME_24h)]; } @@ -485,19 +518,6 @@ function getNowTimeStamp(node, msg) { return new Date(); } - -/** -* support timeData -* @name ITimeObject Data -* @param {Date} now -* @param {number} nowNr -* @param {number} dayNr -* @param {number} monthNr -* @param {number} dateNr -* @param {number} yearNr -* @param {number} dayId -*/ - /** * the definition of the time to compare * @param {*} node the current node object @@ -546,14 +566,14 @@ function getTimeOut(base, time) { /** * check if a given number is in given limits * @param {number} num number angle to compare - * @param {number} low low limit - * @param {number} high high limit - * @return {bool} **true** if the number is inside given limits, at least one limit must be validate, otherwise returns **false** + * @param {number} [low] low limit + * @param {number} [high] high limit + * @return {boolean} **true** if the number is inside given limits, at least one limit must be validate, otherwise returns **false** */ function checkLimits(num, low, high) { // console.debug('checkLimits num=' + num + ' low=' + low + ' high=' + high); // eslint-disable-line - if (typeof low !== 'undefined' && low !== '' && !isNaN(low) && low >= 0) { - if (typeof high !== 'undefined' && high !== '' && !isNaN(high) && high >= 0) { + if (typeof low === 'number' && !isNaN(low)) { + if (typeof high === 'number' && !isNaN(high)) { if (high > low) { return (num > low) && (num < high); } @@ -562,16 +582,31 @@ function checkLimits(num, low, high) { return (num > low); } - if (typeof high !== 'undefined' && high !== '' && !isNaN(high)) { + if (typeof high === 'number' && !isNaN(high)) { return (num < high); } return false; } +/** + * @callback IIsFoundNumberFunc + * @param {number} res - Value of the found value + */ + +/** + * @callback INotFoundFunc + * @param {*} msg - message + * @return {any} + */ + /** * check the type of the message * @param {*} msg message - * @param {*} name property name + * @param {string|Array.} ids property names to check + * @param {string|Array.} [names] topic names to check + * @param {IIsFoundNumberFunc} [isFound] if the topic is found this function will be called with the found value + * @param {INotFoundFunc} [notFound] topic names to check + * @return {number|any} */ function getMsgNumberValue(msg, ids, names, isFound, notFound) { if (ids && msg) { @@ -627,7 +662,10 @@ function getMsgNumberValue(msg, ids, names, isFound, notFound) { /** * check the type of the message * @param {*} msg message - * @param {*} name property name + * @param {string|Array.} ids property names to check + * @param {IIsFoundNumberFunc} [isFound] if the topic is found this function will be called with the found value + * @param {INotFoundFunc} [notFound] topic names to check + * @return {number|any} */ function getMsgNumberValue2(msg, ids, isFound, notFound) { if (ids && msg) { @@ -654,10 +692,22 @@ function getMsgNumberValue2(msg, ids, isFound, notFound) { } return notFound; } + +/** + * @callback IIsFoundBoolFunc + * @param {boolean} result - found value converted to boolean + * @param {*} realResult - real value which was found or the complete payload + * @param {string} topic - topic of the message + */ + /** * check if the msg or msg.property contains a property with value true or the topic contains the given name * @param {*} msg message - * @param {*} name property name + * @param {string|Array.} ids property names to check + * @param {string|Array.} [names] topic names to check + * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value + * @param {INotFoundFunc} [notFound] topic names to check + * @return {boolean|any} */ function getMsgBoolValue(msg, ids, names, isFound, notFound) { if (ids && msg) { @@ -686,7 +736,10 @@ function getMsgBoolValue(msg, ids, names, isFound, notFound) { /** * check if the msg contains a property with value true (no payload check) * @param {*} msg message - * @param {*} name property name + * @param {string|Array.} ids property names to check + * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value + * @param {INotFoundFunc} [notFound] topic names to check + * @return {boolean|any} */ function getMsgBoolValue2(msg, ids, isFound, notFound) { if (ids && msg) { @@ -714,10 +767,13 @@ function getMsgBoolValue2(msg, ids, isFound, notFound) { /** * check if thetopic contains one of the given names * @param {*} msg message - * @param {*} name property name + * @param {string|Array.} [names] topic names to check + * @param {IIsFoundBoolFunc} [isFound] if the topic is found this function will be called with the found value + * @param {INotFoundFunc} [notFound] topic names to check + * @return {boolean} */ function getMsgTopicContains(msg, names, isFound, notFound) { - if (msg) { // && msg.topic + if (msg && names) { if (!Array.isArray(names)) { names = [names]; } @@ -763,7 +819,7 @@ function isDSTObserved(d) { /** * changes the time based on a timezone - * @param {date} date Javascript Date object + * @param {Date} date Javascript Date object * @param {number} timeZoneOffset Offset in Minutes * @return {date} new date object with changed timezone to use with .toLocaleString() */ @@ -781,6 +837,7 @@ function convertDateTimeZone(date, timeZoneOffset) { * @returns {boolean} returns __true__ if it is a valid Date, otherwhise __false__ */ function isValidDate(d) { + // @ts-ignore return d instanceof Date && !isNaN(d); // d !== 'Invalid Date' && !isNaN(d) } @@ -809,7 +866,7 @@ function isoStringToDate( isoString ) { // The month numbers are one "off" from what normal humans would expect // because January == 0. - returnDate.setUTCMonth( parseInt( dateParts[ 1 ] - 1 ) ); + returnDate.setUTCMonth( parseInt( dateParts[ 1 ] ) - 1 ); returnDate.setUTCDate( parseInt( dateParts[ 2 ] ) ); // Set the time parts of the date object. @@ -868,8 +925,8 @@ const parseDate = dateString => { /** * Round a date to the nearest full Hour - * @param {DATE} date Date to round - * @returns {DATE} Date round to next full Hour + * @param {Date} date Date to round + * @returns {Date} Date round to next full Hour */ function roundToHour(date) { return new Date(Math.round(date.getTime() / TIME_1h ) * TIME_1h); @@ -942,30 +999,19 @@ function calcMonthOffset(months, monthstart) { } /*******************************************************************************************************/ -/** - * @typedef {Object} limitationsObj - * @property {number} [next] if greater than 0 the number of days in the future - * @property {string} [days] days for which should be calculated the sun time - * @property {string} [months] months for which should be calculated the sun time - * @property {boolean} [onlyOddDays] - if true only odd days will be used - * @property {boolean} [onlyEvenDays] - if true only even days will be used - * @property {boolean} [onlyOddWeeks] - if true only odd weeks will be used - * @property {boolean} [onlyEvenWeeks] - if true only even weeks will be used - */ - /** * normalize date by adding offset, get only the next valid date, etc... - * @param {Date} d input Date to normalize + * @param {Date|number|object} d input Date to normalize * @param {number} offset offset to add tot he Date object * @param {number} multiplier multiplier for the offset - * @param {limitationsObj} [limit] additional limitations for the calculation + * @param {ILimitationsObj} [limit] additional limitations for the calculation * @return {Date} a normalized date moved tot the future to fulfill all conditions */ function normalizeDate(d, offset, multiplier, limit) { // console.debug(`normalizeDate d=${d} offset=${ offset }, multiplier=${multiplier}, limit=${limit}`); // eslint-disable-line if (d === null || typeof d === 'undefined') { return d; } if (d.value) { d = d.value; } - if (!(d instanceof Date)) { d = Date(d); } + if (!(d instanceof Date)) { d = new Date(d); } d = addOffset(d, offset, multiplier); if (limit.next) { const dNow = new Date(); @@ -985,14 +1031,29 @@ function normalizeDate(d, offset, multiplier, limit) { /** * calculates limitation of a date - * @param {limitationsObj} limit limitation object - * @param {Date} date Date to check - * @returns [date, hasChanged, error] + * @param {ILimitationsObj} limit - limitation object + * @param {Date} d - Date to check + * @returns {ILimitedDate} result limited Date Object. */ function limitDate(limit, d) { let hasChanged = false; let error = ''; - if (limit.days && (limit.days !== '*') && (limit.days !== '')) { + if (typeof(limit.days) === 'string') { + if ((limit.days === '*') || (limit.days === '')) { + delete limit.months; + } else { + const tmp = limit.days.split(','); + limit.days = []; + tmp.forEach(element => { + const el = parseInt(element.trim()); + if (!isNaN(el)) { + // @ts-ignore + limit.days.push(el); + } + }); + } + } + if (limit.days && Array.isArray(limit.days)) { const dayx = calcDayOffset(limit.days, d.getDay()); if (dayx > 0) { d.setDate(d.getDate() + dayx); @@ -1001,8 +1062,22 @@ function limitDate(limit, d) { error = 'No valid day of week found!'; } } - - if (limit.months && (limit.months !== '*') && (limit.months !== '')) { + if (typeof(limit.months) === 'string') { + if ((limit.months === '*') || (limit.months === '')) { + delete limit.months; + } else { + const tmp = limit.months.split(','); + limit.months = []; + tmp.forEach(element => { + const el = parseInt(element.trim()); + if (!isNaN(el)) { + // @ts-ignore + limit.months.push(el); + } + }); + } + } + if (limit.months && Array.isArray(limit.months)) { const monthx = calcMonthOffset(limit.months, d.getMonth()); if (monthx > 0) { d.setMonth(d.getMonth() + monthx); @@ -1138,6 +1213,7 @@ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { let dto = new Date(dt); if (utc || timeZoneOffset === 0) { + // @ts-ignore dto = Date.UTC(dt); } else if (timeZoneOffset) { dto = convertDateTimeZone(dto, timeZoneOffset); @@ -1148,6 +1224,7 @@ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { } } + // @ts-ignore const result = getTimeOfText(String(dt), utc, timeZoneOffset); if (result !== null && typeof result !== 'undefined') { return result; @@ -1158,6 +1235,7 @@ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { if (res !== null && typeof res !== 'undefined') { return res; } res = _parseDate(dt, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } + // @ts-ignore res = _parseArray(dt, _dateFormat.parseTimes, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } if (utc || timeZoneOffset === 0) { @@ -1175,7 +1253,9 @@ function getDateOfText(dt, preferMonthFirst, utc, timeZoneOffset, dNow) { } } + // @ts-ignore res = Date.parse(dt); + // @ts-ignore if (!isNaN(res)) { return res; } } throw new Error('could not evaluate ' + String(dt) + ' as a valid Date or time.'); @@ -1235,6 +1315,7 @@ const _dateFormat = (function () { date = convertDateTimeZone(date, timeZoneOffset); } + // @ts-ignore mask = String(mask || _dateFormat.isoDateTime); // Allow setting the utc argument via the mask @@ -1257,14 +1338,21 @@ const _dateFormat = (function () { const flags = { d, dd: pad2(d), + // @ts-ignore ddd: dF.i18n.dayNames[D + 7], + // @ts-ignore dddd: dF.i18n.dayNames[D], + // @ts-ignore E: dF.i18n.dayNames[D + 7], + // @ts-ignore EE: dF.i18n.dayNames[D], M: M + 1, MM: pad2(M + 1), + // @ts-ignore MMM: dF.i18n.monthNames[M + 12], + // @ts-ignore MMMM: dF.i18n.monthNames[M], + // @ts-ignore NNN: dF.i18n.monthNames[M], yy: String(y).slice(2), yyyy: y, @@ -1301,8 +1389,10 @@ const _dateFormat = (function () { oo: (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), ooo: utc ? 'Z' : (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), oooo: utc ? 'UTC' : (o > 0 ? '-' : '+') + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + // @ts-ignore S: ['th', 'st', 'nd', 'rd'][d % 10 > 3 ? 0 : (d % 100 - d % 10 !== 10) * d % 10], x: dayDiff, + // @ts-ignore xx: ((dayDiff >= -7) && ((dayDiff + 7) < dF.i18n.dayDiffNames.length)) ? dF.i18n.dayDiffNames[dayDiff + 7] : dF.i18n.dayNames[D] }; @@ -1313,26 +1403,27 @@ const _dateFormat = (function () { })(); // Some common format strings +// @ts-ignore _dateFormat.isoDateTime = 'yyyy-MM-dd\'T\'HH:mm:ss'; +// @ts-ignore _dateFormat.parseDates = { monthFirst : ['MMM d, y', 'MMM d,y', 'M/d/y', 'M-d-y', 'M.d.y', 'MMM-d', 'M/d', 'MMM d', 'M-d'], dateFirst : ['d-MMM-y', 'd/M/y', 'd-M-y', 'd.M.y', 'd-MMM', 'd/M', 'd-M', 'd MMM'], general : ['y-M-d', 'y-MMM-d'] }; +// @ts-ignore _dateFormat.parseTimes = ['h:m:s:lt', 'h:m:s.lt', 'h:m:st', 'h:mt', 'h:m:s t', 'h:m:s.t', 'H:m:s:l', 'H:m:s.l', 'H:m:s', 'H:m', 'h:m:s t Z', 'H:m:s Z']; /** * pre defined formats of a given date - * @param {Date} date - JavaScript Date to format - * @param {string} [format] - format of the date * @param {Array.} [dayNames] - Array of day Names in short and ["Sunday", "Monday", ..., "Mo", "Tu", ...] * @param {Array.} [monthNames] - Array of month Names long and short ["January", "February", ..., "Jan", "Feb", ...] * @param {Array.} [dayDiffNames] - Array of names for relative day, starting 7 days ago ["1 week ago", "6 days ago", ..., "Yesterday", "Today", "Tomorrow", ...] - * @param {bool} [utc] - indicates if the formatted date should be in utc or not * @return {any} returns a number, string or object depending on the given Format */ function initializeParser(dayNames, monthNames, dayDiffNames) { + // @ts-ignore _dateFormat.i18n = { dayNames, monthNames, @@ -1340,6 +1431,7 @@ function initializeParser(dayNames, monthNames, dayDiffNames) { }; } +// @ts-ignore _dateFormat.parse = [ {label: 'Year yy (2 digits)', value: 'yy'}, {label: 'Year yyyy (4 digits)', value: 'yyyy'}, @@ -1371,6 +1463,7 @@ _dateFormat.parse = [ {label: 'AM/PM t (1 digit)', value: 't'}, {label: 'AM/PM tt (2 digits)', value: 'tt'} ]; +// @ts-ignore _dateFormat.format = [ {label: 'Year yyyy (4 digits)', value: 'yyyy'}, {label: 'Year yy (2 digits)', value: 'yy'}, @@ -1412,9 +1505,9 @@ _dateFormat.format = [ /** * pre defined formats of a given date - * @param {Date} date - JavaScript Date to format + * @param {Date | number} date - JavaScript Date to format * @param {string} [format] - format of the date - * @param {bool} [utc] - indicates if the formatted date should be in utc or not + * @param {boolean} [utc] - indicates if the formatted date should be in utc or not * @param {number} [timeZoneOffset] - timezone offset for conversation in minutes * @return {any} returns a number, string or object depending on the given Format */ @@ -1423,15 +1516,23 @@ function getFormattedDateOut(date, format, utc, timeZoneOffset) { if (timeZoneOffset === 0) { utc = true; } + // @ts-ignore format = format || 0; + // @ts-ignore if (date.value) { date = date.value; } + /** @type {Date} */ + let d; if (!(date instanceof Date)) { - date = Date(date); - if (!isValidDate(date)) { + // @ts-ignore + d = Date(date); + if (!isValidDate(d)) { throw new Error(`given Date is not a valid Date!!`); } + } else { + d = date; } + // @ts-ignore if (isNaN(format)) { return _dateFormat(date, String(format), utc, timeZoneOffset); } @@ -1439,129 +1540,129 @@ function getFormattedDateOut(date, format, utc, timeZoneOffset) { switch (Number(format)) { case 0: // timeformat_UNIX - milliseconds since Jan 1, 1970 00:00 if (timeZoneOffset) { - return convertDateTimeZone(date, timeZoneOffset).getTime(); + return convertDateTimeZone(d, timeZoneOffset).getTime(); } - return date.getTime(); + return d.getTime(); case 1: // timeformat_ECMA262 - date as string ECMA-262 case 4: - return date.toUTCString(); + return d.toUTCString(); case 2: // timeformat_local - 26.12.2018, 23:40:45 - timeformat_G - 6/15/2009 1:45:30 PM case 14: if (utc) { - return date.toUTCString(); + return d.toUTCString(); } if (timeZoneOffset) { - return convertDateTimeZone(date, timeZoneOffset).toLocaleString(); + return convertDateTimeZone(d, timeZoneOffset).toLocaleString(); } - return date.toLocaleString(); + return d.toLocaleString(); case 3: // timeformat_localTime - 23:40:58 - timeformat_T - 1:45:30 PM case 13: if (utc) { - return date.toLocaleTimeString([], { timeZone: 'UTC' }); + return d.toLocaleTimeString([], { timeZone: 'UTC' }); } if (timeZoneOffset) { - return convertDateTimeZone(date, timeZoneOffset).toLocaleTimeString(); + return convertDateTimeZone(d, timeZoneOffset).toLocaleTimeString(); } - return date.toLocaleTimeString(); + return d.toLocaleTimeString(); case 12: // timeformat_localDate - 26.12.2018 - timeformat_d - 6/15/2009 case 15: if (utc) { - return date.toLocaleDateString([], { timeZone: 'UTC' }); + return d.toLocaleDateString([], { timeZone: 'UTC' }); } if (timeZoneOffset) { - return convertDateTimeZone(date, timeZoneOffset).toLocaleDateString(); + return convertDateTimeZone(d, timeZoneOffset).toLocaleDateString(); } - return date.toLocaleDateString(); + return d.toLocaleDateString(); case 5: // timeparse_ISO8601 - return date.toISOString(); + return d.toISOString(); case 6: // timeformat_ms - return date.getTime() - (new Date()).getTime(); + return d.getTime() - (new Date()).getTime(); case 7: // timeformat_sec - return Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s); + return Math.round((d.getTime() - (new Date()).getTime()) / TIME_1s); case 8: // timeformat_min - return (Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s) / 60); + return (Math.round((d.getTime() - (new Date()).getTime()) / TIME_1s) / 60); case 9: // timeformat_hour - return (Math.round((date.getTime() - (new Date()).getTime()) / TIME_1s) / 3600); + return (Math.round((d.getTime() - (new Date()).getTime()) / TIME_1s) / 3600); case 10: // timeformat_YYYYMMDDHHMMSS if (utc) { - return Number(date.getUTCFullYear() + - pad2(date.getUTCMonth() + 1) + - pad2(date.getUTCDate()) + - pad2(date.getUTCHours()) + - pad2(date.getUTCMinutes()) + - pad2(date.getUTCSeconds())); + return Number(d.getUTCFullYear() + + pad2(d.getUTCMonth() + 1) + + pad2(d.getUTCDate()) + + pad2(d.getUTCHours()) + + pad2(d.getUTCMinutes()) + + pad2(d.getUTCSeconds())); } if (timeZoneOffset) { - date = convertDateTimeZone(date, timeZoneOffset); + d = convertDateTimeZone(d, timeZoneOffset); } - return Number(date.getFullYear() + - pad2(date.getMonth() + 1) + - pad2(date.getDate()) + - pad2(date.getHours()) + - pad2(date.getMinutes()) + - pad2(date.getSeconds())); + return Number(d.getFullYear() + + pad2(d.getMonth() + 1) + + pad2(d.getDate()) + + pad2(d.getHours()) + + pad2(d.getMinutes()) + + pad2(d.getSeconds())); case 11: // timeformat_YYYYMMDD_HHMMSS if (utc) { - return Number(date.getUTCFullYear() + - pad2(date.getUTCMonth() + 1) + - pad2(date.getUTCDate()) + '.' + - pad2(date.getUTCHours()) + - pad2(date.getUTCMinutes()) + - pad2(date.getUTCSeconds())); + return Number(d.getUTCFullYear() + + pad2(d.getUTCMonth() + 1) + + pad2(d.getUTCDate()) + '.' + + pad2(d.getUTCHours()) + + pad2(d.getUTCMinutes()) + + pad2(d.getUTCSeconds())); } if (timeZoneOffset) { - date = convertDateTimeZone(date, timeZoneOffset); + d = convertDateTimeZone(d, timeZoneOffset); } - return Number(date.getFullYear() + - pad2(date.getMonth() + 1) + - pad2(date.getDate()) + '.' + - pad2(date.getHours()) + - pad2(date.getMinutes()) + - pad2(date.getSeconds())); + return Number(d.getFullYear() + + pad2(d.getMonth() + 1) + + pad2(d.getDate()) + '.' + + pad2(d.getHours()) + + pad2(d.getMinutes()) + + pad2(d.getSeconds())); case 16: // timeformat_weekday - Montag, 22.12. - return _dateFormat(date, 'dddd, d.M.', utc, timeZoneOffset); + return _dateFormat(d, 'dddd, d.M.', utc, timeZoneOffset); case 17: // timeformat_weekday2 - heute 22.12., morgen 23.12., übermorgen 24.12., in 3 Tagen 25.12., Montag, 26.12. - return _dateFormat(date, 'xx, d.M.', utc, timeZoneOffset); - case 18: { // customISOstring(date, offset) - date = new Date(date); // copy instance + return _dateFormat(d, 'xx, d.M.', utc, timeZoneOffset); + case 18: { // customISOstring(d, offset) + d = new Date(d); // copy instance let offset = 0; if (!utc) { if (!timeZoneOffset) { - offset = date.getTimezoneOffset(); + offset = d.getTimezoneOffset(); } else { offset = timeZoneOffset; } } const h = Math.floor(Math.abs(offset) / 60); const m = Math.abs(offset) % 60; - date.setMinutes(date.getMinutes() - offset); // apply custom timezone - return date.getUTCFullYear() + '-' // return custom format - + pad2(date.getUTCMonth() + 1) + '-' - + pad2(date.getUTCDate()) + 'T' - + pad2(date.getUTCHours()) + ':' - + pad2(date.getUTCMinutes()) + ':' - + pad2(date.getUTCSeconds()) + d.setMinutes(d.getMinutes() - offset); // apply custom timezone + return d.getUTCFullYear() + '-' // return custom format + + pad2(d.getUTCMonth() + 1) + '-' + + pad2(d.getUTCDate()) + 'T' + + pad2(d.getUTCHours()) + ':' + + pad2(d.getUTCMinutes()) + ':' + + pad2(d.getUTCSeconds()) + (offset === 0 ? 'Z' : (offset < 0 ? '+' : '-') + pad2(h) + ':' + pad2(m)); } case 19: // workweek - return getWeekOfYear(date)[1]; + return getWeekOfYear(d)[1]; case 20: // workweek even - return !(getWeekOfYear(date)[1] % 2); // eslint-disable-line no-extra-boolean-cast + return !(getWeekOfYear(d)[1] % 2); // eslint-disable-line no-extra-boolean-cast case 21: // day of year - return getDayOfYear(date)[1]; // eslint-disable-line no-extra-boolean-cast + return getDayOfYear(d)[1]; // eslint-disable-line no-extra-boolean-cast case 22: // day of year even - return !(getDayOfYear(date)[1] % 2); // eslint-disable-line no-extra-boolean-cast + return !(getDayOfYear(d)[1] % 2); // eslint-disable-line no-extra-boolean-cast } const dNow = new Date(); - const delay = (date.getTime() - dNow.getTime()); - const weekOfYear = getWeekOfYear(date); - const dayOfYear = getDayOfYear(date); + const delay = (d.getTime() - dNow.getTime()); + const weekOfYear = getWeekOfYear(d); + const dayOfYear = getDayOfYear(d); return { - date, - ts: date.getTime(), - timeUTCStr: date.toUTCString(), - timeISOStr: date.toISOString(), + d, + ts: d.getTime(), + timeUTCStr: d.toUTCString(), + timeISOStr: d.toISOString(), delay, delaySec: Math.round(delay / TIME_1s), lc: dNow.getTime(), @@ -1648,7 +1749,7 @@ function getFormattedDateOut(date, format, utc, timeZoneOffset) { /** * check if string is integer * @param {any} val value to check - * @return {bool} **true** if value is integer otherwise **false** + * @return {boolean} **true** if value is integer otherwise **false** */ function _isInteger(val) { const digits = '1234567890'; @@ -1757,12 +1858,15 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset, dNow) { x = 2; y = 4; } + // @ts-ignore year = _getInt(val, i_val, x, y); if (year === null && typeof year !== 'undefined') { return { value: null, error: 'invalid year of format "' + token + '"'}; } + // @ts-ignore i_val += year.length; + // @ts-ignore if (year.length === 2) { if (year > 70) { year = 1900 + (year - 0); @@ -1772,7 +1876,9 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset, dNow) { } } else if (token === 'MMM' || token === 'NNN' || token === 'MMMM') { month = 0; + // @ts-ignore for (let i = 0; i < _dateFormat.i18n.monthNames.length; i++) { + // @ts-ignore const month_name = _dateFormat.i18n.monthNames[i]; if (val.substr(i_val, month_name.length).toLowerCase() === month_name.toLowerCase()) { if (token === 'MMM' || ((token === 'NNN' || token === 'MMMM') && i > 11)) { @@ -1791,7 +1897,9 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset, dNow) { return { value: null, error: 'invalidmonth "' + month + '" of format "' + token + '"' }; } } else if (token === 'EE' || token === 'E' || token === 'dddd' || token === 'ddd') { + // @ts-ignore for (let i = 0; i < _dateFormat.i18n.dayNames.length; i++) { + // @ts-ignore const day_name = _dateFormat.i18n.dayNames[i]; if (val.substr(i_val, day_name.length).toLowerCase() === day_name.toLowerCase()) { i_val += day_name.length; @@ -1799,59 +1907,77 @@ function _getDateFromFormat(val, format, utc, timeZoneOffset, dNow) { } } } else if (token === 'MM' || token === 'M') { + // @ts-ignore month = _getInt(val, i_val, token.length, 2); if (month === null || typeof month === 'undefined' || (month < 1) || (month > 12)) { return { value: null, error: 'invalid month "' + month + '" of format "' + token + '"' }; } + // @ts-ignore i_val += month.length; } else if (token === 'dd' || token === 'd') { + // @ts-ignore date = _getInt(val, i_val, token.length, 2); if (date === null || typeof date === 'undefined' || (date < 1) || (date > 31)) { return { value: null, error: 'invalid date "' + date + '"' }; } + // @ts-ignore i_val += date.length; } else if (token === 'hh' || token === 'h') { + // @ts-ignore hour = _getInt(val, i_val, token.length, 2); if (hour === null || typeof hour === 'undefined' || (hour < 1) || (hour > 12)) { return { value: null, error: 'invalid hour "' + hour + '" of format "' + token + '"' }; } + // @ts-ignore i_val += hour.length; } else if (token === 'HH' || token === 'H') { + // @ts-ignore hour = _getInt(val, i_val, token.length, 2); if (hour === null || typeof hour === 'undefined' || (hour < 0) || (hour > 23)) { return { value: null, error: 'invalid hour "' + hour + '" of format "' + token + '"' }; } + // @ts-ignore i_val += hour.length; } else if (token === 'kk' || token === 'k') { + // @ts-ignore hour = _getInt(val, i_val, token.length, 2); if (hour === null || typeof hour === 'undefined' || (hour < 0) || (hour > 11)) { return { value: null, error: 'invalid hour "' + hour + '" of format "' + token + '"' }; } + // @ts-ignore i_val += hour.length; } else if (token === 'KK' || token === 'K') { + // @ts-ignore hour = _getInt(val, i_val, token.length, 2); if (hour === null || typeof hour === 'undefined' || (hour < 1) || (hour > 24)) { return { value: null, error: 'invalid hour "' + hour + '" of format "' + token + '"' }; } + // @ts-ignore i_val += hour.length; hour--; } else if (token === 'mm' || token === 'm') { + // @ts-ignore min = _getInt(val, i_val, token.length, 2); if (min === null || typeof min === 'undefined' || (min < 0) || (min > 59)) { return { value: null, error: 'invalid hour "' + hour + '" of format "' + token + '"' }; } + // @ts-ignore i_val += min.length; } else if (token === 'ss' || token === 's') { + // @ts-ignore sec = _getInt(val, i_val, token.length, 2); if (sec === null || typeof sec === 'undefined' || (sec < 0) || (sec > 59)) { return { value: null, error: 'invalid sec "' + sec + '" of format "' + token + '"' }; } + // @ts-ignore i_val += sec.length; } else if (token.toLowerCase() === 'lll' || token.toLowerCase() === 'll' || token.toLowerCase() === 'l') { + // @ts-ignore misec = _getInt(val, i_val, token.length, 3); if (misec === null || typeof misec === 'undefined' || (misec < 0) || (misec > 999)) { return { value: null, error: 'invalid millisecond "' + misec + '" of format "' + token + '"' }; } + // @ts-ignore i_val += misec.length; if ( token === 'L' && misec < 10) { misec = misec * 100; @@ -1973,10 +2099,13 @@ function _isTimestamp(str) { */ function _parseDate(val, preferMonthFirst, utc, timeZoneOffset, dNow) { // console.debug('_parseDate val=' + val + ' - preferMonthFirst=' + preferMonthFirst); // eslint-disable-line + // @ts-ignore let res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.monthFirst : _dateFormat.parseDates.dateFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } + // @ts-ignore res = _parseArray(val, (preferMonthFirst) ? _dateFormat.parseDates.dateFirst : _dateFormat.parseDates.monthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } + // @ts-ignore return _parseArray(val, _dateFormat.parseDates.general, utc, timeZoneOffset, dNow); } @@ -2002,21 +2131,27 @@ function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset, dNow) { return result; } + // @ts-ignore let checkList = [_dateFormat.isoDateTime]; if (preferMonthFirst) { + // @ts-ignore checkList = mix(_dateFormat.parseDates.monthFirst, _dateFormat.parseTimes, checkList); + // @ts-ignore checkList = mix(_dateFormat.parseDates.dateFirst, _dateFormat.parseTimes, checkList); } else { + // @ts-ignore checkList = mix(_dateFormat.parseDates.dateFirst, _dateFormat.parseTimes, checkList); + // @ts-ignore checkList = mix(_dateFormat.parseDates.monthFirst, _dateFormat.parseTimes, checkList); } + // @ts-ignore checkList = mix(_dateFormat.parseDates.general, _dateFormat.parseTimes, checkList); return _parseArray(val, checkList, utc, timeZoneOffset, dNow); } /** * parses a date string to given format definition - * @param {string} val date string to parse + * @param {string} date date string to parse * @param {number|string} format Format definition, if it is a number a predefined format will be try * @param {Array.} [dayNames] list of day names * @param {Array.} [monthNames] list of month names @@ -2029,14 +2164,17 @@ function _parseDateTime(val, preferMonthFirst, utc, timeZoneOffset, dNow) { function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, utc, timeZoneOffset, dNow) { // console.debug('parseDateFromFormat date=' + util.inspect(date, { colors: true, compact: 10, breakLength: Infinity }) + ' - format=' + util.inspect(format, { colors: true, compact: 10, breakLength: Infinity }) + ' [' + dayNames + '] - [' + monthNames + '] [' + dayDiffNames + ']'); // eslint-disable-line if (dayNames) { + // @ts-ignore _dateFormat.i18n.dayNames = dayNames; } if (monthNames) { + // @ts-ignore _dateFormat.i18n.monthNames = monthNames; } if (dayDiffNames) { + // @ts-ignore _dateFormat.i18n.dayDiffNames = dayDiffNames; } @@ -2047,7 +2185,9 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u let res = null; + // @ts-ignore if (isNaN(format)) { // timeparse_TextOther + // @ts-ignore res = _getDateFromFormat(date, format, utc, timeZoneOffset, dNow); if (res.error) { throw new Error('could not evaluate format of ' + date + ' (' + format + ') - ' + res.error); @@ -2060,9 +2200,12 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u if (res !== null && typeof res !== 'undefined') { return res; } res = _parseDate(val, preferMonthFirst, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } + // @ts-ignore res = _parseArray(val, _dateFormat.parseTimes, utc, timeZoneOffset, dNow); if (res !== null && typeof res !== 'undefined') { return res; } + // @ts-ignore res = Date.parse(val); + // @ts-ignore if (!isNaN(res)) { return new Date(res); } @@ -2093,13 +2236,13 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u break; case 4: {// timeformat_YYYYMMDDHHMMSS date = String(date); - const year = date.substr(0, 4); - const month = date.substr(4, 2); - const day = date.substr(6, 2); - const hours = date.substr(8, 2); - const mins = date.substr(10, 2); - const secs = date.substr(12, 2); - const mss = date.substr(14); + const year = parseInt(date.substr(0, 4)); + const month = parseInt(date.substr(4, 2)); + const day = parseInt(date.substr(6, 2)); + const hours = parseInt(date.substr(8, 2)); + const mins = parseInt(date.substr(10, 2)); + const secs = parseInt(date.substr(12, 2)); + const mss = parseInt(date.substr(14)); if (utc) { res = Date.UTC(year, month, day, hours, mins, secs, mss); } else if (timeZoneOffset) { @@ -2111,13 +2254,13 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u } case 5: { // timeformat_YYYYMMDD_HHMMSS date = String(date); - const year = date.substr(0, 4); - const month = date.substr(4, 2); - const day = date.substr(6, 2); - const hours = date.substr(9, 2); - const mins = date.substr(11, 2); - const secs = date.substr(13, 2); - const mss = date.substr(15); + const year = parseInt(date.substr(0, 4)); + const month = parseInt(date.substr(4, 2)); + const day = parseInt(date.substr(6, 2)); + const hours = parseInt(date.substr(9, 2)); + const mins = parseInt(date.substr(11, 2)); + const secs = parseInt(date.substr(13, 2)); + const mss = parseInt(date.substr(15)); if (utc) { res = Date.UTC(year, month, day, hours, mins, secs, mss); } else if (timeZoneOffset) { @@ -2128,6 +2271,7 @@ function parseDateFromFormat(date, format, dayNames, monthNames, dayDiffNames, u break; } default: { + // @ts-ignore res = getDateOfText(date); break; } diff --git a/nodes/lib/suncalc.js b/nodes/lib/suncalc.js index 4ad4e04..7544376 100644 --- a/nodes/lib/suncalc.js +++ b/nodes/lib/suncalc.js @@ -1,3 +1,4 @@ +// @ts-check /* (c) 2011-2015, Vladimir Agafonkin SunCalc is a JavaScript library for calculating sun/moon position and light phases. @@ -8,6 +9,167 @@ 'use strict'; const util = require('util'); // eslint-disable-line no-unused-vars +/** +* @typedef {Object} ISunTimeDefRed +* @property {string} name - The Name of the time +* @property {Date} value - Date object with the calculated sun-time +* @property {number} ts - The time as unix timestamp +* @property {number} pos - The position of the sun on the time +* @property {number} julian - The time as julian calendar +* @property {boolean} valid - indicates if the time is valid or not +*/ + +/** +* @typedef {Object} ISunTimeDef +* @property {string} name - The Name of the time +* @property {Date} value - Date object with the calculated sun-time +* @property {number} ts - The time as unix timestamp +* @property {number} pos - The position of the sun on the time +* @property {number} elevation - Angle of the sun on the time +* @property {number} julian - The time as julian calendar +* @property {boolean} valid - indicates if the time is valid or not +* @property {boolean} [deprecated] - indicates if the time is a deprecated time name +* @property {number} [posOrg] - if it is a deprecated name, the original position +*/ + +/** +* @typedef {Object} ISunTimeSingle +* @property {ISunTimeDef} rise - The sun-time for the solar noon (sun is in the highest position) +* @property {ISunTimeDef} set - The sun-time for nadir (darkest moment of the night, sun is in the lowest position) +* @property {string} [error] - string of an error message if an error occurs +*/ + +/** +* @typedef {Object} ISunTimeList +* @property {ISunTimeDefRed} solarNoon - The sun-time for the solar noon (sun is in the highest position) +* @property {ISunTimeDefRed} nadir - The sun-time for nadir (darkest moment of the night, sun is in the lowest position) +* @property {ISunTimeDef} goldenHourDawnStart - The sun-time for morning golden hour (soft light, best time for photography) +* @property {ISunTimeDef} goldenHourDawnEnd - The sun-time for morning golden hour (soft light, best time for photography) +* @property {ISunTimeDef} goldenHourDuskStart - The sun-time for evening golden hour starts +* @property {ISunTimeDef} goldenHourDuskEnd - The sun-time for evening golden hour starts +* @property {ISunTimeDef} sunriseStart - The sun-time for sunrise starts (top edge of the sun appears on the horizon) +* @property {ISunTimeDef} sunriseEnd - The sun-time for sunrise ends (bottom edge of the sun touches the horizon) +* @property {ISunTimeDef} sunsetStart - The sun-time for sunset starts (bottom edge of the sun touches the horizon) +* @property {ISunTimeDef} sunsetEnd - The sun-time for sunset ends (sun disappears below the horizon, evening civil twilight starts) +* @property {ISunTimeDef} blueHourDawnStart - The sun-time for blue Hour start (time for special photography photos starts) +* @property {ISunTimeDef} blueHourDawnEnd - The sun-time for blue Hour end (time for special photography photos end) +* @property {ISunTimeDef} blueHourDuskStart - The sun-time for blue Hour start (time for special photography photos starts) +* @property {ISunTimeDef} blueHourDuskEnd - The sun-time for blue Hour end (time for special photography photos end) +* @property {ISunTimeDef} civilDawn - The sun-time for dawn (morning nautical twilight ends, morning civil twilight starts) +* @property {ISunTimeDef} civilDusk - The sun-time for dusk (evening nautical twilight starts) +* @property {ISunTimeDef} nauticalDawn - The sun-time for nautical dawn (morning nautical twilight starts) +* @property {ISunTimeDef} nauticalDusk - The sun-time for nautical dusk end (evening astronomical twilight starts) +* @property {ISunTimeDef} amateurDawn - The sun-time for amateur astronomical dawn (sun at 12° before sunrise) +* @property {ISunTimeDef} amateurDusk - The sun-time for amateur astronomical dusk (sun at 12° after sunrise) +* @property {ISunTimeDef} astronomicalDawn - The sun-time for night ends (morning astronomical twilight starts) +* @property {ISunTimeDef} astronomicalDusk - The sun-time for night starts (dark enough for astronomical observations) +* @property {ISunTimeDef} [dawn] - Deprecated: alternate for civilDawn +* @property {ISunTimeDef} [dusk] - Deprecated: alternate for civilDusk +* @property {ISunTimeDef} [nightEnd] - Deprecated: alternate for astronomicalDawn +* @property {ISunTimeDef} [night] - Deprecated: alternate for astronomicalDusk +* @property {ISunTimeDef} [nightStart] - Deprecated: alternate for astronomicalDusk +* @property {ISunTimeDef} [goldenHour] - Deprecated: alternate for goldenHourDuskStart +* @property {ISunTimeDef} [sunset] - Deprecated: alternate for sunsetEnd +* @property {ISunTimeDef} [sunrise] - Deprecated: alternate for sunriseStart +* @property {ISunTimeDef} [goldenHourEnd] - Deprecated: alternate for goldenHourDawnEnd +* @property {ISunTimeDef} [goldenHourStart] - Deprecated: alternate for goldenHourDuskStart +*/ + +/** + * @typedef ISunTimeNames + * @type {Object} + * @property {number} angle - angle of the sun position + * @property {string} riseName - name of sun rise + * @property {string} setName - name of sun set + * @property {number} [risePos] - (optional) position at rise + * @property {number} [setPos] - (optional) position at set + */ + + +/** + * @typedef {Object} ISunCoordinates + * @property {number} dec - The declination of the sun + * @property {number} ra - The right ascension of the sun + */ + +/** + * @typedef {Object} ISunPosition + * @property {number} azimuth - The azimuth of the sun in radians + * @property {number} altitude - The altitude of the sun in radians + * @property {number} zenith - The zenith of the sun in radians + * @property {number} azimuthDegrees - The azimuth of the sun in decimal degree + * @property {number} altitudeDegrees - The altitude of the sun in decimal degree + * @property {number} zenithDegrees - The zenith of the sun in decimal degree + * @property {number} declination - The declination of the sun + */ + +/** + * @typedef {Object} IMoonPosition + * @property {number} azimuth - The azimuth of the moon + * @property {number} altitude - The altitude of the moon + * @property {number} azimuthDegrees - The azimuth of the moon in degree + * @property {number} altitudeDegrees - The altitude of the moon in degree + * @property {number} distance - The distance of the moon to the earth + * @property {number} parallacticAngle - The parallactic angle of the moon + * @property {number} parallacticAngleDegrees - The parallactic angle of the moon in degree + */ + + +/** + * @typedef {Object} IDateObj + * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ + * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC + */ + +/** + * @typedef {Object} IPhaseObj + * @property {number} from - The phase start + * @property {number} to - The phase end + * @property {('newMoon'|'waxingCrescentMoon'|'firstQuarterMoon'|'waxingGibbousMoon'|'fullMoon'|'waningGibbousMoon'|'thirdQuarterMoon'|'waningCrescentMoon')} id - id of the phase + * @property {string} emoji - unicode symbol of the phase + * @property {string} name - name of the phase + * @property {string} id - phase name + * @property {number} weight - weight of the phase + * @property {string} css - a css value of the phase + * @property {string} [nameAlt] - an alernate name (not used by this library) + * @property {string} [tag] - additional tag (not used by this library) + */ + +/** + * @typedef {Object} IMoonIlluminationNext + * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ of the next phase + * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC of the next phase + * @property {string} type - The name of the next phase [newMoon, fullMoon, firstQuarter, thirdQuarter] + * @property {IDateObj} newMoon - Date of the next new moon + * @property {IDateObj} fullMoon - Date of the next full moon + * @property {IDateObj} firstQuarter - Date of the next first quater of the moon + * @property {IDateObj} thirdQuarter - Date of the next third/last quater of the moon + */ + +/** + * @typedef {Object} IMoonIllumination + * @property {number} fraction - The fraction of the moon + * @property {IPhaseObj} phase - The phase of the moon + * @property {number} phaseValue - The phase of the moon in the current cycle + * @property {number} angle - The angle of the moon + * @property {IMoonIlluminationNext} next - object containing information about the next phases of the moon + */ +/** + * @typedef {Object} IMoonDataInst + * @property {number} zenithAngle - The zenith angle of the moon + * @property {IMoonIllumination} illumination - object containing information about the next phases of the moon + * + * @typedef {IMoonPosition & IMoonDataInst} IMoonData + */ + +/** + * @typedef {Object} IMoonTimes + * @property {Date|NaN} rise - a Date object if the moon is rising on the given Date, otherwhise NaN + * @property {Date|NaN} set - a Date object if the moon is setting on the given Date, otherwhise NaN + * @property {boolean} alwaysUp - is true if the moon in always up, oitherwise property not exists + * @property {boolean} alwaysDown - is true if the moon in always up, oitherwise property not exists + */ + (function () { 'use strict'; // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas @@ -123,7 +285,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars /** * convert date from Julian calendar - * @param {number} day nmber in julian calendar to convert + * @param {number} j - day number in julian calendar to convert * @return {number} result date as unix timestamp */ function fromJulianDay(j) { @@ -231,16 +393,10 @@ const util = require('util'); // eslint-disable-line no-unused-vars return M + C + P + Math.PI; } - /** - * @typedef {Object} sunCoordsObj - * @property {number} dec - The declination of the sun - * @property {number} ra - The right ascension of the sun - */ - /** * sun coordinates * @param {number} d days in julian calendar - * @returns {sunCoordsObj} + * @returns {ISunCoordinates} */ function sunCoords(d) { const M = solarMeanAnomaly(d); @@ -254,23 +410,12 @@ const util = require('util'); // eslint-disable-line no-unused-vars const SunCalc = {}; - /** - * @typedef {Object} sunposition - * @property {number} azimuth - The azimuth of the sun in radians - * @property {number} altitude - The altitude of the sun in radians - * @property {number} zenith - The zenith of the sun in radians - * @property {number} azimuthDegrees - The azimuth of the sun in decimal degree - * @property {number} altitudeDegrees - The altitude of the sun in decimal degree - * @property {number} zenithDegrees - The zenith of the sun in decimal degree - * @property {number} declination - The declination of the sun - */ - /** * calculates sun position for a given date and latitude/longitude * @param {number} dateValue date as unix timestamp for calculating sun-position * @param {number} lat latitude for calculating sun-position * @param {number} lng longitude for calculating sun-position - * @return {sunposition} result object of sun-position + * @return {ISunPosition} result object of sun-position */ SunCalc.getPosition = function (dateValue, lat, lng) { // console.log(`getPosition dateValue=${dateValue} lat=${lat}, lng=${lng}`); @@ -300,68 +445,25 @@ const util = require('util'); // eslint-disable-line no-unused-vars }; }; - /** - * @typedef {Object} suntime - * @property {string} name - The Name of the time - * @property {Date} value - Date object with the calculated sun-time - * @property {number} ts - The time as unix timestamp - * @property {number} pos - The position of the sun on the time - * @property {number} angle - Angle of the sun on the time - * @property {number} julian - The time as julian calendar - * @property {boolean} valid - indicates if the time is valid or not - */ - - /** - * @typedef {Object} suntimes - * @property {suntime} solarNoon - The sun-time for the solar noon (sun is in the highest position) - * @property {suntime} nadir - The sun-time for nadir (darkest moment of the night, sun is in the lowest position) - * @property {suntime} goldenHourDawnStart - The sun-time for morning golden hour (soft light, best time for photography) - * @property {suntime} goldenHourDawnEnd - The sun-time for morning golden hour (soft light, best time for photography) - * @property {suntime} goldenHourDuskStart - The sun-time for evening golden hour starts - * @property {suntime} goldenHourDuskEnd - The sun-time for evening golden hour starts - * @property {suntime} sunriseStart - The sun-time for sunrise starts (top edge of the sun appears on the horizon) - * @property {suntime} sunriseEnd - The sun-time for sunrise ends (bottom edge of the sun touches the horizon) - * @property {suntime} sunsetStart - The sun-time for sunset starts (bottom edge of the sun touches the horizon) - * @property {suntime} sunsetEnd - The sun-time for sunset ends (sun disappears below the horizon, evening civil twilight starts) - * @property {suntime} blueHourDawnStart - The sun-time for blue Hour start (time for special photography photos starts) - * @property {suntime} blueHourDawnEnd - The sun-time for blue Hour end (time for special photography photos end) - * @property {suntime} blueHourDuskStart - The sun-time for blue Hour start (time for special photography photos starts) - * @property {suntime} blueHourDuskEnd - The sun-time for blue Hour end (time for special photography photos end) - * @property {suntime} civilDawn - The sun-time for dawn (morning nautical twilight ends, morning civil twilight starts) - * @property {suntime} civilDusk - The sun-time for dusk (evening nautical twilight starts) - * @property {suntime} nauticalDawn - The sun-time for nautical dawn (morning nautical twilight starts) - * @property {suntime} nauticalDusk - The sun-time for nautical dusk end (evening astronomical twilight starts) - * @property {suntime} amateurDawn - The sun-time for amateur astronomical dawn (sun at 12° before sunrise) - * @property {suntime} amateurDusk - The sun-time for amateur astronomical dusk (sun at 12° after sunrise) - * @property {suntime} astronomicalDawn - The sun-time for night ends (morning astronomical twilight starts) - * @property {suntime} astronomicalDusk - The sun-time for night starts (dark enough for astronomical observations) - * @property {suntime} [dawn] - Deprecated: alternate for civilDawn - * @property {suntime} [dusk] - Deprecated: alternate for civilDusk - * @property {suntime} [nightEnd] - Deprecated: alternate for astronomicalDawn - * @property {suntime} [night] - Deprecated: alternate for astronomicalDusk - * @property {suntime} [nightStart] - Deprecated: alternate for astronomicalDusk - * @property {suntime} [goldenHour] - Deprecated: alternate for goldenHourDuskStart - * @property {suntime} [sunset] - Deprecated: alternate for sunsetEnd - * @property {suntime} [sunrise] - Deprecated: alternate for sunriseStart - * @property {suntime} [goldenHourEnd] - Deprecated: alternate for goldenHourDawnEnd - * @property {suntime} [goldenHourStart] - Deprecated: alternate for goldenHourDuskStart - */ - - /** sun times configuration (angle, morning name, evening name) */ + /** sun times configuration + * @type {Array.} + */ const sunTimes = SunCalc.times = [ - [6, 'goldenHourDawnEnd', 'goldenHourDuskStart'], // GOLDEN_HOUR_2 - [-0.3, 'sunriseEnd', 'sunsetStart'], // SUNRISE_END - [-0.833, 'sunriseStart', 'sunsetEnd'], // SUNRISE - [-1, 'goldenHourDawnStart', 'goldenHourDuskEnd'], // GOLDEN_HOUR_1 - [-4, 'blueHourDawnEnd', 'blueHourDuskStart'], // BLUE_HOUR - [-6, 'civilDawn', 'civilDusk'], // DAWN - [-8, 'blueHourDawnStart', 'blueHourDuskEnd'], // BLUE_HOUR - [-12, 'nauticalDawn', 'nauticalDusk'], // NAUTIC_DAWN - [-15, 'amateurDawn', 'amateurDusk'], - [-18, 'astronomicalDawn', 'astronomicalDusk'] // ASTRO_DAWN + { angle: 6, riseName: 'goldenHourDawnEnd', setName: 'goldenHourDuskStart'}, // GOLDEN_HOUR_2 + { angle: -0.3, riseName: 'sunriseEnd', setName: 'sunsetStart'}, // SUNRISE_END + { angle: -0.833, riseName: 'sunriseStart', setName: 'sunsetEnd'}, // SUNRISE + { angle: -1, riseName: 'goldenHourDawnStart', setName: 'goldenHourDuskEnd'}, // GOLDEN_HOUR_1 + { angle: -4, riseName: 'blueHourDawnEnd', setName: 'blueHourDuskStart'}, // BLUE_HOUR + { angle: -6, riseName: 'civilDawn', setName: 'civilDusk'}, // DAWN + { angle: -8, riseName: 'blueHourDawnStart', setName: 'blueHourDuskEnd'}, // BLUE_HOUR + { angle: -12, riseName: 'nauticalDawn', setName: 'nauticalDusk'}, // NAUTIC_DAWN + { angle: -15, riseName: 'amateurDawn', setName: 'amateurDusk'}, + { angle: -18, riseName: 'astronomicalDawn', setName: 'astronomicalDusk'} // ASTRO_DAWN ]; - /** alternate time names for backward compatibility */ + /** alternate time names for backward compatibility + * @type {Array.<[string, string]>} + */ const sunTimesAlternate = SunCalc.timesAlternate = [ ['dawn', 'civilDawn'], ['dusk', 'civilDusk'], @@ -377,7 +479,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars /** adds a custom time to the times config */ SunCalc.addTime = function (angle, riseName, setName, risePos, setPos) { - sunTimes.push([angle, riseName, setName, risePos, setPos]); + sunTimes.push({angle, riseName, setName, risePos, setPos}); }; // calculations for sun times @@ -451,7 +553,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars * @param {number} lat latitude for calculating sun-times * @param {number} lng longitude for calculating sun-times * @param {boolean} [inUTC] defines if the calculation should be in utc or local time (default is local) - * @return {suntimes} result object of sunTime + * @return {ISunTimeList} result object of sunTime */ SunCalc.getSunTimes = function (dateValue, lat, lng, noDeprecated, inUTC) { // console.log(`getSunTimes dateValue=${dateValue} lat=${lat}, lng=${lng}, noDeprecated=${noDeprecated}`); @@ -501,7 +603,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars }; for (let i = 0, len = sunTimes.length; i < len; i += 1) { const time = sunTimes[i]; - const sa = time[0]; + const sa = time.angle; let valid = true; let Jset = getSetJ(sa * rad, lw, phi, dec, n, M, L); @@ -520,7 +622,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars const v1 = fromJulianDay(Jset); const v2 = fromJulianDay(Jrise); - result[time[2]] = { + result[time.setName] = { value: new Date(v1), ts: v1, name: time[2], @@ -529,7 +631,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars valid, pos: len + i + 1 }; - result[time[1]] = { + result[time.riseName] = { value: new Date(v2), ts: v2, name: time[1], @@ -550,6 +652,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars result[time[0]].pos = -2; } } + // @ts-ignore return result; }; @@ -561,7 +664,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars * @param {number} elevationAngle sun angle for calculating sun-time * @param {boolean} [degree] defines if the elevationAngle is in degree not in radians * @param {boolean} [inUTC] defines if the calculation should be in utc or local time (default is local) - * @return {suntimes} result object of sunTime + * @return {ISunTimeSingle} result object of single sunTime ***/ SunCalc.getSunTime = function (dateValue, lat, lng, elevationAngle, degree, inUTC) { // console.log(`getSunTime dateValue=${dateValue} lat=${lat}, lng=${lng}, elevationAngle=${elevationAngle}`); @@ -600,30 +703,34 @@ const util = require('util'); // eslint-disable-line no-unused-vars return { set: { + name: 'set', value: new Date(v1), ts: v1, elevation: elevationAngle, julian: Jset, - valid: !isNaN(Jset) + valid: !isNaN(Jset), + pos: 0 }, rise: { + name: 'rise', value: new Date(v2), ts: v2, elevation: elevationAngle, // (180 + (elevationAngle * -1)), julian: Jrise, - valid: !isNaN(Jrise) + valid: !isNaN(Jrise), + pos: 1 } }; }; /** * calculates time for a given azimuth angle for a given date and latitude/longitude - * @param {number} date start date for calculating sun-position + * @param {Date} date start date for calculating sun-position * @param {number} nazimuth azimuth for calculating sun-position * @param {number} lat latitude for calculating sun-position * @param {number} lng longitude for calculating sun-position * @param {boolean} [degree] true if the angle is in degree and not in rad - * @return {sunposition} result object of sun-position + * @return {Date} result time of sun-position */ SunCalc.getSunTimeByAzimuth = function (date, lat, lng, nazimuth, degree) { if (isNaN(nazimuth)) { @@ -683,20 +790,12 @@ const util = require('util'); // eslint-disable-line no-unused-vars }; } - /** - * @typedef {Object} moonposition - * @property {number} azimuth - The azimuth of the moon - * @property {number} altitude - The altitude of the moon - * @property {number} distance - The distance of the moon to the earth - * @property {number} parallacticAngle - The parallactic angle of the moon - */ - /** * calculates moon position for a given date and latitude/longitude * @param {number} dateValue date as unix timestamp for calculating moon-position * @param {number} lat latitude for calculating moon-position * @param {number} lng longitude for calculating moon-position - * @return {moonposition} result object of moon-position + * @return {IMoonPosition} result object of moon-position */ SunCalc.getMoonPosition = function (dateValue, lat, lng) { // console.log(`getMoonPosition dateValue=${dateValue} lat=${lat}, lng=${lng}`); @@ -731,50 +830,12 @@ const util = require('util'); // eslint-disable-line no-unused-vars }; }; - /** - * @typedef {Object} dateData - * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ - * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC - */ - - /** - * @typedef {Object} phaseObj - * @property {number} from - The phase start - * @property {number} to - The phase end - * @property {string} id - id of the phase - * @property {string} emoji - unicode symbol of the phase - * @property {string} name - name of the phase - * @property {string} id - phase name - * @property {number} weight - weight of the phase - * @property {string} css - a css value of the phase - */ - - /** - * @typedef {Object} nextmoonillum - * @property {number} fraction - The fraction of the moon - * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ of the next phase - * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC of the next phase - * @property {string} type - The name of the next phase [newMoon, fullMoon, firstQuarter, thirdQuarter] - * @property {dateData} newMoon - Date of the next new moon - * @property {dateData} fullMoon - Date of the next full moon - * @property {dateData} firstQuarter - Date of the next first quater of the moon - * @property {dateData} thirdQuarter - Date of the next third/last quater of the moon - */ - - /** - * @typedef {Object} moonillumination - * @property {number} fraction - The fraction of the moon - * @property {phaseObj} phase - The phase of the moon - * @property {number} phaseValue - The phase of the moon in the current cycle - * @property {nextmoonillum} next - object containing information about the next phases of the moon - */ - /** * calculations for illumination parameters of the moon, * based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and * Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. * @param {number} dateValue date as unix timestamp for calculating moon-illumination - * @return {moonillumination} result object of moon-illumination + * @return {IMoonIllumination} result object of moon-illumination */ SunCalc.getMoonIllumination = function (dateValue) { // console.log(`getMoonIllumination dateValue=${dateValue}`); @@ -805,7 +866,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars // Calculate the fraction of the moon cycle // const currentfrac = cycleModMs / lunarDaysMs; const next = Math.min(nextNewMoon, nextFirstQuarter, nextFullMoon, nextThirdQuarter); - let phase = ''; + let phase; for (let index = 0; index < fractionOfTheMoonCycle.length; index++) { const element = fractionOfTheMoonCycle[index]; @@ -818,8 +879,10 @@ const util = require('util'); // eslint-disable-line no-unused-vars return { fraction: (1 + cos(inc)) / 2, // fraction2: cycleModMs / lunarDaysMs, + // @ts-ignore phase, phaseValue, + angle, next : { value: next, date: (new Date(next)).toISOString(), @@ -844,6 +907,22 @@ const util = require('util'); // eslint-disable-line no-unused-vars }; }; + /** + * calculations moon position and illumination for a given date and latitude/longitude of the moon, + * @param {number} dateValue date as unix timestamp for calculating moon-illumination + * @param {number} lat latitude for calculating moon-position + * @param {number} lng longitude for calculating moon-position + * @return {IMoonData} result object of moon-illumination + */ + SunCalc.getMoonData = function (dateValue, lat, lng) { + const pos = SunCalc.getMoonPosition(dateValue, lat, lng); + const illum = SunCalc.getMoonIllumination(dateValue); + return Object.assign({ + illumination : illum, + zenithAngle : illum.angle - pos.parallacticAngle + }, pos); + }; + /** * add hours to a date * @param {number} dateValue date as unix timestamp to add hours @@ -854,21 +933,13 @@ const util = require('util'); // eslint-disable-line no-unused-vars return dateValue + h * dayMs / 24; } - /** - * @typedef {Object} moontimes - * @property {Date} rise - a Date object if the moon is rising on the given Date, otherwhise NaN - * @property {Date} set - a Date object if the moon is setting on the given Date, otherwhise NaN - * @property {boolean} alwaysUp - is true if the moon in always up, oitherwise property not exists - * @property {boolean} alwaysDown - is true if the moon in always up, oitherwise property not exists - */ - /** * calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article * @param {number} dateValue date as unix timestamp for calculating moon-times * @param {number} lat latitude for calculating moon-times * @param {number} lng longitude for calculating moon-times * @param {boolean} [inUTC] defines if the calculation should be in utc or local time (default is local) - * @return {moontimes} result object of sunTime + * @return {IMoonTimes} result object of sunTime */ SunCalc.getMoonTimes = function (dateValue, lat, lng, inUTC) { if (isNaN(lat)) { @@ -972,6 +1043,7 @@ const util = require('util'); // eslint-disable-line no-unused-vars // export as Node module / AMD module / browser variable if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; // else if (typeof define === 'function' && define.amd) define(SunCalc); + // @ts-ignore else window.SunCalc = SunCalc; })(); \ No newline at end of file diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js index f7264c6..2259d5f 100644 --- a/nodes/lib/timeControlHelper.js +++ b/nodes/lib/timeControlHelper.js @@ -1,3 +1,4 @@ +// @ts-check /* * This code is licensed under the Apache License Version 2.0. * @@ -24,10 +25,44 @@ * dateTimeHelper.js: *********************************************/ 'use strict'; -const path = require('path'); +/** --- 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("./dateTimeHelper.js").ITimeObject} ITimeObject + * @typedef {import("../10-position-config.js").IPositionConfigNode} IPositionConfigNode + */ + +/** + * @typedef {Object} ITimeControlNodeInstance Extensions for the nodeInstance object type + * @property {IPositionConfigNode} positionConfig - tbd + * @property {string} addId internal used additional id + * @property {Object} nodeData get/set generic Data of the node + * @property {Object} reason - tbd + * @property {string} contextStore - used context store + * @property {Object} rules - tbd + * + * @property {boolean} [levelReverse] - indicator if the Level is in reverse order + * @property {Object} [sunData] - the sun data Object + * @property {Object} nowarn - tbd + * + * @property {Object} autoTrigger autotrigger options + * @property {NodeJS.Timeout} autoTriggerObj autotrigger options + * + * @property {Object} startDelayTimeOut - tbd + * @property {NodeJS.Timeout} startDelayTimeOutObj - tbd + * @property {NodeJS.Timeout} timeOutObj - Overwrite Reset TimeOut Object + * + * @property {function} setState function for settign the state of the node + * ... obviously there are more ... + */ + +/** + * @typedef {ITimeControlNodeInstance & runtimeNode} ITimeControlNode Combine nodeInstance with additional, optional functions + */ const util = require('util'); -const hlp = require( path.resolve( __dirname, './dateTimeHelper.js') ); +const hlp = require( './dateTimeHelper.js' ); const cRuleTime = { // deprecated until : 0, @@ -77,6 +112,15 @@ module.exports = { let RED = null; /******************************************************************************************/ +/** +* Timestamp compare function +* @callback ICompareTimeStamp +* @param {number} timeStamp The timestamp which should be compared +* @returns {Boolean} return true if if the timestamp is valid, otherwise false +*/ + +/******************************************************************************************/ + /** * Returns true if the given object is null or undefined. Otherwise, returns false. * @param {*} object object to check @@ -88,10 +132,11 @@ function isNullOrUndefined(object) { /** * evaluate temporary Data - * @param {*} node node Data + * @param {ITimeControlNode} node node Data * @param {string} type type of type input * @param {string} value value of typeinput * @param {*} data data to cache + * @param {Object} tempData object which holding the chached data * @returns {*} data which was cached */ function evalTempData(node, type, value, data, tempData) { @@ -117,7 +162,7 @@ function evalTempData(node, type, value, data, tempData) { /******************************************************************************************/ /** * clears expire object properties -* @param {*} node node data +* @param {ITimeControlNode} node node data */ function deleteExpireProp(node) { delete node.nodeData.overwrite.expires; @@ -131,7 +176,7 @@ function deleteExpireProp(node) { /** * reset any existing override -* @param {*} node node data +* @param {ITimeControlNode} node node data */ function posOverwriteReset(node) { node.debug(`posOverwriteReset expire=${node.nodeData.overwrite.expireTs}`); @@ -149,7 +194,7 @@ function posOverwriteReset(node) { /** * setup the expiring of n override or update an existing expiring -* @param {*} node node data +* @param {ITimeControlNode} node node data * @param {ITimeObject} oNow the *current* date Object * @param {number} dExpire the expiring time, (if it is NaN, default time will be tried to use) if it is not used, nor a Number or less than 1 no expiring activated */ @@ -191,7 +236,7 @@ function setExpiringOverwrite(node, oNow, dExpire, reason) { /** * check if an override can be reset -* @param {*} node node data +* @param {ITimeControlNode} node node data * @param {*} msg message object * @param {ITimeObject} oNow the *current* date Object */ @@ -217,7 +262,7 @@ function checkOverrideReset(node, msg, oNow, isSignificant) { } /** * setting the reason for override -* @param {*} node node data +* @param {ITimeControlNode} node node data */ function setOverwriteReason(node) { if (node.nodeData.overwrite.active) { @@ -246,7 +291,7 @@ function setOverwriteReason(node) { /******************************************************************************************/ /** * pre-checking conditions to may be able to store temp data - * @param {*} node node data + * @param {ITimeControlNode} node node data * @param {*} msg the message object * @param {*} tempData the temporary storage object */ @@ -314,7 +359,7 @@ function prepareRules(node, msg, tempData, dNow) { * @param {Object} msg the message object * @param {Object} rule the rule data * @param {string} timep rule type - * @param {number} dNow base timestamp + * @param {Date} dNow base timestamp * @return {number} timestamp of the rule */ function getRuleTimeData(node, msg, rule, timep, dNow) { @@ -373,26 +418,6 @@ function getRuleTimeData(node, msg, rule, timep, dNow) { } /*************************************************************************************************************************/ -/** -* Timestamp compare function -* @name ICompareTimeStamp -* @function -* @param {number} timeStamp The timestamp which should be compared -* @returns {Boolean} return true if if the timestamp is valid, otherwise false -*/ - -/** -* support timeData -* @name ITimeObject Data -* @param {Date} now -* @param {number} nowNr -* @param {number} dayNr -* @param {number} monthNr -* @param {number} dateNr -* @param {number} yearNr -* @param {number} dayId -*/ - /** * function to check a rule * @param {Object} node the node object @@ -412,7 +437,7 @@ function compareRules(node, msg, rule, cmp, data) { } } catch (err) { node.warn(RED._('node-red-contrib-sun-position/position-config:errors.getPropertyData', err)); - node.debug(util.inspect(err, Object.getOwnPropertyNames(err))); + node.debug(util.inspect(err)); return null; } } @@ -473,9 +498,9 @@ function compareRules(node, msg, rule, cmp, data) { }; if (rule.time.start) { - getRuleTimeData(node, msg, rule, 'start', data.now, Number.MIN_VALUE); + getRuleTimeData(node, msg, rule, 'start', data.now); if (rule.time.end) { - getRuleTimeData(node, msg, rule, 'end', data.now, Number.MAX_VALUE); + getRuleTimeData(node, msg, rule, 'end', data.now); if (rule.timeData.start.ts > rule.timeData.end.ts) { if (data.dayId === rule.timeData.start.dayId && rule.timeData.start.ts <= data.nowNr) { @@ -495,7 +520,7 @@ function compareRules(node, msg, rule, cmp, data) { return null; } } else if (rule.time.end) { - getRuleTimeData(node, msg, rule, 'end', data.now, Number.MAX_VALUE); + getRuleTimeData(node, msg, rule, 'end', data.now); if (data.dayId !== rule.timeData.end.dayId) { return null; } @@ -518,7 +543,7 @@ function compareRules(node, msg, rule, cmp, data) { */ function checkRules(node, msg, oNow, tempData) { // node.debug('checkRules --------------------'); - prepareRules(node, msg, tempData, oNow.dNow); + prepareRules(node, msg, tempData, oNow.now); // node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); const result = { ruleindex : -1, @@ -578,18 +603,14 @@ function checkRules(node, msg, oNow, tempData) { /*************************************************************************************************************************/ /** * check if a level has a valid value - * @param {*} node the node data + * @param {ITimeControlNode} node the node data * @param {number} level the level to check * @returns {boolean} true if the level is valid, otherwise false */ function validPosition(node, level, allowRound) { // node.debug('validPosition level='+level); - if (level === '' || level === null || typeof level === 'undefined') { - node.warn(`Position is empty!`); - return false; - } - if (isNaN(level)) { - node.warn(`Position: "${level}" is NaN!`); + if (typeof level !== 'number' || level === null || typeof level === 'undefined' || isNaN(level)) { + node.warn(`Position: "${String(level)}" is empty or not a valid number!`); return false; } diff --git a/nodes/locales/de/80-blind-control.json b/nodes/locales/de/80-blind-control.json index 1fa02bc..157e1c0 100644 --- a/nodes/locales/de/80-blind-control.json +++ b/nodes/locales/de/80-blind-control.json @@ -60,8 +60,8 @@ "blindIncrement": "0.01 oder 1", "blindOpenPos": "1 oder 100", "blindClosedPos": "0", - "blindOpenPosOffset": "offset für die Position geöffnet", - "blindClosedPosOffset": "offset für die Position geschlossen", + "blindOpenPosOffset": "(opt) offset für die Position geöffnet", + "blindClosedPosOffset": "(opt) offset für die Position geschlossen", "blindLevelDefault": "Rollladenposition, wenn keine andere zutrifft", "slatPosDefault": "Lamellenposition, wenn keine andere zutrifft", "blindLevelMin": "maximale Rollladenposition Sonnenstands abhängig", diff --git a/nodes/locales/en-US/80-blind-control.json b/nodes/locales/en-US/80-blind-control.json index ec837d8..3590592 100644 --- a/nodes/locales/en-US/80-blind-control.json +++ b/nodes/locales/en-US/80-blind-control.json @@ -56,8 +56,8 @@ "blindIncrement": "0.01 or 1", "blindOpenPos": "100", "blindClosedPos": "0", - "blindOpenPosOffset": "offset for the open position", - "blindClosedPosOffset": "offset for the closed position", + "blindOpenPosOffset": "(opt) offset for the open position", + "blindClosedPosOffset": "(opt) offset for the closed position", "blindLevelDefault": "blind position if no other used", "slatPosDefault": "slat position if no other used", "blindLevelMin": "minimum position if sun calculated position", diff --git a/nodes/types/typedefs.js b/nodes/types/typedefs.js new file mode 100644 index 0000000..2266945 --- /dev/null +++ b/nodes/types/typedefs.js @@ -0,0 +1,361 @@ +/* eslint-disable no-irregular-whitespace */ +/** Define typedefs for linting and JSDoc/ts checks - does not actually contain live code + * + * Original by + * Copyright (c) 2017-2021 Julian Knight (Totally Information) + * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder + * + * Reworked and enhanced by + * Copyright (c) 2022 Robert Gester (Hypnos) + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** editorRED + * @typedef {Object} editorRED The Node-RED core object available to a custom node's .html file + * + */ + +/** runtimeLogging + * @typedef {Object} runtimeLogging Logging. Levels that are output to the Node-RED log are controlled by the logging.console.level setting in settings.js + * @property {function} fatal Lvel 0. Lowest level, things that have broken Node-RED only. + * @property {function} error Level 1. Copy is sent to Editor debug panel as well as error log. + * @property {function} warn Level 2. + * @property {function} info Level 3. + * @property {function} debug Level 4. + * @property {function} trace Level 5. Very verbose output. Should tell the operator everything that is going on. + * @property {function} metric + * @property {function} audit + * @property {function} addHandler + * @property {function} removeHandler + */ + +/** runtimeNodes + * @typedef {Object} runtimeNodes Gives access to other active nodes in the flows. + * @property {function} registerType Register a new type of node to Node-RED. + * @property {function} createNode Create a node instance (called from within registerType function). + * @property {function} getNode Get a reference to another node instance in the current flows. Can then access its properties. + * @property {function} eachNode: [Function: eachNode], + * @property {function} addCredentials: [Function: add], + * @property {function} getCredentials: [Function: get], + * @property {function} deleteCredentials: [Function: delete], + */ + +/** runtimeRED + * @typedef {Object} runtimeRED The core Node-RED runtime object + * @property {expressApp} httpAdmin Reference to the ExpressJS app for Node-RED Admin including the Editor + * @property {expressApp} httpNode Reference to the ExpressJS app for Node-RED user-facing nodes including http-in/-out and Dashboard + * @property {Object} server Node.js http(s) Server object + * @property {runtimeLogging} log Logging. + * @property {runtimeNodes} nodes Gives access to other active nodes in the flows. + * @property {Object} settings Static and Dynamic settings for Node-RED runtime + * + * @property {function} version Get the Node-RED version + * @property {function} require: [Function: requireModule], + * + * @property {Object} comms + * @property {function} comms.publish: [Function: publish] + * + * @property {Object} library + * @property {function} library.register: [Function: register] + * + * @property {Object} auth + * @property {function} auth.needsPermission + * + * @property {function} _: get a i18N string, + * + * @property {Object} events Event handler object + * @property {function} events.on Event Listener function. Types: 'nodes-started', 'nodes-stopped' + * @property {function} events.once + * @property {function} events.addListener + * + * @property {Object} hooks + * @property {function} hooks.has + * @property {function} hooks.clear + * @property {function} hooks.add + * @property {function} hooks.remove + * @property {function} hooks.trigger + * + * @property {Object} util + * @property {function} util.encodeObject: [Function: encodeObject], + * @property {function} util.ensureString: [Function: ensureString], + * @property {function} util.ensureBuffer: [Function: ensureBuffer], + * @property {function} util.cloneMessage: [Function: cloneMessage], + * @property {function} util.compareObjects: [Function: compareObjects], + * @property {function} util.generateId: [Function: generateId], + * @property {function} util.getMessageProperty: [Function: getMessageProperty], + * @property {function} util.setMessageProperty: [Function: setMessageProperty], + * @property {function} util.getObjectProperty: [Function: getObjectProperty], + * @property {function} util.setObjectProperty: [Function: setObjectProperty], + * @property {function} util.evaluateNodeProperty: [Function: evaluateNodeProperty], + * @property {function} util.normalisePropertyExpression: [Function: normalisePropertyExpression], + * @property {function} util.normaliseNodeTypeName: [Function: normaliseNodeTypeName], + * @property {function} util.prepareJSONataExpression: [Function: prepareJSONataExpression], + * @property {function} util.evaluateJSONataExpression: [Function: evaluateJSONataExpression], + * @property {function} util.parseContextStore: [Function: parseContextStore] + */ + +/** runtimeNode + * @typedef {object} runtimeNode Local copy of the node instance config + other info + * @property {Function} send Send a Node-RED msg to an output port + * @property {Function} emit function for let node emit a message + * @property {Function} done Dummy done function for pre-Node-RED 1.0 servers + * @property {function} context get/set context data. Also .flow and .global contexts + * @property {function} on Event listeners for the node instance ('input', 'close') + * @property {Function} removeListener Event handling + * @property {function} error Error log output, also logs to the Editor's debug panel + * @property {function} warn Warning log output, also logs to the Editor's debug panel + * @property {function} log log output + * @property {function} debug debugging log output + * @property {function} status set the node status + * @property {Object=} credentials Optional secured credentials + * @property {Object=} name Internal. + * @property {Object=} id Internal. uid of node instance. + * @property {Object=} _path Internal. + * @property {Object=} type Internal. Type of node instance. + * @property {Object=} z Internal. uid of ??? + * @property {[Array]=} wires Internal. Array of Array of Strings. The wires attached to this node instance (uid's) + * ... obviously there are more ... + */ + +/** runtimeNodeConfig + * @typedef {object} runtimeNodeConfig Configuration of node instance. Will also have Editor panel's defined variables as properties. + * @property {Object=} id Internal. uid of node instance. + * @property {Object=} type Internal. Type of node instance. + * @property {Object=} x Internal + * @property {Object=} y Internal + * @property {Object=} z Internal + * @property {Object=} wires Internal. The wires attached to this node instance (uid's) + */ + +/** uibNode + * @typedef {object} uibNode Local copy of the node instance config + other info + * @property {String} uibNode.id Unique identifier for this instance + * @property {String} uibNode.type What type of node is this an instance of? (uibuilder) + * @property {String} uibNode.name Descriptive name, only used by Editor + * @property {String} uibNode.topic msg.topic overrides incoming msg.topic + * @property {String} uibNode.url The url path (and folder path) to be used by this instance + * @property {String} uibNode.oldUrl The PREVIOUS url path (and folder path) after a url rename + * @property {boolean} uibNode.fwdInMessages Forward input msgs to output #1? + * @property {boolean} uibNode.allowScripts Allow scripts to be sent to front-end via msg? WARNING: can be a security issue. + * @property {boolean} uibNode.allowStyles Allow CSS to be sent to the front-end via msg? WARNING: can be a security issue. + * @property {boolean} uibNode.copyIndex DEPRECATED Copy index.(html|js|css) files from templates if they don't exist? + * @property {String} uibNode.templateFolder Folder name for the source of the chosen template + * @property {String} uibNode.extTemplate Degit url reference for an external template (e.g. from GitHub) + * @property {boolean} uibNode.showfolder Provide a folder index web page? + * @property {boolean} uibNode.useSecurity Use uibuilder's built-in security features? + * @property {boolean} uibNode.tokenAutoExtend Extend token life when msg's received from client? + * @property {Number} uibNode.sessionLength Lifespan of token (in seconds) + * @property {boolean} uibNode.reload If true, notify all clients to reload on a change to any source file + * @property {String} uibNode.sourceFolder (src or dist) the instance FE code folder to be served by ExpressJS + * @property {String} uibNode.jwtSecret Seed string for encryption of JWT + * @property {String} uibNode.customFolder Name of the fs path used to hold custom files & folders for THIS INSTANCE + * @property {Number} uibNode.ioClientsCount How many Socket clients connected to this instance? + * @property {Number} uibNode.rcvMsgCount How many msg's received since last reset or redeploy? + * @property {Object} uibNode.ioChannels The channel names for Socket.IO + * @property {String} uibNode.ioChannels.control SIO Control channel name 'uiBuilderControl' + * @property {String} uibNode.ioChannels.client SIO Client channel name 'uiBuilderClient' + * @property {String} uibNode.ioChannels.server SIO Server channel name 'uiBuilder' + * @property {String} uibNode.ioNamespace Make sure each node instance uses a separate Socket.IO namespace + * @property {Function} uibNode.send Send a Node-RED msg to an output port + * @property {Function=} uibNode.done Dummy done function for pre-Node-RED 1.0 servers + * @property {Function=} uibNode.on Event handler + * @property {Function=} uibNode.removeListener Event handling + * @property {Object=} uibNode.credentials Optional secured credentials + * @property {Object=} uibNode.z Internal + * @property {Object=} uibNode.wires Internal. The wires attached to this node instance (uid's) + * + * @property {boolean} uibNode.commonStaticLoaded Whether the common static folder has been added + * @property {boolean} uibNode.initCopyDone Has the initial template copy been done? + */ + +// ==== vvv These need some work vvv ==== // + +// ExpressJS App +/** + * @typedef {Object} expressApp ExpessJS `app` object + * @property {Object} _events: [Object: null prototype] { mount: [Function: onmount] }, + * @property {number} _eventsCount: 1, + * @property {number} _maxListeners: undefined, + * @property {function} setMaxListeners: [Function: setMaxListeners], + * @property {function} getMaxListeners: [Function: getMaxListeners], + * @property {function} emit: [Function: emit], + * @property {function} addListener: [Function: addListener], + * @property {function} on: [Function: addListener], + * @property {function} prependListener: [Function: prependListener], + * @property {function} once: [Function: once], + * @property {function} prependOnceListener: [Function: prependOnceListener], + * @property {function} removeListener: [Function: removeListener], + * @property {function} off: [Function: removeListener], + * @property {function} removeAllListeners: [Function: removeAllListeners], + * @property {function} listeners: [Function: listeners], + * @property {function} rawListeners: [Function: rawListeners], + * @property {function} listenerCount: [Function: listenerCount], + * @property {function} eventNames: [Function: eventNames], + * @property {function} init: [Function: init], + * @property {function} defaultConfiguration: [Function: defaultConfiguration], + * @property {function} lazyrouter: [Function: lazyrouter], + * @property {function} handle: [Function: handle], + * @property {function} use: [Function: use], + * @property {function} route: [Function: route], + * @property {function} engine: [Function: engine], + * @property {function} param: [Function: param], + * @property {function} set: [Function: set], + * @property {function} path: [Function: path], + * @property {function} enabled: [Function: enabled], + * @property {function} disabled: [Function: disabled], + * @property {function} enable: [Function: enable], + * @property {function} disable: [Function: disable], + * @property {function} acl: [Function (anonymous)], + * @property {function} bind: [Function (anonymous)], + * @property {function} checkout: [Function (anonymous)], + * @property {function} connect: [Function (anonymous)], + * @property {function} copy: [Function (anonymous)], + * @property {function} delete: [Function (anonymous)], + * @property {function} get: [Function (anonymous)], + * @property {function} head: [Function (anonymous)], + * @property {function} link: [Function (anonymous)], + * @property {function} lock: [Function (anonymous)], + * // @ property {function} m-search: [Function (anonymous)], + * @property {function} merge: [Function (anonymous)], + * @property {function} mkactivity: [Function (anonymous)], + * @property {function} mkcalendar: [Function (anonymous)], + * @property {function} mkcol: [Function (anonymous)], + * @property {function} move: [Function (anonymous)], + * @property {function} notify: [Function (anonymous)], + * @property {function} options: [Function (anonymous)], + * @property {function} patch: [Function (anonymous)], + * @property {function} post: [Function (anonymous)], + * @property {function} pri: [Function (anonymous)], + * @property {function} propfind: [Function (anonymous)], + * @property {function} proppatch: [Function (anonymous)], + * @property {function} purge: [Function (anonymous)], + * @property {function} put: [Function (anonymous)], + * @property {function} rebind: [Function (anonymous)], + * @property {function} report: [Function (anonymous)], + * @property {function} search: [Function (anonymous)], + * @property {function} source: [Function (anonymous)], + * @property {function} subscribe: [Function (anonymous)], + * @property {function} trace: [Function (anonymous)], + * @property {function} unbind: [Function (anonymous)], + * @property {function} unlink: [Function (anonymous)], + * @property {function} unlock: [Function (anonymous)], + * @property {function} unsubscribe: [Function (anonymous)], + * @property {function} all: [Function: all], + * @property {function} del: [Function (anonymous)], + * @property {function} render: [Function: render], + * @property {function} listen: [Function: listen], + * @property {function} request: IncomingMessage { app: [Circular *1] }, + * @property {function} response: ServerResponse { app: [Circular *1] }, + * @property {Object} cache: {}, + * @property {Object} engines: {}, + * + * @property {Object} settings: + * // @ property {boolean} settings.'x-powered-by': true, + * @property {string} settings.etag: 'weak', + * // @ property {function} settings.'etag fn': [Function: generateETag], + * @property {string} settings.env: 'development', + * // @ property {string} settings.'query parser': 'extended', + * // @ property {function} settings.'query parser fn': [Function: parseExtendedQueryString], + * // @ property {number} settings.'subdomain offset': 2, + * @property {function} settings.view: [Function: View], + * @property {string} settings.views: 'C:\\src\\nr2\\views', + * // @ property {string} settings.'jsonp callback name': 'callback' + * + * @property {Object} locals: [Object: null prototype] { settings: [Object] }, + * @property {string} mountpath: '/nr/', + * + * @property {function} parent: [Function: app] { + * @property {function} parent._events: [Object: null prototype], + * @property {function} parent._eventsCount: 1, + * @property {function} parent._maxListeners: undefined, + * @property {function} parent.setMaxListeners: [Function: setMaxListeners], + * @property {function} parent.getMaxListeners: [Function: getMaxListeners], + * @property {function} parent.emit: [Function: emit], + * @property {function} parent.addListener: [Function: addListener], + * @property {function} parent.on: [Function: addListener], + * @property {function} parent.prependListener: [Function: prependListener], + * @property {function} parent.once: [Function: once], + * @property {function} parent.prependOnceListener: [Function: prependOnceListener], + * @property {function} parent.removeListener: [Function: removeListener], + * @property {function} parent.off: [Function: removeListener], + * @property {function} parent.removeAllListeners: [Function: removeAllListeners], + * @property {function} parent.listeners: [Function: listeners], + * @property {function} parent.rawListeners: [Function: rawListeners], + * @property {function} parent.listenerCount: [Function: listenerCount], + * @property {function} parent.eventNames: [Function: eventNames], + * @property {function} parent.init: [Function: init], + * @property {function} parent.defaultConfiguration: [Function: defaultConfiguration], + * @property {function} parent.lazyrouter: [Function: lazyrouter], + * @property {function} parent.handle: [Function: handle], + * @property {function} parent.use: [Function: use], + * @property {function} parent.route: [Function: route], + * @property {function} parent.engine: [Function: engine], + * @property {function} parent.param: [Function: param], + * @property {function} parent.set: [Function: set], + * @property {function} parent.path: [Function: path], + * @property {function} parent.enabled: [Function: enabled], + * @property {function} parent.disabled: [Function: disabled], + * @property {function} parent.enable: [Function: enable], + * @property {function} parent.disable: [Function: disable], + * @property {function} parent.acl: [Function (anonymous)], + * @property {function} parent.bind: [Function (anonymous)], + * @property {function} parent.checkout: [Function (anonymous)], + * @property {function} parent.connect: [Function (anonymous)], + * @property {function} parent.copy: [Function (anonymous)], + * @property {function} parent.delete: [Function (anonymous)], + * @property {function} parent.get: [Function (anonymous)], + * @property {function} parent.head: [Function (anonymous)], + * @property {function} parent.link: [Function (anonymous)], + * @property {function} parent.lock: [Function (anonymous)], + * // @ property {function} parent.'m-search': [Function (anonymous)], + * @property {function} parent.merge: [Function (anonymous)], + * @property {function} parent.mkactivity: [Function (anonymous)], + * @property {function} mkcalendar: [Function (anonymous)], + * @property {function} mkcol: [Function (anonymous)], + * @property {function} move: [Function (anonymous)], + * @property {function} notify: [Function (anonymous)], + * @property {function} options: [Function (anonymous)], + * @property {function} patch: [Function (anonymous)], + * @property {function} post: [Function (anonymous)], + * @property {function} pri: [Function (anonymous)], + * @property {function} propfind: [Function (anonymous)], + * @property {function} proppatch: [Function (anonymous)], + * @property {function} purge: [Function (anonymous)], + * @property {function} put: [Function (anonymous)], + * @property {function} rebind: [Function (anonymous)], + * @property {function} report: [Function (anonymous)], + * @property {function} search: [Function (anonymous)], + * @property {function} source: [Function (anonymous)], + * @property {function} subscribe: [Function (anonymous)], + * @property {function} trace: [Function (anonymous)], + * @property {function} unbind: [Function (anonymous)], + * @property {function} unlink: [Function (anonymous)], + * @property {function} unlock: [Function (anonymous)], + * @property {function} unsubscribe: [Function (anonymous)], + * @property {function} all: [Function: all], + * @property {function} del: [Function (anonymous)], + * @property {function} render: [Function: render], + * @property {function} listen: [Function: listen], + * @property {function} request: [IncomingMessage], + * @property {function} response: [ServerResponse], + * @property {function} cache: {}, + * @property {function} engines: {}, + * @property {function} settings: [Object], + * @property {function} locals: [Object: null prototype], + * @property {function} mountpath: '/', + * @property {function} _router: [Function] + */ + +module.exports = {}; \ No newline at end of file From 646c1aca23b7d8c37bc84d3c10bd47603b513749 Mon Sep 17 00:00:00 2001 From: Hypnos <12692680+Hypnos3@users.noreply.github.com> Date: Sat, 5 Mar 2022 18:46:35 +0100 Subject: [PATCH 24/44] cleanup and fixed new rule execution; type changes --- jsconfig.json | 25 ++-- nodes/80-blind-control.html | 26 +++- nodes/80-blind-control.js | 38 +++-- nodes/lib/timeControlHelper.js | 45 +++--- nodes/types/index.d.ts | 9 ++ package-lock.json | 250 +++++++++++++++++++++++++-------- package.json | 9 +- 7 files changed, 291 insertions(+), 111 deletions(-) create mode 100644 nodes/types/index.d.ts diff --git a/jsconfig.json b/jsconfig.json index 2e1495f..b079866 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,15 +1,20 @@ { "compilerOptions": { - "checkJs": true, - "module": "commonJS", - "target": "es6", - "lib": [ - "es2019" - ], - "moduleResolution": "node", - "sourceMap": true, - "alwaysStrict": true, - "resolveJsonModule": true + "baseUrl": "./", + "checkJs": true, + "module": "commonJS", + "target": "es6", + "lib": [ + "es2019" + ], + "paths": { + "@types": ["nodes/types"], + "@nodes": ["nodes"] + }, + "sourceMap": true, + "alwaysStrict": true, + "resolveJsonModule": true }, + "include": ["./nodes/types/typedefs.js"], "exclude": ["node_modules", "**/node_modules/*", "temp", "**/temp/*", "backup", "**/backup/*", "examples", "**/examples/*"] } \ No newline at end of file diff --git a/nodes/80-blind-control.html b/nodes/80-blind-control.html index d314dfb..8fe8762 100644 --- a/nodes/80-blind-control.html +++ b/nodes/80-blind-control.html @@ -22,7 +22,14 @@ -->