Code by Scott שאול בן ישוע
Inovelli-Scene-Creator-Child.groovy 38.3 KB
Newer Older
Stephan Hackett's avatar
Stephan Hackett committed
1
2
/*	
 *	Hubitat Import URL: https://raw.githubusercontent.com/stephack/Hubitat/master/apps/Advanced%20Button%20Controller%20(ABC)/ABC_Child_Creator.groovy
3
 *
4
5
 *
 *
Stephan Hackett's avatar
Stephan Hackett committed
6
7
8
 *	ABC Child Creator for Advanced Button Controller
 *
 *	Author: SmartThings, modified by Bruce Ravenel, Dale Coffing, Stephan Hackett
Stephan Hackett's avatar
Stephan Hackett committed
9
 *
10
11
12
 *  11/05/19 - Added previousTrack support for speakers
 *
 *  10/06/19 - Added Auto as option under Set Fan Speed
13
 *
Stephan Hackett's avatar
Stephan Hackett committed
14
15
 *	08/14/19 - Send Http Requests (POST or GET - simple form encoded)
 *
16
 *	05/18/19 - Speech notifications now allow random messages to be sent (Use ; to separate options)
Stephan Hackett's avatar
Grammer    
Stephan Hackett committed
17
 *			 - cycleFan modified to no longer use numeric setSpeed values as this may be deprecated by HE for future fan devices
Stephan Hackett's avatar
Stephan Hackett committed
18
 *
19
 *	04/29/19 - fixed small UI bug handling '0' level values
20
 *			 - updated adjustFans method
21
 *
Stephan Hackett's avatar
Stephan Hackett committed
22
23
 *	02/19/19 - rules api bug squashed
 *
24
25
26
27
 *	02/17/19 - updated Button Description for rules to show Rule name instead of Rule number
 * 			 - Button Descriptions will now be surrounded by [] for better visibility
 * 			 - Action details are now stored in a state value to allow for better efficiency
 *
28
29
 *	02/10/19 - setColor Level is no longer required (can be left blank)
 *
30
31
 *  02/07/19 - fixed Set Color bug (missing level option)
 *
Stephan Hackett's avatar
Stephan Hackett committed
32
33
34
35
 *	01/14/19 - updated logging output to appropriate type (info vs debug)
 *			 - added input to enable/disable debug logging
 *			 - added url to Raw code at the top of the parent/child apps
 *			   (Thanks for the feedback and suggestions @csteeele)
Stephan Hackett's avatar
Stephan Hackett committed
36
 *			 - update checking code is now done through json file (Thanks to @Cobra for his guidance)
37
 *
Stephan Hackett's avatar
Stephan Hackett committed
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
 *	12/15/18 - updated color scheme to match new HE theme
 *			 - added suppot for Rules API
 *
 *
 *	10/12/18 - adjusted "Set Mode" to comply with mode related updates in firmware 1.1.5
 *
 *
 *	8/01/18 - added Hubitat Safety Monitor Control (created new MODES section for Set Mode and Set HSM)
 * 		added level to setColor()
 *		added new detail parameter "myDetail.mul" (Mode and HSM set to multiple:false)
 *		removed section shallHide for sub inputs .... (section will be visible if primary input has a value...sub value no longer checked)
 *
 *
 * 7/03/18 - code cleanup
 *		Added pictures enhancements and reordered options for better flow
 *		Corrected default child app label (previously defaulted to "ABC Button Mapping" on first save)
 *
 *
 * 7/01/18 - added Released actions for all control sections
 *		Pushed/Held/DoubleTapped/Released hidden from Dimmer Ramp section based on devices capabilities
 *
 * 6/30/18 - adapted fan cycle to be compliant with fanControl capability (removed cycle support for custom driver)
 *		added ability to set specific fan speed
 *		added support for ramping (graceful dimming) - switch/bulb needs changeLevel capability and button device needs releaseableButton capability
 *		
 *
 *	6/02/18 - added ability to cycle custom Hampton Bay Zigbee Fan Controller
 *
 *
 *	4/21/18 - added support for new Sonos Player devices (play/pause, next, previous, mute/unmute, volumeup/down)
 *
 *
 *	3/28/18 - added option to set color and temp
 *		test code for custom commands (not yet working)
 *
 *  2/06/18 - converted code to hubitat format
 * 		removed ability to hide "held options"
 *		removed hwspecifics section as is no longer applicable
 *		adjusted device list to look for "capability.pushableButton"
 *		adjusted buttonDevice subscription (pushed, held, doubleTapped)
 *		adjusted buttonEvent() to swap "name" and "value" as per new rules
 * 2/08/18 - change formatting for Button Config Preview (Blue/Complete color)
 *		Added Double Tap inputs and edited shallHide() getDescription()
 *		added code for showDouble() to only display when controller support DT
 *		removed enableSpec and other Virtual Container Code as this is not supported in Hubitat
 *2/12/18
 * 		Updated to new detailsMap and modified Button Config/Preview pages
 *		hides secondary values if primary not set. When dispayed they are now "required". 
 *
 *2/12/18
 *		Switched to parent/child config	
 *		removed button pics and descriptive text (not utilized by hubitat)
 *
 *10/24/18
 *		added the ability to cycle through Scenes (done using push() command and cycles in alphabetical order only)
 *		minor GUI updates
94
 */
Stephan Hackett's avatar
Stephan Hackett committed
95
96
97

import hubitat.helper.RMUtils

98
def version(){"v0.2.191105"}
99
100

