-
Notifications
You must be signed in to change notification settings - Fork 16
/
smart-porch-light.groovy
705 lines (563 loc) · 19.2 KB
/
smart-porch-light.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
/**
* Porch Light
*
* Copyright 2015 Joseph DiBenedetto
*
* 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.
*
*/
import java.text.SimpleDateFormat
import groovy.time.TimeCategory
definition(
name: "Smart Porch Light",
namespace: "jdiben",
author: "Joseph DiBenedetto",
description: "Turn on your porch light, or any other dimmable light, dimmed to a level you set at sunset and increase to full brightness when someone arrives or some other action is triggered. After a number of minutes the light will dim back to its original level. Optionally, set the light to turn off at a specified time while still turning on when someone arrives. This app runs from sunset to sunrise.\n\nSelect as many lights as you like. Additional triggers include motion detection, door knock, door open and app button. A door bell trigger will also be added in the future.",
category: "Safety & Security",
iconUrl: "http://apps.shiftedpixel.com/porchlight/porchlight.png",
iconX2Url: "http://apps.shiftedpixel.com/porchlight/[email protected]",
iconX3Url: "http://apps.shiftedpixel.com/porchlight/[email protected]"
)
preferences {
page(name: "mainSettingsPage")
page(name: "scheduleSettingsPage")
}
def mainSettingsPage() {
dynamicPage(
name: "mainSettingsPage",
title: "",
install: true,
uninstall: true
) {
//Lights/switches to use. Only allow dimmable lights
section("Control these switches") {
input (
name: "switches",
type: "capability.switchLevel",
title: "Switches?",
required: true,
multiple: true
)
}
//Presence Sensors (including phones)
section("When these people arrive", hideable:true, hidden:(presence == null) ? true : false) {
input (
name: "presence",
type: "capability.presenceSensor",
title: "Who?",
required: false,
multiple: true
)
input (
name: "brightnessLevelPresence",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 100
)
}
//Motion sensors
section("When motion is detected", hideable:true, hidden:(motion == null) ? true : false) {
input (
name: "motion",
type: "capability.motionSensor",
title: "Which?",
required: false,
multiple: true
)
input (
name: "brightnessLevelMotion",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 100
)
}
//Contact sensors
section("When these doors are opened", hideable:true, hidden:(contact == null) ? true : false) {
input (
name: "contact",
type: "capability.contactSensor",
title: "Which?",
required: false,
multiple: true
)
input (
name: "brightnessLevelContact",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 100
)
}
//Vibration sensors to detect a knock
section("When someone knocks on these doors", hideable:true, hidden:(acceleration == null) ? true : false) {
input (
name: "acceleration",
type: "capability.accelerationSensor",
title: "Which?",
required: false,
multiple: true
)
input (
name: "brightnessLevelAcceleration",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 100
)
}
//Enable a button overlay on the app icon to trigger lights
section("When the app button is tapped", hideable:true, hidden:(appButton != true) ? true : false) {
input (
name: "appButton",
type: "bool",
title: "Tap to brighten lights?",
defaultValue: false,
required: false
)
input (
name: "brightnessLevelTap",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 100
)
}
//Minutes after event is detected before lights are set to their standby levels
section("Dim after") {
paragraph "The number of minutes after an event is triggered before the lights are dimmed."
input (
name: "autoOffMinutes",
type: "number",
title: "Minutes (0 - 30)",
required: false,
defaultValue: 5
)
}
section("Standby Light Brightness") {
paragraph "The brightness level that the lights will be set to at sunset and whenever an event times out."
input (
name: "brightnessLevelDefault",
type: "number",
title: "Brightness Level (1-100)?",
required: false,
defaultValue: 10
)
paragraph "If the standby brightness is changed manually, remember that level and override the standby level above until the next day. Due to the delay caused by SmartThings device polling, this may not always work as expected."
input (
name: "rememberLevel",
type: "bool",
title: "Remember changes",
defaultValue: true
)
}
//Open the scheduling page
section("Schedule") {
href(
title: "Active from",
name: "toScheduleSettingsPage",
page: "scheduleSettingsPage",
description: readableSchedule(), //Display a more readable schedule description
state: "complete"
)
}
//Enable certain events to output to hello home
section("Use Notifications") {
input (
name: "useHelloHome",
type: "bool",
title: "Show events in Notifications?",
defaultValue: true
)
}
//Specify a display name for this app (optional)
section("Assign a Name") {
label(
name: "appName",
title: "App Name (optional)",
required: false,
multiple: false
)
}
}
}
def scheduleSettingsPage() {
dynamicPage(
name: "scheduleSettingsPage",
install: false,
uninstall: false,
nextPage: "mainSettingsPage"
) {
section("Schedule") {
paragraph "By default, the app runs from sunset to sunrise. You can offset both sunset and sunrise by up to +/- 2 hours"
input (
name: "sunsetOffset",
type: "enum",
title: "Sunset Offset in minutes?",
options: ['-120', '-105', '-90', '-75', '-60', '-45', '-30', '-15', '0', '15', '30', '45', '60', '75', '90', '105', '120'],
defaultValue: "0"
)
input (
name: "sunriseOffset",
type: "enum",
title: "Sunrise Offset in minutes?",
options: ['-120', '-105', '-90', '-75', '-60', '-45', '-30', '-15', '0', '15', '30', '45', '60', '75', '90', '105', '120'],
defaultValue: "0"
)
}
section("Lights off override") {
paragraph "By default, the lights will turn off at sunrise when the app goes to sleep. Here, you can override the time that those lights are turned off. This will cause the light to turn off when no one is around instead of just dimming. The lights will still come on when someone arrives until sunrise. Leave the time blank to keep the lights on until sunrise."
input(
name: "timeDefaultEnd",
type: "time",
title: "Turn off at",
required: false,
defaultValue: null
)
}
}
}
def installed() {
debug("Installed with settings: ${settings}")
initialize()
}
def updated() {
debug("Updated with settings: ${settings}")
unsubscribe()
unschedule()
initialize()
}
def initialize() {
state.inDefault = false //Are we in the default schedule. Meaning the lights go back to standby levels after an event
state.enabled = false //Is the app enabled for events (sunrise to sunset)
state.active = false //Is an event currently active
state.levels = []
state.lastVerified = new Date()
state.nextEvent = 'sunset'
def times = getSunriseAndSunset(sunsetOffset: formatOffset(sunsetOffset), sunriseOffset: formatOffset(sunriseOffset), date: new Date())
def now = new Date()
if (now < times.sunrise || now > times.sunset) {
sunsetHandler()
} else {
scheduleNextEvent()
}
scheduleDefaultOff()
//Enable events
if (presence != null) subscribe(presence, "presence", presenceHandler)
if (motion != null) subscribe(motion, "motion", motionHandler)
if (contact != null) subscribe(contact, "contact", contactHandler)
if (acceleration != null) subscribe(acceleration, "acceleration.active", accelerationHandler)
if (appButton) subscribe(app, appTouchHandler)
startVerificationSchedule()
}
def sunsetHandler() {
debug('Sunset')
state.enabled = true
state.inDefault = true
state.active = false
state.levels.clear()
state.nextEvent = 'sunrise'
def output = "Smart Porch Light is now active"
if (brightnessLevelDefault != null && brightnessLevelDefault > 0) {
lightSet(brightnessLevelDefault)
output = output + " and has turned your light" + plural(switches.size())[0] + " on to " + brightnessLevelDefault + "%"
}
helloHome(output + ".")
scheduleNextEvent()
}
def scheduleSunset() {
def runTime = getSunriseAndSunset(sunsetOffset: formatOffset(sunsetOffset))
def sunset = runTime.sunset.format("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
state.nextEventTime = sunset
debug("Sunset scheduled for " + sunset.toString())
String sunsetString = sunset.toString()
runOnce(sunsetString, "sunsetHandler")
verifySchedule()
}
def sunriseHandler() {
debug('Sunrise')
state.inDefault = false
state.active = false
state.enabled = false
state.nextEvent = 'sunset'
defaultOff()
if (timeDefaultEnd == null) {
helloHome("It's sunrise. Smart Porch Light has turned off your light" + plural(switches.size())[0] + " and is now inactive.")
} else {
helloHome("It's sunrise. Smart Porch Light is now inactive.")
}
scheduleNextEvent()
}
def scheduleSunrise() {
def runTime = getSunriseAndSunset(sunriseOffset: formatOffset(sunriseOffset))
def sunrise = runTime.sunrise
if (new Date() > runTime.sunrise) {
runTime = getSunriseAndSunset(sunriseOffset: formatOffset(sunriseOffset), date: new Date() + 1)
}
sunrise = runTime.sunrise.format("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
state.nextEventTime = sunrise
debug("Sunrise scheduled for " + sunrise.toString())
String sunriseString = sunrise.toString()
runOnce(sunriseString, "sunriseHandler")
verifySchedule()
}
def defaultOffHandler() {
state.nextEvent = 'sunrise'
defaultOff()
helloHome("Smart Porch Light has turned off your light" + plural(switches.size())[0] + " as scheduled.")
verifySchedule()
}
def scheduleDefaultOff() {
if (brightnessLevelDefault != null && timeDefaultEnd != null) {
schedule(timeDefaultEnd, defaultOffHandler)
debug("Default off set for " + timeDefaultEnd)
}
}
def scheduleNextEvent() {
if (state.nextEvent == 'sunset') {
debug("Next Event: Sunset")
scheduleSunset()
} else {
debug("Next Event: Sunrise")
scheduleSunrise()
}
}
def presenceHandler(evt) {
if(evt.value == "present" && state.enabled && !state.active) {
lightOnEvent(brightnessLevelPresence)
scheduleAutoOff()
helloHome(evt.displayName + " arrived. Smart Porch Light has turned your light" + plural(switches.size())[0] + " on to " + brightnessLevelPresence + "%.")
}
verifySchedule()
}
def motionHandler(evt) {
if (evt.value == "active" && state.enabled && !state.active) {
lightOnEvent(brightnessLevelMotion)
scheduleAutoOff()
helloHome(evt.displayName + " detected motion. Smart Porch Light has turned your light" + plural(switches.size())[0] + " on to " + brightnessLevelMotion + "%.")
}
verifySchedule()
}
def contactHandler(evt) {
if (evt.value == "open" && state.enabled && !state.active) {
def reset=100
if (brightnessLevelContact != null) reset = brightnessLevelContact
lightOnEvent(reset)
scheduleAutoOff()
helloHome(evt.displayName + " opened. Smart Porch Light has turned your light" + plural(switches.size())[0] + " on to " + brightnessLevelContact + "%.")
}
verifySchedule()
}
def accelerationHandler(evt) {
if (evt.value == "active" && state.enabled && !state.active) {
def reset=100
if (brightnessLevelAcceleration != null) reset = brightnessLevelAcceleration
lightOnEvent(reset)
scheduleAutoOff()
helloHome("Someone knocked on " + evt.displayName + ". Smart Porch Light has turned your light" + plural(switches.size())[0] + " on to " + brightnessLevelAcceleration + "%.")
}
verifySchedule()
}
def appTouchHandler(evt) {
def reset=100
if (brightnessLevelTap != null) reset = brightnessLevelTap
lightOnEvent(reset)
scheduleAutoOff()
verifySchedule()
}
def verifySchedule() {
//This method is run every 15 minutes. It's also run each time a trigger is fired in case the schedule that controlls this fails too
def currentTime = new Date()
use(TimeCategory) {
/*
Because we run this method at the time a schedule changes, it's possible that the next event time will not have been updated yet.
This could cause a false positive when checking to see if a schedule was missed. So we set the current time back 1 minute to give
it a sufficient buffer for comparison
*/
currentTime = currentTime - 1.minutes
}
if (
(state.nextEventTime instanceof Date && new Date() > state.nextEventTime) ||
(state.nextEventTime instanceof String && new Date() > new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", state.nextEventTime))
) {
//If we get here it means we missed an event and need to restart the schedules
debug("SCHEDULE FAILED - MISSED EVENT")
restartVerifictionSchedule()
}
if (
(state.lastVerified instanceof Date && new Date() - state.lastVerified > 1200000) ||
(state.lastVerified instanceof String && new Date() - new Date().parse("yyyy-MM-dd'T'HH:mm:ssZ", state.lastVerified) > 1200000)
) {
//If we get here it means that it's been more then 20 minutes since this method was scheduled to run, so lets restart everything
debug("SCHEDULE FAILED - MISSED VERIFICATION")
restartVerifictionSchedule()
}
state.lastVerified = currentTime
}
def startVerificationSchedule() {
//Start the verifiction schedule that ensures everything is still on schedule
schedule("0 0/10 * * * ?", "verifySchedule")
}
def restartVerifictionSchedule() {
//We're here because a schedule failed
helloHome("SCHEDULE FAILED - RESTARTING SMART PORCH LIGHT")
updated() //Restart the app to reset the schedules
//sendPushMessage('Smart Porch Ligt schedule failed and has been restarted')
}
def scheduleAutoOff() {
//Schedule lights to dim after x minutes.
//This is executed after every event and is reset to x minutes on all subsequent events if this schedule hasn't yet run.
if (autoOffMinutes != null) {
debug('Auto off is scheduled for ' + autoOffMinutes + ' minutes')
//Make sure that a valid number was specified. Adjust number if needed
if (autoOffMinutes < 1) {
autoOffMinutes = 1
} else if (autoOffMinutes > 30) {
autoOffMinutes = 30
}
runIn(60 * autoOffMinutes, autoOff)
}
}
def autoOff() {
//Reset lights to default level
state.active = false
lightReset()
def output = "It's been "+ autoOffMinutes + " minute" + plural(autoOffMinutes)[0] + ". "
if (state.inDefault) {
output += "Resetting your light" + plural(switches.size())[0] + " to standby."
} else {
output += "Turning your light" + plural(switches.size())[0] + " off."
}
helloHome(output)
}
def lightSet(level) {
//Don't allow values above 100% for brightness
if (level > 100) {
level = 100
} else if (level == null) {
level = 0
}
//Set lights to specified level
switches.setLevel(level)
debug('brightness set to ' + level)
}
def lightOnEvent(level) {
state.active = true
if (rememberLevel) {
state.levels.clear()
switches.each {
state.levels.add(it.currentValue('level'))
}
}
lightSet(level)
}
def lightReset() {
if (rememberLevel && state.levels.size() == switches.size()) {
switches.eachWithIndex { it, i ->
it.setLevel(state.levels[i])
//helloHome("Light reset to " + state.levels[i] + "%")
}
} else {
//set default "reset" to 0% brightness
def reset = 0
//If brightness level is set, use that instead of the default set above
if (state.inDefault && brightnessLevelDefault != null) reset = brightnessLevelDefault
debug('Auto off executed - reset to default level')
lightSet(reset)
}
debug('reset lights')
}
def defaultOn() {
//Enables app at sunset and turns lights on to default level
state.inDefault = true
lightReset()
debug('Default - schedule started')
}
def defaultOff() {
//Disable app at sunrise or when scheduled and turn lights off
state.inDefault = false
state.active = false
switches.off()
debug('Default - schedule ended')
}
def readableSchedule() {
//Create a more readable schedule description to display on the main settings page when setting on the schedule page are modified.
def sunrise = (sunriseOffset == null) ? 0 : sunriseOffset.toInteger()
def sunset = (sunsetOffset == null) ? 0 : sunsetOffset.toInteger()
//def output = "Active from\n"
def output = ""
if (sunset != null && sunset !=0) output += convertMinutes(sunset) + ((sunset > 0) ? " after " : " before ")
output += "sunset to"
if (sunrise != null && sunrise !=0) output += " " + convertMinutes(sunrise) + ((sunrise > 0) ? " after" : " before")
output += " sunrise."
if (timeDefaultEnd != null) {
output += "\n\nStandby light" + plural(switches.size())[0] + " turn" + plural(switches.size(), true)[0] + " off at "
def outputFormat = new SimpleDateFormat("h:mm a")
def inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
def date = inputFormat.parse(timeDefaultEnd)
output += outputFormat.format(date)
}
return output
}
def convertMinutes(totalMinutes) {
totalMinutes = totalMinutes.abs()
int hours = Math.floor(totalMinutes / 60).toInteger()
int minutes = (totalMinutes % 60)
def output = ""
if (hours > 0) {
output += hours + " hour" + plural(hours)[0]
if (minutes > 0) output += ' and '
}
if (minutes > 0) output += minutes + " minutes"
return output
}
def plural(count, r = false) {
//return an "s" to append to a word if "count" indicates zero or more than one
//Is this really necessary? No, but it makes me happy.
def language = []
if ((count == 1 && !r) || (count != 1 && r)) {
language.addAll(['','is','was'])
} else {
language.addAll(['s','are','were'])
}
return language
}
def formatOffset(offset) {
def formatted =''
if (offset == null || offset == 0) {
formatted = "00:00"
} else {
offset = offset.toInteger()
if (offset < 0) formatted += '-'
def totalMinutes = offset.abs()
int hours = Math.floor(totalMinutes / 60).toInteger()
int minutes = (totalMinutes % 60)
formatted += "0" + hours + ":"
if (minutes == 0) {
formatted += "00"
} else if (minutes < 10) {
formatted += "0" + minutes
} else {
formatted += "" + minutes
}
}
return formatted
}
def debug(msg) {
//Enable debugging. Comment out line below to disable output.
log.debug(msg)
//Uncomment the next line to send debugging messages to hello, home. I use this when live logging breaks, which is often for me, and when I need a way to view data that's logged when I'm not logged in.
//sendNotificationEvent("DEBUG: " + msg)
}
def helloHome(msg) {
if (useHelloHome) sendNotificationEvent(msg)
log.debug("Hello, home: " + msg)
}