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) %}
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.
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
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.
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.
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.
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.
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.
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.:
Thats all fine, but it doesnât solve the problem of turning an entityâs state string into a datetime object, which is what this request is all about.
I realize this post is 4 years old now, but I came across it while in search of a timezone fix. The timezone fix utilizes as_datetime (in the Templating - Home Assistant docs right above as_timestamp) to convert a string into a datetime object, per your request: