Template is living it's own life, not always respecting/wrongly using it's conditions

TL;DR: time-based template sensor is changing it’s value at the time that isn’t configured in the template.

long story:
I’ve got a time-of-day template sensor based on fixed hours and on sun setting time, that I use almost everywhere across my config. the main use is of course light scenes for rooms, bathrooms etc. I’ve got an automation that based on this sensor’s change and value, triggers an action to change input_select with scenes, and then an automation that is triggered by the input_select & it runs a script for scenes change. input_selects are just because sometimes I want to change the scene by hand - and they are not relevant for the problem I’m having (I guess), so we don’t have to worry about it for now :slight_smile:

each part of day is configured as being from X:XX to Y:YY. I’m using “minutes after midnight” count, so the conditions would be easier to check. one time [day end/dusk start] is based on the time of next sun setting. there are some time shifts for weekend and dusk-based scening, but it’s just adding or substracting some minutes. there’s also a “fix” if something starts before midnight, but ends after [24 hrs = 1440 minutes are added to compensate that difference].

everything works fine at hours configured by my list, but there are some strange things happening during hours not configured in the “parts of day” list, when the sensor turns on to the “before value” - example below.

a typical, regular day:

  • OK: at 6:30, “morning” is turned on, and it should last to 9:45.
  • BUG: then, at 7:00, “sleep” pops in, of course all “sleep” automations are triggered [lights turns off etc.]
  • next, at precisely 7:00:01, “morning” comes back [everything’s “back to normal” but that one-second change makes lights blink, or some of them, turned on by hand - are kept off]
  • OK: at 9:45, “day” turns on
  • BUG: at 10:00, “morning” turns on [also with triggering the “morning” automations and routines]
  • at precisely 10:00:01, “day” comes back [again, new triggers]
  • OK: at 21:00, “evening” turns on
  • OK: at 22:45, “night 3” turns on
  • BUG: at 23:00, “evening” comes back [triggers…]
  • at precisely 23:00:01, “night 3” comes back
  • OK: at 23:30, “night 2” turns on
  • BUG: at 0:00, “night 3” comes back [triggers…]
  • OK: at precisely 0:00:01, “night 1” turns on
  • OK: at 0:30 “sleep” turns on without problems

I tried those times in the template tab of developer tools in the dashboard and it looks to be working fine… I have NO IDEA why there’s sensor change at 7:00 and 23:00 as those hours are not on the list… either I’m stupid/blind and can’t find the error in my template [despite that it works fine in template tester] or there’s something baked in the HomeAssistant that I’m not aware of. fun fact: my brother uses the same sensor [with different hour configurations] and he’s experiencing the same anomalies at 7 and 23hrs, so it’s not like the rest of my config is playing with me - our configurations are somewhat different.

my sensor’s yaml below. PLEASE somebody help in my misery…

