Modifying 'Mistake'

This commit is contained in:
Tom Lawson
2016-12-08 19:12:47 -08:00
parent ac386e22c0
commit 91c35fb85b

View File

@@ -14,10 +14,6 @@
* for the specific language governing permissions and limitations under the License.
*
To DO:
add air quality
fix blank activity feed entries
fix double messages on init
*/
@@ -25,14 +21,7 @@ definition(
name: "Ventilation Guru",
namespace: "LawsonAutomation",
author: "Tom Lawson",
description: "This app implements night and day venting using whole house fans and/or other ventilation equipment. " +
"It can supplement or in some climates take the place of conventional air-conditioners or heaters. " +
"It makes use of inside sensors along with current and forecast conditions from Weather Underground to " +
"accurately predict and fully automate when and how much to ventilate. " +
"Configurations typically include an indoor temperature sensor, a whole house fan, and motorized windows/skylights " +
"(though manually controlled windows with contact sensors are also an option). " +
"This app also automatically sets your thermostat to heating or cooling and turns it off during venting. " +
"A simple learning algorithm allows the app to better regulate inside temperatures over time.",
description: "This was a mistaken publication.",
category: "Green Living",
iconUrl: "https://raw.githubusercontent.com/lawsonautomation/icons/master/guru-60.png",
iconX2Url: "https://raw.githubusercontent.com/lawsonautomation/icons/master/guru-120.png",
@@ -46,183 +35,9 @@ def mainPage() {
state.debugMode = false
dynamicPage(name: "mainPage", install: true, uninstall: true) {
section("Miscellaneous Inside Sensors") {
input "myTempSensor", "capability.temperatureMeasurement", required: true, title: "Temperature Sensor"
input "myHumiditySensor", "capability.relativeHumidityMeasurement", required: false, title: "Humidity Sensor"
input "mySmokeDetectors", "capability.smokeDetector", required: false, multiple: true, title: "Smoke Detectors"
input "myThermostats", "capability.thermostat", required: false, multiple: true, title: "Thermostats", submitOnChange: true
}
section("Daytime Comfort Zone (at least 5 F or 3 C range)") {
if (state.debugMode) {
// these two lines are needed as a workaround for a simulator bug
input "myDaytimeMin", "decimal", title: "Minimum", required: true
input "myDaytimeMax", "decimal", title: "Maximum", required: true
} else if (myDaytimeMin == null && myDaytimeMax == null && myThermostats) {
// use thermostat values if you have them
def minTemp = cAdj(70)
def maxTemp = cAdj(78)
minTemp = myThermostats[0]?.currentValue("heatingSetpoint")
maxTemp = myThermostats[0]?.currentValue("coolingSetpoint")
// verify that the comfort zone is at least 5 F
if (maxTemp < minTemp + cAdj(5)) {
maxTemp = minTemp + cAdj(5)
}
input "myDaytimeMin", "decimal", title: "Minimum", required: true, submitOnChange: true, defaultValue: "${minTemp}"
input "myDaytimeMax", "decimal", title: "Maximum", required: true, submitOnChange: true, defaultValue: "${maxTemp}"
} else {
if (myDaytimeMax == null) {
input "myDaytimeMin", "decimal", title: "Minimum", required: true, submitOnChange: true
} else {
def minTemp = myDaytimeMax - cAdj(5)
input "myDaytimeMin", "decimal", title: "Minimum", required: true, submitOnChange: true, range: "-50..${minTemp}"
}
if (myDaytimeMin == null) {
input "myDaytimeMax", "decimal", title: "Maximum", required: true, submitOnChange: true
} else {
def maxTemp = myDaytimeMin + cAdj(5)
input "myDaytimeMax", "decimal", title: "Maximum", required: true, submitOnChange: true, range: "${maxTemp}..100"
}
}
}
section("Ventilation Fans") {
input "myWholeHouseFans", "capability.switch", required: false, multiple: true, title: "Whole House Fans", submitOnChange: true
input "myAtticFans", "capability.switch", required: false, multiple: true, title: "Attic Fans"
input "myGarageFans", "capability.switch", required: false, multiple: true, title: "Garage Fans", submitOnChange: true
if (myGarageFans) {
input "myGarageTempSensor", "capability.temperatureMeasurement", required: false, title: "Garage Temperature Sensor (optional)"
}
input "myBasementFans", "capability.switch", required: false, multiple: true, title: "Basement/Crawlspace Fans"
}
section("Windows/Skylights") {
input "myWindowsSwitch", "capability.switch", required: false, multiple: true, title: "Motorized Windows/Skylights", submitOnChange: true
input "myContactSensors", "capability.contactSensor", required: false, multiple: true, title: "Window Contact Sensors", submitOnChange: true
input "myCoverings", "capability.windowShade", required: false, multiple: true, title: "Window Coverings"
}
if (myWholeHouseFans || myWindowsSwitch) {
section("Whole House Fan and Window/Skylight Operation") {
if (myWholeHouseFans && (myWindowsSwitch || myContactSensors)) {
input "myWindowsNeeded", "number", title: "Number of Open Windows Needed for Whole House Fan (default is 1)", required: false
}
input "myModes", "mode", title: "Whole House Fan/Window Operating Modes (default is All)", multiple: true, required: false
input "mySuspendOperation", "enum", title: "Suspend Whole House Fan/Window Operation", options: ["Temporarily Suspend"], required: false
}
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
}
section("Users Guide") {
href(name: "href",
title: "Descriptions, Tips and Tricks",
required: false,
image: "https://raw.githubusercontent.com/lawsonautomation/icons/master/info.png",
page: "UsersGuide")
}
section("Version 1.2.2 - Copyright © 2016 Thomas Lawson. " +
"If you like this app and would like to contribute to its development (and the development of similar apps), " +
"tap the link below to make a donation.") {
href(name: "LawsonAutomation",
title: "Donate via PayPal",
description: "Tap to Donate",
required: false,
style: "external",
image: "https://raw.githubusercontent.com/lawsonautomation/icons/master/guru-60.png",
url: "http://PayPal.Me/LawsonAutomation")
}
}
}
}
def UsersGuide() {
dynamicPage(name: "UsersGuide", title: "User's Guide", nextPage: "mainPage") {
section("Overview") {
paragraph "This app implements night and day venting using whole house fans and/or other ventilation equipment. " +
"It can supplement or in some climates take the place of conventional air-conditioners or heaters. " +
"It makes use of inside sensors along with current and forecast conditions from Weather Underground to " +
"accurately predict and fully automate when and how much to ventilate. " +
"Configurations typically include a whole house fan, motorized windows/skylights or window's with contact sensors, and an indoor temperature sensor. " +
"It also automatically sets your thermostat to heating or cooling and turns it off during venting. " +
"A simple learning algorithm allows the app to better regulate inside temperatures over time."
}
section("Daytime Comfort Zone Minimum and Maximum") {
paragraph "The comfort zone boundaries should be the same as the daytime settings for your thermostat, " +
"the minimum being the heating set point and the maximum being the cooling set point."
}
section("Temperature Sensor") {
paragraph "The temperature sensor should be placed in a central part of the house away from windows, doors, vents, or any other sources of heat or cold. " +
"When in doubt, place it near your thermostat. Best case is for your temperature sensor to also be your thermostat."
}
section("Humidity Sensor") {
paragraph "This sensor allows inside and outside air to be compared when outside air has moderate to high levels of humidity. " +
"In drier climates such as California this sensor is not necessary. "
}
section("Smoke Detectors") {
paragraph "If any of these sensors detect smoke, whole house fans are turned off and will remain off until no smoke is detected. " +
"Windows are not shut when smoke is present for egress purposes. This is an important, recommended safety feature."
}
section("Thermostats") {
paragraph "These thermostats will be turned off when windows are open or whole house fans are on. " +
"Additionally, thermostats will be set to cooling or heating mode automatically when not venting."
}
section("Whole House Fans") {
paragraph "Whole house fans ventilate inside air into the attic, drawing air from windows or skylights. " +
"These fans will be utilized at appropriate times during the day to raise or lower inside temperatures. " +
"When and for how long they run is calculated using forecast and current temperatures for your area."
paragraph "WARNING: Whole house fans, automated or not, create a strong negative pressure inside the home. " +
"There should be no circumstances in which flame, ash, or smoke can be drawn into the home from a fireplace. " +
"A glass insert or other like measure should always be installed along with a whole house fan to prevent " +
"fire hazard or smoke/ash related damage."
}
section("Attic Fans") {
paragraph "When the house is in cooling mode, attic fans will run continually to lower attic temperatures. " +
"Attic fans are turned off during whole house fan operation to save wear and tear on attic fans. " +
"No attic temperature sensor is required."
}
section("Garage Fans") {
paragraph "When in cooling mode, the garage fan will turn on in the late afternoon and off in the late morning. " +
"These are the times when outside temperatures are lower than garage temperatures. " +
"No garage temperature sensor is required."
}
section("Basement/Crawlspace Fans") {
paragraph "When heating is needed, the basement/crawlspace fans will run when outside temperatures exceed temperatures inside the house. " +
"No basement/crawlspace temperature sensor is required."
}
section("Motorized Windows/Skylights") {
paragraph "Windows/skylights are automatically opened to accommodate heating or cooling when conditions are right."
}
section("Contact Sensors") {
paragraph "Contact sensors can be used either as an extra check that a motorized window is open or to indicate that a manual window has been opened."
}
section("Window Coverings") {
paragraph "These coverings will be opened along with windows when ventilating. " +
"Otherwise, window coverings are opened at sunrise and closed at sunset."
}
section("Whole House Fan/Window Operating Modes") {
paragraph "By default, whole house fans and windows/skylights will operate in all modes. " +
"However, you can specify with this option under which modes you want windows and whole house fans to automatically open/turn on."
}
section("Number of Open Windows Needed") {
paragraph "Whole house fans will not turn on automatically until the total number of window/skylights and contact sensors equals or exceeds this number. " +
"Note: If an open window is automated and has a contact sensor installed, this will count as two open windows."
}
section("Suspend Operation") {
paragraph "The automated operation of Whole House Fans and windows/skylights can be suspended temporarily by clicking the checkbox. " +
"To resume operation, deselect the checkbox."
}
section("Tips and Tricks") {
paragraph "Look in 'Notifications' of the SmartThings app to see what Ventilation Guru is doing."
paragraph "Whole house fan automation can be achieved (for cooling) without motorized windows by using contact sensors and manually opening the windows in the evening. " +
"It is not recommended that whole house fans be automated without motorized windows, contact sensors, or some other means of obtaining outside air, such as louvers."
paragraph "The rule of thumb regarding the open window area required for a whole house fan is to take the square of the diameter of the fan and then double it."
paragraph "If your thermostat is not also controlled by this app, the comfort zone minimum and maximum might need to be within rather than equal to the daytime thermostat setting."
paragraph "WARNING: Without proper ventilation, whole-house fans can create a backdraft in your furnace or water heater, potentially causing carbon monoxide to be pulled into your home. " +
"Operating the fan without open windows may also cause the fan to overheat."
paragraph "WARNING: Whole house fans, automated or not, create a strong negative pressure inside the home. " +
"There should be no circumstances in which flame, ash, or smoke can be drawn into the home from a fireplace. " +
"A glass insert or other like measure should always be installed along with a whole house fan to prevent " +
"fire hazard or smoke/ash related damage."
}
}
}
def installed() {
LOG "Installed with settings: ${settings}"
@@ -239,776 +54,6 @@ def updated() {
// initiallization methods
def initialize() {
initGlobals()
subscriptions()
def nowString = initDaytimeFlag()
initInsideTempAndHumidity()
// change the ventilation algorithm at dawn
subscribe(location, "sunrise", sunriseHandler)
subscribe(location, "sunset", sunsetHandler)
// turn on the garage fan if conditions are right and after 7 PM
state.oldOutsideTemp = nowString > "19:00" ? 999 : state.outsideTemp
// init window coverings
if (myCoverings) {
if (state.daytime) {
openUp(myCoverings, "coverings", "Coverings")
} else {
closeUp(myCoverings, "coverings", "Coverings")
}
}
getCurrentConditions()
runEvery10Minutes(getCurrentConditions)
}
def initGlobals() {
state.morningAdjConst = 5
state.morningAdjustment = 0
state.currentConditionsFailCntr = 99 // start off as if we've never talked to weather underground
state.forecastFailCntr = 99
state.callCntr = 99 // used to get forecast hourly
state.rain = false
state.curPrecip = 0.0
state.curPrecipCntr = 0
// inside mean temp and skew calculations
state.insideTempIndex = 1 // used to update array of inside temperatures
state.insideMeanTempReady = false // true when we have 24 inside temperatures, one per hour
state.greenhouseEffectCntr = 0 // increments when skew is < 1 && > -1
if (state.greenhouseEffect == null) { // keep the old greenhouse effect if we have it
state.greenhouseEffect = cAdj(3.0) // accounts for greenhouse effect and inside sources of heat
} else {
LOG("Saved Greenhouse Effect is ${state.greenhouseEffect}")
sanityCheckGreenhouseEffect()
}
state.skew = 0
state.meanRange = (myDaytimeMax + myDaytimeMin) / 2
state.outsideTemp = state.meanRange
state.meanOutside = state.meanRange
state.outsideHumidity = 20
state.outsideHeatIndex = state.meanRange
state.windowsNeeded = myWindowsNeeded ?: (myWindowsSwitch || myContactSensors) ? 1 : 0
state.newOutsideTemp = state.meanRange
state.oldOutsideTemp = state.meanRange
initDeviceStates()
}
def initDeviceStates() {
// these device state enumerations lock-in manual changes and prevent thrashing
state.Unknown = 0
state.On = 1
state.Off = 2
state.Open = 3
state.Closed = 4
state.Opening = 5
state.Closing = 6
state.Heating = 7
state.Cooling = 8
state.wholeHouseFans = state.Unknown
state.atticFans = state.Unknown
state.basementFans = state.Unknown
state.windowsSwitch = state.Unknown
state.coverings = state.Unknown
state.garageFans = state.Unknown
state.thermostats = state.Unknown
state.modes = state.Unknown
}
def subscriptions() {
subscribe(myTempSensor, "temperature", temperatureChangedHandler)
if (myWindowsSwitch) {
subscribe(myWindowsSwitch, "switch.on", windowOpenedHandler)
subscribe(myWindowsSwitch, "switch.off", windowClosedHandler)
}
if (myWholeHouseFans) {
subscribe(myWholeHouseFans, "switch.on", whfOnHandler)
subscribe(myWholeHouseFans, "switch.off", whfOffHandler)
}
if (myModes) {
subscribe(location, "mode", modeChangedHandler)
}
if (myHumiditySensor) {
subscribe(myHumiditySensor, "humidity", humidityChangedHandler)
}
if (myContactSensors) {
subscribe(myContactSensors, "contact.open", windowOpenedHandler)
subscribe(myContactSensors, "contact.closed", windowClosedHandler)
}
if (mySmokeDetectors) {
subscribe(mySmokeDetectors, "smoke", smokeChangedHandler)
}
}
// returns a string of current time in HH:mm
def initDaytimeFlag() {
def myDate = new Date(now())
def nowString = myDate?.format("HH:mm", location?.timeZone)
def sunriseDate = Date?.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", location?.currentValue("sunriseTime"))
def sunsetDate = Date?.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", location?.currentValue("sunsetTime"))
def sunriseTime = sunriseDate?.format("HH:mm", location?.timeZone)
def sunsetTime = sunsetDate?.format("HH:mm", location?.timeZone)
state.daytime = (nowString > sunriseTime && nowString < sunsetTime)
LOG("Sunrise: ${sunriseTime}, Now: ${nowString}, Sunset: ${sunsetTime}, Daytime: ${state.daytime}")
return nowString
}
def initInsideTempAndHumidity() {
// init inside temp
def currentState = myTempSensor.temperatureState
state.insideTemp = 72.0
if (currentState) {
state.insideTemp = currentState.integerValue
LOG "Initial Inside Temp: ${state.insideTemp}"
} else {
sendNotificationEvent("Ventilation Guru Error: Unable to obtain initial inside temperature!")
}
state.insideHeatIndex = state.insideTemp
state.insideHumidity = 20
//state.highWaterMark = state.insideTemp
// init the inside humidity and heatIndex if we have a humidity sensor
if (myHumiditySensor) {
def response = myHumiditySensor.currentState("humidity")
if (response) {
state.insideHumidity = response?.integerValue
state.insideHeatIndex = getHeatIndex(state.insideTemp, state.insideHumidity)
LOG "Initial Inside Humidity: ${state.insideHumidity} Temp: ${state.insideTemp} HeatIndex: ${state.insideHeatIndex}"
}
}
}
// event handlers
def smokeChangedHandler(evt) {
LOG "Smoke: ${evt?.stringValue}"
makeComparisons()
}
def whfOnHandler(evt) {
if (state.wholeHouseFans != state.On) {
// WHF was manually turned on, so set other devices accordingly
myWindowsSwitch?.on()
myThermostats?.off()
myAtticFans?.off()
}
}
def whfOffHandler(evt) {
if (state.wholeHouseFans != state.Off) {
// reset devices that we changed when WHF was manually turn on
state.windowsSwitch = state.Unknown
state.thermostats = state.Unknown
state.atticFans = state.Unknown
LOG("whfOffHandler")
makeComparisons()
}
}
def windowOpenedHandler(evt) {
LOG("Window opened")
// disable thermostats
turnOff(myThermostats, "thermostats", "Thermostats")
def windowCnt = openWindowCnt()
if (windowCnt >= state.windowsNeeded) {
makeComparisons()
}
}
def windowClosedHandler(evt) {
def windowCnt = openWindowCnt()
if (windowCnt < state.windowsNeeded) {
LOG("Windows closed")
// make sure the WHF is off
turnOff(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
}
if (windowCnt == 0) {
setThermostatMode()
}
}
def setThermostatMode() {
if (myThermostats) {
if (state.wholeHouseFans == state.On || openWindowCnt() != 0) {
// disable thermostats
turnOff(myThermostats, "thermostats", "Thermostats")
} else if (state.skew > 0) {
if (state.thermostats != state.Heating) {
state.thermostats = state.Heating
myThermostats.heat()
sendNotificationEvent("Ventilation Guru: Thermostat(s) set to Heat mode")
}
} else {
if (state.thermostats != state.Cooling) {
state.thermostats = state.Cooling
myThermostats.cool()
sendNotificationEvent("Ventilation Guru: Thermostat(s) set to Cooling mode")
}
}
}
}
def temperatureChangedHandler(evt) {
state.insideTemp = evt?.doubleValue
state.insideHeatIndex = getHeatIndex(state.insideTemp, state.insideHumidity)
LOG("Inside Temp: ${state.insideTemp}")
makeComparisons()
}
def humidityChangedHandler(evt) {
state.insideHumidity = evt?.doubleValue
state.insideHeatIndex = getHeatIndex(state.insideTemp, state.insideHumidity)
LOG "Inside Humidity Changed: ${state.insideHumidity} Inside Temp: ${state.insideTemp} HeatIndex: ${state.insideHeatIndex}"
makeComparisons()
}
// always open window coverings at sunrise
def sunriseHandler(evt) {
state.morningAdjustment = state.morningAdjConst
openUp(myCoverings, "coverings", "Coverings")
state.daytime = true
makeComparisons()
}
def sunsetHandler(evt) {
state.morningAdjustment = 0
state.daytime = false
// close window coverings at sunset if windows are closed
if (openWindowCnt() == 0) {
closeUp(myCoverings, "coverings", "Coverings")
}
makeComparisons()
}
// mode changed
def modeChangedHandler(evt) {
LOG "Mode Changed: ${location.mode}"
makeComparisons()
}
// Utilities
boolean inWrongMode() {
boolean status = false
if (myModes) {
status = true
myModes.each {
LOG("mode sensor: ${mode}, it ${it}")
//def mode = it.currentValue("mode")
def mode = it
if (mode == location.mode) {
LOG("mode OK!!!!!")
status = false
}
}
}
return status
}
def openWindows() {
// don't resend an open command
if (myWindowsSwitch && state.windowsSwitch != state.On && state.windowsSwitch != state.Opening) {
LOG("Open windows start")
state.windowsSwitch = state.Opening
LOG "thermo state: ${myThermostat?.currentValue('thermostatOperatingState')}"
if (myThermostat && myThermostat.currentValue('thermostatOperatingState') != "idle") {
// wait 5 minutes in case the thermostat is in short cycle prevention mode
runIn(5 * 60, physicallyOpenWindows)
} else {
physicallyOpenWindows()
}
}
}
def physicallyOpenWindows() {
if (state.windowsSwitch == state.Opening) {
turnOn(myWindowsSwitch, "windowsSwitch", "Windows")
state.windowsSwitch = state.Opening
// change state later to indicate we're done, preventing WHF from openning before window is done opening
runIn(90, completeOpenWindows)
}
}
def completeOpenWindows() {
if (state.windowsSwitch == state.Opening) {
state.windowsSwitch = state.On
}
makeComparisons()
}
def closeWindows() {
if (myWindowsSwitch) {
if (state.windowsSwitch == state.Unknown) {
turnOff(myWindowsSwitch, "windowsSwitch", "Windows")
LOG("Windows Closed")
} else if (state.windowsSwitch != state.Off && state.windowsSwitch != state.Closing) {
// close windows without thrashing (wait awhile)
state.windowsSwitch = state.Closing
runIn(45 * 60, completeCloseWindows)
}
}
}
def completeCloseWindows() {
LOG "Complete Close windows: ${state.windowsSwitch}"
if (state.windowsSwitch == state.Closing) {
turnOff(myWindowsSwitch, "windowsSwitch", "Windows")
}
}
def turnOnWhfs() {
if (openWindowCnt() >= state.windowsNeeded) {
turnOn(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
}
}
boolean smokeDetected() {
def status = false
if (mySmokeDetectors) {
mySmokeDetectors.each {
def smoke = it.currentValue("smoke")
LOG "smoke sensor: ${smoke}"
if (smoke == "detected") {
status = true
}
}
}
return status
}
// returns number of open windows
int openWindowCnt() {
int cntr = 0
if (myContactSensors) {
// add up contact sensors
myContactSensors.each {
def contact = it.currentValue("contact")
if (contact == "open") {
cntr++
}
}
}
// don't count windows still opening
if (myWindowsSwitch && state.windowsSwitch == state.On) {
// add up windows
myWindowsSwitch.each {
def window = it.currentValue("switch")
if (window == "on") {
cntr++
}
}
}
LOG "openWindowCnt: ${cntr}, windowsRequired: ${state.windowsNeeded}"
return cntr
}
// NOAA formula most accurate from 70-80 degrees F
// HI = 0.5 * {T + 61.0 + [(T-68.0)*1.2] + (RH*0.094)}
def getHeatIndex(double temperature, double humidity) {
// if we have no humidity sensor just deal with dry-bulb values
if (!myHumiditySensor) {
return temperature
}
def temp = temperature
LOG("TempScale: ${location.temperatureScale}")
if (location.temperatureScale == "C") {
// convert to F for calculation
temp = cToF(temp)
}
// calculate heatIndex
temp = 0.5 * (temp + 61.0 + ((temp-68.0) * 1.2) + (humidity * 0.094))
if (location.temperatureScale == "C") {
// convert back to C
temp = fToC(temp)
}
// do not let this formula lower the dry bulb temperature
if (temp < temperature) {
temp = temperature
}
LOG("Temp: ${temperature}, Humidity: ${humidity}, HeatIndex: ${temp}")
return temp
}
def cToF(temp) {
return temp * 1.8 + 32
}
def fToC(temp) {
return (temp - 32) * 0.5555555556
}
// correct an offset if using metric
def cAdj(offset) {
if (location.temperatureScale == "C") {
offset = offset * 0.5555555556
}
return offset
}
def turnOn(device, String devName, String extName) {
if (device) {
// don't send an open command if closing as it's already open
if (state[devName] == state.Closing) {
state[devName] = state.On
} else if (state[devName] != state.On) {
state[devName] = state.On
device.on()
def action = (devName == "windowsSwitch") ? "Opened" : "Turned on"
sendNotificationEvent("Ventilation Guru: ${action} ${extName}")
}
}
}
def turnOff(device, String devName, String extName) {
// don't resend a command
if (device && state[devName] != state.Off) {
state[devName] = state.Off
device.off()
def action = (devName == "windowsSwitch") ? "Closed" : "Turned off"
sendNotificationEvent("Ventilation Guru: ${action} ${extName}")
}
}
def openUp(device, String devName, String extName) {
// don't set an open command if closing as it's already open
if (state[devName] == state.Closing) {
state[devName] = state.Open
} else if (device && state[devName] != state.Open) {
state[devName] = state.Open
device.open()
sendNotificationEvent("Ventilation Guru: Opened ${extName}")
}
}
def closeUp(device, String devName, String extName) {
// don't resend a command
if (device && state[devName] != state.Closed) {
state[devName] = state.Closed
device.close()
sendNotificationEvent("Ventilation Guru: Closed ${extName}")
}
}
def getCurrentConditions() {
def cond = getWeatherFeature("conditions")
if (cond) {
if (location.temperatureScale == "C") {
state.outsideTemp = cond?.current_observation?.temp_c
} else {
state.outsideTemp = cond?.current_observation?.temp_f
}
if (myHumiditySensor) {
// get the outdoor humidity only if needed
def humidityString = cond?.current_observation?.relative_humidity
// drop the % sign and convert to integer
state.outsideHumidity = humidityString?.substring(0, humidityString?.length() - 1)?.toInteger()
if (state.outsideHumidity == null) {
sendNotificationEvent("Ventilation Guru Error: Unable to obtain outside humidity!")
state.outsideHumidity = 20
}
state.outsideHeatIndex = getHeatIndex(state.outsideTemp, state.outsideHumidity)
} else {
state.outsideHeatIndex = state.outsideTemp
}
processCurPrecip(cond?.current_observation.precip_1hr_in.toDouble())
state.currentConditionsFailCntr = 0
} else {
state.currentConditionsFailCntr = state.currentConditionsFailCntr + 1
if (state.currentConditionsFailCntr > 6) {
// inside hourly values are not accurate anymore
state.insideTempIndex = 1
state.insideMeanTempReady = false
}
}
// get forecast every hour in case it has changed
state.callCntr = state.callCntr + 1
if (state.callCntr > 6) {
state.callCntr = 1
getForecast()
}
LOG "Outside Temp: ${state.outsideTemp}, Humidity: ${state.outsideHumidity}, HeatIndex: ${state.outsideHeatIndex}, cur precip: ${state.curPrecip}"
makeComparisons()
setThermostatMode()
}
def processCurPrecip(double curPrecip) {
// sanity check curPrecip
curPrecip = (curPrecip <= 0 || curPrecip > 999) ? 0 : curPrecip
// prevent window thrashing, remember precip for an hour
if (curPrecip) {
state.curPrecip = curPrecip
state.curPrecipCntr = 6
} else if (state.curPrecipCntr <= 0) {
state.curPrecip = 0.0
state.curPrecipCntr = 0
} else {
state.curPrecipCntr = state.curPrecipCntr - 1
}
}
def getForecast() {
// for garage fan, we need to know when outside temp starts to drop
state.oldOutsideTemp = state.newOutsideTemp
state.newOutsideTemp = state.outsideTemp
getHourlyConditions()
def forecastMeanDelta = state.meanRange - state.meanOutside
// remember inside temp every hour
state[state.insideTempIndex.toString()] = state.insideTemp
state.insideTempIndex = state.insideTempIndex + 1
if (state.insideTempIndex > 24) {
state.insideTempIndex = 1
state.insideMeanTempReady = true
}
// get inside mean temp
def insideMean = 0
if (state.insideMeanTempReady) {
for (int i = 1; i <= 24; i++) {
insideMean += state[i.toString()]
}
insideMean /= 24
} else {
insideMean = state.meanRange
}
calculateSkew(insideMean, forecastMeanDelta)
calculateTarget()
LOG "skew: ${state.skew} Target: ${state.target} meanOutside: ${state.meanOutside} insideMean: ${insideMean}" //greenhouseEffect ${state.greenhouseEffect} deltaWeight ${state.deltaWeight}"
}
// target is the goal temperature
def calculateTarget() {
// determine target differently for heating and cooling
if (state.skew <= 0) {
// in cooling mode
state.target = state.meanRange + cAdj(2) + state.skew
if (state.target < myDaytimeMin) {
state.target = myDaytimeMin
}
} else {
// in heating mode, heat only up to daytime range mean
state.target = state.meanRange - cAdj(3) + state.skew
if (state.target > state.meanRange + cAdj(1)) {
state.target = state.meanRange + cAdj(1)
}
}
}
// skew determines how much to heat/cool
def calculateSkew(insideMean, forecastMeanDelta) {
def oldSkew = state.skew
// to determine heating/cooling level, include forecast outside temp mean delta to inside temp mean delta, less a bit for greenhouse effect
double totalDelta = forecastMeanDelta + (state.meanRange - insideMean) / 2
state.skew = totalDelta - state.greenhouseEffect
// learn the best way to anticipate heating and cooling needs
if (state.skew <= 2 && state.skew >= -2) {
// no heating or cooling so inside temp should stay near middle of range
state.greenhouseEffectCntr = state.greenhouseEffectCntr + 1
if (state.greenhouseEffectCntr >= 24) {
state.greenhouseEffectCntr = 0
// no heating or cooling for 24 hours so mean inside temp should equal midpoint of range
state.greenhouseEffect = state.greenhouseEffect + (insideMean - state.meanRange) / 2
// sendNotificationEvent("New Greenhouse Effect = ${state.greenhouseEffect}")
sanityCheckGreenhouseEffect()
// recalculate skew
state.skew = totalDelta - state.greenhouseEffect
LOG "New greenhouse effect adjustment: ${state.greenhouseEffect}, New Skew: ${state.skew}"
}
} else {
// not a good sample, so start over
state.greenhouseEffectCntr = 0
}
// change thermostat heat/cooling mode if things changed
if (myThermostats && state.thermostats == state.On && (oldSkew > 0 && state.skew <= 0 || oldSkew <= 0 && state.skew > 0)) {
// reset thermostat to heating or cooling after making comparisons and completing any actions
state.thermostats = state.Unknown
}
}
def getHourlyConditions() {
def resp = getWeatherFeature("hourly")
if (!resp) {
// couldn't get the forecast so leave the old forecast unchanged
LOG("Ventilation Guru Error: Could not obtain forecast information!")
state.forecastFailCntr = state.forecastFailCntr + 1
return 0
}
// get rain forecast for now and next hour
def hourlyRain = resp?.hourly_forecast?.condition
state.rain = (hourlyRain && (hourlyRain[0].contains("Rain") || hourlyRain[1].contains("Rain")))
if (hourlyRain) {
LOG("hourlyRain ${state.rain} ${hourlyRain}")
}
// get forecast temps
def outsideHourly
if (location.temperatureScale == "F") {
outsideHourly = resp?.hourly_forecast?.temp?.english
} else {
outsideHourly = resp?.hourly_forecast?.temp?.metric
}
double meanOutside = 0
if (outsideHourly) {
state.forecastFailCntr = 0
outsideHourly[0..23].each {
meanOutside += it?.toDouble()
}
meanOutside /= 24
state.meanOutside = meanOutside
}
else {
LOG("Ventilation Guru Error: Could not obtain the hourly forecast values!")
state.forecastFailCntr = state.forecastFailCntr + 1
return 0
}
LOG "Weather Underground: mean Outside: ${state.meanOutside}, hourly: ${outsideHourly[0..23]}"
}
def sanityCheckGreenhouseEffect() {
if (state.greenhouseEffect < cAdj(1)) {
state.greenhouseEffect = cAdj(1)
} else if (state.greenhouseEffect > cAdj(5)) {
state.greenhouseEffect = cAdj(5)
}
}
def turnOffWHFsAndWindows() {
turnOff(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
turnOff(myWindowsSwitch, "windowsSwitch", "Windows")
}
def turnOffFans() {
turnOff(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
turnOff(myAtticFans, "atticFans", "Attic Fans")
turnOff(myBasementFans, "basementFans", "Basement Fans")
turnOff(myGarageFans, "garageFans", "Garage Fans")
}
def makeComparisons() {
LOG("Begin makeComparisons()")
// shut everything off if we can't get the forecast or current conditions for too long
if (state.currentConditionsFailCntr > 6 || state.forecastFailCntr > 4) {
sendNotificationEvent("Ventilation Guru Error: Check internet connection. Can not access Weather Underground!")
turnOffWHFsAndWindows()
turnOffFans()
} else if (smokeDetected()) {
// turn off all fans and thermostats if there's smoke
sendNotificationEvent("Ventilation Guru Warning: Smoke Detected!")
turnOffFans()
turnOff(myThermostats, "thermostats", "Thermostats")
} else {
checkWholeHouseFansAndWindows()
checkAtticFans()
checkGarageFans()
checkBasementFans()
}
}
def checkWholeHouseFansAndWindows() {
if (myWholeHouseFans || myWindowsSwitch) {
// close windows/WHF if not in a selected mode, operation suspended, or we came home and the inside temp is outside the daytime comfort zone
if (inWrongMode() || mySuspendOperation) {
turnOffWHFsAndWindows()
} else if (state.rain || state.curPrecip > 0.0) { // check precipitation
if (state.windowsSwitch == state.On || state.wholeHouseFans == state.On) {
sendNotificationEvent("Ventilation Guru: Windows closed/Whole House Fans turned off due to rain")
}
turnOffWHFsAndWindows()
} else if (state.skew > 0) { // day venting
if (state.insideTemp >= state.target || state.insideTemp + cAdj(6) > state.outsideTemp || state.outsideTemp > myDaytimeMax) { // prevent thermostat with WHF operation
turnOff(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
// leave windows open longer than WHF to prevent window thrashing
if (state.insideTemp >= state.target || state.insideTemp + cAdj(5) > state.outsideTemp || state.outsideTemp > myDaytimeMax) { // prevent thermostat with window open
turnOff(myWindowsSwitch, "windowsSwitch", "Windows")
}
}
else if (state.insideTemp + cAdj(1) <= state.target && state.insideTemp + cAdj(6) < state.outsideTemp) {
openWindows()
if (state.insideTemp + cAdj(7) < state.outsideTemp) {
turnOnWhfs()
}
}
} else { // night venting
if (state.insideTemp <= state.target || state.outsideTemp > myDaytimeMax || state.insideHeatIndex < state.outsideHeatIndex + cAdj(state.morningAdjustment - 2)) {
turnOff(myWholeHouseFans, "wholeHouseFans", "Whole House Fans")
// leave windows open longer than WHF to prevent window thrashing and close windows after a few minutes in case we just went through the morning adjustment
if (state.insideTemp + cAdj(1) <= state.target || state.outsideTemp > myDaytimeMax || state.insideHeatIndex < state.outsideHeatIndex + cAdj(state.morningAdjustment - 3)) {
closeWindows()
}
} else if (state.insideTemp > state.target + cAdj(1) && state.insideHeatIndex > state.outsideHeatIndex + cAdj(state.morningAdjustment)) {
openWindows()
turnOnWhfs()
}
LOG "Inside Temp: ${state.insideTemp} Outside Temp: ${state.outsideTemp} Target: ${state.target}"
}
}
}
def checkAtticFans() {
if (myAtticFans) {
// run attic fans all the time when in cooling mode unless WHF is on
if (state.skew < 0 && state.wholeHouseFans != state.On && state.insideTemp > state.meanRange + state.skew) {
turnOn(myAtticFans, "atticFans", "Attic Fans")
}
else {
turnOff(myAtticFans, "atticFans", "Attic Fans")
}
}
}
def checkGarageFans() {
if (myGarageFans) {
if (myGarageTempSensor) {
// turn on garage fan if cooling mode and outside is cooler than inside
def garageTemp = myGarageTempSensor.temperature
if (state.skew < -1 && garageTemp < state.outsideTemp) {
turnOn(myGarageFans, "garageFans", "Garage Fans")
} else {
turnOff(myGarageFans, "garageFans", "Garage Fans")
}
} else {
// turn on the garage fan if cooling is needed and night time, it's cool outside, or outside temp is dropping fast
if (state.skew < 0 && state.insideTemp > state.meanRange + state.skew) {
if (!state.daytime || state.outsideTemp < state.meanRange) {
turnOn(myGarageFans, "garageFans", "Garage Fans")
} else {
def myDate = new Date(now())
def nowString = myDate?.format("HH:mm", location?.timeZone)
if (state.newOutsideTemp + cAdj(2) <= state.oldOutsideTemp && nowString > "16:00") {
turnOn(myGarageFans, "garageFans", "Garage Fans")
} else {
turnOff(myGarageFans, "garageFans", "Garage Fans")
}
}
} else {
turnOff(myGarageFans, "garageFans", "Garage Fans")
}
}
}
}
def checkBasementFans() {
if (myBasementFans) {
if (state.skew > 0 && state.outsideTemp >= state.insideTemp && state.insideTemp < state.meanRange + state.skew) {
turnOn(myBasementFans, "basementFans", "Basement Fans")
} else {
turnOff(myBasementFans, "basementFans", "Basement Fans")
}
}
}