Sky cloudiness detection for automated shading

Tags: #<Tag:0x00007f7454b64368> #<Tag:0x00007f7454b64200>

Hi Community, gladly I give something back to the great Openhab Community…

I was searching for a solution to automatically detect the sky cloudiness for my automated shading (blinds). I found the weather forecast not precise enough, therefore I decided to have a solution based on illuminance (Lux) measurement.

Requirements

  • Accurate detection of the cloud level
  • Quick reaction, within minutes
  • Works all year around without adjustment
  • Affordable Investment < 150€, low maintenance

Challenges
For a proper cloudiness detection the lux sensor need to point towards the sun, hence it needs at least 3 sensors to cover a full day. It is also not so easy to find a spot, where it is sunny the whole day, sensors are not an obstacle, and they are easy reachable for maintenance etc. Finally, on my latitude the sun ecliptic and therefore the illuminance changes a lot during the year. This needs to take in to consideration when calculating the cloudiness.

Approach
After some experiments I landed with the new Philips Hue outdoor sensors. Motion sensors measure the Lux level as they usually only need to report motion when it is dark. Via the hue bridge the lux level is transmitted every 5mins to Openhab. As they are battery driven and wireless, they can be placed everywhere on the house or the garden. I placed 2 on the carport and one on a garden wall. The one pointing to south I slightly modified. As the sun is high while in the south I had to saw the cover off (and sprayed it white, to preserve my wife’s nerves).
20190922_103946

The Lux level changes during the day dependant on the elevation of the sun. So it needs to be “normalized”. I do that by dividing with the calculated Total_Radiation from the Astro binding. This is the theoretical max radiation at a clear sky. This calculation gives a Cloud Index for each direction.

The 3 sensor need to be masked, as the measurement of sensor shall only contribute when the sun is really in this direction. I found the good old Pythagorean identity sin^2(alpha) + cos^2( alpha ) = 1 helpful, as you can use for alpha the sun azimuth (Astro Binding) which gives a nice weighting and always sums up to 1. Keep in mind, for this approach the sensors need to be orthogonal to each other. This means if the first points to the east (azimuth=90°) the second needs to point to south (azimuth=180°)… But you can point your sensors also to other directions, eg 80°, 170° (but always +90° )…
This gives finally an overall Cloud Index.

Out of the Cloud Index I calculate a FuzzyCloudiness state. If CloudIndex>40 -> “sunny”, if 20…40 -> “partly cloudy”, <20 “heavy cloudy”. This threshold values are dependant on your taste and the calibration of your sensors.

Finally I give the FuzzyCloudiness state over to the blinds control. To avoid an up/down for every cloud passing by, a hysteresis is necessary. CloudInputShading only goes after a hysteresis of 45mins to less strong shading, but immediately goes to a stronger shading when there is brighter sky detected.

Solution
The screen to check the current status. Not often necessary as everything is automatic :slight_smile:

Items Definition (you need also additional Astro Items -> see Astro Binding)

///Hue Items
Number CloudIndex                   "Cloud Index [%.1f] "                       <sun_clouds>
Number CloudIndexAvg                "Cloud Index Avg [%.1f]"                    <sun_clouds>
DateTime CloudIndexChg              "Cloud Index Update [%1$ty-%1$tm-%1$td    %1$tH:%1$tM]" <time>

Number  IlluminanceEast             "Lichtintensität Ost [%.0f Lux]"            <sun_clouds>                     { channel = "hue:0106:001788b29832:16:illuminance" }
Number  BatteryIllEast              "Batterien Ost Status [%s %%]"              <battery>                        { channel = "hue:0106:001788b29832:16:battery_level" }
DateTime IllEastLastRead            "Letzte Aktualisierung  [%1$ty-%1$tm-%1$td    %1$tH:%1$tM]"  <time>  (gMonitor)       { channel = "hue:0106:001788b29832:16:last_updated" }
Switch  MotionEast                  "Bewegungsmelder Ost  [%s]"                 <motion>                         { channel = "hue:0107:001788b29832:15:presence" }
Number  TempEast                    "Temperatur Ost [%.1f°C]"                   <temperature>                    { channel = "hue:0302:001788b29832:17:temperature" }   
Number  CloudIndexEast              "Cloud Index Ost [%.1f] "                  <sun_clouds>

