With DST (Daylight Savings Time) tomorrow I wrote a script to notify me

you beat me to it :wink:

in the new template: format:

template: 

  - binary_sensor:

      - unique_id: zomertijd_binary
        name: Zomertijd
        picture: /local/season/summer.png
        state: >
          {{now().timetuple().tm_isdst == 1}}

      - unique_id: wintertijd_binary
        name: Wintertijd
        picture: /local/season/winter.png
        state: >
          {{now().timetuple().tm_isdst == 0}}

Love you both <3

still having an issue here, because after the actual DST at 3 o’clock last night, it still shows:

the 2 top lines are ok, the 2 bottom lines aren’t. They say:

next change will take place tonight!
clock will then advance 1 hour and we loose an hour…

  {% set x = ['unknown','unavailable'] %}
  {% if states('sensor.daylight_savings_times') not in x %}

  <img src = {{state_attr('binary_sensor.wintertijd','entity_picture')
               if is_state('binary_sensor.wintertijd','on')
               else state_attr('binary_sensor.zomertijd','entity_picture') }} width='80'>

  **Zomertijd - Wintertijd**

  DST is {{'niet actief' if state_attr('sensor.daylight_savings_times','dst_active')
    == false else 'actief'}} en het is {{'Wintertijd' if is_state('binary_sensor.wintertijd','on')
    else 'Zomertijd'}}.

  {% set count = state_attr('sensor.daylight_savings_times','next')
        .days_to_event %}
  {% set datum = as_timestamp(states.sensor.daylight_savings_times.attributes.next.event,0)
        |timestamp_custom('%d %B %Y',default=0) %}
  De volgende wissel vindt

  {%- if count == 0 %} vannacht plaats!
  {%- elif count == 1 %} morgen plaats op {{datum}}.
  {%- elif count == 2 %} overmorgen plaats op {{datum}}.
  {%- elif count > 2 %} plaats over {{count}} dagen op {{datum}}.
  {%- endif %}

  De klok gaat dan 1 uur {{state_attr('sensor.daylight_savings_times','next')
  .clock}} en we {{state_attr('sensor.daylight_savings_times','next').phrase}}.

  {% else %} Calculating
  {% endif %}

ofc, this is built upon the bigger dst template, which I now have to be:

    sensor:

      - 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()}}

#          {{as_timestamp([ns.spring,ns.fall]|min)|timestamp_custom('%A %-d %B %Y at %-H am')}}
        icon: >
          mdi:{{(now().timetuple().tm_isdst == 1)|iif('update','history')}}
        attributes:
          icon_color: >
            {{'gold' if is_state('binary_sensor.zomertijd','on') else 'steelblue'}}
# https://pythontic.com/datetime/datetime/timetuple
          dst_active: >
            {{now().timetuple().tm_isdst == 1}}
          dst_change_tomorrow: >
            {% set dt = now() + timedelta(days=1) %}
            {{now().astimezone().tzinfo != dt.astimezone().tzinfo}}
          dst_changed_today: >
            {% set dt = now() + timedelta(days=-1) %}
            {{now().astimezone().tzinfo != dt.astimezone().tzinfo}}
          next: >
            {%- 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 %}

            {%- set next = [ns.spring, ns.fall]|min %}
            {%- set phrase = 'verliezen een uur' if next == ns.spring else 'krijgen een uur extra' %}
            {%- set clock = 'vooruit' if next == ns.spring else 'terug' %}
            {"spring": "{{ns.spring.isoformat()}}",
             "fall": "{{ns.fall.isoformat()}}",
             "event": "{{next.isoformat()}}",
             "days_to_event":{{(next-today).days}},
             "phrase": "{{phrase}}",
             "clock":"{{clock}}"}

this is triggered per hour, but the attribute next items change per day. Meaning, if the event has happened, and it is still this day, the attributes dont change. Which is a bit of a nasty detail really. Now have to wait a full day for this to become correct again.

current output in dev tools:

- unique_id: daylight_savings_times
        name: Daylight savings times
        device_class: timestamp
        state: >

          2023-03-26T03:00:00+01:00

#          Sunday 26 March 2023 at 4 am
        icon: >
          mdi:update
        attributes:
          icon_color: >
            gold
# https://pythontic.com/datetime/datetime/timetuple
          dst_active: >
            True
          dst_change_tomorrow: >
            
            False
          dst_changed_today: >
            
            True
          next: >
            {"spring": "2023-03-26T03:00:00+01:00",
             "fall": "2023-10-29T03:00:00+01:00",
             "event": "2023-03-26T03:00:00+01:00",
             "days_to_event":0,
             "phrase": "verliezen een uur",
             "clock":"vooruit"}

