Strptime vs as_datetime

I have this automation:

- alias: "Switch On Or Extend Front Door Light When Cover Closed"
  initial_state: true
  trigger:
    - platform: event
      event_type: call_service
      event_data:
        domain: cover
        service: close_cover
  mode: queued
  condition:
    - condition: and
      conditions:
        - condition: or
          conditions:
            - condition: sun
              after: sunset
            - condition: sun
              before: sunrise
        - condition: template
          value_template: >-
            {# if the timer has already run out or it's about to, turn it on for another brief moment #}
            {% set timer = 'timer.front_door_timer' %}
            {% set finishes_at = strptime(state_attr(timer, 'finishes_at'), '%Y-%m-%dT%H:%M:%S%z', None) %}
            {{ finishes_at == None or (finishes_at - now()).seconds < 60 }}
  action:
    - service: script.turn_on
      target:
        entity_id: script.turn_on_light_and_timer
      data:
        variables:
          light: light.front_door
          duration: "00:00:30"

Basically, in the evening when someone opens the main gate or a garage door, some outside lights will turn on and a timer is started to turn it off automatically. I’ve found that the default timer period is almost always long enough to get into the house before the lights turn off. However, sometimes, I want to extend the timer to keep the lights on a little longer. The automation above does this.

Now, I recently read a comment by tom_l (moderator) that he never uses strptime anymore in favour of as_datetime (can’t recall or find the post, but I think it was in comparison with today_at). With the introduction of the defaults for many templating functions, I changed many of my templates but not yet all (the above automation is one of them), but after reading this comment, I did another scan and found this automation to check where I can improve things.

Unlike as_timestamp, one can’t specify a default for as_datetime, but you can specify it for strptime. In the case of timers, the finishes_at attribute is either a datetime or None.

I’ve come up with this alternative, which is basically what as_datetime would do if one could specify a default, but I’m not sure it’s better (if one wants to avoid the use of strptime).

            {% set finishes_at = (state_attr(timer, 'finishes_at') | as_datetime) if state_attr(timer, 'finishes_at') else None %}

I’m genuinely curious to know if there are other ways to achieve this.

EDIT: Second template got cut off when I copied and pasted.

as_datetime returns None when it fails to convert. If your datetime has a timezone (which it does), just change your template to this and it will behave the same way

            {# if the timer has already run out or it's about to, turn it on for another brief moment #}
            {% set timer = 'timer.front_door_timer' %}
            {% set finishes_at = state_attr(timer, 'finishes_at') | as_datetime %}
            {{ finishes_at == None or (finishes_at - now()).seconds < 60 }}

For converting a datetime string to a datetime object, strptime is more flexible than as_datetime.

It’s true that as_datetime doesn’t accept a default value but you can pair it with the default filter in order to provide a value when as_datetime produces none.

Copy-paste the following into the Template Editor and experiment with it:

{{ '2022-01-18T10:04:00' | as_datetime }}
{{ 'Tue Jan 18 10:04:00 2022' | as_datetime }}

{{ '2022-01-18T10:04:00' | as_datetime | default(now(), true) }}
{{ 'Tue Jan 18 10:04:00 2022' | as_datetime | default(now(), true) }}

{{ strptime('Tue Jan 18 10:04:00 2022', '%a %b %d %H:%M:%S %Y') }}

Thank you for both the insightful answers.

I forgot about the standard default filter, as I was only staring at the HA templating docs and not the Jinja2 docs. Good to be reminded of that.

That was actually the first thing I tried, but didn’t mention. In the template editor I got:

{{ state_attr('timer.front_door_timer', 'finishes_at') | as_datetime }}

And if the timer isn’t running/active:

TypeError: float() argument must be a string or a number, not 'NoneType'

Am I hitting one of those cases where the template editor is behaving differently to what would really happen?

The as_datetime filter doesn’t accept a null object (None).

In the examples I posted, none of them feed a None to as_datetime. However, in your case, state_attr() reports None if the finishes_at attribute doesn’t exist (or the timer entity doesn’t exist).

To mitigate it, you can insert a default filter between state_attr() and as_datetime. It’s your choice of what default will feed as_datetime. In the following example, it supplies a string to as_datetime which it won’t reject but also can’t convert so it reports None.

{{ state_attr('timer.front_door_timer', 'finishes_at') | default('x', true) | as_datetime }}

Going with this:

        - condition: template
          value_template: >-
            {# if the timer has already run out or it's about to, turn it on for another brief moment #}
            {% set timer = 'timer.front_door_timer' %}
            {% set finishes_at = state_attr(timer, 'finishes_at') | default(0, true) | as_datetime %}
            {{ (finishes_at - now()).seconds < 60 }}
2 Likes