Checking in all the SmartThings apps; both official and third-party.
authorrtrimana <rtrimana@uci.edu>
Tue, 10 Jul 2018 20:28:04 +0000 (13:28 -0700)
committerrtrimana <rtrimana@uci.edu>
Tue, 10 Jul 2018 20:28:04 +0000 (13:28 -0700)
250 files changed:
README.md
official/alfred-workflow.groovy [new file with mode: 0755]
official/auto-humidity-vent.groovy [new file with mode: 0755]
official/beacon-control.groovy [new file with mode: 0755]
official/beaconthings-manager.groovy [new file with mode: 0755]
official/big-turn-off.groovy [new file with mode: 0755]
official/big-turn-on.groovy [new file with mode: 0755]
official/bon-voyage.groovy [new file with mode: 0755]
official/bose-soundtouch-connect.groovy [new file with mode: 0755]
official/bose-soundtouch-control.groovy [new file with mode: 0755]
official/bright-when-dark-and-or-bright-after-sunset.groovy [new file with mode: 0755]
official/brighten-dark-places.groovy [new file with mode: 0755]
official/brighten-my-path.groovy [new file with mode: 0755]
official/button-controller.groovy [new file with mode: 0755]
official/camera-power-scheduler.groovy [new file with mode: 0755]
official/cameras-on-when-im-away.groovy [new file with mode: 0755]
official/carpool-notifier.groovy [new file with mode: 0755]
official/close-the-valve.groovy [new file with mode: 0755]
official/coffee-after-shower.groovy [new file with mode: 0755]
official/color-coordinator.groovy [new file with mode: 0755]
official/curb-control.groovy [new file with mode: 0755]
official/curling-iron.groovy [new file with mode: 0755]
official/darken-behind-me.groovy [new file with mode: 0755]
official/device-tile-controller.groovy [new file with mode: 0755]
official/door-jammed-notification.groovy [new file with mode: 0755]
official/door-knocker.groovy [new file with mode: 0755]
official/door-state-to-color-light-hue-bulb.groovy [new file with mode: 0755]
official/double-tap.groovy [new file with mode: 0755]
official/dry-the-wetspot.groovy [new file with mode: 0755]
official/ecobee-connect.groovy [new file with mode: 0755]
official/elder-care-daily-routine.groovy [new file with mode: 0755]
official/elder-care-slip-fall.groovy [new file with mode: 0755]
official/energy-alerts.groovy [new file with mode: 0755]
official/energy-saver.groovy [new file with mode: 0755]
official/enhanced-auto-lock-door.groovy [new file with mode: 0755]
official/every-element.groovy [new file with mode: 0755]
official/feed-my-pet.groovy [new file with mode: 0755]
official/flood-alert.groovy [new file with mode: 0755]
official/forgiving-security.groovy [new file with mode: 0755]
official/foscam-connect.groovy [new file with mode: 0755]
official/garage-door-monitor.groovy [new file with mode: 0755]
official/garage-door-opener.groovy [new file with mode: 0755]
official/gentle-wake-up.groovy [new file with mode: 0755]
official/gideon-smart-home.groovy [new file with mode: 0755]
official/gideon.groovy [new file with mode: 0755]
official/gidjit-hub.groovy [new file with mode: 0755]
official/good-night-house.groovy [new file with mode: 0755]
official/good-night.groovy [new file with mode: 0755]
official/goodnight-ubi.groovy [new file with mode: 0755]
official/greetings-earthling.groovy [new file with mode: 0755]
official/habit-helper.groovy [new file with mode: 0755]
official/hall-light-welcome-home.groovy [new file with mode: 0755]
official/has-barkley-been-fed.groovy [new file with mode: 0755]
official/hello-home-phrase-director.groovy [new file with mode: 0755]
official/hub-ip-notifier.groovy [new file with mode: 0755]
official/hue-connect.groovy [new file with mode: 0755]
official/hue-mood-lighting.groovy [new file with mode: 0755]
official/humidity-alert.groovy [new file with mode: 0755]
official/ifttt.groovy [new file with mode: 0755]
official/initial-state-event-streamer.groovy [new file with mode: 0755]
official/it-moved.groovy [new file with mode: 0755]
official/its-too-cold.groovy [new file with mode: 0755]
official/its-too-hot.groovy [new file with mode: 0755]
official/jawbone-button-notifier.groovy [new file with mode: 0755]
official/jawbone-up-connect.groovy [new file with mode: 0755]
official/jenkins-notifier.groovy [new file with mode: 0755]
official/keep-me-cozy-ii.groovy [new file with mode: 0755]
official/keep-me-cozy.groovy [new file with mode: 0755]
official/laundry-monitor.groovy [new file with mode: 0755]
official/left-it-open.groovy [new file with mode: 0755]
official/let-there-be-dark.groovy [new file with mode: 0755]
official/let-there-be-light.groovy [new file with mode: 0755]
official/life360-connect.groovy [new file with mode: 0755]
official/lifx-connect.groovy [new file with mode: 0755]
official/light-follows-me.groovy [new file with mode: 0755]
official/light-up-the-night.groovy [new file with mode: 0755]
official/lighting-director.groovy [new file with mode: 0755]
official/lights-off-when-closed.groovy [new file with mode: 0755]
official/lights-off-with-no-motion-and-presence.groovy [new file with mode: 0755]
official/lock-it-at-a-specific-time.groovy [new file with mode: 0755]
official/lock-it-when-i-leave.groovy [new file with mode: 0755]
official/logitech-harmony-connect.groovy [new file with mode: 0755]
official/mail-arrived.groovy [new file with mode: 0755]
official/make-it-so.groovy [new file with mode: 0755]
official/medicine-management-contact-sensor.groovy [new file with mode: 0755]
official/medicine-management-temp-motion.groovy [new file with mode: 0755]
official/medicine-reminder.groovy [new file with mode: 0755]
official/mini-hue-controller.groovy [new file with mode: 0755]
official/monitor-on-sense.groovy [new file with mode: 0755]
official/mood-cube.groovy [new file with mode: 0755]
official/my-light-toggle.groovy [new file with mode: 0755]
official/netatmo-connect.groovy [new file with mode: 0755]
official/nfc-tag-toggle.groovy [new file with mode: 0755]
official/nobody-home.groovy [new file with mode: 0755]
official/notify-me-when-it-opens.groovy [new file with mode: 0755]
official/notify-me-when.groovy [new file with mode: 0755]
official/notify-me-with-hue.groovy [new file with mode: 0755]
official/obything-music-connect.groovy [new file with mode: 0755]
official/once-a-day.groovy [new file with mode: 0755]
official/opent2t-smartapp-test.groovy [new file with mode: 0755]
official/photo-burst-when.groovy [new file with mode: 0755]
official/plantlink-connector.groovy [new file with mode: 0755]
official/power-allowance.groovy [new file with mode: 0755]
official/presence-change-push.groovy [new file with mode: 0755]
official/presence-change-text.groovy [new file with mode: 0755]
official/quirky-connect.groovy [new file with mode: 0755]
official/ready-for-rain.groovy [new file with mode: 0755]
official/ridiculously-automated-garage-door.groovy [new file with mode: 0755]
official/rise-and-shine.groovy [new file with mode: 0755]
official/routine-director.groovy [new file with mode: 0755]
official/safe-watch.groovy [new file with mode: 0755]
official/scheduled-mode-change.groovy [new file with mode: 0755]
official/send-ham-bridge-command-when.groovy [new file with mode: 0755]
official/severe-weather-alert.groovy [new file with mode: 0755]
official/shabbat-and-holiday-modes.groovy [new file with mode: 0755]
official/simple-control.groovy [new file with mode: 0755]
official/simple-sync-connect.groovy [new file with mode: 0755]
official/simple-sync-trigger.groovy [new file with mode: 0755]
official/single-button-controller.groovy [new file with mode: 0755]
official/sleepy-time.groovy [new file with mode: 0755]
official/smart-alarm.groovy [new file with mode: 0755]
official/smart-auto-lock-unlock.groovy [new file with mode: 0755]
official/smart-energy-service.groovy [new file with mode: 0755]
official/smart-home-ventilation.groovy [new file with mode: 0755]
official/smart-humidifier.groovy [new file with mode: 0755]
official/smart-light-timer-x-minutes-unless-already-on.groovy [new file with mode: 0755]
official/smart-nightlight.groovy [new file with mode: 0755]
official/smart-security.groovy [new file with mode: 0755]
official/smart-turn-it-on.groovy [new file with mode: 0755]
official/smart-windows.groovy [new file with mode: 0755]
official/smartblock-chat-sender.groovy [new file with mode: 0755]
official/smartblock-linker.groovy [new file with mode: 0755]
official/smartblock-manager.groovy [new file with mode: 0755]
official/smartblock-notifier.groovy [new file with mode: 0755]
official/smartweather-station-controller.groovy [new file with mode: 0755]
official/sonos-music-modes.groovy [new file with mode: 0755]
official/sonos-remote-control.groovy [new file with mode: 0755]
official/speaker-control.groovy [new file with mode: 0755]
official/speaker-mood-music.groovy [new file with mode: 0755]
official/speaker-notify-with-sound.groovy [new file with mode: 0755]
official/speaker-weather-forecast.groovy [new file with mode: 0755]
official/sprayer-controller-2.groovy [new file with mode: 0755]
official/spruce-scheduler.groovy [new file with mode: 0755]
official/step-notifier.groovy [new file with mode: 0755]
official/sunrise-sunset.groovy [new file with mode: 0755]
official/switch-activates-home-phrase-or-mode.groovy [new file with mode: 0755]
official/switch-activates-home-phrase.groovy [new file with mode: 0755]
official/switch-changes-mode.groovy [new file with mode: 0755]
official/talking-alarm-clock.groovy [new file with mode: 0755]
official/tcp-bulbs-connect.groovy [new file with mode: 0755]
official/tesla-connect.groovy [new file with mode: 0755]
official/text-me-when-it-opens.groovy [new file with mode: 0755]
official/text-me-when-theres-motion-and-im-not-here.groovy [new file with mode: 0755]
official/the-big-switch.groovy [new file with mode: 0755]
official/the-flasher.groovy [new file with mode: 0755]
official/the-gun-case-moved.groovy [new file with mode: 0755]
official/thermostat-auto-off.groovy [new file with mode: 0755]
official/thermostat-mode-director.groovy [new file with mode: 0755]
official/thermostat-window-check.groovy [new file with mode: 0755]
official/turn-it-on-for-5-minutes.groovy [new file with mode: 0755]
official/turn-it-on-when-im-here.groovy [new file with mode: 0755]
official/turn-it-on-when-it-opens.groovy [new file with mode: 0755]
official/turn-off-with-motion.groovy [new file with mode: 0755]
official/turn-on-only-if-i-arrive-after-sunset.groovy [new file with mode: 0755]
official/ubi.groovy [new file with mode: 0755]
official/undead-early-warning.groovy [new file with mode: 0755]
official/unlock-it-when-i-arrive.groovy [new file with mode: 0755]
official/vacation-lighting-director.groovy [new file with mode: 0755]
official/vinli-home-connect.groovy [new file with mode: 0755]
official/virtual-thermostat.groovy [new file with mode: 0755]
official/wattvision-manager.groovy [new file with mode: 0755]
official/weather-underground-pws-connect.groovy [new file with mode: 0755]
official/weather-windows.groovy [new file with mode: 0755]
official/weatherbug-home.groovy [new file with mode: 0755]
official/wemo-connect.groovy [new file with mode: 0755]
official/when-its-going-to-rain.groovy [new file with mode: 0755]
official/whole-house-fan.groovy [new file with mode: 0755]
official/withings-manager.groovy [new file with mode: 0755]
official/withings.groovy [new file with mode: 0755]
official/working-from-home.groovy [new file with mode: 0755]
official/yoics-connect.groovy [new file with mode: 0755]
third-party/AeonMinimote.groovy [new file with mode: 0755]
third-party/AlarmThing-Alert.groovy [new file with mode: 0755]
third-party/AlarmThing-AlertAll.groovy [new file with mode: 0755]
third-party/AlarmThing-AlertContact.groovy [new file with mode: 0755]
third-party/AlarmThing-AlertMotion.groovy [new file with mode: 0755]
third-party/AlarmThing-AlertSensor.groovy [new file with mode: 0755]
third-party/AutomaticCarHA.groovy [new file with mode: 0755]
third-party/AutomaticReport.groovy [new file with mode: 0755]
third-party/BatteryLow.groovy [new file with mode: 0755]
third-party/BeaconThing.groovy [new file with mode: 0755]
third-party/BeaconThingsManager.groovy [new file with mode: 0755]
third-party/BetterLaundryMonitor.groovy [new file with mode: 0755]
third-party/Color Temp via Virtual Dimmer.groovy [new file with mode: 0755]
third-party/DeviceTamperAlarm.groovy [new file with mode: 0755]
third-party/DoubleTapModeChange.groovy [new file with mode: 0755]
third-party/DoubleTapTimedLight.groovy [new file with mode: 0755]
third-party/FireCO2Alarm.groovy [new file with mode: 0755]
third-party/Hue Party Mode.groovy [new file with mode: 0755]
third-party/JSON.groovy [new file with mode: 0755]
third-party/Light-Alert-SmartApp.groovy [new file with mode: 0755]
third-party/Lutron-Group-Control.groovy [new file with mode: 0755]
third-party/MonitorAndSetEcobeeHumidity.groovy [new file with mode: 0755]
third-party/MonitorAndSetEcobeeTemp.groovy [new file with mode: 0755]
third-party/NotifyIfLeftUnlocked.groovy [new file with mode: 0755]
third-party/Orbit Water Timer.groovy [new file with mode: 0755]
third-party/QuirkyTripper.groovy [new file with mode: 0755]
third-party/RemindToCloseDoggyDoor.groovy [new file with mode: 0755]
third-party/Securify-KeyFob.groovy [new file with mode: 0755]
third-party/SmartPresence.groovy [new file with mode: 0755]
third-party/SmartenIt-ZBWS3B.groovy [new file with mode: 0755]
third-party/Sonos.groovy [new file with mode: 0755]
third-party/StriimLight.groovy [new file with mode: 0755]
third-party/StriimLightConnect.groovy [new file with mode: 0755]
third-party/Switches.groovy [new file with mode: 0755]
third-party/VirtualButton.groovy [new file with mode: 0755]
third-party/VirtualButtons.groovy [new file with mode: 0755]
third-party/WindowOrDoorOpen.groovy [new file with mode: 0755]
third-party/WorkingFromHome.groovy [new file with mode: 0755]
third-party/YouLeftTheDoorOpen.groovy [new file with mode: 0755]
third-party/ZWN-SC7.groovy [new file with mode: 0755]
third-party/Zwave-indicator-manager.groovy [new file with mode: 0755]
third-party/auto-lock-door.smartapp.groovy [new file with mode: 0755]
third-party/buffered-event-sender.groovy [new file with mode: 0755]
third-party/button-controller-for-hlgs.groovy [new file with mode: 0755]
third-party/camera-motion.groovy [new file with mode: 0755]
third-party/circadian-daylight.groovy [new file with mode: 0755]
third-party/ecobeeAwayFromHome.groovy [new file with mode: 0755]
third-party/ecobeeChangeMode.groovy [new file with mode: 0755]
third-party/ecobeeControlPlug.groovy [new file with mode: 0755]
third-party/ecobeeGenerateMonthlyStats.groovy [new file with mode: 0755]
third-party/ecobeeGenerateStats.groovy [new file with mode: 0755]
third-party/ecobeeGenerateWeeklyStats.groovy [new file with mode: 0755]
third-party/ecobeeGetTips.groovy [new file with mode: 0755]
third-party/ecobeeManageClimate.groovy [new file with mode: 0755]
third-party/ecobeeManageGroup.groovy [new file with mode: 0755]
third-party/ecobeeManageVacation.groovy [new file with mode: 0755]
third-party/ecobeeResumeProg.groovy [new file with mode: 0755]
third-party/ecobeeSetClimate.groovy [new file with mode: 0755]
third-party/ecobeeSetFanMinOnTime.groovy [new file with mode: 0755]
third-party/evohome-connect.groovy [new file with mode: 0755]
third-party/garage-switch.groovy [new file with mode: 0755]
third-party/groveStreams.groovy [new file with mode: 0755]
third-party/hue-lights-and-groups-and-scenes-oh-my.groovy [new file with mode: 0755]
third-party/hvac-auto-off.smartapp.groovy [new file with mode: 0755]
third-party/influxdb-logger.groovy [new file with mode: 0755]
third-party/initial-state-event-sender.groovy [new file with mode: 0755]
third-party/initialstate-smart-app-v1.2.0.groovy [new file with mode: 0755]
third-party/loft.groovy [new file with mode: 0755]
third-party/unbuffered-event-sender.groovy [new file with mode: 0755]