fwiw, the mentioned binary_sensors inside the other templates are:

    binary_sensor:

      - unique_id: zomertijd_binary
        name: Zomertijd
        picture: /local/season/summer.png
        state: >
          {{now().timetuple().tm_isdst == 1}}

      - unique_id: wintertijd_binary
        name: Wintertijd
        picture: /local/season/winter.png
        state: >
          {{now().timetuple().tm_isdst == 0}}

so always spot on

after some discussion on Discord with @TheFes, decided to change the template to use today_at(), and some extra evaluating the ns.spring and ns.fall operations.

when ns.fall or ns.spring are already entered (so they are not none anymore) it will use the current value

setting the range to a bit higher than 365 allows it to pick up a next change that would falll out of that range, like it does this year … so, currently changed to:

    sensor:

      - unique_id: daylight_savings_times
        name: Daylight savings times
        device_class: timestamp
        state: >
          {%- set ns = namespace(previous=3,spring=none,fall=none) %}
          {%- set today = today_at().astimezone().replace(hour=ns.previous) %}
          {%- for i in range(500) %}
          {%- set day = (today + timedelta(days=i)).astimezone() %}
          {%- if ns.previous - day.hour == -1 %}
          {%- set ns.spring = today + timedelta(days=i) if ns.spring is none else ns.spring %}
          {%- elif ns.previous - day.hour == 1 %}
          {%- set ns.fall = today + timedelta(days=i) if ns.fall is none else ns.fall %}
          {%- endif %}
          {%- set ns.previous = day.hour %}
          {%- endfor %}

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

        icon: >
          mdi:{{(now().timetuple().tm_isdst == 1)|iif('update','history')}}
        attributes:
          icon_color: >
            {{'gold' if is_state('binary_sensor.zomertijd','on') else 'steelblue'}}
# https://pythontic.com/datetime/datetime/timetuple
          dst_active: >
            {{now().timetuple().tm_isdst == 1}}
          dst_change_tomorrow: >
            {% set dt = now() + timedelta(days=1) %}
            {{now().astimezone().tzinfo != dt.astimezone().tzinfo}}
          dst_changed_today: >
            {% set dt = now() + timedelta(days=-1) %}
            {{now().astimezone().tzinfo != dt.astimezone().tzinfo}}
          next: >
            {%- set ns = namespace(previous=3,spring=none,fall=none) %}
            {%- set today = today_at().astimezone().replace(hour=ns.previous) %}
            {%- for i in range(500) %}
            {%- set day = (today + timedelta(days=i)).astimezone() %}
            {%- if ns.previous - day.hour == -1 %}
            {%- set ns.spring = today + timedelta(days=i) if ns.spring is none else ns.spring %}
            {%- elif ns.previous - day.hour == 1 %}
            {%- set ns.fall = today + timedelta(days=i) if ns.fall is none else ns.fall %}
            {%- endif %}
            {%- set ns.previous = day.hour %}
            {%- endfor %}

            {%- set next = [ns.spring, ns.fall] | reject('<', now()) | min %}
            {%- set result = 'verliezen een uur' if next == ns.spring else 'krijgen een uur extra' %}
            {%- set motion = 'vooruit' if next == ns.spring else 'terug' %}
            {"spring": "{{ns.spring.isoformat()}}",
             "fall": "{{ns.fall.isoformat()}}",
             "event": "{{next.isoformat()}}",
             "days_to_event":{{(next-today).days}},
             "result": "{{result}}",
             "motion":"{{motion}}",
             "phrase":"{{as_timestamp([ns.spring,ns.fall]|min)
                         |timestamp_custom('%A %-d %B %Y at %-H am')}}"
             }

This being a rather generic approach, which is good, we might be able to make it more to the point, as the dst changes (in the EU) always take place the last Sunday in March or October.
that could reduce the number of iterations maybe? Not sure how to implement that, but it seems we only need to check 8-10 Sundays max, instead of 365 days…


coming beta 2023.4 will add some new functionality, relevant for this template, being able to ‘break’ a loop. it should allow to do:

