Shading automation

As this is a recurring question, I’m sharing my ruleset how I’ve setup automated shading using rollershutters and blinds.
Note it’s a full-blown setup that you will likely not need all the parts of. So take this as a starting point and remove and adapt the parts specific to your needs.
Sorry for all the German variable names. Some rainy day, maybe …

The code is definitely not ‘elegant’ and more repititive and verbose than it would need to be efficient, but I prefer to have it this way as this isn’t on programming but on learning, adapting and to ease spotting errors.
Let me know the places where you need further explanation so I can enhance this documentation over time.

  • There’s a number of rollershutters and some venetian blinds (Raffstore or Jalousien in German) in my house.The latter allow for setting the lamella tilt angle, and all of my rollershutters and blinds are capable of driving to a particular percentage.
    A hint to anyone using Fibaro FGRM actuators to operate their venetian blinds: deploy the 2.4M4 milestone or newer to get lamella tilt control to work.
    The morning (eastern) side is exposed to sunlight in the morning hours from sunrise until about 12:45, and sun starts shining into the [after]noon (mostly southern) side windows at 10:15 until around 18:30 (6:30 PM).
    There’s a multi sensor on each side of the house to track luminance and temperature. The southern one is located inside, the eastern one outside of the house, that why there’s different thresholds they’re compared against.

  • The fundamental idea behind this automation is that while the zombie rate among nerds is said to be high, you usually don’t want darkness but just protection from the heat. So I just shade as little as possible.
    This is why I’ve built two zones (morning and noon/afternoon sides of the house) that are controlled independent from each other as the sun is moving around the house. There’s some overlap, but most of the time, one of the two sides is open. I’m also actually driving my shutters to around 35% only. This will block most direct sunrays but will allow for some light to enter. The ‘high’ protection level is to drive them to 75% which still allows for some lightray pass the spacing between lamellas.
    For venetian blinds, I’ve implemented a two-stage shading algorithm: up to a lower luminance threshold, the blinds will be lowered but lamellas will be set to open. When the upper threshold is reached, they, too, will be closed.

You can and must adapt all of these parameters to your specific housing situation.
Yes, it’s far from being perfect, you need to change the code as not even all of the thresholds are changeable via items …
You should experiment with the core ‘shade?’ evaluation function to include those aspects that you feel to be most important to you. I’ve experimented with using temperature and cloudiness forecasts from the weather binding (unreliable), a UV value my sensor delivers (found that to always be 0, so decided I don’t need to check it, but if you happen to live in the Australian Outback … well :slight_smile: ), various luminance levels and other stuff.

  • Shading is based on luminance (sunlight intensity) and temperature measured by the sensors.
    The core evaluation function actually is this. Easy to understand for Germans I guess.

    var boolean Schutzbedarf_morgens = (now.millis > Verschattung_Anfang_morgens) && (now.millis < Verschattung_Ende_morgens) && ((hell_genug_morgens && zu_warm_morgens) || zu_hell_morgens)

Shading happens based on the moving average of the values the sensors are delivering. To flatten out peaks, configure them to deliver values often enough to fit into the time window or increase the window size. Remember to enable “everyMinute” persistence on these items so there’s at least one up to date value when averageSince() is calculated.
Both of these procedures are to compensate for peaks (and errors) in measurements to avoid constant UPs and DOWNs on cloudy days.

  • Sonnenschutzschaltung is a global switch to enable/disable shading automatism.
    Nachtschaltung is a global switch enabled during nighttime only.
    Tageslicht is a global switch that is ON whenever there’s daylight. This is updated based on astro binding events.
    Aussentemperatur_Hysterese_Sonnenschutz is a variable to compensate for higher temperatures if you expose a sensor to direct sunlight instead of measuring the temperature in the shadows. You can’t have both, correct luminance and (shadow) temperature, in a single sensor.