definition(
Sha'ul ben Yeshua's avatar
Sha'ul ben Yeshua committed
101
102
103
104
    name: "Inovelli Scene Creator Child",
    namespace: "Inovelli",
    author: "Scott Grayban",
    description: "Create Scenes for your Inovelli Devices",
105
    category: "My Apps",
Sha'ul ben Yeshua's avatar
Sha'ul ben Yeshua committed
106
    parent: "Inovelli:Inovelli Scene Creator",
Stephan Hackett's avatar
Stephan Hackett committed
107
108
109
    iconUrl: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
    iconX2Url: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
    iconX3Url: "https://cdn.rawgit.com/stephack/ABC/master/resources/images/abc2.png",
110
111
112
)

preferences {
Stephan Hackett's avatar
Stephan Hackett committed
113
114
115
116
117
118
119
120
	page(name: "chooseButton")
	page(name: "configButtonsPage")
	page(name: "timeIntervalInput", title: "Only during a certain time") {
		section {
			input "starting", "time", title: "Starting", required: false
			input "ending", "time", title: "Ending", required: false
		}
	}
121
122
}

Stephan Hackett's avatar
Stephan Hackett committed
123
def chooseButton() {
124
	state.details=getPrefDetails()
Stephan Hackett's avatar
Stephan Hackett committed
125
126
	dynamicPage(name: "chooseButton", install: true, uninstall: true) {
		section(){
Stephan Hackett's avatar
Stephan Hackett committed
127
128
				def appHead = "<img src=https://raw.githubusercontent.com/stephack/Hubitat/master/resources/images/abc2.png height=80 width=80> \n${checkForUpdate()}"
				paragraph "<div style='text-align:center'>${appHead}</div>"
Stephan Hackett's avatar
Stephan Hackett committed
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
		}
		section(getFormat("header", "${getImage("Device", "45")}"+" Step 1: Select Your Button Device")) {
            input "buttonDevice", "capability.pushableButton", title: getFormat("section", "Button Device"), description: "Tap to Select", multiple: false, required: true, submitOnChange: true
		}
        if(buttonDevice){
        	state.buttonType =  buttonDevice.typeName
            if(state.buttonType.contains("Aeon Minimote")) state.buttonType =  "Aeon Minimote"
            if(logEnable) log.debug "Device Type is now set to: "+state.buttonType
            state.buttonCount = manualCount?: buttonDevice.currentValue('numberOfButtons')
            section(getFormat("header", "${getImage("Button", "45")}"+"  Step 2: Configure Your Buttons")) {
            	if(state.buttonCount<1) {
                	paragraph "The selected button device did not report the number of buttons it has. Please specify in the Advanced Config section below."
                }
                else {
                	for(i in 1..state.buttonCount){
                		href "configButtonsPage", title: getFormat("section", "${getImage("Button", "30")}" + " Button ${i}"), state: getDescription(i)!="Tap to configure"? "complete": null, description: getDescription(i), params: [pbutton: i]
                    }
            	}
            }
		}
        section(getFormat("header", "${getImage("Custom", "45")}"+"  Set Custom Name (Optional)")) {
        	label title: "Assign a name:", required: false
            paragraph getFormat("line")
        }
        section("Advanced Config:", hideable: true, hidden: hideOptionsSection()) {
            	input "manualCount", "number", title: "Set/Override # of Buttons?", required: false, description: "Only set if your driver does not report", submitOnChange: true
                input "collapseAll", "bool", title: "Collapse Unconfigured Sections?", defaultValue: true
				input "logEnable", "bool", title: "Enable Debug Logging?", required: false
			}
        section(title: "Only Execute When:", hideable: true, hidden: hideOptionsSection()) {
			def timeLabel = timeIntervalLabel()
			href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null
			input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
					options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
			input "modes", "mode", title: "Only when mode is", multiple: true, required: false
		}
	}
}

def configButtonsPage(params) {
	if (params.pbutton != null) state.currentButton = params.pbutton.toInteger()
	dynamicPage(name: "configButtonsPage", title: "CONFIGURE BUTTON ${state.currentButton}:\n${state.buttonType}", getButtonSections(state.currentButton))
}

def getButtonSections(buttonNumber) {
	return {    	
        def myDetail
        section(getFormat("header", "${getImage("Switches", "45")}"+" SWITCHES")){}
177
		//state.details=getPrefDetails()
178
        for(i in 1..30) {//Build 1st 30 Button Config Options
179
        	myDetail = state.details.find{it.sOrder==i}
Stephan Hackett's avatar
Stephan Hackett committed
180
181
182
        	//
    section(title: myDetail.secLabel, hideable: true, hidden: !(shallHide("${myDetail.id}${buttonNumber}"))) {
				if(showPush(myDetail.desc)) input "${myDetail.id}${buttonNumber}_pushed", myDetail.cap, title: "When Pushed", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
183
184
185
				if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub}${buttonNumber}_pushed", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
                if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub2}${buttonNumber}_pushed", myDetail.subType, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.subOpt
                if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_pushed")) input "${myDetail.sub3}${buttonNumber}_pushed", myDetail.subType, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.subOpt
Stephan Hackett's avatar
Stephan Hackett committed
186
187
				
        		if(showHeld(myDetail.desc)) input "${myDetail.id}${buttonNumber}_held", myDetail.cap, title: "When Held", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
188
189
190
191
192
193
194
195
196
                if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub}${buttonNumber}_held", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
                if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub2}${buttonNumber}_held", myDetail.subType, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.subOpt
                if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_held")) input "${myDetail.sub3}${buttonNumber}_held", myDetail.subType, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.subOpt
        		
				if(showDouble(myDetail.desc)) input "${myDetail.id}${buttonNumber}_doubleTapped", myDetail.cap, title: "When Double Tapped", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
                if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub}${buttonNumber}_doubleTapped", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
                if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub2}${buttonNumber}_doubleTapped", myDetail.subType, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.subOpt
                if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_doubleTapped")) input "${myDetail.sub3}${buttonNumber}_doubleTapped", myDetail.subType, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.subOpt
		
