Skip to content

Instantly share code, notes, and snippets.

@daniel-sc
Last active July 27, 2026 17:05
Show Gist options
  • Select an option

  • Save daniel-sc/074465be04dd9a793cba5b841ab63f83 to your computer and use it in GitHub Desktop.

Select an option

Save daniel-sc/074465be04dd9a793cba5b841ab63f83 to your computer and use it in GitHub Desktop.
Homeassistant blueprint to close/open covers depending on sun position and weather
# see https://community.home-assistant.io/t/close-open-curtain-cover-blinds-based-on-sun-and-weather/584240
blueprint:
name: cover sun
description: Close cover when sun is shining and open when stops. This considers weather (sunny, partly cloudy), sun position (elevation, azimuth) and temperature.
domain: automation
source_url: https://gist.github.com/daniel-sc/074465be04dd9a793cba5b841ab63f83
input:
cover_entity:
name: cover
selector:
entity:
domain: cover
sun_position_start:
name: Sun azimuth position start
description: Sun azimuth. When sun is over this value, shutter could closed.
default: 89
selector:
number:
min: 0
max: 360
unit_of_measurement: °
sun_position_end:
name: Sun azimuth position end
description: Sun azimuth. when sun is over this value, shutter should open.
default: 200
selector:
number:
min: 0
max: 360
unit_of_measurement: °
sun_elevation_start:
name: Sun elevation to start
description: Sun elevation. When sun is over this value, shutter could be closed.
default: 32
selector:
number:
min: -100
max: 360
unit_of_measurement: °
outdoor_temp:
name: Temperature threshold
description: Temperature above which the shutters will close. Based on Temperature source (see below).
default: 16
selector:
number:
min: 0
max: 30
unit_of_measurement: °C
close_position:
name: Closing position of curtain
description: "Target position for closed curtain. Note: 100 is open!"
default: 30
selector:
number:
min: 1
max: 99
no_open_after:
name: Night time begin
description: >
Any open/close action that would happen during the configured night time is ignored.
The automation does not re-evaluate automatically at night time end.
By default, night time handling is disabled because night time begin and end are the same value.
default: "00:00:00"
selector:
time:
no_open_before:
name: Night time end
description: >
Any open/close action that would happen during the configured night time is ignored.
The automation does not re-evaluate automatically at night time end.
By default, night time handling is disabled because night time begin and end are the same value.
default: "00:00:00"
selector:
time:
random_delay_max:
name: Random command delay max
description: Randomly delay cover commands between 0 and this many seconds. This can help avoid simultaneous Zigbee commands when multiple automations trigger at the same time. Set to 0 to disable.
default: 30
selector:
number:
min: 0
max: 300
step: 1
unit_of_measurement: seconds
weather:
name: Weather service
default: "weather.forecast_home"
selector:
entity:
domain: weather
temperature_entity:
name: Temperature source
default: "weather.forecast_home"
selector:
entity:
filter:
- domain:
- weather
- device_class:
- temperature
close_weather_conditions:
name: Weather conditions
description: Close curtain on all of the selected weather conditions (if all other conditions are met)
default:
- sunny
- partlycloudy
selector:
select:
multiple: true
options:
- "clear-night"
- "cloudy"
- "fog"
- "hail"
- "lightning"
- "lightning-rainy"
- "partlycloudy"
- "pouring"
- "rainy"
- "snowy"
- "snowy-rainy"
- "sunny"
- "windy"
- "windy-variant"
- "exceptional"
variables:
sun_elevation_close: !input sun_elevation_start
sun_azimuth_close: !input sun_position_start
sun_azimuth_open: !input sun_position_end
temp_close: !input outdoor_temp
curtain_entity_id: !input cover_entity
curtain_target_position: !input close_position
no_open_after_time: !input no_open_after
no_open_before_time: !input no_open_before
random_delay_max_seconds: !input random_delay_max
is_night: "{{ no_open_after_time is not none and no_open_before_time is not none and (( today_at(no_open_after_time) < now() < today_at(no_open_before_time)) or (today_at(no_open_after_time) > today_at(no_open_before_time) and (now() > today_at(no_open_after_time) or now() < today_at(no_open_before_time)))) }}"
weather_entity_id: !input weather
temperature_entity_id: !input temperature_entity
weather_close: !input close_weather_conditions
curtain_current_position: "{{ state_attr(curtain_entity_id, 'current_position') if state_attr(curtain_entity_id, 'current_position') is not none else state_attr(curtain_entity_id, 'position') }}"
current_temp: >
{% set t = state_attr(temperature_entity_id, 'temperature') %}
{{ (t if t is not none else states(temperature_entity_id)) | float(none) }}
mode: queued # We rely on trigger changes, so 'single' and 'restart' are not possible. 'parallel' might work.
trigger:
- platform: numeric_state
entity_id: sun.sun
attribute: elevation
above: !input sun_elevation_start
id: t_sun_elevation_above
- platform: numeric_state
entity_id: sun.sun
attribute: elevation
below: !input sun_elevation_start
id: t_sun_elevation_below
- platform: numeric_state
entity_id: sun.sun
attribute: azimuth
above: !input sun_position_start
id: t_sun_azimuth_start_above
- platform: numeric_state
entity_id: sun.sun
attribute: azimuth
above: !input sun_position_end
id: t_sun_azimuth_end_above
- platform: state
entity_id: !input weather
to: !input close_weather_conditions
not_from: !input close_weather_conditions
id: t_weather_sunny
- platform: state
entity_id: !input weather
not_to: !input close_weather_conditions
from: !input close_weather_conditions
id: t_weather_not_sunny
# temperature_entity can either be:
# - a weather entity, where temperature is an attribute
# - a sensor entity, where temperature is the state
- platform: numeric_state
entity_id: !input temperature_entity
value_template: >
{{ state.attributes.get('temperature', state.state) | float(none) }}
above: !input outdoor_temp
id: t_temp_above
- platform: numeric_state
entity_id: !input temperature_entity
value_template: >
{{ state.attributes.get('temperature', state.state) | float(none) }}
below: !input outdoor_temp
id: t_temp_below
condition:
# During night time, triggers are ignored
- condition: template
value_template: "{{ not is_night }}"
- condition: or
conditions:
# do close when current cover position is above curtain_target_position
- condition: and
conditions:
- condition: template
value_template: "{{ curtain_current_position > curtain_target_position }}"
- condition: template
value_template: "{{ states(weather_entity_id) in weather_close }}"
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
- condition: template
value_template: "{{ current_temp is number and current_temp >= temp_close }}"
# do open (when it (first) leaves the closed conditions (prevent e.g. opening at night when any trigger fires)):
- condition: and
conditions:
- condition: template
value_template: "{{ curtain_current_position <= curtain_target_position }}"
- condition: or
conditions:
- condition: and
conditions:
- condition: trigger
id: t_weather_not_sunny
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
- condition: template
value_template: "{{ current_temp is number and current_temp >= temp_close }}"
- condition: and
conditions:
- condition: template
value_template: "{{ states(weather_entity_id) in weather_close }}"
- condition: trigger
id: t_sun_elevation_below
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
- condition: template
value_template: "{{ current_temp is number and current_temp >= temp_close }}"
- condition: and
conditions:
- condition: template
value_template: "{{ states(weather_entity_id) in weather_close }}"
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
- condition: trigger
id: t_sun_azimuth_end_above
- condition: template
value_template: "{{ current_temp is number and current_temp >= temp_close }}"
- condition: and
conditions:
- condition: template
value_template: "{{ states(weather_entity_id) in weather_close }}"
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
- condition: trigger
id: t_temp_below
action:
- choose:
# close cover
- conditions:
- condition: template
value_template: "{{ curtain_current_position > curtain_target_position }}"
- condition: template
value_template: "{{ states(weather_entity_id) in weather_close }}"
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
- condition: template
value_template: "{{ current_temp is number and current_temp >= temp_close }}"
sequence:
- delay:
seconds: "{{ range(0, random_delay_max_seconds | int + 1) | random }}"
- service: cover.set_cover_position
data:
position: "{{ curtain_target_position }}"
target:
entity_id: "{{ curtain_entity_id }}"
# open cover (conditions are somewhat redundant here, as 'condition' is more specific..)
- conditions:
- condition: template
value_template: "{{ curtain_current_position <= curtain_target_position }}"
- condition: or
conditions:
- condition: template
value_template: "{{ states(weather_entity_id) not in weather_close }}"
- condition: template
value_template: "{{ state_attr('sun.sun', 'elevation') < sun_elevation_close }}"
- condition: template
value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and (sun_azimuth_close > state_attr('sun.sun', 'azimuth') or state_attr('sun.sun', 'azimuth') > sun_azimuth_open)) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') < sun_azimuth_close and state_attr('sun.sun', 'azimuth') > sun_azimuth_open)) }}"
- condition: template
value_template: "{{ current_temp is number and current_temp < temp_close }}"
sequence:
- delay:
seconds: "{{ range(0, random_delay_max_seconds | int + 1) | random }}"
- service: cover.set_cover_position
data:
position: 100
target:
entity_id: "{{ curtain_entity_id }}"
@riddik14

