Daylight Savings Time (DST) - Template and Automation

Quick query - having installed it on my friends server, I noticed an oddity.

2022-03-27T02:00:00+00:00 is the time being returned for the next change, which is the correct date, but the wrong time, that is the time we will jump to, but then surely that would be +01:00 because +00:00 does not actually exist, it’s an impossible time?

A lot of people get confused about how the time change works - so just to clarify - in the UK, the time change ALWAYS happens at 1am GMT.

1am GMT >> 2am BST ( March 27th )
1am GMT << 2am BST ( October 30th )

Yeah it’s going to look odd under the hood because it’s at the transition. There’s actually 2 valid timestamps. I chose whatever I got back. It should still be the correct time.

small addition:

      - platform: event
        event_type: event_template_reloaded

is just that bit cleaner :wink:
btw Petro, cant we use your new datetime_today() or today_at() function here yet?

for full startup error debugging:

Logger: homeassistant.helpers.event
Source: helpers/template.py:400 
First occurred: November 2, 2021, 17:58:11 (1 occurrences) 
Last logged: November 2, 2021, 17:58:11

Error while processing template: Template("{%- macro hms(t) %} {%- set h, m, s = (t.dst() | string).split(':') | map('int') %} {{- h * 60 * 60 + m * 60 + s }} {%- endmacro %} {%- macro phrase(seconds, name, divisor, mod=None) %} {%- set value = ((seconds | abs // divisor) % (mod if mod else divisor)) | int %} {%- set end = 's' if value > 1 else '' %} {{- '{} {}{}'.format(value, name, end) if value > 0 else '' }} {%- endmacro %} {%- macro total(seconds) %} {%- set values = [ phrase(seconds, 'hour', 60*60, 60*60*24), phrase(seconds, 'minute', 60, 60), ] | select('!=','') | list %} {{- values[:-1] | join(', ') ~ ' and ' ~ values[-1] if values | length > 1 else values | first }} {%- endmacro %} {%- set next = states('sensor.daylight_savings_next') | as_datetime | as_local %} {%- if next is not none %} {%- set ns = hms(next) | int %} {%- set ps = hms(next - timedelta(days=1)) | int %} {% if ns %} lose {{ total(ns) }} {% else %} gain {{ total(ps) }} {% endif %} {%- endif %}")
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 398, in async_render
    render_result = _render_with_context(self.template, compiled, **kwargs)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 1695, in _render_with_context
    return template.render(**kwargs)
  File "/usr/local/lib/python3.9/site-packages/jinja2/environment.py", line 1304, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.9/site-packages/jinja2/environment.py", line 925, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 12, in top-level template code
  File "/usr/src/homeassistant/homeassistant/util/dt.py", line 96, in as_local
    if dattim.tzinfo == DEFAULT_TIME_ZONE:
AttributeError: 'NoneType' object has no attribute 'tzinfo'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 514, in async_render_to_info
    render_info._result = self.async_render(variables, strict=strict, **kwargs)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 400, in async_render
    raise TemplateError(err) from err
homeassistant.exceptions.TemplateError: AttributeError: 'NoneType' object has no attribute 'tzinfo'

doesnt anyone else see this happening? I have no real idea how( on which |int) to mitigate that, so would appreciate any thoughts…

especially because of:

2021-11-09 15:41:00 WARNING (MainThread) [homeassistant.helpers.template] Template warning: 'int' got invalid input '
          None' when rendering template '{%- macro hms(t) %}
          {%- set h, m, s = (t.dst() | string).split(':') | map('int') %}
          {{- h * 60 * 60 + m * 60 + s }}
          {%- endmacro %}

          {%- macro is_dst(t) %}
          {{- hms(t) | int != 0 }}
          {%- endmacro %}

          {%- macro finddst(t, kwarg, rng) %}
          {%- set ns = namespace(previous=is_dst(t), found=none) %}
          {%- for i in range(rng) %}
            {%- set ts = t + timedelta(**{kwarg:i}) %}
            {%- if ns.previous != is_dst(ts) and ns.found is none %}
              {%- set ns.found = i %}
            {%- endif %}
          {%- endfor %}
          {{ ns.found }}
          {%- endmacro %}

          {%- set t = now().replace(hour=0, minute=0, second=0, microsecond=0) %}
          {%- set d = finddst(t, 'days', 365) | int - 1 %}
          {%- set h = finddst(t + timedelta(days=d), 'hours', 24) | int %}
          {%- set m = finddst(t + timedelta(days=d, hours=h), 'minutes', 60) | int %}
          {{ (t.astimezone() + timedelta(days=d, hours=h, minutes=m)).isoformat() }}' but no default was specified. Currently 'int' will return '0', however this template will fail to render in Home Assistant core 2022.1

Same here! Lately also noticed error or warning at 00:00. Will post this when it happens again.

