"Days until X" sensors based on single local calendar

I’m attempting to re-establish template sensors that tell me when the trash and recycle will be next picked up, using local calendars.

Specifically, I have a local calendar with two recurring events: Trash Pickup (which happens weekly), and Recycle Pickup (which happens biweekly). Both happen on the same day of the week, so one week, trash is picked up, and the next week, both trash and recycle are.

I can see from the documentation that the state and attributes of a Local Calendar entity only reflect the next event on that calendar, and if I want data about more than the immediate next event, I would need to request and parse the response data.

How can I build template sensors that look at a local calendar and identify the number of days until (1) the next event with “trash” in the name, and (2) the next event with “recycling” in the name? I’m trying to stick to this configuration rather than splitting the trash and recycle into their own respective calendars because it seems tidier to only have one calendar. Also, understanding this approach might help me apply similar approaches to other calendars.

Here’s an example that I helped another user with, note that this has been updated to use calendar.get_events. If you aren’t using 2023.12 or newer, you will need to make a couple changes.

template:
  - trigger:
      - platform: time
        at: "00:00:00"
    action:
      - service: calendar.get_events
        data:
          start_date_time: "{{today_at()}}"
          duration:
            hours: 168
            minutes: 0
            seconds: 0
        target:
          entity_id: calendar.trash_night
        response_variable: agenda
    sensor:
      - name: "Trash Night Countdown"
        state: >
          {% set midnight = today_at() %}
          {% set event = agenda["calendar.trash_night"].events | selectattr('summary', 'search', 'Trash') 
          | map(attribute='start') | map('as_datetime') | first %}
          {% set delta = (event - midnight).days %}
          {% if delta == 0 %}
            Today
          {% elif delta == 1 %}
            Tomorrow
          {% elif delta == 2 %}
            Day after Tomorrow
          {% else %}
            In {{ delta }} Days
          {% endif %}
1 Like

Thanks for the tip, Drew!

I’ve modified it to include both types of event and to create two sensors for each type of waste: one that just reports the number of days until pickup, and one that reports a verbose string, like you provided.

template:
  - trigger:
      - platform: time
        at: "00:00:00"
    action:
      - service: calendar.get_events
        data:
          start_date_time: "{{today_at()}}"
          duration:
            days: 8
            hours: 0
            minutes: 0
            seconds: 0
        target:
          entity_id: calendar.waste_pickup
        response_variable: agenda
    sensor:
      - name: "Days Until Next Trash Pickup"
        state: >
          {% set midnight = today_at() %}
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Trash Pickup') 
          | map(attribute='start') | map('as_datetime') | first %}
          {% set delta = (event - midnight).days %}
          {{ delta }}
        unit_of_measurement: "Days"
        icon: mdi:calendar-arrow-right
      - name: "Days Until Next Recycling Pickup"
        state: >
          {% set midnight = today_at() %}
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Recycling Pickup') 
          | map(attribute='start') | map('as_datetime') | first %}
          {% set delta = (event - midnight).days %}
          {{ delta }}
        icon: mdi:calendar-arrow-right
        unit_of_measurement: "Days"
      - name: "Trash Night Countdown"
        state: >
          {% set midnight = today_at() %}
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Trash Pickup') 
          | map(attribute='start') | map('as_datetime') | first %}
          {% set delta = (event - midnight).days %}
          {% if delta == 0 %}
            Today
          {% elif delta == 1 %}
            Tomorrow
          {% elif delta == 2 %}
            Day after Tomorrow
          {% else %}
            In {{ delta }} Days
          {% endif %}
        icon: mdi:trash-can
      - name: "Recycle Night Countdown"
        state: >
          {% set midnight = today_at() %}
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Recycling Pickup') 
          | map(attribute='start') | map('as_datetime') | first %}
          {% set delta = (event - midnight).days %}
          {% if delta == 0 %}
            Today
          {% elif delta == 1 %}
            Tomorrow
          {% elif delta == 2 %}
            Day after Tomorrow
          {% else %}
            In {{ delta }} Days
          {% endif %}
        icon: mdi:recycle

