Goodbye “bigtimer” node… [for now] – NodeRED sun and time

It's funny, how a simpler solution comes to you AFTER you figured out the complicated way!

From time to time, I get a crazy idea of changing something small in my automation setup. Usually, an idea like that drags me into a rabbit hole, I didn’t know it had existed. Whatever I do nowadays, I always look at it with an angle: “Would that make an interesting tutorial?” and that angle has turned a simple “Is it night time yet?”  query, into a 200 line long coding challenge. I hope someone will appreciate it the fact I made a tool to check which part of the world needs a flashlight.

The “bigtimer” node is great… until it isn’t

I have been using the “bigtimer” node for a long time. As my NodeRED flows grow, adding the same nodes each time around becomes a burden and a complication that you have to deal with. Having a global set of values that I can use is the best coding practice, than reconfiguring a “bigtimer” node each time you want to deal with a timer or reconfigure a group of the devices that are not connected to the same node.

I wanted to have a global variable, which would store information about the time of the day: day/night. On the surface, it’s a simple idea. When you actually approach the problem (and the fact that you would like to share the idea with other people in different parts of the world) things get a little bit harder.

If you are here to rip the fruit of my work, scroll down to the bottom and download the flow. You can also buy me a coffee while you at it.

Creating a time keeping alternative

To calculate the day|night I decide to convert the current time to seconds from 1970 (Unix time)  and compare the numeric values against the sunset and sunrise values. To be able to do so, I need sunset and sunrise values. I can obtain this using a free sunrise-sunset.org/api. While personally, I can use UTC (the only time zone they offer the values for) I get tangled into the daylight saving, and you my dear reader won’t even come close to that time zone according to my Google Analytics.

Exploring the rabbit hole

I spare you the boring details, this is what information you can get thanks to my NodeRED flow. When you provide your geographical coordinates (longitude, latitude) the flow will:

  • tell you if it’s night or day
  • tell you the sunrise and sunset time
  • tell you the day length
  • provide you with astronomical, nautical, civil dusk and dawn times
  • use these values to create timers
  • all that good stuff in your local time!

Step one: changing coords into a time zone.

Geographical coordinates are used to determine the sunset/sunrise times and the time zone. I need that information otherwise, you are stuck forever with UTC. I didn’t want to hardcode the details to make the flow accessible to everyone. There is a free time zone API that can be used to work out your time zone and the time offset between your home and UTC.

FUNCTION NODE: get timezone
var API = global.get('API_timezone');
var lat = msg.payload.lat;
var lng = msg.payload.long;

msg.url = "http://api.timezonedb.com/v2.1/get-time-zone?key="+API+"&format=json&fields=zoneName&by=position&lat="+lat+"&lng="+lng;
msg.position = {"lat": lat,
                "long": lng,
} 
return msg;
// stored in payload.zoneName
FUNCTION NODE: Save the timezone/coords
global.set('Time_timezone', msg.payload.zoneName);
global.set('Time_long', msg.position.long);
global.set('Time_lat', msg.position.lat);

return msg;

A set of coordinates (saved globally as I will use it each time to query the sun info) is used to establish the home time zone with an HTTP GET request. This request will give me a time zone needed to calculate the offset.

Step two: from X to UTC in plenty of seconds

To calculate the local time, I need an offset. There is a very clever way of calculating the time difference. If I change all values into seconds, I can simply add and subtract the values. Since I already know the “home” zone – I can start the second HTTP GET query, to calculate the offset in seconds between your “home” zone and UTC. I will store that offset as a global variable.

FUNCTION NODE: get the offset
var from = "Europe/London";
var to = global.get('Time_timezone');
var API = global.get('API_timezone');

msg.url = "http://api.timezonedb.com/v2.1/convert-time-zone?key="+API+"&format=json&fields=offset&from="+from+"&to="+to;
return msg;

//https://timezonedb.com/references/convert-time-zone
FUNCTION NODE: Set zone offset
global.set('TimeZone_offset', msg.payload.offset);

return msg;

Step three: rise and shine

