diff --git a/smartapps/lawsonautomation/ventilation-guru.src/ventilation-guru.groovy b/smartapps/lawsonautomation/ventilation-guru.src/ventilation-guru.groovy index c20559a..a4822c9 100644 --- a/smartapps/lawsonautomation/ventilation-guru.src/ventilation-guru.groovy +++ b/smartapps/lawsonautomation/ventilation-guru.src/ventilation-guru.groovy @@ -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") - } - } }