Skip to main content
Selora Homes Selora Homes
Recipe · v1.0.0 · Released Aug 28, 2026

Shades by Sun and Heat

Installs a Home Assistant automation that closes your shades when the sun climbs high and the day is hot, and opens them again as the sun drops, using Home Assistant's own sun position. Add a light and temperature sensor to hold it to genuinely bright, hot afternoons.

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

Type to search across

↑↓ navigate ↵ open
⌘K search