/*
* This app is derived from the community 'Average Temperatures' app written by Bruce Ravenel.
* Dewpoint calculation algorithm thanks to Ashok Aiyar in the Hubitiat Community.
* Ability to use a periodic calculation interval rather than event-driven, due to the irregular update timing from the sensors.
* The smoothing works well with a continuous time series.
*
* Change History:
*
* Date Who What
* ---- --- ----
* 2020-09-15 Guffman Original Creation
* 2020-10-25 Guffman Added clamping due to some random wild readings from the humidity sensors.
* 2021-02-04 Guffman Revised initialize code, added smoothing feature.
* 2021-05-21 Dlmcpaul Added support for Celsius temperature scale
* 2022-10-27 jkister Allow periodic and/or event-driven based schedule
*
*/
definition(
name: "Virtual Dewpoint Sensor",
namespace: "Guffman",
author: "Guffman",
description: "Periodically calculate a dewpoint in degrees Farenheit, given a humidity and a temperature. Allow for time series smoothing using an exponential smoothing filter algorithm.",
category: "Convenience",
importUrl: "https://raw.githubusercontent.com/guffman/Hubitat/master/apps/VirtualDewpointSensor.groovy",
iconUrl: "",
iconX2Url: "")
preferences {
page(name: "mainPage")
}
def mainPage() {
dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) {
section {
input "thisName", "text", title: "Name this virtual dewpoint sensor", required: true, submitOnChange: true
if(thisName) {
app.updateLabel("$thisName")
state.label = thisName
}
input "tempSensor", "capability.temperatureMeasurement", title: "Select Temperature Sensor", submitOnChange: true, required: true, multiple: false
input "humidSensor", "capability.relativeHumidityMeasurement", title: "Select Humidity Sensor", submitOnChange: true, required: true, multiple: false
input "lowClamp", "decimal", title: "Dewpoint clamp value low:", defaultValue: 30.0, required: true, submitOnChange: false
input "highClamp", "decimal", title: "Dewpoint clamp value high:", defaultValue: 70.0, required: true, submitOnChange: false
input "alpha", "decimal", title: "Exponential smoothing filter factor (1.0 = no smoothing, 0.1 = maximum smoothing):", defaultValue: 1.0, required: true, range: "0.1..1.0", submitOnChange: false
input "useSubscribe", "bool", title: "Update value when new data is received from the sensor", defaultValue: false, submitOnChange: true
input "usePeriodic", "bool", title: "Update value on a schedule", defaultValue: true, submitOnChange: true
if(usePeriodic){
input "calcInterval", "enum", title: "Calculation update interval", required: true, options: ["1 min", "5 min", "10 min", "15 min"], defaultValue: "5 min"
}
input "enableDebug", "bool", title: "Enable debug logging", defaultValue: false, submitOnChange: true
}
}
}
def debug (message) {
if(enableDebug){
log.debug "${message}"
}
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
unschedule()
initialize()
}
def initialize() {
// Check if the device exists. If not create it
def dewpointDev = getChildDevice("Dewpoint_${app.id}")
if(!dewpointDev) {
// Create the virtual temperature sensor, initialize it at the current %RH as a rough first guess
dewpointDev = addChildDevice("hubitat", "Virtual Temperature Sensor", "Dewpoint_${app.id}", null, [label: thisName, name: thisName])
dewpointDev.setTemperature(humidSensor.currentValue("humidity"))
}
calcDewpoint()
if(useSubscribe){
debug("subscribing to sensor events")
subscribe(tempSensor, "temperature", calcDewpoint)
subscribe(humidSensor, "humidity", calcDewpoint)
}
if(usePeriodic){
// Schedule updates
debug("enabling periodic schedule")
switch (calcInterval)
{
case "1 min":
runEvery1Minute("calcDewpoint")
break
case "5 min":
runEvery5Minutes("calcDewpoint")
break
case "10 min":
runEvery10Minutes("calcDewpoint")
break
case "15 min":
runEvery15Minutes("calcDewpoint")
break
}
}
}
def calcDewpoint(evt) {
// Compute new Td from current T and %RH
def currentTemp = tempSensor.currentValue("temperature")
def currentHumid = humidSensor.currentValue("humidity")
def tempC = (location.temperatureScale == "F") ? f2c(currentTemp) : currentTemp
def dewpointC = dpC(tempC, currentHumid)
def result = (location.temperatureScale == "F") ? c2f(dewpointC) : dewpointC
def newDewpoint = result.toDouble().round(1)
// Get the prior Td value
def dewpointDev = getChildDevice("Dewpoint_${app.id}")
def prevDewpoint = dewpointDev.currentValue("temperature")
if(! prevDewpoint){
// there is no previous on first run
prevDewpoint = newDewpoint
}
debug("In calcDewpoint, prevDewpoint=${prevDewpoint}, currentTemp=${currentTemp}, currentHumid=${currentHumid}, result=${result}, newDewpoint=${newDewpoint}")
// Validity tests
if ((result >= lowClamp) && (result <= highClamp)) {
// Valid Td value. Compute filter, update the Td device.
state.clamped = false
def smoothedResult = (alpha * newDewpoint) + ((1.0 - alpha) * prevDewpoint)
def smoothedDewpoint = smoothedResult.toDouble().round(1)
debug("In calcDewpoint, smoothedResult=$smoothedResult, smoothedDewpoint=$smoothedDewpoint")
dewpointDev.setTemperature(smoothedDewpoint)
// Update the app label for fancy-ness.
def dptStr = String.format("%.1f", smoothedDewpoint) + "°" + location.temperatureScale
dptStr = dptStr.trim()
updateAppLabel("${dptStr}", "green")
} else {
// Outside clamp limits. Don't update the Td device and don't perform smoothing.
state.clamped = true
// Update the app label for fancy-ness.
def dptStr = String.format("%.1f", newDewpoint) + "°" + location.temperatureScale
dptStr = dptStr.trim()
if (result < lowClamp) {
log.warn "Dewpoint clamped: calculated value ${dptStr} below ${lowClamp}"
updateAppLabel("Below Low Limit - Clamped", "orange", dptStr)
} else if (result > highClamp) {
log.warn "Dewpoint clamped: calculated value ${dptStr} above ${highClamp}"
updateAppLabel("Above High Limit - Clamped", "orange", dptStr)
}
}
}
def f2c (degF) {
def degC = 5 * ((degF-32)/9)
return degC
}
def c2f (degC) {
def degF = 32 + (9*(degC/5))
return degF
}
def dpC (T, RH) {
def Td = 243.04 * (Math.log(RH/100)+((17.625*T)/(243.04+T)))/(17.625-Math.log(RH/100)-((17.625*T)/(243.04+T)))
return Td
}
def updateAppLabel(textStr, textColor, def textPrefix=null) {
def str = (textPrefix != null) ? """ ($textPrefix $textStr)""" : """ ($textStr)"""
app.updateLabel(state.label + str)
}