I know the coordinates. I know the offset. I can make the last HTTP GET request to get the sun info in UTC. This request will contain all the sun information. Before I can store it as global values, I have to recalculate the times.

FUNCTION NODE: Save Recalculated times (the long way)
var timeZoneOffset = global.get('TimeZone_offset');


function recalculateTime(x){
    var z = new Date(x).valueOf();
    var adjustedTime = (z/1000 + timeZoneOffset)*1000;
    return new Date(adjustedTime);
}

function msToTime(duration) {
  var milliseconds = parseInt((duration % 100) / 100);
   var seconds = parseInt((duration / 100) % 60);
    var minutes = parseInt((duration / (100 * 60)) % 60);
    var hours = parseInt((duration / (100 * 60 * 60)) % 24);

  hours = (hours < 10) ? "0" + hours : hours;
  minutes = (minutes < 10) ? "0" + minutes : minutes;
  seconds = (seconds < 10) ? "0" + seconds : seconds;

  return hours + ":" + minutes + ":" + seconds + "." + milliseconds;
}

//sunrise
var sunrise = recalculateTime(msg.payload.results.sunrise);
global.set('Time_sunrise', sunrise);

//sunset
var sunset = recalculateTime(msg.payload.results.sunset);
global.set('Time_sunset', sunset);

//solar_noon
var solar_noon = recalculateTime(msg.payload.results.solar_noon);
global.set('Time_solar_noon', solar_noon);

//day_length
var day_length =  msToTime(msg.payload.results.day_length);
global.set('Time_day_length', day_length);

//civil_twilight_begin
var civil_twilight_begin = recalculateTime(msg.payload.results.civil_twilight_begin);
global.set('Time_civil_twilight_begin', civil_twilight_begin);

//civil_twilight_end
var civil_twilight_end = recalculateTime(msg.payload.results.civil_twilight_end);
global.set('Time_civil_twilight_end', civil_twilight_end);

//nautical_twilight_begin
var nautical_twilight_begin = recalculateTime(msg.payload.results.nautical_twilight_begin);
global.set('Time_nautical_twilight_begin', nautical_twilight_begin);

//nautical_twilight_end
var nautical_twilight_end = recalculateTime(msg.payload.results.nautical_twilight_end);
global.set('Time_nautical_twilight_end', nautical_twilight_end);

//astronomical_twilight_begin
var astronomical_twilight_begin = recalculateTime(msg.payload.results.astronomical_twilight_begin);
global.set('Time_astronomical_twilight_begin', astronomical_twilight_begin);

//astronomical_twilight_end
var astronomical_twilight_end = recalculateTime(msg.payload.results.astronomical_twilight_end);
global.set('Time_astronomical_twilight_end', astronomical_twilight_end);



return msg;

I mentioned before, that the easiest way would be to change the values to seconds, add the offset and change the values back to a human-readable format. This applies to all values apart from the “day length” as that value is given in seconds already.

Step one too many: the easy way

And then I found getTimezoneOffset(); You can forget steps 1 and 2. It’s fair to say I learned a thing or two along the way. This method basically takes the local timezone and calculates the offset between the local timezone and the UTC. Which just ruined half of the tutorial! Oh well… now you have to examples to download.  The returned value is in minutes, so there will be a little bit of math to do.

FUNCTION NODE: Save Recalculated times (the easy way)
let date = new Date()
timeZoneOffset = date.getTimezoneOffset() *60;

function recalculateTime(x){
    var z = new Date(x).valueOf();
    var adjustedTime = (z/1000 + timeZoneOffset)*1000;
    return new Date(adjustedTime);
}

function secondsToHms(d) {
    d = Number(d);

    var h = Math.floor(d / 3600);
    var m = Math.floor(d % 3600 / 60);

    return ('0' + h).slice(-2) + "h " + ('0' + m).slice(-2)+"min";
}

//sunrise
var sunrise = recalculateTime(msg.payload.results.sunrise);
global.set('Time_sunrise', sunrise);

//sunset
var sunset = recalculateTime(msg.payload.results.sunset);
global.set('Time_sunset', sunset);

//solar_noon
var solar_noon = recalculateTime(msg.payload.results.solar_noon);
global.set('Time_solar_noon', solar_noon);

//day_length
var day_length =  secondsToHms(msg.payload.results.day_length);
global.set('Time_day_length', day_length);

//civil_twilight_begin
var civil_twilight_begin = recalculateTime(msg.payload.results.civil_twilight_begin);
global.set('Time_civil_twilight_begin', civil_twilight_begin);

//civil_twilight_end
var civil_twilight_end = recalculateTime(msg.payload.results.civil_twilight_end);
global.set('Time_civil_twilight_end', civil_twilight_end);

//nautical_twilight_begin
var nautical_twilight_begin = recalculateTime(msg.payload.results.nautical_twilight_begin);
global.set('Time_nautical_twilight_begin', nautical_twilight_begin);

//nautical_twilight_end
var nautical_twilight_end = recalculateTime(msg.payload.results.nautical_twilight_end);
global.set('Time_nautical_twilight_end', nautical_twilight_end);

//astronomical_twilight_begin
var astronomical_twilight_begin = recalculateTime(msg.payload.results.astronomical_twilight_begin);
global.set('Time_astronomical_twilight_begin', astronomical_twilight_begin);

//astronomical_twilight_end
var astronomical_twilight_end = recalculateTime(msg.payload.results.astronomical_twilight_end);
global.set('Time_astronomical_twilight_end', astronomical_twilight_end);



return msg;

Where are the timers?

The time-related methods can be used to extract the years,  months, days, hours,  minutes etc. Each value can be compared with a condition using a relatively simple IF statement. You have seen me doing this with “is it night time yet”  where I would compare the current time against the sunrise|sunset conditions.  The same applies when comparing other time values.

FUNCTION NODE: Is it night time yet?
var time = new Date();
var timeNow = time.getTime();

var sunset  = global.get('Time_sunset');
var sunrise = global.get('Time_sunrise');

var sunsetToday = sunset.getTime();
var sunriseToday = sunrise.getTime();

if(timeNow < sunsetToday && timeNow > sunriseToday){
    msg.payload = "it's daytime"
    
}

if(timeNow > sunriseToday && timeNow < sunsetToday){ msg.payload = "it's night" } if(timeNow > sunsetToday){
    msg.payload = "it's night"
}
if(timeNow < sunriseToday){
    msg.payload = "it's night"
}


msg.times = {
    "sunset": sunsetToday,
    "sunrise": sunriseToday,
    "time": timeNow
}

return msg;

The “bigtimer” will have its uses, and my method is not a replacement for that useful node.  If you want to check the value of the time against the existing condition inside the function node, now you have the toolset to do so.

FUNCTION NODE: Test Globals
//sunrise
msg.sunrise = global.get('Time_sunrise');

//sunset
msg.sunset = global.get('Time_sunset');

//solar_noon
msg.noon = global.get('Time_solar_noon');

//day_length
msg.day = global.get('Time_day_length');

//civil_twilight_begin

msg.ctwilight_begin = global.get('Time_civil_twilight_begin');

//civil_twilight_end
msg.ctwilight_end = global.get('Time_civil_twilight_end');

//nautical_twilight_begin
msg.ntwilight_begin = global.get('Time_nautical_twilight_begin');

//nautical_twilight_end

msg.ntwilight_end = global.get('Time_nautical_twilight_end');

//astronomical_twilight_begin
msg.atwilight_begin = global.get('Time_astronomical_twilight_begin');

//astronomical_twilight_end
msg.atwilight_end = global.get('Time_astronomical_twilight_end');



return msg;

Conclusion

Sometimes, it’s all about the journey. I spend an evening working on the flow. The last method was something I came accross after I finished it. I don’t feel bad about it as I like puzzles, but at the same time I would save myself a bit of time and move to the next project earlier. They say knowledge is power. If you read the whole article, you have the power too! Join the discussion on Reddit about this post!

Support NotEnoughTech
A lot of time and effort goes into keeping NotEnoughTech alive! If my work helped you out, consider buying me a coffee or check out exclusive rewards available to Patreon supporters.
SHARE