Template Sensor Configuration for Calendar Events

:wave: Hi

I’m working on tracking waste collection events from my calendar. My goal is to filter events by waste type (e.g., Food Waste, Non-Recyclable Waste, etc.) and use these filtered results to populate the template sensors for each waste type. My below configuration works and populates everything correctly, but I feel like there has to be a more efficient and cleaner way to do this.

Ideally, I’d like to perform the selectattr operation just once for each waste type and reuse the result, minimizing redundant processing. Is there a way to do this?

Here’s an overview of my functional but bulky setup:

template:
  - trigger:
      - platform: time_pattern
        seconds: /10
    action:
      - service: calendar.list_events
        target:
          entity_id: calendar.waste_collection
        data:
          duration:
            days: 14
        response_variable: scheduled_waste_events
    sensor:
      - name: Food Waste Collection Events
        state: |
          {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Food', true) | list | count() }}
        attributes:
          scheduled_events: |
            {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Food', true) | list }}
          next_collection_date: |
            {{ (scheduled_waste_events.events | selectattr('summary', 'search', 'Food', true) | list | first).start }}
          countdown: |
            {{ int((as_timestamp((scheduled_waste_events.events | selectattr('summary', 'search', 'Food', true) | list | first).start) - as_timestamp(today_at('00:00')))/86400)}}

      - name: Non-recyclable Waste Collection Events
        state: |
          {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Non-Recyclable', true) | list | count() }}
        attributes:
          scheduled_events: |
            {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Non-Recyclable', true) | list }}
          next_collection_date: |
            {{ (scheduled_waste_events.events | selectattr('summary', 'search', 'Non-Recyclable', true) | list | first).start }}
          countdown: |
            {{ int((as_timestamp((scheduled_waste_events.events | selectattr('summary', 'search', 'Non-Recyclable', true) | list | first).start) - as_timestamp(today_at('00:00')))/86400)}}

      - name: Paper Cardboard Waste Collection Events
        state: |
          {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Paper', true) | list | count() }}
        attributes:
          scheduled_events: |
            {{ scheduled_waste_events.events | selectattr('summary', 'search', 'Paper', true) | list }}
          next_collection_date: |
            {{ (scheduled_waste_events.events | selectattr('summary', 'search', 'Paper', true) | list | first).start }}
          countdown: |
            {{ int((as_timestamp((scheduled_waste_events.events | selectattr('summary', 'search', 'Paper', true) | list | first).start) - as_timestamp(today_at('00:00')))/86400)}}

# And so on for each waste type...

Thanks.

Be advised that the service call calendar.list_events has been deprecated and replaced by calendar.get_events. The deprecated service call will stop working in version 2024.6.0.

The new service call is even “bulkier” than the new one because it’s designed to handle multiple calendars in the response_variable. For example, this:

scheduled_waste_events.events

changes to this:

scheduled_waste_events['calendar.waste_collection']['events']

As for your original question, I can’t think of a way to reduce the size of each template or minimize duplication. A macro template wouldn’t substantially reduce the overall template size for each attribute’s template and YAML Anchors and Aliases aren’t useful here because each template is slightly different (varying by the type of waste material).

1 Like

Thanks very much @123 . I’ve actually only been able to make this work by iterating on a snippet you’ve shared a while back. Otherwise I’d be lost.

I’ve now changed to calendar.get_events and it has indeed made the templates bulkier, but at least I can rely on it for the foreseeble future.

1 Like

You can define variables in the action block:

template:
  - trigger:
      - platform: time_pattern
        seconds: /10
    action:
      - service: calendar.get_events
        target:
          entity_id: calendar.waste_collection
        data:
          duration:
            days: 14
        response_variable: waste_events
      - variables:
          all_events: "{{ waste_events['calendar.waste_collection'].events }}"
          paper_ev: "{{ all_events | selectattr('summary', 'search', 'Paper', true) }}"
          food_ev: "{{ all_events | selectattr('summary', 'search', 'Food', true) }}"
          non_recycle_ev: "{{ all_events | selectattr('summary', 'search', 'Non-Recyclable', true) }}"
    sensor:
      - name: Food Waste Collection Events
        state: |
          {{ food_ev | list | count() }}
        attributes:
          scheduled_events: |
            {{ food_ev | list }}
          next_collection_date: |
            {{ (food_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((food_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

# And so on for each waste type...
2 Likes

I suggest you mark Didgeridrew’s post with the Solution tag because I believe that’s currently the best you can do to reduce overall template size.

1 Like

Thanks @Didgeridrew ! This works great. Pasting below the template I ended up using, if anyone is interested.

template:
  - trigger:
      - platform: time_pattern
        seconds: /10
    action:
      - service: calendar.get_events
        target:
          entity_id: calendar.waste_collection
        data:
          duration:
            days: 14
        response_variable: scheduled_waste_events
      - variables:
          all_events: "{{ scheduled_waste_events['calendar.waste_collection'].events  }}"
          food_ev: "{{ all_events | selectattr('summary', 'search', 'Food', true) | list }}"
          non_recycle_ev: "{{ all_events | selectattr('summary', 'search', 'Non-Recyclable', true) | list }}"
          paper_ev: "{{ all_events | selectattr('summary', 'search', 'Paper', true) | list }}"
          mixed_ev: "{{ all_events | selectattr('summary', 'search', 'Mixed', true) | list }}"
          garden_ev: "{{ all_events | selectattr('summary', 'search', 'Garden', true) | list }}"
    sensor:
      - name: Food Waste Collection Events
        state: |
          {{ food_ev | count() }}
        attributes:
          scheduled_events: |
            {{ food_ev }}
          next_collection_date: |
            {{ (food_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((food_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

      - name: Non-recyclable Waste Collection Events
        state: |
          {{ non_recycle_ev | count() }}
        attributes:
          scheduled_events: |
            {{ non_recycle_ev }}
          next_collection_date: |
            {{ (non_recycle_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((non_recycle_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

      - name: Paper Cardboard Waste Collection Events
        state: |
          {{ paper_ev | count() }}
        attributes:
          scheduled_events: |
            {{ paper_ev }}
          next_collection_date: |
            {{ (paper_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((paper_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

      - name: Mixed Recycling Waste Collection Events
        state: |
          {{ mixed_ev | count() }}
        attributes:
          scheduled_events: |
            {{ mixed_ev }}
          next_collection_date: |
            {{ (mixed_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((mixed_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}
        icon: mdi:calendar

      - name: Garden Waste Collection Events
        state: |
          {{ garden_ev | count() }}
        attributes:
          scheduled_events: |
            {{ garden_ev }}
          next_collection_date: |
            {{ (garden_ev | first).start }}
          countdown: |
            {{ int((as_timestamp((garden_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

Which gives me below:

I’ve been scratching my head on this one for a couple of days now. I have made sure that my indentation is correct, checked the docs for help and been back and forth trying different things out, but I’m just going round in circles.

I have a calendar to keep track of my late shifts and overtime at work and have changed this template to serve that but I’m getting the error below:

Logger: homeassistant.helpers.script.trigger_update_coordinator
Source: helpers/script.py:2026
First occurred: 11:50:50 (1 occurrences)
Last logged: 11:50:50

Trigger Update Coordinator: Error executing script. Invalid data for call_service at pos 1: Script does not support 'response_variable' for service 'response_variable' which does not support response data.

I have tried both agenda['calendar.work']['events'] and agenda['calendar.work'].events. I have tried plugging that into the state instead of using variables, been back and forth with indentation several times…

- trigger:
    - platform: time_pattern
      seconds: /10
  action:
    - action: calendar.get_events
      target:
        entity_id:
          - calendar.work
      data:
        duration:
          days: 8
      response_variable: agenda
    - variables:
        all_events: "{{ agenda['calendar.work']['events'] }}"
        overtime_events: "{{ all_events | selectattr('summary', 'search', 'Overtime', true) | list }}"
        late_shift_events: "{{ all_events | selectattr('summary', 'search', 'Late Shift', true) | list }}"

  sensor:
    - name: Overtime
      state: |
        {{ (overtime_events | first).start }}
      attributes:
        scheduled_events: |
          {{ overtime_events }}

    - name: Late Shift
      state: |
        {{ (late_shift_events | first).start }}
      attributes:
        scheduled_events: |
          {{ late_shift_events }}

I do have a split setup, with the following in my templates.yaml:
template: !include_dir_merge_list ../entities/template this is why the template: is missing from the top.
I have other template sensors that are working fine in this setup, so I don’t think this is an issue.

The only major difference I can see is that services has changed to actions recently…

I will continue my search for a solution and update if/when I find it, or if someone has a solution, that would be fantastic!

Hey!

It’s my first time trying to use templates. I thought I could use the helper for that but apparently not:


(I understand the code is incomplete but if this 1/3 doesn’t work, there is no point trying the rest)

Is there a way to do this with the helper (or something from the GUI)?

If not I’m not sure what to do: create a template.yaml file and declare it in my conf with template: !include template.yaml?

(Asking before I break something…)

No. The “State Template” field in the Helper editor is for Jinja templating only, not YAML configuration. Advanced functions like triggers are not currently available for Template Helpers.

There are multiple ways to split the configuration. No one method is inherently better than the others, it is down to personal preference and splitting is completely optional. If you are just starting out, you can just add the template key to your configuration.yaml file and place the sensor config underneath it.

I split my configuration the way I mentioned previously.

It doesn’t work, though and I’ll need more help.

What I’ve done:

  1. Created a garbage calendar (called poubelle) with a recurring event for food garbage, the next one being on the 7th at 2:30 PM and the following one on the 14th:

  1. Checked that calendar.get_events worked and that in the next 14 days I have two Poubelle verte:


(Note: I used 336 hours because the GUI doesn’t propose days but I imagine that’s not the issue in the yaml since it worked for people here before)

  1. Created /homeassistant/template.yaml and written this code:
# Creating a sensor for the next Poubelle verte event
- trigger:
  - platform: time_pattern
    seconds: /10
  action:
    - service: calendar.get_events
      target:
        entity_id: calendar.poubelles
      data:
        duration:
          days: 14
      response_variable: waste_events
    - variables:
        all_events: "{{ waste_events['calendar.poubelles'].events }}"
        food_ev: "{{ all_events | selectattr('summary', 'search', 'Poubelle verte', true) }}"
  sensor:
  - name: Poubelle verte template
    state: |
      {{ food_ev | list | count() }}
    attributes:
      scheduled_events: |
        {{ food_ev | list }}
      next_collection_date: |
        {{ (food_ev | first).start }}
      countdown: |
        {{ int((as_timestamp((food_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

  1. Used the developer tools to check and reload all yaml configuration.

  2. Used the developer tools to check the state of sensor.poubelle_verte_template and observe that it is Unknown:

EDIT: I should probably have created a new topic and referenced this one. Is it ok to continue here, though?

Your attributes aren’t handling the lists properly… there are likely errors to that fact in you logs.

# template.yaml
  - trigger:
      - platform: time_pattern
        seconds: /30
    action:
      - action: calendar.get_events
        data:
          duration:
            days: 14
        target:
          entity_id: calendar.poubelles
        response_variable: waste_events
      - variables:
          all_events: "{{ waste_events['calendar.poubelles'].events }}"
          food_ev: "{{ all_events | selectattr('summary', 'search', 'Poubelle verte') | list }}"
    sensor:
      - name: Trash Night template
        state: "{{ food_ev | count }}"
        attributes:
          scheduled_events: "{{ food_ev }}"
          next_collection_date: "{{ (food_ev | first).start }}"
          countdown: |
            {{ int((as_timestamp((food_ev | first).start) - as_timestamp(today_at('00:00')))/86400)}}

It works, thank you!

In the meantime, I tried something different last night and I’d like to share it since I believe it is simpler:

#Prochaine poubelle
  - trigger:
    - platform: time_pattern
      minutes: /15
    sensor:
      - name: Prochaine poubelle
        state: |
          {{ ((state_attr('calendar.poubelles', 'start_time') | as_timestamp - now() | as_timestamp)/(60*60)) | round(1) }}
        attributes:
          couleur: |
            {{ state_attr('calendar.poubelles', 'message') }}

I get one sensor telling me when (state) is the next trash collection in hours and which trash it is (attribute):

And I can also use one mushroom template badge for all three trash types which reminds me to get the trash out 18 hours before collection:

type: custom:mushroom-template-badge
entity: sensor.prochaine_poubelle
content: '{{ state_attr(entity, "couleur") }}'
icon: mdi:trash-can-outline
color: |-
  {% if (state_attr(entity, "couleur") == "Poubelle verte") %}
    green
  {% elif (state_attr(entity, "couleur") == "Poubelle jaune") %}
    jaune
  {% elif (state_attr(entity, "couleur") == "Poubelle grise") %}
    dimgrey
  {% else %}
    red
  {% endif %}
visibility:
  - condition: numeric_state
    entity: sensor.prochaine_poubelle
    below: 18

image

As long as you are aware of the limitations of the method and tailor your calendar to work around them you can use the calendar entity’s state object. This method will only be reliable with calendars whose events never overlap (including All-day events).

Does it show I’m not a coder?

You lost me. It’s unlikely I’ll have two collections overlapping but I’d like to understand your point: I should not use state_attr('calendar.poubelles', 'start_time') but rather perform a calendar.get_events action and then filter the results to extract my data?

My point is just that you should be aware of the limitations of the method you posted. Use whichever method meets your needs and situation.

If your calendar will not contain overlapping events, using state_attr('calendar.poubelles', 'start_time') to get the data from the state object of the calendar entity is fine.

1 Like