Checking in all the SmartThings apps; both official and third-party.
[smartapps.git] / official / vacation-lighting-director.groovy
diff --git a/official/vacation-lighting-director.groovy b/official/vacation-lighting-director.groovy
new file mode 100755 (executable)
index 0000000..c3187b7
--- /dev/null
@@ -0,0 +1,458 @@
+/**
+ *  Vacation Lighting Director
+ * 
+ * Version  2.5 - Moved scheduling over to Cron and added time as a trigger. 
+ *                               Cleaned up formatting and some typos.
+ *                Updated license.
+ *                Made people option optional
+ *                               Added sttement to unschedule on mode change if people option is not selected
+ *
+ * Version  2.4 - Added information paragraphs
+ * 
+ *  Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
+ *
+ *  Copyright 2016 Tim Slagle
+ *
+ *  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.
+ *
+ */
+
+
+// Automatically generated. Make future change here.
+definition(
+    name: "Vacation Lighting Director",
+    namespace: "tslagle13",
+    author: "Tim Slagle",
+    category: "Safety & Security",
+    description: "Randomly turn on/off lights to simulate the appearance of a occupied home while you are away.",
+    iconUrl: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png",
+    iconX2Url: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png"
+)
+
+preferences {
+    page name:"pageSetup"
+    page name:"Setup"
+    page name:"Settings"
+    page name: "timeIntervalInput"
+
+}
+
+// Show setup page
+def pageSetup() {
+
+    def pageProperties = [
+        name:       "pageSetup",
+        title:      "Status",
+        nextPage:   null,
+        install:    true,
+        uninstall:  true
+    ]
+
+       return dynamicPage(pageProperties) {
+       section(""){
+               paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
+                       "Please use each of the the sections below to setup the different preferences to your liking. " 
+        }
+        section("Setup Menu") {
+            href "Setup", title: "Setup", description: "", state:greyedOut()
+            href "Settings", title: "Settings", description: "", state: greyedOutSettings()
+            }
+        section([title:"Options", mobileOnly:true]) {
+            label title:"Assign a name", required:false
+        }
+    }
+}
+
+// Show "Setup" page
+def Setup() {
+
+    def newMode = [
+        name:          "newMode",
+        type:          "mode",
+        title:         "Modes",
+        multiple:      true,
+        required:      true
+    ]
+    def switches = [
+        name:          "switches",
+        type:          "capability.switch",
+        title:         "Switches",
+        multiple:      true,
+        required:      true
+    ]
+    
+    def frequency_minutes = [
+        name:          "frequency_minutes",
+        type:          "number",
+        title:         "Minutes?",
+        required:      true
+    ]
+    
+    def number_of_active_lights = [
+        name:          "number_of_active_lights",
+        type:          "number",
+        title:         "Number of active lights",
+        required:      true,
+    ]
+    
+    def pageName = "Setup"
+    
+    def pageProperties = [
+        name:       "Setup",
+        title:      "Setup",
+        nextPage:   "pageSetup"
+    ]
+
+    return dynamicPage(pageProperties) {
+
+               section(""){            
+                    paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
+                    "you are away.  All of these settings are required in order for the simulator to run correctly."
+        }
+        section("Simulator Triggers") {
+                    input newMode  
+                    href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
+        }
+        section("Light switches to turn on/off") {
+                    input switches           
+        }
+        section("How often to cycle the lights") {
+                    input frequency_minutes            
+        }
+        section("Number of active lights at any given time") {
+                    input number_of_active_lights           
+        }    
+    }
+    
+}
+
+// Show "Setup" page
+def Settings() {
+
+    def falseAlarmThreshold = [
+        name:       "falseAlarmThreshold",
+        type:       "decimal",
+        title:      "Default is 2 minutes",
+        required:      false
+    ]
+    def days = [
+        name:       "days",
+        type:       "enum",
+        title:      "Only on certain days of the week",
+        multiple:   true,
+        required:   false,
+        options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
+    ]
+    
+    def pageName = "Settings"
+    
+    def pageProperties = [
+        name:       "Settings",
+        title:      "Settings",
+        nextPage:   "pageSetup"
+    ]
+    
+    def people = [
+        name:       "people",
+        type:       "capability.presenceSensor",
+        title:      "If these people are home do not change light status",
+        required:      false,
+        multiple:      true
+    ]
+
+    return dynamicPage(pageProperties) {
+
+               section(""){              
+                    paragraph "In this section you can restrict how your simulator runs.  For instance you can restrict on which days it will run " +
+                    "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."
+        }
+        section("Delay to start simulator") {
+                    input falseAlarmThreshold
+        }
+        section("People") {
+                               paragraph "Not using this setting may cause some lights to remain on when you arrive home"
+                    input people            
+        }
+        section("More options") {
+                    input days
+        } 
+    }   
+}
+
+def timeIntervalInput() {
+       dynamicPage(name: "timeIntervalInput") {
+               section {
+                       input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
+                       if (startTimeType in ["sunrise","sunset"]) {
+                               input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
+                       }
+                       else {
+                               input "starting", "time", title: "Start time", required: false
+                       }
+               }
+               section {
+                       input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
+                       if (endTimeType in ["sunrise","sunset"]) {
+                               input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
+                       }
+                       else {
+                               input "ending", "time", title: "End time", required: false
+                       }
+               }
+       }
+}
+
+
+def installed() {
+initialize()
+}
+
+def updated() {
+  unsubscribe();
+  unschedule();
+  initialize()
+}
+
+def initialize(){
+
+       if (newMode != null) {
+               subscribe(location, modeChangeHandler)
+    }
+    if (starting != null) {
+       schedule(starting, modeChangeHandler)
+    }
+    log.debug "Installed with settings: ${settings}"
+}
+
+def modeChangeHandler(evt) {
+               def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60  
+       runIn(delay, scheduleCheck)
+}
+
+
+//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
+def scheduleCheck(evt) {
+    if(allOk){
+        log.debug("Running")
+        // turn off all the switches
+        switches.off()
+        
+        // grab a random switch
+        def random = new Random()
+        def inactive_switches = switches
+        for (int i = 0 ; i < number_of_active_lights ; i++) {
+            // if there are no inactive switches to turn on then let's break
+            if (inactive_switches.size() == 0){
+                break
+            }
+            
+            // grab a random switch and turn it on
+            def random_int = random.nextInt(inactive_switches.size())
+            inactive_switches[random_int].on()
+            
+            // then remove that switch from the pool off switches that can be turned on
+            inactive_switches.remove(random_int)
+        }
+        
+        // re-run again when the frequency demands it
+        schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
+    }
+    //Check to see if mode is ok but not time/day.  If mode is still ok, check again after frequency period.
+    else if (modeOk) {
+        log.debug("mode OK.  Running again")
+        switches.off()
+    }
+    //if none is ok turn off frequency check and turn off lights.
+    else {
+       if(people){
+               //don't turn off lights if anyone is home
+               if(someoneIsHome){
+                   log.debug("Stopping Check for Light")
+                   unschedule()
+               }
+               else{
+                   log.debug("Stopping Check for Light and turning off all lights")
+                   switches.off()
+                   unschedule()
+               }
+       }
+        else if (!modeOk) {
+               unschedule()
+        }
+    }
+}      
+
+
+//below is used to check restrictions
+private getAllOk() {
+       modeOk && daysOk && timeOk && homeIsEmpty
+}
+
+
+private getModeOk() {
+       def result = !newMode || newMode.contains(location.mode)
+       log.trace "modeOk = $result"
+       result
+}
+
+private getDaysOk() {
+       def result = true
+       if (days) {
+               def df = new java.text.SimpleDateFormat("EEEE")
+               if (location.timeZone) {
+                       df.setTimeZone(location.timeZone)
+               }
+               else {
+                       df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+               }
+               def day = df.format(new Date())
+               result = days.contains(day)
+       }
+       log.trace "daysOk = $result"
+       result
+}
+
+private getHomeIsEmpty() {
+  def result = true
+
+  if(people?.findAll { it?.currentPresence == "present" }) {
+    result = false
+  }
+
+  log.debug("homeIsEmpty: ${result}")
+
+  return result
+}
+
+private getSomeoneIsHome() {
+  def result = false
+
+  if(people?.findAll { it?.currentPresence == "present" }) {
+    result = true
+  }
+
+  log.debug("anyoneIsHome: ${result}")
+
+  return result
+}
+
+private getTimeOk() {
+       def result = true
+       def start = timeWindowStart()
+       def stop = timeWindowStop()
+       if (start && stop && location.timeZone) {
+               result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
+       }
+       log.trace "timeOk = $result"
+       result
+}
+
+private timeWindowStart() {
+       def result = null
+       if (startTimeType == "sunrise") {
+               result = location.currentState("sunriseTime")?.dateValue
+               if (result && startTimeOffset) {
+                       result = new Date(result.time + Math.round(startTimeOffset * 60000))
+               }
+       }
+       else if (startTimeType == "sunset") {
+               result = location.currentState("sunsetTime")?.dateValue
+               if (result && startTimeOffset) {
+                       result = new Date(result.time + Math.round(startTimeOffset * 60000))
+               }
+       }
+       else if (starting && location.timeZone) {
+               result = timeToday(starting, location.timeZone)
+       }
+       log.trace "timeWindowStart = ${result}"
+       result
+}
+
+private timeWindowStop() {
+       def result = null
+       if (endTimeType == "sunrise") {
+               result = location.currentState("sunriseTime")?.dateValue
+               if (result && endTimeOffset) {
+                       result = new Date(result.time + Math.round(endTimeOffset * 60000))
+               }
+       }
+       else if (endTimeType == "sunset") {
+               result = location.currentState("sunsetTime")?.dateValue
+               if (result && endTimeOffset) {
+                       result = new Date(result.time + Math.round(endTimeOffset * 60000))
+               }
+       }
+       else if (ending && location.timeZone) {
+               result = timeToday(ending, location.timeZone)
+       }
+       log.trace "timeWindowStop = ${result}"
+       result
+}
+
+private hhmm(time, fmt = "h:mm a")
+{
+       def t = timeToday(time, location.timeZone)
+       def f = new java.text.SimpleDateFormat(fmt)
+       f.setTimeZone(location.timeZone ?: timeZone(time))
+       f.format(t)
+}
+
+private timeIntervalLabel() {
+       def start = ""
+       switch (startTimeType) {
+               case "time":
+                       if (ending) {
+               start += hhmm(starting)
+            }
+                       break
+               case "sunrise":
+               case "sunset":
+               start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
+                       if (startTimeOffset) {
+                               start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
+                       }
+                       break
+       }
+
+    def finish = ""
+       switch (endTimeType) {
+               case "time":
+                       if (ending) {
+               finish += hhmm(ending)
+            }
+                       break
+               case "sunrise":
+               case "sunset":
+               finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
+                       if (endTimeOffset) {
+                               finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
+                       }
+                       break
+       }
+       start && finish ? "${start} to ${finish}" : ""
+}
+
+//sets complete/not complete for the setup section on the main dynamic page
+def greyedOut(){
+       def result = ""
+    if (switches) {
+       result = "complete"     
+    }
+    result
+}
+
+//sets complete/not complete for the settings section on the main dynamic page
+def greyedOutSettings(){
+       def result = ""
+    if (people || days || falseAlarmThreshold ) {
+       result = "complete"     
+    }
+    result
+}