Some help needed in combining multiple window cover automations based on azimuth into one single automation

TLDR Summary: I’d like to simplify my window cover automations based on Sun’s azimuth and elevation which currently span across 8 times 10 different automations (that is for 8 covers, 10 different azimuth value triggers per cover). I’d like to squeeze them into 1 automation per cover with some “If This Than That” coding magic inside the automation, if possible. Example of current automation below. If interested in the background, go on with reading my intro, otherwise skip to The Riddle section :blush:

Intro :

Dear Community,

I’m a huge fan of HA, and I’m done with a couple of integrations and automations, but what I’d like to achieve here is far beyond my programming capabilities :blush:

I’d like to automate my window covers based on how intense (meaning summer, don’t plan to use any LUX sensor, yet) and direct sunlight hits my windows, to maximize cooling effect during summer, and heating effect during winter (which seems to be easier since I just have to keep the covers open after sunrise :blush: )

I saw a couple of solutions, but they require far more knowledge than I’m in possession at the moment (node red blind automation, or appdaemon based python scripts), so for now I’d like to keep it simple and do it the old fashioned way, via automation.

The general idea is:

  • based on the azimuth range where each cover gets hit by sunlight, and based on the elevation range from where I know I’m into the summer season and heating effect is intense, I’d like to partially shut the cover.
  • Each 10 degree change in azimuth should correspond to a 10% movement of the cover.
  • from the start of the azimuth range until the peak value the covers have to close 10% at each 10 degree change of azimuth until they are closed 100% at the peak azimuth value, and from then on they should open gradually again by 10% steps with each 10 degree change of azimuth
  • there are a couple of condition on the automation whether it should fire or not:
  • sun above horizon
  • alarm system is disarmed (meaning we are not at sleep)
  • sun’s elevation is greater then a certain value, meaning we are already in the hot season
  • cover is not closed (which could mean my daughters are still sleeping even though the alarm system was disarmed by any other family member)

Here comes my problem: I can have (I mean: with my current programming capabilities :blush: ) 1 single automation to trigger a 10% percent movement action (either up or down, depending on the azimuth value) when the exact azimuth value kicks in. But since I have to cover around 100 degree change per each window, this would mean 10 different automation per cover, one for each 10 degree shift in azimuth!

(Eg:

  • at azimuth=100 degree, if cover is open, close it to 90% open state, if cover is closed, skip automation;
  • at azimuth=110 degree, if cover is open, close it to 80% open state, if cover is closed, skip automation;
  • … and so on, I’m sure you got the point

The Riddle:

How can I convert this single automation which only kicks in at one single predefined value of azimuth to work with multiple predefined values of azimuth, and trigger different cover positioning action tied to a certain azimuth value? (azimuth=100, then open cover to 90%, azimuth=110 then open cover to 80%, and so on, I have a complete table of this matrix which I can provide if needed). Doing this while checking a couple of conditions at each step which could block running the automation: if alarm is in armed state, do nothing, if sun is below horizon, do nothing, if cover was closed before a closing step, do nothing)

Current version (code copied from automation editor’s YAML view):

alias: Windows cover automation based on Sun's azimuth and elevation (EAST side, azimuth=150)
trigger:
  - platform: template
    value_template: '{{ state_attr(''sun.sun'', ''azimuth'') |int == 150}}'
condition:
  - condition: state
    entity_id: sun.sun
    state: above_horizon
  - condition: state
    entity_id: alarm_control_panel.area_1
    state: disarmed
  - condition: template
    value_template: '{{ state_attr(''sun.sun'', ''elevation'') |int > 30 }}'
  - condition: device
    device_id: e4b4762c0041876dc3f25858ffd64a9d
    domain: cover
    entity_id: cover.EAST1
    type: is_position
    above: 1
  - condition: device
    device_id: 6805b32285feb03729995581b171643d
    domain: cover
    entity_id: cover.EAST2
    type: is_position
    above: 1