sensor:
- platform: template
  sensors:
    0_cfg_tod:
      value_template: >-
        {# #################### current time, sensor & now() based to ensure updates -#}
        {%- set t_now = states('sensor.time_date')[:2]|int*60 + now().minute|int -%}
		{# #################### time shift for the weekend, so we can sleep/party little bit longer -#}
        {%- set t_weekend = 30 if is_state('binary_sensor.0_cfg_weekend', 'on') else 0 -%}
		{# #################### time shift for the dusk - start scene even before sunset -#}
        {%- set t_before = 45 -%}
        {# #################### parts of day -#}
        {%- set t_list = {
          0:{0:'# info ##########', 1:'# name ##########', 2:'# start #########'},
          1:{0:'# 06:30 - 09:45 #', 1:'morning',           2:(6*60)+30},
          2:{0:'# 09:45 - sett. #', 1:'day',               2:(9*60)+45},
          3:{0:'# sett. - 21:00 #', 1:'dusk',              2:((as_timestamp(state_attr('sun.sun', 'next_setting'))|timestamp_custom("%H", false)|int*60)+(as_timestamp(state_attr('sun.sun', 'next_setting'))|timestamp_custom("%M", false)|int))-t_before-t_weekend},
          4:{0:'# 21:00 - 22:45 #', 1:'evening',           2:(21*60)+0},
          5:{0:'# 22:45 - 23:30 #', 1:'night 3',           2:(22*60)+45},
          6:{0:'# 23:30 - 00:00 #', 1:'night 2',           2:(23*60)+30},
          7:{0:'# 00:00 - 00:30 #', 1:'night 1',           2:(0*60)+0},
          8:{0:'# 00:30 - 06:30 #', 1:'sleep',             2:(0*60)+30}
        } -%}
        {# #################### conditions loop -#}
        {%- set t_tod = namespace(current=0) -%}
        {%- for t in range(1, t_list|length) -%}
          {# #################### pair last t_list value with first -#}
          {%- set n = 1 if loop.last else t + 1 -%}
          {# #################### read values from list -#}
          {%- set t_start = t_list[t][2]|int + t_weekend -%}
          {%- set t_stop = t_list[n][2]|int + t_weekend -%}
          {# #################### "starts before midnight, ends after" fix -#}
          {%- set t_temp = 1440 if (t_stop<t_start) else 0 -%}
          {%- set t_stop = t_stop + t_temp -%}
          {%- set t_now = t_now + t_temp if ((t_now < t_start) and (t_now < t_stop)) else t_now -%}
          {# #################### check if current time fits in conditions -#}
          {%- if ((t_now >= t_start) and (t_now < t_stop)) -%}{%- set t_tod.current = t_list[t][1] -%}{%- endif -%}
        {%- endfor %}
        {# #################### return sensor value -#}
        {{ t_tod.current }}
      icon_template: >-
        {# #################### icon config -#}
        {%- set t_icons = {
          'morning': 'mdi:weather-sunset-up',
          'day':     'mdi:weather-sunny',
          'dusk':    'mdi:weather-sunset',
          'evening': 'mdi:weather-sunset-down',
          'night 3': 'mdi:brightness-1',
          'night 2': 'mdi:brightness-2',
          'night 1': 'mdi:brightness-3',
          'sleep':   'mdi:weather-night'
        } -%}
        {{ t_icons[states('sensor.0_cfg_tod')] if states('sensor.0_cfg_tod') in t_icons.keys() else 'mdi:weather-sunny' }}
      attribute_templates:
        tod_value: >-
          {# #################### numeric tod state -#}
          {%- set t_numeric = {
            'morning': 1,
            'day':     2,
            'dusk':    3,
            'evening': 4,
            'night 3': 5,
            'night 2': 6,
            'night 1': 7,
            'sleep':   8
          } -%}
          {{ t_numeric[states('sensor.0_cfg_tod')] if states('sensor.0_cfg_tod') in t_numeric.keys() else 2 }}

(as you can see, first row [0:{}], and first column [0:’’] are ignored - it’s only an information, so my config looks more like a table and is easier to edit/understand starting hours.)

:astonished:

I’m not sure I can help you with that specific template and the “bugs” but I have to ask…

is there some reason that the templates have to be so complicated to end up with a relatively simple thing as you want in the list above?

Here is a much simpler sensor that gives you mostly the same thing:

time_of_day:
      friendly_name: 'Time of Day'
      #entity_id: sensor.time
      value_template: >-
        {% set update = states('sensor.time') %}
        {% if   now().strftime("%H:%M") < "06:00" %} night
        {% elif now().strftime("%H:%M") < "09:00" %} early morning
        {% elif now().strftime("%H:%M") < "11:30" %} morning
        {% elif now().strftime("%H:%M") < "13:30" %} lunchtime
        {% elif now().strftime("%H:%M") < "16:00" %} afternoon
        {% elif now().strftime("%H:%M") < "19:00" %} evening
        {% elif now().strftime("%H:%M") < "21:00" %} late evening
        {% elif now().strftime("%H:%M") < "24:00" %} night
        {% endif %}

you would have to modify it a bit to get the weekend offset but I think it will still be way less complicated than that wall of text you have now.

Unless I’m missing something that requires that level of complexity…

1 Like

@finity thanks for the question. the answer is not easy :smiley:
first of all - I do some things “for fun”, or just to see how this or that template/yaml/jinja works. so yeah, when building them up there’s danger of making it too complicated at the end of the day :wink:
also - wall of text would be smaller after cutting out all comments [I’ve added them to clear some things here and answer possible future questions] and cutting out icon and numeric templates - final code would be just a little bit longer than yours :wink:

and btw - first I had something like your example. but when my ideas and needs grew - I’ve decided to go the other way. especially when the weekend shift popped up and when some of my parts of the day extends after midnight - I didn’t wanted to multiply the {% if %}{% elif %} combos too much [yeah, I understand that my outcome may be not the perfect one tho] and wanted to do some checks in the loop.

so even if your sample works mostly the same - when I started to work on the same source, it quickly turned to a monster which I’ve killed and rewrote with my current template. it may be the lack of skills on my side [maybe there’s a way to have the same functions as mine but with easier template and without any additional entities], but nonetheless I’m genuinely curious what’s happening at those “bug” times and why it’s behaving this way. no matter if I’ll use my template or yours, or even if I’ll pop up with another idea, just want to uderstand what the heck is going on :wink:

Sorry, I couldn’t figure out your logic. You made your types extremely difficult to read by using dictionaries with keys that are integers. By the way, a list does that naturally.

Anyways, I streamlined this so you don’t have to have all these notes inside your data objects.

{# A macro to convert hh:mm time into minutes #}
{%- macro time_in_minutes(t) %}
{%- set h, m = t.split(':') | map('int') %}
{{ h * 60 + m }}
{%- endmacro %}
{# simple sunset calc in an easy to read spot, not in the list #}
{% set sunset = as_timestamp(state_attr('sun.sun', 'next_setting'))|timestamp_custom("%H:%M", false) %}
{# Times of the day as a simple list #}
{# Name, Start, End #}
{% set times = [
  [ 'morning', '06:30', '09:45' ],
  [ 'day',     '09:45',  sunset ],
  [ 'dusk',     sunset, '21:00' ],
  [ 'evening', '21:00', '22:45' ],
  [ 'night 3', '22:45', '23:30' ],
  [ 'night 2', '23:30', '00:00' ],
  [ 'night 1', '00:00', '00:30' ],
  [ 'sleep',   '00:30', '06:30' ],
  ] %}
{# get the current time of day #}
{%- set time = time_in_minutes(states('sensor.time_date')[:5]) | int %}
{%- set midnight = time_in_minutes('24:00') | int %}
{# make namespace with a list of current times of day.  List is overkill but it will help you debug if you fall into 2 times of day #}
{%- set tod = namespace(current=[]) %}
{# Iterate through the times, no need to grab an index with this method #}
{%- for name, start, end in times %}
  {%- set start = time_in_minutes(start) | int %}
  {%- set end = time_in_minutes(end) | int %}
  {# if we cross midight... #}
  {%- if end < start %}
    {%- set time = midnight + time if time < start else time %}
    {%- set end = end if end > midnight else midnight + end %}
  {%- endif %}
  {%- if start <= time < end %}
    {%- set tod.current = tod.current + [ name ] %}
  {%- endif %}
{%- endfor %}
{{ tod.current | first }}

Just let me know how you want to add offsets…

FYI here is proof that it works…

time tod
00:00 night 1
00:05 night 1
00:10 night 1
00:15 night 1
00:20 night 1
00:25 night 1
00:30 sleep
00:35 sleep
00:40 sleep
00:45 sleep
00:50 sleep
00:55 sleep
01:00 sleep
01:05 sleep
01:10 sleep
01:15 sleep
01:20 sleep
01:25 sleep
01:30 sleep
01:35 sleep
01:40 sleep
01:45 sleep
01:50 sleep
01:55 sleep
02:00 sleep
02:05 sleep
02:10 sleep
02:15 sleep
02:20 sleep
02:25 sleep
02:30 sleep
02:35 sleep
02:40 sleep
02:45 sleep
02:50 sleep
02:55 sleep
03:00 sleep
03:05 sleep
03:10 sleep
03:15 sleep
03:20 sleep
03:25 sleep
03:30 sleep
03:35 sleep
03:40 sleep
03:45 sleep
03:50 sleep
03:55 sleep
04:00 sleep
04:05 sleep
04:10 sleep
04:15 sleep
04:20 sleep
04:25 sleep
04:30 sleep
04:35 sleep
04:40 sleep
04:45 sleep
04:50 sleep
04:55 sleep
05:00 sleep
05:05 sleep
05:10 sleep
05:15 sleep
05:20 sleep
05:25 sleep
05:30 sleep
05:35 sleep
05:40 sleep
05:45 sleep
05:50 sleep
05:55 sleep
06:00 sleep
06:05 sleep
06:10 sleep
06:15 sleep
06:20 sleep
06:25 sleep
06:30 morning
06:35 morning
06:40 morning
06:45 morning
06:50 morning
06:55 morning
07:00 morning
07:05 morning
07:10 morning
07:15 morning
07:20 morning
07:25 morning
07:30 morning
07:35 morning
07:40 morning
07:45 morning
07:50 morning
07:55 morning
08:00 morning
08:05 morning
08:10 morning
08:15 morning
08:20 morning
08:25 morning
08:30 morning
08:35 morning
08:40 morning
08:45 morning
08:50 morning
08:55 morning
09:00 morning
09:05 morning
09:10 morning
09:15 morning
09:20 morning
09:25 morning
09:30 morning
09:35 morning
09:40 morning
09:45 day
09:50 day
09:55 day
10:00 day
10:05 day
10:10 day
10:15 day
10:20 day
10:25 day
10:30 day
10:35 day
10:40 day
10:45 day
10:50 day
10:55 day
11:00 day
11:05 day
11:10 day
11:15 day
11:20 day
11:25 day
11:30 day
11:35 day
11:40 day
11:45 day
11:50 day
11:55 day
12:00 day
12:05 day
12:10 day
12:15 day
12:20 day
12:25 day
12:30 day
12:35 day
12:40 day
12:45 day
12:50 day
12:55 day
13:00 day
13:05 day
13:10 day
13:15 day
13:20 day
13:25 day
13:30 day
13:35 day
13:40 day
13:45 day
13:50 day
13:55 day
14:00 day
14:05 day
14:10 day
14:15 day
14:20 day
14:25 day
14:30 day
14:35 day
14:40 day
14:45 day
14:50 day
14:55 day
15:00 day
15:05 day
15:10 day
15:15 day
15:20 day
15:25 day
15:30 day
15:35 day
15:40 dusk
15:45 dusk
15:50 dusk
15:55 dusk
16:00 dusk
16:05 dusk
16:10 dusk
16:15 dusk
16:20 dusk
16:25 dusk
16:30 dusk
16:35 dusk
16:40 dusk
16:45 dusk
16:50 dusk
16:55 dusk
17:00 dusk
17:05 dusk
17:10 dusk
17:15 dusk
17:20 dusk
17:25 dusk
17:30 dusk
17:35 dusk
17:40 dusk
17:45 dusk
17:50 dusk
17:55 dusk
18:00 dusk
18:05 dusk
18:10 dusk
18:15 dusk
18:20 dusk
18:25 dusk
18:30 dusk
18:35 dusk
18:40 dusk
18:45 dusk
18:50 dusk
18:55 dusk
19:00 dusk
19:05 dusk
19:10 dusk
19:15 dusk
19:20 dusk
19:25 dusk
19:30 dusk
19:35 dusk
19:40 dusk
19:45 dusk
19:50 dusk
19:55 dusk
20:00 dusk
20:05 dusk
20:10 dusk
20:15 dusk
20:20 dusk
20:25 dusk
20:30 dusk
20:35 dusk
20:40 dusk
20:45 dusk
20:50 dusk
20:55 dusk
21:00 evening
21:05 evening
21:10 evening
21:15 evening
21:20 evening
21:25 evening
21:30 evening
21:35 evening
21:40 evening
21:45 evening
21:50 evening
21:55 evening
22:00 evening
22:05 evening
22:10 evening
22:15 evening
22:20 evening
22:25 evening
22:30 evening
22:35 evening
22:40 evening
22:45 night 3
22:50 night 3
22:55 night 3
23:00 night 3
23:05 night 3
23:10 night 3
23:15 night 3
23:20 night 3
23:25 night 3
23:30 night 2
23:35 night 2
23:40 night 2
23:45 night 2
23:50 night 2
23:55 night 2
2 Likes

epic… :+1:

Also, that template can have timespans that cross midnight. So you can get rid of your night 1, 2, and 3 crap.

The table in that post is so big it caused my chrome to crash :joy:

Yeah, I noticed that for me too…

EDIT: Just reduced it.

1 Like

@petro THANK YOU for the code & explaination. TBH I quietly wished you’ll chime in - your earlier posts I’ve read, and also earlier answers to my posts showed me a lot of tricks/knowledge [which I probably misuse on a daily basis :smiley: like above using dictionaries instead of lists… but it is informative for me nonetheless].

I’ll dig into it & will test heavily at the evening, as right now I’m still at work, but as of the time of writing I know for sure, that I’ve learned today the {% macro %} thing. something like that crossed my mind few days ago for a different template, but I wasn’t sure if it was possible. now I know :slight_smile:

btw the “night 1, 2, and 3 crap” - I couldn’t find other names, but I did needed three different “part of the night” there :wink: for my automations. so it’s not because crossing the midnight - it’s just because lack of other names :smiley:

if you aren’t crossing midnight, @finity’s template will work fine. But you’d want to modify it to only use time, not now().



{%- set time = states('sensor.time_date')[:5] %}
{%- set sunset = as_timestamp(state_attr('sun.sun', 'next_setting'))|timestamp_custom("%H:%M", false) %}
{% if   time < '00:30' %} night 1
{% elif time < '06:30' %} sleep
{% elif time < '09:45' %} morning
{% elif time <  sunset %} day
{% elif time < '21:00' %} dusk
{% elif time < '22:45' %} evening
{% elif time < '23:30' %} night 3
{% elif time < '24:00' %} night 2
{% endif %}

You can convert times to int’s and perform everything that way. Then adding your offset’s is easy.

{%- macro time_in_minutes(t) %}
{%- set h, m = t.split(':') | map('int') %}
{{ h * 60 + m }}
{%- endmacro %}

{%- set time = time_in_minutes(states('sensor.time_date')[:5]) | int %}
{%- set weekend_offset = 30 if is_state('binary_sensor.0_cfg_weekend', 'on') else 0 %}
{%- set before_offset = 45 %}

{%- set sunset = as_timestamp(state_attr('sun.sun', 'next_setting'))|timestamp_custom("%H:%M", false) %}

{% if time   < time_in_minutes('00:30') | int %} night 1
{% elif time < time_in_minutes('06:30') | int %} sleep
{% elif time < time_in_minutes('09:45') | int %} morning
{% elif time < time_in_minutes(sunset) | int - weekend_offset - before_offset %} day
{% elif time < time_in_minutes('21:00') | int %} dusk
{% elif time < time_in_minutes('22:45') | int %} evening
{% elif time < time_in_minutes('23:30') | int %} night 3
{% elif time < time_in_minutes('24:00') | int %} night 2

no no, just to clear things up: I want to have the option to cross the midnight [I’m still working on exact hours of my three night-parts - one of the part probably will be something like from 23:30 to 0:30 etc.]

will check all the codes. thanks.

worse case, if you do want to cross midnight with 1 ‘name’, just use finity’s method but label the 2 sections the same name. Either way, you want this manageable for yourself.

The root cause of your issue stems from using sensor.time_date and now() as they can be slightly out of sync.

Example:

What if now() is 0.01 seconds ahead of sensor.time_date?

now().hour would be 7, now().minute would be 00, now().second would be 0.00.

sensor.time_date being 0.01 seconds behind will have a state of 06:59.

Take the hour from senor (6) and the minute from now() (0), you end up at 6 am momentarily which is exactly inside your sleep zone.

thanks for the example and explanation about now() and sensor.time_date! I guess that this is the information I was looking for in the first place, but instead all readers focused on my crappy code ;D
in the end it turned out well [I learned few things and will implement them tonight, so it could be tested tomorrow morning] so I consider it a success :wink: will report soon if my “problem” showed up again.

Why is that? Mine is working fine the way it is.

He could have chose now and not time, see my previous post as to why he needs to modify his logic. He was using half sensor.time half now

1 Like

Oh, sorry, I misunderstood.

I thought that you were saying there was something wrong with my template.

Never mind… :wink:

Na, just poorly worded

you could leave out that… not ‘wrong’, just no longer needed :wink:

        {% set time = now().strftime("%H:%M") %}
        {% if   time < "06:00" %} night
        {% elif time < "09:00" %} early morning
        {% elif time < "11:30" %} morning
        {% elif time < "13:30" %} lunchtime
        {% elif time < "16:00" %} afternoon
        {% elif time < "19:00" %} evening
        {% elif time < "21:00" %} late evening
        {% elif time < "24:00" %} night
        {% endif %}

ok, I can confirm that using only one “source of time” [no matter if sensor.time_date or now()] fixed the issue. so solution was simple :slight_smile: @petro thank you for explaining that, I’d never thought that two “keywords” giving time in one line [considering that the clock is the same] could be shifted and with different outcome.

nonetheless - thanks @finity & again @petro for roasting my code. in the end it also works [even if some stuff is totally misused there ;)] but again I’ve learned few new tricks and right now I’m polishing this version as it’s base looked clearer than my template, so it’s a good piece for start :slight_smile:

1 Like

Yeah, I know.

It’s not hurting anything being there and I just haven’t had the motivation to go thru my code to clean all of those out.

I also could go thru and get rid of all of the “_template” entries too but haven’t done that yet either.

:slightly_smiling_face: