Random Time Trigger?

@sooty, @gullygossner, I figured out to pass sunset as a starting time to the presence faker node. There are lots of methods for getting the sun times (including getting the current state of sun.sun from HA) but I ended up using the SunPosition subflow, since it gave me morning times as well as evening and goldenHours. After a simple function to extract the time, I put it into a global variable so I can use it in multiple flows. The second step gets the variable and adds text (e.g. {“windowBegin”: “17:27”}) to format it for the subflow’s dynamic configuration. Here is a sample flow:

[{"id":"ca4442d5.fa1d1","type":"subflow","name":"SunPosition","info":"### Sun Position\n\nSet latitude and longitude in the node\n\n### Input\n\nSet the input `msg` (default `msg.payload`)\n\nInject a valid timestamp\n\n","category":"","in":[{"x":92,"y":96,"wires":[{"id":"c3d02a0c.ef5fa8"}]}],"out":[{"x":308,"y":96,"wires":[{"id":"c3d02a0c.ef5fa8","port":0}]}],"env":[{"name":"latitude","type":"num","value":"","ui":{"label":{"en-US":"Latitude"},"type":"input","opts":{"types":["num"]}}},{"name":"longitude","type":"num","value":"","ui":{"label":{"en-US":"Longitude"},"type":"input","opts":{"types":["num"]}}},{"name":"msg","type":"str","value":"payload","ui":{"label":{"en-US":"msg."},"type":"input","opts":{"types":["str"]}}}],"icon":"font-awesome/fa-sun-o"},{"id":"c3d02a0c.ef5fa8","type":"function","z":"ca4442d5.fa1d1","name":"","func":"/*\n (c) 2011-2015, Vladimir Agafonkin\n SunCalc is a JavaScript library for calculating sun/moon position and light phases.\n https://github.com/mourner/suncalc\n*/\n\nvar PI   = Math.PI,\n    sin  = Math.sin,\n    cos  = Math.cos,\n    tan  = Math.tan,\n    asin = Math.asin,\n    atan = Math.atan2,\n    acos = Math.acos,\n    rad  = PI / 180;\n\n// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas\n\n\n// date/time constants and conversions\n\nvar dayMs = 1000 * 60 * 60 * 24,\n    J1970 = 2440588,\n    J2000 = 2451545;\n\nfunction toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }\nfunction fromJulian(j)  { return new Date((j + 0.5 - J1970) * dayMs); }\nfunction toDays(date)   { return toJulian(date) - J2000; }\n\n\n// general calculations for position\n\nvar e = rad * 23.4397; // obliquity of the Earth\n\nfunction rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }\nfunction declination(l, b)    { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }\n\nfunction azimuth(H, phi, dec)  { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }\nfunction altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }\n\nfunction siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }\n\nfunction astroRefraction(h) {\n    if (h < 0) // the following formula works for positive altitudes only.\n        h = 0; // if h = -0.08901179 a div/0 would occur.\n\n    // formula 16.4 of \"Astronomical Algorithms\" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.\n    // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:\n    return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));\n}\n\n// general sun calculations\n\nfunction solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }\n\nfunction eclipticLongitude(M) {\n\n    var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center\n        P = rad * 102.9372; // perihelion of the Earth\n\n    return M + C + P + PI;\n}\n\nfunction sunCoords(d) {\n\n    var M = solarMeanAnomaly(d),\n        L = eclipticLongitude(M);\n\n    return {\n        dec: declination(L, 0),\n        ra: rightAscension(L, 0)\n    };\n}\n\n\nvar SunCalc = {};\n\n\n// calculates sun position for a given date and latitude/longitude\n\nSunCalc.getPosition = function (date, lat, lng) {\n\n    var lw  = rad * -lng,\n        phi = rad * lat,\n        d   = toDays(date),\n\n        c  = sunCoords(d),\n        H  = siderealTime(d, lw) - c.ra;\n\n    return {\n        azimuth: azimuth(H, phi, c.dec),\n        altitude: altitude(H, phi, c.dec)\n    };\n};\n\n\n// sun times configuration (angle, morning name, evening name)\n\nvar times = SunCalc.times = [\n    [-0.833, 'sunrise',       'sunset'      ],\n    [  -0.3, 'sunriseEnd',    'sunsetStart' ],\n    [    -6, 'dawn',          'dusk'        ],\n    [   -12, 'nauticalDawn',  'nauticalDusk'],\n    [   -18, 'nightEnd',      'night'       ],\n    [     6, 'goldenHourEnd', 'goldenHour'  ]\n];\n\n// adds a custom time to the times config\n\nSunCalc.addTime = function (angle, riseName, setName) {\n    times.push([angle, riseName, setName]);\n};\n\n\n// calculations for sun times\n\nvar J0 = 0.0009;\n\nfunction julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }\n\nfunction approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }\nfunction solarTransitJ(ds, M, L)  { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }\n\nfunction hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }\nfunction observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }\n\n// returns set time for the given sun altitude\nfunction getSetJ(h, lw, phi, dec, n, M, L) {\n\n    var w = hourAngle(h, phi, dec),\n        a = approxTransit(w, lw, n);\n    return solarTransitJ(a, M, L);\n}\n\n\n// calculates sun times for a given date, latitude/longitude, and, optionally,\n// the observer height (in meters) relative to the horizon\n\nSunCalc.getTimes = function (date, lat, lng, height) {\n\n    height = height || 0;\n\n    var lw = rad * -lng,\n        phi = rad * lat,\n\n        dh = observerAngle(height),\n\n        d = toDays(date),\n        n = julianCycle(d, lw),\n        ds = approxTransit(0, lw, n),\n\n        M = solarMeanAnomaly(ds),\n        L = eclipticLongitude(M),\n        dec = declination(L, 0),\n\n        Jnoon = solarTransitJ(ds, M, L),\n\n        i, len, time, h0, Jset, Jrise;\n\n\n    var result = {\n        solarNoon: fromJulian(Jnoon),\n        nadir: fromJulian(Jnoon - 0.5)\n    };\n\n    for (i = 0, len = times.length; i < len; i += 1) {\n        time = times[i];\n        h0 = (time[0] + dh) * rad;\n\n        Jset = getSetJ(h0, lw, phi, dec, n, M, L);\n        Jrise = Jnoon - (Jset - Jnoon);\n\n        result[time[1]] = fromJulian(Jrise);\n        result[time[2]] = fromJulian(Jset);\n    }\n\n    return result;\n};\n\n\n// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas\n\nfunction moonCoords(d) { // geocentric ecliptic coordinates of the moon\n\n    var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude\n        M = rad * (134.963 + 13.064993 * d), // mean anomaly\n        F = rad * (93.272 + 13.229350 * d),  // mean distance\n\n        l  = L + rad * 6.289 * sin(M), // longitude\n        b  = rad * 5.128 * sin(F),     // latitude\n        dt = 385001 - 20905 * cos(M);  // distance to the moon in km\n\n    return {\n        ra: rightAscension(l, b),\n        dec: declination(l, b),\n        dist: dt\n    };\n}\n\nSunCalc.getMoonPosition = function (date, lat, lng) {\n\n    var lw  = rad * -lng,\n        phi = rad * lat,\n        d   = toDays(date),\n\n        c = moonCoords(d),\n        H = siderealTime(d, lw) - c.ra,\n        h = altitude(H, phi, c.dec),\n        // formula 14.1 of \"Astronomical Algorithms\" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.\n        pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));\n\n    h = h + astroRefraction(h); // altitude correction for refraction\n\n    return {\n        azimuth: azimuth(H, phi, c.dec),\n        altitude: h,\n        distance: c.dist,\n        parallacticAngle: pa\n    };\n};\n\n\n// calculations for illumination parameters of the moon,\n// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and\n// Chapter 48 of \"Astronomical Algorithms\" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.\n\nSunCalc.getMoonIllumination = function (date) {\n\n    var d = toDays(date || new Date()),\n        s = sunCoords(d),\n        m = moonCoords(d),\n\n        sdist = 149598000, // distance from Earth to Sun in km\n\n        phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),\n        inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),\n        angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -\n                cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));\n\n    return {\n        fraction: (1 + cos(inc)) / 2,\n        phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,\n        angle: angle\n    };\n};\n\n\nfunction hoursLater(date, h) {\n    return new Date(date.valueOf() + h * dayMs / 24);\n}\n\n// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article\n\nSunCalc.getMoonTimes = function (date, lat, lng, inUTC) {\n    var t = new Date(date);\n    if (inUTC) t.setUTCHours(0, 0, 0, 0);\n    else t.setHours(0, 0, 0, 0);\n\n    var hc = 0.133 * rad,\n        h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,\n        h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;\n\n    // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)\n    for (var i = 1; i <= 24; i += 2) {\n        h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;\n        h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;\n\n        a = (h0 + h2) / 2 - h1;\n        b = (h2 - h0) / 2;\n        xe = -b / (2 * a);\n        ye = (a * xe + b) * xe + h1;\n        d = b * b - 4 * a * h1;\n        roots = 0;\n\n        if (d >= 0) {\n            dx = Math.sqrt(d) / (Math.abs(a) * 2);\n            x1 = xe - dx;\n            x2 = xe + dx;\n            if (Math.abs(x1) <= 1) roots++;\n            if (Math.abs(x2) <= 1) roots++;\n            if (x1 < -1) x1 = x2;\n        }\n\n        if (roots === 1) {\n            if (h0 < 0) rise = i + x1;\n            else set = i + x1;\n\n        } else if (roots === 2) {\n            rise = i + (ye < 0 ? x2 : x1);\n            set = i + (ye < 0 ? x1 : x2);\n        }\n\n        if (rise && set) break;\n\n        h0 = h2;\n    }\n\n    var result = {};\n\n    if (rise) result.rise = hoursLater(t, rise);\n    if (set) result.set = hoursLater(t, set);\n\n    if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;\n\n    return result;\n};\n\n\n\ninput = env.get(\"msg\")\n\n// get today's sunlight times for London\nvar times = SunCalc.getTimes(msg[input],env.get(\"latitude\"), env.get(\"longitude\"));\n\n// format sunrise time from the Date object\nvar sunriseStr = times.sunrise.getHours() + ':' + times.sunrise.getMinutes();\n\n// get position of the sun (azimuth and altitude) at today's sunrise\nvar sunrisePos = SunCalc.getPosition(times.sunrise, 51.5, -0.1);\n\n// get sunrise azimuth in degrees\nvar sunriseAzimuth = sunrisePos.azimuth * 180 / Math.PI;\n\n\nreturn {payload:{times:times,sunrisePos:sunrisePos,sunriseAzimuth:sunriseAzimuth}}\n","outputs":1,"noerr":0,"x":194,"y":96,"wires":[[]]},{"id":"347d9c0d.faa1e4","type":"tab","label":"Test PF","disabled":false,"info":""},{"id":"5265f842.d8d6e8","type":"inject","z":"347d9c0d.faa1e4","name":"","topic":"","payload":"{\"windowBegin\":\"17:27\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":410,"y":220,"wires":[["b4b5c012.cc46a"]]},{"id":"b4b5c012.cc46a","type":"presence-faker","z":"347d9c0d.faa1e4","name":"","onPayload":"on","onPayloadType":"str","onTopic":"","offPayload":"off","offPayloadType":"str","offTopic":"","windowBegin":"21:15","windowEnd":"21:30","minDurationHours":0,"minDurationMinutes":"20","minDurationSeconds":0,"minDuration":"1200","maxDurationHours":0,"maxDurationMinutes":"55","maxDurationSeconds":0,"maxDuration":"3300","minCount":1,"maxCount":"5","startupBehavior":"onStartup","actionOnDisable":"none","x":740,"y":140,"wires":[[]]},{"id":"fb49963b.3870b8","type":"function","z":"347d9c0d.faa1e4","name":"sunset","func":"var time=global.get('sunset')\ntime='{\"windowBegin\": \"' + time + '\"}'\nmsg.payload=time\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":140,"wires":[["1138d166.a5e9ef"]]},{"id":"c494d686.aa1d48","type":"inject","z":"347d9c0d.faa1e4","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":180,"y":140,"wires":[["fb49963b.3870b8"]]},{"id":"1138d166.a5e9ef","type":"json","z":"347d9c0d.faa1e4","name":"","property":"payload","action":"","pretty":false,"x":550,"y":140,"wires":[["b4b5c012.cc46a"]]},{"id":"bfa941ed.03715","type":"inject","z":"347d9c0d.faa1e4","name":"One AM","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"00 01 * * *","once":false,"onceDelay":"","x":160,"y":60,"wires":[["adf4e158.443bd"]]},{"id":"adf4e158.443bd","type":"subflow:ca4442d5.fa1d1","z":"347d9c0d.faa1e4","name":"","env":[{"name":"latitude","type":"num","value":"40"},{"name":"longitude","type":"num","value":"-100"}],"x":370,"y":60,"wires":[["83cc8f23.9e80a"]]},{"id":"83cc8f23.9e80a","type":"change","z":"347d9c0d.faa1e4","name":"Set to sunset","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.times.sunset","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":60,"wires":[["1901046f.59f93c"]]},{"id":"1901046f.59f93c","type":"function","z":"347d9c0d.faa1e4","name":"Parse Time","func":"var test = msg.payload.toString();\ntest = test.substr(16,8)\nmsg.payload = test;\nreturn msg;","outputs":1,"noerr":0,"x":810,"y":60,"wires":[["a944f667.691b98"]]},{"id":"a944f667.691b98","type":"change","z":"347d9c0d.faa1e4","name":"","rules":[{"t":"set","p":"sunset","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1010,"y":60,"wires":[[]]}]
2 Likes

