Developping a module for my tuyaDAEMON project (an IOT framework), I developped a library that can be useful in many meteo node-red applications.
This code "meteoUtils constructor" builds a global singleton object, usable by any flow/node, like a shared library.
Shared functions: getDOY
(aDate
= null), isDayChanged
(nowDay
), toFahrenheit
(tc
), toCelsius
( tf
), toKmHr
(v
), evalHeatIndex
(tc
, RH
), evalDewPoint
(tc
, RH
), evalWindChill
(tc
, v
, hi
).
Here the full code:
// functions for meteo day: to handle DST
Date.prototype.stdTimezoneOffset = function () {
var jan = new Date(this.getFullYear(), 0, 1);
var jul = new Date(this.getFullYear(), 6, 1);
return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
}
Date.prototype.dstCorrection = function () {
return this.stdTimezoneOffset() - this.getTimezoneOffset();
}
// the object meteoUtils is a global singleton with many meteorological functions.
// singleton constructor: only data portion (here empty).
global.set("meteoUtils", {});
context.global.meteoUtils = global.get("meteoUtils");
// alternative, giving up the visibility in node-red context data pad:
// context.global.meteoUtils = {}; // data (here empty).
// singleton constructor: adding required methods
// ===================== DST and meteo day handling
// returns aDate as meteo day-of-year, starting at DAYSTART time (solar: DST compensed).
// Days Starting from 1, but 0 is possible in the morning of January 1st.
// Uses flow.DAYSTART as user defined meteo day start time (default 09:00:00)
// param aDate: a Date instance (to allow calls with past moments)|null. Default = null = now.
context.global.meteoUtils.getDOY = function(aDate = null){
function toSolarTime(utime){
const DTSmin = new Date().dstCorrection(); // in min
const user = String(utime).split(":");
return new Date(0, 0, 0, user[0], user[1] + DTSmin, 0).toLocaleTimeString();
}
const now = (aDate == null)? new Date(): aDate;
const start = new Date(now.getFullYear(), 0, 0);
const diff = (now - start) + ((start.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1000);
const nowDay = Math.ceil(diff / (1000 * 60 * 60 * 24));
const daystart = flow.get('DAYSTART') || "09:00:00";
const startTime = toSolarTime(daystart);
// node.warn(["getDOY() ", nowDay, startTime, now.toLocaleTimeString()]);
if (now.toLocaleTimeString() >= startTime) return (nowDay);
return (nowDay -1);
}
// to trigger operations at day change
// param nowDay: string, name of the reference (flow.)'nowDay', one for process.
context.global.meteoUtils.isDayChanged = function(nowDay){
let today = this.getDOY();
if (isNaN(flow.get(nowDay))) {
// first time
flow.set(nowDay, today);
return true;
}
if (flow.get(nowDay) === today) return false;
if (today === 0) return false; // morning of January 1st, still 365 or 366
// changed
flow.set(nowDay, today);
return true;
}
// =============== unit conversions
// conversion Celsius to Fahrenheit
context.global.meteoUtils.toFahrenheit = function(tc){
return (tc * 9.0/5.0 + 32.0);
}
// conversion Fahrenheit to Celsius
context.global.meteoUtils.toCelsius = function(tf){
return ((tf -32) * 5.0 / 9.0);
}
// conversion m/s to Km/h
context.global.meteoUtils.toKmHr = function(v) {
return (v* 3.6);
}
// ================= more meteo derived values
// -------------- NOAA Heat Index
// see https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
// see https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3801457/
// tested using https://www.wpc.ncep.noaa.gov/html/heatindex.shtml
context.global.meteoUtils.evalHeatIndex = function(tc, RH){
T = this.toFahrenheit(tc);
if (T < 40) return (tc);
// ha = 0.5 * (T + 61.0 + ((T-68.0)*1.2) + (RH*0.094));
hb = -10.3 +1.1*T + 0.047 *RH;
node.warn (["evalHeatIndex simple ("+tc+", "+ RH+") ", hb +" °F", this.toCelsius(hb) ]);
if (hb < 79)
return Number(this.toCelsius(hb).toFixed(1));
hf = -42.379 + 2.04901523*T + 10.14333127*RH -
0.22475541*T*RH - 0.00683783*T*T - 0.05481717*RH*RH +
0.00122874*T*T*RH + 0.00085282*T*RH*RH - 0.00000199*T*T*RH*RH;
// corrections:
if ((RH < 13) && (T >= 80) && (T <= 112))
hf -= ((13.0-RH)/4)*Math.sqrt((17.0- Math.abs(T-95.0))/17);
if ((RH > 85) && (T >= 80) && (T <= 87))
hf += ((RH-85)/10.0) * ((87-T)/5.0);
// node.warn (["evalHeatIndex full ( "+tc+" °C, "+ RH+" %) ", hf +" °F", this.toCelsius(hf) +" °C"]);
return Number(this.toCelsius(hf).toFixed(1));
}
// --- dew point
// see https://en.wikipedia.org/wiki/Dew_point
context.global.meteoUtils.evalDewPoint = function (tc, RH){
z = Math.log(RH/100)+ 18.678*tc/(257.14 + tc);
dpt= 257.14*z/(18.678 -z);
// node.warn (["evalDewPoint( "+tc+" °C, "+ RH+" %) ", dpt +" °C"]);
return Number(dpt.toFixed(1));
}
// --- wind chill
// see https://en.wikipedia.org/wiki/Wind_chill,
context.global.meteoUtils.evalWindChill = function (tc, v, hi){
let vkh = this.toKmHr(v);
// replaced by heat index if T > 10° or msg.payload.wind_max_m_s < 1.3
vc = hi;
if ((tc <= 10.0) && ( v > 1.3))
vc = 13.12 + (0.6215*tc) - (11.37* Math.pow(vkh, 0.16)) + (0.3965*tc* Math.pow(vkh, 0.16));
node.warn (["evalWindChill( "+tc+" °C, "+ v +" m/s, "+ hi+" °C) ", vc +" °C"]);
return (Number(vc.toFixed(1)));
}
Use: copy this code in the 'On Start' of some node, in your flow or in one flow of your project.
Then you can use global calls like this (in any flow, in any node):
if (context.global.meteoUtils.isDayChanged("todayStore")) { ...
A singleton object is a good replacement for shared libraries, having following advantages:
- Easy to implement
- No extra files: all in JSON import/export
- No config file editing, autoinstallation
- Easy to debug
- Visible in node-red 'Context data' debug pad.
Here the original documentation.
In the hope that this will help other users, best regards.
note: this solution is better than my previous JSON function strategy, for simplicity and performances (31s vs 106s in 10000 loop test).
m..s.