Update keep-me-cozy-ii.groovy
[smartapps.git] / official / virtual-thermostat.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  *  in compliance with the License. You may obtain a copy of the License at:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11  *  for the specific language governing permissions and limitations under the License.
12  *
13  *  Virtual Thermostat
14  *
15  *  Author: SmartThings
16  */
17 definition(
18     name: "Virtual Thermostat",
19     namespace: "smartthings",
20     author: "SmartThings",
21     description: "Control a space heater or window air conditioner in conjunction with any temperature sensor, like a SmartSense Multi.",
22     category: "Green Living",
23     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
24     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
25 )
26
27 preferences {
28         section("Choose a temperature sensor... "){
29                 input "sensor", "capability.temperatureMeasurement", title: "Sensor"
30         }
31         section("Select the heater or air conditioner outlet(s)... "){
32                 input "outlets", "capability.switch", title: "Outlets", multiple: true
33         }
34         section("Set the desired temperature..."){
35                 input "setpoint", "decimal", title: "Set Temp"
36         }
37         section("When there's been movement from (optional, leave blank to not require motion)..."){
38                 input "motion", "capability.motionSensor", title: "Motion", required: false
39         }
40         section("Within this number of minutes..."){
41                 input "minutes", "number", title: "Minutes", required: false
42         }
43         section("But never go below (or above if A/C) this value with or without motion..."){
44                 input "emergencySetpoint", "decimal", title: "Emer Temp", required: false
45         }
46         section("Select 'heat' for a heater and 'cool' for an air conditioner..."){
47                 input "mode", "enum", title: "Heating or cooling?", options: ["heat","cool"]
48         }
49 }
50
51 def installed()
52 {
53         subscribe(sensor, "temperature", temperatureHandler)
54         if (motion) {
55                 subscribe(motion, "motion", motionHandler)
56         }
57 }
58
59 def updated()
60 {
61         unsubscribe()
62         subscribe(sensor, "temperature", temperatureHandler)
63         if (motion) {
64                 subscribe(motion, "motion", motionHandler)
65         }
66 }
67
68 def temperatureHandler(evt)
69 {
70         def isActive = hasBeenRecentMotion()
71         if (isActive || emergencySetpoint) {
72                 evaluate(evt.doubleValue, isActive ? setpoint : emergencySetpoint)
73         }
74         else {
75                 outlets.off()
76         }
77 }
78
79 def motionHandler(evt)
80 {
81         if (evt.value == "active") {
82                 def lastTemp = sensor.currentTemperature
83                 if (lastTemp != null) {
84                         evaluate(lastTemp, setpoint)
85                 }
86         } else if (evt.value == "inactive") {
87                 def isActive = hasBeenRecentMotion()
88                 log.debug "INACTIVE($isActive)"
89                 if (isActive || emergencySetpoint) {
90                         def lastTemp = sensor.currentTemperature
91                         if (lastTemp != null) {
92                                 evaluate(lastTemp, isActive ? setpoint : emergencySetpoint)
93                         }
94                 }
95                 else {
96                         outlets.off()
97                 }
98         }
99 }
100
101 private evaluate(currentTemp, desiredTemp)
102 {
103         log.debug "EVALUATE($currentTemp, $desiredTemp)"
104         def threshold = 1.0
105         if (mode == "cool") {
106                 // air conditioner
107                 if (currentTemp - desiredTemp >= threshold) {
108                         outlets.on()
109                 }
110                 else if (desiredTemp - currentTemp >= threshold) {
111                         outlets.off()
112                 }
113         }
114         else {
115                 // heater
116                 if (desiredTemp - currentTemp >= threshold) {
117                         outlets.on()
118                 }
119                 else if (currentTemp - desiredTemp >= threshold) {
120                         outlets.off()
121                 }
122         }
123 }
124
125 private hasBeenRecentMotion()
126 {
127         def isActive = false
128         if (motion && minutes) {
129                 def deltaMinutes = minutes as Long
130                 if (deltaMinutes) {
131                         def motionEvents = motion.eventsSince(new Date(now() - (60000 * deltaMinutes)))
132                         log.trace "Found ${motionEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
133                         if (motionEvents.find { it.value == "active" }) {
134                                 isActive = true
135                         }
136                 }
137         }
138         else {
139                 isActive = true
140         }
141         isActive
142 }
143