Recurring Task Notification with datetime calculations (HVAC Filter Replacement Reminder)

Not yet, there were code issues in the core frontend that prevented proper use of the editor so some things were on hold. I think it is in pretty good shape now, I have not run into any issues that I know of.

You might need card-mod added in HACS or custom integrations for it to work, I have it installed but I am not sure if that is what is changing the style of the custom stack-in card. Here is the new template code:

decluttering_templates:
  last_event:
    default:
      - icon: calendar-clock
      - next_text: Next due in
      - overdue_text: Overdue by
      - interval_text: ''
      - service: script.script_set_timedate
      - comments: Resets the timer of a Last event to now
    card:
      type: 'custom:stack-in-card'
      mode: vertical
      keep:
        background: true
      cards:
        - type: entities
          entities:
            - entity: '[[entity]]'
              type: 'custom:multiple-entity-row'
              icon: 'mdi:[[icon]]'
              name: '[[name]]'
              hold_action:
                action: call-service
                service: '[[service]]'
                service_data:
                  entity: '[[entity]]'
                  timedate: 0
                confirmation:
                  text: Reset Timestamp?
            - type: 'custom:hui-markdown-card'
              content: >-
                {%- set ts_period = [[interval]] %} {%- set ts_event =
                as_timestamp(states.[[entity]].state) %} {%- set ts_now =
                as_timestamp(now())|round(0) %} {%- set ts_delta = (ts_now -
                ts_event) %} {%- set ts_xdiff = (ts_period - ts_delta)|abs %} {%
                if ts_delta < ts_period %}[[next_text]] {% else
                %}[[overdue_text]] {% endif %}{% if ts_xdiff >= 604800 %}{{
                (ts_xdiff // 604800) | int }} w, {{ (ts_xdiff % 604800 // 86400)
                | int }} d {% elif ts_xdiff >= 86400 %}{{ (ts_xdiff % 604800 //
                86400) | int }} d, {{ (ts_xdiff % 86400 // 3600) | int }} h {%
                elif ts_xdiff >= 3600 %}{{ (ts_xdiff % 86400 // 3600) | int }}
                h, {{ (ts_xdiff % 3600 // 60) | int }} m {% elif ts_xdiff >= 600
                %}{{ (ts_xdiff % 3600 // 60) | int }} m {% elif ts_xdiff >= 60
                %}{{ (ts_xdiff % 3600 // 60) | int }} m, {{ (ts_xdiff % 60) |
                int }} s {% else %}{{ (ts_xdiff % 60) | int }} s {% endif %}

                [[interval_text]]
      style: |
        {%- set ts_period = [[interval]] %}
        {%- set ts_event = as_timestamp(states.[[entity]].state) %}
        {%- set ts_now = as_timestamp(now())|round(0) %}
        {%- set ts_delta = (ts_now - ts_event) %}
        ha-card {
          border: solid 2px {% if (ts_period == 0) or (ts_delta < ts_period) %}var(--card-background-color) {% else %}red {% endif %};
        }
  next_event:
    default:
      - icon: calendar-clock
      - next_text: Next due in
      - overdue_text: Overdue by
      - interval_text: ''
      - almost_due_secs: 2000
      - service: script.script_set_timedate
      - comments: Resets the timer of a Next event to now+interval
    card:
      type: 'custom:stack-in-card'
      mode: vertical
      keep:
        background: true
      cards:
        - type: entities
          entities:
            - entity: '[[entity]]'
              type: 'custom:multiple-entity-row'
              icon: 'mdi:[[icon]]'
              name: '[[name]]'
              secondary_info: last-changed
              hold_action:
                action: call-service
                service: '[[service]]'
                service_data:
                  entity: '[[entity]]'
                  timedate: '[[interval]]'
                confirmation:
                  text: Reset Timestamp?
            - type: 'custom:hui-markdown-card'
              content: >-
                {%- set ts_period = 0 %} {%- set ts_event =
                as_timestamp(states.[[entity]].state) %} {%- set ts_now =
                as_timestamp(now())|round(0) %} {%- set ts_delta = (ts_now -
                ts_event) %} {%- set ts_xdiff = (ts_period - ts_delta)|abs %} {%
                if ts_delta < ts_period %}[[next_text]] {% else
                %}[[overdue_text]] {% endif %}{% if ts_xdiff >= 604800 %}{{
                (ts_xdiff // 604800) | int }} w, {{ (ts_xdiff % 604800 // 86400)
                | int }} d {% elif ts_xdiff >= 86400 %}{{ (ts_xdiff % 604800 //
                86400) | int }} d, {{ (ts_xdiff % 86400 // 3600) | int }} h {%
                elif ts_xdiff >= 3600 %}{{ (ts_xdiff % 86400 // 3600) | int }}
                h, {{ (ts_xdiff % 3600 // 60) | int }} m {% elif ts_xdiff >= 600
                %}{{ (ts_xdiff % 3600 // 60) | int }} m {% elif ts_xdiff >= 60
                %}{{ (ts_xdiff % 3600 // 60) | int }} m, {{ (ts_xdiff % 60) |
                int }} s {% else %}{{ (ts_xdiff % 60) | int }} s {% endif %}

                [[interval_text]]
      style: |
        {%- set ts_event = as_timestamp(states.[[entity]].state) %}
        {%- set ts_now = as_timestamp(now())|round(0) %}
        {%- set ts_delta = (ts_now - ts_event) %}
        ha-card {
          border: solid 2px {% if (ts_delta >= 0) %}red {% elif ts_delta > (-1)*[[almost_due_secs]] %}yellow {% else %}var(--card-background-color) {% endif %};
        }
  next_mileage:
    default:
      - icon: calendar-clock
      - next_text: Next due in
      - overdue_text: Overdue by
      - interval_text: ''
      - almost_due_units: 500
      - service: script.script_set_next_mileage
      - unit: miles
      - comments: Sets a mileage value to an input mileage
    card:
      type: 'custom:stack-in-card'
      mode: vertical
      keep:
        background: true
      cards:
        - type: entities
          entities:
            - entity: '[[entity]]'
              type: 'custom:multiple-entity-row'
              icon: 'mdi:[[icon]]'
              name: '[[name]]'
              secondary_info: last-changed
              hold_action:
                action: call-service
                service: '[[service]]'
                service_data:
                  entity: '[[entity]]'
                  source_mileage: '[[source_mileage]]'
                  interval: '[[interval]]'
                confirmation:
                  text: Reset Mileage?
            - type: 'custom:hui-markdown-card'
              content: >-
                {%- set mi_source = states.[[source_mileage]].state|int %} {%-
                set mi_target = states.[[entity]].state|int %} {%- set mi_xdiff
                = (mi_target - mi_source)|abs %} {% if mi_source < mi_target
                %}[[next_text]] {% else %}[[overdue_text]] {% endif %} {{
                mi_xdiff }} [[unit]]

                Due every [[interval]] [[unit]]
      style: |
        {%- set mi_source = states.[[source_mileage]].state|int %}
        {%- set mi_target = states.[[entity]].state|int %}
        {%- set mi_delta = (mi_source - mi_target) %}
        ha-card {
          border: solid 2px {% if (mi_delta >= 0) %}red {% elif mi_delta > (-1)*[[almost_due_units]] %}yellow {% else %}var(--card-background-color) {% endif %};
        }

The additional variables that add the almost due yellow border are almost_due_secs and almost_due_units, the time defaults to 2000s in the “next event” template, which is the only time based template using it above.

2 Likes

Any Updates on this project?

When the template button entity is release (hopefully, VERY soon) it will be perfect for the momentary “reset” on date stamps used in these types of automations. I want to build one of these too for changing water filters and cleaning HVAC filters but using a switch makes me feel dirty… :smiley:

I made a simple one like this (I’ll be updating it to button template when it’s released):

input_datetime:
  water_filters_last_replaced:
    name: Water Filters Last Replaced
    has_date: true
    has_time: false
    icon: mdi:water-circle

sensor:
  - platform: template
    sensors:
      water_filters_age:
        friendly_name: Water Filters Age
        value_template: "{{ (( as_timestamp(now()) - (states.input_datetime.water_filters_last_replaced.attributes.timestamp)) | int /60/1440) | round(0) }}"
        icon_template: mdi:clock-start
        unit_of_measurement: 'Days'
        
input_boolean:
  reset_water_filters_last_replaced:
    name: Reset Water Filters Last Replaced
    icon: mdi:water-sync

automation:
  - id: reset_water_filters_last_replaced
    alias: Reset Water Filters Last Replaced
    initial_state: true
    trigger:
      - platform: state
        entity_id: input_boolean.reset_water_filters_last_replaced
        to: "on"
    action:
      - service: input_datetime.set_datetime
        target:
          entity_id: input_datetime.water_filters_last_replaced
        data:
          timestamp: "{{ now().timestamp() }}"
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.reset_water_filters_last_replaced

It looks like this:
image

4 Likes

Hi,

At home, our 4 kids have different tasks. The kids arrive on wednesday (2 of them) in the odd weeks and on friday (in the odd weeks).
There are 4 tasks (like empty the dishwasher) etc… and every kid has another task, every week. So actually, every 4 weeks they will do the same tasks.
I already created input_select fields with the name of the task & with 4 options (name of the kids). Now I want to setup a schedule starting today (because it is wednesday today) and repeat the tasks for 1 week every 4 weeks.

So to be clear:
Wednesday to wednesday for kid 1 : Dishwasher
Wednesday to wednesday for kid 2 : clean kitchen

And this needs to happen every 4 weeks (because kid 1 will have again task 1)

Is it possible to arrange that with the scheduler card ?

Outcome : I want, based on the scheduler, create a page with an overview of the 4 kids and picture-entity of the task they need to do

Thanks a lot in advance !!!

Kr,

Bart

I tried to implement your code, but I keep getting the error “custom element doesn’t exists: hui-markdown-card”.
So I tried to replace it with type: markdown, but same type of error. Which is strange, since it is documented here: Markdown Card - Home Assistant

Do I miss anything?

You need to install HACS and then use that to install the custom card. Do a Google search for “home assistant HACS” and you should be able to figure it out.

Alright, thanks! I did install all the other custom cards and read about the hui stuff is superseded. So I tried to do it with the new, integrated solution “markdown” but as stated above there is an error. What I did then is just append that card into the custom “stack-in-card” instead of having it be part of the “entities”-card, which threw the error. With that change I got it running, however it doesn’t look that pretty (although it is close to the pictures). I’ll tinker on it and if I get a more streamline card, share it here for everyone else to try :slight_smile:

edit here we go. Result:
image
template (for the custom declutter card):

rec_event:
    default:
      - icon: calendar-clock
      - next_text: Next due in
      - overdue_text: Overdue by
      - almost_due_offset: 1
      - unit_sec: 86400 #=1d 
      - interval_text: ''
      - service: script.script_set_timedate
      - comments: Resets the timer of last event to now
    card:
      type: custom:mushroom-template-card
      entity: '[[entity]]'
      icon: mdi:[[icon]]
      icon_color: |-
        {%- set ts_period = [[interval]]*[[almost_due_offset]]*[[unit_sec]] %}
        {%- set ts_event = as_timestamp(states.[[entity]].state) %}
        {%- set ts_now = as_timestamp(now())|round(0) %}
        {%- set ts_delta = (ts_now - ts_event) %}
        {% if (ts_period != 0) and (ts_delta > ts_period) %}red {% elif (ts_period-ts_delta) < [[almost_due_offset]]*[[unit_sec]] %}orange {% endif %}
      primary: '[[name]]'
      layout: horizontal
      tap_action:
        action: call-service
        service: '[[service]]'
        service_data:
          entity: '[[entity]]'
          timedate: 0
        confirmation:
          text: Reset the timer of last event to now?
      secondary: >-
        {%- set ts_period = [[interval]]*[[unit_sec]] %} {%- set ts_event =
        as_timestamp(states.[[entity]].state) %} {%- set ts_now =
        as_timestamp(now())|round(0) %} {%- set ts_delta = (ts_now - ts_event)
        %} {%- set ts_xdiff = (ts_period - ts_delta)|abs %} {% if ts_delta <
        ts_period %}[[next_text]] {% else %}[[overdue_text]] {% endif %}{% if
        ts_xdiff >= 604800 %}{{ (ts_xdiff // 604800) | int }}w, {{ (ts_xdiff %
        604800 // 86400) | int }}d {% elif ts_xdiff >= 86400 %}{{ (ts_xdiff %
        604800 // 86400) | int }}d {% elif ts_xdiff >= 3600 %}{{ (ts_xdiff %
        86400 // 3600) | int }}h {% elif ts_xdiff >= 600 %}{{ (ts_xdiff % 3600
        // 60) | int }}m {% elif ts_xdiff >= 60 %}{{ (ts_xdiff % 3600 // 60) |
        int }}m, {{ (ts_xdiff % 60) | int }}s {% else %}{{ (ts_xdiff % 60) | int
        }}s {% endif %} [[interval_text]].
      style: |-
        {%- set ts_period = [[interval]]*86400 %}
        {%- set ts_event = as_timestamp(states.[[entity]].state) %}
        {%- set ts_now = as_timestamp(now())|round(0) %}
        {%- set ts_delta = (ts_now - ts_event) %}
        ha-card {
          border: solid 2px {% if (ts_period != 0) and (ts_delta > ts_period) %}red {% elif (ts_period-ts_delta) < [[almost_due_offset]]*[[unit_sec]] %}orange {% elif (ts_period == 0) or (ts_delta < ts_period) %}var(--card-background-color) {% else %}red {% endif %};
        }

the actual card-code:

      - type: custom:decluttering-card
        template: rec_event
        variables:
          - entity: input_datetime.recurring_last_vacuum
          - name: Snoopy
          - icon: robot-vacuum
          - interval: 7
          - interval_text: (due every week)

I have to say though, that with this template, I’m missing the actual date to be displayed on the right. Maybe I’ll add it to the title or somewhere else.

2 Likes

To be honest, I don’t know how you would be able to do this in a dynamic way in an easy way. But what could be a shortcut for your application, is doing some images with all possible scenarios and let them rotate?

Card looks great and Ive stolen your code and template. however would you mind sharing the script to reset the date?

Has anybody managed to create a bar/gauge based on the next due date?

@weemaba999 any progress?
Otherwise you can try to (ab)use the area section of a task to assign it to a person (in stead of a location).

I created this article about how I handle my recurring tasks Home Assistant dashboard: Chores | vd Brink Home Automations
This is only without dynamic persons assigning but if you can manage to script it to assign the the task to the right person/area, the auto-entities card can probably also filter on area. Then you can create a list for each person.

1 Like

Sorry for the late reply. Not looking in here on a regular basis.
So here is my script_set_timedate script for the cards:

alias: Set Timedate
sequence:
  - service: input_datetime.set_datetime
    data:
      timestamp: >-
        {{ now().timestamp() + (timedate | default(0)) * (unit | default(86400))
        }}
      entity_id: "{{ entity }}"
mode: single

Screenshot (7)
Not sure where I went wrong but this doesn’t look right to me. Any idea what I need to fix?

In my automation for some reason it’s give me an error:

alias: "Notify: HVAC Filter"
description: ""
trigger:
  - platform: state
    entity_id: sensor.hvac_filter_days_left
    to: "0"
    id: Days Left HVAC
condition: []
action:
  - service: notify.mobile_app_sm_s918u
    data:
      message: Time to Replace an HVAC Filter
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.hvac_filter_clean
    data: {}
mode: single
    - name: HVAC Filter Days Left
      unique_id: 1ef0d629-911a-4a18-96a1-f0503e77b119
      #friendly_name: 'HVAC Filter Days Left'
      state: >
        {% set hvac_filter_change_date = strptime(states('input_datetime.hvac_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.hvac_filter_days_until_old') | int) %}
        {{ (as_timestamp(hvac_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}
      icon: mdi:clock-end
      unit_of_measurement: 'Days'

does this setup still work? I seem to be getting errors when setting up the sensors.

Still working for me. But I set this up a long time ago now and haven’t had a reason to redo it.

What errors are you getting?

NVM, I see what I’d did wrong. Didn’t add the helpers…

2024-07-02 22:39:00.130 ERROR (MainThread) [homeassistant.helpers.template] Template variable error: ‘None’ has no attribute ‘attributes’ when rendering ‘{{ (( as_timestamp(now()) - (states.input_datetime.upstairs_kitchen_water_filter_date_last_changed.attributes.timestamp)) | int /60/1440) | round(0) }}’


  - platform: template
    sensors:
      upstairs_kitchen_water_filter_change_date:
        friendly_name: 'Upstairs Kitchen Water Filter Change Date'
        value_template: "{{ strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) }}"
        icon_template: mdi:calendar-refresh-outline
      upstairs_kitchen_water_filter_days_left:
        friendly_name: 'Upstairs Kitchen Water Filter Days Left'
        value_template: >
          {% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_nater_filter_days_until_old') | int) %}
          {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}
        icon_template: mdi:clock-end
        unit_of_measurement: 'Days'
      upstairs_kitchen_water_filter_age:
        friendly_name: 'Upstairs Kitchen Water Filter Age'
        value_template: "{{ (( as_timestamp(now()) - (states.input_datetime.upstairs_kitchen_water_filter_date_last_changed.attributes.timestamp)) | int /60/1440) | round(0) }}"
        icon_template: mdi:clock-start
        unit_of_measurement: 'Days'

I did a batch replace of HVAC to upstairs_kitchen_water

Fixed via changing helper entity to Date + Time
I am still getting some errors.

2024-07-03 09:43:54.243 ERROR (MainThread) [homeassistant.components.template.template_entity] TemplateError('ValueError: Template error: strptime got invalid input '2024-07-02' when rendering template '{{ strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) }}' but no default was specified') while processing template 'Template<template=({{ strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) }}) renders=4>' for attribute '_attr_native_value' in entity 'sensor.upstairs_kitchen_water_filter_change_date'
2024-07-03 09:43:54.244 ERROR (MainThread) [homeassistant.helpers.event] Error while processing template: Template<template=({% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) %} {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}) renders=2>
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 2100, in strptime
    return datetime.strptime(string, fmt)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/_strptime.py", line 554, in _strptime_datetime
    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/_strptime.py", line 333, in _strptime
    raise ValueError("time data %r does not match format %r" %
ValueError: time data '2024-07-02' does not match format '%Y-%m-%d %H:%M:%S'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 603, in async_render
    render_result = _render_with_context(self.template, compiled, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 2616, in _render_with_context
    return template.render(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/jinja2/environment.py", line 1304, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.12/site-packages/jinja2/environment.py", line 939, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 1, in top-level template code
  File "/usr/local/lib/python3.12/site-packages/jinja2/sandbox.py", line 394, in call
    return __context.call(__obj, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 2103, in strptime
    raise_no_default("strptime", string)
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 1853, in raise_no_default
    raise ValueError(
ValueError: Template error: strptime got invalid input '2024-07-02' when rendering template '{% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) %} {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}' but no default was specified
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 715, in async_render_to_info
    render_info._result = self.async_render(  # noqa: SLF001
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 605, in async_render
    raise TemplateError(err) from err
homeassistant.exceptions.TemplateError: ValueError: Template error: strptime got invalid input '2024-07-02' when rendering template '{% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) %} {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}' but no default was specified
2024-07-03 09:43:54.246 ERROR (MainThread) [homeassistant.components.template.template_entity] TemplateError('ValueError: Template error: strptime got invalid input '2024-07-02' when rendering template '{% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) %} {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}' but no default was specified') while processing template 'Template<template=({% set upstairs_kitchen_water_filter_change_date = strptime(states('input_datetime.upstairs_kitchen_water_filter_date_last_changed'), '%Y-%m-%d %H:%M:%S') + timedelta(days=states('input_number.upstairs_kitchen_water_filter_days_until_old') | int) %} {{ (as_timestamp(upstairs_kitchen_water_filter_change_date) / 86400 - as_timestamp(now()) / 86400) | int  }}) renders=4>' for attribute '_attr_native_value' in entity 'sensor.upstairs_kitchen_water_filter_days_left'

You’re at least sharing the error, but you also need to share the latest template.