Shading a room by hand means noticing the sun has come around, which nobody does until the room is already too warm. This recipe reads the sun’s position straight from Home Assistant and acts on it: as the sun climbs past the height you set it draws the shades, and as it drops back down in the afternoon it opens them again. No weather service is needed, just Home Assistant’s own idea of where the sun is.
The height is set as an elevation angle, the sun’s height above the horizon in degrees. Forty degrees is late morning in most places and a reasonable point for the sun to start reaching in through the glass. Raise it to shade only around solar noon, lower it to start earlier.
Only when it is worth it
On its own the recipe shades on the sun alone, which is not always what you want on a bright but cold winter day. Adding sensors narrows it. Pick a light sensor as the optional illuminance role and the shades only close when the light is genuinely strong, not on a high but grey sky. Pick an outdoor temperature sensor as the optional role and the close only happens when it is hot, so a cold sunny noon leaves the shades open to let the warmth in. Set the brightness threshold in lux, as your light sensor reports it, and the heat threshold in Celsius; a Fahrenheit sensor is converted for you.
Add speakers as the optional media-player role and the recipe says when it is closing the shades to keep the house cool, so the change is not a mystery when you are in the room.
Files Selora AI's pipeline reads to install this recipe. The manifest declares roles, inputs, and integrations; the package files are Jinja-templated YAML applied to Home Assistant.
# Shades by Sun and Heat — close the shades when the sun is high (and, when the
# sensors are picked, hot and bright) and open them as the sun drops.
#
# Built on Home Assistant's own sun.sun entity, so no weather integration is
# needed. The close triggers on the sun elevation rising past the threshold and
# the open on it falling back, both numeric_state triggers on the elevation
# attribute. Optional illuminance and outdoor temperature sensors add plain
# numeric_state conditions to the close, so it only fires on a bright, hot day.
slug: shades-by-sun-and-heat
version: 1.0.0
title: Shades by Sun and Heat
tagline: The shades draw as the sun climbs high and open as it drops, holding off unless the day is bright and hot.
description: >-
Closes the shades when the sun climbs past the elevation you set and opens
them as it drops, using Home Assistant's own sun position with no weather
service. Optional light and temperature sensors hold the close to genuinely
bright, hot afternoons, and speakers announce it.
author: Selora AI
released: "2026-08-28"
min_integration_version: "0.12.0"
tags: [outdoors, shades, climate]
roles:
# Shades / blinds — multi-select. kind cover. min_count 1.
- id: covers
title: Shades
kind: cover
min_count: 1
selection: required
description: >-
The shades or blinds the recipe closes and opens with the sun.
# Optional illuminance sensor gating the close to bright days. max_count 1.
- id: illuminance
title: Light level sensor
kind: sensor
device_class: illuminance
min_count: 0
max_count: 1
selection: required
description: >-
Optional. A light sensor so the shades only close when the light is
strong. Leave empty to close on the sun alone.
# Optional outdoor temperature sensor gating the close to hot days.
- id: outdoor_temp
title: Outdoor temperature sensor
kind: sensor
device_class: temperature
min_count: 0
max_count: 1
selection: required
description: >-
Optional. An outdoor temperature sensor so the shades only close when it
is hot. Leave empty to close on the sun alone.
# Optional announcement speakers over TTS. media_player. min_count 0.
- id: announce_speakers
title: Announcement speakers
kind: media_player
min_count: 0
selection: required
description: >-
Optional. Speakers that announce the shades closing over TTS. Leave empty
to run silently.
inputs:
# Sun elevation, in degrees above the horizon, at which the shades close.
# 40 degrees is late morning in most places.
- id: sun_elevation
type: number
label: Sun height (degrees)
description: >-
The sun's height above the horizon at which the shades close and, dropping
back through it, open.
default: 40
min: 0
max: 90
# Heat threshold, used only when a temperature sensor is picked. In the unit
# the sensor reports (for example 24 in Celsius or 75 in Fahrenheit).
- id: heat_threshold
type: number
label: Heat threshold (°C)
description: >-
With a temperature sensor picked, the shades only close above this
outdoor temperature. Set in Celsius; a sensor reporting Fahrenheit is
converted automatically, so the same number works either way. 24 °C is
roughly 75 °F.
default: 24
min: -10
max: 55
# Brightness threshold in lux, used only when a light sensor is picked.
- id: bright_lux
type: number
label: Brightness threshold (lux)
description: >-
With a light sensor picked, the shades only close above this reading.
default: 25000
min: 0
max: 150000
# TTS engine entity for the optional spoken announcement. Auto-resolved by the
# tts_engine resolver from the home's configured tts.* entities, so the wizard
# never shows this field. tts.speak works without a paid Home Assistant Cloud
# subscription. The resolver returns an empty string when the home has no TTS
# engine, and the template then skips the announcement. The default below is
# only used by the offline recipe-validation gate.
- id: tts_engine
type: string
label: Text-to-Speech engine
description: >-
Speech engine entity used for the spoken announcement. Resolved
automatically from the Text-to-Speech engines configured in Home
Assistant.
resolver: tts_engine
default: "tts.piper"
package_files:
- package/helpers/closed_state.yaml.j2
- package/automations/shades.yaml.j2
{# Shades by Sun and Heat: one automation closes the shades when the sun is
high and (if the sensors are picked) hot and bright, one opens them as the
sun drops back below the elevation threshold.
The close triggers on the sun elevation rising past the threshold AND, when a
temperature or light sensor is picked, on that reading rising past its
threshold too, so a morning that turns hot after the sun is already up still
closes the shades rather than missing the crossing. Every trigger is gated by
the same conditions (sun high, and hot and bright if those sensors are
picked), so it only ever closes when all are true. A cover-state guard means
a second trigger does not re-close or re-announce when the shades are already
shut.
The open side is guarded on a flag the close side raises, not on the covers'
own state, so the recipe only ever undoes a close it made itself, and a day
the shades never closed produces no spurious opening or announcement. What it
restores is a scene.create snapshot taken before the close, not a blanket
open, so a shade the homeowner had already drawn for privacy goes back to
drawn rather than being pulled open in the afternoon. #}
{% set su = slug | replace('-', '_') %}
{# The temperature role takes any temperature sensor, and numeric_state compares
the raw state with no unit conversion. A threshold that means "in whatever
unit your sensor reports" cannot have a single sensible default: 75 reads as
a warm afternoon in Fahrenheit and as an impossible one in Celsius, so on a
Celsius home picking the optional sensor silently stopped the shades ever
closing. The threshold is therefore declared in Celsius, HA's own canonical
temperature unit, and a Fahrenheit sensor is converted here. #}
{% macro celsius(e) -%}
((states('{{ e }}') | float(0) - 32) * 5 / 9 if state_attr('{{ e }}', 'unit_of_measurement') == '°F' else states('{{ e }}') | float(0))
{%- endmacro %}
{% set closed = 'input_boolean.' ~ su ~ '_closed' %}
automation:
- id: selora_recipe_{{ slug | replace('-', '_') }}_close
alias: "Shades by Sun and Heat: close"
description: >-
When the sun is high, and the day is bright and hot if those sensors are
picked, close the shades.
mode: single
trigger:
# numeric_state fires on a crossing only. Installed, or restarted, with the
# sun already above the threshold there is no crossing left that day, so
# the shades would stay open through the hottest part of it. The startup
# trigger re-asks the same question; every condition below still applies.
- platform: homeassistant
event: start
- platform: numeric_state
entity_id: sun.sun
attribute: elevation
above: {{ inputs.sun_elevation }}
{% if roles.outdoor_temp %}
- platform: template
value_template: "{% raw %}{{{% endraw %} {{ celsius(roles.outdoor_temp[0]) }} > {{ inputs.heat_threshold }} {% raw %}}}{% endraw %}"
{% endif %}
{% if roles.illuminance %}
- platform: numeric_state
entity_id: {{ roles.illuminance[0] }}
above: {{ inputs.bright_lux }}
{% endif %}
condition:
- condition: numeric_state
entity_id: sun.sun
attribute: elevation
above: {{ inputs.sun_elevation }}
{% if roles.outdoor_temp %}
- condition: template
value_template: "{% raw %}{{{% endraw %} {{ celsius(roles.outdoor_temp[0]) }} > {{ inputs.heat_threshold }} {% raw %}}}{% endraw %}"
{% endif %}
{% if roles.illuminance %}
- condition: numeric_state
entity_id: {{ roles.illuminance[0] }}
above: {{ inputs.bright_lux }}
{% endif %}
- condition: template
value_template: "{% raw %}{{ expand({% endraw %}{% for cover in roles.covers %}'{{ cover }}'{{ ', ' if not loop.last else '' }}{% endfor %}{% raw %}) | selectattr('state', 'ne', 'closed') | list | count > 0 }}{% endraw %}"
action:
# Snapshot every cover before touching it. One flag can only say "the
# recipe drew the shades", not which ones were already down: with a
# privacy shade closed by hand and the rest open, closing the group and
# later opening the group would pull that one open too. The snapshot
# records each cover's own position, so the afternoon restore puts every
# shade back exactly where it was, including the one that was already
# shut and any left part-way.
- service: scene.create
data:
scene_id: {{ su }}_before
snapshot_entities:
{% for cover in roles.covers %}
- {{ cover }}
{% endfor %}
- service: cover.close_cover
target:
entity_id:
{% for cover in roles.covers %}
- {{ cover }}
{% endfor %}
- service: input_boolean.turn_on
target:
entity_id: {{ closed }}
{% if roles.announce_speakers and inputs.tts_engine %}
- service: tts.speak
target:
entity_id: {{ inputs.tts_engine }}
data:
media_player_entity_id:
{% for player in roles.announce_speakers %}
- {{ player }}
{% endfor %}
message: "{% raw %}Closing the shades to keep the house cool.{% endraw %}"
{% endif %}
- id: selora_recipe_{{ slug | replace('-', '_') }}_open
alias: "Shades by Sun and Heat: open"
description: >-
When the sun drops back below the set height, open the shades.
mode: single
trigger:
# Same reasoning on the way back: a restart after the sun has already
# dropped would otherwise leave the shades down overnight with the flag
# still raised.
- platform: homeassistant
event: start
- platform: numeric_state
entity_id: sun.sun
attribute: elevation
below: {{ inputs.sun_elevation }}
condition:
# Implied by the crossing trigger, but stated so the startup path cannot
# throw the shades open at midday just because the flag is up.
- condition: numeric_state
entity_id: sun.sun
attribute: elevation
below: {{ inputs.sun_elevation }}
# Only reopen a close this recipe made. The old guard asked whether any
# cover was not open, which is just as true of a shade the homeowner drew
# for privacy or to keep a room dark, and the falling sun pulled those
# open too. The flag is the ownership record the comment always claimed.
- condition: state
entity_id: {{ closed }}
state: "on"
action:
# continue_on_error because a scene made by scene.create lives in memory
# only and does not survive a restart, while the flag does. A restart
# between the close and the open would otherwise raise here, abort the
# sequence, and leave the flag up with the shades down for good. Failing
# this step leaves the shades drawn, which the homeowner can undo, rather
# than wedging the recipe.
- service: scene.turn_on
continue_on_error: true
target:
entity_id: scene.{{ su }}_before
- service: input_boolean.turn_off
target:
entity_id: {{ closed }}
{% if roles.announce_speakers and inputs.tts_engine %}
- service: tts.speak
target:
entity_id: {{ inputs.tts_engine }}
data:
media_player_entity_id:
{% for player in roles.announce_speakers %}
- {{ player }}
{% endfor %}
message: "{% raw %}Opening the shades as the sun comes down.{% endraw %}"
{% endif %}
{# Shades by Sun and Heat helper: a flag marking that the recipe is the one that
drew the shades. The close automation raises it; the open automation requires
it before opening anything and clears it afterwards, so the recipe can only
ever reopen a close it made itself.
Without it the open side guarded on "some cover is not open", which is true of
any shade the homeowner had drawn for privacy or to keep a room dark, and the
falling sun would pull those open too. It survives a restart, so a close made
before a restart still lifts that afternoon. #}
input_boolean:
{{ slug | replace('-', '_') }}_closed:
name: "Shades by Sun and Heat: closed by recipe"
icon: mdi:blinds-horizontal-closed