/**
* **************** Illuminance Calculations and Sync ****************
*
* Usage:
* This was designed to update a virtual illuminance attribute driver attributes based on illuminance
* Updates sunset, lowLight, dayLight bools as illuminance values change
*
* v.2.0 - 10/23 - get states directly from defice instead of saving as state variable
* v.2.1 - 10/24 - added season
**/
import groovy.time.*
definition (
name: "Illuminance Calculations And Sync",
namespace: "Hubitat",
author: "Burgess",
description: "Calculate Illuminance Data and Set Illuminance Values in Illuminance Data Device",
category: "My Apps",
iconUrl: "",
iconX2Url: ""
)
preferences {
page name: "mainPage", title: "", install: true, uninstall: true
}
def mainPage() {
dynamicPage(name: "mainPage") {
section("Illuminance Data Device") {
input (
name: "illuminanceData",
type: "capability.actuator",
title: "Select Illuminance Data Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("Weather Station") {
input (
name: "weatherStation",
type: "capability.illuminanceMeasurement",
title: "Select Weather Station Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("Inside Illuminance Sensor Device") {
input (
name: "indoorSensor",
type: "capability.illuminanceMeasurement",
title: "Select Inside Illuminance Sensor Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("Virtual Auto Light Target Dimmer Device") {
input (
name: "lightTarget",
type: "capability.switchLevel",
title: "Select Virtual Light Target Dimmer Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("Moon Phase Device") {
input (
name: "moonPhase",
type: "capability.actuator",
title: "Select Moon Phase Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("Log To Google Device") {
input (
name: "googleLogs",
type: "capability.actuator",
title: "Select Log to Google Device",
required: true,
multiple: false,
submitOnChange: true
)
}
section("") {
input (
name: "debugMode",
type: "bool",
title: "Enable logging",
required: true,
defaultValue: false
)
}
section("") {
input (
name: "logLevel",
type: "enum",
title: "Logging Level",
options: [1:"Info", 2:"Warning", 3:"Debug"],
multiple: false,
defaultValue: 2,
required: true
)
}
}
}
def installed() {
state.luxArray = [0,0,0,0,0]
state.cloudinessArray = ["","","","",""]
state.maxIlluminance = 0.0
state.hourlyAdd = 0.0
state.addsInHour = 0.0
state.minInHour = 0.0
state.maxInHour = 0.0
state.maxInDay = 0.0
state.variance = 0.0
initialize()
}
def updated() {
//state.cloudinessArray = ["","","","",""]
/*state.luxArray = [0,0,0,0,0]
state.maxIlluminance = 0.0
state.hourlyAdd = 0.0
state.addsInHour = 0.0
state.minInHour = 0.0
state.maxInHour = 0.0
state.maxInDay = 0.0
state.variance = 0.0 */
/*unsubscribe(illuminanceController)
unsubscribe(cloudCeilingController)
unsubscribe(insideSensorController)
unsubscribe(lightTargetController)*/
if (settings?.debugMode && settings?.logLevel == "3") {
runIn(3600, logDebugOff) // one hour
logDebug("Log Level will change from Debug to Info after 1 hour")
}
}
def initialize() {
state.moonPhase = "none"
subscribe(weatherStation, "illuminance", illuminanceController)
subscribe(weatherStation, "temperature", cloudCeilingController)
subscribe(indoorSensor, "illuminance", insideSensorController)
subscribe(lightTarget, "level", lightTargetController)
subscribe(moonPhase, "moonPhase", setMoonPhaseController)
schedule('0 07 22 * * ?', setMaxIlluminance)
schedule('0 00 05 * * ?', updateNoonIlluminance)
schedule('0 0 0 21 MAR ?', setSpring,)
schedule('0 0 0 21 JUN ?', setSummer)
schedule('0 0 0 21 SEP ?', setFall)
schedule('0 0 0 21 DEC ?', setWinter)
}
def setSpring() {illuminanceData.setSeason("spring")}
def setSummer() {illuminanceData.setSeason("summer")}
def setFall() {illuminanceData.setSeason("fall")}
def setWinter() {illuminanceData.setSeason("winter")}
def illuminanceController(evt) {
logDebug("Illuminance Sensor Event = ${evt.value}",1)
def lux = evt.value.toInteger()
illuminanceData.setSensorIlluminance(lux)
// set max illuminance
if (lux > illuminanceData.currentValue("maxIlluminance")) {illuminanceData.setMaxIlluminance(lux)}
// add to lux list
state?.luxArray.push(lux)
if (state?.luxArray.size() > 5)
state.luxArray.removeAt(0)
logDebug(state?.luxArray,3)
calcVariance()
cloudsFromIlluminance(lux)
calcWeatherStats(lux)
calcLightIntensity()
}
def cloudCeilingController(evt) {
logDebug("Cloud Ceiling Controller Called ${evt.value}",3)
double temp = weatherStation.currentValue("temperature")
double dewPoint = weatherStation.currentValue("dewPoint")
logDebug("Temp = ${temp} and dewPoint = ${dewPoint}",3)
double ceiling = (temp-dewPoint)/2.5 * 1000
logDebug("ceiling = ${ceiling}",3)
ceiling = Math.round(ceiling)
logDebug("Cloud Ceiling is ${ceiling}",1)
def clouds = "Not Set"
if (ceiling > 23000 && ceiling <= 45000) clouds = "High Clouds"
else if (ceiling > 6500 && ceiling <= 23000) clouds = "Mid-Level Clouds"
else if (ceiling >= 0 && ceiling <= 6500) clouds = "Low Clouds"
else if (ceiling > 45000) {
ceiling = 0
clouds = "No Clouds"
}
weatherStation.setCloudCeiling(ceiling)
state.cloudCeiling = ceiling
}
def calcVariance() {
int size
double varience
double mean
size = state?.luxArray.size();
double sum = 0.0;
for(double a : state?.luxArray)
sum += a;
mean = sum/size;
double lux = 0;
for(double a :state?.luxArray)
lux += (a-mean)*(a-mean)
variance = lux/(size-1)
variance = variance / 10000
variance = variance.round(2)
logDebug("Variance is ${variance}",1)
state.variance = variance
logDebug("Adding illuminaceVariance to Illumiance Data: ${variance}",3)
illuminanceData.setIlluminanceVariance(variance)
}
def cloudsFromIlluminance(lux) {
double illuminance = lux
double variance = state?.variance
// get sunrise and sunset, now
def riseAndSet = getSunriseAndSunset()
def sunRise = riseAndSet.sunrise
def sunSet = riseAndSet.sunset
def now = new Date()
double sunMinute
def afternoon = false
double morningEvening
double noon
double sunValue
double partlyCloudyVariance
double cloudyVariance
// get Minutes since Sunrise
def sunriseStart = sunRise //new Date(sunRise)
use(TimeCategory)
{
def timeSinceSunrise = now - sunriseStart
state.sunriseDuration = timeSinceSunrise
}
def minSinceSunrise = (state?.sunriseDuration.getHours() * 60) + state?.sunriseDuration.getMinutes()
logDebug("Minutes Since Sunrise is ${minSinceSunrise}",3)
// get Minutes until Sunset
def sunsetEnd = sunSet //new Date(sunSet)
use(TimeCategory)
{
def timeToSunset = sunsetEnd - now
state.sunsetDuration = timeToSunset
}
def minToSunset = (state?.sunsetDuration.getHours() * 60) + state?.sunsetDuration.getMinutes()
logDebug("Minutes To Sunset is ${minToSunset}",3)
// Determine if After or Before Noon
if (minToSunset < minSinceSunrise) {
sunMinute = minToSunset
afternoon = true
} else {
sunMinute = minSinceSunrise
afternoon = false
}
logDebug("sunMinute set to ${sunMinute}",3)
// Calculate sunValue using formulas for the day segments
double noonIlluminance = illuminanceData.currentValue("noonIlluminance") - 9000 // used for noon calcs for Max
// set the formula used for sunValue based on time of day
morningEvening = Math.pow(sunMinute,2) + (sunMinute * 10) //morningEvening = (257.5 * (sunMinute - 108) + 10680)
noon = (Math.pow(sunMinute,2) * 0.05) + (sunMinute * 15) + (noonIlluminance - (noonIlluminance * 0.1))
// Determine which sunValue used for day segment
if (morningEvening < noon) {
sunValue = morningEvening
logDebug("sunValue used is Morning/Evening",3)
} else if (morningEvening > noon) {
sunValue = noon
logDebug("sunValue used is Noon",3)
}
logDebug("sunValue is ${sunValue}",3)
// cloudySunRatio -- limit for the ratio between sunMinute and actual lux for cloudy determination
double cloudySunRatio = ((sunMinute / sunValue) * 100) + (sunMinute * 0.005)
if (sunMinute < 120) cloudySunRatio = cloudySunRatio - (0.01 * sunMinute) // reduce in ealy morning/evening
// variance get larger as the day gets brighter, and smaller as the day gets dimmer
// Use min since sunrise and min to sunset and divide to reduce to a partly cloudy condition .
// cloudy variance marks an illumination variance that is too high to be all cloudy.
double partCloudyDiv = 0.5 // partly cloudy diviser for minute to get an expected variance
double cloudyDiv = 3 // cloudy variance diviser for minute
double newPartlyCloudyVariance
double newCloudyVariance
if (minSinceSunrise < minToSunset) {
newPartlyCloudyVariance = minSinceSunrise / partCloudyDiv
newCloudyVariance = minSinceSunrise / cloudyDiv
} else {
newPartlyCloudyVariance = minToSunset / partCloudyDiv
newCloudyVariance = minToSunset / cloudyDiv
}
// Set cloudySunRatio and rounded variance variables
partlyCloudyVariance = newPartlyCloudyVariance.round(2)
cloudyVariance = newCloudyVariance.round(2)
logDebug("cloudySunRatio set to ${cloudySunRatio}",3)
logDebug("partlyCloudyVariance set to ${partlyCloudyVariance}",3)
logDebug("cloudyVariance set to ${cloudyVariance}",3)
logDebug("Adding cloudySunRatio to Illumiance Data: ${cloudySunRatio}",3)
illuminanceData.setCloudySunRatio(cloudySunRatio)
// set Polarity
def polarity = "positive"
if (illuminance < sunValue) polarity = "negative"
logDebug("polarity is ${polarity}",3)
// set sunRatio
logDebug("Adding sunValue to Illumiance Data: ${sunValue}",3)
illuminanceData.setSunValue(sunValue)
def sunRatio = sunValue / illuminance
logDebug("sunRatio is ${sunRatio}",3)
// Determine if it is cloudy (not in sunny range)
def cloudy = false
if ((sunRatio >= cloudySunRatio) && polarity == "negative") {cloudy = true}
// Determine how cloudy/sunny it is
def cloudiness = "Not Set"
if (variance > partlyCloudyVariance) {
if (cloudy == true) cloudiness = "Partly Cloudy"
else cloudiness = "Partly Sunny"
} else if (variance <= partlyCloudyVariance && variance >= cloudyVariance) { //
if (cloudy == true) cloudiness = "Mostly Cloudy"
else cloudiness = "Mostly Sunny"
} else if (variance <= cloudyVariance) {
if (cloudy == true) cloudiness = "Cloudy"
else cloudiness = "Sunny"
}
if (cloudiness != "Not Set") {
logDebug("Adding cloudiness as cloudiness to Illumiance Data: ${cloudiness}",3)
illuminanceData.setCloudiness(cloudiness)
weatherStation.setCloudConditions(cloudiness)
} else {logDebug("${app.label} - cloudiness was not set!",2)}
// update to most common last 5
cloudiness = getMostCommonCloudiness(cloudiness)
// set cloudiness to sunrise and sunset at edges of day
if (minSinceSunrise > 0 && minSinceSunrise < 15) {
cloudiness = "Sunrise"
}
if (minToSunset > 0 && minToSunset < 15) {
cloudiness = "Sunset"
}
// insert rain/snow if raining or snowing
def precip = weatherStation.currentValue("rainRate") > 0
if (precip) {
cloudiness = getRainRate()
logDebug("cloudiness updated to precip ${cloudiness}")
} else if ((minToSunset < 0 || minSinceSunrise < 0)) {
cloudiness = moonPhase.currentValue("moonPhase")
iconFile = moonPhase.currentValue("moonPhaseImage") // update to moon Phase if night and not raining/snowing
}
// get the icon file name from cloudiness
def iconFile = getIcon(cloudiness)
logDebug("Adding cloudiness as currentCondtions to Weather Station: ${cloudiness}",3)
weatherStation.setCurrentConditions(cloudiness)
logDebug("Adding icon filename as currentIcon to Weather Station: ${iconFile}",3)
weatherStation.setCurrentIcon(iconFile)
logDebug("Cloudiness is ${cloudiness}",1)
// Log to Google
def logParams = "Lux="+lux+"&Clouds="+state?.cloudCeiling+"&Variance="+state?.variance+"&Sun="+sunValue+"&Cloudy="+cloudiness+"&Sun Ratio="+sunRatio+"&Cloudy Sun Ratio="+cloudySunRatio
googleLogs.sendLog("illuminance", logParams)
}
String getRainRate() {
double rainRate = weatherStation.currentValue("rainRate")
def rainSnow = "Rain"
def currentText = ""
if (weatherStation.currentValue("temperature").toInteger() < 34) {
rainSnow = "Snow"
}
if (rainRate >= 0.001 && rainRate <= 0.098) currentText = "Light "+rainSnow
else if (rainRate >= 0.099 && rainRate <= 0.3) currentText = "Moderate "+rainSnow
else if (rainRate >= 0.31 && rainRate <= 2.0) currentText = "Heavy "+rainSnow
else if (rainRate > 2.0) currentText = "Violent "+rainSnow
logDebug("Rain Rate is ${currentText}")
return currentText
}
String getIcon(cloudiness) {
def icon = cloudiness.toLowerCase()
icon = icon.replace(" ","-")
def iconFile = icon + ".svg"
return iconFile
}
String getMostCommonCloudiness(String cloudiness) {
// add to lux list
state?.cloudinessArray.push(cloudiness)
if (state?.cloudinessArray.size() > 5)
state.cloudinessArray.removeAt(0)
logDebug("Cloudiness Array: ${state?.cloudinessArray}",2)
def sentIcon = iconFile
// Use most common of last five as cloudiness, unless sunset, sunrise, or night
if (cloudiness == "Sunset" || cloudiness == "Sunrise" || cloudiness == "Sunny" || cloudiness == "Partly Sunny" || cloudiness == "Mostly Sunny" || cloudiness == "Mostly Cloudy" || cloudiness == "Partly Cloudy" || cloudiness == "Cloudy"){
def sunny = 0
def partlySunny = 0
def mostlySunny = 0
def mostlyCloudy = 0
def partlyCloudy = 0
def cloudy = 0
for (x=0; x<5; x++) {
if (state?.cloudinessArray[x] == "Sunny") sunny++
if (state?.cloudinessArray[x] == "Partly Sunny") partlySunny++
if (state?.cloudinessArray[x] == "Mostly Sunny") mostlySunny++
if (state?.cloudinessArray[x] == "Mostly Cloudy") mostlyCloudy++
if (state?.cloudinessArray[x] == "Partly Cloudy") partlyCloudy++
if (state?.cloudinessArray[x] == "Cloudy") cloudy++
}
def mostCommon = "Sunny"
def greatest = sunny
if (partlySunny > greatest) mostCommon = "Partly Sunny"; greatest == partlySunny
if (mostlySunny > greatest) mostCommon = "Mostly Sunny"; greatest == mostlySunny
if (mostlyCloudy > greatest) mostCommon = "Mostly Cloudy"; greatest == mostlyCloudy
if (partlyCloudy > greatest) mostCommon = "Partly Cloudy"; greatest == partlyCloudy
if (cloudy > greatest) mostCommon = "Cloudy"
logDebug("Most Common is ${mostCommon}",3)
cloudiness = mostCommon
}
return cloudiness
}
def setMoonPhaseController(evt) {
logDebug("Moon Phase Data ${evt.value}",3)
def newPhase = evt.value()
def current = state?.moonPhase
if (current != newPhase) {
weatherData.setMoonPhase(newPhase)
logDebug("Moon Phase updated to ${newPHase}",1)
}
}
def calcLightIntensity(lux) {
logDebug("Calculating Light Intensity",3)
def solarRadiation = weatherStation.currentValue("solarRadiation")
def noonIlluminance = illuminanceData.currentValue("noonIlluminance")
def noonRadiation = (noonIlluminance/100)
def intensity = (solarRadiation / noonRadiation) * 95
intensity = Math.round(intensity)
logDebug("Sending Light Intensity to Illumination Data: ${intensity}",3)
illuminanceData.setLightIntensity(intensity)
logDebug("Light Intensity is ${intensity}",1)
}
// Calc other illuminance Data
def calcWeatherStats(lux) {
logDebug("Calculating Stats with Lux",3)
state.maxInHour = lux
if (lux > state?.maxInDay) state.maxInDay = lux
if (illuminance < state?.minInHour) state.minInHour = lux
}
// set Max Illumance - scedule at end of day
def setMaxIlluminance() {
logDebug("Calculating MaxIlluminance for Day",3)
if (state?.maxInDay > state?.maxIlluminance) {
logDebug("Updating Max Illuminance in Illuminance Data: ${state?.maxIlluminance}",3)
}
}
def updateNoonIlluminance() {
app.updateSetting("logLevel",[value:"3",type:"enum"])
def noonIlluminance = illuminanceData.currentValue("noonIlluminance").toInteger()
logDebug("Current Noon Illuminance is ${noonIlluminance}",1)
// get the year
def now = new Date()
String year = now.format("yyyy")
// create date strings
String summerSolstice = "${year}-06-20 00:00:00"
String winterSolstice = "${year}-12-21 00:00:00"
String yearStart = "${year}-01-1 00:00:01"
String yearEnd = "${year}-12-31 23:59:59"
// convert strings to dates
def summer = Date.parse("yyyy-MM-dd hh:mm:ss", summerSolstice)
def winter = Date.parse("yyyy-MM-dd hh:mm:ss", winterSolstice)
def start = Date.parse("yyyy-MM-dd hh:mm:ss", yearStart)
def end = Date.parse("yyyy-MM-dd hh:mm:ss", yearEnd)
def solsticeState = ""
// determine if before or after summer solstice
if (timeOfDayIsBetween(start, summer, now)) solsticeState = "before"
if (timeOfDayIsBetween(summer, winter, now)) solsticeState = "after"
if (timeOfDayIsBetween(winter, end, now)) solsticeState = "before"
logDebug("solsticeState is ${solsticeState} solstice",1)
// add or substrct to noon illuminance based on summer solstice
def luxPerDay = illuminanceData.currentValue("luxChangePerDay").toInteger()
logDebug("Lux per Day is ${luxPerDay}")
if (solsticeState == "after") {
noonIlluminance = noonIlluminance - luxPerDay
}
if (solsticeState == "before") {
noonIlluminance = noonIlluminance + luxPerDay
}
illuminanceData.setNoonIlluminance(noonIlluminance)
logDebug("Updated Noon Illuminance is ${noonIlluminance}", 1)
}
// sync indoor illuminance to illuminance data
def insideSensorController(evt) {
state.insideSensor = evt.value.toInteger()
logDebug("Inside Sensor Event = ${state?.insideSensor}",3)
illuminanceData.setIndoorIlluminance(evt.value)
}
// sync light target changes to illuminance data
def lightTargetController(evt) {
state.lightTarget = evt.value.toInteger()
logDebug("Light Target Event = ${state?.lightTarget}",3)
illuminanceData.setLightTarget(evt.value)
}
// log debug if no logLevel added
def logDebug(txt) {
try {
if (settings?.debugMode) {
log.debug("${app.label} - ${txt}") // debug
}
} catch(ex) {
log.error("bad debug message")
}
}
// log by level when lvl supplied
def logDebug(txt, lvl){
try {
logLevel = settings?.logLevel.toInteger()
if (settings?.debugMode) {
if (lvl == 3 && logLevel == 3) log.debug("${app.label} - ${txt}") // debug
else if (lvl >= 2 && logLevel >= 2) log.warn("${app.label} - ${txt}") // warn
else if (lvl >= 1 && logLevel >= 1) log.info("${app.label} - ${txt}") // info
}
} catch(ex) {
log.error("bad debug message")
}
}
def logDebugOff() {
logDebug("Turning off debugMode")
app.updateSetting("logLevel",[value:"1",type:"enum"])
}