Automatic blinds / sunscreen control based on sun platform

Bas from DD discord pointed me to a mistake. I thought setting the cover to 100% would fully close it but 100% is actually fully open. Oops. The templates are updated and it should work now.

Edit: I fixed another mistake. Inline comments weren’t processed properly by HA so the template didn’t work. It should finally work now…

To create easily multiple automations for all sides of your home I have build a Blueprint.
The Blueprint uses the sensor you have made based on the template code by @langestefan.

You can configure a time-out, minimum percentage change before adjusting the cover and add multiple actions and conditions to it.

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

---
#version 1.0.5
blueprint:
  name: Cover Height
  description: "`version 1.0.5` \n
    Set cover position based on direct sunlight exposed to window \n\n
    Calculations are done internally in the blueprint removing the need to use a sensor to input a value
    \n for in depth information on the calculations and variables, check this forum [post](https://community.home-assistant.io/t/automatic-blinds-sunscreen-control-based-on-sun-platform/573818)
    \n **Code Owner:** [`@langestefan`](https://community.home-assistant.io/u/langestefan/)
    \n **Blueprint by:** [`@basbruss`](https://community.home-assistant.io/u/basbruss/)"
  domain: automation
  input:
    cover_entity:
      name: Cover
      description: "Cover(s) to change position based on sun. \n only select entities. Devices will not work!"
      selector:
        target:
          entity:
            domain: cover
    azimuth:
      name: Azimuth
      description: The azimuth of the window/cover [**?**](https://community-assets.home-assistant.io/original/4X/5/2/7/527029c7c138eb6146aac68d34e92376b4560fb6.png)
      default: 180
      selector:
        number:
          min: 0
          max: 359
          mode: slider
          step: 1
    distance:
      name: Distance
      description: Distance from the cover to shaded area ![**?**](https://community-assets.home-assistant.io/original/4X/f/2/6/f26221689c32f55b731c5931de5a52791b760e90.jpeg).
      default: 0.5
      selector:
        number:
          min: 0
          max: 3
          mode: slider
          step: 0.1
          unit_of_measurement: "M"
    max_height:
      name: Cover Height
      description: Max height of the cover in Meters.
      default: 2.1
      selector:
        number:
          min: 0
          max: 4
          step: 0.1
          unit_of_measurement: "M"
    min_height:
      name: Minimun Height
      description: The minimum height in meters when the blinds are fully open. This will be 0 in most cases.
      default: 0
      selector:
        number:
          min: 0
          max: 4
          step: 0.1
          unit_of_measurement: "M"
    default_height:
      name: Default Cover Height
      description: The default height of the cover when the sun is not within the range of the window/cover
      default: 60
      selector:
        number:
          min: 0
          max: 100
          mode: slider
          step: 1
          unit_of_measurement: "%"
    default_template:
      name: Default height template
      description: Overrules set value in **Default Cover Height**
      default: ""
      selector:
        template:
    minimum_position:
      name: Minimum position cover
      description: The lowest position allowed to change the cover to.
      default: 0
      selector:
        number:
          min: 0
          max: 100
          mode: slider
          step: 1
          unit_of_measurement: "%"
    degrees:
      name: Field of view
      description: Amount of degrees relative to the sun' azimuth; (90 degrees equals 180 fov)
      default: 90
      selector:
        number:
          min: 0
          max: 90
          mode: slider
          step: 1
          unit_of_measurement: "°"
    azimuth_left:
      name: "Field of view Left"
      description: "**Only change when left and right angles are different**
        \n Amount of degrees from left side of the window \n
        Only use when left and right angles are not equal"
      default: 90
      selector:
        number:
          min: 0
          max: 90
          mode: slider
          step: 1
          unit_of_measurement: "°"
    azimuth_right:
      name: "Field of view Right"
      description: "**Only change when left and right angles are different**
        \n Amount of degrees from left side of the window \n
        Only use when left and right angles are not equal"
      default: 90
      selector:
        number:
          min: 0
          max: 90
          mode: slider
          step: 1
          unit_of_measurement: "°"
    max_elevation:
      name: Maximum Elevation
      description: Maximum angle of elevation
      default: 90
      selector:
        number:
          min: 0
          max: 90
          mode: slider
          step: 1
          unit_of_measurement: "°"
    min_elevation:
      name: Minimum Elevation
      description: Minimum angle of elevation
      default: 0
      selector:
        number:
          min: 0
          max: 90
          mode: slider
          step: 1
          unit_of_measurement: "°"
    change_threshold:
      name: Minimun percentage change
      description: The minimum percentage change to current position of the cover(s) to change position (to save battery)
      default: 0
      selector:
        number:
          min: 0
          max: 100
          mode: slider
          step: 1
          unit_of_measurement: "%"
    time_out:
      name: Time-out
      description: Minimum time between updates (to save battery)
      default: 1
      selector:
        number:
          min: 0
          max: 60
          mode: slider
          step: 1
          unit_of_measurement: minutes
    condition_mode:
      name: Condition mode
      description: Set mode of above conditions to AND or OR
      default: and
      selector:
        select:
          options:
            - label: AND
              value: and
            - label: OR
              value: or
    condition:
      name: Extra Conditions
      description: Extra conditions for the automation
      default: []
      selector:
        action: {}
    action:
      name: Extra Actions
      description: Extra actions to run before intial service
      default: []
      selector:
        action: {}
variables:
  cover_entity: !input cover_entity
  azimuth: !input azimuth
  distance: !input distance
  max_height: !input max_height
  min_height: !input min_height
  default_height: !input default_height
  min_position: !input minimum_position
  degrees: !input degrees
  default_template: !input default_template
  azimuth_left: !input azimuth_left
  azimuth_right: !input azimuth_right
  max_elevation: !input max_elevation
  min_elevation: !input min_elevation
  cover_height: >
    {%- set deg2rad = pi/180 -%}
    {# normalize in range [0,1] #}
    {%- macro norm(x, min, max) %}
      {{ (x - min) / (max - min) }}
    {%- endmacro %}
    {# convert blind height h to percentage [0,100] #}
    {%- macro h2perc(x) %}
      {{ 100 * float(norm(x, h_min, h_max)) }}
    {%- endmacro %}
    {# clip value between [min, max] #}
    {%- macro clipv(x, x_min, x_max) %}
      {{ max(min(x, x_max), x_min) }}
    {%- endmacro %}
    {# constants #}
    {%- set win_azi = azimuth -%}
    {%- set left_azi = azimuth_left | default(90) -%}
    {%- set right_azi = azimuth_right | default(90) -%}
    {%- set elev_high = deg2rad * (max_elevation | default(90)) -%} {# Maximum: 90 #}
    {%- set elev_low = deg2rad * (min_elevation| default(0)) -%} {# Minimum: 0 #}
    {%- set d = distance | default(0.5) -%}
    {%- set h_max = max_height | default(2.10) -%}
    {%- set h_min = min_height | default(0) -%}
    {%- set deg = degrees | default(90) -%}
    {%- set def = default_height | default(60) -%}
    {%- set min_pos = min_position | default(0) -%}
    {%- set def_temp = default_template | default('') -%}
    {% if def_temp | int(-1) >= 0 %}
      {% set def = def_temp %}
    {%endif%}

    {# FOV #}
    {%- if left_azi != right_azi-%}
      {%- set azi_left = deg2rad * -left_azi -%} {# Minimum: -90 #}
      {%- set azi_right = deg2rad * right_azi -%} {# Maximum: 90 #}
    {%-else-%}
      {%- set azi_left = deg2rad * -deg -%} {# Minimum: -90 #}
      {%- set azi_right = deg2rad * deg -%} {# Maximum: 90 #}
    {%-endif-%}
    {%- set fov = deg2rad * deg -%}
    {# get sun elevation / azimuth from sun.sun #}
    {%- set sun_azi = state_attr('sun.sun', 'azimuth') -%}
    {%- set sun_ele = state_attr('sun.sun', 'elevation') -%}
    {# default height, when automatic control is off. #}
    {%- set def_h = def / 100 * h_max -%}
    {%- set alpha = deg2rad * sun_ele -%}
    {%- set gamma = deg2rad * (win_azi - sun_azi) -%}    
    {%- set h = (d / cos(gamma)) * tan(alpha) -%}
    {# gamma is outside of FOV #}
    {%- if gamma < azi_left or gamma > azi_right or alpha < elev_low or alpha > elev_high -%}
      {{ clipv(h2perc(def_h) | round(0) | int , 0, 100) }}
    {# gamma is inside of FOV #}
    {%- else -%}  
      {{ clipv(h2perc(h) | round(0) | int , min_pos, 100) }}
    {%- endif -%}
  change_threshold: !input change_threshold
  time_out: !input time_out
  condition_mode: !input condition_mode
trigger:
  - platform: state
    entity_id:
      - sun.sun
condition: !input condition
action:
  - choose:
      - conditions:
          - condition: template
            value_template: "{{condition_mode == 'and'}}"
        sequence:
          - condition: and
            conditions:
              - condition: template
                value_template: >
                  {%set position = state_attr(cover_entity.entity_id,'current_position')%}
                  {{ ((position | float - cover_height | float) | abs > change_threshold)
                  or (cover_height in [default_height, default_template] and position not in [default_height, default_template])}}
              - condition: template
                value_template: >
                  {{now() - timedelta(minutes=time_out) >= states[cover_entity.entity_id].last_updated }}
          - choose: []
            default: !input action
          - service: cover.set_cover_position
            data:
              position: "{{ cover_height | int(0) }}"
            target: !input cover_entity
      - conditions:
          - condition: template
            value_template: "{{condition_mode == 'or'}}"
        sequence:
          - condition: or
            conditions:
              - condition: template
                value_template: >
                  {%set position = state_attr(cover_entity.entity_id,'current_position')%}
                  {{ ((position | float - cover_height | float) | abs > change_threshold)
                  or (cover_height in [default_height, default_template] and position not in [default_height, default_template])}}
              - condition: template
                value_template: >
                  {{now() - timedelta(minutes=time_out) >= states[cover_entity.entity_id].last_updated }}
          - choose: []
            default: !input action
          - service: cover.set_cover_position
            data:
              position: "{{ cover_height | int(0) }}"
            target: !input cover_entity
mode: single

4 Likes

Hi mate,

Thanks for the good work.

I started to implement and, even if it works, I am trying to fine tune my templates.
I have some few questions for you:

  1. I am not sure I understand h_max and h_min. Are those heights mesured from the ground?
    For example
  • If I have a sliding window (which is 2.1m high) on my ground floor,
    h_min=0
    h_max=2.1

  • If I have a regular window (which would be 1.4m high) at the first floor, then
    h_min= 3.5 (the first floor is roughly 2.5m high then the window sits 1m above that)
    h_max=3.5+1.4=4.9 ?

  • Or maybe the height is measured from the sea level?

  1. I have those kinds of blinds:
    image
    When I set the position to 0, they are completely closed and let no light in. Usually, ona very sunny day, I set the position to 15% because then, I will still see a bit of light through the small holes of the blind which is much more comfortable (This blocks the direct sun light but let a bit of light in)
    My question is, how can I set the minimum position to 15% instead of 0%?

Thanks a lot!

h_max is the height of the window. h_min you can just put to 0. I also tried to explain it in the post:

My question is, how can I set the minimum position to 15% instead of 0%?

You can change this line:

{{- clipv(h2perc(h) | round(0) | int, 0, 100) -}}

To clip at 15%:

{{- clipv(h2perc(h) | round(0) | int, 15, 100) -}}

Thanks for the reply. Very helpful.
Sorry to be fussy but with that definition of h_min and h_max it means that the 2 same windows with the same azimuth, 1 being at the ground floor and the orther one being at the first or even 2nd floor will have the same result?

I might be wrong but I was expecing them to have a different result

The height does matter slightly, but I cannot find any resource that describes how to take it into account so the effect must be neglible.

I did find this resource: Ask Tom: Do times for sunrise and sunset not take altitude into account?

So sunrise is about 1 minute earlier for every 1500 meters altitude above sea level. So I guess we can neglect it.

Perhaps another way to look at it is that the sun’s rays hit the earth at the same angle no matter what height you are:

image

1 Like

Very clear and interesting.
Thanks for taking the time to answer.

So, I am using your template and it’s working beautifuly.
I use the output of the template in Nodered to automate my blinds.

Cheers

1 Like

I don’t have any blinds to use this for but I’m interested in seeing the numbers. I’m struggling with the window azimuth part though. I don’t understand what part of the compass needs to be where :confused:

Can anyone explain that part for dummies please?

You want to measure the azimuth angle of your window in degrees, clockwise, from north (0°). For example if your window faces exactly south it’s at an azimuth angle of 180°. If it faces exactly east it’s at an angle of 90°, if it faces exactly west 270° etc. So basically align the compass such that it points in exactly the same direction as your window is facing.

image

I think this is awesome project :slight_smile:
Do you think it will be possible to add this to blueprint? Not sure if it is possible in full scale, but partially at least?

Not entirely sure what you mean? There is a blueprint here above shared by @basbrus. I will also put it in the main post

I Like this Project, more flexible and looks very good.
After testing something goes wrong on my part und i do not understand the reason. My Sensor looks like:

- sensor:
    name: Solar blinds height percentage Osten
    unique_id: blinds_height_perc_east
    unit_of_measurement: '%'
    state: >
       {% set deg2rad = pi/180 %}

        {%- macro norm(x, min, max) %}
        	{{ (x - min) / (max - min) }}
        {%- endmacro %}
        
        {# convert blind height h to percentage [0,100] #}
        {%- macro h2perc(x) %}
        	{{ 100 * float(norm(x, h_min, h_max)) }}
        {%- endmacro %}

        {%- macro clipv(x, x_min, x_max) %}
        	{{ max(min(x, x_max), x_min) }}
        {%- endmacro %}

        {% set win_azi = 108.25 %}
        {% set d = 0.5 %}
        {% set h_max = 2.15 %}
        {% set h_min = 0 %}
        
        {# field of view you want to enable control #}
        {# !!! MUST be equal or lower than 90 degrees !!! #}
        {# > 90 degrees is behind the window and then calculation is invalid! #}
        {% set fov = deg2rad * 60 %}
        
        {# get sun elevation / azimuth from sun.sun #}
        {% set sun_azi = state_attr('sun.sun', 'azimuth') %}
        {% set sun_ele = state_attr('sun.sun', 'elevation') %}
        
        {% set def_h = 0.6 * h_max %}
        
        {% set alpha = deg2rad * sun_ele %}
        {% set gamma = deg2rad * (win_azi - sun_azi) %}    

        {% set h = (d / cos(gamma)) * tan(alpha) %}

        {% if (alpha > 0) and (gamma | abs < fov) %}
            {{ clipv(h2perc(h) | round(0) | int , 50, 100) }}
        {% else %}  
            {{ clipv(h2perc(def_h) | round(0) | int , 50, 100) }}
        {% endif %}

The calculation itself works but in time where the blinds have to be set to 100% it jumps to 60%
image

Hopefully someone have an idear whats going wrong

Thanks in Advanced

Did you set the azimuth angle correctly? It jumps to 60% (the standard value def_h = 0.6 * h_max) when either the sun moves behind the window or the sun moves below the horizon. Since it happened


I am Living in the building 55 next to the border of the compass

So far as i know its right. What do i have to do to set the default to 100% (full open) after the sun moves around the corner

Yes if your window faces south east then that’s fine. To change the default value you can change this line:

{% set def_h = 0.6 * h_max %}

To:

{% set def_h = h_max %}

This will set the default height to 100% so the window will be fully open.

PS. I think it will be easier for you if you import the blueprint. The import button is at the top of this post

1 Like

i am still using your blueprint. it is perfect. Many thanks for your work. But i need this sensor for working or i am wrong?

edit: I have put a new blueprint at the top of the page which does not need a template sensor anymore!

You can also find it here: :sun_with_face: Automatic blinds / sunscreen control based on sun platform

1 Like

i tested it, but i always get a error

and my blueprint code

alias: Sonnenschutz - Wohnzimmer Automatisch
description: ""
use_blueprint:
  path: basbruss/cover_height_sun.yaml
  input:
    cover_entity:
      device_id: 55d960f40a449d4e7586a344e92fe3cd
    azimuth: 314
    max_height: 1.3
    default_height: 100
    change_threshold: 3

can you perhaps help @langestefan

You will need to use the entity_id instead of the device_id. Entity_id also adds the attributes to the dataset :wink:

1 Like

you are right, i think now it works, gives a way to disable the automation ?

for example when i want to control the cover manuall ?