action:
  - service: cover.set_cover_position
    target:
      device_id: cover.EAST1
    data:
      position: 80
  - service: cover.set_cover_position
    data:
      position: 80
    target:
      device_id: cover.EAST2
mode: single

Sorry if this post was too long, I wanted to be as clear and specific as I could.
Any help – preferably in the form of exact code :blush: – is greatly appreciated!

Anybody willing to help? Tried to condense the many if than that condition into a value template, but I very quickly got confused, really beyond my capabilities unfortunately :frowning:

you could try to set it up using a map for the azimuth values to the position:

trigger:
  - platform: state
    entity_id: sun.sun
condition:
  ....
  ....
action:
  - service: cover.set_cover_position
    target:
      entity_id: cover.east1
    data:
      position: >
        {% set mapper = {
          '100':'90',
          '110':'80',
          '90':'70',
          ...,
          '40':'10' }
          %}  
        {{ mapper[trigger.to_state.attributes.azimuth] }}
...
...

without knowing the entire matrix I just added examples but I think you get the idea. The first part is the azimuth, the second is the position. So you would need to complete the map with all of your azimuth:position values.

Just add that map to each of the automations.

It’s hard to know if you would be able to combine all of your cover entities into one automation since I’m not sure what all of the other requirements are for each different cover.

If they all move to exactly the same position for each azimuthal value then you could just put each one under the same service call and use the one mapper for the position for all of them. And you would end up with one automation.

Otherwise you will need to use a similar separate automation for each one.

EDIT:

I think your service xall data is incorrect too. There are no entities that use upper case.

I fixed it to what I think it should be.

thanks a lot @finity, very interesting and promising approach, seems just what I was looking for! :slight_smile:

no, they don’t all move together, on the contrary, since they are on opposite walls, they move just opposite directions, so I was originally counting on more than one automation which is absolutely fine and manageable.
This way I can make one automation for multiple covers on the same wall which brings total automation count down to 3 in total, which is superb! I’ll definitely give it a try tomorrow and report back!

yep, you’re absolutely right about my service call, upper case entitiy name was only used by me for highlighting reason, but you just made me realize it was a stupid decision. In my actual automation of course they are used in lower case, thx for pointing it out for other readers in the future, I’ll correct it in my final working solution once I have it :wink:

1 Like

Dear @finity,

I’ve put your code on the test bench, but something is not round, please help me investigate.

I have a feeling that the problem is around the service call somewhere, cause the automation is triggered by sun state (I see it on the history), but the cover is not moving with this setup (it is, with my original dumb previous version with the very same set of condition, so that is not causing the trouble).

History of the automation:

here’s the automation code unedited, as it comes out from the YAML editor of the GUI (note: this sign: > from your code suggestion got converted by the YAML editor into this: | )

alias: Redőny automatizáció teszt multiple trigger
trigger:
  - platform: state
    entity_id: sun.sun
condition:
  - condition: state
    entity_id: sun.sun
    state: above_horizon
  - condition: state
    entity_id: alarm_control_panel.area_1
    state: disarmed
  - condition: template
    value_template: '{{ state_attr(''sun.sun'', ''elevation'') |int > 30 }}'
  - condition: device
    device_id: e4b4762c0041876dc3f25858ffd64a9d
    domain: cover
    entity_id: cover.medence_terasz_ajto_redny
    type: is_position
    above: 1
  - condition: device
    device_id: 6805b32285feb03729995581b171643d
    domain: cover
    entity_id: cover.medence_terasz_redny
    type: is_position
    above: 1
action:
  - service: cover.set_cover_position
    target:
      entity_id: cover.medence_terasz_ajto_redny
    data:
      position: |
        {% set mapper = {
          '160' : '90',
          '161' : '80',
          '162' : '70',
          '163' : '20',
          '164' : '90',
          '165' : '10', }
          %}
        {{ mapper[trigger.to_state.attributes.azimuth] }}
mode: single

