Daylight Saving Time - a few thoughts on how I (almost) manage this in Home Assistant using Node-RED

Disclaimer: this was written by a human (no AI involved) - errors, omissions, and my misunderstandings excepted…

I live in the UK, and it has been a long-established ritual since my youth to wake up and to go round the house one Sunday in March and again in October, moving all timepieces either forward or backward one hour. This task did not happen between 1968 and 1971 when the UK stayed on summer time, and over the years many clocks and devices have become ‘smart’ and now change themselves. My very basic central heating thermostat / time-switch for example does this itself, so it really can’t be that difficult, can it?

I now have a solar PV system with inverter and batteries, and I am using Home Assistant (with Node-RED) to monitor the system. I am looking to automate time-based charging, for saver sessions and possibly with Octopus Flux. Octopus Flux has an off-peak period of 02:00 to 05:00 which will always be on local time, and this presents a problem, since my inverter does not deal with DST automatically. With the need to worry about exactly what happens during the night of the DST change, I have been exploring ways to:

  • work out the correct local time and time zone details
  • calculate when Daylight Saving Time (DST) happens
  • decide how to manage the DST changes around Octopus tariffs and inverter charging
  • and how to convert between UTC and local time for both use and display.

I got up at 01:45 (BST) on Sunday 29th October (2023) to watch my Home Assistant manage the DST change from BST (British Summer Time) to GMT (Greenwich Mean Time) to see exactly what happens, and to check some of my Node-RED code. Sad really, but I thought that, for anyone who is interested in this stuff, I would post some musings, my findings, and a some Node-RED code on the subject.

What is DST?

Some countries around the world, mostly in the more northern and southern latitudes, shift their ‘time zone’ by (usually) one hour during the summer to gain extra daylight. This means that clocks typically go forwards in local spring, and back in the autumn.

Time zones?

Every country / region has a defined local time zone setting, based on effective local solar noon, but often standardised and politically adjusted. Time around the world is based on solar noon at the prime meridian at Greenwich, and world time zones are normally the best-fit to 1 hour increments for every 15 degrees longitude off Greenwich. Zone ‘Z’ or Zulu time is time at Greenwich, or +00:00 from GMT. Then going east, zone ‘A’ at +01:00 (central Europe) and so on to +12:00. West is -01:00 to -11:00. (there are, naturally, a few exceptions to this)

UTC, GMT, and standard time

GMT as a British, later international, standard time was originally based on the average time (against a mechanical clock) for noon, the highest point of the sun passing over the prime meridian at Greenwich (London). While the earth rotates at a constant speed, the earth’s tilt and orbit around the sun causes changes to the exact time of solar noon by around +/- 15 minutes over the course of the year. The average, or arithmetic mean, of the clock time for noon at Greenwich gave rise to Greenwich Mean Time (GMT). This is still measured and tied back to solar noon.

Coordinated Universal Time (UTC) as an international standard is slightly adjusted from GMT – the prime meridian has moved since it was first defined (tectonic plates versus accurate GPS) and clocks are now so accurate that small changes in the earth’s rotation are significant. UTC is based on GPS location and atomic clocks, but still tied back to solar time at Greenwich by being infrequently adjusted with leap-seconds. From 2035 it will not be. For practical purposes, although GMT and UTC are not the same, they are equivalent.

UTC as a timestamp has two unique advantages

  • It is monotonic increasing, thus the difference between any two timestamps on any one device can be simply calculated. (it is not affected by DST)

  • It is the same time worldwide, thus the difference between two timestamps at different locations can still be calculated. (it is not affected by time zone location)

The DST clock change (here in the UK)

On Sunday 29th October 2023 the clocks went back one hour at 01:00 UTC, which would have been 02:00 BST (British Summer Time), and thus turned back to 01:00 GMT (Greenwich Mean Time).

This means that the local time was, in sequence:

01:00 BST … 01:59 BST, then 01:00 GMT … 01:59 GMT, and 02:00 GMT, etc

So, the time period 01:00 – 01:59 happened twice. No problem for those asleep in bed, but this is quite a problem for any computer system.

Home Assistant Time (HAT)

I refer back to petro’s response to my erroneous posting elsewhere.