Stephan Hackett's avatar
Stephan Hackett committed
197
        		if(showRelease(myDetail.desc)) input "${myDetail.id}${buttonNumber}_released", myDetail.cap, title: "When Released", multiple: myDetail.mul, required: false, submitOnChange: collapseAll, options: myDetail.opt
198
199
200
201
                if(myDetail.sub && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub}${buttonNumber}_released", myDetail.subType, title: myDetail.sTitle, multiple: false, required: !myDetail.sNotReq, description: myDetail.sDesc, options: myDetail.subOpt
                if(myDetail.sub2 && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub2}${buttonNumber}_released", myDetail.subType, title: myDetail.s2Title, multiple: false, required: !myDetail.s2NotReq, description: myDetail.s2Desc, options: myDetail.subOpt
                if(myDetail.sub3 && isReq("${myDetail.id}${buttonNumber}_released")) input "${myDetail.sub3}${buttonNumber}_released", myDetail.subType, title: myDetail.s3Title, multiple: false, required: !myDetail.s3NotReq, description: myDetail.s3Desc, options: myDetail.subOpt
		}
Stephan Hackett's avatar
Stephan Hackett committed
202
203
204
            if(i==3) section("\n"+getFormat("header", "${getImage("Dimmers", "45")}"+" DIMMERS")){}
            if(i==9) section("\n"+getFormat("header", "${getImage("Color", "45")}"+" COLOR LIGHTS")){}
            if(i==11) section("\n"+getFormat("header", "${getImage("Speakers", "45")}"+" SPEAKERS")){}
205
206
207
208
            if(i==18) section("\n"+getFormat("header", "${getImage("Fans", "45")}"+" FANS")){}
            if(i==21) section("\n"+getFormat("header", "${getImage("Mode", "45")}"+" MODES")){}
			if(i==23) section("\n"+getFormat("header", "${getImage("Rule", "45")}"+" RULE CONTROL")){}
            if(i==24) section("\n"+getFormat("header", "${getImage("Other", "45")}"+" OTHER")){}
209
        }
Stephan Hackett's avatar
Stephan Hackett committed
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
		
		section(getFormat("section", "Notifications (SMS):"), hideable:true , hidden: !shallHide("notifications_${buttonNumber}")) {
			input "notifications_${buttonNumber}_pushed", "text", title: "Message To Send When Pushed:", description: "Enter message to send", required: false, submitOnChange: collapseAll
            input "phone_${buttonNumber}_pushed","phone" ,title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
            if(showHeld()) {
            	paragraph getFormat("line")
				input "notifications_${buttonNumber}_held", "text", title: "Message To Send When Held:", description: "Enter message to send", required: false, submitOnChange: collapseAll
				input "phone_${buttonNumber}_held", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
            }
            if(showDouble()) {
            	paragraph getFormat("line")
				input "notifications_${buttonNumber}_doubleTapped", "text", title: "Message To Send When Double Tapped:", description: "Enter message to send", required: false, submitOnChange: collapseAll
				input "phone_${buttonNumber}_doubleTapped", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
            }
            if(showRelease()) {
            	paragraph getFormat("line")
				input "notifications_${buttonNumber}_released", "text", title: "Message To Send When Released:", description: "Enter message to send", required: false, submitOnChange: collapseAll
				input "phone_${buttonNumber}_released", "phone", title: "Send Text To:", description: "Enter phone number", required: false, submitOnChange: collapseAll
            }
		}
	}
}

def getImage(type, mySize) {
    def loc = "<img src=https://raw.githubusercontent.com/stephack/Hubitat/master/resources/images/"
    if(type == "Device") return "${loc}Device.png height=${mySize} width=${mySize}>   "
    if(type == "Button") return "${loc}Button.png height=${mySize} width=${mySize}>   "
    if(type == "Switches") return "${loc}Switches.png height=${mySize} width=${mySize}>   "
    if(type == "Color") return "${loc}Color.png height=${mySize} width=${mySize}>   "
    if(type == "Dimmers") return "${loc}Dimmers.png height=${mySize} width=${mySize}>   "
    if(type == "Speakers") return "${loc}Speakers.png height=${mySize} width=${mySize}>   "
    if(type == "Fans") return "${loc}Fans.png height=${mySize} width=${mySize}>   "
    if(type == "HSM") return "${loc}Mode.png height=${mySize} width=${mySize}>   "
    if(type == "Mode") return "${loc}Mode.png height=${mySize} width=${mySize}>   "
    if(type == "Other") return "${loc}Other.png height=${mySize} width=${mySize}>   "
    if(type == "Custom") return "${loc}Custom.png height=${mySize} width=${mySize}>   "
    if(type == "Locks") return "${loc}Locks.png height=30 width=30>   "
    if(type == "Sirens") return "${loc}Sirens.png height=30 width=30>   "
    if(type == "Scenes") return "${loc}Scenes.png height=30 width=30>   "
    if(type == "Shades") return "${loc}Shades.png height=30 width=30>   "
    if(type == "SMS") return "${loc}SMS.png height=30 width=30>   "
    if(type == "Speech") return "${loc}Audio.png height=30 width=30>   "
	if(type == "Rule") return "${loc}Rule.png height=${mySize} width=${mySize}>   "
}

def getFormat(type, myText=""){
    if(type == "section") return "<div style='color:#78bf35;font-weight: bold'>${myText}</div>"
    if(type == "command") return "<div style='color:red;font-weight: bold'>${myText}</div>"
    if(type == "header") return "<div style='color:#ffffff;background-color:#392F2E;text-align:center'>${myText}</div>"
    if(type == "line") return "\n<hr style='background-color:#78bf35; height: 2px; border: 0;'></hr>"
}

