How to simplify this binary_sensor template, using filters with count or loops?

This code is intended as a sensor to know if today is a holiday for me, and gives True or False. It works perfectly. But the less we can say it’s the ugliest way to code this.

This code uses “ical Sensor” integration in HACS.

I have tried to use For, or using expand using filters like selectattr, | list and | count > 0 and so on. But clearly I missed something because I coudn’t figure out how to put the right pieces in the right syntax to make it work, despite my search in the docs.

Each sensor is tested to meet both conditions: the event’s date in the calendar must be today and it’s description must be one of the three elements in the list. If at least one event meet the 2 conditions then it’s a day I’m not working.

Please help me simplify in a sensible way.

- binary_sensor:
  - name: congé
    state: >
      {% if (as_timestamp(state_attr('sensor.ical_calend_event_0', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_0', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_1', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_1', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_2', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_2', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_3', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_3', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_4', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_4', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_5', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_5', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_6', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_6', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_7', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_7', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_8', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_8', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% elif (as_timestamp(state_attr('sensor.ical_calend_event_9', 'start')) | timestamp_custom('%j') == as_timestamp(now()) | timestamp_custom('%j') and state_attr('sensor.ical_calend_event_9', 'summary') in ['Congé', 'Férié', 'Vacances']) == True %}
      True
      {% else %}
      False
      {% endif %}

EDIT: Assuming the repeated condition is what you want but I simplified with what you replied in post #4


Not tested, but you can try something like

{% set result = false %}
{% for i in range(0, 10) %}
    {% if int(state_attr('sensor.ical_calend_event_' + (i | string), 'eta'), 999) <= 0 and
              state_attr('sensor.ical_calend_event_' + (i | string), 'summary') in ['Congé', 'Férié', 'Vacances']  
    %}
        {% set result = true %}
    {% endif %}
{% endfor %}
{{ result }}

I don’t have the “ical Sensor” integration installed but in order to help you I need to see an example of the value of the start attribute. Please post at least two examples.

Alternately, copy-paste the following templates into the Template Editor and let me know what it reports:

{{ state_attr('sensor.ical_calend_event_0', 'start') }}
{{ (state_attr('sensor.ical_calend_event_0', 'start') | as_datetime).date() }}

Example 1

summary: Congé
description: Test
location: ‘’
start: ‘2023-02-28T00:00:00-05:00’
end: ‘2023-03-01T00:00:00-05:00’
eta: 0
all_day: true
icon: mdi:calendar
friendly_name: Congé

Example 2

summary: Gilbert travaille
description: ‘’
location: ‘’
start: ‘2023-03-11T08:00:00-05:00’
end: ‘2023-03-11T16:00:00-05:00’
eta: 11
all_day: false
icon: mdi:calendar
friendly_name: Gilbert travaille

{{ state_attr('sensor.ical_calend_event_0', 'start') }}
gives
2023-02-28 00:00:00-05:00

{{ (state_attr('sensor.ical_calend_event_0', 'start') | as_datetime).date() }}
gives
TypeError: float() argument must be a string or a real number, not ‘NoneType’

I’m using ical and eta is always <= 0 if event has started (could be -1 or even less if it is a multiple days event) hence my solution.

I initially tried something along a similar structure but the problem is that the scope of the variable result, when set in a for loop, doesn’t reach outside the loop. Then the result will remain False no matter what.

Tried on my config, you’re right, here is my proposal:

{% set ns = namespace(result=False) %}
{% for i in range(0, 10) %}
    {% if int(state_attr('sensor.ical_calend_event_' + (i | string), 'eta'), 999) <= 0 and
              state_attr('sensor.ical_calend_event_' + (i | string), 'summary') in ['Congé', 'Férié', 'Vacances']  
    %}
        {% set ns.result = True %}
    {% endif %}
{% endfor %}
{{ ns.result }}
1 Like
- binary_sensor:
    - name: congé
      state: >
        {{ expand('sensor.ical_calend_event_0', 'sensor.ical_calend_event_1',
            'sensor.ical_calend_event_2', 'sensor.ical_calend_event_3',
            'sensor.ical_calend_event_4', 'sensor.ical_calend_event_5',
            'sensor.ical_calend_event_6', 'sensor.ical_calend_event_7',
            'sensor.ical_calend_event_8', 'sensor.ical_calend_event_9')
          | selectattr('attributes.summary', 'in', ['Congé', 'Férié', 'Vacances'] )
          | map(attribute='attributes.start') | map('as_timestamp')
          | map('timestamp_custom', '%-j') | map('int', 0)
          | select('eq', now().timetuple().tm_yday) | list | count > 0 }}

If the number of sensors can vary, this version selects all sensors containing the string “ical_calend_event_”.

- binary_sensor:
    - name: congé
      state: >
        {{ states.sensor | selectattr('object_id', 'match', 'ical_calend_event_')
          | selectattr('attributes.summary', 'in', ['Congé', 'Férié', 'Vacances'] )
          | map(attribute='attributes.start') | map('as_timestamp')
          | map('timestamp_custom', '%-j') | map('int', 0)
          | select('eq', now().timetuple().tm_yday) | list | count > 0 }}

EDIT 1

I just noticed that there’s an eta attribute whose value is 0 if the event occurs today. This can be used to simplify the template.

- binary_sensor:
    - name: congé
      state: >
        {{ states.sensor | selectattr('object_id', 'match', 'ical_calend_event_')
          | selectattr('attributes.summary', 'in', ['Congé', 'Férié', 'Vacances'] )
          | selectattr('attributes.eta', 'le', 0) | list | count > 0 }}

or

- binary_sensor:
    - name: congé
      state: >
        {{ expand('sensor.ical_calend_event_0', 'sensor.ical_calend_event_1',
            'sensor.ical_calend_event_2', 'sensor.ical_calend_event_3',
            'sensor.ical_calend_event_4', 'sensor.ical_calend_event_5',
            'sensor.ical_calend_event_6', 'sensor.ical_calend_event_7',
            'sensor.ical_calend_event_8', 'sensor.ical_calend_event_9')
          | selectattr('attributes.summary', 'in', ['Congé', 'Férié', 'Vacances'] )
          | selectattr('attributes.eta', 'le', 0) | list | count > 0 }}

EDIT 2

Correction. Added missing closing parenthesis.

EDIT 3

Correction. Prepended attributes. to all attributes.

1 Like

This is exactly the kind of coding I wanted to learn. But still there seem to be a syntax error I guess. Even with the missing parenthese corrected, for all versions your provided I get the same error:

UndefinedError: ‘homeassistant.helpers.template.TemplateState object’ has no attribute ‘summary’

That’s due to my mistake. I forgot to reference each attribute name with “attributes.” and that’s why Home Assistant complained “has no attribute ‘summary’”. I’ve corrected all four examples above. Test either one (third one is the most concise) and let me know if there are additional errors. Easiest way to test it is, of course, by copy-pasting the template into the Template Editor.

This is wonderful. All four versions work. This is going to be a case study for me and will improve my general skills with Home Assistant. I’m really happy! Thank you a lot.

1 Like

I wish I could mark more than one as a solution. I confirm that this works! How wonderful is that! I read somewhere else on this forum that there was no way for a variable to escape from a loop. Apparently, that was wrong. I will still have to research and study how it actually works. That will be very useful for me to know this is a possible way to go.

Thank you a lot!

A Jinja2 variable defined outside of a for loop can be assigned a different value inside the for loop but that value only exists inside the for loop. Outside of the for loop, the variable’s value remains unchanged by whatever may have been done inside the for loop.

Obviously this presents a problem when you want the for loop to modify the variable’s value. That’s where namespace is useful. It allows you to define a Jinja2 variable that can be assigned a value inside a for loop that is retained outside of the for loop.

You can test it for yourself:

{% set x = 0 %}
{% set ns = namespace(y=0) %}

{%- for i in range(0, 5) %}
  {% set x = x + 1 %}
  {% set ns.y = ns.y + 1 %}
{% endfor -%}
x: {{ x }}
y: {{ ns.y }}

3 Likes