Number  IlluminanceSouth            "Lichtintensität Süd [%.0f Lux]"            <sun_clouds>                     { channel = "hue:0106:001788b29832:41:illuminance" }
Number  BatteryIllSouth             "Batterien Süd Status [%s %%]"              <battery>                        { channel = "hue:0106:001788b29832:41:battery_level" }
DateTime IllSouthLastRead           "Letzte Aktualisierung [%1$ty-%1$tm-%1$td    %1$tH:%1$tM]"  <time> (gMonitor)     { channel = "hue:0106:001788b29832:41:last_updated" }
Switch  MotionSouth                 "Bewegungsmelder Süd  [%s]"                 <motion>                         { channel = "hue:0107:001788b29832:40:presence" }
Number  TempSouth                   "Temperatur Süd [%.1f°C]"                   <temperature>                    { channel = "hue:0302:001788b29832:42:temperature" }   
Number  CloudIndexSouth              "Cloud Index Süd [%.1f] "                 <sun_clouds>

Number  IlluminanceWest             "Lichtintensität West [%.0f Lux]"            <sun_clouds>                     { channel = "hue:0106:001788b29832:22:illuminance" }
Number  BatteryIllWest              "Batterien West Status [%s %%]"              <battery>                        { channel = "hue:0106:001788b29832:22:battery_level" }
DateTime IllWestLastRead            "Letzte Aktualisierung [%1$ty-%1$tm-%1$td    %1$tH:%1$tM]"  <time>  (gMonitor)     { channel = "hue:0106:001788b29832:22:last_updated" }
Switch  MotionWest                  "Bewegungsmelder West  [%s]"                 <motion>                         { channel = "hue:0107:001788b29832:21:presence" }
Number  TempWest                    "Temperatur West [%.1f°C]"                   <temperature>                    { channel = "hue:0302:001788b29832:23:temperature" }   
Number  CloudIndexWest              "Cloud Index West [%.1f] "                   <sun_clouds>

Rules for CloudIndex calculation (for each direction rule is necessary)

//Calculates CloudIndex for East Facade //////////////////////////////////////////////
rule "Calculates CloudIndex for East Facade "
  when 
		 Item IlluminanceEast changed 
	then
		 if ((Total_Radiation.state as Number)>1)  //avoid Division 0
		 { var float vCloudIndex = (IlluminanceEast.state as Number).floatValue / (Total_Radiation.state as Number).floatValue
		   CloudIndexEast.sendCommand(vCloudIndex) 
			} //end if TotalRadiation>1
	end

Rules for masking, FuzzyCloudiness and Hysteresis

//Cloud Detection
var float vCISunny = 40   // Cloudindex over this value is considered as sunny
var float vCICloudy =20   // CloudIndex over this value is consider as Cloudy Sky, below as heavy cloudy
var float vHysteresisCloudy = 2700000 //Hysteresis time [ms] until change to cloudy shading 2'700'000ms = 45min
var float vHysteresisHeavyCloudy = 2700000 //Hysteresis time [ms] until change to heavy cloudy shading 

