It takes way too much effort to get a datetime object through jinja


#1

There is a small but very significant gap in the Jinja functions when trying to do any kind of Date manipulation for template sensors. It takes a lot of code gymnastics to get a datetime object out from an attribute that has a date string, to then do comparison to something like now()

To give a concrete use case, I am tweening my light brightness between dawn and midday, then back down through midday to dusk, so that the lights ramp up and down throughout the day - mimicking the sun.

Part of the problem is that all attributes are strings; so sun.sun.attributes.next_dawn comes back as - for example “2018-01-14T16:51:12+00:00”. We can use strptime to convert this into a date but that is far from easy - as strptime itself is quite limiting - it doesn’t offer a parser for +00:00 as an offset, for example (it will only parse +0000).

This is the code I’ve resorted to to try to parse a sun time to a datetime object:

{% set dawn = strptime(states.sun.sun.attributes.next_dawn.replace('+00:00',''), '%Y-%m-%dT%H:%M:%S').replace(tzinfo=now().tzinfo) %}

I think if there could be a function to reduce down the amount of needed boilerplate, that would be great! Perhaps:

{% set dawn = from_timestamp(states.sun.sun.attributes.next_dawn) %}

#2

I started home automation as a hobby in 2006 using Misterhouse. Less than two years later, I switched to Premise and, a decade later, continue to use it. Last summer I installed openHAB and Home Assistant to learn more about what these two leading open-source solutions have to offer.

Based on my experiences, the one sore-point in Home Assistant is precisely what you have identified, namely the absence of simple date-math functions. In all other systems I’ve used, there was a straightforward way to do date-arithmetic without resorting to awkward string-parsing techniques. Ideally, you want the ability to do basic things like subtract X hours from a given time, or X days from the current date, or other simple arithmetic operations for scheduling purposes.

I am not well-versed with Jinja2 but I believe it treats everything as a string and therefore the need to cast as int or float to ensure arithmetic operations are handled correctly. Ideally, there ought to be a way to cast as time so you could easily subtract two dates to get the number of intervening days. If not that then offering one or more functions, as you’ve suggested, to handle basic date and time arithmetic.


#3

I think the as_timestamp does what you want. see Templating

Example:

Next Dawn: {{ states.sun.sun.attributes.next_dawn }}
Timestamp Dawn: {{ as_timestamp(states.sun.sun.attributes.next_dawn) }}
Local Dawn: {{ as_timestamp(states.sun.sun.attributes.next_dawn) | timestamp_local }}

{% if as_timestamp(states.sun.sun.attributes.next_dawn) >= as_timestamp(now()) %}
Next dawn is in the future
{% else %}
Next dawn is in the past (which should be impossible!)
{% endif %}

Rendered:

Next Dawn: 2019-01-15T13:51:25+00:00
Timestamp Dawn: 1547560285.0
Local Dawn: 2019-01-15 06:51:25


Next dawn is in the future

#4

I think the documentation for the various template extensions could use some improvement. I knew it was possible via the timestamp functions based on some stuff I had done previously, but just based on the documentation I couldn’t figure out how to do it for sure. It even took looking at the code to figure out that as_timestamp returns a float that’s suitable for comparisons like this.


#5

Sadly as_timestamp wont work because it returns a float, and I’m doing date manipulation not just comparison.


#6

Would this work for what you are trying to achieve?


#7

Why can’t you manipulate timestamps?

Subtract one hour - subtract 3600. You need to think in seconds :slight_smile:


#8

From this post, it’s also possible to convert using .hour or .minute then multiplying everything by the appropriate ‘magic number’ (some appropriate multiple of 60) to convert to hours/minutes/seconds and then proceed to perform whatever arithmetic is needed.

now().hour * 3600 + now().minute * 60 + now().second

Still fugly.


#9

Thanks for all of your suggestions. I’m very aware that I can manipulate timestamp numbers, and my code does do that in places - but in this specific case doing math on numbers makes things more complicated, not less. I’ll show my full code, and you can see why date math is needed:

