UNPKG

node-red-contrib-sun-position

Version:
648 lines (613 loc) 30.3 kB
/* * This code is licensed under the Apache License Version 2.0. * * Copyright (c) 2022 Robert Gester * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * */ /******************************************** * 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.<number>} timeDays - ?? * @property {Array.<number>} timeMonths - ?? * * * @property {NodeJS.Timer} timeOutObj - ?? * @property {*} lastMsgObj - ?? * * @property {number} tsCompare - ?? */ /** * @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'); const path = require('path'); const hlp = require(path.join(__dirname, '/lib/dateTimeHelper.js')); /** * 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; case 4: id = 'msg.value'; value = msg.value; 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(); } /** * set the node state * @param {*} node - the node Data * @param {*} data - the state data * @returns {boolean} */ function setstate(node, data, reverse) { if (data.error) { node.status({ fill: 'red', shape: 'dot', text: data.error }); return false; } if (data.warn) { node.status({ fill: 'yellow', shape: 'dot', text: data.warn }); return false; } if (data.start && data.start.error) { hlp.handleError(node, RED._('within-time-switch.errors.error-start-time', { message : data.start.error}), undefined, data.start.error); } 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) { 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; } /** 2024-06-05 - jonferreira quick hack - Change evaluateJSONataExpression to use callback **/ function evaluateJSONataExpression(expr, message) { return new Promise((resolve, reject) => { RED.util.evaluateJSONataExpression(expr, message, (err, res) => { if (err) { reject(err); } else { resolve(res); } }); }); } /** * calc the start and end times * @param {*} node - thje noide data * @param {*} msg - the messege object * @param {Date|null} dNow - the current time * @returns {object} containing start and end Dates */ function calcWithinTimes(node, msg, dNow) { // node.debug('calcWithinTimes'); const result = { start: {}, end: {}, startSuffix: '', endSuffix: '', altStartTime: (node.propertyStart.type !== 'none') && (msg || (node.propertyStart.type !== 'msg')), altEndTime: (node.propertyEnd.type !== 'none') && (msg || (node.propertyEnd.type !== 'msg')), valid: false, warn: '' }; if (node.timeRestrictions.type !== 'none') { try { if (node.timeRestrictions.type === 'jsonata') { if (!node.timeRestrictions.expr) { node.timeRestrictions.expr = this.getJSONataExpression(node, node.timeRestrictions.value); } // node.timeRestrictions.data = RED.util.evaluateJSONataExpression(node.timeRestrictions.expr, msg); node.timeRestrictions.data = evaluateJSONataExpression(node.timeRestrictions.expr, msg); } else { node.timeRestrictions.data = RED.util.evaluateNodeProperty(node.timeRestrictions.value, node.timeRestrictions.type, node, msg); } if (typeof node.timeRestrictions.data === 'object') { // node.debug(util.inspect(node.timeRestrictions, Object.getOwnPropertyNames(node.timeRestrictions))); if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'days')) { node.timeDays = node.timeRestrictions.data.days; } else { delete node.timeDays; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'months')) { node.timeMonths = node.timeRestrictions.data.months; } else { delete node.timeMonths; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'startDate') && node.timeRestrictions.data.startDate !== '') { node.timeStartDate = node.timeRestrictions.data.startDate; } else { delete node.timeStartDate; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'endDate') && node.timeRestrictions.data.endDate !== '') { node.timeEndDate = node.timeRestrictions.data.endDate; } else { delete node.timeEndDate; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'onlyOddDays')) { node.timeOnlyOddDays = node.timeRestrictions.data.onlyOddDays; } else { delete node.timeOnlyOddDays; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'onlyEvenDays')) { node.timeOnlyEvenDays = node.timeRestrictions.data.onlyEvenDays; } else { delete node.timeOnlyEvenDays; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'onlyOddWeeks')) { node.timeOnlyOddWeeks = node.timeRestrictions.data.onlyOddWeeks; } else { delete node.timeOnlyOddWeeks; } if (Object.prototype.hasOwnProperty.call(node.timeRestrictions.data,'onlyEvenWeeks')) { node.timeOnlyEvenWeeks = node.timeRestrictions.data.onlyEvenWeeks; } else { delete node.timeOnlyEvenWeeks; } } } catch (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)); 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)); result.warn = RED._('within-time-switch.errors.invalid-month'); return result; } if ((typeof node.timeStartDate !== 'undefined') || (typeof node.timeEndDate !== 'undefined')) { let dStart,dEnd; if (typeof node.timeStartDate !== 'undefined') { dStart = new Date(node.timeStartDate); dStart.setFullYear(dNow.getFullYear()); dStart.setHours(0, 0, 0, 1); } else { dStart = new Date(dNow.getFullYear(), 0, 0, 0, 0, 0, 1); } if (typeof node.timeEndDate !== 'undefined') { dEnd = new Date(node.timeEndDate); dEnd.setFullYear(dNow.getFullYear()); dEnd.setHours(23, 59, 59, 999); } else { dEnd = new Date(dNow.getFullYear(), 11, 31, 23, 59, 59, 999); } if (dStart < dEnd) { // in the current year - e.g. 6.4. - 7.8. if (dNow < dStart || dNow > dEnd) { result.warn = RED._('within-time-switch.errors.invalid-daterange'); return result; } } else { // switch between year from end to start - e.g. 2.11. - 20.3. if (dNow < dStart && dNow > dEnd) { result.warn = RED._('within-time-switch.errors.invalid-daterange'); return result; } } } const dateNr = dNow.getDate(); if (node.timeOnlyOddDays && (dateNr % 2 === 0)) { // even result.warn = RED._('within-time-switch.errors.only-odd-day'); return result; } if (node.timeOnlyEvenDays && (dateNr % 2 !== 0)) { // odd result.warn = RED._('within-time-switch.errors.only-even-day'); return result; } if (node.timeOnlyOddWeeks) { const weekNr = hlp.getWeekOfYear(dNow)[1]; if (weekNr % 2 === 0) { // even result.warn = RED._('within-time-switch.errors.only-odd-week'); return result; } } if (node.timeOnlyEvenWeeks) { const weekNr = hlp.getWeekOfYear(dNow[1]); if (weekNr % 2 !== 0) { // odd result.warn = RED._('within-time-switch.errors.only-even-week'); return result; } } result.valid = true; if (result.altStartTime) { // node.debug('alternate start times enabled ' + node.propertyStart.type + '.' + node.propertyStart.value); try { node.propertyStart.now = dNow; node.propertyStartThreshold.now = dNow; result.altStartTime = node.positionConfig.comparePropValue(node, msg, node.propertyStart, node.propertyStartOperator, node.propertyStartThreshold); } catch (err) { result.altStartTime = false; hlp.handleError(node, RED._('within-time-switch.errors.invalid-propertyStart-type', node.propertyStart), err); node.log(util.inspect(err)); } } if (result.altStartTime) { // node.debug(`using alternate start time node.timeStart=${ util.inspect(node.timeStart, Object.getOwnPropertyNames(node.timeStart))}, config.timeStartAlt=${ util.inspect(node.timeStartAlt, Object.getOwnPropertyNames(node.timeStartAlt))}`); result.start = node.positionConfig.getTimeProp(node, msg, node.timeStartAlt); result.startSuffix = '⎇ '; } else if (msg || (node.timeStart.type !== 'msg')) { // node.debug(`using alternate start time node.timeStart=${ util.inspect(node.timeStart, Object.getOwnPropertyNames(node.timeStart))}`); result.start = node.positionConfig.getTimeProp(node, msg, node.timeStart); } if (result.altEndTime) { // node.debug('alternate end times enabled ' + node.propertyEnd.type + '.' + node.propertyEnd.value); try { node.propertyEnd.now = dNow; node.propertyEndThreshold.now = dNow; result.altEndTime = node.positionConfig.comparePropValue(node, msg, node.propertyEnd, node.propertyEndOperator, node.propertyEndThreshold); } catch (err) { result.altEndTime = false; hlp.handleError(node, RED._('within-time-switch.errors.invalid-propertyEnd-type', node.propertyEnd), err); } } if (result.altEndTime) { // node.debug(`using alternate start time node.timeEnd=${ util.inspect(node.timeEnd, Object.getOwnPropertyNames(node.timeEnd))}, config.timeEndAlt=${ util.inspect(node.timeEndAlt, Object.getOwnPropertyNames(node.timeEndAlt))}`); result.end = node.positionConfig.getTimeProp(node, msg, node.timeEndAlt); result.endSuffix = ' ⎇'; } else if (msg || (node.timeEnd.type !== 'msg')) { // node.debug(`using alternate start time node.timeEnd=${ util.inspect(node.timeEnd, Object.getOwnPropertyNames(node.timeEnd))}`); result.end = node.positionConfig.getTimeProp(node, msg, node.timeEnd); } // node.debug(util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); return result; } /******************************************************************************************/ /** * 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); // 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 (node.positionConfig.checkNode( error => { const text = RED._('node-red-contrib-sun-position/position-config:errors.config-error', { error }); node.error(text); node.status({fill: 'red', shape: 'dot', text }); return true; }, false)) { return; } node.timeStart = { type: config.startTimeType, value : config.startTime, offsetType : config.startOffsetType, offset : config.startOffset, multiplier : config.startOffsetMultiplier, next : false }; node.timeEnd = { type: config.endTimeType, value : config.endTime, offsetType : config.endOffsetType, offset : config.endOffset, multiplier : config.endOffsetMultiplier, next : false }; node.timeStartAlt = { type: config.startTimeAltType || 'none', value : config.startTimeAlt, offsetType : config.startOffsetAltType, offset : config.startOffsetAlt, multiplier : config.startOffsetAltMultiplier, next : false }; node.propertyStart = { type : config.propertyStartType || 'none', value : config.propertyStart || '' }; 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 = { type: config.endTimeAltType || 'none', value : config.endTimeAlt, offsetType : config.endOffsetAltType, offset : config.endOffsetAlt, multiplier : config.endOffsetAltMultiplier, next : false }; node.propertyEnd = { type : config.propertyEndType || 'none', value : config.propertyEnd || '' }; 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 = { type: config.timeRestrictionsType || 'none', value : config.timeRestrictions }; if (node.timeRestrictions.type === 'jsonata') { try { node.timeRestrictions.expr = node.positionConfig.getJSONataExpression(node, node.timeRestrictions.value); } catch (err) { node.error(RED._('node-red-contrib-sun-position/position-config:errors.invalid-expr', { error:err.message })); node.timeRestrictions.expr = null; } } 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 (node.timeOnlyEvenDays && node.timeOnlyOddDays) { node.timeOnlyEvenDays = false; node.timeOnlyOddDays = false; } if (node.timeOnlyEvenWeeks && node.timeOnlyOddWeeks) { node.timeOnlyEvenWeeks = false; node.timeOnlyOddWeeks = false; } if (typeof config.timedatestart !== 'undefined' && config.timedatestart !== '') { node.timeStartDate = new Date(config.timedatestart); } if (typeof config.timedateend !== 'undefined' && 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 node.timeDays; } else { const tmp = config.timeDays.split(','); node.timeDays = tmp.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 node.timeMonths; } else { const tmp = config.timeMonths.split(','); node.timeMonths = tmp.map( e => parseInt(e) ); } } node.withinTimeValue = { value : config.withinTimeValue ? config.withinTimeValue : 'true', type : config.withinTimeValueType ? config.withinTimeValueType : 'msgInput' }; if (node.withinTimeValue.type === 'input') { node.withinTimeValue.type = 'msgInput'; } node.outOfTimeValue = { value : config.outOfTimeValue ? config.outOfTimeValue : 'false', type : config.outOfTimeValueType ? config.outOfTimeValueType : 'msgInput' }; if (node.outOfTimeValue.type === 'input') { node.outOfTimeValue.type = 'msgInput'; } 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 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('--------- within-time-switch - 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 })); const dNow = getIntDate(node.tsCompare, msg, node); const result = calcWithinTimes(this, msg, dNow); if (result.valid && result.start.value && result.end.value) { msg.withinTimeStart = result.start; msg.withinTimeEnd = result.end; msg.withinTimeStart.id = hlp.getTimeNumberUTC(result.start.value); msg.withinTimeEnd.id = hlp.getTimeNumberUTC(result.end.value); const cmpNow = hlp.getTimeNumberUTC(dNow); 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 + node.positionConfig.toDateTimeString(dNow) + result.endSuffix + ' (' + msg.withinTimeStart.id + ' - ' + cmpNow + ' - ' + msg.withinTimeEnd.id + ')'); if (node.withinTimeValue.type === 'msgInput') { send([msg, null]); // within time } else { const resultMsg = RED.util.cloneMessage(msg); resultMsg.payload = node.positionConfig.getOutDataProp(node, msg, node.withinTimeValue, dNow); send([resultMsg, null]); // within time } done(); 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 + ')'); if (node.withinTimeValue.type === 'msgInput') { send([msg, null]); // within time } else { const resultMsg = RED.util.cloneMessage(msg); resultMsg.payload = node.positionConfig.getOutDataProp(node, msg, node.withinTimeValue, dNow); send([resultMsg, null]); // within time } done(); return null; } else { setstate(node, result, true); } } else { 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); if (node.outOfTimeValue.type === 'msgInput') { send([null, msg]); // out of time } else { const resultMsg = RED.util.cloneMessage(msg); resultMsg.payload = node.positionConfig.getOutDataProp(node, msg, node.outOfTimeValue, dNow); send([null, resultMsg]); // out of time } done(); return null; } catch (err) { node.log(err.message); 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); } return null; }); node.status({}); return; } RED.nodes.registerType('within-time-switch', withinTimeSwitchNode); };