UNPKG

node-red-contrib-sun-position

Version:
1,093 lines (1,044 loc) 63.2 kB
// @ts-check /* * 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: *********************************************/ '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("./dateTimeHelper.js").ITimeObject} ITimeObject * @typedef {import("./dateTimeHelper.js").ILimitationsObj} ILimitationsObj * @typedef {import("../10-position-config.js").IPositionConfigNode} IPositionConfigNode * @typedef {import("../10-position-config.js").ITimePropertyResult} ITimePropertyResult */ /** * @typedef {Object} IAutoTrigger object for sore autotrigger data * @property {number} defaultTime - default next autotriggering time * @property {(0|1|2|3|4|5|6|7|8|9)} type - type of next autotriggering * @property {number} time - next autotrigger in milliseconds * @property {NodeJS.Timeout} [timer] - autotrigger TimeOut Object */ /** * @typedef {Object} IRuleCondition object for a rule condition * * @property {number} condition - position of the condition * @property {string} [conditionText] - description of the condition * @property {string|number} value - first operand * @property {string} valueType - first operand type * @property {function} [valueExpr] - JSONATA expression * @property {string} [valueName] - first operand description * @property {string} [valueNameShort] - first operand description (short version) * @property {string} [valueWorth] - opCallback value * @property {string} operator - operator * @property {string} [operatorText] - operator description * @property {string} [operatorDescription] - operator description enhanced * @property {string} threshold - second operand * @property {string} thresholdType - second operand type * @property {function} [thresholdExpr] - JSONATA expression * @property {string} [thresholdName] - second operand description * @property {string} [thresholdNameShort] - second operand description (short version) * @property {string} [thresholdWorth] - opCallback value * @property {string} [text] - comparision text * @property {string} [textShort] - comparision text (short version) * @property {boolean} result - result of the condition evaluation */ /** * @typedef {Object} IRuleConditionResult object for a rule condition * * @property {number} index - selected condition index * @property {string} [text] - comparision text * @property {string} [textShort] - comparision text (short version) * @property {boolean} result - result of the condition evaluation */ /** * @typedef {Object} IRuleTimeDefSingle object for a rule time definition * * @property {string} value - time value * @property {string} type - type of the time * @property {string|number} offset - time offset value * @property {string} offsetType - time offset type * @property {number} multiplier - time offset value * @property {boolean} next - time offset value * @property {Date} [now] - start time definition */ /** * @typedef {Object} IRuleTimeDef object for a rule time definition * * @property {string} value - time value * @property {string} type - type of the time * @property {string|number} offset - time offset value * @property {string} offsetType - time offset type * @property {number} multiplier - time offset value * @property {boolean} next - time offset value * @property {Date} [now] - start time definition * @property {IRuleTimeDefSingle} [min] - minimum limitation to the time * @property {IRuleTimeDefSingle} [max] - maximum limitation to the time */ /** * @typedef {Object} IRuleTimesDefInt object for a rule time definition * * @property {IRuleTimeDef} [start] - start time definition * @property {IRuleTimeDef} [end] - end time definition */ /** * @typedef {ILimitationsObj & IRuleTimesDefInt} IRuleTimesDef object for a rule time definition */ /** * @typedef {Object} ITimePropertyResultInt object for a rule time definition * * @property {number} ts - time in milliseconds * @property {number} dayId - day id of the date * @property {string} [timeLocal] - time representation * @property {string} [timeLocalDate] - time representation * @property {string} [dateISO] - time representation * @property {string} [dateUTC] - time representation * @property {('default'|'min'|'max')} [source] - source of the data if it comes from minimum or maximum limitation */ /** * @typedef {ITimePropertyResultInt & ITimePropertyResult} ITimePropResult object for a rule time definition */ /** * @typedef {Object} IRuleTimeDataDef object for a rule time definition * * @property {ITimePropResult} [start] - start time definition * @property {ITimePropResult} [end] - end time definition * @property {number} [now] - start time definition */ /** * @typedef {Object} IRuleTimeDataMinMaxDef object for a rule time definition * * @property {ITimePropResult} [start] - start time definition * @property {ITimePropResult} [end] - end time definition */ /** * @typedef {Object} IRuleData object for a rule * @property {boolean} enabled - defines if a rle is enabled or disabled * @property {number} pos - rule position * @property {string} name - name of the rule * @property {number} exec - executuion type of a rule which is defined * @property {number} execUse - executuion type of a rule which is used * @property {boolean} resetOverwrite - overwrites reset * @property {number} importance - importance of the rule * @property {boolean} conditional - defines if the rule has conditions * @property {Array.<IRuleCondition>} conditions - conditions for a rule * @property {IRuleConditionResult} conditonResult - condition resule * @property {IRuleTimesDef} [time] - rule time Data * @property {IRuleTimeDataDef} [timeResult] - object for storing time Data * @property {IRuleTimeDataMinMaxDef} [timeResultMin] - object for storing time Data * @property {IRuleTimeDataMinMaxDef} [timeResultMax] - object for storing time Data * @property {Object} [payload] - rule time Data * @property {Object} [level] - rule time Data * @property {Object} [slat] - rule time Data * @property {string} topic - rule time Data * @property {Object} [outputValue] - rule time Data * @property {string} [outputType] - rule time Data */ /** * @typedef {Object} IRuleResultData object for a rule result * @property {number} ruleindex - index of selected rule * @property {IRuleData} [ruleSel] - selected rule * @property {IRuleData} [ruleSlatOvs] - selected rule * @property {IRuleData} [ruleTopicOvs] - selected rule * @property {IRuleData} [ruleSelMin] - selected rule * @property {IRuleData} [ruleSelMax] - selected rule * @property {IRuleTimeDataDef} [timeResult] - object for storing time Data * @property {IRuleTimeDataDef} [timeResultMin] - object for storing time Data * @property {IRuleTimeDataDef} [timeResultMax] - object for storing time Data */ /** * @typedef {Object} IRulesData object for a rule * @property {Array.<IRuleData>} data - the rules itself * @property {number} count - executuion type of a rule which is defined * @property {number} last1stRun - last rule for first evaluation loop * @property {number} maxImportance - maximum inportance of all rules * @property {boolean} canResetOverwrite - __true__ if any rule can overwrite reset */ /** * @typedef {Object} ISunData object for a rule * @property {(0|1|3|16)} mode - mode of the sun * @property {(0|1|3|16)} modeMax - maximum mode * @property {string} floorLength - floorLength value * @property {string} floorLengthType - type of the floorLength * @property {number} changeAgain - timestamp of the next change * @property {number} minDelta - minimum delta * @property {Object} [level] - rule time Data * @property {Object} [slat] - rule time Data * @property {string} topic - rule time Data */ /** * @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 {IRulesData} rules - definition of the rule Data * * @property {boolean} [levelReverse] - indicator if the Level is in reverse order * @property {ISunData} [sunData] - the sun data Object * @property {Object} nowarn - tbd * * @property {Array.<Object>} results - tbd * * @property {IAutoTrigger} autoTrigger autotrigger options * * @property {Object} startDelayTimeOut - tbd * @property {NodeJS.Timeout} startDelayTimeOutObj - tbd * @property {NodeJS.Timeout} timeOutObj - Overwrite Reset TimeOut Object * ... obviously there are more ... */ /** * @typedef {ITimeControlNodeInstance & runtimeNode} ITimeControlNode Combine nodeInstance with additional, optional functions */ const util = require('util'); const hlp = require( './dateTimeHelper.js' ); 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; 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, checkOverrideReset, setOverwriteReason, prepareRules, getRuleTimeData, validPosition, compareRules, getActiveRule, initializeCtrl, cRuleType, cRuleDefault, cRuleLogOperatorAnd, cRuleLogOperatorOr }; 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 * @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 {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 * @param {boolean} [cachable = false] defines if the value is cachable * @returns {*} data which was cached */ function evalTempData(node, type, value, data, tempData, cachable) { if (!cachable) { return data; } // node.debug(`evalTempData type=${type} value=${value} data=${data}`); const name = `${type}.${value}`; 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] })); } return tempData[name]; } if (node.nowarn[name]) { return undefined; // only one error per run } node.warn(RED._('node-red-contrib-sun-position/position-config:errors.warning', { message: RED._('node-red-contrib-sun-position/position-config:errors.notEvaluablePropertyUsedValue', { type, value, usedValue: 'undefined' }) })); node.nowarn[name] = true; return undefined; } tempData[name] = data; return data; } /******************************************************************************************/ /** * clears expire object properties * @param {ITimeControlNode} node node data */ function deleteExpireProp(node) { delete node.nodeData.overwrite.expires; delete node.nodeData.overwrite.expireTs; delete node.nodeData.overwrite.expireDate; delete node.nodeData.overwrite.expireDateISO; delete node.nodeData.overwrite.expireDateUTC; delete node.nodeData.overwrite.expireTimeLocal; delete node.nodeData.overwrite.expireDateLocal; } /** * reset any existing override * @param {ITimeControlNode} node node data */ function posOverwriteReset(node) { node.debug(`posOverwriteReset expire=${node.nodeData.overwrite.expireTs}`); node.nodeData.overwrite.active = false; node.nodeData.overwrite.importance = 0; if (node.timeOutObj) { clearTimeout(node.timeOutObj); node.timeOutObj = null; } if (node.nodeData.overwrite.expireTs || node.nodeData.overwrite.expires) { deleteExpireProp(node); } node.context().set('overwrite', node.nodeData.overwrite, node.contextStore); } /** * setup the expiring of n override or update an existing expiring * @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 */ function setExpiringOverwrite(node, oNow, dExpire, reason) { node.debug(`setExpiringOverwrite dExpire=${dExpire}, reason=${reason}`); if (node.timeOutObj) { clearTimeout(node.timeOutObj); node.timeOutObj = null; } if (isNaN(dExpire)) { dExpire = node.nodeData.overwrite.expireDuration; node.debug(`using default expire value=${dExpire}`); } node.nodeData.overwrite.expires = Number.isFinite(dExpire) && (dExpire > 0); if (!node.nodeData.overwrite.expires) { node.log(`Overwrite is set which never expire (${reason})`); node.debug(`expireNever expire=${dExpire}ms ${ typeof dExpire } - isNaN=${ isNaN(dExpire) } - finite=${ !isFinite(dExpire) } - min=${ dExpire < 100}`); deleteExpireProp(node); node.context().set('overwrite', node.nodeData.overwrite, node.contextStore); return; } node.nodeData.overwrite.expireTs = (oNow.nowNr + dExpire); node.nodeData.overwrite.expireDate = new Date(node.nodeData.overwrite.expireTs); node.nodeData.overwrite.expireDateISO = node.nodeData.overwrite.expireDate.toISOString(); node.nodeData.overwrite.expireDateUTC = node.nodeData.overwrite.expireDate.toUTCString(); node.nodeData.overwrite.expireDateLocal = node.positionConfig.toDateString(node.nodeData.overwrite.expireDate); node.nodeData.overwrite.expireTimeLocal = node.positionConfig.toTimeString(node.nodeData.overwrite.expireDate); node.log(`Overwrite is set which expires in ${dExpire}ms = ${node.nodeData.overwrite.expireDateISO} (${reason})`); node.timeOutObj = setTimeout(() => { node.log(`Overwrite is expired (timeout)`); posOverwriteReset(node); node.emit('input', { payload: -1, topic: 'internal-triggerOnly-overwriteExpired', force: false }); }, dExpire); node.context().set('overwrite', node.nodeData.overwrite, node.contextStore); } /** * check if an override can be reset * @param {ITimeControlNode} node node data * @param {Object} msg message object * @param {ITimeObject} oNow the *current* date Object */ function checkOverrideReset(node, msg, oNow, isSignificant) { if (node.nodeData.overwrite && node.nodeData.overwrite.expires && (node.nodeData.overwrite.expireTs < oNow.nowNr)) { node.log(`Overwrite is expired (trigger)`); posOverwriteReset(node); } if (isSignificant) { hlp.getMsgBoolValue(msg, ['reset','resetOverwrite'], 'resetOverwrite', val => { node.debug(`reset val="${util.inspect(val, { colors: true, compact: 5, breakLength: Infinity, depth: 10 }) }"`); if (val) { if (node.nodeData.overwrite && node.nodeData.overwrite.active) { node.log(`Overwrite reset by incoming message`); } posOverwriteReset(node); } }); } } /** * setting the reason for override * @param {ITimeControlNode} node node data */ function setOverwriteReason(node) { if (node.nodeData.overwrite.active) { if (node.nodeData.overwrite.expireTs) { node.reason.code = 3; const obj = { importance: node.nodeData.overwrite.importance, timeLocal: node.nodeData.overwrite.expireTimeLocal, dateLocal: node.nodeData.overwrite.expireDateLocal, dateISO: node.nodeData.overwrite.expireDateISO, dateUTC: node.nodeData.overwrite.expireDateUTC }; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.overwriteExpire', obj); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.overwriteExpire', obj); } else { node.reason.code = 2; node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.overwriteNoExpire', { importance: node.nodeData.overwrite.importance }); node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.overwriteNoExpire', { importance: node.nodeData.overwrite.importance }); } // node.debug(`overwrite exit true node.nodeData.overwrite.active=${node.nodeData.overwrite.active}`); return true; } // node.debug(`overwrite exit true node.nodeData.overwrite.active=${node.nodeData.overwrite.active}`); return false; } /******************************************************************************************/ /** * pre-checking conditions to may be able to store temp data * @param {ITimeControlNode} node node data * @param {Object} msg the message object * @param {Object} tempData the temporary storage object * @param {Date} dNow simple Date Object */ 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.timeResult; } if (rule.conditional) { rule.conditonResult = { index : -1, result : false }; for (let i = 0; i < rule.conditions.length; i++) { const el = rule.conditions[i]; if (rule.conditonResult.result === true && el.condition === cRuleLogOperatorOr) { break; // not nessesary, becaue already tue } else if (rule.conditonResult.result === false && el.condition === cRuleLogOperatorAnd) { break; // should never bekome true } delete el.valueWorth; delete el.thresholdWorth; if (el.valueType === 'sunControlMode') { el.result = (node.sunData && (node.sunData.mode === el.value)); } else { el.result = node.positionConfig.comparePropValue(node, msg, { value: el.value, type: el.valueType, // @ts-ignore expr: el.valueExpr, callback: (result, _obj, cachable) => { // opCallback el.valueWorth = _obj.value; return evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, noError:false, now: dNow }, el.operator, { value: el.threshold, type: el.thresholdType, callback: (result, _obj, cachable) => { // opCallback el.thresholdWorth = _obj.value; return evalTempData(node, _obj.type, _obj.value, result, tempData, cachable); }, noError:false, now: dNow } ); } rule.conditonResult = { index : i, result : el.result, text : el.text, textShort : el.textShort }; if (typeof el.thresholdWorth !== 'undefined') { rule.conditonResult.text += ' ' + el.thresholdWorth; rule.conditonResult.textShort += ' ' + hlp.clipStrLength(el.thresholdWorth, 10); } } } } } /** * get time constrainty of a rule * @param {ITimeControlNode} node node data * @param {Object} msg the message object * @param {IRuleData} rule the rule data * @param {('start'|'end')} timep rule type * @param {Date} dNow base timestamp * @param {number} def default value */ function getRuleTimeData(node, msg, rule, timep, dNow, def) { if (!rule.timeResult) { rule.timeResult = {}; } if (!rule.time || !rule.time[timep]) { Object.assign(rule.timeResult, { [timep]: { ts: def } }); return; } rule.time[timep].now = dNow; Object.assign(rule, { timeResult: { [timep]: node.positionConfig.getTimeProp(node, msg, rule.time[timep]) } }); if (rule.timeResult[timep].error) { hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeResult[timep].error }), undefined, rule.timeResult[timep].error); Object.assign(rule.timeResult, { [timep]: { ts: def } }); // node.debug('rule data complete'); // node.debug(util.inspect(rule, { colors: true, compact: 10, depth: 10, breakLength: Infinity })); return; } else if (!rule.timeResult[timep].value) { throw new Error('Error can not calc time!'); } rule.timeResult[timep].source = 'default'; rule.timeResult[timep].ts = rule.timeResult[timep].value.getTime(); // node.debug(`time=${rule.timeResult[timep].value} -> ${new Date(rule.timeResult[timep].value)}`); if (rule.time[timep].min) { rule.time[timep].min.now = dNow; // @ts-ignore if (!rule.timeResultMin) rule.timeResultMin = { start:{}, end:{} }; rule.timeResultMin[timep] = node.positionConfig.getTimeProp(node, msg, rule.time[timep].min); rule.timeResultMin[timep].source = 'min'; if (rule.timeResultMin[timep].error) { hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeResultMin[timep].error }), undefined, rule.timeResultMin[timep].error); } else if (!rule.timeResultMin[timep].value) { throw new Error('Error can not calc minimum time!'); } else { rule.timeResultMin[timep].ts = rule.timeResultMin[timep].value.getTime(); if (rule.timeResultMin[timep].ts > rule.timeResult[timep].ts) { [rule.timeResult[timep], rule.timeResultMin[timep]] = [rule.timeResultMin[timep], rule.timeResult[timep]]; } } } if (rule.time[timep].max) { rule.time[timep].max.now = dNow; // @ts-ignore if (!rule.timeResultMax) rule.timeResultMax = { start:{}, end:{} }; rule.timeResultMax[timep] = node.positionConfig.getTimeProp(node, msg, rule.time[timep].max); rule.timeResultMax[timep].source = 'max'; if (rule.timeResultMax[timep].error) { hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeResultMax[timep].error }), undefined, rule.timeResultMin[timep].error); } else if (!rule.timeResultMax[timep].value) { throw new Error('Error can not calc maximum time!'); } else { rule.timeResultMax[timep].ts = rule.timeResultMax[timep].value.getTime(); if (rule.timeResultMax[timep].ts < rule.timeResult[timep].ts) { [rule.timeResult[timep], rule.timeResultMax[timep]] = [rule.timeResultMax[timep], rule.timeResult[timep]]; } } } rule.timeResult[timep].dayId = hlp.getDayId(rule.timeResult[timep].value); return; } /*************************************************************************************************************************/ /** * function to check a rule * @param {ITimeControlNode} node the node object * @param {Object} msg the message object * @param {IRuleData} rule a rule object to test * @param {ITimeObject|function} tDataOrFilter Now time object or compatibility callback * @param {ITimeObject} [tData] Now time object * @returns {IRuleData|null} returns the rule if rule is valid, otherwhise null */ function compareRules(node, msg, rule, tDataOrFilter, tData) { // node.debug(`compareRules rule ${rule.name} (${rule.pos}) rule=${util.inspect(rule, {colors:true, compact:10})}`); if (rule.conditional) { try { if (!rule.conditonResult.result) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) conditon does not match`); return null; } } catch (err) { node.warn(RED._('node-red-contrib-sun-position/position-config:errors.getPropertyData', err)); node.debug(util.inspect(err)); return null; } } if (!rule.time) { return rule; } if (rule.time && typeof rule.time.operator !== 'undefined' && !rule.time.start && !rule.time.end) { const ttype = (rule.time.operator === cNBC_RULE_TYPE_FROM) ? 'start' : 'end'; 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, next: false }, rule.time[ttype]); 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; } const evaluateRuleForDate = (candidateDate, candidateTs) => { const candidateDayNr = candidateDate.getDate(); const candidateMonthNr = candidateDate.getMonth(); const candidateWeekNr = hlp.getWeekOfYear(candidateDate)[1]; const candidateYearNr = candidateDate.getFullYear(); // @ts-ignore if (rule.time.days && !rule.time.days.includes(candidateDayNr)) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid days`); return false; } // @ts-ignore if (rule.time.months && !rule.time.months.includes(candidateMonthNr)) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid month`); return false; } if (rule.time.onlyOddDays && (candidateDayNr % 2 === 0)) { // even node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even days`); return false; } if (rule.time.onlyEvenDays && (candidateDayNr % 2 !== 0)) { // odd node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd days`); return false; } if (rule.time.onlyOddWeeks && (candidateWeekNr % 2 === 0)) { // even node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even week`); return false; } if (rule.time.onlyEvenWeeks && (candidateWeekNr % 2 !== 0)) { // odd node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd week`); return false; } if (rule.time.dateStart || rule.time.dateEnd) { const dateStart = new Date(rule.time.dateStart); const dateEnd = new Date(rule.time.dateEnd); dateStart.setFullYear(candidateYearNr); dateEnd.setFullYear(candidateYearNr); if (dateEnd > dateStart) { // in the current year if (candidateDate < dateStart || candidateDate > dateEnd) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range within year`); return false; } } else { // switch between year from end to start if (candidateDate < dateStart && candidateDate > dateEnd) { node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range over year`); return false; } } } return false; }; const nowData = (typeof tDataOrFilter === 'function') ? tData : tDataOrFilter; const nowNr = nowData.nowNr; const dayId = nowData.dayId; rule.timeResult = { now: nowData.now }; if (rule.time.start) { getRuleTimeData(node, msg, rule, 'start', nowData.now, Number.MIN_VALUE); if (rule.time.end) { getRuleTimeData(node, msg, rule, 'end', nowData.now, Number.MAX_VALUE); if (rule.timeResult.start.ts > rule.timeResult.end.ts) { if ((dayId === rule.timeResult.start.dayId && rule.timeResult.start.ts <= nowNr) || (dayId === rule.timeResult.end.dayId && rule.timeResult.end.ts > nowNr)) { return rule; } return null; } if (rule.timeResult.start.ts <= nowNr && dayId === rule.timeResult.start.dayId && rule.timeResult.end.ts > nowNr && dayId === rule.timeResult.end.dayId) { return rule; } } if (rule.timeResult.start.ts <= nowNr) { if (!rule.time.end) { return rule; } if (dayId === rule.timeResult.start.dayId) { return rule; } } } else if (rule.time.end) { getRuleTimeData(node, msg, rule, 'end', nowData.now, Number.MAX_VALUE); if (rule.timeResult.end.ts > nowNr && dayId === rule.timeResult.end.dayId) { return rule; } } // node.debug(`compareRules rule ${rule.name} (${rule.pos}) dayId=${dayId} rule-DayID=${rule.timeResult[timep].dayId} num=${num} invalid time`); return null; } /******************************************************************************************/ /** * check all rules and determinate the active rule * @param {ITimeControlNode} 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 {IRuleResultData} the active rule or null */ function getActiveRule(node, msg, oNow, tempData) { // node.debug('getActiveRule --------------------'); prepareRules(node, msg, tempData, oNow.now); // node.debug(`getActiveRule rules.count=${node.rules.count}, rules.last1stRun=${node.rules.last1stRun}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`); /** @type {IRuleResultData} */ const result = { ruleindex : -1, ruleSel : null }; const setRes = (i, res) => { if (res) { // node.debug('new 1. ruleSel ' + util.inspect(res, { colors: true, compact: 5, breakLength: Infinity, depth: 10 })); if (res.level && res.level.operator === cRuleType.slatOversteer) { result.ruleSlatOvs = res; } else if (res.level && res.level.operator === cRuleType.topicOversteer) { result.ruleTopicOvs = res; } else if (res.level && res.level.operator === cRuleType.levelMinOversteer) { result.ruleSelMin = res; } else if (res.level && res.level.operator === cRuleType.levelMaxOversteer) { result.ruleSelMax = res; } else { result.ruleSel = res; result.ruleindex = i; return true; } } return false; }; for (let i = 0; i <= node.rules.last1stRun; ++i) { const rule = node.rules.data[i]; // node.debug('rule ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.last) { continue; } if (setRes(i, compareRules(node, msg, rule, oNow))) break; } if (!result.ruleSel) { // 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 ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })); if (!rule.enabled || rule.execUse === cNBC_RULE_EXEC.first) { continue; } if (setRes(i, compareRules(node, msg, rule, oNow))) break; } } if (node.autoTrigger) { const setAutoTrigger = (ts, type) => { const d = new Date(); d.setHours(24,0,0,1); // next midnight const diff = ts - oNow.nowNr; node.autoTrigger.time = Math.min(node.autoTrigger.time, diff, d.getTime()); node.autoTrigger.type = type; // current rule end }; if (result.ruleSel && result.ruleSel.time && result.timeResult && result.timeResult.end && result.ruleSel.timeResult.end.ts > oNow.nowNr) { node.debug('autoTrigger set to current rule ' + result.ruleSel.pos + ' end'); setAutoTrigger(result.ruleSel.timeResult.end.ts, 1); } else { const times = []; for (let i = (result.ruleindex+1); i < node.rules.count; ++i) { const rule = node.rules.data[i]; if (!rule.time) { continue; } rule.timeResult = { now: oNow }; if (rule.time.start) { getRuleTimeData(node, msg, rule, 'start', oNow, Number.MIN_VALUE); if (rule.timeResult.start.ts > oNow.nowNr) { times.push(rule.timeResult.start.ts); } } if (rule.time.end) { getRuleTimeData(node, msg, rule, 'end', oNow, Number.MAX_VALUE); if (rule.timeResult.end.ts > oNow.nowNr) { times.push(rule.timeResult.end.ts); } } } if (times.length > 0) { times.sort(); node.debug('autoTrigger set to next rule time ' + times[0]); setAutoTrigger(times[0], 2); } else { // check maybe next day const d = new Date(); d.setHours(24,0,0,1); // after next midnight node.autoTrigger.time = Math.min(node.autoTrigger.time, d.getTime()); node.autoTrigger.type = 9; // no rule based autotrigger } } } return result; } /*************************************************************************************************************************/ /** * check if a level has a valid value * @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 (typeof level !== 'number' || level === null || typeof level === 'undefined' || isNaN(level)) { node.warn(`Position: "${String(level)}" is empty or not a valid number!`); return false; } if (level < node.nodeData.levelBottom) { if (node.levelReverse) { node.warn(`Position: "${level}" < open level ${node.nodeData.levelBottom}`); } else { node.warn(`Position: "${level}" < closed level ${node.nodeData.levelBottom}`); } return false; } if (level > node.nodeData.levelTop) { if (node.levelReverse) { node.warn(`Position: "${level}" > closed level ${node.nodeData.levelTop}`); } else { node.warn(`Position: "${level}" > open level ${node.nodeData.levelTop}`); } return false; } if (Number.isInteger(node.nodeData.levelTop) && Number.isInteger(node.nodeData.levelBottom) && Number.isInteger(node.nodeData.increment) && ((level % node.nodeData.increment !== 0) || !Number.isInteger(level) )) { node.warn(`Position invalid "${level}" not fit to increment ${node.nodeData.increment}`); return false; } if (allowRound) { return true; } return Number.isInteger(Number((level / node.nodeData.increment).toFixed(hlp.countDecimals(node.nodeData.increment) + 2))); } // #################################################################################################### /** * initializes the node * @param {runtimeRED} REDLib the level to check * @param {ITimeControlNode} node the node data * @param {Object} config the level to check */ function initializeCtrl(REDLib, node, config) { node.debug(`initialize ${ node.name || node._path || node.id}`); RED = REDLib; const getName = (type, value) => { if (type === 'num') { return value; } else if (type === 'str') { return '"' + value + '"'; } else if (type === 'bool') { return '"' + value + '"'; } else if (type === 'global' || type === 'flow') { value = value.replace(/^#:(.+)::/, ''); } return type + '.' + value; }; const getNameShort = (type, value) => { if (type === 'num') { return value; } else if (type === 'str') { return '"' + hlp.clipStrLength(value,20) + '"'; } else if (type === 'bool') { return '"' + value + '"'; } else if (type === 'global' || type === 'flow') { value = value.replace(/^#:(.+)::/, ''); // special for Homematic Devices if (/^.+\[('|").{18,}('|")\].*$/.test(value)) { value = value.replace(/^.+\[('|")/, '').replace(/('|")\].*$/, ''); if (value.length > 25) { return '...' + value.slice(-22); } return value; } } if ((type + value).length > 25) { return type + '...' + value.slice(-22); } return type + '.' + value; }; node.results = []; config.results.forEach(prop => { const propNew = { outType : prop.pt, outValue : prop.p, type : prop.vt, value : prop.v }; if (this.positionConfig && propNew.type === 'jsonata') { try { propNew.expr = this.positionConfig.getJSONataExpression(this, propNew.value); } catch (err) { this.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error: err.message })); propNew.expr = null; } } node.results.push(propNew); }); // Prepare Rules node.rules.count = node.rules.data.length; node.rules.last1stRun = node.rules.count -1; 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 = []; // @ts-ignore if (rule.validOperandAType && rule.validOperandAType !== 'none') { rule.conditions.push({ condition : cRuleLogOperatorOr, conditionText : '', // @ts-ignore value : rule.validOperandAValue, // @ts-ignore valueType : rule.validOperandAType, // @ts-ignore operator : rule.validOperator, // @ts-ignore operatorText : rule.validOperatorText, // @ts-ignore threshold : rule.validOperandBValue, // @ts-ignore thresholdType : (rule.validOperandBType || 'num'), result : false }); // @ts-ignore const conditionValue = parseInt(rule.valid2LogOperator); // @ts-ignore if (conditionValue > 0 && rule.valid2OperandAType) { rule.conditions.push({ // @ts-ignore condition : conditionValue, // @ts-ignore conditionText : rule.valid2LogOperatorText, // @ts-ignore value : rule.valid2OperandAValue, // @ts-ignore valueType : (rule.valid2OperandAType || 'msg'), // @ts-ignore operator : rule.valid2Operator, // @ts-ignore operatorText : rule.valid2OperatorText, // @ts-ignore threshold : rule.valid2OperandBValue, // @ts-ignore thresholdType : (rule.valid2OperandBType || 'num'), result : false }); } } // @ts-ignore delete rule.validOperandAValue; // @ts-ignore delete rule.validOperandAType; // @ts-ignore delete rule.validOperator; // @ts-ignore delete rule.validOperatorText; // @ts-ignore delete rule.validOperandBValue; // @ts-ignore delete rule.validOperandBType; // @ts-ignore delete rule.valid2LogOperator; // @ts-ignore delete rule.valid2LogOperatorText; // @ts-ignore delete rule.valid2OperandAValue; // @ts-ignore delete rule.valid2OperandAType; // @ts-ignore delete rule.valid2Operator; // @ts-ignore delete rule.valid2OperatorText; // @ts-ignore delete rule.valid2OperandBValue; // @ts-ignore delete rule.valid2OperandBType; // @ts-ignore } // @ts-ignore if(rule.timeType) { // @ts-ignore if (!rule.time && rule.timeType !== 'none') { // @ts-ignore const operator = (parseInt(rule.timeOp) || cNBC_RULE_TYPE_UNTIL); rule.time = { }; /** @type {('start'|'end')} */ let ttype = 'end'; // cNBC_RULE_TYPE_UNTIL if (operator === cNBC_RULE_TYPE_FROM) { // @ts-ignore rule.time.start = {}; ttype = 'start'; } rule.time[ttype] = { // @ts-ignore type : rule.timeType, // @ts-ignore value : (rule.timeValue || ''), // @ts-ignore offsetType : (rule.offsetType || 'none'), // @ts-ignore offset : (rule.offsetValue || 1), // @ts-ignore multiplier : (parseInt(rule.multiplier) || hlp.TIME_1min), next : false, // @ts-ignore days : (rule.timeDays || '*'), // @ts-ignore months : (rule.timeMonths || '*') }; // @ts-ignore if (rule.timeMinType && rule.timeMinType !== 'none') { rule.time[ttype].min = { // @ts-ignore type : rule.timeMinType, // @ts-ignore value : (rule.timeMinValue || ''), // @ts-ignore offsetType : (rule.offsetMinType || 'none'), // @ts-ignore offset : (rule.offsetMinValue || 1), // @ts-ignore multiplier : (parseInt(rule.multiplierMin) || 60000), next : false }; } // @ts-ignore if (rule.timeMaxType && rule.timeMaxType !== 'none') { rule.time[ttype].max = { // @ts-ignore type : rule.timeMaxType, // @ts-ignore value : (rule.timeMaxValue || ''), // @ts-ignore offsetType : (rule.offsetMaxType || 'none'), // @ts-ignore offset : (rule.offsetMaxValue || 1), // @ts-ignore multiplier : (parseInt(rule.multiplierMax) || 60000), next : false }; } } // @ts-ignore if (rule.timeDays && rule.timeDays !== '*') rule.time.days = rule.timeDays; // @ts-ignore if (rule.timeMonths && rule.timeMonths !== '*') rule.time.months = rule.timeMonths; // @ts-ignore if (rule.timeOnlyOddDays) rule.time.onlyOddDays = rule.timeOnlyOddDays; // @ts-ignore if (rule.timeOnlyEvenDays) rule.time.onlyEvenDays = rule.timeOnlyEvenDays; // @ts-ignore if (rule.timeDateStart) rule.time.dateStart = rule.timeDateStart; // @ts-ignore if (rule.timeDateEnd) rule.time.dateEnd = rule.timeDateEnd; // @ts-ignore delete