index facac0d..59fbd9d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
-# smartthings_app
\ No newline at end of file
+# smartthings_app
+
+Official and third-party SmartThings applications downloaded/copied from IoTBench.
+
+https://github.com/IoTBench/IoTBench-test-suite
diff --git a/official/alfred-workflow.groovy b/official/alfred-workflow.groovy
new file mode 100755 (executable)
index 0000000..3170ee5
--- /dev/null
@@ -0,0 +1,194 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Alfred Workflow
+ *
+ *  Author: SmartThings
+ */
+
+definition(
+    name: "Alfred Workflow",
+    namespace: "vlaminck",
+    author: "SmartThings",
+    description: "This SmartApp allows you to interact with the things in your physical graph through Alfred.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app@2x.png",
+    oauth: [displayName: "SmartThings Alfred Workflow", displayLink: ""]
+)
+
+preferences {
+       section("Allow Alfred to Control These Things...") {
+               input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
+        input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
+       }
+}
+
+mappings {
+       path("/switches") {
+               action: [
+                       GET: "listSwitches",
+                       PUT: "updateSwitches"
+               ]
+       }
+       path("/switches/:id") {
+               action: [
+                       GET: "showSwitch",
+                       PUT: "updateSwitch"
+               ]
+       }
+    path("/locks") {
+               action: [
+                       GET: "listLocks",
+                       PUT: "updateLocks"
+               ]
+       }
+       path("/locks/:id") {
+               action: [
+                       GET: "showLock",
+                       PUT: "updateLock"
+               ]
+       }
+}
+
+def installed() {}
+
+def updated() {}
+
+def listSwitches() {
+       switches.collect { device(it,"switch") }
+}
+void updateSwitches() {
+       updateAll(switches)
+}
+def showSwitch() {
+       show(switches, "switch")
+}
+void updateSwitch() {
+       update(switches)
+}
+
+def listLocks() {
+       locks.collect { device(it, "lock") }
+}
+void updateLocks() {
+       updateAll(locks)
+}
+def showLock() {
+       show(locks, "lock")
+}
+void updateLock() {
+       update(locks)
+}
+
+private void updateAll(devices) {
+       def command = request.JSON?.command
+       def type = params.param1
+       if (!devices) {
+               httpError(404, "Devices not found")
+       }
+       
+       if (command){
+               devices.each { device ->
+                       executeCommand(device, type, command)
+               }
+       }
+}
+
+private void update(devices) {
+       log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
+       def command = request.JSON?.command
+       def type = params.param1
+       def device = devices?.find { it.id == params.id }
+
+       if (!device) {
+               httpError(404, "Device not found")
+       }
+
+       if (command) {
+               executeCommand(device, type, command)
+       }
+}
+
+/**
+ * Validating the command passed by the user based on capability.
+ * @return boolean
+ */
+def validateCommand(device, deviceType, command) {
+       def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
+       def currentDeviceCapability = getCapabilityName(deviceType)
+       if (capabilityCommands[currentDeviceCapability]) {
+               return command in capabilityCommands[currentDeviceCapability] ? true : false
+       } else {
+               // Handling other device types here, which don't accept commands
+               httpError(400, "Bad request.")
+       }
+}
+
+/**
+ * Need to get the attribute name to do the lookup. Only
+ * doing it for the device types which accept commands
+ * @return attribute name of the device type
+ */
+def getCapabilityName(type) {
+    switch(type) {
+               case "switches":
+                       return "Switch"
+               case "locks":
+                       return "Lock"
+               default:
+                       return type
+       }
+}
+
+/**
+ * Constructing the map over here of
+ * supported commands by device capability
+ * @return a map of device capability -> supported commands
+ */
+def getDeviceCapabilityCommands(deviceCapabilities) {
+       def map = [:]
+       deviceCapabilities.collect {
+               map[it.name] = it.commands.collect{ it.name.toString() }
+       }
+       return map
+}
+
+/**
+ * Validates and executes the command
+ * on the device or devices
+ */
+def executeCommand(device, type, command) {
+       if (validateCommand(device, type, command)) {
+               device."$command"()
+       } else {
+               httpError(403, "Access denied. This command is not supported by current capability.")
+       }       
+}
+
+private show(devices, name) {
+       def device = devices.find { it.id == params.id }
+       if (!device) {
+               httpError(404, "Device not found")
+       }
+       else {
+               def s = device.currentState(name)
+               [id: device.id, label: device.displayName, name: device.displayName, state: s]
+       }
+}
+
+private device(it, name) {
+       if (it) {
+               def s = it.currentState(name)
+               [id: it.id, label: it.displayName, name: it.displayName, state: s]
+    }
+}
diff --git a/official/auto-humidity-vent.groovy b/official/auto-humidity-vent.groovy
new file mode 100755 (executable)
index 0000000..a0d4544
--- /dev/null
@@ -0,0 +1,277 @@
+/**
+ *  Auto Humidity Vent
+ *
+ *  Copyright 2014 Jonathan Andersson
+ *
+ *  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.
+ *
+ */
+definition (
+
+    name: "Auto Humidity Vent",
+    namespace: "jonathan-a",
+    author: "Jonathan Andersson",
+    description: "When the humidity reaches a specified level, activate one or more vent fans until the humidity is reduced to a specified level.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn.png",
+    iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn@2x.png"
+
+)
+
+
+preferences {
+
+       section("Enable / Disable the following functionality:") {
+        input "app_enabled", "bool", title: "Auto Humidity Vent", required:true, defaultValue:true
+        input "fan_control_enabled", "bool", title: "Vent Fan Control", required:true, defaultValue:true
+       }
+
+       section("Choose a humidity sensor...") {
+               input "humidity_sensor", "capability.relativeHumidityMeasurement", title: "Humidity Sensor", required: true
+       }
+       section("Enter the relative humudity level (%) above which the vent fans will activate:") {
+               input "humidity_a", "number", title: "Humidity Activation Level", required: true, defaultValue:70
+       }
+       section("Enter the relative humudity level (%) below which the vent fans will deactivate:") {
+               input "humidity_d", "number", title: "Humidity Deactivation Level", required: true, defaultValue:65
+       }
+
+       section("Select the vent fans to control...") {
+               input "fans", "capability.switch", title: "Vent Fans", multiple: true, required: true
+       }
+
+       section("Select the vent fan energy meters to monitor...") {
+               input "emeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
+               input "price_kwh", "decimal", title: "Cost in cents per kWh (12 is US avg)", required: true, defaultValue:12
+       }
+
+       section("Set notification options:") {
+        input "sendPushMessage", "bool", title: "Push notifications", required:true, defaultValue:false
+        input "phone", "phone", title: "Send text messages to", required: false
+    }
+
+}
+
+
+def installed() {
+
+       log.debug "${app.label} installed with settings: ${settings}"
+
+       state.app_enabled = false
+       state.fan_control_enabled = false
+
+       state.fansOn = false
+       state.fansOnTime = now()
+       state.fansLastRunTime = 0
+
+       initialize()
+
+}
+
+
+def uninstalled()
+{
+
+       send("${app.label} uninstalled.")
+    
+       state.app_enabled = false
+
+       set_fans(false)
+
+       state.fan_control_enabled = false
+
+}
+
+
+def updated() {
+
+       log.debug "${app.label} updated with settings: ${settings}"
+
+       unsubscribe()
+
+       initialize()
+
+}
+
+
+def initialize() {
+
+       if (settings.fan_control_enabled) {
+               if(state.fan_control_enabled == false) {
+                       send("Vent Fan Control Enabled.")
+        } else {
+               log.debug "Vent Fan Control Enabled."
+        }
+
+               state.fan_control_enabled = true
+       } else {
+               if(state.fan_control_enabled == true) {
+                       send("Vent Fan Control Disabled.")
+        } else {
+               log.debug "Vent Fan Control Disabled."
+        }
+
+               state.fan_control_enabled = false
+       }
+
+       if (settings.app_enabled) {
+               if(state.app_enabled == false) {
+                       send("${app.label} Enabled.")
+        } else {
+               log.debug "${app.label} Enabled."
+        }
+
+               subscribe(humidity_sensor, "humidity", "handleThings")
+
+               state.app_enabled = true
+       } else {
+               if(state.app_enabled == true) {
+                       send("${app.label} Disabled.")
+        } else {
+               log.debug "${app.label} Disabled."
+        }
+
+               state.app_enabled = false
+    }
+
+    handleThings()
+
+}
+
+
+def handleThings(evt) {
+
+
+       log.debug "handleThings()"
+
+       if(evt) {
+               log.debug "$evt.descriptionText"
+    }
+        
+       def h = 0.0 as BigDecimal
+       if (settings.app_enabled) {
+           h = settings.humidity_sensor.currentValue('humidity')
+/*
+               //Simulator is broken and requires this work around for testing.        
+               if (settings.humidity_sensor.latestState('humidity')) {
+               log.debug settings.humidity_sensor.latestState('humidity').stringValue[0..-2]
+               h = settings.humidity_sensor.latestState('humidity').stringValue[0..-2].toBigDecimal()
+        } else {
+               h = 20
+        }        
+*/
+       }
+
+       log.debug "Humidity: $h%, Activate: $humidity_a%, Deactivate: $humidity_d%"
+
+    def activateFans = false
+    def deactivateFans = false
+    
+       if (settings.app_enabled) {
+        
+               if (state.fansOn) {
+            if (h > humidity_d) {
+                log.debug "Humidity not sufficient to deactivate vent fans: $h > $humidity_d"
+            } else {
+                log.debug "Humidity sufficient to deactivate vent fans: $h <= $humidity_d"
+                deactivateFans = true
+            }
+        } else {
+            if (h < humidity_a) {
+                log.debug "Humidity not sufficient to activate vent fans: $h < $humidity_a"
+            } else {
+                log.debug "Humidity sufficient to activate vent fans: $h >= $humidity_a"
+                activateFans = true
+            }
+        }
+       }
+
+       if(activateFans) {
+               set_fans(true)
+    }
+       if(deactivateFans) {
+               set_fans(false)
+    }
+
+}
+
+
+def set_fans(fan_state) {
+
+       if (fan_state) {
+       if (state.fansOn == false) {
+            send("${app.label} fans On.")
+            state.fansOnTime = now()
+            if (settings.fan_control_enabled) {
+                if (emeters) {
+                    emeters.reset()
+                }
+                fans.on()
+            } else {
+                send("${app.label} fan control is disabled.")
+            }
+            state.fansOn = true
+               } else {
+            log.debug "${app.label} fans already On."
+               }        
+    } else {
+       if (state.fansOn == true) {
+               send("${app.label} fans Off.")
+            state.fansLastRunTime = (now() - state.fansOnTime)
+
+                   BigInteger ms = new java.math.BigInteger(state.fansLastRunTime)
+                       int seconds = (BigInteger) (((BigInteger) ms / (1000I))                  % 60I)
+                       int minutes = (BigInteger) (((BigInteger) ms / (1000I * 60I))            % 60I)
+                       int hours   = (BigInteger) (((BigInteger) ms / (1000I * 60I * 60I))      % 24I)
+                       int days    = (BigInteger)  ((BigInteger) ms / (1000I * 60I * 60I * 24I))
+
+                       def sb = String.format("${app.label} cycle: %d:%02d:%02d:%02d", days, hours, minutes, seconds)
+                       
+                   send(sb)
+
+                       if (settings.fan_control_enabled) {
+               fans.off()
+                if (emeters) {
+                    log.debug emeters.currentValue('energy')
+                    //TODO: How to ensure latest (most accurate) energy reading?
+                    emeters.poll() //[configure, refresh, on, off, poll, reset]
+//                    emeters.refresh() //[configure, refresh, on, off, poll, reset]
+                    state.fansLastRunEnergy = emeters.currentValue('energy').sum()
+                    state.fansLastRunCost = ((state.fansLastRunEnergy * price_kwh) / 100.0) 
+                    send("${app.label} cycle: ${state.fansLastRunEnergy}kWh @ \$${state.fansLastRunCost}")
+                }
+               } else {
+               send("${app.label} fan control is disabled.")
+            }
+               state.fansOn = false
+            state.fansHoldoff = now()
+        } else {
+            log.debug "${app.label} fans already Off."
+        }
+    }
+
+}
+
+
+private send(msg) {
+
+       if (sendPushMessage) {
+        sendPush(msg)
+    }
+
+    if (phone) {
+        sendSms(phone, msg)
+    }
+
+    log.debug(msg)
+}
+
diff --git a/official/beacon-control.groovy b/official/beacon-control.groovy
new file mode 100755 (executable)
index 0000000..007b455
--- /dev/null
@@ -0,0 +1,317 @@
+/**
+ *  Beacon Control
+ *
+ *  Copyright 2014 Physical Graph Corporation
+ *
+ *  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.
+ *
+ */
+definition(
+       name: "Beacon Control",
+       category: "SmartThings Internal",
+       namespace: "smartthings",
+       author: "SmartThings",
+       description: "Execute a Hello, Home phrase, turn on or off some lights, and/or lock or unlock your door when you enter or leave a monitored region",
+       iconUrl: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol.png",
+       iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol@2x.png"
+)
+
+preferences {
+       page(name: "mainPage")
+       
+       page(name: "timeIntervalInput", title: "Only during a certain time") {
+               section {
+                       input "starting", "time", title: "Starting", required: false
+                       input "ending", "time", title: "Ending", required: false
+               }
+       }
+}
+
+def mainPage() {
+       dynamicPage(name: "mainPage", install: true, uninstall: true) {
+
+               section("Where do you want to watch?") {
+                       input name: "beacons", type: "capability.beacon", title: "Select your beacon(s)", 
+                               multiple: true, required: true
+               }
+
+               section("Who do you want to watch for?") {
+                       input name: "phones", type: "device.mobilePresence", title: "Select your phone(s)", 
+                               multiple: true, required: true
+               }
+
+               section("What do you want to do on arrival?") {
+                       input name: "arrivalPhrase", type: "enum", title: "Execute a phrase", 
+                               options: listPhrases(), required: false
+                       input "arrivalOnSwitches", "capability.switch", title: "Turn on some switches", 
+                               multiple: true, required: false
+                       input "arrivalOffSwitches", "capability.switch", title: "Turn off some switches", 
+                               multiple: true, required: false
+                       input "arrivalLocks", "capability.lock", title: "Unlock the door",
+                               multiple: true, required: false
+               }
+
+               section("What do you want to do on departure?") {
+                       input name: "departPhrase", type: "enum", title: "Execute a phrase", 
+                               options: listPhrases(), required: false
+                       input "departOnSwitches", "capability.switch", title: "Turn on some switches", 
+                               multiple: true, required: false
+                       input "departOffSwitches", "capability.switch", title: "Turn off some switches", 
+                               multiple: true, required: false
+                       input "departLocks", "capability.lock", title: "Lock the door",
+                               multiple: true, required: false
+               }
+
+               section("Do you want to be notified?") {
+                       input "pushNotification", "bool", title: "Send a push notification"
+                       input "phone", "phone", title: "Send a text message", description: "Tap to enter phone number", 
+                               required: false
+               }
+
+               section {
+                       label title: "Give your automation a name", description: "e.g. Goodnight Home, Wake Up"
+               }
+
+               def timeLabel = timeIntervalLabel()
+               section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
+                       href "timeIntervalInput", title: "Only during a certain time", 
+                               description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
+
+                       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
+               }
+       }
+}
+
+// Lifecycle management
+def installed() {
+       log.debug "<beacon-control> Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "<beacon-control> Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(beacons, "presence", beaconHandler)
+}
+
+// Event handlers
+def beaconHandler(evt) {
+       log.debug "<beacon-control> beaconHandler: $evt"
+
+       if (allOk) {
+               def data = new groovy.json.JsonSlurper().parseText(evt.data)
+                // removed logging of device names. can be added back for debugging
+               //log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
+
+               def beaconName = getBeaconName(evt)
+                // removed logging of device names. can be added back for debugging
+               //log.debug "<beacon-control> beaconName: $beaconName"
+
+               def phoneName = getPhoneName(data)
+                // removed logging of device names. can be added back for debugging
+               //log.debug "<beacon-control> phoneName: $phoneName"
+               if (phoneName != null) {
+            def action = data.presence == "1" ? "arrived" : "left"
+            def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"
+
+            if (action == "arrived") {
+                msg = arriveActions(msg)
+            }
+            else if (action == "left") {
+                msg = departActions(msg)
+            }
+            log.debug "<beacon-control> msg: $msg"
+
+            if (pushNotification || phone) {
+                def options = [
+                    method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"),
+                    phone: phone
+                ]
+                sendNotification(msg, options)
+            }
+        }
+       }
+}
+
+// Helpers
+private arriveActions(msg) {
+       if (arrivalPhrase || arrivalOnSwitches || arrivalOffSwitches || arrivalLocks) msg += ", so"
+       
+       if (arrivalPhrase) {
+               log.debug "<beacon-control> executing: $arrivalPhrase"
+               executePhrase(arrivalPhrase)
+               msg += " ${prefix('executed')} $arrivalPhrase."
+       }
+       if (arrivalOnSwitches) {
+               log.debug "<beacon-control> turning on: $arrivalOnSwitches"
+               arrivalOnSwitches.on()
+               msg += " ${prefix('turned')} ${list(arrivalOnSwitches)} on."
+       }
+       if (arrivalOffSwitches) {
+               log.debug "<beacon-control> turning off: $arrivalOffSwitches"
+               arrivalOffSwitches.off()
+               msg += " ${prefix('turned')} ${list(arrivalOffSwitches)} off."
+       }
+       if (arrivalLocks) {
+               log.debug "<beacon-control> unlocking: $arrivalLocks"
+               arrivalLocks.unlock()
+               msg += " ${prefix('unlocked')} ${list(arrivalLocks)}."
+       }
+       msg
+}
+
+private departActions(msg) {
+       if (departPhrase || departOnSwitches || departOffSwitches || departLocks) msg += ", so"
+       
+       if (departPhrase) {
+               log.debug "<beacon-control> executing: $departPhrase"
+               executePhrase(departPhrase)
+               msg += " ${prefix('executed')} $departPhrase."
+       }
+       if (departOnSwitches) {
+               log.debug "<beacon-control> turning on: $departOnSwitches"
+               departOnSwitches.on()
+               msg += " ${prefix('turned')} ${list(departOnSwitches)} on."
+       }
+       if (departOffSwitches) {
+               log.debug "<beacon-control> turning off: $departOffSwitches"
+               departOffSwitches.off()
+               msg += " ${prefix('turned')} ${list(departOffSwitches)} off."
+       }
+       if (departLocks) {
+               log.debug "<beacon-control> unlocking: $departLocks"
+               departLocks.lock()
+               msg += " ${prefix('locked')} ${list(departLocks)}."
+       }
+       msg
+}
+
+private prefix(word) {
+       def result
+       def index = settings.prefixIndex == null ? 0 : settings.prefixIndex + 1
+       switch (index) {
+               case 0:
+                       result = "I $word"
+                       break
+               case 1:
+                       result = "I also $word"
+                       break
+               case 2:
+                       result = "And I $word"
+                       break
+               default:
+                       result = "And $word"
+                       break
+       }
+
+       settings.prefixIndex = index
+       log.trace "prefix($word'): $result"
+       result
+}
+
+private listPhrases() {
+       location.helloHome.getPhrases().label
+}
+
+private executePhrase(phraseName) {
+       if (phraseName) {
+               location.helloHome.execute(phraseName)
+               log.debug "<beacon-control> executed phrase: $phraseName"
+       }
+}
+
+private getBeaconName(evt) {
+       def beaconName = beacons.find { b -> b.id == evt.deviceId }
+       return beaconName
+}
+
+private getPhoneName(data) {    
+       def phoneName = phones.find { phone ->
+               // Work around DNI bug in data
+               def pParts = phone.deviceNetworkId.split('\\|')
+               def dParts = data.dni.split('\\|')
+        pParts[0] == dParts[0]
+       }
+       return phoneName
+}
+
+private hideOptionsSection() {
+       (starting || ending || days || modes) ? false : true
+}
+
+private getAllOk() {
+       modeOk && daysOk && timeOk
+}
+
+private getModeOk() {
+       def result = !modes || modes.contains(location.mode)
+       log.trace "<beacon-control> modeOk = $result"
+       result
+}
+
+private getDaysOk() {
+       def result = true
+       if (days) {
+               def df = new java.text.SimpleDateFormat("EEEE")
+               if (location.timeZone) {
+                       df.setTimeZone(location.timeZone)
+               }
+               else {
+                       df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+               }
+               def day = df.format(new Date())
+               result = days.contains(day)
+       }
+       log.trace "<beacon-control> daysOk = $result"
+       result
+}
+
+private getTimeOk() {
+       def result = true
+       if (starting && ending) {
+               def currTime = now()
+               def start = timeToday(starting, location?.timeZone).time
+               def stop = timeToday(ending, location?.timeZone).time
+               result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
+       }
+       log.trace "<beacon-control> timeOk = $result"
+       result
+}
+
+private hhmm(time, fmt = "h:mm a") {
+       def t = timeToday(time, location.timeZone)
+       def f = new java.text.SimpleDateFormat(fmt)
+       f.setTimeZone(location.timeZone ?: timeZone(time))
+       f.format(t)
+}
+
+private timeIntervalLabel() {
+       (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
+}
+
+private list(List names) {
+       switch (names.size()) {
+               case 0:
+                       return null
+               case 1:
+                       return names[0]
+               case 2:
+                       return "${names[0]} and ${names[1]}"
+               default:
+                       return "${names[0..-2].join(', ')}, and ${names[-1]}"
+       }
+}
diff --git a/official/beaconthings-manager.groovy b/official/beaconthings-manager.groovy
new file mode 100755 (executable)
index 0000000..3254f32
--- /dev/null
@@ -0,0 +1,147 @@
+/**
+ *  BeaconThing Manager
+ *
+ *  Copyright 2015 obycode
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "BeaconThings Manager",
+    namespace: "com.obycode",
+    author: "obycode",
+    description: "SmartApp to interact with the BeaconThings iOS app. Use this app to integrate iBeacons into your smart home.",
+    category: "Convenience",
+    iconUrl: "http://beaconthingsapp.com/images/Icon-60.png",
+    iconX2Url: "http://beaconthingsapp.com/images/Icon-60@2x.png",
+    iconX3Url: "http://beaconthingsapp.com/images/Icon-60@3x.png",
+    oauth: true)
+
+
+preferences {
+       section("Allow BeaconThings to talk to your home") {
+
+       }
+}
+
+def installed() {
+  log.debug "Installed with settings: ${settings}"
+
+  initialize()
+}
+
+def initialize() {
+}
+
+def uninstalled() {
+  removeChildDevices(getChildDevices())
+}
+
+mappings {
+  path("/beacons") {
+    action: [
+    DELETE: "clearBeacons",
+    POST:   "addBeacon"
+    ]
+  }
+
+  path("/beacons/:id") {
+    action: [
+    PUT: "updateBeacon",
+    DELETE: "deleteBeacon"
+    ]
+  }
+}
+
+void clearBeacons() {
+  removeChildDevices(getChildDevices())
+}
+
+void addBeacon() {
+  def beacon = request.JSON?.beacon
+  if (beacon) {
+    def beaconId = "BeaconThings"
+    if (beacon.major) {
+      beaconId = "$beaconId-${beacon.major}"
+      if (beacon.minor) {
+        beaconId = "$beaconId-${beacon.minor}"
+      }
+    }
+    log.debug "adding beacon $beaconId"
+    def d = addChildDevice("com.obycode", "BeaconThing", beaconId,  null, [label:beacon.name, name:"BeaconThing", completedSetup: true])
+    log.debug "addChildDevice returned $d"
+
+    if (beacon.present) {
+      d.arrive(beacon.present)
+    }
+    else if (beacon.presence) {
+      d.setPresence(beacon.presence)
+    }
+  }
+}
+
+void updateBeacon() {
+  log.debug "updating beacon ${params.id}"
+  def beaconDevice = getChildDevice(params.id)
+  // def children = getChildDevices()
+  // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
+  if (!beaconDevice) {
+    log.debug "Beacon not found directly"
+    def children = getChildDevices()
+    beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
+    if (!beaconDevice) {
+      log.debug "Beacon not found in list either"
+      return
+    }
+  }
+
+  // This could be just updating the presence
+  def presence = request.JSON?.presence
+  if (presence) {
+    log.debug "Setting ${beaconDevice.label} to $presence"
+    beaconDevice.setPresence(presence)
+  }
+
+  // It could be someone arriving
+  def arrived = request.JSON?.arrived
+  if (arrived) {
+    log.debug "$arrived arrived at ${beaconDevice.label}"
+    beaconDevice.arrived(arrived)
+  }
+
+  // It could be someone left
+  def left = request.JSON?.left
+  if (left) {
+    log.debug "$left left ${beaconDevice.label}"
+    beaconDevice.left(left)
+  }
+
+  // or it could be updating the name
+  def beacon = request.JSON?.beacon
+  if (beacon) {
+    beaconDevice.label = beacon.name
+  }
+}
+
+void deleteBeacon() {
+  log.debug "deleting beacon ${params.id}"
+  deleteChildDevice(params.id)
+  // def children = getChildDevices()
+  // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
+  // if (beaconDevice) {
+  //   deleteChildDevice(beaconDevice.deviceNetworkId)
+  // }
+}
+
+private removeChildDevices(delete) {
+  delete.each {
+    deleteChildDevice(it.deviceNetworkId)
+  }
+}
diff --git a/official/big-turn-off.groovy b/official/big-turn-off.groovy
new file mode 100755 (executable)
index 0000000..f5fae99
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Big Turn OFF
+ *
+ *  Author: SmartThings
+ */
+definition(
+    name: "Big Turn OFF",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn your lights off when the SmartApp is tapped or activated",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
+)
+
+preferences {
+       section("When I touch the app, turn off...") {
+               input "switches", "capability.switch", multiple: true
+       }
+}
+
+def installed()
+{
+       subscribe(location, changedLocationMode)
+       subscribe(app, appTouch)
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(location, changedLocationMode)
+       subscribe(app, appTouch)
+}
+
+def changedLocationMode(evt) {
+       log.debug "changedLocationMode: $evt"
+       switches?.off()
+}
+
+def appTouch(evt) {
+       log.debug "appTouch: $evt"
+       switches?.off()
+}
diff --git a/official/big-turn-on.groovy b/official/big-turn-on.groovy
new file mode 100755 (executable)
index 0000000..f395ab3
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Big Turn ON
+ *
+ *  Author: SmartThings
+ */
+
+definition(
+    name: "Big Turn ON",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn your lights on when the SmartApp is tapped or activated.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
+)
+
+preferences {
+       section("When I touch the app, turn on...") {
+               input "switches", "capability.switch", multiple: true
+       }
+}
+
+def installed()
+{
+       subscribe(location, changedLocationMode)
+       subscribe(app, appTouch)
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(location, changedLocationMode)
+       subscribe(app, appTouch)
+}
+
+def changedLocationMode(evt) {
+       log.debug "changedLocationMode: $evt"
+       switches?.on()
+}
+
+def appTouch(evt) {
+       log.debug "appTouch: $evt"
+       switches?.on()
+}
diff --git a/official/bon-voyage.groovy b/official/bon-voyage.groovy
new file mode 100755 (executable)
index 0000000..cb9593d
--- /dev/null
@@ -0,0 +1,147 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Bon Voyage
+ *
+ *  Author: SmartThings
+ *  Date: 2013-03-07
+ *
+ *  Monitors a set of presence detectors and triggers a mode change when everyone has left.
+ */
+
+definition(
+    name: "Bon Voyage",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Monitors a set of SmartSense Presence tags or smartphones and triggers a mode change when everyone has left.  Used in conjunction with Big Turn Off or Make It So to turn off lights, appliances, adjust the thermostat, turn on security apps, and more.",
+    category: "Mode Magic",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
+)
+
+preferences {
+       section("When all of these people leave home") {
+               input "people", "capability.presenceSensor", multiple: true
+       }
+       section("Change to this mode") {
+               input "newMode", "mode", title: "Mode?"
+       }
+       section("False alarm threshold (defaults to 10 min)") {
+               input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
+       }
+       section( "Notifications" ) {
+               input("recipients", "contact", title: "Send notifications to", required: false) {
+                       input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
+                       input "phone", "phone", title: "Send a Text Message?", required: false
+               }
+       }
+
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+        // commented out log statement because presence sensor label could contain user's name
+       //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
+       subscribe(people, "presence", presence)
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+        // commented out log statement because presence sensor label could contain user's name
+       //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
+       unsubscribe()
+       subscribe(people, "presence", presence)
+}
+
+def presence(evt)
+{
+       log.debug "evt.name: $evt.value"
+       if (evt.value == "not present") {
+               if (location.mode != newMode) {
+                       log.debug "checking if everyone is away"
+                       if (everyoneIsAway()) {
+                               log.debug "starting sequence"
+                               runIn(findFalseAlarmThreshold() * 60, "takeAction", [overwrite: false])
+                       }
+               }
+               else {
+                       log.debug "mode is the same, not evaluating"
+               }
+       }
+       else {
+               log.debug "present; doing nothing"
+       }
+}
+
+def takeAction()
+{
+       if (everyoneIsAway()) {
+               def threshold = 1000 * 60 * findFalseAlarmThreshold() - 1000
+               def awayLongEnough = people.findAll { person ->
+                       def presenceState = person.currentState("presence")
+                       if (!presenceState) {
+                               // This device has yet to check in and has no presence state, treat it as not away long enough
+                               return false
+                       }
+                       def elapsed = now() - presenceState.rawDateCreated.time
+                       elapsed >= threshold
+               }
+               log.debug "Found ${awayLongEnough.size()} out of ${people.size()} person(s) who were away long enough"
+               if (awayLongEnough.size() == people.size()) {
+                       // TODO -- uncomment when app label is available
+                       def message = "SmartThings changed your mode to '${newMode}' because everyone left home"
+                       log.info message
+                       send(message)
+                       setLocationMode(newMode)
+               } else {
+                       log.debug "not everyone has been away long enough; doing nothing"
+               }
+       } else {
+       log.debug "not everyone is away; doing nothing"
+    }
+}
+
+private everyoneIsAway()
+{
+       def result = true
+       for (person in people) {
+               if (person.currentPresence == "present") {
+                       result = false
+                       break
+               }
+       }
+       log.debug "everyoneIsAway: $result"
+       return result
+}
+
+private send(msg) {
+       if (location.contactBookEnabled) {
+        log.debug("sending notifications to: ${recipients?.size()}")
+               sendNotificationToContacts(msg, recipients)
+       }
+       else  {
+               if (sendPushMessage != "No") {
+                       log.debug("sending push message")
+                       sendPush(msg)
+               }
+
+               if (phone) {
+                       log.debug("sending text message")
+                       sendSms(phone, msg)
+               }
+       }
+       log.debug msg
+}
+
+private findFalseAlarmThreshold() {
+       (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold : 10
+}
diff --git a/official/bose-soundtouch-connect.groovy b/official/bose-soundtouch-connect.groovy
new file mode 100755 (executable)
index 0000000..883c6e8
--- /dev/null
@@ -0,0 +1,609 @@
+/**
+ *  Bose SoundTouch (Connect)
+ *
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ */
+ definition(
+    name: "Bose SoundTouch (Connect)",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Control your Bose SoundTouch speakers",
+    category: "SmartThings Labs",
+    iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png",
+    iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png",
+    iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png",
+    singleInstance: true
+)
+
+preferences {
+    page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
+}
+
+/**
+ * Get the urn that we're looking for
+ *
+ * @return URN which we are looking for
+ *
+ * @todo This + getUSNQualifier should be one and should use regular expressions
+ */
+def getDeviceType() {
+    return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
+}
+
+/**
+ * If not null, returns an additional qualifier for ssdUSN
+ * to avoid spamming the network
+ *
+ * @return Additional qualifier OR null if not needed
+ */
+def getUSNQualifier() {
+    return "uuid:BO5EBO5E-F00D-F00D-FEED-"
+}
+
+/**
+ * Get the name of the new device to instantiate in the user's smartapps
+ * This must be an app owned by the namespace (see #getNameSpace).
+ *
+ * @return name
+ */
+def getDeviceName() {
+    return "Bose SoundTouch"
+}
+
+/**
+ * Returns the namespace this app and siblings use
+ *
+ * @return namespace
+ */
+def getNameSpace() {
+    return "smartthings"
+}
+
+/**
+ * The deviceDiscovery page used by preferences. Will automatically
+ * make calls to the underlying discovery mechanisms as well as update
+ * whenever new devices are discovered AND verified.
+ *
+ * @return a dynamicPage() object
+ */
+def deviceDiscovery()
+{
+    if(canInstallLabs())
+    {
+        def refreshInterval = 3 // Number of seconds between refresh
+        int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
+        state.deviceRefreshCount = deviceRefreshCount + refreshInterval
+
+        def devices = getSelectableDevice()
+        def numFound = devices.size() ?: 0
+
+        // Make sure we get location updates (contains LAN data such as SSDP results, etc)
+        subscribeNetworkEvents()
+
+        //device discovery request every 15s
+        if((deviceRefreshCount % 15) == 0) {
+            discoverDevices()
+        }
+
+        // Verify request every 3 seconds except on discoveries
+        if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
+            verifyDevices()
+        }
+
+        log.trace "Discovered devices: ${devices}"
+
+        return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
+            section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
+                input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices, submitOnChange: true
+            }
+        }
+    }
+    else
+    {
+        def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
+
+To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
+
+        return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
+            section("Upgrade") {
+                paragraph "$upgradeNeeded"
+            }
+        }
+    }
+}
+
+/**
+ * Called by SmartThings Cloud when user has selected device(s) and
+ * pressed "Install".
+ */
+def installed() {
+    log.trace "Installed with settings: ${settings}"
+    initialize()
+}
+
+/**
+ * Called by SmartThings Cloud when app has been updated
+ */
+def updated() {
+    log.trace "Updated with settings: ${settings}"
+    unsubscribe()
+    initialize()
+}
+
+/**
+ * Called by SmartThings Cloud when user uninstalls the app
+ *
+ * We don't need to manually do anything here because any children
+ * are automatically removed upon the removal of the parent.
+ *
+ * Only time to do anything here is when you need to notify
+ * the remote end. And even then you're discouraged from removing
+ * the children manually.
+ */
+def uninstalled() {
+}
+
+/**
+ * If user has selected devices, will start monitoring devices
+ * for changes (new address, port, etc...)
+ */
+def initialize() {
+    log.trace "initialize()"
+    state.subscribe = false
+    if (selecteddevice) {
+        addDevice()
+        refreshDevices()
+        subscribeNetworkEvents(true)
+    }
+}
+
+/**
+ * Adds the child devices based on the user's selection
+ *
+ * Uses selecteddevice defined in the deviceDiscovery() page
+ */
+def addDevice(){
+    def devices = getVerifiedDevices()
+    def devlist
+    log.trace "Adding childs"
+
+    // If only one device is selected, we don't get a list (when using simulator)
+    if (!(selecteddevice instanceof List)) {
+        devlist = [selecteddevice]
+    } else {
+        devlist = selecteddevice
+    }
+
+    log.trace "These are being installed: ${devlist}"
+
+    devlist.each { dni ->
+        def d = getChildDevice(dni)
+        if(!d) {
+            def newDevice = devices.find { (it.value.mac) == dni }
+            def deviceName = newDevice?.value.name
+            if (!deviceName)
+                deviceName = getDeviceName() + "[${newDevice?.value.name}]"
+            d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
+            d.boseSetDeviceID(newDevice.value.deviceID)
+            log.trace "Created ${d.displayName} with id $dni"
+            // sync DTH with device, done here as it currently don't work from the DTH's installed() method
+            d.refresh()
+        } else {
+            log.trace "${d.displayName} with id $dni already exists"
+        }
+    }
+}
+
+/**
+ * Resolves a DeviceNetworkId to an address. Primarily used by children
+ *
+ * @param dni Device Network id
+ * @return address or null
+ */
+def resolveDNI2Address(dni) {
+    def device = getVerifiedDevices().find { (it.value.mac) == dni }
+    if (device) {
+        return convertHexToIP(device.value.networkAddress)
+    }
+    return null
+}
+
+/**
+ * Joins a child to the "Play Everywhere" zone
+ *
+ * @param child The speaker joining the zone
+ * @return A list of maps with POST data
+ */
+def boseZoneJoin(child) {
+    log = child.log // So we can debug this function
+
+    def results = []
+    def result = [:]
+
+    // Find the master (if any)
+    def server = getChildDevices().find{ it.boseGetZone() == "server" }
+
+    if (server) {
+        log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
+        child.boseSetZone("client")
+
+        result['endpoint'] = "/setZone"
+        result['host'] = server.getDeviceIP() + ":8090"
+        result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
+        getChildDevices().each{ it ->
+            log.trace "child: " + child
+            log.trace "zone : " + it.boseGetZone()
+            if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
+                result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
+        }
+        result['body'] = result['body'] + '</zone>'
+    } else {
+        log.debug "boseJoinZone() No server, add it!"
+        result['endpoint'] = "/setZone"
+        result['host'] = child.getDeviceIP() + ":8090"
+        result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
+        result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
+        result['body'] = result['body'] + '</zone>'
+        child.boseSetZone("server")
+    }
+    results << result
+    return results
+}
+
+def boseZoneReset() {
+    getChildDevices().each{ it.boseSetZone(null) }
+}
+
+def boseZoneHasMaster() {
+    return getChildDevices().find{ it.boseGetZone() == "server" } != null
+}
+
+/**
+ * Removes a speaker from the play everywhere zone.
+ *
+ * @param child Which speaker is leaving
+ * @return a list of maps with POST data
+ */
+def boseZoneLeave(child) {
+    log = child.log // So we can debug this function
+
+    def results = []
+    def result = [:]
+
+    // First, tag us as a non-member
+    child.boseSetZone(null)
+
+    // Find the master (if any)
+    def server = getChildDevices().find{ it.boseGetZone() == "server" }
+
+    if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
+        log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
+        result['endpoint'] = "/removeZoneSlave"
+        result['host'] = server.getDeviceIP() + ":8090"
+        result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
+        result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
+        result['body'] = result['body'] + '</zone>'
+        results << result
+    } else {
+        log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
+        // Dismantle the entire thing, first send this to master
+        result['endpoint'] = "/removeZoneSlave"
+        result['host'] = child.getDeviceIP() + ":8090"
+        result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
+        getChildDevices().each{ dev ->
+            if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
+                result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
+        }
+        result['body'] = result['body'] + '</zone>'
+        results << result
+
+        // Also issue this to each individual client
+        getChildDevices().each{ dev ->
+            if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
+                log.trace "Additional device: " + dev
+                result['host'] = dev.getDeviceIP() + ":8090"
+                results << result
+            }
+        }
+    }
+
+    return results
+}
+
+/**
+ * Define our XML parsers
+ *
+ * @return mapping of root-node <-> parser function
+ */
+def getParsers() {
+    [
+        "root" : "parseDESC",
+        "info" : "parseINFO"
+    ]
+}
+
+/**
+ * Called when location has changed, contains information from
+ * network transactions. See deviceDiscovery() for where it is
+ * registered.
+ *
+ * @param evt Holds event information
+ */
+def onLocation(evt) {
+    // Convert the event into something we can use
+    def lanEvent = parseLanMessage(evt.description, true)
+    lanEvent << ["hub":evt?.hubId]
+
+    // Determine what we need to do...
+    if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
+        (getUSNQualifier() == null ||
+         lanEvent?.ssdpUSN?.contains(getUSNQualifier())
+        )
+       )
+    {
+        parseSSDP(lanEvent)
+    }
+    else if (
+        lanEvent.headers && lanEvent.body &&
+        lanEvent.headers."content-type"?.contains("xml")
+        )
+    {
+        def parsers = getParsers()
+        def xmlData = new XmlSlurper().parseText(lanEvent.body)
+
+        // Let each parser take a stab at it
+        parsers.each { node,func ->
+            if (xmlData.name() == node)
+                "$func"(xmlData)
+        }
+    }
+}
+
+/**
+ * Handles SSDP description file.
+ *
+ * @param xmlData
+ */
+private def parseDESC(xmlData) {
+    log.info "parseDESC()"
+
+    def devicetype = getDeviceType().toLowerCase()
+    def devicetxml = body.device.deviceType.text().toLowerCase()
+
+    // Make sure it's the type we want
+    if (devicetxml == devicetype) {
+        def devices = getDevices()
+        def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
+        if (device && !device.value?.verified) {
+            // Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
+            device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
+        } else {
+            log.error "parseDESC(): The xml file returned a device that didn't exist"
+        }
+    }
+}
+
+/**
+ * Handle BOSE <info></info> result. This is an alternative to
+ * using the SSDP description standard. Some of the speakers do
+ * not support SSDP description, so we need this as well.
+ *
+ * @param xmlData
+ */
+private def parseINFO(xmlData) {
+    log.info "parseINFO()"
+    def devicetype = getDeviceType().toLowerCase()
+
+    def deviceID = xmlData.attributes()['deviceID']
+    def device = getDevices().find {it?.key?.contains(deviceID)}
+    if (device && !device.value?.verified) {
+        device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true]
+    }
+}
+
+/**
+ * Handles SSDP discovery messages and adds them to the list
+ * of discovered devices. If it already exists, it will update
+ * the port and location (in case it was moved).
+ *
+ * @param lanEvent
+ */
+def parseSSDP(lanEvent) {
+    //SSDP DISCOVERY EVENTS
+    def USN = lanEvent.ssdpUSN.toString()
+    def devices = getDevices()
+
+    if (!(devices."${USN}")) {
+        //device does not exist
+        log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
+        devices << ["${USN}":lanEvent]
+    } else {
+        // update the values
+        def d = devices."${USN}"
+        if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
+            log.trace "parseSSDP() Updating device location (ip & port)"
+            d.networkAddress = lanEvent.networkAddress
+            d.deviceAddress = lanEvent.deviceAddress
+        }
+    }
+}
+
+/**
+ * Generates a Map object which can be used with a preference page
+ * to represent a list of devices detected and verified.
+ *
+ * @return Map with zero or more devices
+ */
+Map getSelectableDevice() {
+    def devices = getVerifiedDevices()
+    def map = [:]
+    devices.each {
+        def value = "${it.value.name}"
+        def key = it.value.mac
+        map["${key}"] = value
+    }
+    map
+}
+
+/**
+ * Starts the refresh loop, making sure to keep us up-to-date with changes
+ *
+ */
+private refreshDevices() {
+    discoverDevices()
+    verifyDevices()
+    runIn(300, "refreshDevices")
+}
+
+/**
+ * Starts a subscription for network events
+ *
+ * @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
+ */
+private subscribeNetworkEvents(force=false) {
+    if (force) {
+        unsubscribe()
+        state.subscribe = false
+    }
+
+    if(!state.subscribe) {
+        subscribe(location, null, onLocation, [filterEvents:false])
+        state.subscribe = true
+    }
+}
+
+/**
+ * Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType())
+ */
+private discoverDevices() {
+    log.trace "discoverDevice() Issuing SSDP request"
+    sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
+}
+
+/**
+ * Walks through the list of unverified devices and issues a verification
+ * request for each of them (basically calling verifyDevice() per unverified)
+ */
+private verifyDevices() {
+    def devices = getDevices().findAll { it?.value?.verified != true }
+
+    devices.each {
+        verifyDevice(
+            it?.value?.mac,
+            convertHexToIP(it?.value?.networkAddress),
+            convertHexToInt(it?.value?.deviceAddress),
+            it?.value?.ssdpPath
+        )
+    }
+}
+
+/**
+ * Verify the device, in this case, we need to obtain the info block which
+ * holds information such as the actual mac to use in certain scenarios.
+ *
+ * Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
+ * functions.
+ *
+ * @param deviceNetworkId The DNI of the device
+ * @param ip The address of the device on the network (not the same as DNI)
+ * @param port The port to use (0 will be treated as invalid and will use 80)
+ * @param devicessdpPath The URL path (for example, /desc)
+ *
+ * @note Result is captured in locationHandler()
+ */
+private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) {
+    if(ip) {
+        def address = ip + ":8090"
+        sendHubCommand(new physicalgraph.device.HubAction([
+            method: "GET",
+            path: "/info",
+            headers: [
+                HOST: address,
+            ]]))
+    } else {
+        log.warn("verifyDevice() IP address was empty")
+    }
+}
+
+/**
+ * Returns an array of devices which have been verified
+ *
+ * @return array of verified devices
+ */
+def getVerifiedDevices() {
+    getDevices().findAll{ it?.value?.verified == true }
+}
+
+/**
+ * Returns all discovered devices or an empty array if none
+ *
+ * @return array of devices
+ */
+def getDevices() {
+    state.devices = state.devices ?: [:]
+}
+
+/**
+ * Converts a hexadecimal string to an integer
+ *
+ * @param hex The string with a hexadecimal value
+ * @return An integer
+ */
+private Integer convertHexToInt(hex) {
+    Integer.parseInt(hex,16)
+}
+
+/**
+ * Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD
+ *
+ * @param hex Address represented in hex
+ * @return String containing normal IPv4 dot notation
+ */
+private String convertHexToIP(hex) {
+    if (hex)
+        [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
+    else
+        hex
+}
+
+/**
+ * Tests if this setup can support SmarthThing Labs items
+ *
+ * @return true if it supports it.
+ */
+private Boolean canInstallLabs()
+{
+    return hasAllHubsOver("000.011.00603")
+}
+
+/**
+ * Tests if the firmwares on all hubs owned by user match or exceed the
+ * provided version number.
+ *
+ * @param desiredFirmware The version that must match or exceed
+ * @return true if hub has same or newer
+ */
+private Boolean hasAllHubsOver(String desiredFirmware)
+{
+    return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
+}
+
+/**
+ * Creates a list of firmware version for every hub the user has
+ *
+ * @return List of firmwares
+ */
+private List getRealHubFirmwareVersions()
+{
+    return location.hubs*.firmwareVersionString.findAll { it }
+}
\ No newline at end of file
diff --git a/official/bose-soundtouch-control.groovy b/official/bose-soundtouch-control.groovy
new file mode 100755 (executable)
index 0000000..442200b
--- /dev/null
@@ -0,0 +1,341 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Bose® SoundTouch® Control
+ *
+ *  Author: SmartThings & Joe Geiger
+ *
+ *  Date: 2015-30-09
+ */
+definition(
+    name: "Bose® SoundTouch® Control",
+    namespace: "smartthings",
+    author: "SmartThings & Joe Geiger",
+    description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
+    category: "SmartThings Labs",
+    iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png",
+    iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png",
+    iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png"
+)
+
+preferences {
+       page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true)
+       page(name: "timeIntervalInput", title: "Only during a certain time") {
+               section {
+                       input "starting", "time", title: "Starting", required: false
+                       input "ending", "time", title: "Ending", required: false
+               }
+       }
+}
+
+def mainPage() {
+       dynamicPage(name: "mainPage") {
+               def anythingSet = anythingSet()
+               if (anythingSet) {
+                       section("When..."){
+                               ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
+                               ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
+                               ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
+                               ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
+                               ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
+                               ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
+                               ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
+                               ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
+                               ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
+                               ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
+                               ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
+                               ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
+                               ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
+                       }
+               }
+               section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
+                       ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
+                       ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
+                       ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
+                       ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
+                       ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
+                       ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
+                       ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
+                       ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
+                       ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
+                       ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
+                       ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
+                       ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
+                       ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
+               }
+               section("Perform this action"){
+                       input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
+                               "Turn On & Play",
+                               "Turn Off",
+                               "Toggle Play/Pause",
+                               "Skip to Next Track",
+                               "Skip to Beginning/Previous Track",
+                "Play Preset 1",
+                "Play Preset 2",
+                "Play Preset 3",
+                "Play Preset 4",
+                "Play Preset 5",
+                "Play Preset 6"
+                       ]
+               }
+               section {
+                       input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true
+               }
+               section("More options", hideable: true, hidden: true) {
+                       input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
+                       input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
+                       href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
+                       input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
+                               options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
+                       if (settings.modes) {
+               input "modes", "mode", title: "Only when mode is", multiple: true, required: false
+            }
+                       input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
+               }
+               section([mobileOnly:true]) {
+                       label title: "Assign a name", required: false
+                       mode title: "Set for specific mode(s)"
+               }
+       }
+}
+
+private anythingSet() {
+       for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
+               if (settings[name]) {
+                       return true
+               }
+       }
+       return false
+}
+
+private ifUnset(Map options, String name, String capability) {
+       if (!settings[name]) {
+               input(options, name, capability)
+       }
+}
+
+private ifSet(Map options, String name, String capability) {
+       if (settings[name]) {
+               input(options, name, capability)
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       subscribeToEvents()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       unschedule()
+       subscribeToEvents()
+}
+
+def subscribeToEvents() {
+       log.trace "subscribeToEvents()"
+       subscribe(app, appTouchHandler)
+       subscribe(contact, "contact.open", eventHandler)
+       subscribe(contactClosed, "contact.closed", eventHandler)
+       subscribe(acceleration, "acceleration.active", eventHandler)
+       subscribe(motion, "motion.active", eventHandler)
+       subscribe(mySwitch, "switch.on", eventHandler)
+       subscribe(mySwitchOff, "switch.off", eventHandler)
+       subscribe(arrivalPresence, "presence.present", eventHandler)
+       subscribe(departurePresence, "presence.not present", eventHandler)
+       subscribe(smoke, "smoke.detected", eventHandler)
+       subscribe(smoke, "smoke.tested", eventHandler)
+       subscribe(smoke, "carbonMonoxide.detected", eventHandler)
+       subscribe(water, "water.wet", eventHandler)
+       subscribe(button1, "button.pushed", eventHandler)
+
+       if (triggerModes) {
+               subscribe(location, modeChangeHandler)
+       }
+
+       if (timeOfDay) {
+               schedule(timeOfDay, scheduledTimeHandler)
+       }
+}
+
+def eventHandler(evt) {
+       if (allOk) {
+               def lastTime = state[frequencyKey(evt)]
+               if (oncePerDayOk(lastTime)) {
+                       if (frequency) {
+                               if (lastTime == null || now() - lastTime >= frequency * 60000) {
+                                       takeAction(evt)
+                               }
+                               else {
+                                       log.debug "Not taking action because $frequency minutes have not elapsed since last action"
+                               }
+                       }
+                       else {
+                               takeAction(evt)
+                       }
+               }
+               else {
+                       log.debug "Not taking action because it was already taken today"
+               }
+       }
+}
+
+def modeChangeHandler(evt) {
+       log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
+       if (evt.value in triggerModes) {
+               eventHandler(evt)
+       }
+}
+
+def scheduledTimeHandler() {
+       eventHandler(null)
+}
+
+def appTouchHandler(evt) {
+       takeAction(evt)
+}
+
+private takeAction(evt) {
+       log.debug "takeAction($actionType)"
+       def options = [:]
+       if (volume) {
+               bose.setLevel(volume as Integer)
+               options.delay = 1000
+       }
+
+       switch (actionType) {
+               case "Turn On & Play":
+                       options ? bose.on(options) : bose.on()
+                       break
+               case "Turn Off":
+                       options ? bose.off(options) : bose.off()
+                       break
+               case "Toggle Play/Pause":
+                       def currentStatus = bose.currentValue("playpause")
+                       if (currentStatus == "play") {
+                               options ? bose.pause(options) : bose.pause()
+                       }
+                       else if (currentStatus == "pause") {
+                               options ? bose.play(options) : bose.play()
+                       }
+                       break
+               case "Skip to Next Track":
+                       options ? bose.nextTrack(options) : bose.nextTrack()
+                       break
+               case "Skip to Beginning/Previous Track":
+                       options ? bose.previousTrack(options) : bose.previousTrack()
+                       break
+        case "Play Preset 1":
+                       options ? bose.preset1(options) : bose.preset1()
+                       break
+        case "Play Preset 2":
+                       options ? bose.preset2(options) : bose.preset2()
+                       break 
+        case "Play Preset 3":
+                       options ? bose.preset3(options) : bose.preset3()
+                       break
+        case "Play Preset 4":
+                       options ? bose.preset4(options) : bose.preset4()
+                       break
+        case "Play Preset 5":
+                       options ? bose.preset5(options) : bose.preset5()
+                       break
+        case "Play Preset 6":
+                       options ? bose.preset6(options) : bose.preset6()
+                       break
+               default:
+                       log.error "Action type '$actionType' not defined"
+       }
+
+       if (frequency) {
+               state.lastActionTimeStamp = now()
+       }
+}
+
+private frequencyKey(evt) {
+       //evt.deviceId ?: evt.value
+       "lastActionTimeStamp"
+}
+
+private dayString(Date date) {
+       def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
+       if (location.timeZone) {
+               df.setTimeZone(location.timeZone)
+       }
+       else {
+               df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+       }
+       df.format(date)
+}
+
+private oncePerDayOk(Long lastTime) {
+       def result = true
+       if (oncePerDay) {
+               result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
+               log.trace "oncePerDayOk = $result"
+       }
+       result
+}
+
+// TODO - centralize somehow
+private getAllOk() {
+       modeOk && daysOk && timeOk
+}
+
+private getModeOk() {
+       def result = !modes || modes.contains(location.mode)
+       log.trace "modeOk = $result"
+       result
+}
+
+private getDaysOk() {
+       def result = true
+       if (days) {
+               def df = new java.text.SimpleDateFormat("EEEE")
+               if (location.timeZone) {
+                       df.setTimeZone(location.timeZone)
+               }
+               else {
+                       df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+               }
+               def day = df.format(new Date())
+               result = days.contains(day)
+       }
+       log.trace "daysOk = $result"
+       result
+}
+
+private getTimeOk() {
+       def result = true
+       if (starting && ending) {
+               def currTime = now()
+               def start = timeToday(starting, location?.timeZone).time
+               def stop = timeToday(ending, location?.timeZone).time
+               result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
+       }
+       log.trace "timeOk = $result"
+       result
+}
+
+private hhmm(time, fmt = "h:mm a")
+{
+       def t = timeToday(time, location.timeZone)
+       def f = new java.text.SimpleDateFormat(fmt)
+       f.setTimeZone(location.timeZone ?: timeZone(time))
+       f.format(t)
+}
+
+private timeIntervalLabel()
+{
+       (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
+}
+// TODO - End Centralize
diff --git a/official/bright-when-dark-and-or-bright-after-sunset.groovy b/official/bright-when-dark-and-or-bright-after-sunset.groovy
new file mode 100755 (executable)
index 0000000..79764a8
--- /dev/null
@@ -0,0 +1,762 @@
+definition(
+    name: "Bright When Dark And/Or Bright After Sunset",
+    namespace: "Arno",
+    author: "Arnaud",
+    description: "Turn ON light(s) and/or dimmer(s) when there's movement and the room is dark with illuminance threshold and/or between sunset and sunrise. Then turn OFF after X minute(s) when the brightness of the room is above the illuminance threshold or turn OFF after X minute(s) when there is no movement.",
+    category: "Convenience",
+    iconUrl: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg",
+    iconX2Url: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg"
+)
+
+preferences
+{
+       page(name: "configurations")
+       page(name: "options")
+
+       page(name: "timeIntervalInput", title: "Only during a certain time...")
+       {
+               section
+               {
+                       input "starting", "time", title: "Starting", required: false
+                       input "ending", "time", title: "Ending", required: false
+                       }
+               }
+}
+
+def configurations()
+{
+       dynamicPage(name: "configurations", title: "Configurations...", uninstall: true, nextPage: "options")
+       {
+               section(title: "Turn ON lights on movement when...")
+               {
+                       input "dark", "bool", title: "It is dark?", required: true
+            input "sun", "bool", title: "Between sunset and surise?", required: true
+                       }
+               section(title: "More options...", hidden: hideOptionsSection(), hideable: true)
+               {
+                       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
+                       }
+               section ("Assign a name")
+               {
+                       label title: "Assign a name", required: false
+                       }
+               }
+}
+
+def options()
+{
+       if (dark == true && sun == true)
+       {
+               dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark and between sunset and sunrise...", install: true, uninstall: true)
+               {
+                       section("Control these light(s)...")
+                       {
+                               input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
+                       }    
+               section("Control these dimmer(s)...")
+                       { 
+                       input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
+                       input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
+                               }
+                       section("Turning ON when it's dark and there's movement...")
+                       {
+                               input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
+                               } 
+                       section("And then OFF when it's light or there's been no movement for...")
+                       {
+                               input "delayMinutes", "number", title: "Minutes?", required: false
+                               }
+                       section("Using this light sensor...")
+                       {
+                               input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true
+                       input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false
+                               }
+                       section ("And between sunset and sunrise...")
+                       {
+                               input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00"
+                               input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
+                       input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00"
+                               input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
+                               }
+                       section ("Zip code (optional, defaults to location coordinates when location services are enabled)...")
+                       {
+                               input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code"
+                               }
+                       }
+               }
+       else if (dark == true && sun == false)
+       {
+       dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark...", install: true, uninstall: true)
+               {
+                       section("Control these light(s)...")
+                       {
+                               input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
+                       }    
+               section("Control these dimmer(s)...")
+                       { 
+                       input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
+                       input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
+                               }
+                       section("Turning ON when it's dark and there's movement...")
+                       {
+                               input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
+                               } 
+                       section("And then OFF when it's light or there's been no movement for...")
+                       {
+                               input "delayMinutes", "number", title: "Minutes?", required: false
+                               }
+                       section("Using this light sensor...")
+                       {
+                               input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true
+                       input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false
+                               }
+                       }
+               }
+    else if (sun == true && dark == false)
+       {
+       dynamicPage(name: "options", title: "Lights will turn ON on movement between sunset and sunrise...", install: true, uninstall: true)
+               {
+                       section("Control these light(s)...")
+                       {
+                               input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
+                       }    
+               section("Control these dimmer(s)...")
+                       { 
+                       input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
+                       input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
+                               }
+                       section("Turning ON there's movement...")
+                       {
+                               input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
+                               } 
+                       section("And then OFF there's been no movement for...")
+                       {
+                               input "delayMinutes", "number", title: "Minutes?", required: false
+                               }
+                       section ("Between sunset and sunrise...")
+                       {
+                               input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00"
+                               input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
+                       input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00"
+                               input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
+                               }
+                       section ("Zip code (optional, defaults to location coordinates when location services are enabled)...")
+                       {
+                               input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code"
+                               }
+                       }
+               }
+       else
+       {
+       dynamicPage(name: "options", title: "Lights will turn ON on movement...", install: true, uninstall: true)
+               {
+                       section("Control these light(s)...")
+                       {
+                               input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
+                       }    
+               section("Control these dimmer(s)...")
+                       { 
+                       input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
+                       input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
+                               }
+                       section("Turning ON when there's movement...")
+                       {
+                               input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
+                               } 
+                       section("And then OFF when there's been no movement for...")
+                       {
+                               input "delayMinutes", "number", title: "Minutes?", required: false
+                               }
+                       }
+       }
+}
+
+def installed()
+{
+       log.debug "Installed with settings: ${settings}."
+       initialize()
+}
+
+def updated()
+{
+       log.debug "Updated with settings: ${settings}."
+       unsubscribe()
+       unschedule()
+       initialize()
+}
+
+def initialize()
+{
+       subscribe(motionSensor, "motion", motionHandler)
+    if (lights != null && lights != "" && dimmers != null && dimmers != "")
+       {
+        log.debug "$lights subscribing..."
+       subscribe(lights, "switch", lightsHandler)
+        log.debug "$dimmers subscribing..."
+       subscribe(dimmers, "switch", dimmersHandler)
+        if (dark == true && lightSensor != null && lightSensor != "")
+               {
+               log.debug "$lights and $dimmers will turn ON when movement detected and when it is dark..."
+                       subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
+                       }
+               if (sun == true)
+               {
+               log.debug "$lights and $dimmers will turn ON when movement detected between sunset and sunrise..."
+                       astroCheck()
+               subscribe(location, "position", locationPositionChange)
+            subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
+            subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
+                       }
+        else if (dark != true && sun != true)
+            {
+            log.debug "$lights and $dimmers will turn ON when movement detected..."
+            }
+       }
+    else if (lights != null && lights != "")
+       {
+        log.debug "$lights subscribing..."
+       subscribe(lights, "switch", lightsHandler)
+        if (dark == true && lightSensor != null && lightSensor != "")
+               {
+               log.debug "$lights will turn ON when movement detected and when it is dark..."
+                       subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
+                       }
+               if (sun == true)
+               {
+               log.debug "$lights will turn ON when movement detected between sunset and sunrise..."
+                       astroCheck()
+               subscribe(location, "position", locationPositionChange)
+            subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
+            subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
+                       }
+        else if (dark != true && sun != true)
+            {
+            log.debug "$lights will turn ON when movement detected..."
+            }
+       }
+       else if (dimmers != null && dimmers != "")
+       {
+        log.debug "$dimmers subscribing..."
+       subscribe(dimmers, "switch", dimmersHandler)
+        if (dark == true && lightSensor != null && lightSensor != "")
+               {
+               log.debug "$dimmers will turn ON when movement detected and when it is dark..."
+                       subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
+                       }
+               if (sun == true)
+               {
+               log.debug "$dimmers will turn ON when movement detected between sunset and sunrise..."
+                       astroCheck()
+               subscribe(location, "position", locationPositionChange)
+            subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
+            subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
+                       }
+        else if (dark != true && sun != true)
+            {
+            log.debug "$dimmers will turn ON when movement detected..."
+            }
+       }
+        log.debug "Determinating lights and dimmers current value..."
+        if (lights != null && lights != "")
+               {
+            if (lights.currentValue("switch").toString().contains("on"))
+                {
+                state.lightsState = "on"
+                log.debug "Lights $state.lightsState."
+                }
+            else if (lights.currentValue("switch").toString().contains("off"))
+                {
+                state.lightsState = "off"
+                log.debug "Lights $state.lightsState."
+                }
+            else
+                {
+                log.debug "ERROR!"
+                }
+                       }
+               if (dimmers != null && dimmers != "")
+               {
+            if (dimmers.currentValue("switch").toString().contains("on"))
+                {
+                state.dimmersState = "on"
+                log.debug "Dimmers $state.dimmersState."
+                }
+            else if (dimmers.currentValue("switch").toString().contains("off"))
+                {
+                state.dimmersState = "off"
+                log.debug "Dimmers $state.dimmersState."
+                }
+            else
+                {
+                log.debug "ERROR!"
+                }
+                       }
+}
+            
+def locationPositionChange(evt)
+{
+       log.trace "locationChange()"
+       astroCheck()
+}
+
+def sunriseSunsetTimeHandler(evt)
+{
+       state.lastSunriseSunsetEvent = now()
+       log.debug "SmartNightlight.sunriseSunsetTimeHandler($app.id)"
+       astroCheck()
+}
+
+def motionHandler(evt)
+{
+       log.debug "$evt.name: $evt.value"
+       if (evt.value == "active")
+       {
+        unschedule(turnOffLights)
+       unschedule(turnOffDimmers)
+        if (dark == true && sun == true)
+               {
+            if (darkOk == true && sunOk == true)
+               {
+                log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark or because $motionSensor detected motion between sunset and sunrise..."
+                if (lights != null && lights != "")
+                    {
+                    log.debug "Lights: $lights will turn ON..."
+                    turnOnLights()
+                    }
+                if (dimmers != null && dimmers != "")
+                    {
+                    log.debug "Dimmers: $dimmers will turn ON..."
+                    turnOnDimmers()
+                    }
+                               }
+                       else if (darkOk == true && sunOk != true)
+               {
+                log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..."
+                if (lights != null && lights != "")
+                    {
+                    log.debug "Lights: $lights will turn ON..."
+                    turnOnLights()
+                    }
+                if (dimmers != null && dimmers != "")
+                    {
+                    log.debug "Dimmers: $dimmers will turn ON..."
+                    turnOnDimmers()
+                    }
+                               }
+                       else if (darkOk != true && sunOk == true)
+               {
+                log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..."
+                if (lights != null && lights != "")
+                    {
+                    log.debug "Lights: $lights will turn ON..."
+                    turnOnLights()
+                    }
+                if (dimmers != null && dimmers != "")
+                    {
+                    log.debug "Dimmers: $dimmers will turn ON..."
+                    turnOnDimmers()
+                    }
+                               }
+                       else
+               {
+                               log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright or because time not between sunset and surise."
+                }
+                       }
+               else if (dark == true && sun != true)
+               {
+            if (darkOk == true)
+               {
+                log.debug "Lights and dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..."
+                if (lights != null && lights != "")
+                    {
+                    log.debug "Lights: $lights will turn ON..."
+                    turnOnLights()
+                    }
+                if (dimmers != null && dimmers != "")
+                    {
+                    log.debug "Dimmers: $dimmers will turn ON..."
+                    turnOnDimmers()
+                    }
+                               }
+                       else
+               {
+                               log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright."
+                }
+               }
+               else if (dark != true && sun == true)
+               {
+            if (sunOk == true)
+               {
+                log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..."
+                if (lights != null && lights != "")
+                    {
+                    log.debug "Lights: $lights will turn ON..."
+                    turnOnLights()
+                    }
+                if (dimmers != null && dimmers != "")
+                    {
+                    log.debug "Dimmers: $dimmers will turn ON..."
+                    turnOnDimmers()
+                    }
+                               }
+                       else
+               {
+                               log.debug "Lights and dimmers will not turn ON because time not between sunset and surise."
+                }
+               }
+               else if (dark != true && sun != true)
+               {
+            log.debug "Lights and dimmers will turn ON because $motionSensor detected motion..."
+            if (lights != null && lights != "")
+                               {
+                               log.debug "Lights: $lights will turn ON..."
+                               turnOnLights()
+                               }
+                       if (dimmers != null && dimmers != "")
+               {
+                               log.debug "Dimmers: $dimmers will turn ON..."
+                               turnOnDimmers()
+                               }
+               }
+               }
+       else if (evt.value == "inactive")
+       {
+        unschedule(turnOffLights)
+       unschedule(turnOffDimmers)
+               if (state.lightsState != "off" || state.dimmersState != "off")
+               {
+            log.debug "Lights and/or dimmers are not OFF."
+                       if (delayMinutes)
+               {
+                def delay = delayMinutes * 60
+                if (dark == true && sun == true)
+                    {
+                    log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark or between sunset and sunrise..."
+                    if (lights != null && lights != "")
+                        {
+                        log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffLights)
+                        }
+                     if (dimmers != null && dimmers != "")
+                        {
+                        log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffDimmers)
+                        }
+                    }
+                else if (dark == true && sun != true)
+                    {
+                    log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark..."
+                    if (lights != null && lights != "")
+                        {
+                        log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffLights)
+                        }
+                     if (dimmers != null && dimmers != "")
+                        {
+                        log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffDimmers)
+                        }
+                    }
+                else if (dark != true && sun == true)
+                    {
+                    log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) between sunset and sunrise..."
+                    if (lights != null && lights != "")
+                        {
+                        log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffLights)
+                        }
+                     if (dimmers != null && dimmers != "")
+                        {
+                        log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffDimmers)
+                        }
+                    }
+                else if (dark != true && sun != true)
+                    {
+                    log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s)..."
+                    if (lights != null && lights != "")
+                        {
+                        log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffLights)
+                        }
+                    if (dimmers != null && dimmers != "")
+                        {
+                        log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
+                        runIn(delay, turnOffDimmers)
+                        }
+                    }
+                }
+                       else
+                       {
+                       log.debug "Lights and dimmers will stay ON because no turn OFF delay was set..."
+                               }
+            }
+               else if (state.lightsState == "off" && state.dimmersState == "off")
+               {
+               log.debug "Lights and dimmers are already OFF and will not turn OFF in $delayMinutes minute(s)."
+                       }
+               }
+}
+
+def lightsHandler(evt)
+{
+       log.debug "Lights Handler $evt.name: $evt.value"
+    if (evt.value == "on")
+       {
+        log.debug "Lights: $lights now ON."
+        unschedule(turnOffLights)
+        state.lightsState = "on"
+        }
+       else if (evt.value == "off")
+       {
+        log.debug "Lights: $lights now OFF."
+        unschedule(turnOffLights)
+        state.lightsState = "off"
+        }
+}
+
+def dimmersHandler(evt)
+{
+       log.debug "Dimmer Handler $evt.name: $evt.value"
+    if (evt.value == "on")
+       {
+        log.debug "Dimmers: $dimmers now ON."
+        unschedule(turnOffDimmers)
+        state.dimmersState = "on"
+        }
+       else if (evt.value == "off")
+       {
+        log.debug "Dimmers: $dimmers now OFF."
+        unschedule(turnOffDimmers)
+        state.dimmersState = "off"
+        }
+}
+
+def illuminanceHandler(evt)
+{
+       log.debug "$evt.name: $evt.value, lastStatus lights: $state.lightsState, lastStatus dimmers: $state.dimmersState, motionStopTime: $state.motionStopTime"
+       unschedule(turnOffLights)
+    unschedule(turnOffDimmers)
+    if (evt.integerValue > 999)
+       {
+        log.debug "Lights and dimmers will turn OFF because illuminance is superior to 999 lux..."
+        if (lights != null && lights != "")
+                       {
+                       log.debug "Lights: $lights will turn OFF..."
+                       turnOffLights()
+                       }
+               if (dimmers != null && dimmers != "")
+                       {
+                       log.debug "Dimmers: $dimmers will turn OFF..."
+                       turnOffDimmers()
+                       }
+               }
+       else if (evt.integerValue > ((luxLevel != null && luxLevel != "") ? luxLevel : 50))
+               {
+               log.debug "Lights and dimmers will turn OFF because illuminance is superior to $luxLevel lux..."
+        if (lights != null && lights != "")
+                       {
+                       log.debug "Lights: $lights will turn OFF..."
+                       turnOffLights()
+                       }
+               if (dimmers != null && dimmers != "")
+                       {
+                       log.debug "Dimmers: $dimmers will turn OFF..."
+                       turnOffDimmers()
+                       }
+               }
+}
+
+def turnOnLights()
+{
+       if (allOk)
+       {
+        if (state.lightsState != "on")
+            {
+            log.debug "Turning ON lights: $lights..."
+            lights?.on()
+            state.lightsState = "on"
+            }
+        else
+            {
+            log.debug "Lights: $lights already ON."
+            }
+               }
+       else
+       {
+        log.debug "Time, days of the week or mode out of range! $lights will not turn ON."
+        }
+}
+
+def turnOnDimmers()
+{
+       if (allOk)
+       {
+        if (state.dimmersState != "on")
+            {
+            log.debug "Turning ON dimmers: $dimmers..."
+            settings.dimmers?.setLevel(level)
+            state.dimmersState = "on"
+            }
+        else
+            {
+            log.debug "Dimmers: $dimmers already ON."
+            }
+               }
+       else
+       {
+        log.debug "Time, days of the week or mode out of range! $dimmers will not turn ON."
+        }
+}
+
+
+def turnOffLights()
+{
+       if (allOk)
+       {
+        if (state.lightsState != "off")
+            {
+            log.debug "Turning OFF lights: $lights..."
+            lights?.off()
+            state.lightsState = "on"
+            }
+        else
+            {
+            log.debug "Lights: $lights already OFF."
+            }
+               }
+       else
+       {
+        log.debug "Time, day of the week or mode out of range! $lights will not turn OFF."
+        }
+}
+
+def turnOffDimmers()
+{
+       if (allOk)
+       {
+        if (state.dimmersState != "off")
+            {
+            log.debug "Turning OFF dimmers: $dimmers..."
+            dimmers?.off()
+            state.dimmersState = "off"
+            }
+        else
+            {
+            log.debug "Dimmers: $dimmers already OFF."
+            }
+               }
+       else
+       {
+        log.debug "Time, day of the week or mode out of range! $dimmers will not turn OFF."
+        }
+}
+
+def astroCheck()
+{
+       def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
+       state.riseTime = s.sunrise.time
+       state.setTime = s.sunset.time
+       log.debug "Sunrise: ${new Date(state.riseTime)}($state.riseTime), Sunset: ${new Date(state.setTime)}($state.setTime)"
+}
+
+private getDarkOk()
+{
+       def result
+       if (dark == true && lightSensor != null && lightSensor != "")
+        {
+               result = lightSensor.currentIlluminance < ((luxLevel != null && luxLevel != "") ? luxLevel : 50)
+               }
+       log.trace "darkOk = $result"
+       result
+}
+
+private getSunOk()
+{
+       def result
+       if (sun == true)
+       {
+               def t = now()
+               result = t < state.riseTime || t > state.setTime
+               }
+       log.trace "sunOk = $result"
+       result
+}
+
+private getSunriseOffset()
+{
+       sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null
+}
+
+private getSunsetOffset()
+{
+       sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null
+}
+
+private getAllOk()
+{
+       modeOk && daysOk && timeOk
+}
+
+private getModeOk()
+{
+       def result = !modes || modes.contains(location.mode)
+       log.trace "modeOk = $result"
+       result
+}
+
+private getDaysOk()
+{
+       def result = true
+       if (days)
+       {
+               def df = new java.text.SimpleDateFormat("EEEE")
+               if (location.timeZone)
+               {
+                       df.setTimeZone(location.timeZone)
+                       }
+               else
+               {
+                       df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+                       }
+               def day = df.format(new Date())
+               result = days.contains(day)
+               }
+       log.trace "daysOk = $result"
+       result
+}
+
+private 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
+               }
+       log.trace "timeOk = $result"
+       result
+}
+
+private hhmm(time, fmt = "h:mm a")
+{
+       def t = timeToday(time, location.timeZone)
+       def f = new java.text.SimpleDateFormat(fmt)
+       f.setTimeZone(location.timeZone ?: timeZone(time))
+       f.format(t)
+}
+
+private hideOptionsSection()
+{
+       (starting || ending || days || modes) ? false : true
+}
+
+private timeIntervalLabel()
+{
+       (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
+}
diff --git a/official/brighten-dark-places.groovy b/official/brighten-dark-places.groovy
new file mode 100755 (executable)
index 0000000..c8ab1d0
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Brighten Dark Places
+ *
+ *  Author: SmartThings
+ */
+definition(
+    name: "Brighten Dark Places",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn your lights on when a open/close sensor opens and the space is dark.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance@2x.png"
+)
+
+preferences {
+       section("When the door opens...") {
+               input "contact1", "capability.contactSensor", title: "Where?"
+       }
+       section("And it's dark...") {
+               input "luminance1", "capability.illuminanceMeasurement", title: "Where?"
+       }
+       section("Turn on a light...") {
+               input "switch1", "capability.switch"
+       }
+}
+
+def installed()
+{
+       subscribe(contact1, "contact.open", contactOpenHandler)
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(contact1, "contact.open", contactOpenHandler)
+}
+
+def contactOpenHandler(evt) {
+       def lightSensorState = luminance1.currentIlluminance
+       log.debug "SENSOR = $lightSensorState"
+       if (lightSensorState != null && lightSensorState < 10) {
+               log.trace "light.on() ... [luminance: ${lightSensorState}]"
+               switch1.on()
+       }
+}
diff --git a/official/brighten-my-path.groovy b/official/brighten-my-path.groovy
new file mode 100755 (executable)
index 0000000..8f3eac8
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Brighten My Path
+ *
+ *  Author: SmartThings
+ */
+definition(
+    name: "Brighten My Path",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn your lights on when motion is detected.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png"
+)
+
+preferences {
+       section("When there's movement...") {
+               input "motion1", "capability.motionSensor", title: "Where?", multiple: true
+       }
+       section("Turn on a light...") {
+               input "switch1", "capability.switch", multiple: true
+       }
+}
+
+def installed()
+{
+       subscribe(motion1, "motion.active", motionActiveHandler)
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(motion1, "motion.active", motionActiveHandler)
+}
+
+def motionActiveHandler(evt) {
+       switch1.on()
+}
diff --git a/official/button-controller.groovy b/official/button-controller.groovy
new file mode 100755 (executable)
index 0000000..283a4e3
--- /dev/null
@@ -0,0 +1,322 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *     Button Controller
+ *
+ *     Author: SmartThings
+ *     Date: 2014-5-21
+ */
+definition(
+    name: "Button Controller",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Control devices with buttons like the Aeon Labs Minimote",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png"
+)
+
+preferences {
+       page(name: "selectButton")
+       page(name: "configureButton1")
+       page(name: "configureButton2")
+       page(name: "configureButton3")
+       page(name: "configureButton4")
+
+       page(name: "timeIntervalInput", title: "Only during a certain time") {
+               section {
+                       input "starting", "time", title: "Starting", required: false
+                       input "ending", "time", title: "Ending", required: false
+               }
+       }
+}
+
+def selectButton() {
+       dynamicPage(name: "selectButton", title: "First, select your button device", nextPage: "configureButton1", uninstall: configured()) {
+               section {
+                       input "buttonDevice", "capability.button", title: "Button", multiple: false, required: true
+               }
+
+               section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
+
+                       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 configureButton1() {
+       dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
+               nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
+}
+def configureButton2() {
+       dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
+               nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
+}
+
+def configureButton3() {
+       dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
+               nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
+}
+def configureButton4() {
+       dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here",
+               install: true, uninstall: true, getButtonSections(4))
+}
+
+def getButtonSections(buttonNumber) {
+       return {
+               section("Lights") {
+                       input "lights_${buttonNumber}_pushed", "capability.switch", title: "Pushed", multiple: true, required: false
+                       input "lights_${buttonNumber}_held", "capability.switch", title: "Held", multiple: true, required: false
+               }
+               section("Locks") {
+                       input "locks_${buttonNumber}_pushed", "capability.lock", title: "Pushed", multiple: true, required: false
+                       input "locks_${buttonNumber}_held", "capability.lock", title: "Held", multiple: true, required: false
+               }
+               section("Sonos") {
+                       input "sonos_${buttonNumber}_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false
+                       input "sonos_${buttonNumber}_held", "capability.musicPlayer", title: "Held", multiple: true, required: false
+               }
+               section("Modes") {
+                       input "mode_${buttonNumber}_pushed", "mode", title: "Pushed", required: false
+                       input "mode_${buttonNumber}_held", "mode", title: "Held", required: false
+               }
+               def phrases = location.helloHome?.getPhrases()*.label
+               if (phrases) {
+                       section("Hello Home Actions") {
+                               log.trace phrases
+                               input "phrase_${buttonNumber}_pushed", "enum", title: "Pushed", required: false, options: phrases
+                               input "phrase_${buttonNumber}_held", "enum", title: "Held", required: false, options: phrases
+                       }
+               }
+        section("Sirens") {
+            input "sirens_${buttonNumber}_pushed","capability.alarm" ,title: "Pushed", multiple: true, required: false
+            input "sirens_${buttonNumber}_held", "capability.alarm", title: "Held", multiple: true, required: false
+        }
+
+               section("Custom Message") {
+                       input "textMessage_${buttonNumber}", "text", title: "Message", required: false
+               }
+
+        section("Push Notifications") {
+            input "notifications_${buttonNumber}_pushed","bool" ,title: "Pushed", required: false, defaultValue: false
+            input "notifications_${buttonNumber}_held", "bool", title: "Held", required: false, defaultValue: false
+        }
+
+        section("Sms Notifications") {
+            input "phone_${buttonNumber}_pushed","phone" ,title: "Pushed", required: false
+            input "phone_${buttonNumber}_held", "phone", title: "Held", required: false
+        }
+       }
+}
+
+def installed() {
+       initialize()
+}
+
+def updated() {
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(buttonDevice, "button", buttonEvent)
+}
+
+def configured() {
+       return buttonDevice || buttonConfigured(1) || buttonConfigured(2) || buttonConfigured(3) || buttonConfigured(4)
+}
+
+def buttonConfigured(idx) {
+       return settings["lights_$idx_pushed"] ||
+               settings["locks_$idx_pushed"] ||
+               settings["sonos_$idx_pushed"] ||
+               settings["mode_$idx_pushed"] ||
+        settings["notifications_$idx_pushed"] ||
+        settings["sirens_$idx_pushed"] ||
+        settings["notifications_$idx_pushed"]   ||
+        settings["phone_$idx_pushed"]
+}
+
+def buttonEvent(evt){
+       if(allOk) {
+               def buttonNumber = evt.data // why doesn't jsonData work? always returning [:]
+               def value = evt.value
+               log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)"
+               log.debug "button: $buttonNumber, value: $value"
+
+               def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value && it.data == evt.data}
+               log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds"
+
+               if(recentEvents.size <= 1){
+                       switch(buttonNumber) {
+                               case ~/.*1.*/:
+                                       executeHandlers(1, value)
+                                       break
+                               case ~/.*2.*/:
+                                       executeHandlers(2, value)
+                                       break
+                               case ~/.*3.*/:
+                                       executeHandlers(3, value)
+                                       break
+                               case ~/.*4.*/:
+                                       executeHandlers(4, value)
+                                       break
+                       }
+               } else {
+                       log.debug "Found recent button press events for $buttonNumber with value $value"
+               }
+       }
+}
+
+def executeHandlers(buttonNumber, value) {
+       log.debug "executeHandlers: $buttonNumber - $value"
+
+       def lights = find('lights', buttonNumber, value)
+       if (lights != null) toggle(lights)
+
+       def locks = find('locks', buttonNumber, value)
+       if (locks != null) toggle(locks)
+
+       def sonos = find('sonos', buttonNumber, value)
+       if (sonos != null) toggle(sonos)
+
+       def mode = find('mode', buttonNumber, value)
+       if (mode != null) changeMode(mode)
+
+       def phrase = find('phrase', buttonNumber, value)
+       if (phrase != null) location.helloHome.execute(phrase)
+
+       def textMessage = findMsg('textMessage', buttonNumber)
+
+       def notifications = find('notifications', buttonNumber, value)
+       if (notifications?.toBoolean()) sendPush(textMessage ?: "Button $buttonNumber was pressed" )
+
+       def phone = find('phone', buttonNumber, value)
+       if (phone != null) sendSms(phone, textMessage ?:"Button $buttonNumber was pressed")
+
+    def sirens = find('sirens', buttonNumber, value)
+    if (sirens != null) toggle(sirens)
+}
+
+def find(type, buttonNumber, value) {
+       def preferenceName = type + "_" + buttonNumber + "_" + value
+       def pref = settings[preferenceName]
+       if(pref != null) {
+               log.debug "Found: $pref for $preferenceName"
+       }
+
+       return pref
+}
+
+def findMsg(type, buttonNumber) {
+       def preferenceName = type + "_" + buttonNumber
+       def pref = settings[preferenceName]
+       if(pref != null) {
+               log.debug "Found: $pref for $preferenceName"
+       }
+
+       return pref
+}
+
+def toggle(devices) {
+       log.debug "toggle: $devices = ${devices*.currentValue('switch')}"
+
+       if (devices*.currentValue('switch').contains('on')) {
+               devices.off()
+       }
+       else if (devices*.currentValue('switch').contains('off')) {
+               devices.on()
+       }
+       else if (devices*.currentValue('lock').contains('locked')) {
+               devices.unlock()
+       }
+       else if (devices*.currentValue('lock').contains('unlocked')) {
+               devices.lock()
+       }
+       else if (devices*.currentValue('alarm').contains('off')) {
+        devices.siren()
+    }
+       else {
+               devices.on()
+       }
+}
+
+def changeMode(mode) {
+       log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes"
+
+       if (location.mode != mode && location.modes?.find { it.name == mode }) {
+               setLocationMode(mode)
+       }
+}
+
+// execution filter methods
+private getAllOk() {
+       modeOk && daysOk && timeOk
+}
+
+private getModeOk() {
+       def result = !modes || modes.contains(location.mode)
+       log.trace "modeOk = $result"
+       result
+}
+
+private getDaysOk() {
+       def result = true
+       if (days) {
+               def df = new java.text.SimpleDateFormat("EEEE")
+               if (location.timeZone) {
+                       df.setTimeZone(location.timeZone)
+               }
+               else {
+                       df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
+               }
+               def day = df.format(new Date())
+               result = days.contains(day)
+       }
+       log.trace "daysOk = $result"
+       result
+}
+
+private getTimeOk() {
+       def result = true
+       if (starting && ending) {
+               def currTime = now()
+               def start = timeToday(starting, location.timeZone).time
+               def stop = timeToday(ending, location.timeZone).time
+               result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
+       }
+       log.trace "timeOk = $result"
+       result
+}
+
+private hhmm(time, fmt = "h:mm a")
+{
+       def t = timeToday(time, location.timeZone)
+       def f = new java.text.SimpleDateFormat(fmt)
+       f.setTimeZone(location.timeZone ?: timeZone(time))
+       f.format(t)
+}
+
+private hideOptionsSection() {
+       (starting || ending || days || modes) ? false : true
+}
+
+private timeIntervalLabel() {
+       (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
+}
diff --git a/official/camera-power-scheduler.groovy b/official/camera-power-scheduler.groovy
new file mode 100755 (executable)
index 0000000..40fe521
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Schedule the Camera Power
+ *
+ *  Author: danny@smartthings.com
+ *  Date: 2013-10-07
+ */
+
+definition(
+    name: "Camera Power Scheduler",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn the power on and off at a specific time. ",
+    category: "Available Beta Apps",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule@2x.png"
+)
+
+preferences {
+       section("Camera power..."){
+               input "switch1", "capability.switch", multiple: true
+       }
+       section("Turn the Camera On at..."){
+               input "startTime", "time", title: "Start Time", required:false
+       }
+       section("Turn the Camera Off at..."){
+               input "endTime", "time", title: "End Time", required:false
+       }    
+}
+
+def installed()
+{
+       initialize()
+}
+
+def updated()
+{
+       unschedule()
+       initialize()
+}
+
+def initialize() {
+    /*
+       def tz = location.timeZone
+    
+    //if it's after the startTime but before the end time, turn it on
+    if(startTime && timeToday(startTime,tz).time > timeToday(now,tz).time){
+    
+      if(endTime && timeToday(endTime,tz).time < timeToday(now,tz).time){
+        switch1.on()
+      }
+      else{
+        switch1.off()
+      }
+    }
+    else if(endTime && timeToday(endtime,tz).time > timeToday(now,tz).time)
+    {
+      switch1.off()
+    }
+    */
+    
+    if(startTime)
+      runDaily(startTime, turnOnCamera)
+    if(endTime)
+      runDaily(endTime,turnOffCamera)
+}
+
+def turnOnCamera()
+{
+  log.info "turned on camera"
+  switch1.on()
+}
+
+def turnOffCamera()
+{
+  log.info "turned off camera"
+  switch1.off()
+}
diff --git a/official/cameras-on-when-im-away.groovy b/official/cameras-on-when-im-away.groovy
new file mode 100755 (executable)
index 0000000..42051ba
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Cameras On When I'm Away
+ *
+ *  Author: danny@smartthings.com
+ *  Date: 2013-10-07
+ */
+
+definition(
+    name: "Cameras On When I'm Away",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn cameras on when I'm away",
+    category: "Available Beta Apps",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence@2x.png"
+)
+
+preferences {
+       section("When all of these people are home...") {
+               input "people", "capability.presenceSensor", multiple: true
+       }
+       section("Turn off camera power..."){
+               input "switches1", "capability.switch", multiple: true
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}"
+       subscribe(people, "presence", presence)
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}"
+       unsubscribe()
+       subscribe(people, "presence", presence)
+}
+
+def presence(evt)
+{
+       log.debug "evt.name: $evt.value"
+       if (evt.value == "not present") {
+               
+        log.debug "checking if everyone is away"
+        if (everyoneIsAway()) {
+            log.debug "starting on Sequence"
+
+            runIn(60*2, "turnOn") //two minute delay after everyone has left
+        }
+       }
+       else {
+       if (!everyoneIsAway()) {
+          turnOff()
+        }
+       }
+}
+
+def turnOff()
+{
+    log.debug "canceling On requests"
+    unschedule("turnOn")
+    
+    log.info "turning off the camera"
+    switches1.off()
+}
+
+def turnOn()
+{
+
+       log.info "turned on the camera"
+    switches1.on()
+
+       unschedule("turnOn") // Temporary work-around to scheduling bug
+}
+
+private everyoneIsAway()
+{
+       def result = true
+       for (person in people) {
+               if (person.currentPresence == "present") {
+                       result = false
+                       break
+               }
+       }
+       log.debug "everyoneIsAway: $result"
+       return result
+}
+
+
diff --git a/official/carpool-notifier.groovy b/official/carpool-notifier.groovy
new file mode 100755 (executable)
index 0000000..32a215f
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ * title: Carpool Notifier
+ *
+ * description:
+ * Do you carpool to work with your spouse? Do you pick your children up from school? Have they been waiting in doors for you? Let them know you've arrived with Carpool Notifier.
+ *
+ * This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you.
+ *
+ * category: Family
+
+ * icon:               https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png
+ * icon2X:     https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt%402x.png
+ *
+ *  Author: steve
+ *  Date: 2013-11-19
+ */
+
+definition(
+    name: "Carpool Notifier",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Send notifications to your carpooling buddies when you arrive to pick them up. If the person you are picking up is home, and has been for 5 minutes or more, they will get a notification when you arrive.",
+    category: "Green Living",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png"
+)
+
+preferences {
+       section() {
+               input(name: "driver", type: "capability.presenceSensor", required: true, multiple: false, title: "When this person arrives", description: "Who's driving?")
+               input("recipients", "contact", title: "Notify", description: "Send notifications to") {
+                       input(name: "phoneNumber", type: "phone", required: true, multiple: false, title: "Send a text to", description: "Phone number")
+               }
+               input(name: "message", type: "text", required: false, multiple: false, title: "With the message:", description: "Your ride is here!")
+               input(name: "rider", type: "capability.presenceSensor", required: true, multiple: false, title: "But only when this person is not with you", description: "Who are you picking up?")
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(driver, "presence.present", presence)
+}
+
+def presence(evt) {
+
+       if (evt.value == "present" && riderIsHome())
+       {
+//             log.debug "Rider Is Home; Send A Text"
+               sendText()
+       }
+
+}
+
+def riderIsHome() {
+
+//     log.debug "rider presence: ${rider.currentPresence}"
+
+       if (rider.currentPresence != "present")
+       {
+               return false
+       }
+
+       def riderState = rider.currentState("presence")
+//     log.debug "riderState: ${riderState}"
+       if (!riderState)
+       {
+               return true
+       }
+
+       def latestState = rider.latestState("presence")
+
+       def now = new Date()
+       def minusFive = new Date(minutes: now.minutes - 5)
+
+
+       if (minusFive > latestState.date)
+       {
+               return true
+       }
+
+       return false
+}
+
+def sendText() {
+       if (location.contactBookEnabled) {
+               sendNotificationToContacts(message ?: "Your ride is here!", recipients)
+       }
+       else {
+               sendSms(phoneNumber, message ?: "Your ride is here!")
+       }
+}
diff --git a/official/close-the-valve.groovy b/official/close-the-valve.groovy
new file mode 100755 (executable)
index 0000000..0e6cc6f
--- /dev/null
@@ -0,0 +1,87 @@
+/**
+ *  Close a valve if moisture is detected
+ *
+ *  Copyright 2014 SmartThings
+ *
+ *     Author: Juan Risso
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Close The Valve",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Close a selected valve if moisture is detected, and get notified by SMS and push notification.",
+    category: "Safety & Security",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png"
+)
+
+preferences {
+       section("When water is sensed...") {
+               input "sensor", "capability.waterSensor", title: "Where?", required: true, multiple: true
+       }
+       section("Close the valve...") {
+               input "valve", "capability.valve", title: "Which?", required: true, multiple: false
+       }
+       section("Send this message (optional, sends standard status message if not specified)"){
+               input "messageText", "text", title: "Message Text", required: false
+       }
+       section("Via a push notification and/or an SMS message"){
+               input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
+               input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes","No"]
+       }
+       section("Minimum time between messages (optional)") {
+               input "frequency", "decimal", title: "Minutes", required: false
+       }    
+}
+
+def installed() {
+       subscribe(sensor, "water", waterHandler)
+}
+
+def updated() {
+       unsubscribe()
+       subscribe(sensor, "water", waterHandler)
+}
+
+def waterHandler(evt) {
+       log.debug "Sensor says ${evt.value}"
+       if (evt.value == "wet") {
+               valve.close()
+       }
+       if (frequency) {
+               def lastTime = state[evt.deviceId]
+               if (lastTime == null || now() - lastTime >= frequency * 60000) {
+                       sendMessage(evt)
+               }
+       }
+       else {
+               sendMessage(evt)
+       }    
+}
+
+private sendMessage(evt) {
+       def msg = messageText ?: "We closed the valve because moisture was detected"
+       log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'"
+
+       if (!phone || pushAndPhone != "No") {
+               log.debug "sending push"
+               sendPush(msg)
+       }
+       if (phone) {
+               log.debug "sending SMS"
+               sendSms(phone, msg)
+       }
+       if (frequency) {
+               state[evt.deviceId] = now()
+       }
+}
\ No newline at end of file
diff --git a/official/coffee-after-shower.groovy b/official/coffee-after-shower.groovy
new file mode 100755 (executable)
index 0000000..599759e
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ *  Coffee After Shower
+ *
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Coffee After Shower",
+    namespace: "hwustrack",
+    author: "Hans Wustrack",
+    description: "This app is designed simply to turn on your coffee machine while you are taking a shower.",
+    category: "My Apps",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
+
+
+preferences {
+    section("About") {
+        paragraph "This app is designed simply to turn on your coffee machine " +
+            "while you are taking a shower."
+    }
+       section("Bathroom humidity sensor") {
+               input "bathroom", "capability.relativeHumidityMeasurement", title: "Which humidity sensor?"
+       }
+    section("Coffee maker to turn on") {
+       input "coffee", "capability.switch", title: "Which switch?"
+    }
+    section("Humidity level to switch coffee on at") {
+       input "relHum", "number", title: "Humidity level?", defaultValue: 50
+    }
+}
+
+def installed() {
+       subscribe(bathroom, "humidity", coffeeMaker)
+}
+
+def updated() {
+       unsubscribe()
+       subscribe(bathroom, "humidity", coffeeMaker)
+}
+
+def coffeeMaker(shower) {
+       log.info "Humidity value: $shower.value"
+       if (shower.value.toInteger() > relHum) {
+               coffee.on()
+    } 
+}
diff --git a/official/color-coordinator.groovy b/official/color-coordinator.groovy
new file mode 100755 (executable)
index 0000000..dc5dfb7
--- /dev/null
@@ -0,0 +1,176 @@
+/**
+ *     Color Coordinator 
+ *  Version 1.1.1 - 11/9/16
+ *  By Michael Struck
+ *
+ *  1.0.0 - Initial release
+ *  1.1.0 - Fixed issue where master can be part of slaves. This causes a loop that impacts SmartThings. 
+ *  1.1.1 - Fix NPE being thrown for slave/master inputs being empty.
+ *
+ *
+ *  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.
+ *
+ */
+definition(
+       name: "Color Coordinator",
+       namespace: "MichaelStruck",
+       author: "Michael Struck",
+       description: "Ties multiple colored lights to one specific light's settings",
+       category: "Convenience",
+       iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC.png",
+       iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC@2x.png"
+)
+
+preferences {
+       page name: "mainPage"
+}
+
+def mainPage() {
+       dynamicPage(name: "mainPage", title: "", install: true, uninstall: false) {
+               def masterInList = slaves?.id?.find{it==master?.id}
+        if (masterInList) {
+               section ("**WARNING**"){
+               paragraph "You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.", image: "https://raw.githubusercontent.com/MichaelStruck/SmartThingsPublic/master/img/caution.png"
+            }
+        }
+        section("Master Light") {
+                       input "master", "capability.colorControl", title: "Colored Light", required: true
+               }
+               section("Lights that follow the master settings") {
+                       input "slaves", "capability.colorControl", title: "Colored Lights",  multiple: true, required: true, submitOnChange: true
+               }
+       section([mobileOnly:true], "Options") {
+                       input "randomYes", "bool",title: "When Master Turned On, Randomize Color", defaultValue: false
+                       href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions"
+        }
+       }
+}
+
+page(name: "pageAbout", title: "About ${textAppName()}", uninstall: true) {
+       section {
+       paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
+       }
+       section("Instructions") {
+               paragraph textHelp()
+       }
+    section("Tap button below to remove application"){
+    }
+}
+
+def installed() {   
+       init() 
+}
+
+def updated(){
+       unsubscribe()
+    init()
+}
+
+def init() {
+       subscribe(master, "switch", onOffHandler)
+       subscribe(master, "level", colorHandler)
+    subscribe(master, "hue", colorHandler)
+    subscribe(master, "saturation", colorHandler)
+    subscribe(master, "colorTemperature", tempHandler)
+}
+//-----------------------------------
+def onOffHandler(evt){
+       if (slaves && master) {
+               if (!slaves?.id.find{it==master?.id}){
+               if (master?.currentValue("switch") == "on"){
+                   if (randomYes) getRandomColorMaster()
+                               else slaves?.on()
+               }
+               else {
+                   slaves?.off()  
+               }
+               }
+       }
+}
+
+def colorHandler(evt) {
+       if (slaves && master) {
+               if (!slaves?.id?.find{it==master?.id} && master?.currentValue("switch") == "on"){
+                       log.debug "Changing Slave units H,S,L"
+               def dimLevel = master?.currentValue("level")
+               def hueLevel = master?.currentValue("hue")
+               def saturationLevel = master.currentValue("saturation")
+                       def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer]
+               slaves?.setColor(newValue)
+               try {
+                       log.debug "Changing Slave color temp"
+                       def tempLevel = master?.currentValue("colorTemperature")
+                       slaves?.setColorTemperature(tempLevel)
+               }
+                       catch (e){
+                       log.debug "Color temp for master --"
+               }
+               }
+       }
+}
+
+def getRandomColorMaster(){
+    def hueLevel = Math.floor(Math.random() *1000)
+    def saturationLevel = Math.floor(Math.random() * 100)
+    def dimLevel = master?.currentValue("level")
+       def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer]
+    log.debug hueLevel
+    log.debug saturationLevel
+    master.setColor(newValue)
+    slaves?.setColor(newValue)   
+}
+
+def tempHandler(evt){
+       if (slaves && master) {
+           if (!slaves?.id?.find{it==master?.id} && master?.currentValue("switch") == "on"){
+               if (evt.value != "--") {
+                   log.debug "Changing Slave color temp based on Master change"
+                   def tempLevel = master.currentValue("colorTemperature")
+                   slaves?.setColorTemperature(tempLevel)
+               }
+               }
+       }
+}
+
+//Version/Copyright/Information/Help
+
+private def textAppName() {
+       def text = "Color Coordinator"
+}      
+
+private def textVersion() {
+    def text = "Version 1.1.1 (12/13/2016)"
+}
+
+private def textCopyright() {
+    def text = "Copyright © 2016 Michael Struck"
+}
+
+private def textLicense() {
+    def text =
+               "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"+
+               "\n\n"+
+               "    http://www.apache.org/licenses/LICENSE-2.0"+
+               "\n\n"+
+               "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."
+}
+
+private def textHelp() {
+       def text =
+       "This application will allow you to control the settings of multiple colored lights with one control. " +
+        "Simply choose a master control light, and then choose the lights that will follow the settings of the master, "+
+        "including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature."
+}
diff --git a/official/curb-control.groovy b/official/curb-control.groovy
new file mode 100755 (executable)
index 0000000..3bd1ec4
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ *  Curb Control
+ *
+ *  Author: Curb
+ */
+
+definition(
+    name: "Curb Control",
+    namespace: "Curb",
+    author: "Curb",
+    description: "This SmartApp allows you to interact with the switches in your physical graph through Curb.",
+    category: "Convenience",
+    iconUrl: "http://energycurb.com/images/logo.png",
+    iconX2Url: "http://energycurb.com/images/logo.png",
+    oauth: [displayName: "SmartThings Curb Control", displayLink: "energycurb.com"]
+)
+
+preferences {
+       section("Allow Curb to Control These Things...") {
+               input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
+       }
+}
+
+mappings {
+    path("/") {
+        action: [
+            GET: "index"
+        ]
+    }
+       path("/switches") {
+               action: [
+                       GET: "listSwitches",
+                       PUT: "updateSwitches"
+               ]
+       }
+       path("/switches/:id") {
+               action: [
+                       GET: "showSwitch",
+                       PUT: "updateSwitch"
+               ]
+       }
+}
+
+def installed() {}
+
+def updated() {}
+
+def index(){
+    [[url: "/switches"]]
+}
+
+def listSwitches() {
+       switches.collect { device(it,"switch") }
+}
+void updateSwitches() {
+       updateAll(switches)
+}
+def showSwitch() {
+       show(switches, "switch")
+}
+void updateSwitch() {
+       update(switches)
+}
+
+private void updateAll(devices) {
+       def command = request.JSON?.command
+       if (command) {
+               switch(command) {
+                       case "on":
+                               devices.on()
+                               break
+                       case "off":
+                               devices.off()
+                               break
+                       default:
+                               httpError(403, "Access denied. This command is not supported by current capability.")
+               }
+       }
+}
+
+private void update(devices) {
+       log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
+       def command = request.JSON?.command
+       if (command) {
+               def device = devices.find { it.id == params.id }
+               if (!device) {
+                       httpError(404, "Device not found")
+               } else {
+                       switch(command) {
+                               case "on":
+                                       device.on()
+                                       break
+                               case "off":
+                                       device.off()
+                                       break
+                               default:
+                                       httpError(403, "Access denied. This command is not supported by current capability.")
+                       }
+               }
+       }
+}
+
+private show(devices, name) {
+       def d = devices.find { it.id == params.id }
+       if (!d) {
+               httpError(404, "Device not found")
+       }
+       else {
+        device(d, name)
+       }
+}
+
+private device(it, name){
+    if(it) {
+               def s = it.currentState(name)
+               [id: it.id, label: it.displayName, name: it.displayName, state: s]    
+    }
+}
diff --git a/official/curling-iron.groovy b/official/curling-iron.groovy
new file mode 100755 (executable)
index 0000000..8f9203f
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Curling Iron
+ *
+ *  Author: SmartThings
+ *  Date: 2013-03-20
+ */
+definition(
+    name: "Curling Iron",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turns on an outlet when the user is present and off after a period of time",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
+)
+
+preferences {
+       section("When someone's around because of...") {
+               input name: "motionSensors", title: "Motion here", type: "capability.motionSensor", multiple: true, required: false
+               input name: "presenceSensors", title: "And (optionally) these sensors being present", type: "capability.presenceSensor", multiple: true, required: false
+       }
+       section("Turn on these outlet(s)") {
+               input name: "outlets", title: "Which?", type: "capability.switch", multiple: true
+       }
+       section("For this amount of time") {
+               input name: "minutes", title: "Minutes?", type: "number", multiple: false
+       }
+}
+
+def installed() {
+       subscribeToEvents()
+}
+
+def updated() {
+       unsubscribe()
+       subscribeToEvents()
+}
+
+def subscribeToEvents() {
+       subscribe(motionSensors, "motion.active", motionActive)
+       subscribe(motionSensors, "motion.inactive", motionInactive)
+       subscribe(presenceSensors, "presence.not present", notPresent)
+}
+
+def motionActive(evt) {
+       log.debug "$evt.name: $evt.value"
+       if (anyHere()) {
+               outletsOn()
+       }
+}
+
+def motionInactive(evt) {
+       log.debug "$evt.name: $evt.value"
+       if (allQuiet()) {
+               outletsOff()
+       }
+}
+
+def notPresent(evt) {
+       log.debug "$evt.name: $evt.value"
+       if (!anyHere()) {
+               outletsOff()
+       }
+}
+
+def allQuiet() {
+       def result = true
+       for (it in motionSensors) {
+               if (it.currentMotion == "active") {
+                       result = false
+                       break
+               }
+       }
+       return result
+}
+
+def anyHere() {
+       def result = true
+       for (it in presenceSensors) {
+               if (it.currentPresence == "not present") {
+                       result = false
+                       break
+               }
+       }
+       return result
+}
+
+def outletsOn() {
+       outlets.on()
+       unschedule("scheduledTurnOff")
+}
+
+def outletsOff() {
+       def delay = minutes * 60
+       runIn(delay, "scheduledTurnOff")
+}
+
+def scheduledTurnOff() {
+       outlets.off()
+       unschedule("scheduledTurnOff") // Temporary work-around to scheduling bug
+}
+
+
diff --git a/official/darken-behind-me.groovy b/official/darken-behind-me.groovy
new file mode 100755 (executable)
index 0000000..e5576bf
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Darken Behind Me
+ *
+ *  Author: SmartThings
+ */
+definition(
+    name: "Darken Behind Me",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn your lights off after a period of no motion being observed.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png"
+)
+
+preferences {
+       section("When there's no movement...") {
+               input "motion1", "capability.motionSensor", title: "Where?"
+       }
+       section("Turn off a light...") {
+               input "switch1", "capability.switch", multiple: true
+       }
+}
+
+def installed()
+{
+       subscribe(motion1, "motion.inactive", motionInactiveHandler)
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(motion1, "motion.inactive", motionInactiveHandler)
+}
+
+def motionInactiveHandler(evt) {
+       switch1.off()
+}
diff --git a/official/device-tile-controller.groovy b/official/device-tile-controller.groovy
new file mode 100755 (executable)
index 0000000..fbc7195
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ *  Device Tile Controller
+ *
+ *  Copyright 2016 SmartThings
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Device Tile Controller",
+    namespace: "smartthings/tile-ux",
+    author: "SmartThings",
+    description: "A controller SmartApp to install virtual devices into your location in order to simulate various native Device Tiles.",
+    category: "SmartThings Internal",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
+    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
+    singleInstance: true)
+
+
+preferences {
+    // landing page
+    page(name: "defaultPage")
+}
+
+def defaultPage() {
+    dynamicPage(name: "defaultPage", install: true, uninstall: true) {
+        section {
+            paragraph "Select on Unselect the devices that you want to install"
+        }
+        section(title: "Multi Attribute Tile Types") {
+            input(type: "bool", name: "genericDeviceTile", title: "generic", description: "A device that showcases the various use of generic multi-attribute-tiles.", defaultValue: "false")
+            input(type: "bool", name: "lightingDeviceTile", title: "lighting", description: "A device that showcases the various use of lighting multi-attribute-tiles.", defaultValue: "false")
+            input(type: "bool", name: "thermostatDeviceTile", title: "thermostat", description: "A device that showcases the various use of thermostat multi-attribute-tiles.", defaultValue: "true")
+            input(type: "bool", name: "mediaPlayerDeviceTile", title: "media player", description: "A device that showcases the various use of mediaPlayer multi-attribute-tiles.", defaultValue: "false")
+            input(type: "bool", name: "videoPlayerDeviceTile", title: "video player", description: "A device that showcases the various use of videoPlayer multi-attribute-tiles.", defaultValue: "false")
+        }
+        section(title: "Device Tile Types") {
+            input(type: "bool", name: "standardDeviceTile", title: "standard device tiles", description: "A device that showcases the various use of standard device tiles.", defaultValue: "false")
+            input(type: "bool", name: "valueDeviceTile", title: "value device tiles", description: "A device that showcases the various use of value device tiles.", defaultValue: "false")
+            input(type: "bool", name: "presenceDeviceTile", title: "presence device tiles", description: "A device that showcases the various use of color control device tile.", defaultValue: "false")
+        }
+        section(title: "Other Tile Types") {
+            input(type: "bool", name: "carouselDeviceTile", title: "image carousel", description: "A device that showcases the various use of carousel device tile.", defaultValue: "false")
+            input(type: "bool", name: "sliderDeviceTile", title: "slider", description: "A device that showcases the various use of slider device tile.", defaultValue: "false")
+            input(type: "bool", name: "colorWheelDeviceTile", title: "color wheel", description: "A device that showcases the various use of color wheel device tile.", defaultValue: "false")
+        }
+    }
+}
+
+def installed() {
+    log.debug "Installed with settings: ${settings}"
+}
+
+def uninstalled() {
+    getChildDevices().each {
+        deleteChildDevice(it.deviceNetworkId)
+    }
+}
+
+def updated() {
+    log.debug "Updated with settings: ${settings}"
+    unsubscribe()
+    initializeDevices()
+}
+
+def initializeDevices() {
+    settings.each { key, value ->
+        log.debug "$key : $value"
+        def existingDevice = getChildDevices().find { it.name == key }
+        log.debug "$existingDevice"
+        if (existingDevice && !value) {
+            deleteChildDevice(existingDevice.deviceNetworkId)
+        } else if (!existingDevice && value) {
+            String dni = UUID.randomUUID()
+            log.debug "$dni"
+            addChildDevice(app.namespace, key, dni, null, [
+                label: labelMap()[key] ?: key,
+                completedSetup: true
+            ])
+        }
+    }
+}
+
+// Map the name of the Device to a proper Label
+def labelMap() {
+    [
+        genericDeviceTile: "Tile Multiattribute Generic",
+        lightingDeviceTile: "Tile Multiattribute Lighting",
+        thermostatDeviceTile: "Tile Multiattribute Thermostat",
+        mediaPlayerDeviceTile: "Tile Multiattribute Media Player",
+        videoPlayerDeviceTile: "Tile Multiattribute Video Player",
+        standardDeviceTile: "Tile Device Standard",
+        valueDeviceTile: "Tile Device Value",
+        presenceDeviceTile: "Tile Device Presence",
+        carouselDeviceTile: "Tile Device Carousel",
+        sliderDeviceTile: "Tile Device Slider",
+        colorWheelDeviceTile: "Tile Device Color Wheel"
+    ]
+}
diff --git a/official/door-jammed-notification.groovy b/official/door-jammed-notification.groovy
new file mode 100755 (executable)
index 0000000..b44ddcf
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ *  Door Jammed Notification
+ *
+ *  Copyright 2015 John Rucker
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Door Jammed Notification",
+    namespace: "JohnRucker",
+    author: "John.Rucker@Solar-current.com",
+    description: "Sends a SmartThings notification and text messages when your CoopBoss detects a door jam.",
+    category: "My Apps",
+    iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png",
+    iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png",
+    iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png")
+
+preferences {
+    section("When the door state changes") {
+        paragraph "Send a SmartThings notification when the coop's door jammed and did not close."
+               input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false            
+        input("recipients", "contact", title: "Recipients", description: "Send notifications to") {
+               input "phone", "phone", title: "Phone number?", required: true}
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(doorSensor, "doorState", coopDoorStateHandler)
+}
+
+def coopDoorStateHandler(evt) {
+       if (evt.value == "jammed"){
+        def msg = "WARNING ${doorSensor.displayName} door is jammed and did not close!"
+        log.debug "WARNING ${doorSensor.displayName} door is jammed and did not close, texting $phone"
+
+        if (location.contactBookEnabled) {
+            sendNotificationToContacts(msg, recipients)
+        }
+        else {
+            sendPush(msg)
+            if (phone) {
+                sendSms(phone, msg)
+            }
+        }
+       }        
+}
\ No newline at end of file
diff --git a/official/door-knocker.groovy b/official/door-knocker.groovy
new file mode 100755 (executable)
index 0000000..53ca7d9
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ *  Door Knocker
+ *
+ *  Author: brian@bevey.org
+ *  Date: 9/10/13
+ *
+ *  Let me know when someone knocks on the door, but ignore
+ *  when someone is opening the door.
+ */
+
+definition(
+    name: "Door Knocker",
+    namespace: "imbrianj",
+    author: "brian@bevey.org",
+    description: "Alert if door is knocked, but not opened.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
+)
+
+preferences {
+  section("When Someone Knocks?") {
+    input name: "knockSensor", type: "capability.accelerationSensor", title: "Where?"
+  }
+
+  section("But not when they open this door?") {
+    input name: "openSensor", type: "capability.contactSensor", title: "Where?"
+  }
+
+  section("Knock Delay (defaults to 5s)?") {
+    input name: "knockDelay", type: "number", title: "How Long?", required: false
+  }
+
+  section("Notifications") {
+    input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
+    input "phone", "phone", title: "Send a Text Message?", required: false
+  }
+}
+
+def installed() {
+  init()
+}
+
+def updated() {
+  unsubscribe()
+  init()
+}
+
+def init() {
+  state.lastClosed = 0
+  subscribe(knockSensor, "acceleration.active", handleEvent)
+  subscribe(openSensor, "contact.closed", doorClosed)
+}
+
+def doorClosed(evt) {
+  state.lastClosed = now()
+}
+
+def doorKnock() {
+  if((openSensor.latestValue("contact") == "closed") &&
+     (now() - (60 * 1000) > state.lastClosed)) {
+    log.debug("${knockSensor.label ?: knockSensor.name} detected a knock.")
+    send("${knockSensor.label ?: knockSensor.name} detected a knock.")
+  }
+
+  else {
+    log.debug("${knockSensor.label ?: knockSensor.name} knocked, but looks like it was just someone opening the door.")
+  }
+}
+
+def handleEvent(evt) {
+  def delay = knockDelay ?: 5
+  runIn(delay, "doorKnock")
+}
+
+private send(msg) {
+  if(sendPushMessage != "No") {
+    log.debug("Sending push message")
+    sendPush(msg)
+  }
+
+  if(phone) {
+    log.debug("Sending text message")
+    sendSms(phone, msg)
+  }
+
+  log.debug(msg)
+}
diff --git a/official/door-state-to-color-light-hue-bulb.groovy b/official/door-state-to-color-light-hue-bulb.groovy
new file mode 100755 (executable)
index 0000000..db42954
--- /dev/null
@@ -0,0 +1,141 @@
+/**
+ *  CoopBoss Door Status to color
+ *
+ *  Copyright 2015 John Rucker
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Door State to Color Light (Hue Bulb)",
+    namespace: "JohnRucker",
+    author: "John Rucker",
+    description: "Change the color of your Hue bulbs based on your coop's door status.",
+    category: "My Apps",
+    iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png",
+    iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png",
+    iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png")
+
+
+preferences {
+       section("When the door opens/closese...") {
+       paragraph "Sets a Hue bulb or bulbs to a color based on your coop's door status:\r  unknown = white\r  open = blue\r  opening = purple\r  closed = green\r  closing = pink\r  jammed = red\r  forced close = orange."
+               input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false
+               input "bulbs", "capability.colorControl", title: "pick a bulb", required: true, multiple: true
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(doorSensor, "doorState", coopDoorStateHandler)
+}
+
+def coopDoorStateHandler(evt) {
+    log.debug "${evt.descriptionText}, $evt.value"
+       def color = "White"
+    def hueColor = 100
+    def saturation = 100
+    Map hClr = [:]
+    hClr.hex = "#FFFFFF"
+
+       switch(evt.value) {
+       case "open":
+               color = "Blue"
+            break;
+        case "opening":
+               color = "Purple"
+            break;
+        case "closed":
+               color = "Green"
+            break;
+       case "closing":
+               color = "Pink"
+            break;
+        case "jammed":
+               color = "Red"
+            break;
+        case "forced close":
+               color = "Orange"
+            break;
+        case "unknown":
+               color = "White"
+            break;        
+    }   
+       
+       switch(color) {
+               case "White":
+                       hueColor = 52
+                       saturation = 19
+                       break;
+               case "Daylight":
+                       hueColor = 53
+                       saturation = 91
+                       break;
+               case "Soft White":
+                       hueColor = 23
+                       saturation = 56
+                       break;
+               case "Warm White":
+                       hueColor = 20
+                       saturation = 80 //83
+                       break;
+               case "Blue":
+                       hueColor = 70
+            hClr.hex = "#0000FF"
+                       break;
+               case "Green":
+                       hueColor = 39
+            hClr.hex = "#00FF00"
+                       break;
+               case "Yellow":
+                       hueColor = 25
+            hClr.hex = "#FFFF00"            
+                       break;
+               case "Orange":
+                       hueColor = 10
+            hClr.hex = "#FF6000"
+                       break;
+               case "Purple":
+                       hueColor = 75
+            hClr.hex = "#BF7FBF"
+                       break;
+               case "Pink":
+                       hueColor = 83
+            hClr.hex = "#FF5F5F"
+                       break;
+               case "Red":
+                       hueColor = 100
+            hClr.hex = "#FF0000"
+                       break;
+       }    
+    
+    //bulbs*.on()
+    bulbs*.setHue(hueColor)
+       bulbs*.setSaturation(saturation)   
+    bulbs*.setColor(hClr)
+    
+    //bulbs.each{
+       //it.on()  // Turn the bulb on when open (this method does not come directly from the colorControl capability)
+       //it.setLevel(100)  // Make sure the light brightness is 100%       
+       //it.setHue(hueColor)
+               //it.setSaturation(saturation) 
+        //}        
+}
\ No newline at end of file
diff --git a/official/double-tap.groovy b/official/double-tap.groovy
new file mode 100755 (executable)
index 0000000..596d494
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Double Tap
+ *
+ *  Author: SmartThings
+ */
+definition(
+    name: "Double Tap",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Turn on or off any number of switches when an existing switch is tapped twice in a row.",
+    category: "Convenience",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
+)
+
+preferences {
+       section("When this switch is double-tapped...") {
+               input "master", "capability.switch", title: "Where?"
+       }
+       section("Turn on or off all of these switches as well") {
+               input "switches", "capability.switch", multiple: true, required: false
+       }
+       section("And turn off but not on all of these switches") {
+               input "offSwitches", "capability.switch", multiple: true, required: false
+       }
+       section("And turn on but not off all of these switches") {
+               input "onSwitches", "capability.switch", multiple: true, required: false
+       }
+}
+
+def installed()
+{
+       subscribe(master, "switch", switchHandler, [filterEvents: false])
+}
+
+def updated()
+{
+       unsubscribe()
+       subscribe(master, "switch", switchHandler, [filterEvents: false])
+}
+
+def switchHandler(evt) {
+       log.info evt.value
+
+       // use Event rather than DeviceState because we may be changing DeviceState to only store changed values
+       def recentStates = master.eventsSince(new Date(now() - 4000), [all:true, max: 10]).findAll{it.name == "switch"}
+       log.debug "${recentStates?.size()} STATES FOUND, LAST AT ${recentStates ? recentStates[0].dateCreated : ''}"
+
+       if (evt.physical) {
+               if (evt.value == "on" && lastTwoStatesWere("on", recentStates, evt)) {
+                       log.debug "detected two taps, turn on other light(s)"
+                       onSwitches()*.on()
+               } else if (evt.value == "off" && lastTwoStatesWere("off", recentStates, evt)) {
+                       log.debug "detected two taps, turn off other light(s)"
+                       offSwitches()*.off()
+               }
+       }
+       else {
+               log.trace "Skipping digital on/off event"
+       }
+}
+
+private onSwitches() {
+       (switches + onSwitches).findAll{it}
+}
+
+private offSwitches() {
+       (switches + offSwitches).findAll{it}
+}
+
+private lastTwoStatesWere(value, states, evt) {
+       def result = false
+       if (states) {
+
+               log.trace "unfiltered: [${states.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
+               def onOff = states.findAll { it.physical || !it.type }
+               log.trace "filtered:   [${onOff.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
+
+               // This test was needed before the change to use Event rather than DeviceState. It should never pass now.
+               if (onOff[0].date.before(evt.date)) {
+                       log.warn "Last state does not reflect current event, evt.date: ${evt.dateCreated}, state.date: ${onOff[0].dateCreated}"
+                       result = evt.value == value && onOff[0].value == value
+               }
+               else {
+                       result = onOff.size() > 1 && onOff[0].value == value && onOff[1].value == value
+               }
+       }
+       result
+}
diff --git a/official/dry-the-wetspot.groovy b/official/dry-the-wetspot.groovy
new file mode 100755 (executable)
index 0000000..17cf709
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ *  Dry the Wetspot
+ *
+ *  Copyright 2014 Scottin Pollock
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Dry the Wetspot",
+    namespace: "smartthings",
+    author: "Scottin Pollock",
+    description: "Turns switch on and off based on moisture sensor input.",
+    category: "Safety & Security",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png"
+)
+
+
+preferences {
+       section("When water is sensed...") {
+               input "sensor", "capability.waterSensor", title: "Where?", required: true
+       }
+       section("Turn on a pump...") {
+               input "pump", "capability.switch", title: "Which?", required: true
+       }
+}
+
+def installed() {
+       subscribe(sensor, "water.dry", waterHandler)
+       subscribe(sensor, "water.wet", waterHandler)
+}
+
+def updated() {
+       unsubscribe()
+       subscribe(sensor, "water.dry", waterHandler)
+       subscribe(sensor, "water.wet", waterHandler)
+}
+
+def waterHandler(evt) {
+       log.debug "Sensor says ${evt.value}"
+       if (evt.value == "wet") {
+               pump.on()
+       } else if (evt.value == "dry") {
+               pump.off()
+       }
+}
+
diff --git a/official/ecobee-connect.groovy b/official/ecobee-connect.groovy
new file mode 100755 (executable)
index 0000000..cc26358
--- /dev/null
@@ -0,0 +1,890 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *     Ecobee Service Manager
+ *
+ *     Author: scott
+ *     Date: 2013-08-07
+ *
+ *  Last Modification:
+ *      JLH - 01-23-2014 - Update for Correct SmartApp URL Format
+ *      JLH - 02-15-2014 - Fuller use of ecobee API
+ *      10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines
+ */
+
+include 'localization'
+
+definition(
+               name: "Ecobee (Connect)",
+               namespace: "smartthings",
+               author: "SmartThings",
+               description: "Connect your Ecobee thermostat to SmartThings.",
+               category: "SmartThings Labs",
+               iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
+               iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png",
+               singleInstance: true
+) {
+       appSetting "clientId"
+}
+
+preferences {
+       page(name: "auth", title: "ecobee", nextPage:"", content:"authPage", uninstall: true, install:true)
+}
+
+mappings {
+       path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
+       path("/oauth/callback") {action: [GET: "callback"]}
+}
+
+def authPage() {
+       log.debug "authPage()"
+
+       if(!atomicState.accessToken) { //this is to access token for 3rd party to make a call to connect app
+               atomicState.accessToken = createAccessToken()
+       }
+
+       def description
+       def uninstallAllowed = false
+       def oauthTokenProvided = false
+
+       if(atomicState.authToken) {
+               description = "You are connected."
+               uninstallAllowed = true
+               oauthTokenProvided = true
+       } else {
+               description = "Click to enter Ecobee Credentials"
+       }
+
+       def redirectUrl = buildRedirectUrl
+       log.debug "RedirectUrl = ${redirectUrl}"
+       // get rid of next button until the user is actually auth'd
+       if (!oauthTokenProvided) {
+               return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
+                       section() {
+                               paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
+                               href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description
+                       }
+               }
+       } else {
+               def stats = getEcobeeThermostats()
+               log.debug "thermostat list: $stats"
+               log.debug "sensor list: ${sensorsDiscovered()}"
+               return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) {
+                       section("") {
+                               paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings."
+                               input(name: "thermostats", title:"Select Your Thermostats", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats])
+                       }
+
+                       def options = sensorsDiscovered() ?: []
+                       def numFound = options.size() ?: 0
+                       if (numFound > 0)  {
+                               section("") {
+                                       paragraph "Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings."
+                                       input(name: "ecobeesensors", title: "Select Ecobee Sensors ({{numFound}} found)", messageArgs: [numFound: numFound], type: "enum", required:false, description: "Tap to choose", multiple:true, options:options)
+                               }
+                       }
+               }
+       }
+}
+
+def oauthInitUrl() {
+       log.debug "oauthInitUrl with callback: ${callbackUrl}"
+
+       atomicState.oauthInitState = UUID.randomUUID().toString()
+
+       def oauthParams = [
+                       response_type: "code",
+                       scope: "smartRead,smartWrite",
+                       client_id: smartThingsClientId,
+                       state: atomicState.oauthInitState,
+                       redirect_uri: callbackUrl
+       ]
+
+       redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}")
+}
+
+def callback() {
+       log.debug "callback()>> params: $params, params.code ${params.code}"
+
+       def code = params.code
+       def oauthState = params.state
+
+       if (oauthState == atomicState.oauthInitState) {
+               def tokenParams = [
+                       grant_type: "authorization_code",
+                       code      : code,
+                       client_id : smartThingsClientId,
+                       redirect_uri: callbackUrl
+               ]
+
+               def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}"
+
+               httpPost(uri: tokenUrl) { resp ->
+                       atomicState.refreshToken = resp.data.refresh_token
+                       atomicState.authToken = resp.data.access_token
+               }
+
+               if (atomicState.authToken) {
+                       success()
+               } else {
+                       fail()
+               }
+
+       } else {
+               log.error "callback() failed oauthState != atomicState.oauthInitState"
+       }
+
+}
+
+def success() {
+       def message = """
+        <p>Your ecobee Account is now connected to SmartThings!</p>
+        <p>Click 'Done' to finish setup.</p>
+    """
+       connectionStatus(message)
+}
+
+def fail() {
+       def message = """
+        <p>The connection could not be established!</p>
+        <p>Click 'Done' to return to the menu.</p>
+    """
+       connectionStatus(message)
+}
+
+def connectionStatus(message, redirectUrl = null) {
+       def redirectHtml = ""
+       if (redirectUrl) {
+               redirectHtml = """
+                       <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
+               """
+       }
+
+       def html = """
+        <!DOCTYPE html>
+        <html>
+            <head>
+                <meta name="viewport" content="width=640">
+                <title>Ecobee & SmartThings connection</title>
+                <style type="text/css">
+                    @font-face {
+                        font-family: 'Swiss 721 W01 Thin';
+                        src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
+                        src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
+                        font-weight: normal;
+                        font-style: normal;
+                    }
+                    @font-face {
+                        font-family: 'Swiss 721 W01 Light';
+                        src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
+                        src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
+                        url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
+                        font-weight: normal;
+                        font-style: normal;
+                    }
+                    .container {
+                        width: 90%;
+                        padding: 4%;
+                        text-align: center;
+                    }
+                    img {
+                        vertical-align: middle;
+                    }
+                    p {
+                        font-size: 2.2em;
+                        font-family: 'Swiss 721 W01 Thin';
+                        text-align: center;
+                        color: #666666;
+                        padding: 0 40px;
+                        margin-bottom: 0;
+                    }
+                    span {
+                        font-family: 'Swiss 721 W01 Light';
+                    }
+                </style>
+            </head>
+        <body>
+            <div class="container">
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/ecobee%402x.png" alt="ecobee icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
+                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
+                ${message}
+            </div>
+        </body>
+    </html>
+    """
+
+       render contentType: 'text/html', data: html
+}
+
+def getEcobeeThermostats() {
+       log.debug "getting device list"
+       atomicState.remoteSensors = []
+
+    def bodyParams = [
+        selection: [
+            selectionType: "registered",
+            selectionMatch: "",
+            includeRuntime: true,
+            includeSensors: true
+        ]
+    ]
+       def deviceListParams = [
+               uri: apiEndpoint,
+               path: "/1/thermostat",
+               headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
+        // TODO - the query string below is not consistent with the Ecobee docs:
+        // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
+               query: [format: 'json', body: toJson(bodyParams)]
+       ]
+
+       def stats = [:]
+       try {
+               httpGet(deviceListParams) { resp ->
+                       if (resp.status == 200) {
+                               resp.data.thermostatList.each { stat ->
+                                       atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors <<  stat.remoteSensors
+                                       def dni = [app.id, stat.identifier].join('.')
+                                       stats[dni] = getThermostatDisplayName(stat)
+                               }
+                       } else {
+                               log.debug "http status: ${resp.status}"
+                       }
+               }
+       } catch (groovyx.net.http.HttpResponseException e) {
+        log.trace "Exception polling children: " + e.response.data.status
+        if (e.response.data.status.code == 14) {
+            atomicState.action = "getEcobeeThermostats"
+            log.debug "Refreshing your auth_token!"
+            refreshAuthToken()
+        }
+    }
+       atomicState.thermostats = stats
+       return stats
+}
+
+Map sensorsDiscovered() {
+       def map = [:]
+       log.info "list ${atomicState.remoteSensors}"
+       atomicState.remoteSensors.each { sensors ->
+               sensors.each {
+                       if (it.type != "thermostat") {
+                               def value = "${it?.name}"
+                               def key = "ecobee_sensor-"+ it?.id + "-" + it?.code
+                               map["${key}"] = value
+                       }
+               }
+       }
+       atomicState.sensors = map
+       return map
+}
+
+def getThermostatDisplayName(stat) {
+    if(stat?.name) {
+        return stat.name.toString()
+    }
+    return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
+}
+
+def getThermostatTypeName(stat) {
+       return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart"
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       log.debug "initialize"
+       def devices = thermostats.collect { dni ->
+               def d = getChildDevice(dni)
+               if(!d) {
+                       d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"])
+                       log.debug "created ${d.displayName} with id $dni"
+               } else {
+                       log.debug "found ${d.displayName} with id $dni already exists"
+               }
+               return d
+       }
+
+       def sensors = ecobeesensors.collect { dni ->
+               def d = getChildDevice(dni)
+               if(!d) {
+                       d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"])
+                       log.debug "created ${d.displayName} with id $dni"
+               } else {
+                       log.debug "found ${d.displayName} with id $dni already exists"
+               }
+               return d
+       }
+       log.debug "created ${devices.size()} thermostats and ${sensors.size()} sensors."
+
+       def delete  // Delete any that are no longer in settings
+       if(!thermostats && !ecobeesensors) {
+               log.debug "delete thermostats ands sensors"
+               delete = getAllChildDevices() //inherits from SmartApp (data-management)
+       } else { //delete only thermostat
+               log.debug "delete individual thermostat and sensor"
+               if (!ecobeesensors) {
+                       delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) }
+               } else {
+                       delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) && !ecobeesensors.contains(it.deviceNetworkId)}
+               }
+       }
+       log.warn "delete: ${delete}, deleting ${delete.size()} thermostats"
+       delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management)
+
+       //send activity feeds to tell that device is connected
+       def notificationMessage = "is connected to SmartThings"
+       sendActivityFeeds(notificationMessage)
+       atomicState.timeSendPush = null
+       atomicState.reAttempt = 0
+
+       pollHandler() //first time polling data data from thermostat
+
+       //automatically update devices status every 5 mins
+       runEvery5Minutes("poll")
+
+}
+
+def pollHandler() {
+       log.debug "pollHandler()"
+       pollChildren(null) // Hit the ecobee API for update on all thermostats
+
+       atomicState.thermostats.each {stat ->
+               def dni = stat.key
+               log.debug ("DNI = ${dni}")
+               def d = getChildDevice(dni)
+               if(d) {
+                       log.debug ("Found Child Device.")
+                       d.generateEvent(atomicState.thermostats[dni].data)
+               }
+       }
+}
+
+def pollChildren(child = null) {
+    def thermostatIdsString = getChildDeviceIdsString()
+    log.debug "polling children: $thermostatIdsString"
+
+    def requestBody = [
+        selection: [
+            selectionType: "thermostats",
+            selectionMatch: thermostatIdsString,
+            includeExtendedRuntime: true,
+            includeSettings: true,
+            includeRuntime: true,
+            includeSensors: true
+        ]
+    ]
+
+       def result = false
+
+       def pollParams = [
+        uri: apiEndpoint,
+        path: "/1/thermostat",
+        headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
+        // TODO - the query string below is not consistent with the Ecobee docs:
+        // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
+        query: [format: 'json', body: toJson(requestBody)]
+    ]
+
+       try{
+               httpGet(pollParams) { resp ->
+                       if(resp.status == 200) {
+                log.debug "poll results returned resp.data ${resp.data}"
+                atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
+                updateSensorData()
+                storeThermostatData(resp.data.thermostatList)
+                result = true
+                log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
+            }
+               }
+       } catch (groovyx.net.http.HttpResponseException e) {
+               log.trace "Exception polling children: " + e.response.data.status
+        if (e.response.data.status.code == 14) {
+            atomicState.action = "pollChildren"
+            log.debug "Refreshing your auth_token!"
+            refreshAuthToken()
+        }
+       }
+       return result
+}
+
+// Poll Child is invoked from the Child Device itself as part of the Poll Capability
+def pollChild() {
+       def devices = getChildDevices()
+
+       if (pollChildren()) {
+               devices.each { child ->
+                       if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) {
+                               if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
+                                       def tData = atomicState.thermostats[child.device.deviceNetworkId]
+                                       log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
+                                       child.generateEvent(tData.data) //parse received message from parent
+                               } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
+                                       log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
+                                       return null
+                               }
+                       }
+               }
+       } else {
+               log.info "ERROR: pollChildren()"
+               return null
+       }
+
+}
+
+void poll() {
+       pollChild()
+}
+
+def availableModes(child) {
+       debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
+       debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
+
+       def tData = atomicState.thermostats[child.device.deviceNetworkId]
+
+       debugEvent("Data = ${tData}")
+
+       if(!tData) {
+               log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
+               return null
+       }
+
+       def modes = ["off"]
+
+    if (tData.data.heatMode) {
+        modes.add("heat")
+    }
+    if (tData.data.coolMode) {
+        modes.add("cool")
+    }
+    if (tData.data.autoMode) {
+        modes.add("auto")
+    }
+    if (tData.data.auxHeatMode) {
+        modes.add("auxHeatOnly")
+    }
+
+    return modes
+}
+
+def currentMode(child) {
+       debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
+       debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
+
+       def tData = atomicState.thermostats[child.device.deviceNetworkId]
+
+       debugEvent("Data = ${tData}")
+
+       if(!tData) {
+               log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
+               return null
+       }
+
+       def mode = tData.data.thermostatMode
+       return mode
+}
+
+def updateSensorData() {
+       atomicState.remoteSensors.each {
+               it.each {
+                       if (it.type != "thermostat") {
+                               def temperature = ""
+                               def occupancy = ""
+                               it.capability.each {
+                                       if (it.type == "temperature") {
+                                               if (it.value == "unknown") {
+                                                       temperature = "--"
+                                               } else {
+                                                       if (location.temperatureScale == "F") {
+                                                               temperature = Math.round(it.value.toDouble() / 10)
+                                                       } else {
+                                                               temperature = convertFtoC(it.value.toDouble() / 10)
+                                                       }
+
+                                               }
+                                       } else if (it.type == "occupancy") {
+                                               if(it.value == "true") {
+                            occupancy = "active"
+                        } else {
+                                                       occupancy = "inactive"
+                        }
+                                       }
+                               }
+                               def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code
+                               def d = getChildDevice(dni)
+                               if(d) {
+                                       d.sendEvent(name:"temperature", value: temperature)
+                                       d.sendEvent(name:"motion", value: occupancy)
+                               }
+                       }
+               }
+       }
+}
+
+def getChildDeviceIdsString() {
+       return thermostats.collect { it.split(/\./).last() }.join(',')
+}
+
+def toJson(Map m) {
+    return groovy.json.JsonOutput.toJson(m)
+}
+
+def toQueryString(Map m) {
+       return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
+}
+
+private refreshAuthToken() {
+       log.debug "refreshing auth token"
+
+       if(!atomicState.refreshToken) {
+               log.warn "Can not refresh OAuth token since there is no refreshToken stored"
+       } else {
+               def refreshParams = [
+                       method: 'POST',
+                       uri   : apiEndpoint,
+                       path  : "/token",
+                       query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
+               ]
+
+               def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
+               //changed to httpPost
+               try {
+                       def jsonMap
+                       httpPost(refreshParams) { resp ->
+                               if(resp.status == 200) {
+                                       log.debug "Token refreshed...calling saved RestAction now!"
+                                       debugEvent("Token refreshed ... calling saved RestAction now!")
+                                       saveTokenAndResumeAction(resp.data)
+                           }
+            }
+               } catch (groovyx.net.http.HttpResponseException e) {
+                       log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
+                       def reAttemptPeriod = 300 // in sec
+                       if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
+                               runIn(reAttemptPeriod, "refreshAuthToken")
+                       } else if (e.statusCode == 401) { // unauthorized
+                               atomicState.reAttempt = atomicState.reAttempt + 1
+                               log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
+                               if (atomicState.reAttempt <= 3) {
+                                       runIn(reAttemptPeriod, "refreshAuthToken")
+                               } else {
+                                       sendPushAndFeeds(notificationMessage)
+                                       atomicState.reAttempt = 0
+                               }
+                       }
+               }
+       }
+}
+
+/**
+ * Saves the refresh and auth token from the passed-in JSON object,
+ * and invokes any previously executing action that did not complete due to
+ * an expired token.
+ *
+ * @param json - an object representing the parsed JSON response from Ecobee
+ */
+private void saveTokenAndResumeAction(json) {
+    log.debug "token response json: $json"
+    if (json) {
+        debugEvent("Response = $json")
+        atomicState.refreshToken = json?.refresh_token
+        atomicState.authToken = json?.access_token
+        if (atomicState.action) {
+            log.debug "got refresh token, executing next action: ${atomicState.action}"
+            "${atomicState.action}"()
+        }
+    } else {
+        log.warn "did not get response body from refresh token response"
+    }
+    atomicState.action = ""
+}
+
+/**
+ * Executes the resume program command on the Ecobee thermostat
+ * @param deviceId - the ID of the device
+ *
+ * @retrun true if the command was successful, false otherwise.
+ */
+boolean resumeProgram(deviceId) {
+    def payload = [
+        selection: [
+            selectionType: "thermostats",
+            selectionMatch: deviceId,
+            includeRuntime: true
+        ],
+        functions: [
+            [
+                type: "resumeProgram"
+            ]
+        ]
+    ]
+    return sendCommandToEcobee(payload)
+}
+
+/**
+ * Executes the set hold command on the Ecobee thermostat
+ * @param heating - The heating temperature to set in fahrenheit
+ * @param cooling - the cooling temperature to set in fahrenheit
+ * @param deviceId - the ID of the device
+ * @param sendHoldType - the hold type to execute
+ *
+ * @return true if the command was successful, false otherwise
+ */
+boolean setHold(heating, cooling, deviceId, sendHoldType) {
+    // Ecobee requires that temp values be in fahrenheit multiplied by 10.
+    int h = heating * 10
+    int c = cooling * 10
+
+    def payload = [
+        selection: [
+            selectionType: "thermostats",
+            selectionMatch: deviceId,
+            includeRuntime: true
+        ],
+        functions: [
+            [
+                type: "setHold",
+                params: [
+                    coolHoldTemp: c,
+                    heatHoldTemp: h,
+                    holdType: sendHoldType
+                ]
+            ]
+        ]
+    ]
+
+    return sendCommandToEcobee(payload)
+}
+
+/**
+ * Executes the set fan mode command on the Ecobee thermostat
+ * @param heating - The heating temperature to set in fahrenheit
+ * @param cooling - the cooling temperature to set in fahrenheit
+ * @param deviceId - the ID of the device
+ * @param sendHoldType - the hold type to execute
+ * @param fanMode - the fan mode to set to
+ *
+ * @return true if the command was successful, false otherwise
+ */
+boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) {
+    // Ecobee requires that temp values be in fahrenheit multiplied by 10.
+    int h = heating * 10
+    int c = cooling * 10
+
+    def payload = [
+        selection: [
+            selectionType: "thermostats",
+            selectionMatch: deviceId,
+            includeRuntime: true
+        ],
+        functions: [
+            [
+                type: "setHold",
+                params: [
+                    coolHoldTemp: c,
+                    heatHoldTemp: h,
+                    holdType: sendHoldType,
+                    fan: fanMode
+                ]
+            ]
+        ]
+    ]
+
+       return sendCommandToEcobee(payload)
+}
+
+/**
+ * Sets the mode of the Ecobee thermostat
+ * @param mode - the mode to set to
+ * @param deviceId - the ID of the device
+ *
+ * @return true if the command was successful, false otherwise
+ */
+boolean setMode(mode, deviceId) {
+    def payload = [
+        selection: [
+            selectionType: "thermostats",
+            selectionMatch: deviceId,
+            includeRuntime: true
+        ],
+        thermostat: [
+            settings: [
+                hvacMode: mode
+            ]
+        ]
+    ]
+       return sendCommandToEcobee(payload)
+}
+
+/**
+ * Makes a request to the Ecobee API to actuate the thermostat.
+ * Used by command methods to send commands to Ecobee.
+ *
+ * @param bodyParams - a map of request parameters to send to Ecobee.
+ *
+ * @return true if the command was accepted by Ecobee without error, false otherwise.
+ */
+private boolean sendCommandToEcobee(Map bodyParams) {
+       def isSuccess = false
+       def cmdParams = [
+               uri: apiEndpoint,
+               path: "/1/thermostat",
+               headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
+               body: toJson(bodyParams)
+       ]
+
+       try{
+        httpPost(cmdParams) { resp ->
+            if(resp.status == 200) {
+                log.debug "updated ${resp.data}"
+                def returnStatus = resp.data.status.code
+                if (returnStatus == 0) {
+                    log.debug "Successful call to ecobee API."
+                    isSuccess = true
+                } else {
+                    log.debug "Error return code = ${returnStatus}"
+                    debugEvent("Error return code = ${returnStatus}")
+                }
+            }
+        }
+       } catch (groovyx.net.http.HttpResponseException e) {
+        log.trace "Exception Sending Json: " + e.response.data.status
+        debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
+        if (e.response.data.status.code == 14) {
+            // TODO - figure out why we're setting the next action to be pollChildren
+            // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error?
+            atomicState.action = "pollChildren"
+            log.debug "Refreshing your auth_token!"
+            refreshAuthToken()
+        } else {
+            debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
+            log.error "Authentication error, invalid authentication method, lack of credentials, etc."
+        }
+    }
+
+    return isSuccess
+}
+
+def getChildName()           { return "Ecobee Thermostat" }
+def getSensorChildName()     { return "Ecobee Sensor" }
+def getServerUrl()           { return "https://graph.api.smartthings.com" }
+def getShardUrl()            { return getApiServerUrl() }
+def getCallbackUrl()         { return "https://graph.api.smartthings.com/oauth/callback" }
+def getBuildRedirectUrl()    { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
+def getApiEndpoint()         { return "https://api.ecobee.com" }
+def getSmartThingsClientId() { return appSettings.clientId }
+
+def debugEvent(message, displayEvent = false) {
+       def results = [
+               name: "appdebug",
+               descriptionText: message,
+               displayed: displayEvent
+       ]
+       log.debug "Generating AppDebug Event: ${results}"
+       sendEvent (results)
+}
+
+//send both push notification and mobile activity feeds
+def sendPushAndFeeds(notificationMessage) {
+       log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
+       log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
+       if (atomicState.timeSendPush) {
+               if (now() - atomicState.timeSendPush > 86400000) { // notification is sent to remind user once a day
+                       sendPush("Your Ecobee thermostat " + notificationMessage)
+                       sendActivityFeeds(notificationMessage)
+                       atomicState.timeSendPush = now()
+               }
+       } else {
+               sendPush("Your Ecobee thermostat " + notificationMessage)
+               sendActivityFeeds(notificationMessage)
+               atomicState.timeSendPush = now()
+       }
+       atomicState.authToken = null
+}
+
+/**
+ * Stores data about the thermostats in atomicState.
+ * @param thermostats - a list of thermostats as returned from the Ecobee API
+ */
+private void storeThermostatData(thermostats) {
+    log.trace "Storing thermostat data: $thermostats"
+    def data
+    atomicState.thermostats = thermostats.inject([:]) { collector, stat ->
+        def dni = [ app.id, stat.identifier ].join('.')
+        log.debug "updating dni $dni"
+
+        data = [
+            coolMode: (stat.settings.coolStages > 0),
+            heatMode: (stat.settings.heatStages > 0),
+            deviceTemperatureUnit: stat.settings.useCelsius,
+            minHeatingSetpoint: (stat.settings.heatRangeLow / 10),
+            maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10),
+            minCoolingSetpoint: (stat.settings.coolRangeLow / 10),
+            maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10),
+            autoMode: stat.settings.autoHeatCoolFeatureEnabled,
+            deviceAlive: stat.runtime.connected == true ? "true" : "false",
+            auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
+            temperature: (stat.runtime.actualTemperature / 10),
+            heatingSetpoint: stat.runtime.desiredHeat / 10,
+            coolingSetpoint: stat.runtime.desiredCool / 10,
+            thermostatMode: stat.settings.hvacMode,
+            humidity: stat.runtime.actualHumidity,
+            thermostatFanMode: stat.runtime.desiredFanMode
+        ]
+        if (location.temperatureScale == "F") {
+            data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"]
+            data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"]
+            data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"]
+            data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"]
+            data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"]
+            data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"]
+            data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"]
+
+        }
+
+        if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") {
+            data["deviceTemperatureUnit"] = "F"
+
+        } else {
+            data["deviceTemperatureUnit"] = "C"
+        }
+
+        collector[dni] = [data:data]
+        return collector
+    }
+    log.debug "updated ${atomicState.thermostats?.size()} thermostats: ${atomicState.thermostats}"
+}
+
+def sendActivityFeeds(notificationMessage) {
+       def devices = getChildDevices()
+       devices.each { child ->
+               child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent
+       }
+}
+
+def convertFtoC (tempF) {
+       return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
+}
diff --git a/official/elder-care-daily-routine.groovy b/official/elder-care-daily-routine.groovy
new file mode 100755 (executable)
index 0000000..e032420
--- /dev/null
@@ -0,0 +1,143 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Elder Care
+ *
+ *  Author: SmartThings
+ *  Date: 2013-03-06
+ *
+ *  Stay connected to your loved ones. Get notified if they are not up and moving around 
+ *  by a specified time and/or if they have not opened a cabinet or door according to a set schedule. 
+ */
+
+definition(
+    name: "Elder Care: Daily Routine",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Stay connected to your loved ones. Get notified if they are not up and moving around by a specified time and/or if they have not opened a cabinet or door according to a set schedule.",
+    category: "Family",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png"
+)
+
+preferences {
+       section("Who are you checking on?") {
+               input "person1", "text", title: "Name?"
+       }
+       section("If there's no movement (optional, leave blank to not require)...") {
+               input "motion1", "capability.motionSensor", title: "Where?", required: false
+       }
+       section("or a door or cabinet hasn't been opened (optional, leave blank to not require)...") {
+               input "contact1", "capability.contactSensor", required: false
+       }
+       section("between these times...") {
+               input "time0", "time", title: "From what time?"
+               input "time1", "time", title: "Until what time?"
+       }
+       section("then alert the following people...") {
+               input("recipients", "contact", title: "People to notify", description: "Send notifications to") {
+                       input "phone1", "phone", title: "Phone number?", required: false
+               }
+       }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       schedule(time1, "scheduleCheck")
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated
+       unschedule()
+       schedule(time1, "scheduleCheck")
+}
+
+def scheduleCheck()
+{
+       if(noRecentContact() && noRecentMotion()) {
+               def person = person1 ?: "your elder"
+               def msg = "Alert! There has been no activity at ${person}'s place ${timePhrase}"
+               log.debug msg
+
+               if (location.contactBookEnabled) {
+                       sendNotificationToContacts(msg, recipients)
+               }
+               else {
+                       if (phone1) {
+                               sendSms(phone1, msg)
+                       } else {
+                               sendPush(msg)
+                       }
+               }
+       } else {
+               log.debug "There has been activity ${timePhrase}, not sending alert"
+       }
+}
+
+private noRecentMotion()
+{
+       if(motion1) {
+               def motionEvents = motion1.eventsSince(sinceTime)
+               log.trace "Found ${motionEvents?.size() ?: 0} motion events"
+               if (motionEvents.find { it.value == "active" }) {
+                       log.debug "There have been recent 'active' events"
+                       return false
+               } else {
+                       log.debug "There have not been any recent 'active' events"
+                       return true
+               }
+       } else {
+               log.debug "Motion sensor not enabled"
+               return true
+       }
+}
+
+private noRecentContact()
+{
+       if(contact1) {
+               def contactEvents = contact1.eventsSince(sinceTime)
+               log.trace "Found ${contactEvents?.size() ?: 0} door events"
+               if (contactEvents.find { it.value == "open" }) {
+                       log.debug "There have been recent 'open' events"
+                       return false
+               } else {
+                       log.debug "There have not been any recent 'open' events"
+                       return true
+               }
+       } else {
+               log.debug "Contact sensor not enabled"
+               return true
+       }
+}
+
+private getSinceTime() {
+       if (time0) {
+               return timeToday(time0, location?.timeZone)
+       }
+       else {
+               return new Date(now() - 21600000)
+       }
+}
+
+private getTimePhrase() {
+       def interval = now() - sinceTime.time
+       if (interval < 3600000) {
+               return "in the past ${Math.round(interval/60000)} minutes"
+       }
+       else if (interval < 7200000) {
+               return "in the past hour"
+       }
+       else {
+               return "in the past ${Math.round(interval/3600000)} hours"
+       }
+}
diff --git a/official/elder-care-slip-fall.groovy b/official/elder-care-slip-fall.groovy
new file mode 100755 (executable)
index 0000000..2c893ae
--- /dev/null
@@ -0,0 +1,124 @@
+/**
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ *  Elder Care: Slip & Fall
+ *
+ *  Author: SmartThings
+ *  Date: 2013-04-07
+ * 
+ */
+
+definition(
+    name: "Elder Care: Slip & Fall",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Monitors motion sensors in bedroom and bathroom during the night and detects if occupant does not return from the bathroom after a specified period of time.",
+    category: "Family",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png"
+)
+
+preferences {
+       section("Bedroom motion detector(s)") {
+               input "bedroomMotion", "capability.motionSensor", multiple: true
+       }
+       section("Bathroom motion detector") {
+               input "bathroomMotion", "capability.motionSensor"
+       }
+    section("Active between these times") {
+       input "startTime", "time", title: "Start Time"
+        input "stopTime", "time", title: "Stop Time"
+    }
+    section("Send message when no return within specified time period") {
+       input "warnMessage", "text", title: "Warning Message"
+        input "threshold", "number", title: "Minutes"
+    }
+    section("To these contacts") {
+               input("recipients", "contact", title: "Recipients", description: "Send notifications to") {
+                       input "phone1", "phone", required: false
+                       input "phone2", "phone", required: false
+                       input "phone3", "phone", required: false
+               }
+    }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       state.active = 0
+       subscribe(bedroomMotion, "motion.active", bedroomActive)
+    subscribe(bathroomMotion, "motion.active", bathroomActive)
+}
+
+def bedroomActive(evt) {
+       def start = timeToday(startTime, location?.timeZone)
+    def stop = timeToday(stopTime, location?.timeZone)
+    def now = new Date()
+    log.debug "bedroomActive, status: $state.ststus, start: $start, stop: $stop, now: $now"
+    if (state.status == "waiting") {
+       log.debug "motion detected in bedroom, disarming"
+       unschedule("sendMessage")
+        state.status = null
+    }
+    else {
+        if (start.before(now) && stop.after(now)) {
+            log.debug "motion in bedroom, look for bathroom motion"
+            state.status = "pending"
+        }
+        else {
+            log.debug "Not in time window"
+        }
+    }
+}
+
+def bathroomActive(evt) {
+       log.debug "bathroomActive, status: $state.status"
+       if (state.status == "pending") {
+       def delay = threshold.toInteger() * 60
+       state.status = "waiting"
+        log.debug "runIn($delay)"
+        runIn(delay, sendMessage)
+    }
+}
+
+def sendMessage() {
+       log.debug "sendMessage"
+       def msg = warnMessage
+    log.info msg
+
+       if (location.contactBookEnabled) {
+               sendNotificationToContacts(msg, recipients)
+       }
+       else {
+               sendPush msg
+               if (phone1) {
+                       sendSms phone1, msg
+               }
+               if (phone2) {
+                       sendSms phone2, msg
+               }
+               if (phone3) {
+                       sendSms phone3, msg
+               }
+       }
+    state.status = null
+}
diff --git a/official/energy-alerts.groovy b/official/energy-alerts.groovy
new file mode 100755 (executable)
index 0000000..bd394a1
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ *  Energy Saver
+ *
+ *  Copyright 2014 SmartThings
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Energy Alerts",
+    namespace: "smartthings",
+    author: "SmartThings",
+    description: "Get notified if you're using too much energy",
+    category: "Green Living",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png",
+    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png"
+)
+
+preferences {
+       section {
+               input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null)
+        input(name: "aboveThreshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.")
+        input(name: "belowThreshold", type: "number", title: "Or Reports Below...", required: true, description: "in either watts or kw.")
+       }
+    section {
+        input("recipients", "contact", title: "Send notifications to") {
+            input(name: "sms", type: "phone", title: "Send A Text To", description: null, required: false)
+            input(name: "pushNotification", type: "bool", title: "Send a push notification", description: null, defaultValue: true)
+        }
+    }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(meter, "power", meterHandler)
+}
+
+def meterHandler(evt) {
+
+    def meterValue = evt.value as double
+
+    if (!atomicState.lastValue) {
+       atomicState.lastValue = meterValue
+    }
+
+    def lastValue = atomicState.lastValue as double
+    atomicState.lastValue = meterValue
+
+    def dUnit = evt.unit ?: "Watts"
+
+    def aboveThresholdValue = aboveThreshold as int
+    if (meterValue > aboveThresholdValue) {
+       if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
+                   def msg = "${meter} reported ${evt.value} ${dUnit} which is above your threshold of ${aboveThreshold}."
+           sendMessage(msg)
+        } else {
+//             log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
+        }
+    }
+
+
+    def belowThresholdValue = belowThreshold as int
+    if (meterValue < belowThresholdValue) {
+       if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
+                   def msg = "${meter} reported ${evt.value} ${dUnit} which is below your threshold of ${belowThreshold}."
+           sendMessage(msg)
+        } else {
+//             log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"
+        }
+    }
+}
+
+def sendMessage(msg) {
+    if (location.contactBookEnabled) {
+        sendNotificationToContacts(msg, recipients)
+    }
+    else {
+        if (sms) {
+            sendSms(sms, msg)
+        }
+        if (pushNotification) {
+            sendPush(msg)
+        }
+    }
+}
diff --git a/official/energy-saver.groovy b/official/energy-saver.groovy
new file mode 100755 (executable)
index 0000000..7f7d536
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ *  Energy Saver
+ *
+ *  Copyright 2014 SmartThings
+ *
+ *  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.
+ *
+ */
+definition(
+    name: "Energy Saver",
+    namespace: "smartthings",
+    author: "SmartThings",
+               description: "Turn things off if you're using too much energy",
+    category: "Green Living",
+    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
+    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png",
+    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
+)
+
+preferences {
+       section {
+               input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null)
+        input(name: "threshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.")
+       }
+    section {
+       input(name: "switches", type: "capability.switch", title: "Turn Off These Switches", required: true, multiple: true, description: null)
+    }
+}
+
+def installed() {
+       log.debug "Installed with settings: ${settings}"
+       initialize()
+}
+
+def updated() {
+       log.debug "Updated with settings: ${settings}"
+       unsubscribe()
+       initialize()
+}
+
+def initialize() {
+       subscribe(meter, "power", meterHandler)
+}
+
+def meterHandler(evt) {
+    def meterValue = evt.value as double
+    def thresholdValue = threshold as int
+    if (meterValue > thresholdValue) {
+           log.debug "${meter} reported energy consumption above ${threshold}. Turning of switches."
+       switches.off()
+    }
+}
diff --git a/official/enhanced-auto-lock-door.groovy b/official/enhanced-auto-lock-door.groovy
new file mode 100755 (executable)
index 0000000..81bbdc9
--- /dev/null
@@ -0,0 +1,115 @@
+definition(
+    name: "Enhanced Auto Lock Door",
+    namespace: "Lock Auto Super Enhanced",
+    author: "Arnaud",
+    description: "Automatically locks a specific door after X minutes when closed  and unlocks it when open after X seconds.",
+    category: "Safety & Security",
+    iconUrl: "http://www.gharexpert.com/mid/4142010105208.jpg",
+    iconX2Url: "http://www.gharexpert.com/mid/4142010105208.jpg"
+)
+
+preferences{
+    section("Select the door lock:") {
+        input "lock1", "capability.lock", required: true
+    }
+    section("Select the door contact sensor:") {
+        input "contact", "capability.contactSensor", required: true
+    }   
+    section("Automatically lock the door when closed...") {
+        input "minutesLater", "number", title: "Delay (in minutes):", required: true
+    }
+    section("Automatically unlock the door when open...") {
+        input "secondsLater", "number", title: "Delay (in seconds):", required: true
+    }
+    section( "Notifications" ) {
+        input("recipients", "contact", title: "Send notifications to", required: false) {
+            input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
+        }
+    }
+}
+
+def installed(){
+    initialize()
+}
+
+def updated(){
+    unsubscribe()
+    unschedule()
+    initialize()
+}
+
+def initialize(){
+    log.debug "Settings: ${settings}"
+    subscribe(lock1, "lock", doorHandler, [filterEvents: false])
+    subscribe(lock1, "unlock", doorHandler, [filterEvents: false])  
+    subscribe(contact, "contact.open", doorHandler)
+    subscribe(contact, "contact.closed", doorHandler)
+}
+
+def lockDoor(){
+    log.debug "Locking the door."
+    lock1.lock()
+    if(location.contactBookEnabled) {
+        if ( recipients ) {
+            log.debug ( "Sending Push Notification..." ) 
+            sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients)
+        }
+    }
+    if (phoneNumber) {
+        log.debug("Sending text message...")
+        sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!")
+    }
+}
+
+def unlockDoor(){
+    log.debug "Unlocking the door."
+    lock1.unlock()
+    if(location.contactBookEnabled) {
+        if ( recipients ) {
+            log.debug ( "Sending Push Notification..." ) 
+            sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients)
+        }
+    }
+    if ( phoneNumber ) {
+        log.debug("Sending text message...")
+        sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!")
+    }
+}
+
+def doorHandler(evt){
+    if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...  
+        //def delay = (secondsLater) // runIn uses seconds
+        runIn( secondsLater, unlockDoor )   // ...schedule (in minutes) to unlock...  We don't want the door to be closed while the lock is engaged. 
+    }
+    else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
+        unschedule( unlockDoor ) // ...we don't need to unlock it later.
+    }
+    else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
+        unschedule( lockDoor ) // ...we don't need to lock it later.
+    }   
+    else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
+       //def delay = (minutesLater * 60) // runIn uses seconds
+        runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
+    }
+    else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
+        unschedule( lockDoor ) // ...we don't need to lock it later.
+    }
+    else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
+        //def delay = (minutesLater * 60) // runIn uses seconds
+        runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
+    }
+    else { //Opening or Closing door when locked (in case you have a handle lock)
+        log.debug "Unlocking the door."
+        lock1.unlock()
+        if(location.contactBookEnabled) {
+            if ( recipients ) {
+                log.debug ( "Sending Push Notification..." ) 
+                sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients)
+            }
+        }
+        if ( phoneNumber ) {
+            log.debug("Sending text message...")
+            sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!")
+        }
+    }
+}
\ No newline at end of file
diff --git a/official/every-element.groovy b/official/every-element.groovy
new file mode 100755 (executable)
index 0000000..417ae1c
--- /dev/null
@@ -0,0 +1,568 @@
+/**
+ *  Every Element
+ *
+ *  Copyright 2015 SmartThings
+ *
+ *  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.
+ *
+ */
+definition(
+        name: "Every Element",
+        namespace: "smartthings/examples",
+        author: "SmartThings",
+        description: "Every element demonstration app",
+        category: "SmartThings Internal",
+        iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
+        iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
+)
+
+preferences {
+    // landing page
+    page(name: "firstPage")
+
+    // PageKit
+    page(name: "buttonsPage")
+    page(name: "imagePage")
+    page(name: "inputPage")
+    page(name: "inputBooleanPage")
+    page(name: "inputIconPage")
+    page(name: "inputImagePage")
+    page(name: "inputDevicePage")
+    page(name: "inputCapabilityPage")
+    page(name: "inputRoomPage")
+    page(name: "inputModePage")
+    page(name: "inputSelectionPage")
+    page(name: "inputHubPage")
+    page(name: "inputContactBookPage")
+    page(name: "inputTextPage")
+    page(name: "inputTimePage")
+    page(name: "appPage")
+    page(name: "hrefPage")
+    page(name: "paragraphPage")
+    page(name: "videoPage")
+    page(name: "labelPage")
+    page(name: "modePage")
+
+    // Every element helper pages
+    page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
+    page(name: "flattenedPage")
+}
+
+def firstPage() {
+    dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
+        section {
+            href(page: "appPage", title: "Element: 'app'")
+            href(page: "buttonsPage", title: "Element: 'buttons'")
+            href(page: "hrefPage", title: "Element: 'href'")
+            href(page: "imagePage", title: "Element: 'image'")
+            href(page: "inputPage", title: "Element: 'input'")
+            href(page: "labelPage", title: "Element: 'label'")
+            href(page: "modePage", title: "Element: 'mode'")
+            href(page: "paragraphPage", title: "Element: 'paragraph'")
+            href(page: "videoPage", title: "Element: 'video'")
+        }
+        section {
+            href(page: "flattenedPage", title: "All of the above elements on a single page")
+        }
+    }
+}
+
+def inputPage() {
+    dynamicPage(name: "inputPage", title: "Links to every 'input' element") {
+        section {
+            href(page: "inputBooleanPage", title: "to boolean page")
+            href(page: "inputIconPage", title: "to icon page")
+            href(page: "inputImagePage", title: "to image page")
+            href(page: "inputSelectionPage", title: "to selection page")
+            href(page: "inputTextPage", title: "to text page")
+            href(page: "inputTimePage", title: "to time page")
+        }
+        section("subsets of selection input") {
+            href(page: "inputDevicePage", title: "to device selection page")
+            href(page: "inputCapabilityPage", title: "to capability selection page")
+            href(page: "inputRoomPage", title: "to room selection page")
+            href(page: "inputModePage", title: "to mode selection page")
+            href(page: "inputHubPage", title: "to hub selection page")
+            href(page: "inputContactBookPage", title: "to contact-book selection page")
+        }
+    }
+}
+
+def inputBooleanPage() {
+    dynamicPage(name: "inputBooleanPage") {
+        section {
+            paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`"
+        }
+        section {
+            input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null)
+            input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description")
+        }
+        section("defaultValue: 'true'") {
+            input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true")
+        }
+        section("with image") {
+            input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null)
+            input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
+        }
+    }
+}
+def inputIconPage() {
+    dynamicPage(name: "inputIconPage") {
+        section {
+            paragraph "`description` is not displayed for icon elements"
+            paragraph "`multiple` has no effect because you can only choose a single icon"
+        }
+        section("required: true") {
+            input(type: "icon", name: "iconRequired", title: "without description", required: true)
+            input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true)
+        }
+        section("with image") {
+            paragraph "The image specified will be replaced after an icon is selected"
+            input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
+        }
+    }
+}
+def inputImagePage() {
+    dynamicPage(name: "inputImagePage") {
+        section {
+            paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)"
+            paragraph "Go to the device preferences of a Mobile Presence device to see it in action"
+            paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences"
+            input(type: "image", title: "This is kind of what it looks like", required: false)
+        }
+    }
+}
+
+
+def optionsGroup(List groups, String title) {
+    def group = [values:[], order: groups.size()]
+    group.title = title ?: ""
+    groups << group
+    return groups
+}
+def addValues(List groups, String key, String value) {
+    def lastGroup = groups[-1]
+    lastGroup["values"] << [
+            key: key,
+            value: value,
+            order: lastGroup["values"].size()
+    ]
+    return groups
+}
+def listToMap(List original) {
+    original.inject([:]) { result, v ->
+        result[v] = v
+        return result
+    }
+}
+def addGroup(List groups, String title, values) {
+    if (values instanceof List) {
+        values = listToMap(values)
+    }
+
+    values.inject(optionsGroup(groups, title)) { result, k, v ->
+        return addValues(result, k, v)
+    }
+    return groups
+}
+def addGroup(values) {
+    addGroup([], null, values)
+}
+/* Example usage of options builder
+
+// Creating grouped options
+    def newGroups = []
+    addGroup(newGroups, "first group", ["foo", "bar", "baz"])
+    addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"])
+
+// simple list
+    addGroup(["a", "b", "c"])
+
+// simple map
+    addGroup(["a": "yes", "b": "no", "c": "maybe"])​​​​
+*/
+
+
+def inputSelectionPage() {
+
+    def englishOptions = ["One", "Two", "Three"]
+    def spanishOptions = ["Uno", "Dos", "Tres"]
+    def groupedOptions = []
+    addGroup(groupedOptions, "English", englishOptions)
+    addGroup(groupedOptions, "Spanish", spanishOptions)
+
+    dynamicPage(name: "inputSelectionPage") {
+
+        section("options variations") {
+            paragraph "tap these elements and look at the differences when selecting an option"
+            input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
+            input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
+        }
+
+        section("list vs map") {
+            paragraph "These should be identical in UI, but are different in code and will produce different settings"
+            input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"]))
+            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"]))
+        }
+
+        section("segmented") {
+            paragraph "segmented should only work if there are either 2 or 3 options to choose from"
+            input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
+            input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
+
+            paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
+            input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
+            input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
+
+            paragraph "specifying defaultValue still works with segmented selection elements"
+            input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
+        }
+
+        section("required: true") {
+            input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true)
+        }
+
+        section("multiple: true") {
+            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)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputTextPage() {
+    dynamicPage(name: "inputTextPage", title: "Every 'text' variation") {
+        section("style and functional differences") {
+            input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true)
+            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)
+        }
+        section("text") {
+            input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false)
+        }
+        section("password") {
+            input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false)
+        }
+        section("email") {
+            input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false)
+        }
+        section("phone") {
+            input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false)
+        }
+        section("decimal") {
+            input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false)
+        }
+        section("number") {
+            input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false)
+        }
+
+        section("specified ranges") {
+            paragraph "You can limit number and decimal inputs to a specific range."
+            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)
+            paragraph "Negative limits will add a negative symbol to the keyboard."
+            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)
+            paragraph "Specify * to not limit one side or the other."
+            input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false)
+            input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false)
+            paragraph "If you don't specify a range, it defaults to 0..*"
+        }
+    }
+}
+def inputTimePage() {
+    dynamicPage(name: "inputTimePage") {
+        section {
+            input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false)
+            input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false)
+            input(type: "time", name: "timeRequired", title: "required: true", required: true)
+        }
+    }
+}
+
+/// selection subsets
+def inputDevicePage() {
+
+    dynamicPage(name: "inputDevicePage") {
+
+        section("required: true") {
+            input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputCapabilityPage() {
+
+    dynamicPage(name: "inputCapabilityPage") {
+
+        section("required: true") {
+            input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputRoomPage() {
+
+    dynamicPage(name: "inputRoomPage") {
+
+        section("required: true") {
+            input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputModePage() {
+
+    dynamicPage(name: "inputModePage") {
+
+        section("required: true") {
+            input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputHubPage() {
+
+    dynamicPage(name: "inputHubPage") {
+
+        section("required: true") {
+            input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+def inputContactBookPage() {
+
+    dynamicPage(name: "inputContactBookPage") {
+
+        section("required: true") {
+            input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected")
+        }
+
+        section("multiple: true") {
+            input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
+        }
+
+        section("with image") {
+            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")
+        }
+    }
+}
+
+def appPage() {
+    dynamicPage(name: "appPage", title: "Every 'app' type") {
+        section {
+            paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
+        }
+        section("app") {
+            app(
+                    name: "app",
+                    title: "required:false, multiple:false",
+                    required: false,
+                    multiple: false,
+                    namespace: "Steve",
+                    appName: "Child SmartApp"
+            )
+            app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
+            app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
+            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")
+        }
+        section("multiple:true") {
+            app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
+        }
+        section("multiple:true with image") {
+            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")
+        }
+    }
+}
+
+def labelPage() {
+    dynamicPage(name: "labelPage", title: "Every 'Label' type") {
+        section("label") {
+            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."
+            paragraph "There are 3 here as an example. Never use more than 1 label element on a page."
+            label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
+            label(name: "labelRequired", title: "required:true", required: true, multiple: false)
+            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")
+        }
+    }
+}
+
+def modePage() {
+    dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
+        section("mode") {
+            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."
+            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."
+            paragraph "There are 4 here as an example. Never use more than 1 mode element on a page."
+            mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
+            mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
+            mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
+            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")
+        }
+    }
+}
+
+def paragraphPage() {
+    dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
+        section("paragraph") {
+            paragraph "This is how you should make a paragraph element"
+            paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
+        }
+    }
+}
+
+def hrefPage() {
+    dynamicPage(name: "hrefPage", title: "Every 'href' variation") {
+        section("stylistic differences") {
+            href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete")
+            href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
+            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")
+        }
+        section("functional differences") {
+            href(page: "deadEnd", title: "to a page within the app")
+            href(url: "http://www.google.com", title: "to a url using all defaults")
+            href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true)
+        }
+    }
+}
+
+def buttonsPage() {
+    dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
+        section("Simple Buttons") {
+            paragraph "If there are an odd number of buttons, the last button will span the entire view area."
+            buttons(name: "buttons1", title: "1 button", buttons: [
+                    [label: "foo", action: "foo"]
+            ])
+            buttons(name: "buttons2", title: "2 buttons", buttons: [
+                    [label: "foo", action: "foo"],
+                    [label: "bar", action: "bar"]
+            ])
+            buttons(name: "buttons3", title: "3 buttons", buttons: [
+                    [label: "foo", action: "foo"],
+                    [label: "bar", action: "bar"],
+                    [label: "baz", action: "baz"]
+            ])
+            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: [
+                    [label: "foo", action: "foo"],
+                    [label: "bar", action: "bar"]
+            ])
+        }
+        section("Colored Buttons") {
+            buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
+