Number Lux_Terrasse_morgens "Durchschnitt Ostseite [%.0f lux]"                                          <sun>                   (Garten_Status)
Number Lux_Terrasse_mittags "Durchschnitt Südseite  [%.0f lux]"                                         <sun>                   (Garten_Status)
Number Lux_Grenzwert_Sonnenschutz_morgens_niedrig "Schwellwert unten morgens [%d lux]"                  <sun>                   (Garten_Status,Einstellungen)
Number Lux_Grenzwert_Sonnenschutz_morgens_hoch "Schwellwert oben morgens [%d lux]"                      <sun>                   (Garten_Status,Einstellungen)
Number Lux_Grenzwert_Sonnenschutz_mittags_niedrig "Schwellwert unten mittags [%d lux]"                  <sun>                   (Garten_Status,Einstellungen)
Number Lux_Grenzwert_Sonnenschutz_mittags_hoch "Schwellwert oben mittags [%d lux]"                      <sun>                   (Garten_Status,Einstellungen)
Number UV_Grenzwert_Sonnenschutz "UV Schwellwert Sonnenschutz [UV index %.0f]"                          <sun>                   (Garten_Status,Einstellungen)

Number Aussentemperatur_Grenzwert_Sonnenschutz "Schwellwert Sonnenschutz aussen [%.1f °C]"              <temperature>           (Garten_Status,Einstellungen)
Number Innentemperatur_Grenzwert_Sonnenschutz "Schwellwert Sonnenschutz innen [%.1f °C]"                <temperature>           (Garten_Status,Einstellungen)
Number Aussentemperatur_Hysterese_Sonnenschutz "Hysterese Sonnenschutz aussen [%.1f °C]"                <temperature>           (Garten_Status,Einstellungen)
Number Innentemperatur_Hysterese_Sonnenschutz "Hysterese Sonnenschutz innen [%.1f °C]"                  <temperature>           (Garten_Status,Einstellungen)
Number Rollaedenstatus "Rolladen auf/zu [MAP(]"                                        <rollershutter>         (Einstellungen)
Switch Rolladenautomatik "Rolladenautomatik hausweit [MAP(]"                               <rollershutter>         (Einstellungen)
Switch Sonnenschutz_Bianca "Sonnenschutz Bianca [MAP(]"                                    <daylight>              (EG_Bianca,Einstellungen)
Switch Sonnenschutzschaltung "Sonnenschutzschaltung [MAP(]"                                <blinds>                (Einstellungen)
Switch Sonnenschutz "Sonnenschutz [MAP(]"                                                  <blinds>
Number Rollaedenoeffnen "Rolläden öffnen"                                                               <rollershutter>
Switch Rollaedenschliessen "Rolläden schliessen"                                                        <rollershutter>


import org.openhab.model.script.actions.Timer

val int Rolladen_vollstaendig_geschlossen = 99
val int Lamellen_vollstaendig_geschlossen = 99
val int Rolladen_lichtdurchlaessig = 35
val int Rolladen_lichtundurchlaessig = 75
val int Lamellen_lichtundurchlaessig = 32
val int Rolladen_offen = 0
val int Lamellen_offen = 1
val int Bewoelkung_Grenzwert_Sonnenschutz = 30.0
val int Lux_Durchschnitt_Nachlauf_hoch = 5              // Rueckwaertsfenster in Minuten fuer gleitenden Durchschnitt
val int Lux_Durchschnitt_Nachlauf_niedrig = 10          // Rueckwaertsfenster in Minuten fuer gleitenden Durchschnitt
val int Aktionsfenster = 10                             // keine mehrfache Aenderung innerhalb dieser Zeitspanne in Minuten

val long Verschattung_Anfang_morgens_statisch = (7 * 3600) * 1000                       // Start 7:00
val long Verschattung_Ende_morgens_statisch = (12 * 3600 + 30 * 60) * 1000              // Ende 12:30
val long Verschattung_Anfang_mittags_statisch = (10 * 3600 + 15 * 60) * 1000            // Start 10:15
val long Verschattung_Ende_mittags_statisch = (18 * 3600 + 30 * 60) * 1000              // Ende 18:30

// ACHTUNG muss taeglich neu initialisiert werden !
var long Verschattung_Anfang_morgens
var long Verschattung_Ende_morgens
var long Verschattung_Anfang_mittags
var long Verschattung_Ende_mittags

