Wester/Catholic Easter date (sunday) sensor

Hi there,

I just stumbled accross this piece of code in my configuration and thought it might be useful. It is a sensor that reports this year’s Easter sunday as a timestamp. It uses this algorithm from “Fundamental Algorithms: The Art of Computer Programming, Vol. I” by Donald Knuth.

# easter sensor
sensor:
- platform: template
  sensors:
    easter:
      friendly_name: "Ostersonntag"
      device_class: timestamp
      value_template: >-
        {% set Y = now().year %}
        {% set G = Y % 19 + 1 %}
        {% set C = (Y / 100) | int + 1 %}
        {% set X = (3*C / 4) |int - 12 %}
        {% set Z = ((8*C + 5) / 25) |int - 5 %}
        {% set D = (5*Y / 4)|int - X - 10 %}
        {% set E = (11*G + 20 + Z - X ) % 30 %}
        {% set E = E+1 if (E==25 and G>11 or E==24) else E %}
        {% set N = 44 - E %}
        {% set N = N+30 if N<21 else N %}
        {% set N = N + 7 - (( D + N ) % 7) %}
        {% set M = 4    if N>31 else 3 %}
        {% set N = N-31 if N>31 else N %}
        {{ as_local(as_datetime('%04d-%02d-%02dT00:00:00'%(Y,M,N))) }}

I use it to calculate carnival dates from it as follows, but it can be used to calculate any holiday that depends on Easter dates.

# Note: Actually, the range() function will report [45 ... 51].
{{ (as_local(as_datetime(states('sensor.easter'))) - now()).days in range(45, 52) }}

Hope this helps someone…

Andreas

Update: Fix warning for timestamp not having timezone set.

3 Likes

Thank you… I was just about to implement the same, when I thought: let’s check the community and see if someone has done it already :smiley:

That’s always worth a try. :wink: Glad it did help!

If it ever helps, mother of all scripts to work out a whole pile of ‘special days’ in the year. It sets two (must be pre-setup) helpers after it’s run: input_text.special_day_text (name of the special day) and input_boolean.special_day (on if it is a special day, off if it isn’t)…
It’s not built for speed, it’s built to be easy to read and hack about with - I run it at 7am in the morning on a timed automation (just because that keeps the special day going until 7am the next day… which is generally what you want - but if you were being precise about it you’d run it at midnight every day…)

