4 * Copyright 2015 SmartThings
6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7 * in compliance with the License. You may obtain a copy of the License at:
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13 * for the specific language governing permissions and limitations under the License.
17 name: "Every Element",
18 namespace: "smartthings/examples",
19 author: "SmartThings",
20 description: "Every element demonstration app",
21 category: "SmartThings Internal",
22 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
23 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
28 page(name: "firstPage")
31 page(name: "buttonsPage")
32 page(name: "imagePage")
33 page(name: "inputPage")
34 page(name: "inputBooleanPage")
35 page(name: "inputIconPage")
36 page(name: "inputImagePage")
37 page(name: "inputDevicePage")
38 page(name: "inputCapabilityPage")
39 page(name: "inputRoomPage")
40 page(name: "inputModePage")
41 page(name: "inputSelectionPage")
42 page(name: "inputHubPage")
43 page(name: "inputContactBookPage")
44 page(name: "inputTextPage")
45 page(name: "inputTimePage")
47 page(name: "hrefPage")
48 page(name: "paragraphPage")
49 page(name: "videoPage")
50 page(name: "labelPage")
51 page(name: "modePage")
53 // Every element helper pages
54 page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
55 page(name: "flattenedPage")
59 dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
61 href(page: "appPage", title: "Element: 'app'")
62 href(page: "buttonsPage", title: "Element: 'buttons'")
63 href(page: "hrefPage", title: "Element: 'href'")
64 href(page: "imagePage", title: "Element: 'image'")
65 href(page: "inputPage", title: "Element: 'input'")
66 href(page: "labelPage", title: "Element: 'label'")
67 href(page: "modePage", title: "Element: 'mode'")
68 href(page: "paragraphPage", title: "Element: 'paragraph'")
69 href(page: "videoPage", title: "Element: 'video'")
72 href(page: "flattenedPage", title: "All of the above elements on a single page")
78 dynamicPage(name: "inputPage", title: "Links to every 'input' element") {
80 href(page: "inputBooleanPage", title: "to boolean page")
81 href(page: "inputIconPage", title: "to icon page")
82 href(page: "inputImagePage", title: "to image page")
83 href(page: "inputSelectionPage", title: "to selection page")
84 href(page: "inputTextPage", title: "to text page")
85 href(page: "inputTimePage", title: "to time page")
87 section("subsets of selection input") {
88 href(page: "inputDevicePage", title: "to device selection page")
89 href(page: "inputCapabilityPage", title: "to capability selection page")
90 href(page: "inputRoomPage", title: "to room selection page")
91 href(page: "inputModePage", title: "to mode selection page")
92 href(page: "inputHubPage", title: "to hub selection page")
93 href(page: "inputContactBookPage", title: "to contact-book selection page")
98 def inputBooleanPage() {
99 dynamicPage(name: "inputBooleanPage") {
101 paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`"
104 input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null)
105 input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description")
107 section("defaultValue: 'true'") {
108 input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true")
110 section("with image") {
111 input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null)
112 input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
116 def inputIconPage() {
117 dynamicPage(name: "inputIconPage") {
119 paragraph "`description` is not displayed for icon elements"
120 paragraph "`multiple` has no effect because you can only choose a single icon"
122 section("required: true") {
123 input(type: "icon", name: "iconRequired", title: "without description", required: true)
124 input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true)
126 section("with image") {
127 paragraph "The image specified will be replaced after an icon is selected"
128 input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
132 def inputImagePage() {
133 dynamicPage(name: "inputImagePage") {
135 paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)"
136 paragraph "Go to the device preferences of a Mobile Presence device to see it in action"
137 paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences"
138 input(type: "image", title: "This is kind of what it looks like", required: false)
144 def optionsGroup(List groups, String title) {
145 def group = [values:[], order: groups.size()]
146 group.title = title ?: ""
150 def addValues(List groups, String key, String value) {
151 def lastGroup = groups[-1]
152 lastGroup["values"] << [
155 order: lastGroup["values"].size()
159 def listToMap(List original) {
160 original.inject([:]) { result, v ->
165 def addGroup(List groups, String title, values) {
166 if (values instanceof List) {
167 values = listToMap(values)
170 values.inject(optionsGroup(groups, title)) { result, k, v ->
171 return addValues(result, k, v)
175 def addGroup(values) {
176 addGroup([], null, values)
178 /* Example usage of options builder
180 // Creating grouped options
182 addGroup(newGroups, "first group", ["foo", "bar", "baz"])
183 addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"])
186 addGroup(["a", "b", "c"])
189 addGroup(["a": "yes", "b": "no", "c": "maybe"])
193 def inputSelectionPage() {
195 def englishOptions = ["One", "Two", "Three"]
196 def spanishOptions = ["Uno", "Dos", "Tres"]
197 def groupedOptions = []
198 addGroup(groupedOptions, "English", englishOptions)
199 addGroup(groupedOptions, "Spanish", spanishOptions)
201 dynamicPage(name: "inputSelectionPage") {
203 section("options variations") {
204 paragraph "tap these elements and look at the differences when selecting an option"
205 input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
206 input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
209 section("list vs map") {
210 paragraph "These should be identical in UI, but are different in code and will produce different settings"
211 input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"]))
212 input(type: "enum", name: "selectionMap", title: "Choose a device", description: "settings will be something like ['device1-id']", groupedOptions: addGroup(["device1-id": "Device1 Label", "device2-id": "Device2 Label"]))
215 section("segmented") {
216 paragraph "segmented should only work if there are either 2 or 3 options to choose from"
217 input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
218 input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
220 paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
221 input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
222 input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
224 paragraph "specifying defaultValue still works with segmented selection elements"
225 input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
228 section("required: true") {
229 input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true)
232 section("multiple: true") {
233 input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
236 section("with image") {
237 input(type: "enum", name: "selectionWithImage", title: "This has an image", description: "and a description", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
241 def inputTextPage() {
242 dynamicPage(name: "inputTextPage", title: "Every 'text' variation") {
243 section("style and functional differences") {
244 input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true)
245 input(type: "text", name: "textWithImage", title: "with image", description: "This should look different when nothing has been entered", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", required: false)
248 input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false)
250 section("password") {
251 input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false)
254 input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false)
257 input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false)
260 input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false)
263 input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false)
266 section("specified ranges") {
267 paragraph "You can limit number and decimal inputs to a specific range."
268 input(range: "50..150", type: "decimal", name: "decimalRange50..150", title: "only values between 50 and 150 will pass validation", description: "no special formatting", required: false)
269 paragraph "Negative limits will add a negative symbol to the keyboard."
270 input(range: "-50..50", type: "number", name: "numberRange-50..50", title: "only values between -50 and 50 will pass validation", description: "no special formatting", required: false)
271 paragraph "Specify * to not limit one side or the other."
272 input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false)
273 input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false)
274 paragraph "If you don't specify a range, it defaults to 0..*"
278 def inputTimePage() {
279 dynamicPage(name: "inputTimePage") {
281 input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false)
282 input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false)
283 input(type: "time", name: "timeRequired", title: "required: true", required: true)
288 /// selection subsets
289 def inputDevicePage() {
291 dynamicPage(name: "inputDevicePage") {
293 section("required: true") {
294 input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected")
297 section("multiple: true") {
298 input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
301 section("with image") {
302 input(type: "device.switch", name: "deviceRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
306 def inputCapabilityPage() {
308 dynamicPage(name: "inputCapabilityPage") {
310 section("required: true") {
311 input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected")
314 section("multiple: true") {
315 input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
318 section("with image") {
319 input(type: "capability.switch", name: "capabilityRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
323 def inputRoomPage() {
325 dynamicPage(name: "inputRoomPage") {
327 section("required: true") {
328 input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected")
331 section("multiple: true") {
332 input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
335 section("with image") {
336 input(type: "room", name: "roomRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
340 def inputModePage() {
342 dynamicPage(name: "inputModePage") {
344 section("required: true") {
345 input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected")
348 section("multiple: true") {
349 input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
352 section("with image") {
353 input(type: "mode", name: "modeRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
359 dynamicPage(name: "inputHubPage") {
361 section("required: true") {
362 input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected")
365 section("multiple: true") {
366 input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
369 section("with image") {
370 input(type: "hub", name: "hubRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
374 def inputContactBookPage() {
376 dynamicPage(name: "inputContactBookPage") {
378 section("required: true") {
379 input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected")
382 section("multiple: true") {
383 input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
386 section("with image") {
387 input(type: "contact", name: "contactRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
393 dynamicPage(name: "appPage", title: "Every 'app' type") {
395 paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
400 title: "required:false, multiple:false",
404 appName: "Child SmartApp"
406 app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
407 app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
408 app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
410 section("multiple:true") {
411 app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
413 section("multiple:true with image") {
414 app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
420 dynamicPage(name: "labelPage", title: "Every 'Label' type") {
422 paragraph "The difference between a label element and a text input element is that the label element will effect the SmartApp directly by setting the label. An input element will place the set value in the SmartApp's settings."
423 paragraph "There are 3 here as an example. Never use more than 1 label element on a page."
424 label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
425 label(name: "labelRequired", title: "required:true", required: true, multiple: false)
426 label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
432 dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
434 paragraph "The difference between a mode element and a mode input element is that the mode element will effect the SmartApp directly by setting the modes it executes in. A mode input element will place the set value in the SmartApp's settings."
435 paragraph "Another difference is that you can select 'All Modes' when choosing which mode the SmartApp should execute in. This is the same as selecting no modes. When a SmartApp does not have modes specified, it will execute in all modes."
436 paragraph "There are 4 here as an example. Never use more than 1 mode element on a page."
437 mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
438 mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
439 mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
440 mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
445 def paragraphPage() {
446 dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
447 section("paragraph") {
448 paragraph "This is how you should make a paragraph element"
449 paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
455 dynamicPage(name: "hrefPage", title: "Every 'href' variation") {
456 section("stylistic differences") {
457 href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete")
458 href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
459 href(page: "deadEnd", title: "with image and description", description: "and state: 'complete'", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", state: "complete")
461 section("functional differences") {
462 href(page: "deadEnd", title: "to a page within the app")
463 href(url: "http://www.google.com", title: "to a url using all defaults")
464 href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true)
470 dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
471 section("Simple Buttons") {
472 paragraph "If there are an odd number of buttons, the last button will span the entire view area."
473 buttons(name: "buttons1", title: "1 button", buttons: [
474 [label: "foo", action: "foo"]
476 buttons(name: "buttons2", title: "2 buttons", buttons: [
477 [label: "foo", action: "foo"],
478 [label: "bar", action: "bar"]
480 buttons(name: "buttons3", title: "3 buttons", buttons: [
481 [label: "foo", action: "foo"],
482 [label: "bar", action: "bar"],
483 [label: "baz", action: "baz"]
485 buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
486 [label: "foo", action: "foo"],
487 [label: "bar", action: "bar"]
490 section("Colored Buttons") {
491 buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
492 [label: "complete", action: "bar", backgroundColor: "complete"],
493 [label: "required", action: "bar", backgroundColor: "required"]
495 buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
496 [label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
497 [label: "fg: #ffac00", action: "foo", color: "#ffac00"],
498 [label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
500 buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
501 [label: "green", action: "foo", backgroundColor: "green"],
502 [label: "red", action: "foo", backgroundColor: "red"],
503 [label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
511 dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
513 image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
514 image(name: "imageWithMultipleImages", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, images: ["https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"])
520 dynamicPage(name: "videoPage", title: "Every 'video' type") { // TODO: finish this
522 // TODO: update this when there is a videoElement method
523 element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://f.cl.ly/items/0w0D1p0K2D0d190F3H3N/Image%202015-12-14%20at%207.57.27%20AM.jpg", video: "http://f.cl.ly/items/3O2L03471l2K3E3l3K1r/Zombie%20Kid%20Likes%20Turtles.mp4")
528 def flattenedPage() {
530 firstPage().sections[0].body.each { hrefElement ->
531 if (hrefElement.name != "inputPage") {
532 // inputPage is a bunch of hrefs
533 allSections += "${hrefElement.page}"().sections
536 // collect the input elements
537 inputPage().sections.each { section ->
538 section.body.each { hrefElement ->
539 allSections += "${hrefElement.page}"().sections
542 def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
543 flattenedPage.sections = allSections
548 dynamicPage(name: "deadEnd") {
554 log.debug "Installed with settings: ${settings}"
560 log.debug "Updated with settings: ${settings}"
567 // TODO: subscribe to attributes, devices, locations, etc.