A custom template instead of tod

Last afternoon I got an idea! :smiley:

I have been using helpers with tod (time of day), created them many, many years ago.
I wanted the outdoor lights to be on during the night so I set before to sunrise plus some time offset and after to sunset minus some time offset.
This didn’t work as I wanted. So I did the inverse, created a new helper where I set after to sunrise plus some time offset and before to sunset minus some time offset.
This was called ‘outdoorday’. Then the old outdoornight became ‘not outdoorday’. Not perfect, but it works most of the time.

The problem here that it is the old style of helpers in the configuration.yaml, they don’t have an ID, and therefor can not be edited in the UI…
So now I got some energy to test and learn new things…
Custom Templates! TADA!

Started to look at sensor.sun_next_* properies, tried many variants in the template tool.
Got a macro to work and was pretty proud of it, but it failed at som stage.
So I started to think about it, and came to this conslusion:
It is

night when sensor.sun_next_rising < sensor.sun_next_setting

Simple as that basicly!

But could not make it that simple I needed the offsets, and got different offsets for indoor lights, outdoor lights, christnas lights indoor and christnas lights outdoor.
That’s why the macro… And now I can use number helpers for simple adjustment, even the Mrs can dio it!

{% macro night_on(sunrise_offset, sunset_offset) %}
{{ states(“sensor.sun_next_rising”) | as_datetime | as_local + timedelta(minutes=sunrise_offset) < states(“sensor.sun_next_setting”)| as_datetime|as_local + timedelta(minutes=sunset_offset) }}
{% endmacro %}

The call:

{% from ‘mytod.jinja’ import night_on %}
{{night_on(30,-20)}}

With off at 30 minutes before sunrise and on at 20 minutes before sunset!

Now I just have to figure out how to put multiple macros in a file…
I have loads that I need to move…

Just noticed, it doesn’t work as I thought…
I need to save the times somehow…, at least for the sunrise…
I will wait for the sunset at 14.42 and se how it works there…

Please use the pre-formatted text button </> to format your config. Not the quote button.

I didn’t test it but I think, when using negative offsets, the dates for the next_* events might still be “old”. This seems to work:

{% macro night_on(sunrise_offset, sunset_offset) %}
{% set rising = (states('sensor.sun_next_rising')|as_datetime + timedelta(minutes=sunrise_offset))|as_timestamp|timestamp_custom("%H:%M") %}
{% set setting = (states('sensor.sun_next_setting')|as_datetime + timedelta(minutes=sunset_offset))|as_timestamp|timestamp_custom("%H:%M") %}
{% set n = now()|as_timestamp|timestamp_custom("%H:%M") %}
{{ n < rising or n >= setting }}
{% endmacro %}

You could also make that into a trigger-based binary sensor that only changes state when needed.

Trigger sensor code
- trigger:
  - trigger: sun
    event: sunrise
    offset: "00:30:00"
    id: sunrise
  - trigger: sun
    event: sunset
    offset: "- 00:20:00"
    id: sunset
  - trigger: event
    event_type: event_template_reloaded
    id: 'template_reload'
  - trigger: homeassistant
    event: start
    id: 'ha_start'
  binary_sensor:
    - name: Outdoor night
      unique_id: 5ea6c97b-3c34-49ff-9ee8-7087d8233aac
      availability: "{{ has_value('sensor.sun_next_rising') and has_value('sensor.sun_next_setting') }}"
      state: >-
        {% if trigger.id in ['ha_start','template_reload'] %}
          {% set rising = (states('sensor.sun_next_rising')|as_datetime + timedelta(minutes=30))|as_timestamp|timestamp_custom("%H:%M") %}
          {% set setting = (states('sensor.sun_next_setting')|as_datetime + timedelta(minutes=-20))|as_timestamp|timestamp_custom("%H:%M") %}
          {% set n = now()|as_timestamp|timestamp_custom("%H:%M") %}
          {{ 'off' if setting > n >= rising else 'on' }}
        {% else %}
          {{ 'off' if trigger.id == 'sunrise' else 'on' }}
        {% endif %}

I will look into the examples…

Sure, I can set the same offset time for all, but thats not what I want.
And I don’t want to duplicate code and configitems more than necessary…
A problem we have here in Scandinavia is that sunset and sunrise differs dependent on time of year.
Today the sun sets at 3.12 pm and rises tomorrow at 8.42 am. In the summer it sets at around 11.15 pm and rises 2.30 am.

