Last_changed in lovelace, fix timezone

I have a markdown card in lovelace that shows when the front door was last opened, but it’s one hour off.

content: >-
  {{
  states.binary_sensor.homematic_door_sensor_state.last_changed.strftime('%H:%M
  Uhr (%d.%m.%Y)') }}
title: Tür zuletzt geöffnet
type: markdown

Bildschirmfoto 2020-02-06 um 08.41.41

As stated here by @pnbruckner, “last_changed” is not timezone aware. But I cannot find out how to adjust the timezone in lovelace. I was able to change the string by doing this:

{{ states.binary_sensor.homematic_door_sensor_state.last_changed|regex_replace(find='\+00:00', replace='+01:00') }}

but then I cannot use strftime on the result. Any ideas how to solve this in an elegant way?

This should work

content: >-
  {{ as_timestamp(states.binary_sensor.homematic_door_sensor_state.last_changed) | timestamp_custom("%H:%M  Uhr (%d.%m.%Y)") }}
title: Tür zuletzt geöffnet
type: markdown
3 Likes

This works! So I assume “as_timestamp” is aware of the time zone?

as_timestamp() converts timestamp strings and datetime objects to an integer. It represents seconds from the beginning of time (in code, that’s 1970). That is UTC. But the timestamp_custom converts timestamp integers to your local time format.

1 Like

You misunderstood what I said. I was talking about the last_changed field from a database query. I was not talking about the last_changed field of a State Object. The former is a string with no time zone suffix. The latter is a Python time zone aware datetime object in UTC.

So, when you used the strftime method, you were formatting a UTC time for output. You’d have to convert the datetime object to your local time zone first to get the result you were looking for. @petro’s solution basically does that via different, but equivalent, mechanisms. To be complete, you could have also done it this way:

content: >-
  {{
  states.binary_sensor.homematic_door_sensor_state.last_changed.astimezone()
  .strftime('%H:%M Uhr (%d.%m.%Y)') }}
title: Tür zuletzt geöffnet
type: markdown

The astimezone method converts a time zone aware datetime object to another time zone, adjusting the date & time data accordingly. The default behavior (i.e., with no arguments specified) converts to the “system local timezone.”

2 Likes

I’ll just drop this here, as I have spent hours debugging time-issues - especially when it comes to comparing times from different sources.
So here is a summary I have learned.

This is what (some of) the different options you have using jinja2 for time-conversions will return.
Every option here returns the number of seconds since Unix Epoch, but with or without taking local timezone into consideration, and also some returns a float, and some returns a string so make sure you compare apples to apples. If you need to do any arithmetic, you also need to convert the strings to int or float before adding or subtracting seconds.

now:

UTC float    {{ now().timestamp() }}
Local string {{ now().timestamp() | timestamp_custom("%s") }}
Local string {{ now().strftime('%s') }}
Local string {{ now().astimezone().strftime('%s') }}
UTC float    {{ now().astimezone().timestamp() }}

last_changed

UTC float    {{ states.binary_sensor.mysensor.last_changed.timestamp() }}
Local string {{ states.binary_sensor.mysensor.last_changed.timestamp() | timestamp_custom("%s") }}

Local string {{ states.binary_sensor.mysensor.last_changed.strftime("%s") }}
UTC string   {{ states.binary_sensor.mysensor.last_changed.strftime("%s") | timestamp_custom("%s") }}
Local string {{ states.binary_sensor.mysensor.last_changed.strftime("%s") | int | timestamp_custom("%s") }}
Local string {{ states.binary_sensor.mysensor.last_changed.strftime("%s") | float | timestamp_custom("%s") }}

Local string {{ states.binary_sensor.mysensor.last_changed.astimezone().strftime("%s") }}
UTC float    {{ states.binary_sensor.mysensor.last_changed.astimezone().timestamp() }}

input_datetime

UTC float    {{ state_attr('input_datetime.mydatetime', 'timestamp') }}
Local string {{ state_attr('input_datetime.mydatetime', 'timestamp') | timestamp_custom("%s") }}

And just something to be aware of. If you have an input_datetime without date (eg. has_date: false) that you use to manipulate other time-entries (like adding X minutes to some timestamp in a script or automation)

{{ states('input_datetime.onlytime') }}
  • Returns a string (eg. “00:10:00”)
{{ state_attr('input_datetime.onlytime', 'timestamp') }}
  • Returns a float - the number of seconds that you would expect (e.g. “600” in this case)

But if you do some manipulation of this. Something like

{{ state_attr('input_datetime.onlytime', 'timestamp') | timestamp_custom("%s") }}
  • You will get back a string, where the local timeset-offset is added (or subtracted) to/from the number of seconds. So if you are in UTC+1, you will get back 600 + 3600 = 4200 (as a string)

You can easily see what happens if you do a

{{ state_attr('input_datetime.onlytime', 'timestamp') | timestamp_custom("%Y-%m-%d %H:%M:%S") }}
  • Which returns “1970-01-01 01:10:00”

So this leads us to the following

Check if last_changed is more than ten minutes ago:
You can either compare 2 UTC floats:

{% if states.binary_sensor.mysensor.last_changed.timestamp() < (now().timestamp() - state_attr('input_datetime.onlytime', 'timestamp')) %}

Or compare 2 local strings:

{% if states.binary_sensor.mysensor.last_changed.strftime("%s") | int < (now().strftime('%s') | int - state_attr('input_datetime.onlytime', 'timestamp') | int) %}

But don’t mix them up…

1 Like