@camsam - This is perfect! I’m now updating my holiday mode nodered flows!

Hi there, you might also want to check out the suncron node, which can be configured to emit messages based on the sun position.

suncron looks good, very complete. I will still use presence-faker where random offsets are desirable, however.

Found a quirk in the sunPosition subflow. When my flow above ran at 1:00 AM Saturday, it set the sunset variable for Friday evening instead of Saturday. Changed the nightly runtime to 4:00 AM and it worked as expected.

This is what I’m looking for but have no idea where to start with your code.
I’m used to creating nodered flows with the simple fields and gui but not with code.
What is configured in your function and json nodes before the presence-faker?

Thanks

almost there I think. Just the parse time node is not working.

var test = msg.payload.toString();
test = test.substr(16,8);
msg.payload = test;
return msg;
{"value":"2021-09-26T21:44:28.842Z","ts":1632692668842.414,"name":"sunsetEnd","elevation":-0.833,"julian":2459484.40588938,"valid":true,"pos":-2,"deprecated":true,"posOrg":13}
""

This works fine:

var test = "2021-09-26T21:44:28.842Z";
test = test.substr(11,5);
msg.payload = test;
return msg;

= 21:44

But this nothing

var test = msg.payload.toString();
test = test.substr(11,5);
msg.payload = test;
return msg;

Success with updated function:

var test = msg.payload.value.toString();
test = test.substr(16,5)
msg.payload = test;
return msg;

Looking good I think

Adjusted the json settings - all ok - removed the double quotes.

Now I wonder why the sunset offset is not being added to the windowBegin time,

Hi @yonny24 -

It does look like you are getting it.

The version of sun position is different than the one I used. Yours looks more powerful and somewhat more complicated. To me it seems that the offset does not do what you expect it to. I think it is optionally used with a start time and an end time to set a window during which the sun is either up or down, i.e. a boolean called “msg.payload.sunInSky” is true or false. Not sure how you would use it to get what you want.

Cheers

I had been using ver 0.20.8 of Node-RED when I created the example 2019 and had not upgraded until recently (ver 2.0.5). After the above reply, I figured I’d see what’s new and noticed a new (to me) node, “Date/Time Formatter”. I think you can use it to avoid the json step and get your offset at the same time. Try something like this:

I use the schedex node, you set a time, say 7pm and then an offset, you can then tick a box so it does a random time within that offset. ie set the offset to 30, then it will start at a random time between 7pm and 7:30pm