Daylight Savings Time (DST) - Template and Automation

Here is a template sensor that contains the next daylight savings time. You can use it to get the next time change. Please let me know if it doesn’t work for your time zone, also please verify it against your current time changes. It should get the time change down to the minute.

No Internet Required

Template Sensor

Paste this code into configuration.yaml. If you already have a template section, remove the template: line when pasting.
template:
- trigger:
  - platform: time
    at: "00:00:00"
  - platform: homeassistant
    event: start
  - platform: event
    event_type: call_service
    event_data:
      domain: template
      service: reload
  sensor:
  - unique_id: dst_next
    name: "Daylight Savings: Next"
    device_class: timestamp
    state: >
      {%- macro hms(t) %}
      {%- set dststr = t.dst() | string if t.dst() is not none else "00:00:00" %}
      {%- set h, m, s = dststr.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', 366) | int - 1 %}
      {%- set h = finddst(t + timedelta(days=d), 'hours', 25) | int - 1 %}
      {%- set m = finddst(t + timedelta(days=d, hours=h), 'minutes', 61) | int %}
      {{ (t + timedelta(days=d, hours=h, minutes=m)) }}
  - unique_id: dst_phrase
    name: "Daylight Savings: Phrase"
    state: >
      {%- macro hms(t) %}
      {%- set dststr = t.dst() | string if t.dst() is not none else "00:00:00" %}
      {%- set h, m, s = dststr.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 | default}}
      {%- endmacro %}
      {%- macro finddelta(t, kwarg, rng) %}
      {%- set ns = namespace(previous=hms(t), found=none) %}
      {%- for i in range(rng) %}
        {%- set ts = t + timedelta(**{kwarg:i}) %}
        {%- if ns.previous != hms(ts) and ns.found is none %}
          {%- set before = hms(ts - timedelta(days=1)) | int %}
          {%- set after = hms(ts) | int %}
          {%- set ns.found =  after - before %}
        {%- endif %}
      {%- endfor %}
      {{ ns.found }}
      {%- endmacro %}
      
      {%- set t = now().replace(hour=0, minute=0, second=0, microsecond=0) %}
      {%- set delta = finddelta(t, 'days', 365) | int %}
      {% if delta > 0 %}
        lose {{ total(delta | abs) }}
      {% else %}
        gain {{ total(delta | abs) }}
      {% endif %}
- sensor:
  - unique_id: dst_days
    name: "Daylight Savings: Days Until"
    unit_of_measurement: days
    state: >
      {% set next = states('sensor.daylight_savings_next') | as_datetime %}
      {% set today = now().replace(hour=0, minute=0, second=0, microsecond=0) %}
      {{ (next - today).days if next is not none else 0 }}
    availability: >
      {{ states('sensor.daylight_savings_next') not in ['unknown', 'unavailable'] }}

You can also watch my live changes to this sensor here.


Requires Internet

Special thanks to @nickrout for pointing out the resource.

Rest Sensors

rest:
- resource_template: "http://worldtimeapi.org/api/timezone/{{ now().tzinfo }}"
  sensor:
  - name: "Daylight Savings: Next"
    value_template: >
      {{ ((value_json.dst_from, value_json.dst_until) | map('as_datetime') | map('as_local') | select('>', now()) | first).isoformat() }}
    device_class: timestamp

  - name: "Daylight Savings: Days Until"
    value_template: >
      {%- set next =  (value_json.dst_from, value_json.dst_until) | map('as_datetime') | map('as_local') | select('>', now()) | first  %}
      {{ (next-today_at()).days }}
    unit_of_measurement: days

  - name: "Daylight Savings: Phrase"
    value_template: >
      {%- 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 %}
      {% set next = (value_json.dst_from, value_json.dst_until) | select('>', utcnow().isoformat()) | first %}
      {% set name, value = value_json.items() | list | selectattr('1', '==', next) | first %}
      {% set seconds = value_json.dst_offset %}
      {%- set values = [ 
        phrase(seconds, 'hour', 60*60, 60*60*24),
        phrase(seconds, 'minute', 60, 60),
        ] | select('!=','') | list %}
      {{ 'gain' if name == 'dst_until' else 'lose' }} {{ values[:-1] | join(', ') ~ ' and ' ~ values[-1] if values | length > 1 else values | first }}

Paired Automation

This will notify you 7 days before and 1 day before dst.

- alias: Timed Event - DST Warning
  id: dst_warning
  trigger:
    - platform: time
      at: '10:00:00'
    - platform: time
      at: '19:00:00'
  condition:
    - condition: template
      value_template: >
        {{ states('sensor.daylight_savings_days_until') | int(0) in [7,1] }}
  action:
    - service: persistent_notification.create
      data:
        message: >
          {%- set days = states('sensor.daylight_savings_days_until') | int(0) %}
          {%- set plural = 's' if days | int(0) > 1 else '' %}
          Daylight savings in {{ days }} day{{plural}}, you will {{ states('sensor.daylight_savings_phrase')  }}!
1 Like

State works in Template editor here, and returns the date today (which is correct).
Then tried the code for the attributes events part in template editor, but get the error:

UndefinedError: 'today_at' is undefined

Sorry! Gotta be on 2021.11 Changed it to work in 2021.10

1 Like

Bookmarked for reminder on 8th November :wink:

I updated it, try it now

Confirmed working:

{
  "next": {
    "timestamp": "2021-10-31T01:00:00+00:00",
    "phrase": "gain 1 hour",
    "days": 0
  },
  "after": {
    "timestamp": "2022-03-27T02:00:00+01:00",
    "phrase": "lose 1 hour",
    "days": 147
  }
}

So are the datetimes correct?

It tries to determine the exact minute that the change occurs at

Yes, we changed back to GMT this morning at 2am (BST) and we change back to BST on 27th March 2022 at 1am (GMT)

(It takes about 3 seconds to render)

yeah it’s a pita to calculate.

Everything about the archaic ritual is a pain… The sooner we all agree to ditch it altogether the better. Hopefully a significant chunk of the World will be done with it next year if the EU ever get the proposal through.

And now that python uses pytz, we can actually use the .dst() method, so this can be optimized I’m sure.

1 Like

This api (which comes with warnings admittedly) tells you when your dst started and ends

curl "http://worldtimeapi.org/api/timezone/Pacific/Auckland"
1 Like

Nice, that makes these rest sensors pretty easy. Downside is that it requires internet.

Attached a solution using the resource

This appears to return the incorrect result in the Southern Hemisphere, we should lose an hour in April and gain the hour in October

template:
- sensor:
  - unique_id: dst
    name: Daylight Savings Times
    device_class: timestamp
    state: >
      2022-04-03T02:00:00+10:00
    attributes:
      template: dst
      events: >
        {"next": {'timestamp': '2022-04-03T02:00:00+10:00', 'phrase': 'gain 1 hour', 'days': 153}, "after": {'timestamp': '2022-10-02T03:00:00+11:00', 'phrase': 'lose 1 hour', 'days': 335} }

I’ll have to revamp it, you can use the internet one for now. I believe that should work.

If you want to use the non-internet one, for the time being just swap the word gain and lose with each other in the template

1 Like

Cheers - a note that it needs a small adjustment might suffice over having to rebuild the logic in the template

Yah, true. But I like this crap, so why not. What TZ are you in so I can play around?

1 Like

fair enough! - Australia/Sydney