ericvitale / ST-Average-Temperature-Trigger

This SmartThings SmartApp uses the average temperature of your temperature sensors and decides based on input to set your thermostat
Apache License 2.0
3 stars 4 forks source link

Feature Request: Simulated Standard Deviation Device Type (that get's updated like the Avg type currently does) #1

Open johnny2678 opened 8 years ago

johnny2678 commented 8 years ago

First, Thanks for this. It always amazes me when my brain comes up with a wacky idea like averaging together temp sensors to determine when to run the AC/heat and someone else has already coded it.

In addition to heat/cool, I run the thermostat fan 15 minutes of every hour to even out hot/cool spots in the house. Instead of running the fan on this schedule, I was thinking it would be nice to have a device that measured the STD of all temp sensors and kick off the thermostat fan when the STD gets too high and run it until the STD returns to normal levels.

It's a theory at the moment but I'm guessing that this model would run the fan less than the scheduled 15/60 minutes (which is the lowest fan schedule you can set with the Nest).

Unfortunately, I'm no coder. Any interesting in creating an STD device type? Hoping it's an easy mod of your existing Average device type. That would at least allow me to examine my baseline and test my theory using CoRE.

ericvitale commented 8 years ago

Update your app an try now. Make sure you are on version 1.0.2

I also updated it to allow multiple automations in a single parent app. BTW, don't try to automate both your thermostat temperature and thermostat fan in the same automation, do two if that is what you want.

BTW - I have not tested this. I just saw your message and spent 30 minutes updating the code. Let me know if it works.

johnny2678 commented 8 years ago

In addition to not being a coder, I'm also not an HVAC guy so my understanding of how/when to use the fan (as opposed to actively heating/cooling) could be WAY off. For heating/cooling, using an AVG measurement makes absolute sense because it will kick in at a certain temp and run until a threshold is met. BUT here's the reason I was suggesting controlling the fan with a STDEV measurement vs an AVG measurement:

Say your averaging together the temps in the LR, BR, and Office. The way the app is now, you have the fan set to go off at 80 and off at 78 (it's summer so we're hoping for a cooling effect). Only the fan isn't actively cooling so you wouldn't expect the average temperature to change. The other consideration is the following scenario where all rooms are the same temperature, but the threshold has been reached so the fan kicks in. In this case, it would just be moving 80 degree air from one room to another not accomplishing anything.

LR - 80 BR - 80 Office - 80

Average = 80 / STDEV = 0

The reason I was thinking it would be nice to control the fan by STDEV is it would kick in when the temperature variation between rooms rises above a certain threshold (works for heating or cooling) and turn back off again when the temperature variation has subsided to a preset level. This would normalize any hot/cool spots in the home without running the heat/cool, which should be cheaper as I understand it.

Consider the same three rooms:

LR - 78 BR - 82 Office - 85

Average = 81.667 / STDEV = 3.511

If we set the fan to turn on when the STDEV is above 3.5 and turn off when the STDEV is below 2, then you could possibly achieve the following measurements:

LR - 79 BR - 82 Office - 82.5

Average = 81.667 (unchanged) / STDEV = 1.89

The average temp hasn't changed because the fan doesn't heat/cool but the temperature in the rooms is more even than before.

Seems like this could be accomplished with another device type for STDEV, and a toggle in the app to "Set virtual device based on temp standard deviation".

Anyway, that was just my proposal and thanks for hearing me out. You're the one doing all the work so feel free to tell me to pound sand ;)

EDIT: formatting

johnny2678 commented 8 years ago

Ok, I couldn't leave it alone. I think I have it working with the following variation to your app code. Testing now. All credit goes to you.

 *  Average Temperature Trigger
 *
 *  Version 1.0.2 - 08/04/14
 *   -- Added the ability to just control an HVAC fan based on average temperature.
 *   -- Now a parent child app for multiple automations.
 *  Version 1.0.1 - 07/24/16
 *   -- Added proper logging to make the app less verbose.
 *   -- Added the active setting.
 *   -- Renamed to Average Temperature Trigger
 *
 *  Version 1.0.0 - 07/05/16
 *   -- Initial Build
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  You can find this SmartApp @ https://github.com/ericvitale/ST-Average-Temperature-Trigger
 *  Don't forget the Settable Temperature Measurement virtual device in the same repository as the SmartApp.
 *  You can find my other device handlers & SmartApps @ https://github.com/ericvitale
 *
 */

definition(
    name: "${appName()}",
    namespace: "ericvitale",
    author: "Eric Vitale",
    description: "Control a thermostat or update a virtual temperature reporting device based on the average temperature of temperature sensors.",
    category: "",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")

preferences {
    page(name: "startPage")
    page(name: "parentPage")
    page(name: "childStartPage")
}

def startPage() {
    if (parent) {
        childStartPage()
    } else {
        parentPage()
    }
}

def parentPage() {
    return dynamicPage(name: "parentPage", title: "", nextPage: "", install: false, uninstall: true) {
        section("Create a new child app.") {
            app(name: "childApps", appName: appName(), namespace: "ericvitale", title: "New Temperature Automation", multiple: true)
        }
    }
}

def childStartPage() {
    return dynamicPage(name: "childStartPage", title: "", install: true, uninstall: true) {
        section("Settable Sensor") {
            input "settableSensor", "capability.temperatureMeasurement", title: "Virtual Settable Temperature Sensor", multiple: false, required: false
            input "setVirtualTemp", "bool", title: "Set virtual temp based on average temp?", required: true, defaultValue: false
        }

//Added by JH: include option to store STDEV in a separate virtual temp sensor
        section("Settable STDEV Sensor") {
            input "setSTDSensor", "capability.temperatureMeasurement", title: "Virtual STDEV Temperature Sensor", multiple: false, required: false
            input "setSTDTemp", "bool", title: "Set virtual STDEV temp device based on temperature variation?", required: true, defaultValue: false
        }

        section("Select your thermostat.") {
            input "thermostat", "capability.thermostat", multiple:false, title: "Thermostat", required: false
            input "setThermostat", "bool", title: "Set your thermostate temperature based on average temperature?", required: true, defaultValue: false
            input "controlFan", "bool", title: "Turn on your HVAC fan based on an average temperature?", required: true, defaultValue: false
        }

        section("Select your temperature sensors.") {
            input "temperatureSensors", "capability.temperatureMeasurement", multiple: true
        }

        section("Select the temperature at which you want to begin active cooling.") {
            input "maxTemp", "decimal", title: "Max Temperature", range: "*", required: false
        }

        section("Select the temperature at which you want to cool to.") {
            input "coolingSetpoint", "decimal", title: "Cooling Setpoint", range: "*", required: false
        }

        section("Select the temperature at which you want to begin active heating.") {
            input "minTemp", "decimal", title: "Min Temperature", range: "*", required: false
        }

        section("Select the temperature at which you want to heat to.") {
            input "heatingSetpoint", "decimal", title: "Heating Setpoint", range: "*", required: false
        }

        section("Select the temp variation (STDEV) at which you want to use the fan to circulate air.") {
            input "fanActivate", "decimal", title: "Activate Fan", range: "*", required: false
        }

        section("Stop running the fan when the temp variation (STDEV) reaches this value.") {
            input "fanDeactivate", "decimal", title: "Deactivate Fan", range: "*", required: false
        }

        section("Setting") {
            label(title: "Assign a name", required: false)
            input "active", "bool", title: "Rules Active?", required: true, defaultValue: true
            input "logging", "enum", title: "Log Level", required: true, defaultValue: "DEBUG", options: ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]
        }
    }
}

private def appName() { return "${parent ? "Temperature Automation" : "Average Temperature Trigger"}" }

private determineLogLevel(data) {
    switch (data?.toUpperCase()) {
        case "TRACE":
            return 0
            break
        case "DEBUG":
            return 1
            break
        case "INFO":
            return 2
            break
        case "WARN":
            return 3
            break
        case "ERROR":
            return 4
            break
        default:
            return 1
    }
}

def log(data, type) {
    data = "ATT -- ${data ?: ''}"

    if (determineLogLevel(type) >= determineLogLevel(settings?.logging ?: "INFO")) {
        switch (type?.toUpperCase()) {
            case "TRACE":
                log.trace "${data}"
                break
            case "DEBUG":
                log.debug "${data}"
                break
            case "INFO":
                log.info "${data}"
                break
            case "WARN":
                log.warn "${data}"
                break
            case "ERROR":
                log.error "${data}"
                break
            default:
                log.error "ATT -- Invalid Log Setting"
        }
    }
}

def installed() {
    log("Installed with settings: ${settings}", "INFO")
    initialization()
}

def updated() {
    log("Updated with settings: ${settings}", "INFO")
    unsubscribe()
    initialization()
}

def initialization() {
    log.debug "Begin initialization()."

    if(parent) { 
        initChild() 
    } else {
        initParent() 
    }

    log.debug "End initialization()."
}

def initParent() {
    log.debug "initParent()"
}

def initChild() {
    if(active) {
        log("App is active.", "INFO")
        subscribe(temperatureSensors, "temperature", temperatureHandler)

        if(controlFan && setThermostat) {
            log("You cannot control a thermostat and an HVAC fan with the same automation. Install another. Defaulting to thermostat.", "WARN")
            controlFan = false
        }

        updateTemp()
    } else {
        log("App is not active.", "INFO")
    }

    log("Initialization complete.", "INFO")
}

def temperatureHandler(evt) {
    log("Temperature event ${evt.descriptionText} and value: ${evt.doubleValue}.", "INFO")
    updateTemp()
}

def updateTemp() {
    def averageTemp = 0.0
    def tempstdTemp = 0.0
    def stdTemp = 0.0
    def currentState

    temperatureSensors.each() {
        log("${it.displayName} Temp: ${it.currentValue("temperature")}.", "TRACE")  

        currentState = it.currentState("temperature")

        log("currentState.integerValue: ${currentState.integerValue}.", "TRACE")

        try {
            averageTemp += currentState.integerValue
        } catch(e) {
            log("ERROR -- ${e}", "ERROR")
        }
    }

    try {
        averageTemp = averageTemp / temperatureSensors.size()
    } catch(e) {
        log("ERROR -- ${e}", "ERROR")
    }

//added by JH - calculate STDEV of the temp sensor values  
    temperatureSensors.each() {
//        log("${it.displayName} Temp: ${it.currentValue("temperature")}.", "TRACE")  

        currentState = it.currentState("temperature")

//        log("currentState.integerValue: ${currentState.integerValue}.", "TRACE")

        try {
            tempstdTemp += (averageTemp - currentState.integerValue) * (averageTemp - currentState.integerValue)
        } catch(e) {
            log("ERROR -- ${e}", "ERROR")
        }
    }

    try {
        tempstdTemp = tempstdTemp / temperatureSensors.size()
    } catch(e) {
        log("ERROR -- ${e}", "ERROR")
    }

    try {
        stdTemp = Math.sqrt(tempstdTemp)
    } catch(e) {
        log("ERROR -- ${e}", "ERROR")
    }

    if(setThermostat) {
        log("Evaluating thermostat rules...", "INFO")
        if(averageTemp > maxTemp) {
            log("Begin cooling to ${coolingSetpoint}.", "INFO")
            beginCooling(coolingSetpoint)
        } else if(averageTemp < minTemp) {
            log("Begin heating to ${heatingSetpoint}.", "INFO")
            beginHeating(heatingSetpoint)
        } else {
            log("Temperature is just right.", "INFO")
        }
    }

//modified by JH - use STDEV to determine fan activation
    if(controlFan){
        log("Evaluating fan rules...", "INFO")

        if(thermostat.thermostatMode == "auto" || thermostat.thermostatMode == "off") {

            if(stdTemp > fanActivate) {
                log("Turning on fan in order to reduce temp variation to: +/- ${fanDeactivate}.", "INFO")
                turnFanOn()
            } else {
                if(thermostat.thermostatFanMode == "on") {
                    turnFanAuto()
                    log("Turning fan to auto.", "INFO")
                }

                log("Temperature variation between rooms has been reduced.", "INFO")
            }

        } else {
            log("You are already in a running mode of ${thermostat.thermostatMode}, ignoring your fan control request.", "INFO")
        }
    }

    if(setVirtualTemp) {
        log("Updating ${settableSensor.label} to ${Math.round(averageTemp * 100) / 100}.", "INFO")
        settableSensor.setTemperature((Math.round(averageTemp * 100) / 100).toString())
    }

//added by JH - if capture to temp sensor is enabled, then store STDEV in temp sensor
    if(setSTDTemp) {
        log("Updating ${setSTDSensor.label} to ${Math.round(stdTemp * 100) / 100}.", "INFO")
        setSTDSensor.setTemperature((Math.round(stdTemp * 100) / 100).toString())
    }    
}

def beginCooling(val) {
    log("Setting coolingSetpoint to: ${val}.", "DEBUG")
    thermostat.setCoolingSetpoint(val)
}

def beginHeating(val) {
    log("Setting heatingSetpoint to: ${val}.", "DEBUG")
    thermostat.setHeatingSetpoint(val)
}

def turnFanOn() {
    thermostat.fanOn()
}

def turnFanAuto() {
    thermostat.fanAuto()
}

EDIT: Code clarification w/ comments

EDIT2: tracking nicely so far (1st graph is Average / 2nd graph is STDEV) http://imagebucket.net/s54rkgvg50f0/2016-08-05_17-18-29.png

ericvitale commented 8 years ago

How is it going? Your image is a dead link... http://imagebucket.net/s54rkgvg50f0/2016-08-05_17-18-29.png

johnny2678 commented 8 years ago

Oops, sorry, here's an updated one. I left it alone over the weekend to let it capture some data. Next step is to run the fan and see how low I can get the STDEV. That will help determine the lower threshold to turn off the fan after it's been triggered.

2016-08-08_08-39-58

ericvitale commented 8 years ago

where and how are you logging that data? I didn't see anything in the code.

johnny2678 commented 8 years ago

www.initialstate.com - create a free account and link it to smartthings