Is there a way to simplify this so that I don’t repeat as much templating?

There are a number of ways to reduce the amount of templating including yaml anchors, template macros, or rewriting the template significantly.

But you can reduce it a bit without resorting to any of those by flattening variables and setting the “countdown” sensors to be based on the states of the “days until” sensors:

template:
  - trigger:
      - platform: time
        at: "00:00:00"
    action:
      - service: calendar.get_events
        data:
          start_date_time: "{{today_at()}}"
          duration:
            days: 8
        target:
          entity_id: calendar.waste_pickup
        response_variable: agenda
    sensor:
      - name: "Days Until Next Trash Pickup"
        state: >
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Trash Pickup') | map(attribute='start') | map('as_datetime') | first %}
          {{ (event | as_local - today_at()).days }}
        unit_of_measurement: "Days"
        icon: mdi:calendar-arrow-right
      - name: "Days Until Next Recycling Pickup"
        state: >
          {% set event = agenda["calendar.waste_pickup"].events | selectattr('summary', 'search', 'Recycling Pickup') | map(attribute='start') | map('as_datetime') | first %}
          {{ (event | as_local - today_at()).days }}
        icon: mdi:calendar-arrow-right
        unit_of_measurement: "Days"

  - sensor:
      - name: "Trash Night Countdown"
        state: >
          {% set delta = states('sensor.days_until_next_trash_pickup') | int %}
          {% if delta == 0 %} Today
          {% elif delta == 1 %} Tomorrow
          {% elif delta == 2 %} Day after Tomorrow
          {% else %} In {{ delta }} Days
          {% endif %}
        icon: mdi:trash-can
      - name: "Recycle Night Countdown"
        state: >
          {% set delta = states('sensor.days_until_next_trash_pickup') | int %}
          {% if delta == 0 %} Today
          {% elif delta == 1 %} Tomorrow
          {% elif delta == 2 %} Day after Tomorrow
          {% else %} In {{ delta }} Days
          {% endif %}
        icon: mdi:recycle
1 Like

When I try this, I’m getting this error in the logs:

“Error rendering state template for sensor.next_trash_pickup: TypeError: can’t subtract offset-naive and offset-aware datetimes”

Repeated for all four sensors.

Since this template uses data requested as a service call, I’m having a hard time troubleshooting it using the Template Editor; otherwise, I’d be monkeying around in there trying to fix things.

That’s likely just due to your events being all-day.

Change the last line on the “Days until” sensors to:

{{ (event|as_local - today_at()).days }}

@Didgeridrew You’re being a fantastic help, thank you.

Okay, last thing I’m toying with: would it be possible to instead consolidate various takes on this info (date of next pickup, days until, verbose countdown, day of week of next pickup) into attributes of the single sensor entity? So for instance, a “Next Trash Pickup” sensor that returns the date as its state, but then has the other items above as attributes.

To be honest, I’m not entirely sure if this is an appropriate use of attributes. Maybe I’m barking up the wrong tree here, but I wanted to check.

It’s your template sensor in your home assistant instance… you get to choose what appropriate uses of attributes are. :slight_smile:

In order to do it and minimize repeated templates, it’s better to structure it as one trigger-based sensor that gets the dates from the service call and two state-based sensors that process that into the attributes.