{%- set n = now() %}
{%- set m = today_at().astimezone(utcnow().tzinfo) %}
{%- set t = n.hour - utcnow().hour + (0 if n.timetuple().tm_isdst else 1) %}
{%- set ns = namespace(spring='', autumn='') %}
{%- for i in range(1,750) %}
{%- set d1, d2 = n + timedelta(days=i-1), n + timedelta(days=i) %}
{%- set d1dst, d2dst = d1.timetuple().tm_isdst, d2.timetuple().tm_isdst %}
  {%- if ns.spring and ns.autumn %}
    {%- break %}
  {%- else %}
    {%- if d1dst > d2dst %}
      {%- set ns.autumn = as_local(m + timedelta(days=i, hours=t)).isoformat() %}
    {%- elif d1dst < d2dst %}
      {%- set ns.spring = as_local(m + timedelta(days=i, hours=t)).isoformat() %}
    {%- endif %}
  {%- endif %}
{%- endfor %}

TheFes wrote that, so he’s the expert, and credits go to him (again)

1 Like

A very informative inspiring discussion. But for me just receiving a notification on the night before a DTS change, was good enough. I made this simple automation for that:

alias: DTS changes
description: Notify at 20h when DTS changes tonight
trigger:
  - platform: time
    at: "20:00:00"
    variables:
      currDST: "{{ now().timetuple().tm_isdst }}"
      nextDST: "{{ (now()+timedelta(days=1)).timetuple().tm_isdst }}"
condition:
  - condition: template
    value_template: "{{ currDST != nextDST }}"
action:
  - service: notify.notify
    data:
      title: |
        {{ 'Wintertijd gaat in' if currDST > nextDST else 'Zomertijd gaat in' }}
      message: |
        {{ 'De klok wordt vannacht een uur terug gezet' if currDST > nextDST else
           'De klok wordt vannacht een uur vooruit gezet' }}
mode: single

All kudos how to determine DTS changes are for the authors above :blush:

3 Likes

I never realized we could set those variable in the trigger block like that, and always worked around it by moving all to the action block

alias: DTS changes
description: Notify at 20h when DTS changes tonight
trigger:
  - platform: time
    at: "20:00:00"
condition: []
action:
  - variables:
      currDST: "{{ now().timetuple().tm_isdst }}"
      nextDST: "{{ (now()+timedelta(days=1)).timetuple().tm_isdst }}"
      winter: "{{currDST > nextDST}}"
  - condition: >
      {{ currDST != nextDST }}
  - service: notify.notify
    data:
      title: >
        {{ 'Winter'' if winter else 'Zomer'}}tijd gaat in
      message: >
        De klok wordt vannacht een uur {{'terug' if winter else 'vooruit '}} gezet
mode: single

looking for the documentation on that, to figure out if there are specifics to take into account…

;–)

nvm, it is hidden here Automation Trigger - Home Assistant

so the shorter version:

  - id: notificatie_dst_wissel
    trigger:
      platform: time
      at:
        - '10:00'
        - '19:00'
      variables:
        curr: '{{now().timetuple().tm_isdst}}'
        next: '{{(now()+timedelta(days=1)).timetuple().tm_isdst}}'
        winter: '{{curr > next}}'
    condition:
      >
       {{curr != next}}
    action:
      service: notify.notify
      data:
        title: >
          {{'Winter' if winter else 'Zomer'}}tijd gaat in
        message: >
          De klok wordt vannacht een uur {{'terug' if winter else 'vooruit '}} gezet

I also found out recently that variables already can be defined in the triggers. The real power of that is that the variables can be set differently for various triggers :smiling_face:
That can save logic to evaluate trigger ids in the actions for particular automations and trigger based templates

yes, ive been looking for tha for some time, but just hadnt realized these were available already.

Doesnt stand out somehow in the docs…
now back to my yaml and find those automations…

Using this template didn’t seem to work for me:

    {%- 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 }}
ValueError: Template error: timestamp_local got invalid input '144 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

I do have the sensor.date as a date object.

change to

{%- set today = today_at('02:00') | as_datetime | as_local %}

FYI easy_time can get you this info

1 Like

Thanks for your reply :pray:

Can easy time tell me whether now is DST or not?

EDIT - This is what I came up with which works in my region (Israel):

template:
  - binary_sensor:
      - name: "DST"
        unique_id: "is_dst_active"
        state: >
          {% from 'easy_time.jinja' import next_dst %}
          {%- set datetime_obj = next_dst() | as_datetime %}
          {%- set month = datetime_obj.month %}
          {{ month != 3 }}

Thanks for your contribution to the community, highly appreciated.