Yes those are big differences, and after 2nd thought, I think that my solution for the macro is wrong. With large changes in the sunrise and sunset times from day to day, the macro could report a false change of day->night->day in the morning and night->day->night in the evening.

For my macro to work, the time of sun events must remain constant throughout the day (24 hours). For this purpose I created a new sensor, and the macro now uses this sensor.

Helper sensor
- trigger:
  - trigger: time_pattern
    hours: 0
    minutes: 0
    id: 'time'
  - trigger: homeassistant
    event: start
    id: 'ha_start'
  - trigger: event
    event_type: event_template_reloaded
    id: 'template_reload'
  sensor:
    - name: Solar events
      unique_id: 14e494dd-1011-43fb-bb27-1b1351063b2a
      state: "{{ now() }}"
      attributes:
        sunrise: >-
          {% set current = this.attributes.get('sunrise') %}
          {% set no_data = ['unknown','unavailable',none,''] %}
          {% set refresh = trigger.id == 'time' or current in no_data or (current|as_datetime).day != now().day %}
          {{ states('sensor.sun_next_rising') if refresh else current }}
        sunset: >-
          {% set current = this.attributes.get('sunset') %}
          {% set no_data = ['unknown','unavailable',none,''] %}
          {% set refresh = trigger.id == 'time' or current in no_data or (current|as_datetime).day != now().day %}
          {{ states('sensor.sun_next_setting') if refresh else current }}
Macro
{% macro night_on(sunrise_offset, sunset_offset) %}
{% set rising = (state_attr('sensor.solar_events','sunrise')|as_datetime + timedelta(minutes=sunrise_offset))|as_timestamp|timestamp_custom("%H:%M") %}
{% set setting = (state_attr('sensor.solar_events','sunset')|as_datetime + timedelta(minutes=sunset_offset))|as_timestamp|timestamp_custom("%H:%M") %}
{% set n = now()|as_timestamp|timestamp_custom("%H:%M") %}
{{ n < rising or n >= setting }}
{% endmacro %}

I’ve been thinking about it and testing some, everything is slow for me since I had a stroke in june last year.

But, if I create two helpers, sunsetmem and sunrisemem, and at noon everyday they reeturn sun.sun_next_sunset resp. sun.sun_next_sunrise, otherwise they return themself.

I have to think a little more, but I have an idea now…

Some thing like this for sunsetmem:

{% if is_state('sensor.sunsetmem', 'unavailable') or states('sensor.sunsetmem')=='unknown' or is_state_attr('sun.sun','rising',False) %}
{{ states("sensor.sun_next_setting")| as_datetime|as_local }}
{% else %}
{{ states('sensor.sunsetmem') }}
{% endif %}

and add a test if now() is before sun.sun_next_sunrice, then it should be {{ states(“sensor.sun_next_setting”) | as_datetime|as_local - timedelta(hours=24) }}

I think this is a bit too complicated. You just need to “freeze” the dates for current day, and the sooner the better (right after the current date changes but not before it changes). You can use this code as state for ‘sunrisemem’ template helper:

{% set current = this.state|default('unknown') %}
{% set refresh = current in ['unknown','unavailable'] or as_datetime(current).day != now().day %}
{{ states('sensor.sun_next_rising') if refresh else current }}

And repeat for ‘sunsetmem’. And then inside the macro, you can just check if now() is smaller than sunrisemem+shift or greater than sunsetmem+shift.

{% macro night_on(sunrise_offset, sunset_offset) %}
{% set r = states('sensor.sunrisemem')|as_datetime + timedelta(minutes=sunrise_offset) %}
{% set s = states('sensor.sunsetmem')|as_datetime + timedelta(minutes=sunset_offset) %}
{% set n = now() %}
{{ n < r or n >= s }}
{% endmacro %}

I think, atleast sunsettingmem should be changed at noon, because then the probabillity that the “night lights” is off.

I will try this when I get home!
And BIG THANKS!

You’re welcome, it’s enlightening. I’ve been stuck in the belief that days in Scandinavia are always shorter than, say, Germany (shameful, yes :smiley: )

The sun never sets before noon, so reading the date of the next sunset at noon (12:00 or 12:00 p.m.) will give the same result as reading it earlier, just after midnight (00:00 or 12:00 a.m.).

IMO, the only limitation of this method is that the outdoor day always falls within the boundaries of a calendar day. E.g. if the sun sets at 23:14 in the evening (11:14 p.m), and you add 1 hour to the outdoor day, to extend it to 00:14 (12:14 a.m.) next day, the day will still end at midnight 23:59:59 / 11:59:59 p.m.

