Fixed sun elevation thresholds don’t work optimally throughout the year due to Earth’s axial tilt and orbital motion:
Winter Issues:
Summer Issues:
Daylight Saving Time:
Template sensors with sinusoidal interpolation automatically adapt thresholds based on the current date, matching the Earth’s actual solar cycle.
The sensors provide dynamic threshold values that change throughout the year:
Example scenario (Berlin, 52°N):

The graph above shows how the dynamic sun elevation threshold changes throughout the year, comparing sinusoidal interpolation (recommended) with linear interpolation.
Why Sine is Better:
Add this to your configuration.yaml or create a new file in packages/:
template:
- trigger:
- trigger: time
at: "00:01:00"
- platform: event
event_type:
- homeassistant_started
sensor:
# ========================================
# Dynamic Sun Elevation - Opening
# ========================================
- name: "Sun Elevation Up Dynamic"
unique_id: sun_elevation_up_dynamic
unit_of_measurement: "°"
icon: mdi:weather-sunset-up
state: >
{% set day = now().timetuple().tm_yday %}
{% set lat = state_attr('zone.home', 'latitude') | float(52.0) %}
{# Reference values for 50°N #}
{% set summer = -2.0 %}
{% set winter = 5.0 %}
{# Sinusoidal interpolation (physically correct) #}
{# Day 80.75 ≈ March 21 (spring equinox) as reference #}
{% set seasonal_factor = sin(2 * pi * (day - 80.75) / 365) %}
{% set base = winter + (summer - winter) * (seasonal_factor + 1) / 2 %}
{# Latitude adjustment: ±0.2° per degree from 50°N #}
{% set adjustment = (lat - 50) * 0.2 %}
{% set final = base + adjustment %}
{{ final | round(1) }}
# ========================================
# Dynamic Sun Elevation - Closing
# ========================================
- name: "Sun Elevation Down Dynamic"
unique_id: sun_elevation_down_dynamic
unit_of_measurement: "°"
icon: mdi:weather-sunset-down
state: >
{% set day = now().timetuple().tm_yday %}
{% set lat = state_attr('zone.home', 'latitude') | float(52.0) %}
{# Reference values for 50°N #}
{% set summer = -1.0 %}
{% set winter = 2.0 %}
{# Sinusoidal interpolation (physically correct) #}
{% set seasonal_factor = sin(2 * pi * (day - 80.75) / 365) %}
{% set base = winter + (summer - winter) * (seasonal_factor + 1) / 2 %}
{# Latitude adjustment #}
{% set adjustment = (lat - 50) * 0.2 %}
{% set final = base + adjustment %}
{{ final | round(1) }}
Settings → System → Restart
Developer Tools → States → Search for:
sensor.sun_elevation_up_dynamicsensor.sun_elevation_down_dynamicBoth should show numeric values (e.g., 2.3)
In your CCA automation configuration:
sensor.sun_elevation_up_dynamicsensor.sun_elevation_down_dynamicDone! 🎉 Your covers now adapt automatically to seasons.
CRITICAL: Make sure your summer and winter values are NOT inverted!
Many users initially configure these values backwards. Here’s how to verify you have them correct:
Opening sensor (Sun Elevation Up Dynamic):
{% set summer = -2.0 %} # ✅ LOWER value for summer (opens EARLIER)
{% set winter = 5.0 %} # ✅ HIGHER value for winter (opens LATER)
Closing sensor (Sun Elevation Down Dynamic):
{% set summer = -1.0 %} # ✅ LOWER value for summer (closes LATER)
{% set winter = 2.0 %} # ✅ HIGHER value for winter (closes EARLIER)
If you have this, it’s WRONG:
# Opening sensor - INVERTED (WRONG!)
{% set summer = 5.0 %} # ❌ Summer value HIGHER than winter
{% set winter = -2.0 %} # ❌ Winter value LOWER than summer
# Closing sensor - INVERTED (WRONG!)
{% set summer = 2.0 %} # ❌ Summer value HIGHER than winter
{% set winter = -1.0 %} # ❌ Winter value LOWER than summer
After setting up your sensors, verify the values in Developer Tools:
In Summer (June-July):
In Winter (December-January):
If your values are the opposite, your configuration is INVERTED!
sun elevation > threshold
sun elevation < threshold

The graph above visualizes how both opening and closing sensors work together throughout the year, creating different “open window” durations in summer vs winter.
The dynamic sensors provide threshold values that are compared with the current sun elevation:
Opening Logic (Sun Elevation Up):
current sun elevation > sensor valueClosing Logic (Sun Elevation Down):
current sun elevation < sensor value✅ Reads your latitude from zone.home automatically
✅ Calculates optimal thresholds for your location
✅ Interpolates smoothly between summer and winter values
✅ Updates daily to follow sun’s annual path
✅ No maintenance required after initial setup
The sensors use these baseline values for 50°N latitude (Central Europe):
| Sensor | Summer (Jun 21) | Winter (Dec 21) | Range |
|---|---|---|---|
| Opening | -2.0° | 5.0° | 7.0° span |
| Closing | -1.0° | 2.0° | 3.0° span |
Automatic latitude adjustment: ±0.2° per degree difference from 50°N

The graph above shows how the opening sensor thresholds automatically adjust for different latitudes across Europe.
| Location | Latitude | Opening Summer | Opening Winter |
|---|---|---|---|
| Tromsø 🇳🇴 | 69.6°N | 1.9° | 8.9° |
| Stockholm 🇸🇪 | 59.3°N | -0.1° | 6.9° |
| Copenhagen 🇩🇰 | 55.7°N | -0.9° | 6.1° |
| Berlin 🇩🇪 | 52.5°N | -1.5° | 5.5° |
| London 🇬🇧 | 51.5°N | -1.7° | 5.3° |
| Paris 🇫🇷 | 48.9°N | -2.2° | 4.8° |
| Vienna 🇦🇹 | 48.2°N | -2.4° | 4.6° |
| Milan 🇮🇹 | 45.5°N | -3.1° | 3.9° |
| Rome 🇮🇹 | 41.9°N | -3.8° | 3.2° |
| Athens 🇬🇷 | 38.0°N | -4.6° | 2.4° |
You probably DON’T need to customize if:
You SHOULD customize if:
Modify the summer and winter values to shift timing:
{# Original values #}
{% set summer = -2.0 %}
{% set winter = 5.0 %}
{# Open earlier in morning #}
{% set summer = -4.0 %} # Lower value = earlier opening
{% set winter = 3.0 %}
{# Open later in morning #}
{% set summer = 0.0 %} # Higher value = later opening
{% set winter = 7.0 %}
Rule of thumb: Each 1° change shifts timing by approximately 4-6 minutes.
Change latitude sensitivity by modifying the 0.2 multiplier:
{# Standard (recommended) #}
{% set adjustment = (lat - 50) * 0.2 %}
{# More aggressive latitude adjustment #}
{% set adjustment = (lat - 50) * 0.3 %}
{# Less aggressive latitude adjustment #}
{% set adjustment = (lat - 50) * 0.1 %}
Create asymmetric behavior:
{# Aggressive summer (very early), conservative winter #}
{% set summer = -5.0 %}
{% set winter = 3.0 %}
{# Conservative summer, aggressive winter (very late) #}
{% set summer = -1.0 %}
{% set winter = 8.0 %}
# Opening sensor
{% set summer = -4.0 %} # Was: -2.0
{% set winter = 3.0 %} # Was: 5.0
# Closing sensor
{% set summer = -3.0 %} # Was: -1.0
{% set winter = 0.0 %} # Was: 2.0
Effect: Covers open ~30 min earlier, close ~30 min earlier
# Opening sensor
{% set summer = 0.0 %} # Was: -2.0
{% set winter = 7.0 %} # Was: 5.0
# Closing sensor
{% set summer = 1.0 %} # Was: -1.0
{% set winter = 4.0 %} # Was: 2.0
Effect: Covers open ~30 min later, close ~30 min later
# Opening sensor - very late
{% set summer = 3.0 %}
{% set winter = 10.0 %}
# Closing sensor - very early
{% set summer = 1.0 %}
{% set winter = 5.0 %}
Effect: Minimum exposure time, maximum privacy
# Opening sensor - early to capture morning sun
{% set summer = -5.0 %}
{% set winter = 2.0 %}
# Closing sensor - late to keep warmth inside
{% set summer = -4.0 %}
{% set winter = 0.0 %}
Effect: Opens early (solar gain), closes late (heat retention)

The diagram above shows how the sine function transforms the seasonal cycle into smooth threshold values.
elevation = winter + (summer - winter) × (sin(2π × (day - 80.75) / 365) + 1) / 2
Day 80.75 ≈ March 21 (Spring Equinox)
This is the reference point where:
Dec 21 (Day 355) Jan 1
MAXIMUM THRESHOLD MAXIMUM THRESHOLD
╲╱ ╲╱
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
╱ ╲ ╱ ╲
Mar 21 ╲ ╱ Sep 23
(Day 80) ╲ ╱ (Day 266)
EQUINOX ╲╱ EQUINOX
╱╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
Jun 21 (Day 172)
MINIMUM THRESHOLD
Note: For threshold values with summer < winter,
the sine curve is inverted compared to sun's declination
Example: September 20 (Day 263), 52°N (Berlin), Opening sensor
# 1. Get inputs
day = 263
lat = 52.0
summer = -2.0
winter = 5.0
# 2. Calculate seasonal factor
days_from_equinox = 263 - 80.75 = 182.25
radians = 2 × π × 182.25 / 365 = 3.14 rad
sin_value = sin(3.14) ≈ 0.00
seasonal_factor = 0.00
# 3. Calculate base elevation
base = 5.0 + (-2.0 - 5.0) × (0.00 + 1) / 2
base = 5.0 + (-7.0) × 0.5
base = 1.5°
# 4. Apply latitude adjustment
adjustment = (52.0 - 50.0) × 0.2 = 0.4°
final = 1.5 + 0.4 = 1.9°
# Result: Threshold = 1.9° (halfway between summer and winter)
Value Range: -1 to +1
1.0 ┤ ╭───╮
│ ╱ ╲
0.5 ┤ ╱ ╲
│╱ ╲
0.0 ┼ ╲ ╱
│ ╲ ╱
-0.5 ┤ ╲ ╱
│ ╰
-1.0 ┤
└─┬───┬───┬───┬───┬─
0 π/2 π 3π/2 2π
(Day 80) (172) (266) (355)
Transformation to 0-1 range:
normalized = (sin_value + 1) / 2
This ensures smooth interpolation between winter (0) and summer (1) values.
Developer Tools → Template → Test this:
{{ states('sensor.sun_elevation_up_dynamic') }}
{{ states('sensor.sun_elevation_down_dynamic') }}
Should return numbers like: 2.3 and 0.1
Check values at different dates:
{# Current date #}
{{ states('sensor.sun_elevation_up_dynamic') }}
{# Simulate summer (Day 172) #}
{% set day = 172 %}
{% set lat = state_attr('zone.home', 'latitude') | float(50.0) %}
{% set summer = -2.0 %}
{% set winter = 5.0 %}
{% set seasonal_factor = sin(2 * pi * (day - 80.75) / 365) %}
{% set base = winter + (summer - winter) * (seasonal_factor + 1) / 2 %}
{% set adjustment = (lat - 50) * 0.2 %}
{{ (base + adjustment) | round(1) }}
{# Should be close to -2.0° #}
{# Simulate winter (Day 355) #}
{% set day = 355 %}
{# ... same calculation ... #}
{# Should be close to 5.0° (or adjusted for your latitude) #}
Enable Automation Traces to verify sensor usage:
t_open_5 or t_close_5value_template shows sensor valueCause: Template syntax error or missing zone.home
Solution:
zone.home exists in Developer Tools → States{{ state_attr('zone.home', 'latitude') }}Cause: Default values don’t match your preferences
Solution: Adjust summer/winter reference values (see Customization)
Diagnosis table:
| Problem | Current Threshold | Action |
|---|---|---|
| Opens too early | Too low (e.g., -3°) | Increase summer/winter values |
| Opens too late | Too high (e.g., +8°) | Decrease summer/winter values |
| Closes too early | Too high (e.g., +5°) | Decrease summer/winter values |
| Closes too late | Too low (e.g., -6°) | Increase summer/winter values |
Check:
Normal behavior: Sensors update when:
Not needed: Sensors don’t need to update more than once per day - daily changes are gradual.
Extreme seasonal variation requires wider range:
# Opening sensor
{% set summer = -5.0 %} # Much lower minimum
{% set winter = 7.0 %} # Much higher maximum
# Closing sensor
{% set summer = -4.0 %}
{% set winter = 3.0 %}
Less variation, higher year-round sun:
# Opening sensor
{% set summer = -4.0 %} # Lower baseline
{% set winter = 8.0 %} # Higher baseline
# Closing sensor
{% set summer = -2.0 %}
{% set winter = 4.0 %}
Invert the seasons by adding 182.5 days (6 months):
{% set seasonal_factor = sin(2 * pi * (day - 80.75 + 182.5) / 365) %}
This shifts the curve so:
For extreme latitudes, use quadratic adjustment:
{% set lat_diff = lat - 50 %}
{% set adjustment = lat_diff * 0.2 + (lat_diff ** 2) * 0.01 %}
This increases sensitivity at higher/lower latitudes.
For maximum control, use discrete monthly values:
{% set month = now().month %}
{% set monthly_values = {
1: -2.0, 2: -1.0, 3: 1.0, 4: 3.0,
5: 4.5, 6: 5.0, 7: 4.5, 8: 3.0,
9: 1.0, 10: -1.0, 11: -1.5, 12: -2.0
} %}
{{ monthly_values[month] }}
Add hysteresis to prevent rapid oscillation:
# In CCA configuration
Sun Elevation Up: 5.0°
Brightness Hysteresis: 1000 lx # Add this for stability
Hysteresis is handled by CCA, not in the sensors.
Use default values for 1-2 weeks before adjusting. This helps you understand baseline behavior.
Change values by ±0.5° to ±1.0° at a time. Large changes can cause unexpected behavior.
Fine-tune in spring/autumn (equinoxes) when changes are fastest. This ensures good behavior year-round.
Account for buildings, trees, hills:
# Blocked sunrise by building - delay opening
{% set summer = 1.0 %} # Higher = later
{% set winter = 7.0 %}
East-facing rooms might need different values than west-facing:
# East (morning sun) - open earlier
sensor.sun_elevation_up_dynamic_east:
{% set summer = -4.0 %}
{% set winter = 3.0 %}
# West (evening sun) - open later
sensor.sun_elevation_up_dynamic_west:
{% set summer = 0.0 %}
{% set winter = 7.0 %}
Use CCA’s time windows to limit operation:
Time Up Early: 06:00
Time Up Late: 10:00
+ Dynamic Sensor
= Opens between 06:00-10:00 when sun reaches threshold
Add sensors to Lovelace dashboard to visualize annual cycle:
type: history-graph
entities:
- sensor.sun_elevation_up_dynamic
- sensor.sun_elevation_down_dynamic
- sun.sun
hours_to_show: 168 # 1 week