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).
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 theoretical max sun radiation on a vertical surface. The sun radiation is calculated from sun elevation, azimuth,… (data provided by the Astro binding). Finally the 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>30 -> “sunny”, if 15…30 -> “partly cloudy”, <15 “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 there is a hysteresis of 15mins (adjustable) a new state needs to be active.
Solution
Input: Lux Measurement of 3 sensors around the house
Output: Calculated FuzzyCloudiness state: “Sunny”, “Partly Cloudy” or “Cloudy”
The screen to check the current status. Not often necessary as everything is automatic
Items Definition (you need also additional Astro Items -> see Astro Binding)
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:1:LightLevelEast:illuminance" }
Number BatteryIllEast "Batterien Ost Status [%s %%]" <battery> { channel = "hue:0106:1:LightLevelEast:battery_level" }
DateTime IllEastLastRead "Letzte Aktualisierung [%1$ty-%1$tm-%1$td %1$tH:%1$tM]" <time> (gMonitor) { channel = "hue:0106:1:LightLevelEast:last_updated" }
Number TempEast "Temperatur Ost [%.1f°C]" <temperature> { channel = "hue:0302:1:TempEast:temperature" }
Number CloudIndexEast "Cloud Index Ost [%.1f] " <sun_clouds>
Number ClearSkyRadiationEast "Max Strahlung Ost [%.0f W/m2]" <solarplant>
Number IlluminanceSouth "Lichtintensität Süd [%.0f Lux]" <sun_clouds> { channel = "hue:0106:1:LightLevelSouth:illuminance" }
Number BatteryIllSouth "Batterien Süd Status [%s %%]" <battery> { channel = "hue:0106:1:LightLevelSouth:battery_level" }
DateTime IllSouthLastRead "Letzte Aktualisierung [%1$ty-%1$tm-%1$td %1$tH:%1$tM]" <time> (gMonitor){ channel = "hue:0106:1:LightLevelSouth:last_updated" }
Number TempSouth "Temperatur Süd [%.1f°C]" <temperature> { channel = "hue:0302:1:TempSouth:temperature" }
Number CloudIndexSouth "Cloud Index Süd [%.1f] " <sun_clouds>
Number ClearSkyRadiationSouth "Max Strahlung Süd [%.0f W/m2]" <solarplant>
Number IlluminanceWest "Lichtintensität West [%.0f Lux]" <sun_clouds> { channel = "hue:0106:1:LightLevelWest:illuminance" }
Number BatteryIllWest "Batterien West Status [%s %%]" <battery> { channel = "hue:0106:1:LightLevelWest:battery_level" }
DateTime IllWestLastRead "Letzte Aktualisierung [%1$ty-%1$tm-%1$td %1$tH:%1$tM]" <time> (gMonitor){ channel = "hue:0106:1:LightLevelWest:last_updated" }
Number TempWest "Temperatur West [%.1f°C]" <temperature> { channel = "hue:0302:1:TempWest:temperature" }
Number CloudIndexWest "Cloud Index West [%.1f] " <sun_clouds>
Number ClearSkyRadiationWest "Max Strahlung West [%.0f W/m2]" <solarplant>
Rules for max sun radiation and CloudIndex calculation (for each direction rule is necessary).
Remark: The max sun radiation could be also used for solar panel efficiency calculation!
import java.time.ZonedDateTime // needed for getDayOfYear
rule "Calculates CloudIndex for East Facade "
when
Item IlluminanceEast changed
then
//Astro Input
val Number vdayOfYear = (ZonedDateTime::now().getDayOfYear());
val Number vSunElevDeg = (Elevation.state as Number).floatValue
val Number vSunElevRad = Math.toRadians(vSunElevDeg)
val Number vSunAzimuthDeg = (Azimuth.state as Number).floatValue
val Number vSunAzimuthRad = Math.toRadians(vSunAzimuthDeg)
//Collector Input
val Number vCollectorTiltDeg = 90 //Collector Tilt Angle towards Ground. Must be between 0 (horizontal)..90°(vertical)
val Number vCollectorTiltRad = Math.toRadians(vCollectorTiltDeg) //Collector Tilt Angle Radians
val Number vCollectorAzimuthDeg = 90 // Collector Azimuth (90°=East, 180°= South, 270°=West)
val Number vCollectorAzimuthRad = Math.toRadians(vCollectorAzimuthDeg)
//Ground Reflectance guidance values:
//Gravel roof=0.1 ¦ Ordinary ground/grass=0.2 ¦ Desert sand=0.4 ¦ Ocean ice=0.6 ¦ Fresh snow=0.8
val Number vGroundReflect = 0.2
//Calculations (Are tested against excel-exact same values. Formulas seem to me from plausibility check not so precise for tilt<>90° )
val Number vAirMass = Math.abs(1/(Math.sin(vSunElevRad))) //Air Mass Ratio
//val Number vExtraTerraDay = Math.cos(Math.toRadians(((360*vdayOfYear)/365).doubleValue)) //Intermediate Value (not used)
//val Number vExtraTerrRadiation = 1370 * (1 + (0.034 * vExtraTerraDay)) //Extraterrestrial Solar Radiation (not used)
val Number vAppaInt1 = (360F / 365F) * (vdayOfYear-275F)
val Number vAppaInt2 = Math.sin(Math.toRadians(vAppaInt1.doubleValue))
val Number vAppaExtraTerrRadiation = 1160 + 75F * vAppaInt2 //Apparent Extraterrestrial Solar Radiation
//logInfo("Radi","DayofY: " + vdayOfYear + " AirMass: " + vAirMass + " ExtraTerra " + vExtraTerrRadiation + " Apparen Extra Terra " + vAppaExtraTerrRadiation)
val Number vOptInt1 = (360F/365F)*(vdayOfYear-100F)
val Number vOpticalDepth = 0.174 + (0.035 * Math.sin(Math.toRadians((vOptInt1).doubleValue)))
val Number vSkyDiffuseF = 0.095 + (0.04 * Math.sin(Math.toRadians((vOptInt1).doubleValue)))
var Number vClearSkyBeamRad = 0
if (vSunElevDeg>0) { vClearSkyBeamRad = vAppaExtraTerrRadiation * Math.pow(2.71828, -(vOpticalDepth*vAirMass).doubleValue) }
//logInfo("Radi2"," ODepth: " + vOpticalDepth + " vSkyDiffuseF: " + vSkyDiffuseF + " CSBeam: " + vClearSkyBeamRad)
val Number vInAngInt1 = Math.cos(vSunElevRad)
var Number vInAngInt2 = Math.cos((vSunAzimuthRad-vCollectorAzimuthRad).doubleValue)
if (vInAngInt2<0) vInAngInt2 = 0 // If Sun Azimuth and Collector Azimuth are more than 90° apart no contribution to the value
val Number vInAngInt3 = Math.sin(vCollectorTiltRad)
val Number vInAngInt4 = Math.sin(vSunElevRad)
val Number vInAngInt5 = Math.cos(vCollectorTiltRad)
val Number vIncidenceAngle = vInAngInt1*vInAngInt2*vInAngInt3 + vInAngInt4*vInAngInt5
var Number vBeamInsoCollector = 0
if (vIncidenceAngle>0) { vBeamInsoCollector = vClearSkyBeamRad * vIncidenceAngle }
val Number vDiffuseRadiInt1 = (1F + vInAngInt5) / 2F
val Number vDiffuseRadi = vSkyDiffuseF * vClearSkyBeamRad * vDiffuseRadiInt1
//logInfo("Radi3","vIncidenceAngle: "+ vIncidenceAngle + " vBeamInsoCollector: " + vBeamInsoCollector + " vDiffuseRadi: "+vDiffuseRadi )
val Number vReflectRadi = vGroundReflect * vClearSkyBeamRad * ( (vInAngInt4 + vSkyDiffuseF)*((1F-vInAngInt5)/2F) )
val Number vCOllectorTotalRadiation = vBeamInsoCollector + vDiffuseRadi + vReflectRadi
//logInfo ("Radiation", "Sun Elev="+vSunElevDeg + " Sun Azi="+ vSunAzimuthDeg + " BeamCollector="+vBeamInsoCollector+ " Collector Radiation="+vCOllectorTotalRadiation)
//logInfo ("Radiation", "Col Tilt="+vCollectorTiltDeg + " Col Azi="+ vCollectorAzimuthDeg +" DiffuseRadi="+vDiffuseRadi + " vReflectRadi" + vReflectRadi)
//Calculate Cloud Index
if ((vCOllectorTotalRadiation)>100) //avoid Sunrise/Sunset Calculations
{ val Number vCloudIndex = (IlluminanceEast.state as Number) / vCOllectorTotalRadiation
CloudIndexEast.sendCommand(vCloudIndex)
} //end if TotalRadiation>1
ClearSkyRadiationEast.sendCommand(vCOllectorTotalRadiation)
end
Rules for masking, FuzzyCloudiness and Hysteresis
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
Sitemap with some charting
Text item=AutoLux
{ Frame label="Zusammenzug"
{ Switch item=AutoLux
Text item=CloudIndex
Text label=">30=Sunny(3) >15=Cloudy(2)"
Text item=CloudIndexChg
Selection item=FuzzyCloudiness mappings=[1="Stark Bewölkt", 2="Bewölkt", 3="Sonnig"]
Text item=Time2LessShadingMin
Text item=Time2MoreShadingMin
Text item=TimeStampLastSunny
Text item=TimeStampLastCloudy
Text item=TimeStampLastHeavyCloudy
Frame label="Bewölkungsgrad Ost"
{ Text item=CloudIndexEast
Text item=IlluminanceEast
Text item=ClearSkyRadiationEast
Text item=IllEastLastRead
Text item=BatteryIllEast
Text item=TempEast
} //End Frame
Frame label="Bewölkungsgrad Süd"
{ Text item=CloudIndexSouth
Text item=IlluminanceSouth
Text item=ClearSkyRadiationSouth
Text item=IllSouthLastRead
Text item=BatteryIllSouth
Text item=TempSouth
} //End Frame
Frame label="Bewölkungsgrad West"
{ Text item=CloudIndexWest
Text item=IlluminanceWest
Text item=ClearSkyRadiationWest
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