alias: Is It A Special Day
sequence:
  - variables:
      special_text: |-
        {# Output is text string that fits with phrase 'today is...'
         # or the string 'unknown'
         #}

        {# Nth weekday in a month #}
        {%- macro x_weekday_in_month(o,d,m,y) -%}
        {%- set f = as_local(as_datetime('%04d-%02d-01T00:00:00'%(y,m))) -%}
        {{- f + timedelta(days = (7 - f.isoweekday() + d if d < f.isoweekday()
          else d - f.isoweekday()) + 7*(o-1)) -}}
        {%- endmacro -%}

        {# Last Nth weekday in a month #}
        {%- macro x_last_in_month(o,d,m,y) -%}
        {%- set l = as_local(as_datetime('%04d-%02d-01T00:00:00'%(y,(m+1)))) -
          timedelta(days=1) -%}
        {{- l - timedelta(days = (l.isoweekday() - d if d <= l.isoweekday()
          else 7 - d + l.isoweekday()) + 7*(o-1)) -}}
        {%- endmacro -%}

        {# Nth weekday after a date #}
        {%- macro x_weekday_after(o,d,t,m,y) -%}
        {%- set f = as_local(as_datetime('%04d-%02d-%02dT00:00:00'%(y,m,t))) -%}
        {{- f + timedelta(days = (7 - f.isoweekday() + d if d < f.isoweekday()
          else d - f.isoweekday()) + 7*(o-1)) -}}
        {%- endmacro -%}

        {# Nth weekday before a date #}
        {%- macro x_weekday_before(o,d,t,m,y) -%}
        {%- set l = as_local(as_datetime('%04d-%02d-%02dT00:00:00'%(y,m,t))) -%}
        {{- l - timedelta(days = (l.isoweekday() - d if d <= l.isoweekday()
          else 7 - d + l.isoweekday()) + 7*(o-1)) -}}
        {%- endmacro -%}

        {# Mangle now() into midnight with timezone #}
        {%- set cur_date = now() -%}
        {%- set cur_year = cur_date.year -%}
        {%- set cur_month = cur_date.month -%}
        {%- set cur_day = cur_date.day -%}
        {%- set cur_date =
          as_local(as_datetime('%04d-%02d-%02dT00:00:00'%(cur_year,cur_month,cur_day)))
        -%}

        {# Set default output text #}
        {%- set output_text = "unknown" -%}

        {%- if cur_month == 1 -%}

          {%- set new_years_day = as_local(as_datetime('%04d-01-01T00:00:00'%(cur_year))) -%}
          {%- set ninth_night = as_local(new_years_day + timedelta(days = 1)) -%}
          {%- set tenth_night = as_local(new_years_day + timedelta(days = 2)) -%}
          {%- set eleventh_night = as_local(new_years_day + timedelta(days = 3)) -%}
          {%- set twelfth_night = as_local(new_years_day + timedelta(days = 4)) -%}
          {%- set epiphany = as_local(new_years_day + timedelta(days = 5)) -%}
          {%- set burns_night = as_local(as_datetime('%04d-01-25T00:00:00'%(cur_year))) -%}

          {%- if cur_date == new_years_day -%}
            {%- set output_text = "New Year's Day" -%}
          {%- elif cur_date == ninth_night -%}
            {%- set output_text = "Ninth Night" -%}
          {%- elif cur_date == tenth_night -%}
            {%- set output_text = "Tenth Night" -%}
          {%- elif cur_date == eleventh_night -%}
            {%- set output_text = "Eleventh Night" -%}
          {%- elif cur_date == twelfth_night -%}
            {%- set output_text = "Twelfth Night" -%}
          {%- elif cur_date == epiphany -%}
            {%- set output_text = "Epiphany" -%}
          {%- elif cur_date == burns_night -%}
            {%- set output_text = "Burns Night" -%}
          {%- endif -%}

        {# Start looking for interesting dates... Easter is somewhere in here #}
        {%- elif cur_month > 1 and cur_month < 6 -%}

          {#- Easter - based on a calculated date -#}
          {%- set Y = cur_year -%}
          {%- set G = Y % 19 + 1 -%}
          {%- set C = (Y / 100) | int + 1 -%}
          {%- set X = (3*C / 4) | int - 12 -%}
          {%- set Z = ((8*C + 5) / 25) | int - 5 -%}
          {%- set D = (5*Y / 4)| int - X - 10 -%}
          {%- set E = (11*G + 20 + Z - X ) % 30 -%}
          {%- set E = E+1 if (E == 25 and G > 11 or E == 24) else E -%}
          {%- set N = 44 - E -%}
          {%- set N = N+30 if N < 21 else N -%}
          {%- set N = N + 7 - (( D + N ) % 7) -%}
          {%- set M = 4 if N > 31 else 3 -%}
          {%- set N = N-31 if N > 31 else N -%}
          {%- set easter = as_local(as_datetime('%04d-%02d-%02dT00:00:00'%(Y,M,N))) -%}

          {# Calculate all the interesting days in Easter - easier to see if done in one go #}
          {%- set shrove_monday = as_local(easter - timedelta(days=48)) -%}
          {%- set shrove_tuesday = as_local(easter - timedelta(days=47)) -%}
          {%- set ash_wednesday = as_local(easter - timedelta(days=46)) -%}
          {%- set mothering_sunday = as_local(easter - timedelta(days=21)) -%}
          {%- set passion_sunday = as_local(easter - timedelta(days=14)) -%}
          {%- set palm_sunday = as_local(easter - timedelta(days=7)) -%}
          {%- set good_friday = as_local(easter - timedelta(days=2)) -%}
          {%- set ascension = as_local(easter + timedelta(days=39)) -%}
          {%- set ascension_sunday = as_local(easter + timedelta(days=42)) -%}
          {%- set easter_monday = as_local(easter + timedelta(days=1)) -%}

          {# Check all the dates relevant to Easter #}
          {%- if cur_date >= shrove_monday and cur_date <= ascension_sunday -%}

            {# Set default text #}
            {%- if cur_date > ash_wednesday and cur_date < easter -%}
              {%- set output_text = "Lent" -%}
            {%- endif -%}
            
            {%- if cur_date == shrove_monday -%}
              {%- set output_text = "Shrove Monday" -%}
            {%- elif cur_date == shrove_tuesday -%}
              {%- set output_text = "Shrove Tuesday" -%} 
            {%- elif cur_date == ash_wednesday -%}
              {%- set output_text = "Ash Wednesday" -%}
            {%- elif cur_date == mothering_sunday -%}
              {%- set output_text = "Mothering Sunday" -%}
            {%- elif cur_date == passion_sunday -%}
              {%- set output_text = "Passion Sunday" -%}
            {%- elif cur_date == palm_sunday -%}
              {%- set output_text = "Palm Sunday" -%}
            {%- elif cur_date == good_friday -%}
              {%- set output_text = "Good Friday" -%}
            {%- elif cur_date == easter -%}
              {%- set output_text = "Easter Day" -%}
            {%- elif cur_date == ascension -%}
              {%- set output_text = "Ascension Day" -%}
            {%- elif cur_date == ascension_sunday -%}
              {%- set output_text = "Ascension Sunday" -%}
            {%- elif cur_date | string == easter_monday -%}
              {%- set output_text = "Easter Monday" -%}
            {%- endif -%}
          {%- endif -%}

          {# Other months in the first half of the year #}
          {%- if cur_month == 2 -%}

            {%- set candlemas = as_local(as_datetime('%04d-02-02T00:00:00'%(cur_year))) -%}
            {%- set valentines_day = as_local(as_datetime('%04d-02-14T00:00:00'%(cur_year))) -%}

            {%- if cur_date == candlemas -%}
              {%- set output_text = "Candlemas" -%}
            {%- elif cur_date == valentines_day -%}
              {%- set output_text = "Valentines Day" -%}
            {%- endif -%}

          {%- elif cur_month == 3 -%}

            {%- set clock_forward = x_last_in_month(1,7,3,cur_year) -%}
            {%- set stpatricks_day = as_local(as_datetime('%04d-03-17T00:00:00'%(cur_year))) -%}

            {%- if cur_date == clock_forward -%}
              {%- set output_text = "the day the clocks move forwards one hour" -%}
            {%- elif cur_date == stpatricks_day -%}
              {%- set output_text = "Saint Patrick's Day" -%}
            {%- endif -%}

          {%- elif cur_month == 5 -%}

            {%- set may_early_bank = x_weekday_in_month(1,1,5,cur_year) -%}
            {%- set spring_bank = x_last_in_month(1,1,5,cur_year) -%}

            {%- if cur_date == may_early_bank -%}
              {%- set output_text = "the Early May Bank Holiday" -%}
            {%- elif cur_date == spring_bank -%}
              {%- set output_text = "the Spring Bank Holiday" -%}
            {%- endif -%}

          {%- endif -%}

        {# Specific months in the second half of the year #}

        {%- elif cur_month == 6 -%}

          {%- set fathers_day = x_weekday_in_month(3,7,6,cur_year) -%}
          
          {%- if cur_date == fathers_day -%}
            {%- set output_text = "Fathers Day" -%}
          {%- endif -%}

        {%- elif cur_month == 7 -%}

          {%- set st_swithins_day = as_local(as_datetime('%04d-07-15T00:00:00'%(cur_year))) -%}

          {%- if cur_date == st_swithins_day -%}
            {%- set output_text = "Saint Swithin's Day" -%}
          {%- endif -%}

        {%- elif cur_month == 8 -%}

          {%- set summer_bank = x_last_in_month(1,1,8,cur_year) -%}

          {%- if cur_date == summer_bank -%}
            {%- set output_text = "the Summer Bank Holiday" -%}
          {%- endif -%}

        {%- elif cur_month == 9 -%}

          {%- set michaelmas = as_local(as_datetime('%04d-09-29T00:00:00'%(cur_year))) -%}

          {%- if cur_date == michaelmas -%}
            {%- set output_text = "Michaelmas" -%}
          {%- endif -%}

        {%- elif cur_month == 10 -%}

          {%- set clock_backward = x_last_in_month(1,7,10,cur_year) -%}
          {%- set halloween = as_local(as_datetime('%04d-10-31T00:00:00'%(cur_year))) -%}

          {%- if cur_date == clock_backward -%}
            {%- set output_text = "the day the clocks move backwards one hour" -%}
          {%- elif cur_date == halloween -%}
            {%- set output_text = "Holloween" -%} 
          {%- endif -%}

        {%- elif cur_month >= 11 -%}

          {%- set all_saints_day = as_local(as_datetime('%04d-11-01T00:00:00'%(cur_year))) -%}
          {%- set bonfire_night = as_local(as_datetime('%04d-11-05T00:00:00'%(cur_year))) -%}

          {%- if cur_date == all_saints_day -%}
            {%- set output_text = "All Saints' Day" -%}
          {%- elif cur_date == bonfire_night -%}
            {%- set output_text = "Guy Fawkes Night" -%} 
          {%- endif -%}

          {# Advent can start 27 November #}

          {#- Christmas based off a fixed date, but advent moves about -#}
          {%- set christmas_day = as_local(as_datetime('%04d-12-25T00:00:00'%(cur_year))) -%}
          {%- set first_advent = as_local(christmas_day - timedelta(days = 21 + (christmas_day.isoweekday()))) -%}
          {%- set second_advent = as_local(first_advent + timedelta(days = 7)) -%}
          {%- set third_advent = as_local(first_advent + timedelta(days = 14)) -%}
          {%- set fourth_advent = as_local(first_advent + timedelta(days = 21)) -%}
          {%- set christmas_eve = as_local(christmas_day - timedelta(days = 1)) -%}
          {%- set boxing_day = as_local(christmas_day + timedelta(days = 1)) -%}
          {%- set third_night = as_local(christmas_day + timedelta(days = 3)) -%}
          {%- set fourth_night = as_local(christmas_day + timedelta(days = 4)) -%}
          {%- set fifth_night = as_local(christmas_day + timedelta(days = 5)) -%}
          {%- set sixth_night = as_local(christmas_day + timedelta(days = 6)) -%}
          {%- set new_year_eve = as_local(as_datetime('%04d-12-31T00:00:00'%(cur_year))) -%}

          {%- if cur_date >= first_advent and cur_date < christmas_eve -%}
            {%- set output_text = "Advent" -%}
          {%- endif -%}
          
          {%- if cur_date == christmas_day -%}
            {%- set output_text = "Christmas Day" -%}
          {%- elif cur_date == first_advent -%}
            {%- set output_text = "the First Advent Sunday" -%} 
          {%- elif cur_date == second_advent -%}
            {%- set output_text = "the Second Advent Sunday" -%}
          {%- elif cur_date == third_advent -%}
            {%- set output_text = "the Third Advent Sunday" -%}
          {%- elif cur_date == fourth_advent -%}
            {%- set output_text = "the Fourth Advent Sunday" -%}
          {%- elif cur_date == christmas_eve -%}
            {%- set output_text = "Christmas Eve" -%}
          {%- elif cur_date == boxing_day -%}
            {%- set output_text = "Boxing Day" -%}
          {%- elif cur_date == third_night -%}
            {%- set output_text = "Third Night" -%}
          {%- elif cur_date == fourth_night -%}
            {%- set output_text = "Fourth Night" -%}
          {%- elif cur_date == fifth_night -%}
            {%- set output_text = "Fifth Night" -%}
          {%- elif cur_date == sixth_night -%}
            {%- set output_text = "Sixth Night" -%}
          {%- elif cur_date == new_year_eve -%}
            {%- set output_text = "New Year's Eve" -%}
          {%- endif -%}

        {%- else -%}

          {# Not a special day... #}
          {%- set output_text = "unknown" -%}

        {%- endif -%}

        {# Do some funky Lunar stuff #}
        {%- set lunar_text = "" -%}
        {%- if (states('sensor.moon') == "new_moon") -%}
          {%- if (cur_month == 1 and cur_day > 20) or
              (cur_month == 2 and cur_day < 22) -%}
            {%- set lunar_text = "Lunar and Chinese New Year's Eve" -%}
          {%- else -%}
            {%- set lunar_text = "a New Moon" -%}
          {%- endif -%}
        {%- elif (states('sensor.moon') == "full_moon") -%}
            {%- set lunar_text = "a Full Moon" -%}
        {%- endif -%}
        {%- if lunar_text != "" -%}
          {%- if output_text == "unknown" -%}
            {%- set output_text = lunar_text -%}
          {%- else -%}
            {%- set output_text = output_text + " and " + lunar_text -%}
          {%- endif -%}
        {%- endif -%}

        {{ output_text }}
  - if:
      - condition: template
        value_template: "{{ special_text != \"unknown\" }}"
    then:
      - service: input_text.set_value
        data:
          value: "{{ special_text }}"
        target:
          entity_id: input_text.special_day_text
      - service: input_boolean.turn_on
        data: {}
        target:
          entity_id: input_boolean.special_day
    else:
      - service: input_text.set_value
        data:
          value: unknown
        target:
          entity_id: input_text.special_day_text
      - service: input_boolean.turn_off
        data: {}
        target:
          entity_id: input_boolean.special_day
mode: queued
max: 10

And a corresponding script, this you can run after the previous one, it looks at the input_text.special_day_text, picks a ‘colour to represent the day’ and sets another two helpers: input_number.special_day_colour with the H colour value, and input_text.special_day_colour_name with the human readable colour name - both of which can be used in a bunch of automations or dashboards elsewhere.

To convert a human readable colour name (like ‘Red’) into a hue colour value it uses a template light.fake_light - which you define like this in one of your config yaml files and with the associated helpers:

helpers to define fake light:
input_number.fake_light_brightness
input_number.fake_light_hue
input_number.fake_light_saturation

and the template fake light…

- platform: template
  lights:
    fake_light:
      unique_id: fake_light
      friendly_name: "Fake Light"
      level_template: "{{ states('input_number.fake_light_brightness')|int }}"
      value_template: "{{ states('input_number.fake_light_brightness')|int > 0 }}"
      color_template: "({{states('input_number.fake_light_hue')|int}}, {{states('input_number.fake_light_saturation')|int}})"
      turn_on:
      turn_off:
      set_level:
        service: input_number.set_value
        data:
          value: "{{ brightness }}"
          entity_id: input_number.fake_light_brightness
      set_color:
        - service: input_number.set_value
          data:
            value: "{{ h }}"
            entity_id: input_number.fake_light_hue
        - service: input_number.set_value
          data:
            value: "{{ s }}"
            entity_id: input_number.fake_light_saturation

and the script…

alias: Special Day Colour
sequence:
  - variables:
      special_colour: |-
        {# Output is colour text string or 'unknown'
         #}
        {%- set special_day_text = states('input_text.special_day_text') -%}
        {%- if special_day_text != "unknown" -%}
          {%- if 'irthday' in special_day_text -%}
            {%- set name = special_day_text |
              regex_replace("^([^\s']+).*", "\\1") | lower -%}
            {%- set colour = states('input_select.' + name + '_welcome_colour') -%}
            {%- if colour != "unknown" -%}
              {{ colour }}
            {%- else -%}
              Random
            {%- endif -%}
          {%- elif 'Valentines' in special_day_text -%}
            Red
          {%- elif 'Palm' in special_day_text -%}
            YellowGreen
          {%- elif 'Moon' in special_day_text -%}
            White
          {%- elif 'Holloween' in special_day_text -%}
            Orange
          {%- elif 'Patrick' in special_day_text -%}
            Green
          {%- elif 'Advent' in special_day_text -%}
            OrangeRed
          {%- elif 'Bank' in special_day_text -%}
            Amber
          {%- elif 'Burns' in special_day_text -%}
            Blue
          {%- elif 'Father' in special_day_text -%}
            PaleBlue
          {%- elif 'Mother' in special_day_text -%}
            HotPink
          {%- elif 'Fawkes' in special_day_text -%}
            Orange
          {%- elif 'Epiphany' in special_day_text -%}
            LemonChiffon
          {%- elif 'Lent' in special_day_text -%}
            Yellow
          {%- elif 'Easter' in special_day_text -%}
            Yellow
          {%- elif 'Shrove' in special_day_text -%}
            Yellow
          {%- elif 'Passion' in special_day_text -%}
            PowderBlue
          {%- elif 'Good' in special_day_text -%}
            Yellow
          {%- elif 'Ascension' in special_day_text -%}
            PaleYellow
          {%- elif 'Candlemas' in special_day_text -%}
            Red
          {%- else -%}
            Random
          {%- endif -%}
        {%- else -%}
          unknown
        {%- endif -%}
  - if:
      - condition: template
        value_template: "{{ special_colour != \"unknown\" and special_colour != \"Random\" }}"
    then:
      - service: light.turn_on
        data:
          color_name: "{{ special_colour }}"
        target:
          entity_id: light.fake_light
      - service: input_number.set_value
        data:
          value: "{{ states('input_number.fake_light_hue') }}"
        target:
          entity_id: input_number.special_day_colour
      - service: input_text.set_value
        data:
          value: "{{ special_colour }}"
        target:
          entity_id: input_text.special_day_colour_name
    else:
      - service: input_number.set_value
        data:
          value: -1
        target:
          entity_id: input_number.special_day_colour
      - service: input_text.set_value
        data:
          value: "{{ special_colour }}"
        target:
          entity_id: input_text.special_day_colour_name
mode: queued
icon: mdi:palette
max: 10

Added the script to work out if today is a special day to Github, just in case anyone finds it useful Special Day