{% set morning = now().replace(hour=0,minute=0,second=0) %}
{% set dawn = strptime(states.sun.sun.attributes.next_dawn.replace('+00:00',''), '%Y-%m-%dT%H:%M:%S').replace(year=now().year,month=now().month,day=now().day,tzinfo=now().tzinfo) %}
{% set dusk = strptime(states.sun.sun.attributes.next_dusk.replace('+00:00', ''), '%Y-%m-%dT%H:%M:%S').replace(year=now().year,month=now().month,day=now().day,tzinfo=now().tzinfo) %}
{% set noon = strptime(states.sun.sun.attributes.next_noon.replace('+00:00', ''), '%Y-%m-%dT%H:%M:%S').replace(year=now().year,month=now().month,day=now().day,tzinfo=now().tzinfo) %}
{% set midnight = now().replace(hour=23,minute=59,second=59) %}
{% set maxb = states.input_number.max_brightness.state | float %}
{% set minb = states.input_number.min_brightness.state | float %}
{% set time = now() -%}
{% if time > morning and time < dawn -%}
  {{ minb }}
{% elif time > dawn and time < noon -%}
  {% set elapsed = as_timestamp(time) - as_timestamp(dawn) -%}
  {% set total = as_timestamp(noon) - as_timestamp(dawn) -%}
  {% set desired = 100 * (elapsed/total) * ((elapsed/total)) + minb %}
  {{ ((desired, maxb) | min, minb) | max }}
{% elif time > noon and time < dusk -%}
  {{ maxb }}
{% elif time > dusk and time < midnight -%}
  {% set elapsed = as_timestamp(time) - as_timestamp(dusk) -%}
  {% set total = as_timestamp(midnight) - as_timestamp(dusk) -%}
  {% set desired = maxb - (-maxb * (elapsed/total) * ((elapsed/total) - 2)) %}
  {{ ((desired, maxb) | min, minb) | max }}
{% endif -%}

As you can see, I take the next_dawn time from sun.sun and apply the time to today. As soon as today’s dawn has passed, sun.sun sets next_dawn to tomorrow’s dawn, which is unusable for my purposes, as I need the dawn from today. That isn’t a case of simply executing as_timestamp(states.sun.sun.attributes.next_dawn) - 86400 because that could get me yesterdays dawn time also.

Also thanks to @jimpower for suggesting that component; it looks great however I don’t think it’ll fit my particular set of needs.


#10

Take a look at @pnbruckner custom sun.sun component which gives you next sunrise etc…


#11

The issue you’re having parsing a date & time string is not Jinja’s fault. That is a well known Python issue; i.e., it generates strings from datetimes that it can’t parse. (At least, that’s what I found when I looked into this some time back. I guess I don’t remember all the details.) Jinja is just exposing Python features for you to use.

You can work around the issue by using .replace('+00:','+00') (assuming, of course, the date & time string is in UTC, which sun.sun’s attributes are.) E.g.:

{% set dawn = strptime(states.sun.sun.attributes.next_dawn.replace('+00:','+00'), '%Y-%m-%dT%H:%M:%S%z') %}

If you’re interested in my enhanced sun component, you can check out its doc page.


#12

Python has a datetime module that would help simplify some often-used date arithmetic. For example, timedelta can provide the difference in two dates in whatever intervals (days, hours, minutes, seconds) you desire. Sure would be nice to have access to it within a Jinja template.


#13

As I said, Jinja is effectively exposing some Python. E.g., now() actually returns a Python timezone aware datetime object, and you can therefore use all of its usual methods, such as replace. Also, if you subtract two datetime objects, you get a Python timedelta object. E.g.:

{% set t1 = now() %}
{% set t2 = t1.replace(hour=t1.hour+1) %}
{{ t2 - t1 }}
{{ (t2 - t1).total_seconds() }}

Results in:

1:00:00
3600.0