Daylight Savings Time (DST) - Template and Automation

Here in The Netherlands and most of the other countries in Europe it’s always last weekend of October and last weekend of March. :wink:
I understand the principle of adapting the above code. :+1:

Date of the last Sunday in October or March can be 25-31. Because the range is the same for both months, the macro’s code is simplified.

Try this version and let me know if it works for your time zone.

{%- macro get_date(mode, yr) %}
{%- set ns = namespace(d=[]) %}
{%- set t = now().replace(year=yr, month = 3 if mode == 'dst' else 10, hour=3, minute=0, second=0, microsecond=0) %}
{%- for i in range(25, 31) if t.replace(day=i).timetuple().tm_isdst == (1 if mode == 'dst' else 0) %}  
{%- set ns.d = ns.d + [i] %}
{%- endfor -%}
{{ t.replace(day=ns.d[0]) }}
{%- endmacro -%}

{{ get_date('dst', 2022) }}
{{ get_date('std', 2022) }}
1 Like

I originally had something similar to yours however I kept getting questions about the setup similiar to yours. You had to adjust the time and it would end up being off if you didn’t create the proper setup variables. So I came up with this solution that will work for everyone with just a copy/paste.

1 Like

IIRC my original DST, you only had to set the hour and it would find it. People didn’t set the hour properly…

No question, your version is more comprehensive than the region-specific one I presented. (Adapting it for other regions is left as an exercise for those who are comfortable with Jinja2.)

Well, I worked out the errors. This calc is a pita. Anyways, I updated it. Hopefully it works when everyone pushes forward an hour. The previous rendition worked when we were in that timeframe, however after the change it did not. Only time will tell.

I see what you did there :joy:

this is new for me, using dev 2021.12

sensor.daylight_savings_times is providing a string for its state, while the device class is 'timestamp', this is not valid and will be unsupported from Home Assistant 2022.2

seeing on several core integrations, but also on this template sensor:

how can we mitigate that?

      - unique_id: daylight_savings_times
        name: Daylight savings times
        device_class: timestamp
        state: >
          {%- set ns = namespace(previous=3,spring=none,fall=none) %}
          {%- set today = strptime(states('sensor.date'),'%Y-%m-%d').astimezone().replace(hour=ns.previous) %}
          {%- for i in range(365) %}
          {%- set day = (today + timedelta(days=i)).astimezone() %}
          {%- if ns.previous - day.hour == -1 %}
          {%- set ns.spring = today + timedelta(days=i) %}
          {%- elif ns.previous - day.hour == 1 %}
          {%- set ns.fall = today + timedelta(days=i) %}
          {%- endif %}
          {%- set ns.previous = day.hour %}
          {%- endfor %}

          {{([ns.spring,ns.fall]|min).isoformat()}}

or, is this a bug in the template: integration itself, because the state seems a valid timestamp…

It’s looking like there’s a breaking change with timestamp sensors, investigating now. The good news is that it won’t change much about the template, you’d just be removing .isoformat(). I’m gathering the details now and I’ll update you on the changes.

2 Likes

It’s not an intended change, lots of changes coming through with datetimes. Ignore the warning and don’t update the tempalte.

Ok, will sit tight. Seeing it on several others too. Some are already fixed in Core, this wasn’t just yet.

Yes, they are working through the changes. Probably will see these warnings for a while. Template will get resolved quickly, things outside of ‘internal’ will be awhile.

now this doesn’t error anymore on the timestamp, and the new output of timestamp_local, please let me ask if there is a preferred syntax:

 {{([ns.spring,ns.fall]|min).isoformat()}}
 
{{as_timestamp([ns.spring,ns.fall]|min)|timestamp_local}}

both show a correct timestamp now, albeit with a different output:

Today I am getting the error “ValueError: day is out of range for month” for binary_sensor.daylight_saving_time, causing binary_sensor.daylight_saving_time to return as unavailable.

I just noticed I’m getting warnings in the log about this sensor.