def shallHide(myFeature) {
	if(collapseAll) return (settings["${myFeature}_pushed"]||settings["${myFeature}_held"]||settings["${myFeature}_doubleTapped"]||settings["${myFeature}_released"]||settings["${myFeature}"])
	return true
}

def isReq(myFeature) {
    (settings[myFeature])? true : false
}

def showPush(desc) {
    if(buttonDevice.hasCapability("PushableButton")){ 	//is device pushable?
        if(desc.contains("Ramp")){									
            if(buttonDevice.hasCapability("HoldableButton")) return false	//if this is the Ramp section and device is also Holdable, then hide Pushed option
        }
        return true
277
    }
Stephan Hackett's avatar
Stephan Hackett committed
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
	return false
}

def showHeld(desc) {
    return buttonDevice.hasCapability("HoldableButton")
}

def showDouble(desc) {
    if(desc && desc.contains("Ramp")) return false //remove DoubleTapped option when setting smooth dimming button/devices
    return buttonDevice.hasCapability("DoubleTapableButton")
}

def showRelease(desc) {
    if(desc && desc.contains("Ramp")) return false //remove On Release option when setting smooth dimming button/devices
    return buttonDevice.hasCapability("ReleasableButton")
}

def getDescription(dNumber) {
    def descript = ""
    if(!(settings.find{it.key.contains("_${dNumber}_")})) return "Tap to configure"
    if(settings.find{it.key.contains("_${dNumber}_pushed")}) descript = "\nPUSHED:"+getDescDetails(dNumber,"_pushed")+"\n"
    if(settings.find{it.key.contains("_${dNumber}_held")}) descript = descript+"\nHELD:"+getDescDetails(dNumber,"_held")+"\n"
    if(settings.find{it.key.contains("_${dNumber}_doubleTapped")}) descript = descript+"\nTAPx2:"+getDescDetails(dNumber,"_doubleTapped")+"\n"
    if(settings.find{it.key.contains("_${dNumber}_released")}) descript = descript+"\nRELEASED:"+getDescDetails(dNumber,"_released")+"\n"
	return descript
}

def getDescDetails(bNum, type){
	def numType=bNum+type
	def preferenceNames = settings.findAll{it.key.contains("_${numType}")}.sort()		//get all configured settings that: match button# and type, AND are not false
    if(!preferenceNames){
    	return "  **Not Configured** "
    }
    else {
    	def formattedPage =""
    	preferenceNames.each {eachPref->
314
315
316
317
318
319
320
321
322
        	def prefDetail = state.details.find{eachPref.key.contains(it.id)}	//gets decription of action being performed(eg Turn On)
        						
			def prefDevice		//name of device the action is being performed on (eg Bedroom Fan)
			if(prefDetail.sub == "valRule"){
				prefDevice = " : " + getRuleName(eachPref.value)	//extracts rules name (instead if number) for button description
			}
			else {
				prefDevice = " : ${eachPref.value}"// was only needed to cleanup display in ST..not necessary in HE->           - "[" - "]"	
			}
323
324
			def thisSub = settings[prefDetail.sub + numType]
			def prefSubValue = thisSub != null? thisSub:"(!Missing!)"
Stephan Hackett's avatar
Stephan Hackett committed
325
326
327
328
329
330
331
332
333
334
335
336
337
            def sub2Value = settings[prefDetail.sub2 + numType]
            def sub3Value = settings[prefDetail.sub3 + numType]
            if(sub2Value) prefSubValue += ", S: ${sub2Value}"
            if(sub3Value) prefSubValue += ", L: ${sub3Value}"
            if(prefDetail.type=="normal") formattedPage += "\n- ${prefDetail.desc}${prefDevice}"
            if(prefDetail.type=="hasSub") formattedPage += "\n- ${prefDetail.desc}${prefSubValue}${prefDevice}"
            if(prefDetail.type=="bool") formattedPage += "\n- ${prefDetail.desc}"
    	}
		return formattedPage
    }
}

def getRules(){
338
339
340
341
342
343
344
345
346
347
348
349
350
	rules = RMUtils.getRuleList()
	//converts rules list to easily parsed format and stores in state.rules for easy access
	state.rules=[:] 
	rules.each{rule->
		rule.each{pair->
			state.rules[pair.key]=pair.value 
		}
	}
	////////////////////////////////////////////////////
	return rules
}

def getRuleName(num){	//allows button descriptions for RuleAPI controls to show Rule Name instead of Rule Number
Stephan Hackett's avatar
Stephan Hackett committed
351
	getRules()
352
353
	def holder=[]
	num.each{ruleNum->
Stephan Hackett's avatar
Stephan Hackett committed
354
		holder << state.rules.find{it.key==ruleNum.toInteger()}.value
355
356
	}
	return holder
357
358
}

359
360
361
362
363
364
365
366
367
368
def installed() {
	initialize()
}

def updated() {
	unsubscribe()
	initialize()
}

def initialize() {
Stephan Hackett's avatar
Stephan Hackett committed
369
370
371
372
373
374
375
376
    if(logEnable) log.debug "INITIALIZED with settings: ${settings}"
    if(logEnable) log.debug app.label
    if(!app.label || app.label == "default")app.updateLabel(defaultLabel())
	subscribe(buttonDevice, "pushed", buttonEvent)
	subscribe(buttonDevice, "held", buttonEvent)
	subscribe(buttonDevice, "doubleTapped", buttonEvent)
    subscribe(buttonDevice, "released", buttonEvent)
    state.lastshadesUp = true
377
	state.details=getPrefDetails()
Stephan Hackett's avatar
Stephan Hackett committed
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
}