//Calculates FuzzyCloudiness (Cloud Cover) from Cloud Index  //////////////////////////////////////////////
rule "Calculates FuzzyCloudiness (Cloud Cover) from Cloud Index "
  when 
    	 Item CloudIndexEast changed or
		 Item CloudIndexSouth changed or
		 Item CloudIndexWest changed 
  then
         var float vCloudIndex = 1.0 
         var float vAzimuthDeg = (Azimuth.state as Number).floatValue
         var float vAzimuthRad = Math.toRadians(vAzimuthDeg).floatValue
         var float vMaskingEast = vAzimuthRad + 0 // if your sensor is not pointing to East (Azimuth=90°) you can adjust the value here. Keep in mind that sensors must be South=East+90° and West=East+180°
         var float vMaskingSouth = (vMaskingEast-Math.PI()/2).floatValue // never change as this masking is based on the formula Sin(a)^2+Sin(a-90°)^2 = 1
         var float vMaskingWest =  (vMaskingEast-Math.PI()).floatValue // never change as this masking is based on the formula Sin(a)^2+Sin(a-90°)^2 = 1

         if ((vAzimuthDeg>=0.0)&&(vAzimuthDeg<=90.0)) //Azimuth 0..90
         {    vCloudIndex = (CloudIndexEast.state as Number).floatValue //as there is no LUX sensor on north facade it can not contribute to the calculation
         }    // end Azimuth 0..90

        if ((vAzimuthDeg>90.0)&&(vAzimuthDeg<=180.0)) //Azimuth 90..180
         {    vCloudIndex = ((Math.pow(Math.sin(vMaskingEast),2)*(CloudIndexEast.state as Number))+((Math.pow(Math.sin(vMaskingSouth),2))*(CloudIndexSouth.state as Number))).floatValue                        
         }    //Azimuth 90..180

        if ((vAzimuthDeg>180.0)&&(vAzimuthDeg<=270.0)) //Azimuth 180..270
         {    vCloudIndex = ((Math.pow(Math.sin(vMaskingSouth),2)*(CloudIndexSouth.state as Number))+((Math.pow(Math.sin(vMaskingWest),2))*(CloudIndexWest.state as Number))).floatValue                        
         }    //Azimuth 180..270

         if ((vAzimuthDeg>270)&&(vAzimuthDeg<=360.0)) //Azimuth 270..360
         {    vCloudIndex = (CloudIndexWest.state as Number).floatValue //as there is no LUX sensor on north facade it can not contribute to the calculation
         }    // end Azimuth 270..360

         //logInfo("FuzzyCloudiness", "Azimuth: " + vAzimuthDeg + "  IndexSouth: " + CloudIndexSouth.state + " IndexWest: " + CloudIndexWest.state + " CloudIndex: " + vCloudIndex)
		 CloudIndex.sendCommand(vCloudIndex) 
		 CloudIndexChg.postUpdate(now.toString)   //Set Update Timestamp

		     if (vCloudIndex>=vCISunny)   // If Sunny Detected vCISunny
		     {   if (FuzzyCloudiness.state != 3) FuzzyCloudiness.postUpdate(3)	 
		 	     TimeStampLastSunny.postUpdate(now.toString)  //Set Time Last Sunny
		     } //end if Sunny

            if ((vCloudIndex<vCISunny) && (vCloudIndex>=vCICloudy))      // If Cloudy Detected 
		     {   if (FuzzyCloudiness.state != 2) FuzzyCloudiness.postUpdate(2)	 
		 	     TimeStampLastCloudy.postUpdate(now.toString)  //Set Time Last Cloudy
		     } //end if Cloudy

            if (vCloudIndex<vCICloudy)      // If heavy cloudy Detected 
		     {   if (FuzzyCloudiness.state != 1) FuzzyCloudiness.postUpdate(1)	 
		 	     TimeStampLastHeavyCloudy.postUpdate(now.toString)  //Set Time Last HeavyCloudy
		     } //end if Cloudy
end

//Sets Shading according to the measured and calculated Cloudiness  //////////////////////////////////////////////
rule "Sets shading according to the measured and calculated Cloudiness"
when 
    	 Item AutoLux changed or
		 Item CloudIndex changed  //ensure triggering as there can be changes necessary because SinceSunny/Cloud is longer than hysteresis
