node-red-contrib-sun-position
Version:
NodeRED nodes to get sun and moon position
1,067 lines (995 loc) • 126 kB
JavaScript
// @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