def defaultLabel() {
	return "${buttonDevice} Mapping"
}

def getPrefDetails(){
	def detailMappings =
    	[[id:'lightOn_', sOrder:1, desc:'Turn On ', comm:turnOn, type:"normal", secLabel: getFormat("section", "Turn On"), cap: "capability.switch", mul: true],
     	 [id:"lightOff_", sOrder:2, desc:'Turn Off', comm:turnOff, type:"normal", secLabel: getFormat("section", "Turn Off"), cap: "capability.switch", mul: true],
         [id:'lights_', sOrder:3, desc:'Toggle On/Off', comm:toggle, type:"normal", secLabel: getFormat("section", "Toggle On/Off"), cap: "capability.switch", mul: true],
         
         [id:"lightDim_", sOrder:4, desc:'Dim to ', comm:turnDim, sub:"valLight", subType:"number", type:"hasSub", secLabel: getFormat("section", "On to Level - Group 1"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
     	 [id:"lightD2m_", sOrder:5, desc:'Dim to ', comm:turnDim, sub:"valLight2", subType:"number", type:"hasSub", secLabel: getFormat("section", "On to Level - Group 2"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
         [id:'dimPlus_', sOrder:6, desc:'Brightness +', comm:levelUp, sub:"valDimP", subType:"number", type:"hasSub", secLabel: getFormat("section", "Increase Level By"), cap: "capability.switchLevel", sTitle: "Increase by", sDesc:"0 to 15", mul: true],
     	 [id:'dimMinus_', sOrder:7, desc:'Brightness -', comm:levelDown, sub:"valDimM", subType:"number", type:"hasSub", secLabel: getFormat("section", "Decrease Level By"), cap: "capability.switchLevel", sTitle: "Decrease by", sDesc:"0 to 15", mul: true],
         [id:'lightsDT_', sOrder:8, desc:'Toggle Off/Dim to ', comm:dimToggle, sub:"valDT", subType:"number", type:"hasSub", secLabel: getFormat("section", "Toggle OnToLevel/Off"), cap: "capability.switchLevel", sTitle: "Bright Level", sDesc:"0 to 100%", mul: true],
         [id:'lightsRamp_', sOrder:9, desc:'Ramp ', comm:rampUp, sub:"valDir", subType:"enum", subOpt:['up','down'], type:"hasSub", secLabel: getFormat("section", "Ramp Up/Down"), cap: "capability.changeLevel", sTitle: "Ramp Direction (Up/Down)", sDesc:"Up or Down", mul: true],
         
         [id:'lightColorTemp_', sOrder:10, desc:'Set Light Color Temp to ', comm:colorSetT, sub:"valColorTemp", subType:"number", type:"hasSub", secLabel: getFormat("section", "Set Temperature"), cap: "capability.colorTemperature", sTitle: "Color Temp", sDesc:"2000 to 9000", mul: true],
398
         [id:'lightColor_', sOrder:11, desc:'Set Light Color H:', comm:colorSet, sub:"valHue", subType:"number", sub2:"valSat", sub3:"valLvl", type:"hasSub", secLabel: getFormat("section", "Set Color"), cap: "capability.colorControl", sTitle: "Hue", s2Title: "Saturation", s3Title: "Lvl", sDesc:"0 to 100", s2Desc:"0 to 100", s3Desc:"0 to 100", mul: true, s3NotReq:true],
Stephan Hackett's avatar
Stephan Hackett committed
399
400
401
402
403
     	          
         [id:"speakerpp_", sOrder:12, desc:'Toggle Play/Pause', comm:speakerplaystate, type:"normal", secLabel: getFormat("section", "Toggle Play/Pause"), cap: "capability.musicPlayer", mul: true],
     	 [id:'speakervu_', sOrder:13, desc:'Volume +', comm:levelUp, sub:"valSpeakU", subType:"number", type:"hasSub", secLabel: getFormat("section", "Increase Volume By"), cap: "capability.musicPlayer", sTitle: "Increase by", sDesc:"0 to 15", mul: true],
     	 [id:"speakervd_", sOrder:14, desc:'Volume -', comm:levelDown, sub:"valSpeakD", subType:"number", type:"hasSub", secLabel: getFormat("section", "Decrease Volume By"), cap: "capability.musicPlayer", sTitle: "Decrease by", sDesc:"0 to 15", mul: true],
         [id:'speakernt_', sOrder:15, desc:'Next Track', comm:speakernexttrack, type:"normal", secLabel: getFormat("section", "Go to Next Track"), cap: "capability.musicPlayer", mul: true],
404
405
406
         [id:'speakerpt_', sOrder:16, desc:'Previous Track', comm:speakerprevioustrack, type:"normal", secLabel: getFormat("section", "Go to Previous Track"), cap: "capability.musicPlayer", mul: true],
         [id:'speakermu_', sOrder:17, desc:'Mute', comm:speakermute, type:"normal", secLabel: getFormat("section", "Speakers Toggle Mute"), cap: "capability.musicPlayer", mul: true],
         [id:"musicPreset_", sOrder:18, desc:'Cycle Preset', comm:cyclePlaylist, type:"normal", secLabel: getFormat("section", "Preset to Cycle"), cap: "device.VirtualContainer", mul: true],         
Stephan Hackett's avatar
Stephan Hackett committed
407
         
408
409
410
         [id:'fanSet_', sOrder:19, desc:'Set Fan to ', comm:setFan, sub:"valSpeed", subType:"enum", subOpt:['off','low','medium-low','medium','high','auto'], type:"hasSub", secLabel: getFormat("section", "Set Speed"), cap: "capability.fanControl", sTitle: "Set Speed to", sDesc:"L/ML/M/H/A", mul: true],
         [id:"fanCycle_", sOrder:20, desc:'Cycle Fan Speed', comm:cycleFan, type:"normal", secLabel: getFormat("section", "Cycle Speed"), cap: "capability.fanControl", mul: true],         
         [id:"fanAdjust_", sOrder:21,desc:'Adjust', comm:adjustFan, type:"normal", secLabel: getFormat("section", "Cycle Speed (Legacy)"), cap: "capability.switchLevel", mul: true],
Stephan Hackett's avatar
Stephan Hackett committed
411
         
412
413
         [id:"mode_", sOrder:22, desc:'Set Mode', comm:changeMode, type:"normal", secLabel: getFormat("section", "Set Mode"), cap: "mode", mul: false],
     	 [id:"hsm_", sOrder:23, desc:'Set HSM', comm:setHSM, type:"normal", secLabel: getFormat("section", "Set HSM"), cap: "enum", opt:['armAway','armHome','disarm','armRules','disarmRules','disarmAll','armAll','cancelAlerts'], mul: false],
Stephan Hackett's avatar
Stephan Hackett committed
414

415
         [id:'rule_', sOrder:24, desc:'Rule To ', comm:ruleExec, sub:"valRule", subType:"enum", subOpt:['Run','Stop','Pause','Resume','Evaluate','Set Boolean True','Set Boolean False'], type:"hasSub", secLabel: getFormat("section", "Rule and Actions"), cap: "enum", opt: getRules(), sTitle: "Select Action Type", sDesc:"Choose Action", mul: true],
Stephan Hackett's avatar
Stephan Hackett committed
416
		 
417
418
419
420
421
422
         [id:"locks_", sOrder:25, desc:'Lock', comm:setUnlock, type:"normal", secLabel: getFormat("section", "Locks (Lock Only)"), cap: "capability.lock", mul: true],
		 [id:'cycleScenes_', sOrder:26, desc:'Cycle', comm:cycle, type:"normal", secLabel: getFormat("section", "Scenes (Cycle)"), cap: "device.SceneActivator", mul: true, isCycle: true],
         [id:"shadeAdjust_", sOrder:27,desc:'Adjust', comm:adjustShade, type:"normal", secLabel: getFormat("section", "Shades (Up/Down/Stop)"), cap: "capability.doorControl", mul: true],
         [id:'sirens_', sOrder:28, desc:'Toggle', comm:toggle, type:"normal", secLabel: getFormat("section", "Sirens (Toggle)"), cap: "capability.alarm", mul: true],
         [id:'httpRequest_', sOrder:29, desc:'Send: ', comm:hRequest, sub:"reqUrl", subType:"text", type:"hasSub", secLabel: getFormat("section", "Send Http Request"), cap: "enum", opt:['POST', 'GET'], sTitle:"HTTP URL", sDesc:"Enter complete url to send", mul: false],
         [id:"speechDevice_", sOrder:30, desc:'Send Msg To', comm:speechHandle, type:"normal", secLabel: getFormat("section", "Notifications (Speech):"), sub:"speechTxt", cap: "capability.speechSynthesis", subType:"text", sTitle: "Message To Speak:", sDesc:"Enter message to speak (Random messages: Use ; to separate choices)", mul: true],///set type to normal instead of sub so message text is not displayed
Stephan Hackett's avatar
Stephan Hackett committed
423
424
425
426
427
		 
		 [id:"notifications_", desc:'Send Push Notification', comm:messageHandle, sub:"valNotify", type:"bool"],
     	 [id:"phone_", desc:'Send SMS', comm:smsHandle, sub:"notifications_", type:"normal"],
        ]
    return detailMappings
428
429
}

Stephan Hackett's avatar
Stephan Hackett committed
430
def checkForUpdate(){
Stephan Hackett's avatar
Stephan Hackett committed
431
	def params = [uri: "https://raw.githubusercontent.com/stephack/Hubitat/master/apps/Advanced%20Button%20Controller%20(ABC)/child.json",
Stephan Hackett's avatar
Stephan Hackett committed
432
433
434
435
436
				   	contentType: "application/json"]
       	try {
			httpGet(params) { response ->
				def results = response.data
				def appStatus
Stephan Hackett's avatar
Stephan Hackett committed
437
438
				if(version() == results.currVersion) {
					appStatus = "<small>Child ${version()}</small><br>${results.noUpdateImg}"
Stephan Hackett's avatar
Stephan Hackett committed
439
440
				}
				else {
Stephan Hackett's avatar
Stephan Hackett committed
441
442
					appStatus = "<small>Child ${version()}</small><br>${results.updateImg}${results.changeLog}"
					log.warn "ABC Child App does not appear to be the latest version: Please update."
Stephan Hackett's avatar
Stephan Hackett committed
443
444
445
446
447
448
				}
				return appStatus
			}
		} 
        catch (e) {
        	log.error "Error:  $e"
449
    	}
450
451
}

Stephan Hackett's avatar
Stephan Hackett committed
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def buttonEvent(evt) {
	if(allOk) {
    	def buttonNumber = evt.value
		def pressType = evt.name
		if(logEnable) log.debug "$buttonDevice: Button $buttonNumber was $pressType"
        
        //detects if button is used for graceful hold to dim function then calls stopLevelChange()
        if(pressType == "released" && settings["lightsRamp_${buttonNumber}_pushed"]){
        	rampEnd(settings["lightsRamp_${buttonNumber}_pushed"])
        }
        if(pressType == "released" && settings["lightsRamp_${buttonNumber}_held"]){
        	rampEnd(settings["lightsRamp_${buttonNumber}_held"])
        }        
        
    	def preferenceNames = settings.findAll{it.key.contains("_${buttonNumber}_${pressType}")}
    	preferenceNames.each{eachPref->
468
        	def prefDetail = state.details?.find{eachPref.key.contains(it.id)}		//returns the detail map of id,desc,comm,sub
Stephan Hackett's avatar
Stephan Hackett committed
469
470
471
472
473
474
475
476
477
        	def PrefSubValue = settings["${prefDetail.sub}${buttonNumber}_${pressType}"] //value of subsetting (eg 100)
            def PrefSub2Value = settings["${prefDetail.sub2}${buttonNumber}_${pressType}"] //value of subsetting (eg 100)
            def PrefSub3Value = settings["${prefDetail.sub3}${buttonNumber}_${pressType}"]	//value of subsetting (eg 100)
            if(prefDetail.sub3) "$prefDetail.comm"(eachPref.value,PrefSubValue, PrefSub2Value, PrefSub3Value)
            	else if(prefDetail.sub2) "$prefDetail.comm"(eachPref.value,PrefSubValue, PrefSub2Value)
        		else if(prefDetail.sub) "$prefDetail.comm"(eachPref.value,PrefSubValue)
                else if(prefDetail.isCycle) "$prefDetail.comm"(eachPref.value, "${eachPref.key}")
        	else "$prefDetail.comm"(eachPref.value)
    	}
Stephan Hackett's avatar
Stephan Hackett committed
478
	}
479
480
}

Stephan Hackett's avatar
Stephan Hackett committed
481
def speechHandle(devices, msg){
482
483
484
485
486
487
488
489
490
491
    log.info "Sending ${msg} to ${devices}"
	if(msg.contains(";")) {
		def myPool = msg.split(";")
		def poolSize = myPool.size()
		def randomItem = Math.abs(new Random().nextInt() % poolSize)
		devices.speak(myPool[randomItem-1])
	}
	else{
		devices.speak(msg)
	}
Stephan Hackett's avatar
Stephan Hackett committed
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
}

def turnOn(devices) {
	log.info "Turning On: $devices"
	devices.on()
}

def turnOff(devices) {
	log.info "Turning Off: $devices"
	devices.off()
}

def turnDim(devices, level) {
	log.info "Dimming (to $level): $devices"
	devices.setLevel(level)
}

def colorSet(devices,hueVal,satVal,lvlVal) {
    log.info "Setting Color (to H:$hueVal, S:$satVal, L:$lvlVal): $devices"
    def myColor = [:]
    myColor.hue = hueVal.toInteger()
    myColor.saturation = satVal.toInteger()
514
    if(lvlVal) myColor.level = lvlVal.toInteger()
Stephan Hackett's avatar
Stephan Hackett committed
515
516
517
518
519
520
521
522
523
524
    devices.setColor(myColor)//([hue:hueVal,saturation:satVal,level:50]) 
}

def colorSetT(devices, temp) {
    log.info "Setting Color Temp (to $temp): $devices"
    devices.setColorTemperature(temp)    
}

def adjustFan(device) {
	log.info "Adjusting: $device"
525
526
	def currentLevel = device.currentLevel[0]
	if(device.currentSwitch[0] == 'off') device.setLevel(15)
Stephan Hackett's avatar
Stephan Hackett committed
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
	else if (currentLevel < 34) device.setLevel(50)
  	else if (currentLevel < 67) device.setLevel(90)
	else device.off()
}

def adjustShade(device) {
	log.info "Shades: $device = ${device.currentMotor} state.lastUP = $state.lastshadesUp"
	if(device.currentMotor in ["up","down"]) {
    	state.lastshadesUp = device.currentMotor == "up"
    	device.stop()
    } else {
    	state.lastshadesUp ? device.down() : device.up()
        state.lastshadesUp = !state.lastshadesUp
    }
}

def setFan(devices, speed){
	log.info "Setting Speed to $speed: $devices"
    devices.setSpeed(speed)
}

def speakerplaystate(device) {
	log.info "Toggling Play/Pause: $device"
	device.currentStatus.contains('playing')? device.pause() : device.play()
}
   
def speakernexttrack(device) {
	log.info "Next Track Sent to: $device"
	device.nextTrack()
}   

558
559
560
561
562
def speakerprevioustrack(device) {
	log.info "Previous Track Sent to: $device"
	device.previousTrack()
} 

Stephan Hackett's avatar
Stephan Hackett committed
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
def speakermute(device) {
	log.info "Toggling Mute/Unmute: $device"
	device.currentMute.contains('unmuted')? device.mute() : device.unmute()
} 

def levelUp(device, inclevel) {
	log.info "Incrementing Level (by +$inclevel): $device"
	def currentVol = device.currentLevel[0]//device.currentValue('level')[0]	//currentlevel return a list...[0] is first item in list ie volume level
    def newVol = currentVol + inclevel
  	device.setLevel(newVol)
    if(logEnable) log.debug "Level increased by $inclevel to $newVol"
}

def levelDown(device, declevel) {
	log.info "Decrementing Level (by -$declevel): $device"
	def currentVol = device.currentLevel[0]//device.currentValue('level')[0]
    def newVol = currentVol - declevel
  	device.setLevel(newVol)
    if(logEnable) log.debug "Level decreased by $declevel to $newVol"
}

def rampUp(devices, dir){
    log.info "Ramping ${dir}: $devices"
    devices.startLevelChange(dir)
}

def rampEnd(device){
	log.info "Ending Ramp: $device"
    device.stopLevelChange()    
}

def setUnlock(devices) {
	log.info "Locking: $devices"
	devices.lock()
}

def toggle(devices) {
	log.info "Toggling: $devices"
	if (devices*.currentValue('switch').contains('on')) {
		devices.off()
603
	}
Stephan Hackett's avatar
Stephan Hackett committed
604
605
	else if (devices*.currentValue('switch').contains('off')) {
		devices.on()
606
	}
Stephan Hackett's avatar
Stephan Hackett committed
607
608
609
610
611
	else if (devices*.currentValue('alarm').contains('off')) {
        devices.siren()
    }
	else {
		devices.on()
612
	}
Stephan Hackett's avatar
Stephan Hackett committed
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
}

def dimToggle(devices, dimLevel) {
	log.info "Toggling On/Off | Dimming (to $dimLevel): $devices"
	if (devices*.currentValue('switch').contains('on')) devices.off()
	else devices.setLevel(dimLevel)
}

def runRout(rout){
	log.info "Running: $rout"
	location.helloHome.execute(rout)
}

def ruleExec(rules, action){
	log.info "Performing ${action} Action on Rules: ${rules}"
	def ruleAction
	if(action == "Run") ruleAction =  "runRuleAct"
	if(action == "Stop") ruleAction =  "stopRuleAct"
	if(action == "Pause") ruleAction =  "pauseRule"
	if(action == "Resume") ruleAction =  "resumeRule"
	if(action == "Evaluate") ruleAction =  "runRule"
	if(action == "Set Boolean True") ruleAction =  "setRuleBooleanTrue"
	if(action == "Set Boolean False") ruleAction =  "setRuleBooleanFalse"
	
	RMUtils.sendAction(rules, ruleAction, app.label)
}

def messageHandle(msg, inApp) {
	if(inApp==true) {
    	log.info "Push notification sent"
    	sendPush(msg)
644
645
646
	}
}

Stephan Hackett's avatar
Stephan Hackett committed
647
648
649
650
def smsHandle(phone, msg){
    log.info "SMS sent"
    sendSms(phone, msg ?:"No custom text entered on: $app.label")
}
651

Stephan Hackett's avatar
Stephan Hackett committed
652
653
654
def setHSM(hsmMode) {
    sendLocationEvent(name: "hsmSetArm", value: hsmMode)
}
655

Stephan Hackett's avatar
Stephan Hackett committed
656
657
658
659
def changeMode(mode) {
	log.info "Changing Mode to: $mode"
	if (location.mode != mode && location.modes?.find { it.name == mode}) setLocationMode(mode)
}
660

Stephan Hackett's avatar
Stephan Hackett committed
661
662
def cycleFan(devices) { //all fans will sync speeds with fisrt fan in the list
    log.info "Cycling: $devices"
663
664
665
666
667
668
    def mySpeed = devices[0].currentSpeed
    if(mySpeed == "off") devices.setSpeed("low") 
    if(mySpeed == "low") devices.setSpeed("medium-low") 
    if(mySpeed == "medium-low") devices.setSpeed("medium") 
    if(mySpeed == "medium") devices.setSpeed("high")
    if(mySpeed == "high") devices.setSpeed("off") 
Stephan Hackett's avatar
Stephan Hackett committed
669
}
670

Stephan Hackett's avatar
Stephan Hackett committed
671
672
673
674
675
676
677
def cycle(devices, devIndex) {
    log.info "Cycling: $devices"
    def mySize = devices.size() -1
    if(!state."${devIndex}" || state."${devIndex}" > mySize) state."${devIndex}" = 0
    devices[state."${devIndex}"].push()
    state."${devIndex}" ++
}
678

Stephan Hackett's avatar
Stephan Hackett committed
679
680
681
def cyclePlaylist(devices){
    devices.cycle()
}
Stephan Hackett's avatar
Stephan Hackett committed
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703

def hRequest(reqType, myUrl){
    def params = [
        uri: myUrl,
		contentType: 'application/x-www-form-urlencoded'
    ]
	
    if(logEnable) log.debug "${reqType} - ${params}"
	if(reqType == "POST") asynchttpPost('myPostResponse', params, [type: reqType])
    if(reqType == "GET") asynchttpGet('myPostResponse', params, [type: reqType])
  	  	
}

def myPostResponse(response,data){
	if(response.status != 200) {
		log.error "HTTP ${data["type"]} Request returned error ${response.status}. Check your URL!"
	}
    else {
        if(logEnable) log.debug "HTTP ${data["type"]} Request sent successfully"
    }
}

Stephan Hackett's avatar
Stephan Hackett committed
704
705
706
// execution filter methods
private getAllOk() {
	modeOk && daysOk && timeOk
707
708
}

Stephan Hackett's avatar
Stephan Hackett committed
709
710
711
712
713
private getModeOk() {
	def result = !modes || modes.contains(location.mode)
	if(logEnable) log.debug "modeOk = $result"
	result
}
714

Stephan Hackett's avatar
Stephan Hackett committed
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
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)
	}
	if(logEnable) log.debug "daysOk = $result"
	result
}
731

Stephan Hackett's avatar
Stephan Hackett committed
732
733
734
735
736
737
738
739
740
741
742
private getTimeOk() {
	def result = true
	if (starting && ending) {
		def currTime = now()
		def start = timeToday(starting).time
		def stop = timeToday(ending).time
		result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
	}
	if(logEnable) log.debug "timeOk = $result"
	result
}
743

Stephan Hackett's avatar
Stephan Hackett committed
744
745
746
747
748
749
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)
}
750

Stephan Hackett's avatar
Stephan Hackett committed
751
752
753
private hideOptionsSection() {
	(starting || ending || days || modes || manualCount) ? false : true
}
754

Stephan Hackett's avatar
Stephan Hackett committed
755
756
757
private timeIntervalLabel() {
	(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}