then
         if (AutoLux.state == ON) //If auto cloudiness detection is turned on
         {   var Number millisSinceSunny =  (now.millis) - ((TimeStampLastSunny.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)  //How long since last Sunny?
             var Number millisSinceCloudy = (now.millis) - ((TimeStampLastCloudy.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli) //How long since last Cloudy?
                          
             if (FuzzyCloudiness.state == 3) // if Sunny 
             { if (CloudInputShading != 3) CloudInputShading.postUpdate(3) // Set Shading for "Sunny" weather
             } // end if Sunny

             if ((FuzzyCloudiness.state == 2) && (millisSinceSunny>=vHysteresisCloudy)) //if it is cloudy for longer time than Hystersis time
             { if (CloudInputShading != 2) CloudInputShading.postUpdate(2) // Set Shading for "Cloudy" weather
             } //end if Cloudy for longer time

             if ((FuzzyCloudiness.state == 1) && ( (millisSinceSunny>=vHysteresisCloudy)&&(millisSinceCloudy>=vHysteresisHeavyCloudy)) )//if it is heavy cloudy for longer time than Hystersis time
             { if (CloudInputShading != 1) CloudInputShading.postUpdate(1) // Set Shading for "Heavy Cloudy" weather
             } //end if Heavy Cloudy for longer time
             //logInfo("FuzzyCloud","SinceLast Sunny: "+ millisSinceSunny + " Since Last Cloudy: " + millisSinceCloudy+ "")
         }  //end AutLux==ON
end

Sitemap with some charting (needs persistence etc)

          Frame label="Ausgangslage für Beschattung" {
            Text item=AutoLux
                { Frame label="Zusammenzug"
                  {   Switch item=AutoLux
                      Text item=CloudIndex
                      Text label=">40 ->Sunny(3), >20 ->Cloudy(2)" 
                      Text item=CloudIndexChg
                      Selection item=FuzzyCloudiness mappings=[1="Stark Bewölkt", 2="Bewölkt", 3="Sonnig"]
                      Text item=TimeStampLastSunny
                      Text item=TimeStampLastCloudy
                      Text item=TimeStampLastHeavyCloudy
                      Text item=Total_Radiation
                  }   //End Frame
                  Frame label="Beleuchtungsstärke Ost"
                  {   Text item=IlluminanceEast
                      Text item=CloudIndexEast
                      Text item=IllEastLastRead
                      Text item=BatteryIllEast
                      Text item=TempEast
                  }   //End Frame  

                  Frame label="Beleuchtungsstärke Süd"
                  {   Text item=IlluminanceSouth
                      Text item=CloudIndexSouth
                      Text item=IllSouthLastRead
                      Text item=BatteryIllSouth
                      Text item=TempSouth
                  }   //End Frame  

                  Frame label="Beleuchtungsstärke West"
                  {   Text item=IlluminanceWest
                      Text item=CloudIndexWest
                      Text item=IllWestLastRead
                      Text item=BatteryIllWest
                      Text item=TempWest
                  }   //End Frame  
                  
                  Frame label="Auswertung Cloudiness" // Chart
                  { Switch item=Heat_E_Chart_Period mappings=[0="Stunde", 1="Tag", 2="Woche"]
                    Chart  item=CloudIndex period=h refresh=10000  legend=false visibility=[Heat_E_Chart_Period==0, Heat_E_Chart_Period=="Uninitialized", Heat_E_Chart_Period==NULL]
                    Chart  item=CloudIndex period=D refresh=30000 legend=false visibility=[Heat_E_Chart_Period==1]
                    Chart  item=CloudIndex period=W refresh=30000 legend=false visibility=[Heat_E_Chart_Period==2]

                    Chart  item=FuzzyCloudiness period=h refresh=10000  legend=false visibility=[Heat_E_Chart_Period==0, Heat_E_Chart_Period=="Uninitialized", Heat_E_Chart_Period==NULL]
                    Chart  item=FuzzyCloudiness period=D refresh=30000 legend=false visibility=[Heat_E_Chart_Period==1]
                    Chart  item=FuzzyCloudiness period=W refresh=30000 legend=false visibility=[Heat_E_Chart_Period==2]

                    Chart  item=CloudInputShading period=h refresh=10000  legend=false visibility=[Heat_E_Chart_Period==0, Heat_E_Chart_Period=="Uninitialized", Heat_E_Chart_Period==NULL]
                    Chart  item=CloudInputShading period=D refresh=30000 legend=false visibility=[Heat_E_Chart_Period==1]
                    Chart  item=CloudInputShading period=W refresh=30000 legend=false visibility=[Heat_E_Chart_Period==2]

                  }
                } //End Text Item
9 Likes

Yes I can’t wait for the rest

1 Like

Thomas, we need the solution :grinning:
Very interesting device, didn’t know about it yet. Does it really report the lux values and is the openHAB binding able to read those values?

Here we go, have fun with sun and shade … :sunglasses:

1 Like

Yep. The sensor provides illuminance, motion, and even temperature. Additionally “last updated” and “battery level”. Cheers Thomas

1 Like

I bought my first outdoor sensor for testing, this is a really great device, well build, thx for pointing to it.
Did you disassemble the sensor to paint it? (I have to paint it white, too).
If yes, I hope it will still be weatherproof after reassembling :sunglasses:

To paint i used some masking tape to cover the white plastic of the sensor. I took fine sandpaper to scratch the black plastic parts (paint holds better). Then I sprayed it. Dissassemble is in my opinion not necessary.

1 Like

I am thinking of placing it just upside down as I don’t need the motion feature.
Then I don’t have to saw the cover off.
A bit of silicone should fill the gap between the cover and the sensor …

There is a little hole facing down the main body (to drain water if seals are not 100% close) . If you place the device upside down, you need to seal this. Its probably wise to make a new one on the other side. The same for the cover, if you silicon the gap I would drill a hole on the other side to drain. Thats why I thought I better cut the cover off. This goes quite easy with a metall handsaw which you find in every bigger tool case.

1 Like