1 /** Name: SmartPresence
2 * Author: George Sudarkoff
3 * Change mode based on presense and current mode.
8 namespace: "sudarkoff.com",
9 author: "George Sudarkoff",
10 description: "Change mode (by invoking various \"Hello, Home\" phrases) based on presense and current mode.",
11 category: "Mode Magic",
12 iconUrl: "https://raw.githubusercontent.com/sudarkoff/smarttings/master/SmartPresence.png",
13 iconX2Url: "https://raw.githubusercontent.com/sudarkoff/smarttings/master/SmartPresence@2x.png"
17 page(name: "selectPhrases")
19 page( name:"Settings", title:"Settings", uninstall:true, install:true ) {
22 input "starting", "time", title: "Starting", required: false
23 input "ending", "time", title: "Ending", required: false
26 section("False alarm threshold (defaults to 10 min)") {
27 input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
30 section("Zip code (for sunrise/sunset)") {
31 input "zip", "decimal", required: true
34 section("Notifications") {
35 input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
38 section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
39 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
40 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
41 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
47 def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.awayDay)
48 dynamicPage(name: "selectPhrases", title: "Configure", nextPage:"Settings", uninstall: true) {
49 section("All of these sensors") {
50 input "people", "capability.presenceSensor", title: "Monitor All of These Presences", required: true, multiple: true, refreshAfterSelection:true
53 def phrases = location.helloHome?.getPhrases()*.label
56 section("Run This Phrase When...") {
58 input "sunriseAway", "enum", title: "It's Sunrise and Everybody's Away", required: true, options: phrases, refreshAfterSelection:true
59 input "sunsetAway", "enum", title: "It's Sunset and Everybody's Away", required: true, options: phrases, refreshAfterSelection:true
60 input "sunriseHome", "enum", title: "It's Sunrise and Somebody's Home", required: true, options: phrases, refreshAfterSelection:true
61 input "sunsetHome", "enum", title: "It's Sunset and Somebody's Home", required: true, options: phrases, refreshAfterSelection:true
62 input "awayDay", "enum", title: "Last Person Leaves and It's Daytime", required: true, options: phrases, refreshAfterSelection:true
63 input "awayNight", "enum", title: "Last Person Leaves and It's Nighttime", required: true, options: phrases, refreshAfterSelection:true
64 input "homeDay", "enum", title: "Somebody's Back and It's Daytime", required: true, options: phrases, refreshAfterSelection:true
65 input "homeNight", "enum", title: "Somebody's Back and It's Nighttime", required: true, options: phrases, refreshAfterSelection:true
85 subscribe(people, "presence", presence)
95 schedule("0 0/5 * 1/1 * ? *", checkSun)
99 // TODO: Use location information if zip is not provided
100 def zip = settings.zip as String
101 def sunInfo = getSunriseAndSunset(zipCode: zip)
104 if(sunInfo.sunrise.time > current ||
105 sunInfo.sunset.time < current) {
106 state.sunMode = "sunset"
109 state.sunMode = "sunrise"
112 log.info("Sunset: ${sunInfo.sunset.time}")
113 log.info("Sunrise: ${sunInfo.sunrise.time}")
114 log.info("Current: ${current}")
115 log.info("sunMode: ${state.sunMode}")
117 if(current < sunInfo.sunrise.time) {
118 runIn(((sunInfo.sunrise.time - current) / 1000).toInteger(), setSunrise)
121 if(current < sunInfo.sunset.time) {
122 runIn(((sunInfo.sunset.time - current) / 1000).toInteger(), setSunset)
127 state.sunMode = "sunrise";
128 changeSunMode(newMode)
132 state.sunMode = "sunset";
133 changeSunMode(newMode)
137 def changeSunMode(newMode) {
139 if(everyoneIsAway() && (state.sunMode = "sunrise")) {
140 log.info("Sunrise but nobody's home, switching to Away mode.")
141 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
142 runIn(delay, "setAway")
145 if(everyoneIsAway() && (state.sunMode = "sunset")) {
146 log.info("Sunset and nobody's home, switching to Away mode")
147 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
148 runIn(delay, "setAway")
152 log.info("Somebody's home, switching to Home mode")
160 if(evt.value == "not present") {
161 log.debug("Checking if everyone is away")
163 if(everyoneIsAway()) {
164 log.info("Everybody's gone, running Away sequence.")
165 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
166 runIn(delay, "setAway")
169 def lastTime = state[evt.deviceId]
170 if (lastTime == null || now() - lastTime >= 1 * 60000) {
171 log.info("Somebody's back, running Home sequence")
174 state[evt.deviceId] = now()
180 if(everyoneIsAway()) {
181 if(state.sunMode == "sunset") {
182 def message = "SmartMode says \"${awayNight}\"."
185 location.helloHome.execute(settings.awayNight)
186 } else if(state.sunMode == "sunrise") {
187 def message = "SmartMode says \"${awayDay}\"."
190 location.helloHome.execute(settings.awayDay)
192 log.debug("Mode is the same, not evaluating.")
195 log.info("Somebody returned home before we switched to '${newAwayMode}'")
200 log.info("Setting Home Mode!")
202 if(state.sunMode == "sunset") {
203 def message = "SmartMode says \"${homeNight}\"."
205 location.helloHome.execute(settings.homeNight)
207 sendSms(phone1, message)
210 if(state.sunMode == "sunrise"){
211 def message = "SmartMode says \"${homeDay}\"."
213 location.helloHome.execute(settings.homeDay)
215 sendSms(phone1, message)
220 private everyoneIsAway() {
223 if(people.findAll { it?.currentPresence == "present" }) {
227 log.debug("everyoneIsAway: ${result}")
232 private anyoneIsHome() {
235 if(people.findAll { it?.currentPresence == "present" }) {
239 log.debug("anyoneIsHome: ${result}")
245 if(sendPushMessage != "No") {
246 log.debug("Sending push message")
256 modeOk && daysOk && timeOk
259 private getModeOk() {
260 def result = !modes || modes.contains(location.mode)
261 log.trace "modeOk = $result"
265 private getDaysOk() {
268 def df = new java.text.SimpleDateFormat("EEEE")
269 if (location.timeZone) {
270 df.setTimeZone(location.timeZone)
273 df.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))
275 def day = df.format(new Date())
276 result = days.contains(day)
278 log.trace "daysOk = $result"
282 private getTimeOk() {
284 if (starting && ending) {
286 def start = timeToday(starting).time
287 def stop = timeToday(ending).time
288 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
290 log.trace "timeOk = $result"
294 private hhmm(time, fmt = "h:mm a")
296 def t = timeToday(time, location.timeZone)
297 def f = new java.text.SimpleDateFormat(fmt)
298 f.setTimeZone(location.timeZone ?: timeZone(time))
302 private getTimeIntervalLabel()
304 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
307 private hideOptionsSection() {
308 (starting || ending || days || modes) ? false : true