Need help calculating time in a template

I have a sensor that reports the last time a device_tracker’s location changed and I’m trying to calculate the time that has passed since then (eg: now - last_changed), but I’m not a Jinja Ninja. :sweat:

This is essentially my starting point:


{{ now() }} - {{ states.sensor.husband_s_moto_x4.attributes.last_changed }}

…and I think I’ve made some progress:


{{ as_timestamp(now()) - as_timestamp(states.sensor.husband_s_moto_x4.attributes.last_changed) }}

…but I can’t figure out how to format/filter the result into a human-friendly form. :thinking:

Can any of you Jinja Warriors :sunglasses: offer some advice?

EDIT: Added code to make replies easier.

try using the

device_class: timestamp

see: https://www.home-assistant.io/components/sensor/#device-class

there’s also this:

      hassio_online:
        friendly_name: Hassio online
        value_template: >
          {{ relative_time(states.binary_sensor.hassio.last_changed) }}
1 Like

Ah, formatting after-the-fact (outside of this template) hadn’t even crossed my mind.
However (which I failed to clarify in the OP :pensive:), I’m planning to combine this template with other (non-timestamp) templates as attributes in a template_sensor. If I’m understanding correctly this would not work with your solution would it, Mariusthvdb?

not really sure what you are planning to do, but yes, both the device_class and the ‘relative’ work on date time objects only.

If you show us what you want to add to the template, we can see how to help.

1 Like

Take a look at this

1 Like

My initial goal was to incorporate states and attributes from several components into a single template_sensor, but after some fiddling I decided that was too limiting for my use-case – which is to display the data in a more-info popup on a google static map (generic camera on a picture_element card) that shows the locations of our family members. So instead I installed thomasloven/ lovelace-popup-card and I’m creating individual template_sensors to populate it.

Good info, I wasn’t too clear on how that functioned. Wouldn’t the device_class option convert the output to a timestamp though?
I’m not familiar with the relative_time function and didn’t find any documentation after a cursory search.

After a little more playing I think I’ve made some more progress using utcnow to isolate days / hours / minutes:

{{ (utcnow().day) - (states.sensor.wife_s_moto_x4.last_changed).day }}
{{ (utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour }}
{{ (utcnow().minute) - (states.sensor.wife_s_moto_x4.last_changed).minute }}

I can now perform the calculations separately then reassemble the outputs within the template to display as though they were a single string. This also allows me to omit useless info (zeros) and properly syntax labels (1 hour, X hours, etc.). So this is an example of what I have so far:

sensor.yaml

  - platform: template
    sensors:
      wifes_time_at_location:
        value_template: >
            {% if (states.sensor.wife_s_moto_x4.last_changed) == 'None' %}
            Updating...
            {% elif ((utcnow().day) - (states.sensor.wife_s_moto_x4.last_changed).day) == 0 %}
            {% elif ((utcnow().day) - (states.sensor.wife_s_moto_x4.last_changed).day) == 1 %}
            1 day, 
            {% elif ((utcnow().day) - (states.sensor.wife_s_moto_x4.last_changed).day) >= 1 %}
            {{ (utcnow().day) - (states.sensor.wife_s_moto_x4.last_changed).day }} days, 
            {%- endif %}
            {%- if ((utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour) == 0 %}
            {% elif (((utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour) >= 1 + ((utcnow().minute) - (states.sensor.wife_s_moto_x4.last_changed).minute) < 1) %}
            1 hour
            {% elif ((utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour) == 1 %}
            1 hour & 
            {% elif ((utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour) >= 1 %}
            {{ (utcnow().hour) - (states.sensor.wife_s_moto_x4.last_changed).hour }} hours & 
            {%- endif %}
            {% if ((utcnow().minute) - (states.sensor.wife_s_moto_x4.last_changed).minute) == 1 %}
            1 minute
            {% elif ((utcnow().minute) - (states.sensor.wife_s_moto_x4.last_changed).minute) >= 1 %}
            {{ (utcnow().minute) - (states.sensor.wife_s_moto_x4.last_changed).minute }} minutes
            {%- endif %}
        icon_template: mdi:timer
        friendly_name: Time at Location

I’m not sure I’ve covered all necessary calculations yet – it’s a WIP.

I’m also experiencing some really unusual behavior: Templates that calculate correctly in the Template Editor will sometimes calculate incorrectly when copy/pasted into the template_sensor. Have you encountered anything like that before?

Wow, LOTS of info there! Much of it over my head right now, but thanks for the resource.

Here’s an example of the weird behavior I’m seeing:

template_sensor shows 3 miles:

Exact same code copied/pasted into Template Editor shows (correctly) less than 1 mile:

Here’s the codeblock:

  - platform: template
    sensors:
      wifes_distance_from_home:
        value_template: >
            {% if (distance(float(states.device_tracker.zy225dsn4h_2.attributes.latitude), float(states.device_tracker.zy225dsn4h_2.attributes.longitude), XX.XXXXX, XX.XXXXX) | round(0)) <= 1 -%}
            Less than 1 mile
            {% elif (distance(float(states.device_tracker.zy225dsn4h_2.attributes.latitude), float(states.device_tracker.zy225dsn4h_2.attributes.longitude), XX.XXXXX, XX.XXXXX) | round(0)) == 1 -%}
            About 1 mile
            {% elif (distance(float(states.device_tracker.zy225dsn4h_2.attributes.latitude), float(states.device_tracker.zy225dsn4h_2.attributes.longitude), XX.XXXXX, XX.XXXXX) | round(0)) >= 1 -%}
            About {{ distance(float(states.device_tracker.zy225dsn4h_2.attributes.latitude), float(states.device_tracker.zy225dsn4h_2.attributes.longitude), XX.XXXXX, XX.XXXXX) | round(0) }} miles
            {% endif %}
        icon_template: >
            {% if is_state('sensor.wife_s_moto_x4', 'home') %}
            mdi:home
            {% elif (distance(float(states.device_tracker.zy225dsn4h_2.attributes.latitude), float(states.device_tracker.zy225dsn4h_2.attributes.longitude), XX.XXXXX, XX.XXXXX) | round(0)) >=1 %}
            mdi:steering
            {% else %}
            mdi:steering-off
            {% endif %}
        friendly_name: Distance from Home

:thinking:

Edit: added code, fixed typo
Edit 2: redacted lon/lat

While posting I noticed a(nother?) mistake: I have if ... <= 1 folowed by elif ... == 1. :roll_eyes:
I’ve corrected it now to if ... < 1elif ... == 1.

Ah, I think I just realized what was causing the strange behavior: I did not have an entity_id referenced to trigger updating the template_sensor. Per the docs. :persevere:

I’ve now added:
entity_id: device_tracker.zy225dsn4h_2

That’s only required if the sensor can’t determine an entity to update from.

I’m pretty sure if that is the case then there will be a warning printed in the log. At least that used to be true. If there is no warning then I don’t think that is a problem. But it also can’t hurt to put it in either.

1 Like

since your calculating proximities, you might have a look at integration Proximity :wink: makes for easier templates/automations using these as entities in your config.

1 Like
  1. You should use params there. If your states change during the execution of the template, you’ll get odd results. The chances of this are low, but it can happen the way you are doing it.
  2. Lat and Lon are floats inside attributes, no need to convert.
  3. Rounding in the if statement to zero will remove the decimal place, this means you’ll only get results that fall on the whole number. Meaning anything less than 0.5 miles will be zero, everything between 0.5 and 1.0 will be 1. Which leads to the next point. You should round to 1st decimal place instead.
  4. Your elif == 1 was never being hit because the if statement before it if <= 1 will get hit first.
  5. You can combine == 1 and > 1 into 1 statement and programatically add the plural letter s.
  - platform: template
    sensors:
      wifes_distance_from_home:
        value_template: >
            {% set dist = distance(state_attr('device_tracker.zy225dsn4h_2','latitude'), state_attr('device_tracker.zy225dsn4h_2','longitude'), XX.XXXXX, XX.XXXXX) | round(1) %}
            {% if dist < 1 -%}
            Less than 1 mile
            {% else -%}
            About {{ dist | round(0) }} mile{{ '' if dist | round(0) == 1 else 's' }}
            {% endif %}
        icon_template: >
            {% set dist = distance(state_attr('device_tracker.zy225dsn4h_2','latitude'), state_attr('device_tracker.zy225dsn4h_2','longitude'), XX.XXXXX, XX.XXXXX) | round(1) %}
            {% if is_state('sensor.wife_s_moto_x4', 'home') %}
            mdi:home
            {% elif dist >= 1 %}
            mdi:steering
            {% else %}
            mdi:steering-off
            {% endif %}
        friendly_name: Distance from Home
1 Like

@finity: Adding entity_id seems to have fixed the updating problem so far. I guess the sensor couldn’t detect any entities because they’re all defined within templates that it can’t decipher…? I have some other sensors in this set (time_at_location, battery_charge, etc.) where I used entity_id: sensor.time to update every minute.

@ Mariusthvdb: Yes, excellent recommendation! I had forgotten about Proximity but it’s now added to my todo list. Thank you.

@ petro:

I’m not sure where you’re referring to. :thinking:

  1. Lat and Lon are floats inside attributes, no need to convert.

I don’t know why I didn’t notice that; Lat & Lon must be the most obvious examples of floats one would encounter! :sweat_smile:

  1. …anything less than 0.5 miles will be zero, everything between 0.5 and 1.0 will be 1…You should round to 1st decimal place instead.

Hmm. I rounded to zero because I don’t care to have the smaller units displayed, but could that somehow throw off my calculations? It doesn’t seem like it would (in this context) since the rounding is performed after calculating, but maybe it would be a better practice for future usage (such as Proximity)…?

  1. Your elif == 1 was never being hit because the if statement before it if <= 1 will get hit first.

I noticed and corrected that already, but good catch …again! Thanks.

    5.

  - platform: template
    sensors:
      wifes_distance_from_home:
        value_template: >
            {% set dist = distance(state_attr('device_tracker.zy225dsn4h_2','latitude'), state_attr('device_tracker.zy225dsn4h_2','longitude'), XX.XXXXX, XX.XXXXX) | round(1) %}
            {% if dist < 1 -%}
            Less than 1 mile
            {% else -%}
            About {{ dist | round(0) }} mile{{ '' if dist | round(0) == 1 else 's' }}
            {% endif %}
        icon_template: >
            {% set dist = distance(state_attr('device_tracker.zy225dsn4h_2','latitude'), state_attr('device_tracker.zy225dsn4h_2','longitude'), XX.XXXXX, XX.XXXXX) | round(1) %}
            {% if is_state('sensor.wife_s_moto_x4', 'home') %}
            mdi:home
            {% elif dist >= 1 %}
            mdi:steering
            {% else %}
            mdi:steering-off
            {% endif %}
        friendly_name: Distance from Home

Oh this is just beautiful!!! :star_struck:
So much more elegant – thank you! :sunglasses:

Edit: fixed typo

2 Likes

I meant variables. Using {% set xxxx = .... %} means you’ll only access the state machine in that line essentially. This forces you to get the answer 1 time instead of multiple times. As the code executes, it doesn’t get a new number each time the function is called. It just gets the number once. Like I said before, it would be extremely rare that you run into differing numbers, but it could happen. So why not avoid it?

1 Like

Ah OK, that makes sense (after being explained :sweat_smile:).

I have several other sensors I now plan on adapting your code to as well. Thank you so much; you da man! :sunglasses:

Working beautifully!

Map that buttons were placed onto (pre-existing):
map

Wife’s popup card:
B

My popup card:
J

codeblock for sensors:

###### Text in ALL CAPS needs to be updated per end-user's specific configuration and preferences.
###### GPS Device Tracker required <https://www.home-assistant.io/components/device_tracker/>
###### Home Assistant Community Thread <https://community.home-assistant.io/t/need-help-calculating-time-in-a-template/130033/16?u=jparthum>
###### Big thanks to 'petro' for cleaning, correcting and making code more concise <https://community.home-assistant.io/u/petro>

### Component to integrate with OpenStreetMap Reverse Geocode (PLACE) <https://github.com/custom-components/places>
  - platform: places
    name: PLACES SENSOR
    devicetracker_id: device_tracker.XXXXXX
    options: zone,place
    map_provider: google
    home_zone: zone.HOME
    api_key: API KEY

### Place Type (Creates new entity: "sensor.place_type")
  - platform: template
    sensors:
      place_type:
        entity_id: sensor.PLACES_SENSOR_NAME
        value_template: >
            {{ states.sensor.PLACES_SENSOR_NAME.attributes.place_type | capitalize }}
        icon_template: mdi:label
        friendly_name: FRIENDLY NAME # <-- (Named "Place Type" in example)

### Address (Creates new entity: "sensor.formatted_address")
  - platform: template
    sensors:
      formatted_address:
        entity_id: sensor.PLACES_SENSOR_NAME
        value_template: >
            {% set street_number = states.sensor.PLACES_SENSOR_NAME.attributes.street_number %}
            {% set street = states.sensor.PLACES_SENSOR_NAME.attributes.street %}
            {% set city = states.sensor.PLACES_SENSOR_NAME.attributes.city %}
            {% set zip = states.sensor.PLACES_SENSOR_NAME.attributes.postal_code %}
            {{ '(unknown number)' if street_number == '-' else street_number }} {{ '(unknown street)' if street == '-' else street }}, {{ '(unknown city)' if city == '-' else city }} [{{ '(unknown ZIP)' if zip == '-' else zip }}]
        icon_template: mdi:crosshairs
        friendly_name: FRIENDLY NAME # <-- (Named "Address" in example)

### Distance from Home (Creates new entity: "sensor.distance_from_home")
  - platform: template
    sensors:
      distance_from_home:
        value_template: >
            {% set dist = distance(state_attr('device_tracker.XXXXXX','latitude'), state_attr('device_tracker.XXXXXX','longitude'), XX.XXXXXX, -XX.XXXXXX) | round(1) %} # <-- "XX.XXXXXX" = Home Latitude, "-XX.XXXXXX" = Home Longitude
            {% if dist < 1 -%}
            Within 1 mile
            {% else -%}
            About {{ dist | round(0) }} mile{{ '' if dist | round(0) == 1 else 's' }}
            {% endif %}
        icon_template: >
            {% if is_state('device_tracker.XXXXXX', 'home') %}
            mdi:home
            {% else %}
            mdi:steering{{ '-off' if distance(state_attr('device_tracker.XXXXXX','latitude'), state_attr('device_tracker.XXXXXX','longitude'), XX.XXXXXX, -XX.XXXXXX) | round(1) < 1 }}# <-- "XX.XXXXXX" = Home Latitude, "-XX.XXXXXX" = Home Longitude
            {% endif %}
        friendly_name: FRIENDLY NAME # <-- (Named "Distance from Home" in example)

### Time at Location (Creates new entity: "sensor.time_at_location")
  - platform: template
    sensors:
      time_at_location:
        entity_id: sensor.time
        value_template: >
            {% set day = ((utcnow().day) - (states.device_tracker.XXXXXX.last_updated).day) %}
            {% set hour = ((utcnow().hour) - (states.device_tracker.XXXXXX.last_updated).hour) %}
            {% set minute = ((utcnow().minute) - (states.device_tracker.XXXXXX.last_updated).minute) %}
            {%- if day < 1 -%}
            {%- else -%}
            {{ day }} Day{{ '' if day == 1 else 's' }}
            {%- endif -%}
            {{ ' & ' if day >= 1 and hour >= 1 and minute == 0 }}{{ ', ' if day >= 1 and hour >= 1 and minute >= 1 }}
            {%- if hour < 1 -%}
            {%- else -%}
            {{ hour }} Hour{{ '' if hour == 1 else 's' }}
            {%- endif -%}
            {{ ' & ' if day >= 1 and hour == 0 and minute >= 1 or day == 0 and hour >= 1 and minute >= 1 or day >= 1 and hour >= 1 and minute >= 1 }}
            {%- if minute < 1 -%}
            {%- else -%}
            {{ minute }} Minute{{ '' if minute == 1 else 's' }}
            {%- endif %}
        icon_template: mdi:timer
        friendly_name: FRIENDLY NAME # <-- (Named "Time at Location" in example)

### Battery Charge (Creates new entity: "sensor.battery_charge")
  - platform: template
    sensors:
      battery_charge:
        entity_id: sensor.time
        value_template: "{{ states.device_tracker.XXXXXX.attributes.battery_level | round }}%"
        icon_template: >
            {% set batt_level = states.device_tracker.XXXXXX.attributes.battery_level | round() -%}
            mdi:battery
            {%- if batt_level >= 95 -%}
            {%- elif batt_level >= 85 -%}
            -90
            {%- elif batt_level >= 75 -%}
            -80
            {%- elif batt_level >= 65 -%}
            -70
            {%- elif batt_level >= 55 -%}
            -60
            {%- elif batt_level >= 45 -%}
            -50
            {%- elif batt_level >= 35 -%}
            -40
            {%- elif batt_level >= 25 -%}
            -30
            {%- elif batt_level >= 15 -%}
            -20
            {%- elif batt_level >= 5 -%}
            -10
            {%- elif batt_level < 5 -%}
            -outline
            {%- else -%}
            -unknown
            {% endif %}
        friendly_name: FRIENDLY NAME # <-- (Named "Battery Charge" in example)

### Last Updated (Creates new entity: "sensor.last_updated")
  - platform: template
    sensors:
      last_updated:
        entity_id: sensor.time
        value_template: >
            {% set day = ((utcnow().day) - (states.device_tracker.XXXXXX.last_updated).day) %}
            {% set hour = ((utcnow().hour) - (states.device_tracker.XXXXXX.last_updated).hour) %}
            {% set minute = ((utcnow().minute) - (states.device_tracker.XXXXXX.last_updated).minute) %}
            {%- if day < 1 -%}
            {%- else -%}
            {{ day }} Day{{ '' if day == 1 else 's' }}
            {%- endif -%}
            {{ ' & ' if day >= 1 and hour >= 1 and minute == 0 }}{{ ', ' if day >= 1 and hour >= 1 and minute >= 1 }}
            {%- if hour < 1 -%}
            {%- else -%}
            {{ hour }} Hour{{ '' if hour == 1 else 's' }}
            {%- endif -%}
            {{ ' & ' if day >= 1 and hour == 0 and minute >= 1 or day == 0 and hour >= 1 and minute >= 1 or day >= 1 and hour >= 1 and minute >= 1 }}
            {%- if minute < 1 -%}
            {%- else -%}
            {{ minute }} Minute{{ '' if minute == 1 else 's' }}
            {%- endif %} ago
        icon_template: >
            mdi:clock{{ '-alert' if (utcnow().day) - (states.device_tracker.XXXXXX.last_updated).day >= 1 or (utcnow().day) - (states.device_tracker.XXXXXX.last_updated).hour >= 12 }}
        friendly_name: FRIENDLY NAME # <-- (Named "Last Updated" in example)

I may be looking at this wrong but shouldn’t “time_at_location” use last_changed instead of last_updated?

Good catch, @Bartem. I ran out of room on the card and stopped using that particular sensor shortly after this was posted, so I can’t confirm or dispute which works best. I seem to recall encountering inaccuracies with one of them due to the nature of GPS tracker reporting but my memory is vague. :confused:

Thanks for sharing your work… I ended using most of your sensors, and used the formatted address one as the basis for something kind of different but you saved me a ton of work and helped lead me in the right direction!

1 Like