Copy link
Copy Markdown

Logger: homeassistant.components.homeassistant.triggers.numeric_state
Source: components/homeassistant/triggers/numeric_state.py:200
Integration: Home Assistant Core Integration (documentation, issues)
First occurred: 9 febbraio 2024 alle ore 23:03:01 (20 occurrences)
Last logged: 08:23:01

Error in 'cover sun auto' trigger: In 'numeric_state' condition: entity weather.forecast_ home state 'rainy' cannot be processed as a number
Error in 'cover sun auto' trigger: In 'numeric_state' condition: entity weather.forecast_home state 'cloudy' cannot be processed as a number

@daniel-sc

Copy link
Copy Markdown
Author

@daniel-sc

Copy link
Copy Markdown
Author

@riddik14 this was a warning because the blueprint did not cleanly handle variance between weather/temperature entity - this should be fixed with the latest version.

@temp

temp commented Jul 6, 2026

Copy link
Copy Markdown

Hi @daniel-sc. Love your blueprint! But I'm having some problems lately...

Bug: the "open" root-gate condition is edge-triggered and can permanently miss its window

I've been debugging why my shading covers (using this blueprint) stay closed long after the sun has clearly moved past the shading window (azimuth well beyond sun_position_end, elevation and temperature both still high). Traced it down to a real logic issue.
There are two separate places that decide whether to open:

