node-red-contrib-astro-filter
Version:
Node-RED node for filtering messages based on astronomical events (solstices and equinoxes)
190 lines (162 loc) • 6.98 kB
JavaScript
module.exports = function(RED, customCurrentDateFn) {
const astronomy = require('astronomy-engine');
const moment = require('moment-timezone');
function AstroFilterNode(config) {
RED.nodes.createNode(this, config);
const node = this;
// Store configuration
node.eventType = config.eventType || 'june_solstice';
node.startOffset = parseInt(config.startOffset) || 0;
node.endOffset = parseInt(config.endOffset) || 0;
node.useAbsoluteDiff = config.useAbsoluteDiff || false;
updateNodeStatus(node, true);
// Register input handler
node.on('input', handleInput);
function handleInput(msg, send, done) {
// Update node status and gets the result
const result = updateNodeStatus(node, false);
// Add the difference in days to the message payload
if (!msg.payload || typeof msg.payload !== 'object') {
msg.payload = {};
}
// Calculate days difference
msg.payload.astroDiff = node.useAbsoluteDiff ? Math.abs(result.daysDiff) : result.daysDiff;
// If within range, pass the message through
if (result.isInRange) {
send(msg);
}
// Signal completion
if (done) {
done();
}
}
return this
}
const currentDate = customCurrentDateFn || function() { return new Date(); };
/**
* Get the astronomical event date for the specified year and event type
* @param {string} eventType - The type of event to calculate
* @param {number} year - The year to calculate for
* @returns {Date} - JavaScript Date object representing the event
*/
function getAstronomicalEventDate(eventType, year) {
// Use the Seasons function to get all events for the year
const seasons = astronomy.Seasons(year);
// Get the appropriate event based on the configuration
let eventTime;
switch (eventType) {
case 'march_equinox':
eventTime = seasons.mar_equinox;
break;
case 'june_solstice':
eventTime = seasons.jun_solstice;
break;
case 'september_equinox':
eventTime = seasons.sep_equinox;
break;
case 'december_solstice':
eventTime = seasons.dec_solstice;
break;
}
return eventTime.date;
}
/**
* Calculate event range information for a specific year
* @param {string} eventType - The type of event
* @param {number} year - The year to calculate for
* @param {number} startOffset - Days offset from the event (can be negative)
* @param {number} endOffset - Days offset from the event (can be negative)
* @param {moment} testMoment - The test date as moment object
* @returns {object} - Range information object
*/
function calculateEventRange(eventType, year, startOffset, endOffset, testMoment) {
// Calculate event date for the specified year
const eventDate = getAstronomicalEventDate(eventType, year);
const eventMoment = moment(eventDate).startOf('day');
const rangeStart = moment(eventMoment).add(startOffset, 'days');
const rangeEnd = moment(eventMoment).add(endOffset, 'days').endOf('day');
const isInRange = testMoment.isSameOrAfter(rangeStart) && testMoment.isSameOrBefore(rangeEnd);
// Calculate days difference from event date
const daysDiff = testMoment.diff(eventMoment, 'days');
const rangeText = `${rangeStart.format('MMM D')} to ${rangeEnd.format('MMM D')}`;
return {
isInRange,
daysDiff,
rangeText,
eventMoment,
rangeStart,
rangeEnd
};
}
/**
* Check if a date is within the configured range around the event
* @param {Date} testDate - The date to check
* @param {string} eventType - The type of event
* @param {number} startOffset - Days offset from the event (can be negative)
* @param {number} endOffset - Days offset from the event (can be negative)
* @returns {object} - Result object with range information
*/
function isWithinEventRange(testDate, eventType, startOffset, endOffset) {
// Convert test date to moment
const testMoment = moment(testDate).startOf('day');
const currentYear = testMoment.year();
// Calculate range information for current year
let result = calculateEventRange(eventType, currentYear, startOffset, endOffset, testMoment);
// If not in range and it's early in the year (Jan-Mar), check if it falls within
// a range that started in the previous year (especially for December solstice)
if (!result.isInRange && testMoment.month() < 6 && eventType === 'december_solstice') {
// Calculate range information for previous year
const prevYearResult = calculateEventRange(eventType, currentYear - 1, startOffset, endOffset, testMoment);
if (prevYearResult.isInRange) {
result = prevYearResult;
}
}
return {
isInRange: result.isInRange,
daysDiff: result.daysDiff,
rangeText: result.rangeText
};
}
/**
* Update node status on deploy or after input processing
* @param {object} node - The node to update
* @param {boolean} deploying - Whether this is being called during node deployment
* @returns {object} - Result object with range information
*/
function updateNodeStatus(node, deploying) {
const result = isWithinEventRange(
currentDate(),
node.eventType,
node.startOffset,
node.endOffset
);
// Initialize status on deploy with range information
if (deploying) {
node.status({
fill: "grey",
shape: "dot",
text: `Range: ${result.rangeText}`
});
return;
}
// Update status with current date and range information
if (result.isInRange) {
// Within range - green status
node.status({
fill: "green",
shape: "dot",
text: `In range: ${result.rangeText}`
});
} else {
// Outside range - red status
node.status({
fill: "red",
shape: "ring",
text: `Outside range: ${result.rangeText}`
});
}
return result;
}
// Register the node
RED.nodes.registerType("astro-filter", AstroFilterNode);
};