try adding casting the value to a ‘float’:

{{ mapper[trigger.to_state.attributes.azimuth] | float}}

Or maybe an ‘int’.

I’m not sure what the ‘position’ key is expecting.

But you should have an error in the logs to tell you what the error is.

tried float, tried int, doesn’t work. I caught this in the log:

Error while executing automation automation.redony_automatizacio_teszt: expected int for dictionary value @ data['position']
Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 117.3
Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 118.19

two types of error are in the logs,

first:

Redőny automatizáció teszt multiple trigger: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: dict object has no element 135.88
Redőny automatizáció teszt multiple trigger: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: dict object has no element 136.97
Redőny automatizáció teszt multiple trigger: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: dict object has no element 138.07
Redőny automatizáció teszt multiple trigger: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: dict object has no element 139.19
Redőny automatizáció teszt multiple trigger: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: dict object has no element 140.32

Second:

  • Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 135.88
  • Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 136.97
  • Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 138.07
  • Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 139.19
  • Error while executing automation automation.redony_automatizacio_teszt: Error rendering data template: UndefinedError: dict object has no element 140.3

Ah, I think I see what is happening.

since the azimuth changes on every 0.1 degrees the template is only mapped to 10 degree changes.

So there is no corresponding mapped value for 140.3 degrees. only 160, 161, 162, etc.

please post the matrix of values that you are trying to watch for and the associated positions and I’ll try to figure out how to get what you want to do.

aham, makes sense. so there should be a range definition itself, right? Like: current azimuth is above 140 and below 150 then position should be 90.

Anyhow, since I can’t translate it into a proper code, I leave it to the professionals and admire their job :slight_smile:

I uploaded the full matrix into a google spreadsheet, obviously different sides have different value mappings, but if you only guide me how to do it for one exact cover, I hope I’ll be able to do the rest…

Link to azimuth-cover position mapping matrix

I really appreciate you helping me out here @finity!!

Ok…

completely change of attack…

The matrix info changed everything.

There is no way to use a map to get the values for position. So you’ll have to use a different template for the position in the service call for each cover that uses a different matrix.

I’ve worked out the math for the templates (high school geometry came in pretty handy) but there were two covers I had to modify the matrix a bit to get the math to be easier by making it more linear.

You can use the same automation as above but in the “position:” field use the following templates:

cover 1, cover 2:

{% if states('sensor.sun_azimuth') | float < 80 %}
  0
{% elif states('sensor.sun_azimuth') | float >= 170 %}
  100
{% else %}
  {{ (((states('sensor.sun_azimuth') | float - 70 ) / 10) | int) * 10 }}
{% endif %}

cover 3, cover, 4, cover 5:

{% if states('sensor.sun_azimuth') | float < 80 %}
  0
{% elif states('sensor.sun_azimuth') | float >= 80 and states('sensor.sun_azimuth') | float < 140 %}
  {{ (((-2 * (states('sensor.sun_azimuth') | float )  + 280) / 10) | int) * 10 }}
{% elif states('sensor.sun_azimuth') | float >= 140 and states('sensor.sun_azimuth') | float < 220 %}
  0
{% elif states('sensor.sun_azimuth') | float >= 220 and states('sensor.sun_azimuth') | float <= 250 %}
    {{ ((( 2 * (states('sensor.sun_azimuth') | float )  - 420) / 10) | int) * 10 }}
{% else %}
  100
{% endif %}

Cover 6:


{% if states('sensor.sun_azimuth') | float < 80 %}
  0 
{% elif states('sensor.sun_azimuth') | float >= 80 and states('sensor.sun_azimuth') | float < 240 %}
  {{ ((( 270 -  (states('sensor.sun_azimuth') | float )) / 10) | int) * 10 }}
{% elif states('sensor.sun_azimuth') | float >= 240 and states('sensor.sun_azimuth') | float < 250 %}
  20
{% elif states('sensor.sun_azimuth') | float >= 250 and states('sensor.sun_azimuth') | float < 260 %}
  0
{% else %}
  {{ (((4 *  (states('sensor.sun_azimuth') | float) - 1020 ) / 10) | int) * 10 }}
{% endif %}