If this is a problem, you could change the condition in the sensors like this:

{% set refresh = current in ['unknown','unavailable'] or (as_datetime(current).day != now().day and now().hour >= 1) %}

so that reading of new dates for current day happens at 01:00 a.m.

Have you considered using the Sun2 integration? Instead of the rolling “next_” type sensors that the core Sun integration provides, it creates sensors that provide date time attributes for “today”, “tomorrow”, and “yesterday” which makes this kind of template a lot easier.

You could set your macro like:

{% macro night_on(sunrise_offset, sunset_offset) %}
{% set rise = state_attr("sensor.home_sun_rising", "today")%}
{% set setting = state_attr("sensor.home_sun_setting", "today") %}
{{ rise + timedelta(minutes=sunrise_offset) >= now() or
now() >= setting + timedelta(minutes=sunset_offset) }}
{% endmacro %}

And you would use a trigger-based template binary sensor:

template:
  - triggers:
      - trigger: time_pattern
        minutes: /5  #change this to whatever update frequency you need
      - trigger: homeassistant
        event: start
    binary_sensor:
      - name: Outdoor Night
        state: |
          {% from 'mytod.jinja' import night_on %}
          {{night_on(30,-20)}}

Can’t get the sun2 to work!
And it looks like to much keyboarding for each …

Just got 11 repairs…
Its old, mainly template sensors made in the configuration file, I have to fix them first…

I got on the Home Assistant train late enough to avoid running into the old syntax.

There’s one thing I should’ve stressed earlier: ‘sunrisemem’ & ‘sunsetmem’ should be trigger-based helpers (that’s why the first sensor I posted in this topic, with 2 attributes, was also trigger-based). Data saved to “normal” template helpers can’t survive HA restart or reloading of templates. They will then update with current values from “sun_next_*” sensors, and that will break the macro.

Trigger-based versions of sunrisemem & sunsetmem
template:
  - trigger:
    - trigger: time_pattern
      hours: 1
      minutes: '*'
    - trigger: time_pattern
      hours: 2
      minutes: '*'
    action:
      - variables:
          no_data: "{{ ['unknown','unavailable',none] }}"
    sensor:
      - name: sunrisemem
        unique_id: 7267808b-b54e-40c9-b984-98d09ac8a6d5
        state: >-
          {% set current = this.state|default('unknown') %}
          {% set refresh = current in no_data or as_datetime(current).day != now().day %}
          {{ states('sensor.sun_next_rising') if refresh else current }}
      - name: sunsetmem
        unique_id: 986eb480-d301-491c-bdb3-f096134c9a9a
        state: >-
          {% set current = this.state|default('unknown') %}
          {% set refresh = current in no_data or as_datetime(current).day != now().day %}
          {{ states('sensor.sun_next_setting') if refresh else current }}

New helpers will appear as unknown until their first update at 1:00 a.m. From then on, they will always give valid data for the macro, provided your computer is on at any time during the two-hour update window (1:00 a.m.–3:00 a.m.).

Got sun2 working!! :smiley:

And I can use it directly!

{{now()|as_datetime|as_local}} => 2025-12-05 16:30:00.500313+01:00

{{ states("sensor.hemtest_sol_soluppgang")|as_datetime|as_local }} => 2025-12-05 08:47:01+01:00

{{ states("sensor.hemtest_sol_solnedgang")|as_datetime|as_local }} => 2025-12-05 15:07:42+01:00

Rising (hemtest_sol_soluppgang) and setting (hemtest_sol_solnedgang) seems to give the values for current day.
I’m guessing that they will change at midnight!
I skip sunrisemem and sunsetmem!
Make everything a little bit easier… :grin:
Think it’s going to be a late night…

{% macro night_on(sunrise_offset, sunset_offset) %}
{% set r = states('sensor.hemtest_sol_soluppgang')|as_datetime | as_local + timedelta(minutes=sunrise_offset) %}
{% set s = states('sensor.hemtest_sol_solnedgang')|as_datetime | as_local + timedelta(minutes=sunset_offset) %}
{% set n = now() |as_datetime | as_local %}
{{ n < r or n >= s }}
{% endmacro %}

tested, works like a charm!

now() already produces a localized datetime object, so the |as_datetime | as_local is unnecessary.

I’m glad it works! :slight_smile: Sun2 is certainly a better option.