template:
  - trigger:
      - platform: time
        at: "00:00:00"
      - platform: homeassistant
        event: start
    action:
      - service: calendar.get_events
        data:
          start_date_time: "{{today_at()}}"
          duration:
            days: 15
        target:
          entity_id: calendar.waste_pickup
        response_variable: agenda
    sensor:
      - name: Waste Calendar Check
        state: "{{ now() }}"
        attributes:
          date_map: |
            {% set ns = namespace(types={}) %}
            {% for type in ['Recycling', 'Trash'] %}
              {% set date = (agenda['calendar.waste_pickup']['events'] | selectattr('summary', 'search', type, ignorecase=true) 
              | sort(attribute= 'start') | map(attribute='start') | map('as_datetime') | map('as_local') | first).date() %}
              {% set ns.types = dict(ns.types, **{type: date|string}) %}
            {%- endfor %}
            {{ ns.types }}

  - sensor:      
      - name: "Next Trash Pickup"
        state: "{{ state_attr('sensor.waste_calendar_check', 'date_map')['Trash'] }}"
        icon: mdi:calendar-arrow-right
        <<: &waste_attr
          attributes:
            day_of_week: "{{ strptime(this.state, '%Y-%m-%d', now()).strftime('%A') }}"
            days_until: |
              {% set event = strptime(this.state, '%Y-%m-%d', now()) %}
              {{ (event|as_local - today_at()).days }}
            countdown: |
              {% set delta = this.attributes.days_until %}
              {% if delta == 0 %} Today
              {% elif delta == 1 %} Tomorrow
              {% elif delta == 2 %} Day after Tomorrow
              {% else %} In {{ delta }} Days {% endif %}      
      - name: "Next Recycling Pickup"
        state: "{{ state_attr('sensor.waste_calendar_check', 'date_map')['Recycling'] }}"
        icon: mdi:calendar-arrow-right
        <<: *waste_attr
2 Likes

Wow, Drew, you’ve been a huge help. Thank you. I made some tweaks and deployed this and it’s great.

I’ll be looking at this and learning from it for a while. Thanks for illustrating how to use YAML aliases and anchors, too—hadn’t seen that before!

Really appreciate all the effort you put into writing these examples so I could learn. Cheers.

Hi Drew - been trying to follow the help you gave Spencer. I’m still pretty new to templating and can’t seem to get the sensors to appear in my install.

I’ve copied and pasted your template into the templates.yaml file (omitting templates: line at the top) and tweaked code as required to my cal is correct and search terms too.

Couple of questions…

  1. Should the sensors just appear in my install after resetting HA
  2. As the sensors are only listed as a name, do they default to have an entity in lower case and underscore e.g. “Important Sensor” > “important_sensor”

Thanks so much and apologies for the basic questions. I’ve tried to get to grips with templates and sensor creation but still feels like the dark arts at the moment!

  1. If this is your first Template entity, you need to restart Home Assistant not just reload this can be done through the “on/off” button at the top right of the Systems settings page or through the YAML tab of developer tools.
  2. Yes, the default entity ID will be the entity domain (i.e. sensor.) followed by an object ID (important_sensor) which is a slugified version of the name.

If you still can’t find the sensor after restarting, double check that you have properly “included”/merged your templates.yaml file into your configuration. You should have the following line in your configuration.yaml file:

template: !include templates.yaml

Thanks for your help - after some playing around I have got the sensors to work, however…the Calendar Check doesn’t seem to populate the date_map attr.

I have made some tweaks to your but only so it plays nicely with my calendar, search terms and UK English :slight_smile:

- trigger:
    - platform: time
      at: "00:00:00"
    - platform: homeassistant
      event: start
  action:
    - service: calendar.get_events
      data:
        start_date_time: "{{today_at()}}"
        duration:
          days: 15
      target:
        entity_id: "calendar.home"
        response_variable: agenda
  sensor:
    - name: Bin Calendar Check
      state: "{{ now() }}"
      attributes:
        date_map: |
          {% set ns = namespace(types={}) %}
          {% for type in ['Recycling', 'Garden', 'General'] %}
            {% set date = (agenda['calendar.home']['events'] | selectattr('summary', 'search', type, ignorecase=true) 
            | sort(attribute= 'start') | map(attribute='start') | map('as_datetime') | map('as_local') | first).date() %}
            {% set ns.types = dict(ns.types, **{type: date|string}) %}
          {%- endfor %}
          {{ ns.types }}
- sensor:
    - name: "Next Recycling Bin Collection"
      state: "{{ state_attr('sensor.bin_calendar_check', 'date_map')['Recycling'] }}"
      icon: mdi:calendar-arrow-right
      <<: &waste_attr
        attributes:
          day_of_week: "{{ strptime(this.state, '%Y-%m-%d', now()).strftime('%A') }}"
          days_until: |
            {% set event = strptime(this.state, '%Y-%m-%d', now()) %}
            {{ (event|as_local - today_at()).days }}
          countdown: |
            {% set delta = this.attributes.days_until %}
            {% if delta == 0 %} Today
            {% elif delta == 1 %} Tomorrow
            {% elif delta == 2 %} Day after Tomorrow
            {% else %} In {{ delta }} Days {% endif %}
    - name: "Next Garden Bin Collection"
      state: "{{ state_attr('sensor.bin_calendar_check', 'date_map')['Garden'] }}"
      icon: mdi:calendar-arrow-right
      <<: *waste_attr
    - name: "Next General Bin Collection"
      state: "{{ state_attr('sensor.bin_calendar_check', 'date_map')['General'] }}"
      icon: mdi:calendar-arrow-right
      <<: *waste_attr

I noticed you said something about all day events, my collections are as such so should I add…

{{ (event|as_local - today_at()).days }}

…at the end of the code?

Cheers!

Your response_variable key is indented too far.

Also, keep in mind that OP’s situation specified that there would be at least 1 calendar entry for each service type in the 15 day span. If that is not your situation the template will need to be modified to either query a longer time span or reject types that don’t have event before the date variable is set.

The configuration you posted already has the as_local filter in use in the days_until attribute…

I’m trying to follow along with a simplified approach of what is outlined above, however even though I’ve managed to get the sensor available to HA, the value I get always seems to be “unknown”

Ultimately I want to get to the same point as is outlined above, with multiple sensors for days until each garbage pick up, however for now I’ll settle for just one working. I’ve pulled this back to the most simple case I could to try and get it to work.

For completeness, I’m calling my templates.yaml file from my configuration.yaml file in the normal way:

template: !include templates.yaml

In my templates.yaml file I have my garbage collection sensor I’m trying to build. One thing I’d note is that I do have some other sensors defined in here - I don’t think this is an issue but worth noting (I’ve removed most of them for brevity, but have kept one as an example just to highlight the structure of what I’m getting):

- sensor:
    - name: Front Bedroom Humidity
      state: "{{ state_attr('climate.bedroom', 'current_humidity') }}"
      unit_of_measurement: '%'
      icon: mdi:water-percent
- trigger:
    - platform: time
      at: "00:00:00"
  action:
    - service: calendar.get_events
      data:
        start_date_time: "{{today_at()}}"
        duration:
          days: 15
      target:
        entity_id: calendar.garbage_cycle
      response_variable: agenda
  sensor:
    - name: "Waste Green"
      state: >
        {% set midnight = today_at() %}
        {% set event = agenda["calendar.garbage_cycle"].events | selectattr('summary', 'search', 'Green Waste') 
        | map(attribute='start') | map('as_datetime') | first %}
        {% set delta = (event - midnight).days %}
        {{ delta }}
      unit_of_measurement: "Days"
      icon: mdi:leaf

Once I configure this, restart HA or reload the template entities, I get the sensor.waste_green entity available. However it’s state is always unknown. I’ve confirmed the service is correct from the dev tools and returns data like the below:

calendar.garbage_cycle:
  events:
    - start: "2024-05-23"
      end: "2024-05-24"
      summary: Green Waste
      description: Green waste information
    - start: "2024-05-23"
      end: "2024-05-24"
      summary: Recycling
      description: Recycling Bin
    - start: "2024-05-30"
      end: "2024-05-31"
      summary: General Waste
      description: General waste red bin
    - start: "2024-05-30"
      end: "2024-05-31"
      summary: Green Waste
      description: Green waste information

I thought maybe the trigger hadn’t run, however I just set it to be a few minutes from my current time, let it run well past that but unfortuantely the trigger didn’t run then and update.

Any thoughts or help would be super appreciated.

You’re suffering the same issue as Post #6… doing math between timezone-naive and timezone-aware datetime objects. You need to localize the event variable’s value:

...
{% set delta = (event | as_local - midnight).days %}
....

Ace - thanks for that. Also seems you have to get at least one successful run before it clears the “unknown” status on the entity. So the change above with as_local and then using a trigger in a few minutes time seemed to do the trick.

Thanks @Didgeridrew !