2022-01-08 10:01:20 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'timestamp_local' got invalid input '64 days, 0:00:00' when rendering template '{%- set ns = namespace(previous = 2, spring=none, fall=none) %} {%- set today = strptime(states('sensor.date'), '%Y-%m-%d').astimezone().replace(hour=ns.previous) %} {%- for i in range(365) %}
  {%- set day = (today + timedelta(days=i)).astimezone() %}
  {%- if ns.previous - day.hour == -1 %}
    {%- set ns.spring = today + timedelta(days=i) | timestamp_local %}
  {%- elif ns.previous - day.hour == 1 %}
    {%- set ns.fall = today + timedelta(days=i) | timestamp_local %}
  {%- endif %}
  {%- set ns.previous = day.hour %}
{%- endfor %} {{ [ns.spring, ns.fall] | min }}' but no default was specified. Currently 'timestamp_local' will return '64 days, 0:00:00', however this template will fail to render in Home Assistant core 2022.1
2022-01-08 10:01:20 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'timestamp_local' got invalid input '302 days, 0:00:00' when rendering template '{%- set ns = namespace(previous = 2, spring=none, fall=none) %} {%- set today = strptime(states('sensor.date'), '%Y-%m-%d').astimezone().replace(hour=ns.previous) %} {%- for i in range(365) %}
  {%- set day = (today + timedelta(days=i)).astimezone() %}
  {%- if ns.previous - day.hour == -1 %}
    {%- set ns.spring = today + timedelta(days=i) | timestamp_local %}
  {%- elif ns.previous - day.hour == 1 %}
    {%- set ns.fall = today + timedelta(days=i) | timestamp_local %}
  {%- endif %}
  {%- set ns.previous = day.hour %}
{%- endfor %} {{ [ns.spring, ns.fall] | min }}' but no default was specified. Currently 'timestamp_local' will return '302 days, 0:00:00', however this template will fail to render in Home Assistant core 2022.1
2022-01-08 10:01:20 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'timestamp_local' got invalid input '64 days, 0:00:00' when rendering template '{%- set ns = namespace(previous = 2, spring=none, fall=none) %} {%- set today = strptime(states('sensor.date'), '%Y-%m-%d').astimezone().replace(hour=ns.previous) %} {%- for i in range(365) %}
  {%- set day = (today + timedelta(days=i)).astimezone() %}
  {%- if ns.previous - day.hour == -1 %}
    {%- set ns.spring = today + timedelta(days=i) | timestamp_local %}
  {%- elif ns.previous - day.hour == 1 %}
    {%- set ns.fall = today + timedelta(days=i) | timestamp_local %}
  {%- endif %}
  {%- set ns.previous = day.hour %}
{%- endfor %} {{ [ns.spring, ns.fall] | min }}' but no default was specified. Currently 'timestamp_local' will return '64 days, 0:00:00', however this template will fail to render in Home Assistant core 2022.1
2022-01-08 10:01:20 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'timestamp_local' got invalid input '302 days, 0:00:00' when rendering template '{%- set ns = namespace(previous = 2, spring=none, fall=none) %} {%- set today = strptime(states('sensor.date'), '%Y-%m-%d').astimezone().replace(hour=ns.previous) %} {%- for i in range(365) %}
  {%- set day = (today + timedelta(days=i)).astimezone() %}
  {%- if ns.previous - day.hour == -1 %}
    {%- set ns.spring = today + timedelta(days=i) | timestamp_local %}
  {%- elif ns.previous - day.hour == 1 %}
    {%- set ns.fall = today + timedelta(days=i) | timestamp_local %}
  {%- endif %}
  {%- set ns.previous = day.hour %}
{%- endfor %} {{ [ns.spring, ns.fall] | min }}' but no default was specified. Currently 'timestamp_local' will return '302 days, 0:00:00', however this template will fail to render in Home Assistant core 2022.1

If I update the template to specify a default value for timestamp_local:

{%- set ns.spring = today + timedelta(days=i) | timestamp_local(0) %}

then I get a data type mismatch:

TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'int'

I’ve tried to figure out the needed changes but I’m not getting anywhere.

I’ve kind of lost track about what is supposed to be the “latest & greatest” version.

suggestions?

EDIT:

Do I just need to remove the “| timestamp_local” filters from the ns.spring & ns.fall calculations and convert it at the end?

this seems to work:

{%- set ns = namespace(previous = 2, spring=none, fall=none) %}
{%- set today = strptime(states('sensor.date'), '%Y-%m-%d').astimezone().replace(hour=ns.previous) %}
{%- for i in range(365) %}
   {%- set day = (today + timedelta(days=i)).astimezone() %}
   {%- if ns.previous - day.hour == -1 %}
     {%- set ns.spring = today + timedelta(days=i) %}
   {%- elif ns.previous - day.hour == 1 %}
     {%- set ns.fall = today + timedelta(days=i) %}
   {%- endif %}
   {%- set ns.previous = day.hour %}
{%- endfor %}
{{as_timestamp([ns.spring,ns.fall]|min)|timestamp_local}}

I haven’t tried it in the system yet to see if I still get warnings but it works in dev tools.

Moved away from using that template because it was resource heavy and finding the date beyond the next one basically changed whenever the DST changed. That made it a nightmare to manage. This is the current version I’m using.

with this automation:

2 Likes

What makes it resource heavy? Because it isn’t a trigger sensor and just runs every minute? Or is there something inherent in the template that makes it that way? Or both?

If it’s the former couldn’t it be converted to a trigger sensor that only runs at the same specified interval in your new one?

Is the new one more accurate than the other?

I compared the output of your new one to my slightly modified version of the old one (to get rid of the errors) and they are the same except one is a local timestamp and the other is in UTC. Which isn’t a big deal since I don’t care about the time just the date.

there is a lot more code in your newer version to wade thru to figure out so if the old version is accurate for at least the next DST then the original is way easier to understand.

Basically what I’m asking is what is the benefits of updating to the new more complex code? If there are big benefits I’ll gladly switch to the new version.

Not trying to be argumentative. Just trying to learn.

Just the minutely thing. But there was some other issues related to users with the one you’re running. It required you to configure the hour properly. People didn’t do that correctly so I made a new template that searches to the minute when the transition occurs (some guys TZ in another thread had the transition occur at 1:05 am). So, I reworked the template and it was REALLY resource heavy because it was looping 365 * 1440.

I optimized it again and now it loops at most 365 + 24 + 60, which is 84 more loops than the one you’re running.

Basically, the code iterates through every day of the year, when it finds the day it transitions, it stops looping the days. Then it iterates the hours, when it finds the hour transition, it stops looping the hours. Then in iterates the minute, when it finds the minute transition, it stops looping the minutes. Realistically, DST happens every 6 months, usually at 1, 2, or 3 am… so the most loops it will do is 183 + 3 (or 6 if you’re that guy with 5 minutes past 1am).

TBH, I might just make a DST integration at this point with the same code. I’m apprehensive because it might not get approved based on how janky it is. But, it’s really the only way to get the correct transition. I do not understand why this information can’t just be accessed, but no programming language displays the actual transition. They all hide it under the hood as a on/off. Makes no sense.

1 Like

that makes sense.

Since I really don’t care about the exact transition time whether it’s 1, 2, 3 or especially 1:05 :laughing: I’ll probably just transition it to a triggered sensor and hopefully be done with it since it seems to give me all the info I need.

Thanks for the additional info, tho.

and for anyone else coming around later, I actually put the latest code from my post above in production and it works as before with no more warnings.