Home Assistant, as I understand it, has a back end, which knows nothing about the time, and a front end, which always runs on local time. Hence “HA” runs on ‘Local Time’, or HALT.
Servers ideally should never ever run on DST-impacted time as it just causes too many problems. Although HALT being local time is convenient in daily use, it does generate problems, hence the need to record ‘timestamps’ in UTC. Put simply - you can’t have two identical timestamps for different times, which is why 01:30 and 01:30 have to be either 01:30 BST and 01:30 GMT with the timezone suffix, or 00:30 UTC and 01:30 UTC (which does not need a suffix as it does not change).

The HA machine local ‘time zone’ can be thought of as an offset from GMT (or UTC). This is set in HA configuration, as +/- a number of hours, and this offset can be easily used to convert from local time to UTC, just add the ‘offset’ to UTC to get local time.

DST is far more complicated since this is ‘location-event based’. This requires, for example, the time zone ‘London’ setting, which can be used to lookup the time zone and DST rules from a TZ (time zone) database or similar.

Both Python and JavaScript have time zone aware libraries that can be used to deal with DST, and there are websites and APIs available to find out current time based on geo-location.

I use the forecast.solar integration in HA, and also call the forecast API myself directly. Solar forecast calls require geo-location (either lifted from the HA location settings or added manually) and as part of the result returned include the location details (place name) and forecast data given with both UTC and local time. This is yet another way of finding out where you are and what time it is… From my forecast.solar API calls I can use the difference between the returned UTC and ‘local’ time to work out time zone offset and current DST setting.

My Observations:

  • HALT (local time) does indeed go from 01:59 to 01:00 at DST change (BST->GMT) and appeared to do so ‘instantly’ (it was worth getting up just to check that)

  • HA has a date-time integration which can provide sensors for date, time, date-time, ISO time, and UTC time. ISO time is local time formatted to ISO standard, and hence the difference between the same time (ie now) in UTC and ISO local time format can be used to calculate the machine time zone offset (including DST if it currently applies).

https://www.home-assistant.io/integrations/time_date/

  • HA generates all timestamps in UTC, so they have to be converted from / to local time (hence the time zone and DST must be set correctly). HA time zone settings are in configuration. Node-RED, which I use, pulls TZ details from the host machine operating system (ie HA) however this can be read and modified using the Node-RED environment variable process.TZ.

  • UTC timestamps are converted to local time by HA for display, either by the ‘front end’ or by JavaScript in the web pages on display devices. This can make debugging challenging when trying to display a UTC timestamp as it often ends up being converted to local time, either by HA or by the browser display device. (HA now also applies ‘relative’ time to timestamps shown in the dashboard, thus what I think is a time can appear in the dashboard as ‘5 minutes ago’ which does not always help.)

So, above is some Octopus Agile pricing which is given in UTC. In the UK in the summer this should be BST as +1 hour, but I am looking at my HA using NabuCasa when on holiday in Europe, so my phone works out where it is (from the local network signals) and sets its time zone to Central European Time, but being in DST it is CEST (Central Europe Summer Time) which is +2 hour off UTC and +1 hour off BST. Confusing certainly: this gives me the same event in three time zones - the Octopus Agile price time in UTC, my machine back home time in BST, and my phone time in CEST.

Below is a part of the flex-table card setting, where I use JavaScript to modify a UTC time stamp using the toLocaleString function to achieve the display shown above.

  - data: both_array
    modify: >-
      (new Date(x.from)).toLocaleString("en-GB", {day:"2-digit", month:"short",
      hour:"2-digit", minute:"2-digit", timeZoneName:"short"})
    name: Start

Observation - you don’t know what the time really is without a time zone qualifier (thus UTC/GMT/BST/CET/CEST etc should ideally appear against every time).

My immediate DST problems are…

  • One day a year there are 25 hours, and one day a year 23 hours in the day. (So that 01:00-01:59 happens twice once a year, and does not happen at all once a year.)

  • Octopus energy tariffs are posted using UTC only. What happens at DST when I want to charge my solar battery?

  • Octopus meter readings are given using local time.

  • My inverter time has to be manually changed when DST happens.

Problem – setting an alarm to get up (you can’t).

I used my android ‘smart’ phone to set an alarm for 01:45. It would only set this for 01:45 GMT (the second 01:45 not the first one that I really wanted). I had to use a timer instead. Since 01:45 BST could be confused with 01:45 GMT, the phone selected the GMT time for me. As it turns out, this is basically how software deals with the DST-backwards change; it ignores one of the two duplicate hour periods. The question is, which ‘hour’ does it ignore?