Cover 7, Cover 8:
Changed the following
250 = 0
260 = 20
270 = 40
280 = 60
290 = 80
300 = 100

{% if states('sensor.sun_azimuth') | float < 80 %}
  0 
{% elif states('sensor.sun_azimuth') | float >= 80 and states('sensor.sun_azimuth') | float <= 170 %}
  100
{% elif states('sensor.sun_azimuth') | float >= 170 and states('sensor.sun_azimuth') | float < 220 %}
  {{ (((-2 * (states('sensor.sun_azimuth') | float )  + 440) / 10) | int) * 10 }}
{% elif states('sensor.sun_azimuth') | float >= 220 and states('sensor.sun_azimuth') | float <= 270 %}
  0
{% else %}
  {{ ((( 2 * (states('sensor.sun_azimuth') | float )  - 500) / 10) | int) * 10 }}
{% endif %}

you’ll notice in covers 7 & 8 I needed to change a couple of ranges as noted.

I hope I got it all right. It was a lot of “cyphering”. :laughing:

wow, it’s overwhelming for the first sight :smiley: :smiley:

I need a day or two to understand it. What’s the math behind it?

What I don’t get on the first look: what’s the need for using percentage value for azimuth? I mean, that’s what I understand from the very first line… Why don’t we use exact values?

it’s the slope-intercept form of a straight line formula.

y=mx + b

Where m = the slope of the line (rise over run) and b = the y axis intercept point (y = 0).

And because you have several inflection points in your “graph” (which is basically what your matrix is - its a chart of “x,y” coordinates on a graph) then you need to calculate each segment separately. Hence why you need a few If-elif-else conditions in the template so it uses the correct formula depending on where you are on the graph.

I don’t follow.

I’m using the azimuth (the x value in the y=mx+b formula) to solve for the percentage (the y value).

the rest of it is diving by 10, converting to an integrer (removing the decimal values) then multiplying that by 10 again to get the intervals in the tens (10, 20, 30…)

example.

say the azimuth is 114.3 degrees.

according to the matrix for cover 1 the percent is 40%.

If you do the math for the percent it would come out to 44.3% for the value. Your matrix is in intervals of 10%.

so to get those:

44.3/10 = 4.43
convert to an integer is 4
4*10 = 40%

so the value for 114.3 degrees results in a position of 40%.

EDIT:

Oh, and the reason I needed to modify the matrix a bit where I did is because using the points you had originally didn’t result in the graph being a straight line. It was a curved line. And those are a bunch harder to find the formula for. So I cheated a bit and changed the graph. :stuck_out_tongue_closed_eyes:

Thanks a lot mate, makes much more sense even to me now :slight_smile:

I’ll try it out and report back.
PS: now I have to exchange one of my relays that control my shutters (Shelly 2.5), because this little bastard decided after 2 years to die on me. The power measurement chip died in it, so I can’t position it into exact values anymore, only wide open, or totally closed. I’m starting to loose my faith in Shelly devices after investing into a couple of them… My light control switches (Shelly 1L) are randomly stuck in ON position, so the lights can’t be turned off. EVERY time I have to remove the light switches, and give a slight tap on the back of the Shelly box, so the relay releases itself. Good for a couple of days, and boom, stuck again. Soooo annoying. And now my so far so much trusted Shelly 2.5 died.

I’ve got a few Shelly 1’s and haven’t had any issues with them at all.

Maybe you got lucky and ran into a bad batch. Or I got lucky with a good batch. :laughing:

And you’re welcome :slightly_smiling_face:

Hopefully I got it right.

okay, so I tried your code, went straight to cover 7 and 8, since the sun is already setting. Not what I expected…