val float Lux_Grenzwert_Sonnenschutz_morgens_niedrig_statisch = 10000.0
val float Lux_Grenzwert_Sonnenschutz_morgens_hoch_statisch = 29000.0
val float Lux_Grenzwert_Sonnenschutz_mittags_niedrig_statisch = 300.0
val float Lux_Grenzwert_Sonnenschutz_mittags_hoch_statisch = 1000.0
val float Aussentemperatur_Grenzwert_Sonnenschutz_statisch = 21.0
val float Innentemperatur_Grenzwert_Sonnenschutz_statisch = 21.0
val float Aussentemperatur_Hysterese_Sonnenschutz_statisch = 2.0
val float Innentemperatur_Hysterese_Sonnenschutz_statisch = 2.0

var boolean SB_alt_morgens = false
var boolean SB_alt_hoch_morgens = false
var boolean SB_alt_mittags = false
var boolean SB_alt_hoch_mittags = false
var boolean SB_Aenderung_mittags = true
var boolean SB_Aenderung_morgens = true
var boolean SB_Reset = true
var long letzte_Aenderung_morgens = Verschattung_Anfang_morgens
var long letzte_Aenderung_mittags = Verschattung_Anfang_mittags

rule "Sonnenschutz Init virtual Items"
        System started
        Item Nachtschaltung received command OFF
        Item Sonnenschutzschaltung received command

        Verschattung_Anfang_morgens = now.withTimeAtStartOfDay.millis + Verschattung_Anfang_morgens_statisch
        Verschattung_Ende_morgens   = now.withTimeAtStartOfDay.millis + Verschattung_Ende_morgens_statisch
        Verschattung_Anfang_mittags = now.withTimeAtStartOfDay.millis + Verschattung_Anfang_mittags_statisch
        Verschattung_Ende_mittags   = now.withTimeAtStartOfDay.millis + Verschattung_Ende_mittags_statisch
        SB_Aenderung_mittags = true
        SB_Aenderung_morgens = true
        SB_Reset = true

        letzte_Aenderung_morgens = Verschattung_Anfang_morgens
        letzte_Aenderung_mittags = Verschattung_Anfang_mittags

        logInfo("rules", "Sonnenschutz Initialisierung.")


rule "Sonnenschutzcheck"
        Time cron "0 1/2 8-18 ? * MON-SUN"
        Item Sonnenschutzschaltung changed to ON
        var float a = (Soll_Temp_Wohnen.state as DecimalType).floatValue - (Aussentemperatur_Hysterese_Sonnenschutz.state as DecimalType).floatValue
        var float i = (Soll_Temp_Wohnen.state as DecimalType).floatValue - (Innentemperatur_Hysterese_Sonnenschutz.state as DecimalType).floatValue

//      logDebug("rules", "Sonnenschutzcheck debug 0: Soll_Temp_Wohnen = " + Soll_Temp_Wohnen.state + "; Hysterese innen/aussen = " + Innentemperatur_Hysterese_Sonnenschutz.state + "/" + Aus
sentemperatur_Hysterese_Sonnenschutz.state + "; aussen = " + a + "; innen = " + i)


        if ((Sonnenschutzschaltung.state == ON) && (Nachtschaltung.state != ON) && (Szene.state <= 2))