The action itself (second choose branch) correctly checks live state: weather no longer in weather_close OR elevation < sun_elevation_close OR azimuth outside the [sun_position_start, sun_position_end] window OR temp < temp_close. This is state-based and would evaluate correctly at any time.
The root condition: block that gates whether the automation proceeds to the action at all repeats the same four checks, but each one is additionally tied to condition: trigger / id: <specific_trigger> (t_weather_not_sunny, t_sun_elevation_below, t_sun_azimuth_end_above, t_temp_below). Since these are numeric_state/state triggers, they only fire once, at the exact instant a threshold is crossed. If any of the other three sub-conditions isn't simultaneously true at that precise instant, the whole automation short-circuits with failed_conditions — and since the triggering event was a one-time edge, that opportunity is gone until some other trigger happens to align with all four conditions at once.

In my case: sun_position_end was crossed hours ago, and weather/temperature/elevation have stayed well within "should be open" territory ever since, but the automation logged several failed_conditions runs (triggered by unrelated weather-entity updates) instead of opening, because the root gate never got re-evaluated in a way that matched its rigid trigger-id requirement.
Fix: replace the four trigger-id-gated and blocks in the root gate's "open" OR-list with the same plain state-based checks the action already uses correctly. This makes the gate re-evaluate correctly on any of the existing triggers, instead of requiring one very specific edge to coincide with all conditions.

--- cover_sun_blueprint.yaml (original)
+++ cover_sun_blueprint.yaml (fix)
@@ inside the "do open" OR-block of the root condition @@
       - condition: and
         conditions:
           - condition: template
             value_template: "{{ curtain_current_position <= curtain_target_position }}"
           - condition: or
             conditions:
-              - condition: and
-                conditions:
-                  - condition: trigger
-                    id: t_weather_not_sunny
-                  - condition: template
-                    value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
-                  - condition: template
-                    value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
-                  - condition: template
-                    value_template: "{{ current_temp is number and current_temp >= temp_close }}"
-
-              - condition: and
-                conditions:
-                  - condition: template
-                    value_template: "{{ states(weather_entity_id) in weather_close }}"
-                  - condition: trigger
-                    id: t_sun_elevation_below
-                  - condition: template
-                    value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
-                  - condition: template
-                    value_template: "{{ current_temp is number and current_temp >= temp_close }}"
-
-              - condition: and
-                conditions:
-                  - condition: template
-                    value_template: "{{ states(weather_entity_id) in weather_close }}"
-                  - condition: template
-                    value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
-                  - condition: trigger
-                    id: t_sun_azimuth_end_above
-                  - condition: template
-                    value_template: "{{ current_temp is number and current_temp >= temp_close }}"
-
-              - condition: and
-                conditions:
-                  - condition: template
-                    value_template: "{{ states(weather_entity_id) in weather_close }}"
-                  - condition: template
-                    value_template: "{{ state_attr('sun.sun', 'elevation') >= sun_elevation_close }}"
-                  - condition: template
-                    value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and sun_azimuth_close <= state_attr('sun.sun', 'azimuth') <= sun_azimuth_open) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') >= sun_azimuth_close or state_attr('sun.sun', 'azimuth') <= sun_azimuth_open)) }}"
-                  - condition: trigger
-                    id: t_temp_below
+              # Same checks the action itself already uses - plain state
+              # based, not tied to one specific trigger id. Any of the
+              # existing triggers can now re-evaluate this correctly,
+              # instead of requiring one exact edge to coincide with
+              # all four conditions at once.
+              - condition: template
+                value_template: "{{ states(weather_entity_id) not in weather_close }}"
+              - condition: template
+                value_template: "{{ state_attr('sun.sun', 'elevation') < sun_elevation_close }}"
+              - condition: template
+                value_template: "{{ (sun_azimuth_close <= sun_azimuth_open and (sun_azimuth_close > state_attr('sun.sun', 'azimuth') or state_attr('sun.sun', 'azimuth') > sun_azimuth_open)) or (sun_azimuth_close > sun_azimuth_open and (state_attr('sun.sun', 'azimuth') < sun_azimuth_close and state_attr('sun.sun', 'azimuth') > sun_azimuth_open)) }}"
+              - condition: template
+                value_template: "{{ current_temp is number and current_temp < temp_close }}"

I've verified this against a live instance: with the current fix, the condition evaluates true immediately when azimuth/elevation/temp/weather actually call for opening, rather than waiting for a coincidental exact-edge match.
Happy to test a PR if you'd like to take a different approach to fixing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment