2 * Vacation Lighting Director
4 * Version 2.5 - Moved scheduling over to Cron and added time as a trigger.
5 * Cleaned up formatting and some typos.
7 * Made people option optional
8 * Added sttement to unschedule on mode change if people option is not selected
10 * Version 2.4 - Added information paragraphs
12 * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
14 * Copyright 2016 Tim Slagle
16 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
17 * in compliance with the License. You may obtain a copy of the License at:
19 * http://www.apache.org/licenses/LICENSE-2.0
21 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
22 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
23 * for the specific language governing permissions and limitations under the License.
28 // Automatically generated. Make future change here.
30 name: "Vacation Lighting Director",
31 namespace: "tslagle13",
33 category: "Safety & Security",
34 description: "Randomly turn on/off lights to simulate the appearance of a occupied home while you are away.",
35 iconUrl: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png",
36 iconX2Url: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png"
40 page name: "timeIntervalInput"
49 def pageProperties = [
57 return dynamicPage(pageProperties) {
59 paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
60 "Please use each of the the sections below to setup the different preferences to your liking. "
62 section("Setup Menu") {
63 href "Setup", title: "Setup", description: "", state:greyedOut()
64 href "Settings1", title: "Settings1", description: "", state: greyedOutSettings()
66 section([title:"Options", mobileOnly:true]) {
67 label title:"Assign a name", required:false
84 type: "capability.switch",
90 def frequency_minutes = [
91 name: "frequency_minutes",
97 def number_of_active_lights = [
98 name: "number_of_active_lights",
100 title: "Number of active lights",
104 def pageName = "Setup"
106 def pageProperties = [
109 nextPage: "pageSetup"
112 return dynamicPage(pageProperties) {
115 paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
116 "you are away. All of these settings are required in order for the simulator to run correctly."
118 section("Simulator Triggers") {
120 href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
122 section("Light switches to turn on/off") {
125 section("How often to cycle the lights") {
126 input frequency_minutes
128 section("Number of active lights at any given time") {
129 input number_of_active_lights
138 def falseAlarmThreshold = [
139 name: "falseAlarmThreshold",
141 title: "Default is 2 minutes",
147 title: "Only on certain days of the week",
150 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
153 def pageName = "Settings1"
155 def pageProperties = [
158 nextPage: "pageSetup"
163 type: "capability.presenceSensor",
164 title: "If these people are home do not change light status",
169 return dynamicPage(pageProperties) {
172 paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " +
173 "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
175 section("Delay to start simulator") {
176 input falseAlarmThreshold
179 paragraph "Not using this setting may cause some lights to remain on when you arrive home"
182 section("More options") {
188 def timeIntervalInput() {
189 dynamicPage(name: "timeIntervalInput") {
191 input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
192 if (startTimeType in ["sunrise","sunset"]) {
193 input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
196 input "starting", "time", title: "Start time", required: false
200 input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
201 if (endTimeType in ["sunrise","sunset"]) {
202 input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
205 input "ending", "time", title: "End time", required: false
224 if (newMode != null) {
225 subscribe(location, modeChangeHandler)
227 if (starting != null) {
228 schedule(starting, modeChangeHandler)
230 log.debug "Installed with settings: ${settings}"
233 def modeChangeHandler(evt) {
234 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
235 runIn(delay, scheduleCheck)
239 //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
240 def scheduleCheck(evt) {
243 // turn off all the switches
246 // grab a random switch
247 def random = new Random()
248 def inactive_switches = switches
249 for (int i = 0 ; i < number_of_active_lights ; i++) {
250 // if there are no inactive switches to turn on then let's break
251 if (inactive_switches.size() == 0){
255 // grab a random switch and turn it on
256 def random_int = random.nextInt(inactive_switches.size())
257 inactive_switches[random_int].on()
259 // then remove that switch from the pool off switches that can be turned on
260 inactive_switches.remove(random_int)
263 // re-run again when the frequency demands it
264 schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
266 //Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
268 log.debug("mode OK. Running again")
271 //if none is ok turn off frequency check and turn off lights.
274 //don't turn off lights if anyone is home
276 log.debug("Stopping Check for Light")
280 log.debug("Stopping Check for Light and turning off all lights")
292 //below is used to check restrictions
294 modeOk && daysOk && timeOk && homeIsEmpty
298 private getModeOk() {
299 def result = !newMode || newMode.contains(location.mode)
300 log.trace "modeOk = $result"
304 private getDaysOk() {
307 def df = new java.text.SimpleDateFormat("EEEE")
308 if (location.timeZone) {
309 df.setTimeZone(location.timeZone)
312 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
314 def day = df.format(new Date())
315 result = days.contains(day)
317 log.trace "daysOk = $result"
321 private getHomeIsEmpty() {
324 if(people?.findAll { it?.currentPresence == "present" }) {
328 log.debug("homeIsEmpty: ${result}")
333 private getSomeoneIsHome() {
336 if(people?.findAll { it?.currentPresence == "present" }) {
340 log.debug("anyoneIsHome: ${result}")
345 private getTimeOk() {
347 def start = timeWindowStart()
348 def stop = timeWindowStop()
349 if (start && stop && location.timeZone) {
350 result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
352 log.trace "timeOk = $result"
356 private timeWindowStart() {
358 if (startTimeType == "sunrise") {
359 result = location.currentState("sunriseTime")?.dateValue
360 if (result && startTimeOffset) {
361 result = new Date(result.time + Math.round(startTimeOffset * 60000))
364 else if (startTimeType == "sunset") {
365 result = location.currentState("sunsetTime")?.dateValue
366 if (result && startTimeOffset) {
367 result = new Date(result.time + Math.round(startTimeOffset * 60000))
370 else if (starting && location.timeZone) {
371 result = timeToday(starting, location.timeZone)
373 log.trace "timeWindowStart = ${result}"
377 private timeWindowStop() {
379 if (endTimeType == "sunrise") {
380 result = location.currentState("sunriseTime")?.dateValue
381 if (result && endTimeOffset) {
382 result = new Date(result.time + Math.round(endTimeOffset * 60000))
385 else if (endTimeType == "sunset") {
386 result = location.currentState("sunsetTime")?.dateValue
387 if (result && endTimeOffset) {
388 result = new Date(result.time + Math.round(endTimeOffset * 60000))
391 else if (ending && location.timeZone) {
392 result = timeToday(ending, location.timeZone)
394 log.trace "timeWindowStop = ${result}"
398 private hhmm(time, fmt = "h:mm a")
400 def t = timeToday(time, location.timeZone)
401 def f = new java.text.SimpleDateFormat(fmt)
402 f.setTimeZone(location.timeZone ?: timeZone(time))
406 private timeIntervalLabel() {
408 switch (startTimeType) {
411 start += hhmm(starting)
416 start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
417 if (startTimeOffset) {
418 start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
424 switch (endTimeType) {
427 finish += hhmm(ending)
432 finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
434 finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
438 start && finish ? "${start} to ${finish}" : ""
441 //sets complete/not complete for the setup section on the main dynamic page
450 //sets complete/not complete for the settings section on the main dynamic page
451 def greyedOutSettings(){
453 if (people || days || falseAlarmThreshold ) {