Problem – my inverter time has to be changed manually

My inverter does not keep good time – it gains about 4 or 5 seconds a day. For accurate time-based charge/discharge switching this means I have to regularly reset the time. I use a bit of code that compares HA time to inverter time following every Modbus read. When the difference is 20 seconds I write the machine (local) time back to the inverter. I recently extended this to action the DST one-hour change as well, and this was the code I wanted to test.

It worked, but not perfectly. The issue comes down to JSONata use of $toMillis().

In JSONata $toMillis() takes a string timestamp and converts to Unix Milliseconds, using underlying JavaScript.

If the string is “2023-10-29T01:30:00.000Z” then $toMillis() takes this as UTC time and converts accordingly.
For a string “2023-10-29T01:30:00” without the critical ‘Z’, $toMillis() takes this as ‘local’ time and will correctly change this to UTC, adjusting for the local time zone (DST aware).

This feature I use as an easy way to find the current time zone offset – uniquely for the UK if UTC time equals local time then it must be GMT, otherwise we are on BST.

In JSONata, $now() returns a UTC time stamp, so it is very easy to chop off the trailing part and

(
    $s:=function($t){$floor($toMillis($t)/1000)};
    $n:=$now();
    $l:=$substringBefore($n,".");
    $s($l)-$s($n);
)

will be the seconds between local time and UTC. Simple! 0 seconds, GMT, 3600 seconds, BST.

But there is a slight problem with JavaScript
01:30 local time was BST hence 00:30 UTC and the code works.

02:00 local time was now GMT hence 02:00 UTC which also works nicely.

However, between 01:00 UTC and 02:00 UTC (01:00-02:00 GMT) using a local timestamp returns UTC-1.

This is because a simple $toMillis() can’t tell the difference between 01:30 BST and 01:30 GMT without the time zone, so apparently assumes BST (and thus subtracts an hour) right up to 02:00. My code failed, as the local time was 01:30 GMT, which the code treated as 01:30 BST the second time around, and still subtracted one hour to get to UTC when it should not have done.

Since JavaScript underpins much of the HA front end and stuff like JSONata, it also applies to templating. A quick check on HA templates shows that

JavaScript standard states that

If the time zone offset is absent, the date-time is interpreted as a local time.

The nub of the problem is that 00:30 UTC when in BST maps to 01:30, and 01:30 UTC when in GMT also maps to 01:30. Going backwards, 01:30 local time is seen as 01:30 BST even when the machine is now set to GMT (but only for one hour). My HA had moved the local time back to 01:30 GMT, but without a given +00:00 JavaScript still works in terms of BST between 01:00 GMT and 02:00 GMT.

Conclusion: HA moved to local time at 01:00 UTC, JavaScript effectively moved to local time at 02:00 UTC.

I found the following article (see Broken Parser – A Web Reality Issue) informative, particularly as it confirms that in JS a date-time without a time zone will always be regarded as local time.

https://maggiepint.com/2017/04/11/fixing-javascript-date-web-compatibility-and-reality/

Problem – I am triggering a flow every hour using a timer

I use both the inject node on repeat as well as cronplus node to trigger flows at regular intervals. The cronplus node is set to “30 3 * * * ? *” so that it fires at three and half minutes past each hour. I can confirm that this node fired at 00:03:30, then not at 01:03:30 (BST) but did fire at 01:03:30 GMT.

This meant that my flow did not run at 01:03:30 BST and therefore did not run twice at 01:03:30 ‘local time’, but that left a gap of two actual hours between the every-hour flow execution.

What if I do want to run a flow every single hour? The simple ‘inject’ node is actually quite complicated when using ‘repeat’. For ‘interval’ it will fire every, say 60 minutes, based on when the flow was deployed or restarted. However, for ‘interval between times’ and ‘at a specific time’ the nodes uses cron, hence every 60 minutes will fire at the exact hour. Since ‘cron’ appears to be DST aware, the DST-backwards (first) hour apparently cannot be triggered. Same issue as my smartphone!

Problem - Octopus meter readings are in local time

Yes, 01:00:00 and 01:30:00 happen twice. For keeping records based on a 24 hour day, the best thing I can do is to add them together so that I get one set of 48 half-hours to the day. When the clocks go forward I will have to add in two half-hours that are missing for 01:00:00 and 01:30:00

Problem – Octopus Agile tariff is in UTC