rule "Sonnenschutz kalkulieren"
        Item Sonnenschutz received command
        var float Temp_morgens = (TempAussen.state as DecimalType)
        var float Temp_mittags = (EG_Wohnen_Auge_Temp.state as DecimalType)
        var int Lux_morgens_hoch = Garten_Auge_Lux.averageSince(now.minusMinutes(Lux_Durchschnitt_Nachlauf_hoch))
        var int Lux_morgens_niedrig = Garten_Auge_Lux.averageSince(now.minusMinutes(Lux_Durchschnitt_Nachlauf_niedrig))
        var int Lux_mittags_hoch = EG_Wohnen_Auge_Lux.averageSince(now.minusMinutes(Lux_Durchschnitt_Nachlauf_hoch))
        var int Lux_mittags_niedrig = EG_Wohnen_Auge_Lux.averageSince(now.minusMinutes(Lux_Durchschnitt_Nachlauf_niedrig))

        logDebug("rules", "Sonnenschutz debug 0: Durchschnittshelligkeit morgens niedrig[hoch]/mittags niedrig[hoch] = " + Lux_morgens_niedrig + "[" + Lux_morgens_hoch + "]/" + Lux_mittags_n
iedrig + "[" + Lux_mittags_hoch + "]"  )
        var boolean hell_genug_morgens = Lux_morgens_niedrig > (Lux_Grenzwert_Sonnenschutz_morgens_niedrig.state as DecimalType)
        var boolean zu_hell_morgens = Lux_morgens_hoch > (Lux_Grenzwert_Sonnenschutz_morgens_hoch.state as DecimalType)
        var boolean hell_genug_mittags = Lux_mittags_niedrig > (Lux_Grenzwert_Sonnenschutz_mittags_niedrig.state as DecimalType)
        var boolean zu_hell_mittags = Lux_mittags_hoch > (Lux_Grenzwert_Sonnenschutz_mittags_hoch.state as DecimalType)
        var boolean zu_warm_morgens = (Temp_morgens > (Aussentemperatur_Grenzwert_Sonnenschutz.state as DecimalType) || Temp_max.state > Aussentemperatur_Grenzwert_Sonnenschutz.state)
                && (Lux_morgens_niedrig > (Lux_Grenzwert_Sonnenschutz_morgens_niedrig.state as DecimalType))
        var boolean zu_warm_mittags = (Temp_mittags > (Aussentemperatur_Grenzwert_Sonnenschutz.state as DecimalType) || Temp_max.state > Aussentemperatur_Grenzwert_Sonnenschutz.state)
                && (Lux_mittags_niedrig > (Lux_Grenzwert_Sonnenschutz_mittags_niedrig.state as DecimalType))

        var boolean Schutzbedarf_morgens = (now.millis > Verschattung_Anfang_morgens) && (now.millis < Verschattung_Ende_morgens) && ((hell_genug_morgens && zu_warm_morgens) || zu_hell_morge
        var boolean Schutzbedarf_mittags = (now.millis > Verschattung_Anfang_mittags) && (now.millis < Verschattung_Ende_mittags) && ((hell_genug_mittags && zu_warm_mittags) || zu_hell_mitta

        var boolean Schutzbedarf_hoch_morgens = (Schutzbedarf_morgens && zu_hell_morgens && zu_warm_morgens)
        var boolean Schutzbedarf_hoch_mittags = (Schutzbedarf_mittags && zu_hell_mittags && zu_warm_mittags)

        SB_Aenderung_morgens = (SB_Reset || SB_alt_hoch_morgens != Schutzbedarf_hoch_morgens || SB_alt_morgens != Schutzbedarf_morgens)
        SB_Aenderung_mittags = (SB_Reset || SB_alt_hoch_mittags != Schutzbedarf_hoch_mittags || SB_alt_mittags != Schutzbedarf_mittags)

        var long c = now.millis - Aktionsfenster * 60 * 1000
        logDebug("rules","debug Sonnenschutz morgens: (jetzt - Ruheintervall) " + c + " >? letzte Aenderung " + letzte_Aenderung_morgens + " = " + (c > letzte_Aenderung_morgens))
        if (SB_Aenderung_morgens && now.millis < Verschattung_Ende_morgens && c > letzte_Aenderung_morgens) {
                if (Schutzbedarf_morgens) {
                        logInfo("rules", "Sonnenschutz: Verschatte vormittags beschienene Fensterflächen ...")

                        if (EG_Kueche_Tuer_Kontakt.state != OPEN && EG_Kueche_Fenster_Kontakt.state != OPEN) {
                                if (Schutzbedarf_hoch_morgens) {
                                } else {
                        if (EG_Essen_Fenster_Kontakt.state != OPEN)
                                if (Schutzbedarf_hoch_morgens)

                        if (SB_Aenderung_morgens == true) {
                                if (EG_Wohnen_Tuer_li1_Kontakt.state != OPEN && EG_Wohnen_Tuer_li2_Kontakt.state != OPEN)
                                if (EG_Wohnen_Tuer_Mitte_li_Kontakt.state != OPEN && EG_Wohnen_Tuer_Mitte_re_Kontakt.state != OPEN) {
                        if (Schutzbedarf_hoch_morgens) {
                                logDebug("rules", "debug Sonnenschutz morgens: Jalousie Mitte = " + EG_Wohnen_Jalousie_Mitte.state + "; Lamellen (Ist/Soll) = " + EG_Wohnen_Jalousie_Mitte_Lam
ellen.state + "/" + Lamellen_lichtundurchlaessig)
                                if (SB_Aenderung_morgens) {
                                        logDebug("rules", "debug Sonnenschutz morgens: SB_alt_hoch_morgens(" + SB_alt_hoch_morgens + ") != Schutzbedarf_hoch_morgens(" + Schutzbedarf_hoch_mor
gens + "), schliesse Lamellen Mitte")
                        } else {
                                logDebug("rules", "debug Sonnenschutz morgens: Jalousie Mitte = " + EG_Wohnen_Jalousie_Mitte.state + "; Lamellen (Ist/Soll) = " + EG_Wohnen_Jalousie_Mitte_Lam
ellen.state + "/1")
                                if (SB_Aenderung_morgens) {
                                        logDebug("rules", "debug Sonnenschutz: SB_alt_morgens(" + SB_alt_morgens + ") != Schutzbedarf_morgens(" + Schutzbedarf_morgens + "), öffne Lamellen Mi
                } else {
                        logInfo("rules", "Sonnenschutz: keine Verschattung nötig, öffne vormittags beschienene Fensterflächen ...")


                        logDebug("rules", "debug Sonnenschutz: vormittags fertig heraufgefahren.")

        } else
                logInfo("rules", "Sonnenschutz: keine Anpassung der Verschattung auf den vormittags beschienenen Fensterflächen nötig.")

        logDebug("rules", "Sonnenschutz Mittags-Check : Schaltung = " + Sonnenschutzschaltung.state + "; Lichtstärke (" + Lux_Durchschnitt_Nachlauf_hoch + "/" + Lux_Durchschnitt_Nachlauf_nie
drig + "-min-Durchschnitt/Grenzwert[hoch]) = " + Lux_mittags_hoch + "/" + Lux_mittags_niedrig + "/" + Lux_Grenzwert_Sonnenschutz_mittags_niedrig.state + "[" + Lux_Grenzwert_Sonnenschutz_mitt
ags_hoch.state + "] lux; Bewölkung (aktuell/Grenzwert) = " + aktuell_Bewoelkung.state + "/" + Bewoelkung_Grenzwert_Sonnenschutz + " %; Temperatur(Sensor/max. heute/Grenzwert) = " + Temp_mitt
ags + "/" + Temp_max.state + "/" + Aussentemperatur_Grenzwert_Sonnenschutz.state + " °C; Schutzbedarf mittags (aktuell[hoch]/alt[hoch]/Änderung) = " + Schutzbedarf_mittags + "[" + Schutzbeda
rf_hoch_mittags + "]/" + SB_alt_mittags + "[" + SB_alt_hoch_mittags + "]/" + SB_Aenderung_mittags)

        logDebug("rules","debug Sonnenschutz mittags: (jetzt - Ruheintervall) " + c + " >? letzte Aenderung " + letzte_Aenderung_mittags + " = " + (c > letzte_Aenderung_mittags))

        if (SB_Aenderung_mittags && (now.millis > Verschattung_Anfang_mittags) && (now.millis < Verschattung_Ende_mittags) && c > letzte_Aenderung_mittags) {
                 if (Schutzbedarf_mittags) {
                        logInfo("rules", "Sonnenschutz: Verschatte (nach)mittags beschienene Fensterflächen ...")

                        if (SB_alt_mittags != Schutzbedarf_mittags) {
                                if (EG_Wohnen_Tuer_re1_Kontakt.state != OPEN && EG_Wohnen_Tuer_re2_Kontakt.state != OPEN)  {

                        if (Schutzbedarf_hoch_mittags) {
                                if (SB_Aenderung_mittags) {
                                        logDebug("rules", "debug Sonnenschutz: SB_alt_hoch_mittags(" + SB_alt_hoch_mittags + ") != Schutzbedarf_hoch_mittags(" + Schutzbedarf_hoch_mittags + "
), schliesse Lamellen rechts")
                        } else {
                                if (SB_Aenderung_mittags) {
                                        logDebug("rules", "debug Sonnenschutz: SB_alt_mittags(" + SB_alt_mittags + ") != Schutzbedarf_mittags(" + Schutzbedarf_mittags + "), öffne Lamellen re

                        if (EG_Bianca_Tuer_Kontakt.state != OPEN && Sonnenschutz_Bianca.state == ON) {
                                if (Schutzbedarf_hoch_mittags)
                        if (EG_Wohnen_Tuer_alt_Kontakt.state != OPEN) {
                                if (Schutzbedarf_hoch_mittags)
                } else {
                        logInfo("rules", "Sonnenschutz: keine Verschattung nötig, öffne (nach)mittags beschienene Fensterflächen ...")

                        if (Sonnenschutz_Bianca.state == ON)

                        logDebug("rules", "debug Sonnenschutz: nachmittags fertig heraufgefahren.")
        } else
                logInfo("rules", "Sonnenschutz: keine Anpassung der Verschattung auf den nachmittags beschienenen Fensterflächen nötig.")

        if (SB_Aenderung_morgens) {
                  SB_Aenderung_morgens = false
                  letzte_Aenderung_morgens = now.millis
        if (SB_Aenderung_mittags) {
                  SB_Aenderung_mittags = false
                  letzte_Aenderung_mittags = now.millis
        SB_alt_morgens = Schutzbedarf_morgens
        SB_alt_hoch_morgens = Schutzbedarf_hoch_morgens
        SB_alt_mittags = Schutzbedarf_mittags
        SB_alt_hoch_mittags = Schutzbedarf_hoch_mittags
        SB_Reset = false

        logDebug("rules", "Sonnenschutz: Verschattungs-Check ausgeführt.")

rule "Sonnenschutz-Jalousien wieder hoch"
        Time cron "0 35 12 ? * MON-SUN"
        Time cron "0 35 18 ? * MON-SUN"
        if (now.isAfter(Verschattung_Ende_mittags)) {
                if (Sonnenschutz_Bianca.state == ON)

                SB_Aenderung_mittags = true
                logDebug("rules", "Sonnenschutz Mittagseite beendet, Jalousien werden wieder hochgefahren.")
        } else {

                SB_Aenderung_morgens = true
                logDebug("rules", "Sonnenschutz Morgenseite beendet, Jalousien werden wieder hochgefahren.")


Hi Markus,

Nice code, might use it. which persistence are you using to calculate average?


rrd4j, but any except mapdb should do.

I happen to use mapdb for my items so no good, Now I went ahead and also installed the mysql add-on as I did install mysql earlier. It is working like a treat!

Another question if I may:
You use var boolean SB_alt_morgens = false and update that at the end to be the “new state”. Does that get saved in the rule for next time? Ie if that boolean is now true, next time the rule runs it is actually remembered as true? Or do I need to create an item for that too?

Thanks in advance!

The variables defined at the beginning of the rule file are initialized at OH startup.
They remain in memory as long as OH runs so the next time the rule runs it will be remembered.
These variable can be seen by all rules in that file. They are “global” to that file.
You can kind of think of them as items created when OH starts. BUT you can not persist these.

I do have a similar rule set in place for my shading automation - but I lack some best practise for overriding them via manual control.

Use case:
If I decide for some reason to position the blinds differently than the rules, I just change them - but if the cron (or whatever trigger) calls, they’re set to the rule preset again.

My workaround(?) solution:
for now I have a variable for each of my six blinds, which is triggered, if I press the button manually and stays until it gets reset (on midnight or by some action).

I don’t find this very smart, how do you cope with manual overrides?

I struggled with my blinds on OH so I turned to node-red
I use node-red-contrib-blindcontroller - lots of options, fully automatic if you want, seasonal settings… IT’S BLINDING!!
You can set a manual value and an expiry time for the manual override

Perfect, thanks!

You want it cheap and like to solder? :grinning:
ESP8266 with ESPEasy firmware and BH1750 sensor (under 10 Euro)

1 Like

You can obviously use any OH-connected sensor. As I’ve got ZWave in place anyway, I’m using Fibaro FGMS and Aeotec Multisensors.

PS: All, please open a new thread on questions that are not directly on the code so all of these “examples” threads stay clean. You can also retract posts.

Could you please post your setup for the blinds with node-red-contrib-blindcontroller… I am totalling failing…

Automation works fine… but if the automation sets the value for OH it produces a feedback in the Input trigger and is treated as manual override… any suggestions?

In another thread please.