4 * Source: https://github.com/tslagle13/SmartThings/blob/master/Director-Series-Apps/Lighting-Director/Lighting%20Director.groovy
6 * Current Version: 2.9.4
11 * Version - 1.30.1 Modification by Michael Struck - Fixed syntax of help text and titles of scenarios, along with a new icon
12 * Version - 1.40.0 Modification by Michael Struck - Code optimization and added door contact sensor capability
13 * Version - 1.41.0 Modification by Michael Struck - Code optimization and added time restrictions to each scenario
14 * Version - 2.0 Tim Slagle - Moved to only have 4 slots. Code was to heavy and needed to be trimmed.
15 * Version - 2.1 Tim Slagle - Moved time interval inputs inline with STs design.
16 * Version - 2.2 Michael Struck - Added the ability to activate switches via the status locks and fixed some syntax issues
17 * Version - 2.5 Michael Struck - Changed the way the app unschedules re-triggered events
18 * Version - 2.5.1 Tim Slagle - Fixed Time Logic
19 * Version - 2.6 Michael Struck - Added the additional restriction of running triggers once per day and misc cleanup of code
20 * Version - 2.7 Michael Struck - Added feature that turns off triggering if the physical switch is pressed.
21 * Version - 2.81 Michael Struck - Fixed an issue with dimmers not stopping light action
22 * Version - 2.9 Michael Struck - Fixed issue where button presses outside of the time restrictions prevent the triggers from firing and code optimization
23 * Version - 2.9.1 Tim Slagle - Further enhanced time interval logic.
24 * Version - 2.9.2 Brandon Gordon - Added support for acceleration sensors.
25 * Version - 2.9.3 Brandon Gordon - Added mode change subscriptions.
26 * Version - 2.9.4 Michael Struck - Code Optimization when triggers are tripped
28 * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
30 * Copyright 2015 Tim Slagle and Michael Struck
32 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
33 * in compliance with the License. You may obtain a copy of the License at:
35 * http://www.apache.org/licenses/LICENSE-2.0
37 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
38 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
39 * for the specific language governing permissions and limitations under the License.
44 name: "Lighting Director",
45 namespace: "tslagle13",
46 author: "Tim Slagle & Michael Struck",
47 description: "Control up to 4 sets (scenarios) of lights based on motion, door contacts and illuminance levels.",
48 category: "Convenience",
49 iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector.png",
50 iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png",
51 iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png")
54 page(name: "timeIntervalInputA", title: "Only during a certain time", refreshAfterSelection:true) {
56 input "A_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true
57 input "A_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true
61 page name:"pageSetupScenarioA"
67 def pageProperties = [
74 return dynamicPage(pageProperties) {
75 section("Setup Menu") {
76 href "pageSetupScenarioA", title: getTitle(settings.ScenarioNameA), description: getDesc(settings.ScenarioNameA), state: greyOut(settings.ScenarioNameA)
78 section([title:"Options", mobileOnly:true]) {
79 label title:"Assign a name", required:false
84 // Show "pageSetupScenarioA" page
85 def pageSetupScenarioA() {
86 //input name: "A_switches", type: "capability.switch", title: "Control the following switches...", multiple: true, required: false
89 type: "capability.switch",
90 title: "Control the following switches...",
94 //input name: "A_dimmers", type: "capability.switchLevel", title: "Dim the following...", multiple: true, required: false
97 type: "capability.switchLevel",
98 title: "Dim the following...",
102 //input name: "A_motion", type: "capability.motionSensor", title: "Using these motion sensors...", multiple: true, required: false
105 type: "capability.motionSensor",
106 title: "Using these motion sensors...",
110 //input name: "A_acceleration", type: "capability.accelerationSensor", title: "Or using these acceleration sensors...", multiple: true, required: false
111 def inputAccelerationA = [
112 name: "A_acceleration",
113 type: "capability.accelerationSensor",
114 title: "Or using these acceleration sensors...",
118 //input name: "A_contact", type: "capability.contactSensor", title: "Or using these contact sensors...", multiple: true, required: false
119 def inputContactA = [
121 type: "capability.contactSensor",
122 title: "Or using these contact sensors...",
126 //input name: "A_triggerOnce", type: "bool", title: "Trigger only once per day...", defaultValue: false
127 def inputTriggerOnceA = [
128 name: "A_triggerOnce",
130 title: "Trigger only once per day...",
133 //input name: "A_switchDisable", type: "bool", title: "Stop triggering if physical switches/dimmers are turned off...", defaultValue: false
134 def inputSwitchDisableA = [
135 name: "A_switchDisable",
137 title: "Stop triggering if physical switches/dimmers are turned off...",
140 //input name: "A_lock", type: "capability.lock", title: "Or using these locks....", multiple: true, required: false
143 type: "capability.lock",
144 title: "Or using these locks...",
148 //input name: "A_mode", type: "mode", title: "Only during the following modes...", multiple: true, required: false
152 title: "Only during the following modes...",
156 //input name: "A_day", type: "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Only on certain days of the week...", multiple: true, required: false
160 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
161 title: "Only on certain days of the week...",
166 //input name: "A_level", type: "enum", options: [10,20,30,40,50,60,70,80,90,100], title: "Set dimmers to this level", multiple: false, required: false
170 options: [10,20,30,40,50,60,70,80,90,100],
171 title: "Set dimmers to this level",
176 //input name: "A_turnOnLux", type: "number", title: "Only run this scenario if lux is below...", multiple: false, required: false
177 def inputTurnOnLuxA = [
180 title: "Only run this scenario if lux is below...",
184 //input name: "A_luxSensors", type: "capability.illuminanceMeasurement", title: "On these lux sensors", multiple: false, required: false
185 def inputLuxSensorsA = [
186 name: "A_luxSensors",
187 type: "capability.illuminanceMeasurement",
188 title: "On these lux sensors",
192 //input name: "A_turnOff", type: "number", title: "Turn off this scenario after motion stops or doors close/lock (minutes)...", multiple: false, required: false
193 def inputTurnOffA = [
196 title: "Turn off this scenario after motion stops or doors close/lock (minutes)...",
200 //input name: "ScenarioNameA", type: "text", title: "Scenario Name", multiple: false, required: false
201 def inputScenarioNameA = [
202 name: "ScenarioNameA",
204 title: "Scenario Name",
209 def pageProperties = [
210 name: "pageSetupScenarioA",
213 return dynamicPage(pageProperties) {
214 section("Name your scenario") {
215 input inputScenarioNameA
218 section("Devices included in the scenario") {
220 input inputAccelerationA
227 section("Scenario settings") {
229 input inputTurnOnLuxA
230 input inputLuxSensorsA
234 section("Scenario restrictions") {
235 input inputTriggerOnceA
236 input inputSwitchDisableA
237 href "timeIntervalInputA", title: "Only during a certain time...", description: getTimeLabel(A_timeStart, A_timeEnd), state: greyedOutTime(A_timeStart, A_timeEnd), refreshAfterSelection:true
266 //subscribe(A_motion, "motion", onEventA)
267 subscribe(A_motion, "motion", onEventA)
271 subscribe(A_acceleration, "acceleration", onEventA)
275 subscribe(A_contact, "contact", onEventA)
279 subscribe(A_lock, "lock", onEventA)
282 if(A_switchDisable) {
283 subscribe(A_switches, "switch.off", onPressA)
284 subscribe(A_dimmers, "switch.off", onPressA)
291 if (/*(!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))*/true) { //Checks to make sure this scenario should be triggered more then once in a day
292 if ((!A_mode || A_mode.contains(location.mode)) && true/*getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)*/) { //checks to make sure we are not opperating outside of set restrictions.
293 if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ //checks to make sure illimunance is either not cared about or if the value is within the restrictions
294 def A_levelOn = A_level as Integer
296 //Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action.
297 if (getInputOk(A_motion, A_contact, A_lock, A_acceleration)) {
298 log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameA}'")
299 settings.A_dimmers?.setLevel(A_levelOn)
300 settings.A_switches?.on()
302 state.A_triggered = true
304 runOnce (getMidnight(), midNightReset)
307 if (state.A_timerStart){
308 unschedule(delayTurnOffA)
309 state.A_timerStart = false
313 //if none of the above paramenters meet the expectation of the app then turn off
316 if (settings.A_turnOff) {
317 runIn(A_turnOff * 60, "delayTurnOffA")
318 state.A_timerStart = true
321 settings.A_switches?.off()
322 settings.A_dimmers?.setLevel(0)
323 if (state.A_triggered) {
324 runOnce (getMidnight(), midNightReset)
331 log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction. Not running scenario.")
337 settings.A_switches?.off()
338 settings.A_dimmers?.setLevel(0)
339 state.A_timerStart = false
340 if (state.A_triggered) {
341 runOnce (getMidnight(), midNightReset)
346 //when physical switch is actuated disable the scenario
348 if ((!A_mode || A_mode.contains(location.mode)) && /*getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)*/true) { //checks to make sure we are not opperating outside of set restrictions.
349 if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){
350 if (/*(!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))*/true) {
352 state.A_triggered = true
353 unschedule(delayTurnOffA)
354 runOnce (getMidnight(), midNightReset)
355 log.debug "Physical switch in '${ScenarioNameA}' pressed. Triggers for this scenario disabled."
362 //resets once a day trigger at midnight so trigger can be ran again the next day.
363 def midNightReset() {
364 state.A_triggered = false
365 state.B_triggered = false
366 state.C_triggered = false
367 state.D_triggered = false
370 private def helpText() {
372 "Select motion sensors, acceleration sensors, contact sensors or locks to control a set of lights. " +
373 "Each scenario can control dimmers and switches but can also be " +
374 "restricted to modes or between certain times and turned off after " +
375 "motion stops, doors close or lock. Scenarios can also be limited to " +
376 "running once or to stop running if the physical switches are turned off."
380 //should scenario be marked complete or not
381 def greyOut(scenario){
389 //should i mark the time restriction green or grey
390 def greyedOutTime(start, end){
399 def getTitle(scenario) {
407 //recursively applies label to each scenario depending on if the scenario has deatils inside it or not
408 def getDesc(scenario) {
409 def desc = "Tap to create a scenario"
411 desc = "Tap to edit scenario"
418 def midnightToday = timeToday("23:59", location.timeZone)
422 //used to recursively check device states when methods are triggered
423 private getInputOk(motion, contact, lock, acceleration) {
425 def motionDetected = false
426 def accelerationDetected = false
427 def contactDetected = false
428 def unlockDetected = false
432 if (motion.latestValue("motion") == "active") {
433 motionDetected = true
438 if (acceleration.latestValue("acceleration") == "active") {
439 accelerationDetected = true
444 if (contact.latestValue("contact").contains("open")) {
445 contactDetected = true
450 if (lock.latestValue("lock").contains("unlocked")) {
451 unlockDetected = true
455 result = motionDetected || contactDetected || unlockDetected || accelerationDetected
460 private getTimeOk(starting, ending) {
462 if (starting && ending) {
464 def start = timeToday(starting).time
465 def stop = timeToday(ending).time
466 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
470 result = currTime >= start
473 result = currTime <= stop
476 log.trace "timeOk = $result"
480 def getTimeLabel(start, end){
481 def timeLabel = "Tap to set"
484 timeLabel = "Between" + " " + hhmm(start) + " " + "and" + " " + hhmm(end)
487 timeLabel = "Start at" + " " + hhmm(start)
490 timeLabel = "End at" + hhmm(end)
495 private hhmm(time, fmt = "h:mm a")
497 def t = timeToday(time, location.timeZone)
498 def f = new java.text.SimpleDateFormat(fmt)
499 f.setTimeZone(location.timeZone ?: timeZone(time))
503 private getDayOk(dayList) {
506 def df = new java.text.SimpleDateFormat("EEEE")
507 if (location.timeZone) {
508 df.setTimeZone(location.timeZone)
511 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
513 def day = df.format(new Date())
514 result = dayList.contains(day)