Octopus tariffs (UK energy supplier) are provided only in UTC, so during BST the local time is one hour ahead. Since humans work on local time, it would be good to display and work with the Agile price times in BST. However, for controlling such things as timed-charging, it is necessary to ensure that both the HA automation and the inverter / battery are using the same time zone.

Working with an array of tariffs that spans DST time change requires more work. I have written stand alone Node-RED code to calculate the DST time changes (in UTC) which I use to convert UTC to local time for an array, including where it crosses a DST time change.

  "import": [
    {
      "from": "2023-10-28T23:30:00Z",
      "upto": "2023-10-29T00:00:00Z",
      "date": "2023-10-28",
      "timefrom": "23:30",
      "timeupto": "00:00",
      "localdate": "2023-10-29",
      "localfrom": "00:30 BST",
      "localupto": "01:00 BST",
      "dstchange": false,
      "isofrom": "2023-10-29T00:30:00.00+01:00",
      "isoupto": "2023-10-29T01:00:00.00+01:00",
      "value": 2.898,
      "index": 0,
      "link_last": false,
      "link_next": false,
      "position": "only"
    },
    {
      "from": "2023-10-29T01:00:00Z",
      "upto": "2023-10-29T01:30:00Z",
      "date": "2023-10-29",
      "timefrom": "01:00",
      "timeupto": "01:30",
      "localdate": "2023-10-29",
      "localfrom": "01:00 GMT",
      "localupto": "01:30 GMT",
      "dstchange": false,
      "isofrom": "2023-10-29T01:00:00.00+00:00",
      "isoupto": "2023-10-29T01:30:00.00+00:00",
      "value": 4.83,
      "index": 1,
      "link_last": false,
      "link_next": true,
      "position": "start"
    },

The extract above is ‘best periods’ and shows it is possible to convert UTC to local time correctly, adding either GMT/BST or +01:00 etc as required. The ‘from’ and ‘upto’ are the Octopus given times in UTC. The ‘local-’ and ‘iso-’ fields are calculated from UTC using my pre-calculated DST start/end times.

Even more demanding is managing a binary switch where the ON-OFF (consecutive period) extends across a DST time change.

For my Agile Binary Switch, I use the cronplus node to setup dynamic schedules based on combined best-price Agile tariff periods. The cronplus node requires the calculated and injected dynamic schedules to be defined in local time (not UTC).

[
  {
    "switch": "ON",
    "name": "OctopusAgile ON 2",
    "active": true,
    "description": "in 4 hours 25 minutes 17 seconds",
    "UTC": "2023-10-29T00:30:25.000Z",
    "local": "Oct 29, 2023, 01:30:25 GMT+1"
  },
  {
    "switch": "OFF",
    "name": "OctopusAgile OFF 2",
    "active": true,
    "description": "in 6 hours 54 minutes 27 seconds",
    "UTC": "2023-10-29T02:59:35.000Z",
    "local": "Oct 29, 2023, 02:59:35 GMT"
  }
]

Once dynamic schedules are passed to and created in the cronplus node, they can be output as ‘status’ for checking. The status output (above) shows UTC times as well as local times. To avoid timing errors and switching ‘on the hour’ I have shifted the switch start and stop by an adjustable +/- 25 seconds, but the outcome is a time period that runs from 00:30 to 03:00 UTC or 2.5 hours. In local time, across the DST change, this is 01:30 BST to 03:00 GMT - still 2.5 hours. It works!

Node-RED Code to calculate UK (or Europe) DST change times
This is a single change node with JSONata. It is stand-alone, and caclulates the DST start and end times for the UK based on the current rule: 01:00 UTC last Sunday in March / October.
It will update the start/end times only on year change, but can be called anytime to update the outcomes. The output provides the current DST setting and time zone name, and can be used to decide if a DST change is within 48 hours, and if so, which way (GMT to BST or BST to GMT).

This works for the UK but can be adapted for Europe as we use the same rule.

Node-RED code: DST for UK:

[{"id":"18a019dec90422b8","type":"change","z":"4459582bc445079d","g":"676f561a30e4c7c0","name":"UK DST","rules":[{"t":"set","p":"dst","pt":"msg","to":"DST","tot":"flow","dc":true},{"t":"set","p":"dst","pt":"msg","to":"(\t/* November 2023: For UK only - calculate DST dates (GMT->BST, BST->GMT) */\t\t/* $now() is evaluated once only and used throughout the expression */\t    $nw:=$now();\t\t/* FUNCTION: return day of week (DOW) given 'yyyy-mm-dd' where SUN=0 */\t    $dayofweek:=function($date) {(\t        $y:=$number($substring($date,0,4));\t        $m:=$number($substring($date,5,2));\t        $d:=$number($substring($date,8,2));\t        $m<3 ? $y:=$y-1;\t        $t:=[0,3,2,5,0,3,5,1,4,6,2,4];\t        ($y + $floor($y/4) - $floor($y/100) + $floor($y/400) + $t[$m-1] +$d)%7;\t    )};\t\t/* FUNCTION: return start and end DST timestamps for a given year yyyy (int) */\t    $dstdates:=function($year) {(\t        $mar:=$year & \"-03-\";\t        $oct:=$year & \"-10-\";\t        $dstrt:=$mar & (31-$dayofweek($mar & \"31\")  & \"T01:00:00.000Z\");\t        $dstop:=$oct & (31-$dayofweek($oct & \"31\")  & \"T01:00:00.000Z\");\t        $a:=$toMillis($dstrt);\t        $b:=$toMillis($dstop);\t        $n:=$toMillis($nw);\t        $mp:=($a+$b)/2;\t\t        {\"year\": $year,\t        \"timestamp\": $nw,\t        \"now\": $n,\t        \"DSTstart\": $dstrt,\t        \"on\": $a,\t        \"DSTstop\":  $dstop,\t        \"off\": $b,\t        \"DSToffset\": (1*60*60*1000),\t        \"midpoint\": $mp\t        }\t    )};\t\t/* read in from context - if does not exist or if year has changed, update */\t    $yearis:=$number($substringBefore($nw,\"-\"));\t    $dst:= $exists(dst) ? dst : $dstdates($yearis);\t    $dst.year!=$yearis ? $dstdates($yearis);\t\t/* update 'now' in record and decide if DST (GMT/BST), if 'close' to change */\t/* note which way it went/will go, and which timestamp to test against      */\t\t    $n:=$toMillis($nw);\t    $a:=$dst.on;\t    $b:=$dst.off;\t    $mp:=$dst.midpoint;\t\t    $tz:= $n<$a or $n>=$b ? \"GMT\" : \"BST\";\t    $close:=$min([$abs($n-$a), $abs($n-$b)])/3600000<=48;    /* test for 48 hours either side */\t    $GMTBST:=$n<$mp;\t\t    $tzfrom:= $close ? ($GMTBST ? \"GMT\" : \"BST\" ) : $tz;\t    $tzgoto:= $close ? ($GMTBST ? \"BST\" : \"GMT\" ) : $tz;\t\t    $update:={\t        \"timestamp\": $nw,\t        \"now\": $n,\t        \"TZnow\": $tz,\t        \"DSTon\": $tz=\"BST\",        \t        \"changing\": $close,\t        \"toDST\": $GMTBST,\t        \"TZfrom\": $tzfrom,\t        \"TZtobe\": $tzgoto,\t        \"testvalue\": $GMTBST ? $a:$b\t    };\t    $dst ~> | $ | $update |\t)","tot":"jsonata"},{"t":"set","p":"DST","pt":"flow","to":"dst","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":160,"wires":[["20e3255ecf185722"]]}]

Produces

{
"year":2023,
"timestamp":"2023-11-05T22:31:39.010Z",
"now":1699223499010,
"DSTstart":"2023-03-26T01:00:00.000Z",
"on":1679792400000,
"DSTstop":"2023-10-29T01:00:00.000Z",
"off":1698541200000,
"DSToffset":3600000,
"midpoint":1689166800000,
"TZnow":"GMT",
"DSTon":false,
"changing":false,
"toDST":false,
"TZfrom":"GMT",
"TZtobe":"GMT",
"testvalue":1698541200000
}

Date-time formatter (moment) node
I really like this node. It is a time formatting node using the moment.js library, and it can take in any time format in any time zone and convert it to any time format in any time zone. It can also add/subtract an offset to a timestamp, so great for getting ‘yesterday’ or ‘tomorrow’, and it manages DST (mostly).

Without an input it assumes ‘now’ timestamp
Without a locale setting it assumes the machine locale setting (‘C’).
Without an input/output time zone it assume the machine time zone, and lifts these from settings.

The node is very flexible - it can take in almost any well-formatted time stamp string or number, and easy for example to set the output time zone to ‘Europe/Paris’ in case you want to run HA in London for someone in Paris…

It can also accept any of the moment.js library formatting instructions, so can be used to obtain date, time, string representations of day and month, day of week, week of year, day of year, and Unix time in milliseconds or seconds, as well as time zone offset and time zone name.

My bucket has a hole in it dear Home Assistant, a hole

The moment.js library is a great bit of code as it contains encoded all the time zone and DST instructions for every world location, variously packaged with 10-years of data or 1970-2030 (at the time of writing). If you have read this far and want the full detail, then

https://momentjs.com/timezone/docs/#/using-timezones/parsing-ambiguous-inputs/

is a good explanation on how moment.js manages DST with the ambiguity of duplicate and missing hours. It is worth noting that, for fall-back with the duplicate hour, only the first instance of the hour is used. Hence, for me, 01:30 (either BST or GMT) will always be seen to be 01:30 BST and converted to 00:30 UTC by subtracting one hour. Again, given that HA has moved 01:30 to 01:30 GMT, there appears to be no simple way to take Home Assistant Local Time (HALT) and convert to UTC correctly for this one hour period, without using the time zone offset.
So I need the local time from HA, with the time zone information.
HA gives UTC as 2023-11-12, 11:49
HA gives ISO (local) as 2023-11-12T11:49:00

There is no time zone given. I still think that HA uses UTC and converts this to local time, but it would be good if HA UTC was in the full format including TZ.

To get full format UTC from the HA UTC entity I can modify the given string by adding the ISO ‘T’ and a trailing ‘00.000Z’, but there are other ways to do this.

Node-RED code to extract additional time information such as day of week, week of year, and so on. Useful stuff. It is worth noting that the HA date-time integration only updates every minute on the minute (and this applies to HA templates with date time sensors) so it is necessary to use other means to get times with seconds, and other means if you want the time zone itself.

[{"id":"5645415b8af83243","type":"inject","z":"95dab86a0bf42ced","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":1440,"wires":[["27da4d8646612fda"]]},{"id":"4ebe207b619b65b3","type":"debug","z":"95dab86a0bf42ced","name":"payload","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":660,"y":1440,"wires":[]},{"id":"27da4d8646612fda","type":"moment","z":"95dab86a0bf42ced","name":"DateTimeInfo","topic":"LocalTime","input":"","inputType":"date","inTz":"Europe/London","adjAmount":0,"adjType":"days","adjDir":"add","format":"[date]YYYY-MM-DD [time]HH:mm:ss.SSS [day]ddd [month]MMM [quarter]Q [doy]DDD [dow]e [week]W [unix]X [zone]Z [tz]z","locale":"C","output":"payload","outputType":"msg","outTz":"Europe/London","x":300,"y":1440,"wires":[["e146d5b8933273f9"]]},{"id":"e146d5b8933273f9","type":"change","z":"95dab86a0bf42ced","name":"Extract Fields","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t    \t/* FUNCTION: take payload array, extract matching field then return value */\t    $value:=function($field){(\t        $a[$contains($field)]~>$replace($field,\"\");\t    )};\t\t    $a:=$split(payload,\" \");\t    $date:=$value(\"date\");\t    $datearray:=$split($date,\"-\");\t    $fullyear:=$number($datearray[0]);\t    $time:=$value(\"time\");\t    $timearray:=$split($time,\":\");\t    $fullsecond:=$timearray[2]~>$number();\t    $second:=$floor($fullsecond);\t    $quarter:=$value(\"quarter\")~>$number();\t    $dayofweek:=$value(\"dow\")~>$number();\t    $dayofyear:=$value(\"doy\")~>$number();\t    $week:=$value(\"week\")~>$number();\t    $unixsec:=$value(\"unix\")~>$number();\t    $zone:=$value(\"zone\");\t    $offset:=$parseInteger($substringBefore($zone,\":\"), \"#0\");\t    $tzname:=$value(\"tz\");\t    $t:=$date & \"T\" & $time & \"Z\";\t    $utc:=$fromMillis($toMillis($t) + (3600000*$offset));\t    {\"date\": $date,\t    \"fullyear\": $fullyear,\t    \"year\": $fullyear-2000,\t    \"month\": $number($datearray[1]),\t    \"day\": $number($datearray[2]),\t    \"dayis\": $value(\"day\"),\t    \"monthis\": $value(\"month\"),\t    \"time\": $time,\t    \"hour\": $number($timearray[0]),\t    \"minute\": $number($timearray[1]),\t    \"fullsecond\": $fullsecond,\t    \"second\": $second,\t    \"unixseconds\": $unixsec,\t    \"quarter\": $quarter,\t    \"week\": $week,\t    \"dayofweek\": $dayofweek,\t    \"dayofyear\": $dayofyear,\t    \"timezone\": $zone,\t    \"tzname\": $tzname,\t    \"offsethours\": $offset,\t    \"utc\": $utc,\t    \"now\": $now()\t    };\t\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":1440,"wires":[["4ebe207b619b65b3"]]}]

This generates

{"date":"2023-11-12",
"fullyear":2023,
"year":23,
"month":11,
"day":12,
"dayis":"Sun",
"monthis":"Nov",
"time":"11:57:35.611",
"hour":11,"minute":57,
"fullsecond":35.611,
"second":35,
"unixseconds":1699790255,
"quarter":4,
"week":45,
"dayofweek":0,
"dayofyear":316,
"timezone":"+00:00",
"tzname":"GMT",
"offsethours":0,
"utc":"2023-11-12T11:57:35.611Z",
"now":"2023-11-12T11:57:35.611Z"}

And finally - reading HA and NR time zone settings (configuration)
Using an API node, websocket mode, the command

{"type": "get_config"}

pulls the HA configuration, including ‘time_zone’ as “Europe/London” in my case.

Using a simple inject node, set to ‘$ env variable’ and ‘TZ’ as the variable will get the NR time zone, which should be the same unless it has been explicitly modified.

Conclusion
DST is a challenge, and not easy to deal with.
Mapping between Local time and UTC requires the full time zone to work around DST, whatever method is used.

Testing any DST code is not easy, and the events only happen once per year, so in five months time I will probably get up at 00:45 GMT and wait for the clocks to go forward so I can again check to see if my code is working correctly!

Time for a coffee. Does anyone actually read this stuff?

3 Likes

I wrote a macro library that calculates all this.

Here’s 3 sensors that you can use in automations that tells you everything you’d want to know about DST.

template:
- sensor:
  - name: Next Daylight Savings
    unique_id: NextDaylightSavings
    device_class: timestamp
    state: >
      {% from 'easy_time.jinja' import next_dst %}
      {{ next_dst() }}

  - name: Days Until Next Daylight Savings
    unique_id: DaysUntilNextDaylightSavings
    unit_of_measurement: days
    state: >
      {% from 'easy_time.jinja' import days_until_dst %}
      {{ days_until_dst() }} 

  - name: Daylight Savings Phrase
    unique_id: DaylightSavingsPhrase
    unit_of_measurement: days
    state: >
      {% from 'easy_time.jinja' import next_dst_phrase %}
      {{ next_dst_phrase() }} 

Then you can use them in your automations. FYI the phrase is translated to your language.

The example phrase in english is gain 1 hour, or lose 1 hour.

I know, I have researched and read some of it.

I do my work in Node-RED rather than templates (hence the title includes ‘using Node-RED’) and so I am looking for something similar in that. I did look at your DST coding and noted that (if I am correct) you are looping round a full year to find the DST change point by inspection. I do my calculation directly using the UK/EU rule, and I calculate both the next and previous timestamps (and can do this for any given year) as a function. Just another way of doing this, and coping with the situation, in Node-RED.

Seriously - if you don’t feel that my posting adds any value to the community then please do delete it.

It loops at most 182 + 24 + 60 times and breaks on first found value, which is typically hour 2 minute 0. So typically the most loops is 184.

I never said that. On a personal note, this is a huge wall of text and it will be a sensor overload for most people who want a quick dirty solution.

Yes, I quite understand - I have spend many hours writing up this post over the past two months trying to get the right balance (and get it factually correct) but if the ‘quick and dirty solution’ is what is wanted here then I am probably the wrong person to be contributing in this forum.

Can you remove the entire posting, or tell me how best to do so?
Thanks

I don’t understand why you’re so offended… I’m just giving some pointers. There’s no reason to delete it.

I have been dealing with time calculations and time changes on computers for a very long time (I don’t even want to calculate that :slightly_smiling_face:). It has been fascinating and frustrating.

I sincerely thank you BOTH (@Biscuit & @petro) for your contributions on this.

1 Like