When I enabled the automation, the cover went fully down at azimuth 260, when it’s supposed to go down to 20% only. I waited for Azimuth 270, but nothing happened… triggering work, according to the automation’s history, but again, the action part has some problem…

Here’s the code in it’s current version, removed all conditions to avoid any side issues…

alias: Cover automation with multiple azimuth trigger
trigger:
  - platform: state
    entity_id: sun.sun
condition: []
action:
  - service: cover.set_cover_position
    target:
      entity_id: cover.cover7
    data:
      position: >
        {% if states('sensor.sun_azimuth') | float < 80 %}
          0 
        {% elif states('sensor.sun_azimuth') | float >= 80 and
        states('sensor.sun_azimuth') | float <= 170 %}
          100
        {% elif states('sensor.sun_azimuth') | float >= 170 and
        states('sensor.sun_azimuth') | float < 220 %}
          {{ (((-2 * (states('sensor.sun_azimuth') | float )  + 440) / 10) | int) * 10 }}
        {% elif states('sensor.sun_azimuth') | float >= 220 and
        states('sensor.sun_azimuth') | float <= 270 %}
          0
        {% else %}
          {{ ((( 2 * (states('sensor.sun_azimuth') | float )  - 500) / 10) | int) * 10 }}
        {% endif %}
mode: single

Some additional info: action seems to be working, but maybe not exactly how I imagined :slight_smile:

So after turning on the automation and the cover went fully down I pulled the cover up manually, and then again it went down after a few seconds. The cover’s history does not give any indication to what caused the closing event. I can see that I opened it by homekit, but the closing event does not say anything related.

@finity, this morning the same happened. After a couple of minutes when I opened the covers in the morning, they went straight fully down. (cover7 and 8 with the above code). Many thanks for your debugging efforts!!

Do the rest work and only 7 & 8 are acting up?

I just looked at the code and I think I see the issue.

I used your old values for the inflection points (if, elif…) but used my new numbers for the equations.

try this for those two:

        {% if states('sensor.sun_azimuth') | float < 80 %}
          0 
        {% elif states('sensor.sun_azimuth') | float >= 80 and
        states('sensor.sun_azimuth') | float <= 170 %}
          100
        {% elif states('sensor.sun_azimuth') | float >= 170 and
        states('sensor.sun_azimuth') | float < 220 %}
          {{ (((-2 * (states('sensor.sun_azimuth') | float )  + 440) / 10) | int) * 10 }}
        {% elif states('sensor.sun_azimuth') | float >= 220 and
        states('sensor.sun_azimuth') | float <= 250 %}
          0
        {% else %}
          {{ ((( 2 * (states('sensor.sun_azimuth') | float )  - 500) / 10) | int) * 10 }}
        {% endif %}

I changed the fourth inflection point from ‘<= 270’ to ‘<=250’.

And I see you figured out that you needed to either tag my username to get my attention. Or you can also use the reply button in my post to do the same. If you just reply to the thread I’ll eventually see it in my ‘unread’ posts list but it won’t pop up in my notifications.

I had no idea that you posted those other two posts yesterday because I hadn’t gotten around to reading thru my unread threads yet. Busy day yesterday. :laughing:

hopefully that solves you issue tho.

you needed to either tag my username to get my attention

yep, correct, forgot to do that @finity, won’t miss it again :slight_smile:

Busy day yesterday.

no worries, absolutely no time pressure, whenever you have the chance to help me I’m still freakin’ grateful…

Do the rest work and only 7 & 8 are acting up?

To be honest, currently I’m testing with only one. During covid home office and home schooling I’m stuck working at the dining table, sitting at the south cover (7&8), so I’m testing with this one because I can see what is going on :slight_smile:

try this for those two:

Exchanged the code to this one at 10:00 at azimuth 127 degree and again, boom, the cover went down. I wait for the afternoon to see if it comes up gradually or not. It seems that it doesn’t want to be open when it should (this one should be open from 80 up until azimuth reaches value 170, then it should start closing).