UNPKG

node-red-contrib-sun-position

Version:
1,067 lines (995 loc) 126 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. * */ /******************************************************************************************************* * position-config: ******************************************************************************************************/ 'use strict'; /******************************************************************************************************* * --- imported type definitions --- ******************************************************************************************************/ /** --- 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("suncalc3").ISunTimeDef} ISunTimeDef * @typedef {import("suncalc3").ISunTimeSingle} ISunTimeSingle * @typedef {import("suncalc3").ISunTimeList} ISunTimeList * @typedef {import("suncalc3").ISunPosition} ISunPosition * @typedef {import("suncalc3").IMoonPosition} IMoonPosition * @typedef {import("suncalc3").IMoonIllumination} IMoonIllumination * @typedef {import("suncalc3").IMoonData} IMoonData * @typedef {import("suncalc3").IMoonTimes} IMoonTimes */ /******************************************************************************************************* * --- generic type definitions --- ******************************************************************************************************/ /** * @typedef {Object} IPositionConfigNode Extensions for the nodeInstance object type * @property {boolean} valid * @property {number} latitude * @property {number} longitude * @property {('deg'|'rad')} angleType * @property {number} tzOffset * @property {number} tzDST * @property {string} contextStore * @property {object} cache * * @property {FktRegister} register register a node as child * @property {FktDeregister} 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 {FktGetCustomAngles} getCustomAngles get list of custom angles * @property {FktGetSunTimesList} getSunTimesList get list of all suntimes including custom ones * @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 {FktGetJSONataExpression} getJSONataExpression get a prepared JSONATA Expression * @property {FktGetPropValue} getPropValue get a property value from a type input in Node-Red * @property {FktComparePropValue} comparePropValue compared two property's * @property {FktGetSunCalc} getSunCalc * @property {FktGetMoonCalc} getMoonCalc * ... obviously there are more ... */ /******************************************************************************************************* * --- Interfaces --- ******************************************************************************************************/ /** * This callback is displayed as a global member. * @callback IValuePropertyTypeCallback * @param {*} result - the result of the property value from a type input in Node-Red * @param {IValuePropertyType} data - the given data to the function * @param {boolean} cachable - boolean which is true if the value is cachable * @returns {*} value of the type input */ /** * @typedef {Object} ITypedValue * @property {String} type - type of the value * @property {*} value - value */ /** * @typedef {Object} IValuePropertyType * @property {String} type - type of the value * @property {*} value - value * @property {*} [expr] - optional prepared Jsonata expression * @property {IValuePropertyTypeCallback} [callback] - function which should be called after value was recived * @property {Boolean} [noError] - true if no error shoudl be given in GUI * @property {Date} [now] base Date to use for Date time functions */ /** * @typedef {Object} INodeCacheSunData * @property {ISunTimeList} times * @property {Number} dayId * @property {ISunPosition} [sunPosAtSolarNoon] */ /** * @typedef {Object} INodeCacheMoonData * @property {IMoonTimes} times * @property {Number} dayId * @property {IMoonPosition} positionAtRise * @property {IMoonPosition} positionAtSet */ /** * @typedef {Object} INodeCacheData * @property {ISunDataResult|{ts: number}} lastSunCalc - last mooncalc * @property {Object} lastMoonCalc - last mooncalc * @property {INodeCacheSunData} sunTimesToday - last mooncalc * @property {INodeCacheSunData} sunTimesTomorrow - last mooncalc * @property {INodeCacheSunData} sunTimesAdd1 - last mooncalc * @property {INodeCacheSunData} sunTimesAdd2 - last mooncalc * @property {INodeCacheMoonData} moonTimesToday - last mooncalc * @property {INodeCacheMoonData} moonTimesTomorrow - last mooncalc * @property {INodeCacheMoonData} moonTimes2Days - last mooncalc */ /** * @typedef {Object} ISunDataResult * @property {Number} ts - the date of the calculated sun data as timestamp * @property {Date} lastUpdate - the date of the calculated sun data * @property {String} lastUpdateStr - date as string * @property {Number} latitude - latitude * @property {Number} longitude - longitude * @property {Number} height -observer height * @property {('deg'|'rad')} angleType * @property {Number} azimuth * @property {Number} altitude * @property {Number} altitudeDegrees * @property {Number} azimuthDegrees * @property {Number} altitudeRadians * @property {Number} azimuthRadians * @property {ISunTimeList} times * @property {ISunPosition} [positionAtSolarNoon] * @property {Number} [altitudePercent] */ /** * @typedef {Object} IMoonDataResult * @property {Number} ts - the date of the calculated sun data as timestamp * @property {Date} lastUpdate - the date of the calculated sun data * @property {String} lastUpdateStr - date as string * @property {Number} latitude - latitude * @property {Number} longitude - longitude * @property {('deg'|'rad')} angleType * @property {Number} azimuth * @property {Number} altitude * @property {Number} altitudeDegrees * @property {Number} azimuthDegrees * @property {Number} altitudeRadians * @property {Number} azimuthRadians * @property {Number} distance * @property {Number} parallacticAngle * @property {IMoonIllumination} illumination * @property {Number} zenithAngle * @property {IMoonTimes} times * @property {IMoonTimes} [timesNext] * @property {IMoonPosition} [positionAtRise] * @property {IMoonPosition} [positionAtSet] * @property {Number} [altitudePercent] * @property {IMoonPosition} [highestPosition] * @property {Boolean} [isUp] */ /** * @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 */ /** * @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} IOffsetData * @property {String} [offset] - value of the offset * @property {String} [offsetType] - type name of the offset * @property {IValuePropertyTypeCallback} [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] - latitude * @property {number} [longitude] - longitude * @property {number} [height] - height definition * @property {*} [expr] - optional prepared Jsonata expression * * @typedef {ITimePropertyTypeInt & ILimitationsObj & ITypedValue & IOffsetData} ITimePropertyType */ /** * get a float value from a type input in Node-Red * @typedef {Object} IGetFloatPropData * @property {String} type - type of the value * @property {*} value - value * @property {*} [expr] - optional prepared Jsonata expression * @property {number} [def] - default value if can not get float value * @property {IValuePropertyTypeCallback} [callback] - callback function for getting getPropValue * @property {Boolean} [noError] - true if no error should be given in GUI * @property {Date} [now] base Date to use for Date time functions */ /** * @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 */ /******************************************************************************************************* * --- Functions --- ******************************************************************************************************/ /** * check this node for configuration errors * @typedef {function} FktRegister * @param {runtimeNode} srcnode node to register as child node * @public */ /** * check this node for configuration errors * @typedef {function} FktDeregister * @param {runtimeNode} srcnode node to register as child node * @param {function} done node to register as child node * @returns {*} result of the function * @public */ /** * This callback type is called `requestCallback` and is displayed as a global symbol. * * @callback onErrorCallback * @param {String} errorMessage - the error message * @return {Boolean|String} returns true if ok otherwise an string with the error */ /** * check this node for configuration errors * @typedef {function} FktCheckNode * @property {onErrorCallback} [onError] - if an error occurs this function will be called * @property {Boolean|function} [onOk=false] - if no error, this function will be called or the return value in case of ok * @return {Boolean|String} returns the result of onError if an error occurs, otherwise result of onOk */ /** * Formate a Date Object * @typedef {function} FktFormatDate * @param {Date} dt Date to format to Date and Time string * @returns {String} formated Date object */ /** * get list of custom angles * @typedef {function} FktGetCustomAngles * @param {string} filter filter to the angle names * @returns {Array.<{value: string, name: string, angle: number, [i:number]}>} */ /** * get list of all suntimes including custom ones * @typedef {function} FktGetSunTimesList * @param {string} filter filter to the angle names * @param {boolean} [onlyCustom=false] if true only custom angles will be given * @returns {Array.<{value: string, name: string, angle: number, [i:number]}>} */ /** * get a float value from a type input in Node-Red * @typedef {function} FktGetFloatProp * @param {runtimeNode} _srcNode - source node information * @param {Object} msg - message object * @param {IGetFloatPropData} data - input data object * @returns {number} float property */ /** * get an formated date prepared for output * @typedef {function} FktFormatOutDate * @param {runtimeNode} _srcNode - source node for logging * @param {Object} msg - the message object * @param {Date} dateValue - the source date object which should be formated * @param {ITimePropertyType} data - additional formating and control data */ /** * get the time Data prepared for output * @typedef {function} FktGetOutDataProp * @param {runtimeNode} _srcNode - source node for logging * @param {Object} 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 {runtimeNode} _srcNode The base node * @param {Object} msg The Message Object to set the Data * @param {String} type type of the property to set * @param {*} value value of the property to set * @param {*} msgPropertyData Data object to set to the property */ /** * get the time Data from a typed input * @typedef {function} FktGetTimeProp * @param {runtimeNode} _srcNode - source node for logging * @param {Object} msg - the message object * @param {ITimePropertyType} data - a Data object * @returns {ITimePropertyResult} value of the type input */ /** * get a prepared JSONATA Expression * @typedef {function} FktGetJSONataExpression * @param {runtimeNode} _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 {runtimeNode} _srcNode - source node information * @param {Object} msg - message object * @param {IValuePropertyType} data - data object with more information * @param {Boolean} [noError] - true if no error shoudl be given in GUI * @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 {runtimeNode} _srcNode - source node information * @param {Object} msg - message object * @property {IValuePropertyType} operandA - first operand * @property {String} compare - compare between the both operands * @property {IValuePropertyType} operandB - second operand * @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} FktGetSunCalc * @param {Date} [date] - defines the date to calculates sun data for (can be a number too) * @param {Boolean} [calcTimes] - defines if times should be calculated * @param {Boolean} [sunInSky] - is sun in sky should determinated * @param {Number} [specLatitude] - optionaly special latitude * @param {Number} [specLongitude] - optionaly special longitude * @param {Number} [specHeight] - optionaly observer height * @returns {ISunDataResult} * @public */ /** * compared two property's * @typedef {function} FktGetMoonCalc * @param {Date} [date] - defines the date to calculates sun data for (can be a number too) * @param {Boolean} [calcTimes] - defines if times should be calculated * @param {Boolean} [moonInSky] - is moon in sky should determinated * @param {Number} [specLatitude] - optionaly special latitude * @param {Number} [specLongitude] - optionaly special longitude * @returns {IMoonDataResult} * @public */ /** * check an array if an array has duplicates. * * @callback FktHasDuplicates * @param {Array.<String>} arr - An array of strings. * @returns {Boolean} __true__ if array has duplicates */ /** * check an array if an array has duplicates. * * @callback FktValidateCustomTimes * @param {Array.<{riseName: String, setName: String, angle: number, rad: Boolean}>} riseName - An array of strings. * @returns {Boolean} __true__ if array has duplicates */ /*******************************************************************************************************/ // Implementation /*******************************************************************************************************/ /** Export the function that defines the node */ module.exports = function (/** @type {runtimeRED} */ RED) { 'use strict'; const hlp = require('./lib/dateTimeHelper.js'); const util = require('util'); const sunCalc = require('suncalc3'); /** generic configuration Node * @class * @extends {runtimeNode} * @constructor * @public */ class positionConfigurationNode { /** * creates a new instance of the settings node and initializes them * @param {*} config - configuration of the node */ constructor(config) { RED.nodes.createNode(this, config); /** Copy 'this' object in case we need it in context of callbacks of other functions. * @type {runtimeNode} * @private */ // @ts-ignore const node = this; try { /** @type {String} - name of the node */ this.name = config.name; /** @type {Boolean} - indicator if the node is valid */ this.valid = true; /** @type {number} - latitude angle */ this.latitude = parseFloat(Object.prototype.hasOwnProperty.call( // @ts-ignore this.credentials, 'posLatitude') ? this.credentials.posLatitude : config.latitude); /** @type {number} - longitude angle */ this.longitude = parseFloat(Object.prototype.hasOwnProperty.call( // @ts-ignore this.credentials, 'posLongitude') ? this.credentials.posLongitude : config.longitude); /** @type {number} - observer height */ this.height = parseFloat( // @ts-ignore this.credentials.height); // @ts-ignore if (typeof this.credentials.height === 'undefined' || this.credentials.height === '') { this.height = 0; } /** @type {FktCheckNode} - checkNode checks the node configuration */ this.checkNode( error => { // @ts-ignore this.error(error); // @ts-ignore this.status({fill: 'red', shape: 'dot', text: error }); this.valid = false; }); /** @type {('deg'|'rad')} - type of the angle */ this.angleType = config.angleType; /** @type {number} - time zone offset */ this.tzOffset = parseInt(config.timeZoneOffset || 99); /** @type {number} - time zone DST */ this.tzDST = parseInt(config.timeZoneDST || 0); /** @type {String} - type of the angle */ this.contextStore = config.contextStore; /** @type {FktHasDuplicates} - check an array if an array has duplicates */ this.hasDuplicates = arr => { return arr.some(item => { return (arr.indexOf(item) !== arr.lastIndexOf(item)); }); }; /** @type {Array.<{name: String, angle: number}>} - custom angle definition */ this.customAngles = []; if (config.predefAngles && (config.predefAngles.length > 0)) { const EXP = /^[0-9a-zA-Z_\s]+$/; const names = []; config.predefAngles.forEach((/** @type {{ angle: number; name: String;rad: Boolean }} */ time) => { if (!EXP.test(time.name) || isNaN(time.angle) || typeof time.angle !== 'number') { node.error(RED._('position-config.errors.custom-angles', time)); this.valid = false; } else if (names.includes(time.name)) { node.error(RED._('position-config.errors.custom-angles-duplicate', time)); this.valid = false; } else { names.push(time.name); this.customAngles.push({ name: time.name, angle: time.angle }); } }); } const times = config.sunPositions; if (times) { times.forEach((/** @type {{ angle: number; riseName: String; setName: String }} */ time, /** @type {number} */ index) => { if (!sunCalc.addTime(time.angle, time.riseName, time.setName, index+100, index+200, this.angleType === 'deg')) { node.error(RED._('position-config.errors.invalid-custom-suntime', time)); this.valid = false; } }); } /** @type {INodeCacheData} - cache object */ this.cache = { lastSunCalc: { ts: 0 }, lastMoonCalc: { ts: 0 }, // @ts-ignore sunTimesToday: {}, // @ts-ignore sunTimesTomorrow: {}, // @ts-ignore sunTimesAdd1: {}, // @ts-ignore sunTimesAdd2: {}, // @ts-ignore moonTimesToday: {}, // @ts-ignore moonTimesTomorrow: {}, // @ts-ignore moonTimes2Days: {} }; if (isNaN(this.tzOffset) || this.tzOffset > 99 || this.tzOffset < -99) { this.tzOffset = 99; } 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; // this.debug('no tzOffset defined (tzDST=' + this.tzDST + ')'); } // this.debug(`initialize latitude=${this.latitude} longitude=${this.longitude} tzOffset=${this.tzOffset} tzDST=${this.tzDST}`); /** @type {number|string} - standard time format */ this.stateTimeFormat = config.stateTimeFormat || 3; /** @type {number|string} - standard date format */ this.stateDateFormat = config.stateDateFormat || 12; // this.debug('load position-config ' + this.name + ' latitude:' + this.latitude + ' long:' + this.longitude + ' angelt:' + this.angleType + ' TZ:' + this.tzOffset); const today = new Date(); const dayId = hlp.getDayId(today); // this._getUTCDayId(today); this._sunTimesRefresh(today.valueOf(), dayId); this._moonTimesRefresh(today.valueOf(), dayId); hlp.initializeParser(RED._('common.days', { returnObjects: true}), RED._('common.months', { returnObjects: true}), RED._('common.dayDiffNames', { returnObjects: true})); /** @type {Object} - cache object */ this.subNodes = {}; } catch (err) { // @ts-ignore this.debug(util.inspect(err)); // @ts-ignore this.status({ fill: 'red', shape: 'ring', text: RED._('errors.error-title') }); throw err; } } /** * register a node as child * @param {runtimeNode} srcnode node to register as child node * @public */ register(srcnode) { this.subNodes[srcnode.id] = srcnode; } /** * remove a previous registered node as child * @param {runtimeNode} srcnode node to remove * @param {function} done function which should be executed after deregister * @returns {*} result of the function * @public */ deregister(srcnode, done) { delete this.subNodes[srcnode.id]; return done(); } /*******************************************************************************************************/ /** * check this node for configuration errors * @property {onErrorCallback} [onError] - if an error occurs this function will be called * @property {Boolean|function} [onOk=false] - if no error, this function will be called or the return value in case of ok * @return {Boolean|String} returns the result of onError if an error occurs, otherwise result of onOk * @public */ checkNode(onError, onOk) { 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)) { return onError(RED._('position-config.errors.latitude-missing')); } if (isNaN(this.longitude) || (this.longitude < -180) || (this.longitude > 180)) { return onError(RED._('position-config.errors.longitude-missing')); } if (Number.isNaN(this.height)) { return onError(RED._('position-config.errors.height-wrong')); } if (typeof onOk === 'function') { return onOk(); } return onOk ? true : false; } /*******************************************************************************************************/ /** * gets sun time by Name * @param {Date} dNow current time * @param {String} name 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 {ILimitationsObj} [limit] additional limitations for the calculation * @param {number} [latitude] latitude * @param {number} [longitude] longitude * @param {number} [height] height definition * @return {ITimeResult} result object of sunTime * @private */ _getSunTimeByName(dNow, name, offset, multiplier, limit, latitude, longitude, height) { // 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); const mheight = (height || this.height); if (latitude && longitude) { result = Object.assign({}, sunCalc.getSunTimes(dNow.getTime(), latitude, longitude, mheight, true)[name]); } else { latitude = this.latitude; longitude = this.longitude; this._sunTimesCheck(); // refresh if needed, get dayId // this.debug(`_getSunTimeByName name=${name} 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[name]); // needed for a object copy } else if (dayId === this.cache.sunTimesTomorrow.dayId) { result = Object.assign({}, this.cache.sunTimesTomorrow.times[name]); // needed for a object copy } else if (dayId === this.cache.sunTimesAdd1.dayId) { result = Object.assign({},this.cache.sunTimesAdd1.times[name]); // needed for a object copy } else if (dayId === this.cache.sunTimesAdd2.dayId) { result = Object.assign({},this.cache.sunTimesAdd2.times[name]); // needed for a object copy } else { // this.debug('sun-time not in cache - calc time'); this.cache.sunTimesAdd2 = { dayId: this.cache.sunTimesAdd1.dayId, times: this.cache.sunTimesAdd1.times }; this.cache.sunTimesAdd1 = { dayId, times : sunCalc.getSunTimes(dNow.getTime(), latitude, longitude, mheight, true) }; result = Object.assign({},this.cache.sunTimesAdd1.times[name]); // needed for a object copy } } if (!result) { // @ts-ignore this.error(RED._('position-config.errors.invalid-custom-suntime', {name})); return { error: RED._('position-config.errors.invalid-custom-suntime', {name}), valid: false, ts: dNow.valueOf(), value: dNow, pos: NaN, angle: NaN, julian: NaN }; } result.value = hlp.addOffset(new Date(result.value), offset, multiplier); if (limit.next && result.value.getTime() <= dNow.getTime()) { if (dayId === this.cache.sunTimesToday.dayId) { result = Object.assign({}, this.cache.sunTimesTomorrow.times[name]); result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } const dateBase = new Date(dNow); while (result.value.getTime() <= dNow.getTime()) { dateBase.setUTCDate(dateBase.getUTCDate() + 1); result = Object.assign(result, sunCalc.getSunTimes(dateBase.getTime(), latitude, longitude, mheight, true)[name]); result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } } const r = hlp.limitDate(limit, result.value); if (r.error) { result.error = r.error; } else { result.value = r.date; } if (r.hasChanged) { this.checkNode(error => { throw new Error(error); }); result = Object.assign(result, sunCalc.getSunTimes(result.value.valueOf(), latitude, longitude, mheight, true)[name]); result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } // this.debug('_getSunTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); return result; } /** * gets sun time by Elevation * @param {Date} dNow current time * @param {number} elevationAngle name of the sun time * @param {ITimePropertyType} tprop additional limitations for the calculation * @param {('set'|'rise'|'both')} [prop=both] property (set, rise or both) to return * @return {ISunTimeSingle} result object of sunTime * @private */ _getSunTimeByElevation(dNow, elevationAngle, tprop, prop) { if (!hlp.isValidDate(dNow)) { const dto = new Date(dNow); if (hlp.isValidDate(dto)) { dNow = dto; } else { dNow = new Date(); } } // 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 height = (tprop.height || this.height); const degree = (this.angleType === 'deg'); const result = Object.assign({},sunCalc.getSunTime(dNow.valueOf(), latitude, longitude, elevationAngle, height, degree)); const offsetX = this._getOffsetVal( // @ts-ignore this, null, tprop, dNow); const calc = (result, recalc) => { result.value = hlp.addOffset(new Date(result.value), offsetX, tprop.multiplier); if (tprop.next && result.value.getTime() <= dNow.getTime()) { const datebase = new Date(dNow); while (result.value.getTime() <= dNow.getTime()) { datebase.setUTCDate(datebase.getUTCDate() + 1); result = Object.assign({},recalc(datebase.valueOf())); result.value = hlp.addOffset(new Date(result.value), offsetX, tprop.multiplier); } } const r = hlp.limitDate(tprop, result.value); if (r.error) { result.error = r.error; } else { result.value = r.date; } if (r.hasChanged) { this.checkNode(error => { throw new Error(error); }); result = Object.assign(result, recalc(result.value.valueOf())); result.value = hlp.addOffset(new Date(result.value), offsetX, tprop.multiplier); } }; if (prop==='rise') { calc(result.rise, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, height, degree).rise); return result; } else if (prop==='set') { calc(result.set, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, height, degree).set); return result; } calc(result.rise, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, height, degree).rise); calc(result.set, dt => sunCalc.getSunTime(dt, latitude, longitude, elevationAngle, height, degree).set); return result; } /** * gets sun time by Azimuth * @param {Date} dNow current time * @param {number} azimuthAngle angle of the sun * @param {ITimePropertyType} [tprop] additional limitations for the calculation * @param {number} [latitude] latitude * @param {number} [longitude] longitude * @return {Date} result object of sunTime * @private */ _getSunTimeByAzimuth(dNow, azimuthAngle, degree, tprop, latitude, longitude) { if (!hlp.isValidDate(dNow)) { const dto = new Date(dNow); if (hlp.isValidDate(dto)) { dNow = dto; } else { dNow = new Date(); } } // 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); const offsetX = this._getOffsetVal( // @ts-ignore this, null, tprop, dNow); result = hlp.addOffset(result, offsetX, tprop.multiplier); if (tprop.next && result.getTime() <= dNow.getTime()) { const datebase = new Date(dNow); while (result.getTime() <= dNow.getTime()) { datebase.setUTCDate(datebase.getUTCDate() + 1); result = sunCalc.getSunTimeByAzimuth(datebase, latitude, longitude, azimuthAngle, degree); result = hlp.addOffset(result, offsetX, tprop.multiplier); } } const r = hlp.limitDate(tprop, result); if (r.error) { result = null; } else { result = r.date; } if (r.hasChanged) { this.checkNode(error => { throw new Error(error); }); result = sunCalc.getSunTimeByAzimuth(result, latitude, longitude, azimuthAngle, degree); result = hlp.addOffset(result, offsetX, tprop.multiplier); } return result; } /** * gets previous and next sun time * @param {Date} dNow current time * @return {ISunTimeDefNextLast} result object of sunTime * @private */ _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 })}`); if (dayId === this.cache.sunTimesToday.dayId) { result = this.cache.sunTimesToday.times; } else if (dayId === this.cache.sunTimesTomorrow.dayId) { result = this.cache.sunTimesTomorrow.times; } else if (dayId === this.cache.sunTimesAdd1.dayId) { result = this.cache.sunTimesAdd1.times; } 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, times: this.cache.sunTimesAdd1.times }; this.cache.sunTimesAdd1 = { dayId, times: sunCalc.getSunTimes(dNow.valueOf(), this.latitude, this.longitude, this.height, false) }; result = this.cache.sunTimesAdd1.times; } const sortable = []; for (const key in result) { if (result[key].pos >= 0) { sortable.push(result[key]); } } sortable.sort((a, b) => { 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 })}`); let last = sortable[0]; if (last.ts >= dNowTs) { return { next : { value : new Date(last.value), name : last.name, pos : last.pos, valid : last.valid, elevation : last.elevation }, last : { value : new Date(result['nadir'].value), name : result['nadir'].name, pos : result['nadir'].pos, valid : result['nadir'].valid, elevation : result['nadir'].elevation } }; } for (let i = 1; i < sortable.length; i++) { const element = sortable[i]; if (dNowTs < element.ts) { return { next : { value : new Date(element.value), name : element.name, pos : element.pos, valid : element.valid, elevation : element.elevation }, last : { value : new Date(last.value), name : last.name, pos : last.pos, valid : last.valid, elevation : last.elevation } }; } last = element; } dayId += 1; if (dayId === this.cache.sunTimesToday.dayId) { result = this.cache.sunTimesToday.times; } else if (dayId === this.cache.sunTimesTomorrow.dayId) { result = this.cache.sunTimesTomorrow.times; } else if (dayId === this.cache.sunTimesAdd1.dayId) { result = this.cache.sunTimesAdd1.times; } else if (dayId === this.cache.sunTimesAdd2.dayId) { result = this.cache.sunTimesAdd2.times; } else { result = sunCalc.getSunTimes(dNow.valueOf() + hlp.TIME_24h, this.latitude, this.longitude, this.height, false); // needed for a object copy } const sortable2 = []; for (const key in result) { if (result[key].pos >=0) { sortable2.push(result[key]); } } sortable2.sort((a, b) => { return a.ts - b.ts; }); return { next : { value : new Date(sortable2[0].value), name : sortable2[0].name, pos : sortable2[0].pos, valid : sortable2[0].valid, elevation : sortable2[0].elevation }, last : { value : new Date(last.value), name : last.name, pos : last.pos, valid : last.valid, elevation : last.elevation } }; } /*******************************************************************************************************/ /** * 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 {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 * @private */ _getMoonTimeByName(dNow, value, offset, multiplier, limit, latitude, longitude) { const result = {}; const dateBase = new Date(dNow); const dayId = hlp.getDayId(dNow); // this._getUTCDayId(dNow); if (latitude && longitude) { result.value = sunCalc.getMoonTimes(dNow.getTime(), latitude, longitude)[value]; } else { latitude = this.latitude; 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 })}`); if (dayId === this.cache.moonTimesToday.dayId) { result.value = this.cache.moonTimesToday.times[value]; // needed for a object copy } else if (dayId === this.cache.moonTimesTomorrow.dayId) { result.value = this.cache.moonTimesTomorrow.times[value]; // needed for a object copy } else if (dayId === this.cache.moonTimes2Days.dayId) { result.value = this.cache.moonTimes2Days.times[value]; // needed for a object copy } else { result.value = sunCalc.getMoonTimes(dNow.getTime(), this.latitude, this.longitude)[value]; // needed for a object copy } } if (hlp.isValidDate(result.value)) { result.value = hlp.addOffset(new Date(result.value.getTime()), offset, multiplier); if (limit.next && result.value.getTime() <= dNow.getTime()) { if (dayId === this.cache.moonTimesToday.dayId) { result.value = this.cache.moonTimesTomorrow.times[value]; result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } else if (dayId === this.cache.moonTimesTomorrow.dayId) { result.value = this.cache.moonTimes2Days.times[value]; result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } while (hlp.isValidDate(result.value) && result.value.getTime() <= dNow.getTime()) { dateBase.setUTCDate(dateBase.getUTCDate() + 1); result.value = sunCalc.getMoonTimes(dateBase.getTime(), latitude, longitude)[value]; result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } } } while (!hlp.isValidDate(result.value)) { dateBase.setUTCDate(dateBase.getUTCDate() + 1); result.value = sunCalc.getMoonTimes(dateBase.getTime(), latitude, longitude)[value]; } result.value = new Date(result.value.getTime()); const r = hlp.limitDate(limit, result.value); if (r.error) { result.error = r.error; } else { result.value = r.date; } if (r.hasChanged) { this.checkNode(error => { throw new Error(error); }); result.value = new Date(sunCalc.getMoonTimes(result.value.valueOf(), latitude, longitude)[value]); result.value = hlp.addOffset(new Date(result.value), offset, multiplier); } // this.debug('_getMoonTimeByName result=' + util.inspect(result, { colors: true, compact: 10, breakLength: Infinity })); return result; } /*******************************************************************************************************/ /** * Formate a Date Object to a Date and Time String * @param {Date} dt Date to format to D