Logger: homeassistant.helpers.template
Source: helpers/template.py:1254
First occurred: 9 November 2021, 14:49:52 (2 occurrences)
Last logged: 00:00:01
Template warning: 'int' got invalid input ' None' when rendering template
'{%- macro hms(t) %} 
 {%- set h, m, s = (t.dst() | string).split(':') | map('int') %}
 {{- h * 60 * 60 + m * 60 + s }}
 {%- endmacro %}

 {%- macro is_dst(t) %}
 {{- hms(t) | int != 0 }}
 {%- endmacro %}

 {%- macro finddst(t, kwarg, rng) %}
 {%- set ns = namespace(previous=is_dst(t), found=none) %}
 {%- for i in range(rng) %}
 {%- set ts = t + timedelta(**{kwarg:i}) %}
 {%- if ns.previous != is_dst(ts) and ns.found is none %}
 {%- set ns.found = i %}
 {%- endif %}
 {%- endfor %}
 {{ ns.found }}
 {%- endmacro %}

 {%- set t = now().replace(hour=0, minute=0, second=0, microsecond=0) %}
 {%- set d = finddst(t, 'days', 365) | int - 1 %}
 {%- set h = finddst(t + timedelta(days=d), 'hours', 24) | int %}
 {%- set m = finddst(t + timedelta(days=d, hours=h), 'minutes', 60) | int %}
 {{ (t.astimezone() + timedelta(days=d, hours=h, minutes=m)).isoformat() }}'
 
but no default was specified. Currently 'int' will return '0', however this template will fail to render in Home Assistant core 2022.1

updated the template, see if you still get the errors. However it is troubling that your now() object does not have a dst applied.

Here’s something I cooked up specifically for use in Canada/USA. I imagine it can probably be adapted for use in other parts of the world as well.

It leverages the fact that DST begins on the second Sunday in March and ends the first Sunday of November (i.e. start of Standard Time).

  • The second Sunday in March can fall on a date between 8 and 14.
  • The first Sunday in November can fall on a date between 1 and 7.

The two date-range constraints help to simplify and accelerate the calculation.

The following Jinja2 macro, given the desired time-change ('dst' or 'std') and year, will report the time-change date. For example, the result of:

{{ get_date('dst', 2022) }}

is:

2022-03-13 03:00:00-04:00
Show macro code
{%- macro get_date(mode, yr) %}
{%- set ns = namespace(d=[]) %}
{%- set t = now().replace(year=yr, month = 3 if mode == 'dst' else 11, hour=3, minute=0, second=0, microsecond=0) %}
{%- set rmin, rmax = (8,15) if mode == 'dst' else (1,8) %}
{%- for i in range(rmin, rmax) 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 -%}

The macro is used to create a Template Binary Sensor reporting:

  • state: DST is on or off
  • next_dst: date of next change to DST
  • next_std: date of next change to Standard Time

as well as a Template Sensor reporting:

  • state: days to next time-change date
  • next_change: std/dst

Show Template code
template:
- trigger:
  - platform: time
    at: "00:03:00"
  - platform: homeassistant
    event: start
  - platform: event
    event_type: event_template_reloaded
  binary_sensor:
  - unique_id: dst
    name: "Daylight Saving Time"
    state: "{{ now().timetuple().tm_isdst == 1 }}"
    attributes:
      next_dst: >
        {%- macro get_date(mode, yr) %}
        {%- set ns = namespace(d=[]) %}
        {%- set t = now().replace(year=yr, month = 3 if mode == 'dst' else 11, hour=3, minute=0, second=0, microsecond=0) %}
        {%- set rmin, rmax = (8,15) if mode == 'dst' else (1,8) %}
        {%- for i in range(rmin, rmax) 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 -%}

        {% set d = get_date('dst', now().year) | as_datetime %}
        {{ d.isoformat() if d > now() else as_datetime(get_date('dst', now().year+1)).isoformat() }}
      next_std: >
        {%- macro get_date(mode, yr) %}
        {%- set ns = namespace(d=[]) %}
        {%- set t = now().replace(year=yr, month = 3 if mode == 'dst' else 11, hour=3, minute=0, second=0, microsecond=0) %}
        {%- set rmin, rmax = (8,15) if mode == 'dst' else (1,8) %}
        {%- for i in range(rmin, rmax) 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 -%}

        {% set d = get_date('std', now().year) | as_datetime %}
        {{ d.isoformat() if d > now() else as_datetime(get_date('std', now().year+1)).isoformat() }}
  sensor:
  - unique_id: days_to_time_change
    name: Days To Time Change
    state: >
      {% set t = state_attr('binary_sensor.daylight_saving_time', 'next_dst' if is_state('binary_sensor.daylight_saving_time', 'off') else 'next_std') | as_datetime %}
      {{ (t - now()).days }}
    attributes:
      next_change: "{{ 'dst' if is_state('binary_sensor.daylight_saving_time', 'off') else 'std' }}"

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.