Changes in classes: new concept for latest value + all types of events generated...
[smartthings-infrastructure.git] / Extractor / App2 / App2.groovy
index 61b6434be9f296ed50f595d13aaae68ce8dde5cf..3c4c9d111f95e7f857ebd96e67b00836f366db1a 100644 (file)
 /**
- *  Beacon Control
+ *  Medicine Management - Contact Sensor
  *
- *  Copyright 2014 Physical Graph Corporation
+ *  Copyright 2016 Jim Mangione
  *
  *  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.
  *
+ * Logic: 
+ * --- Send notification at the medicine reminder time IF draw wasn't alread opened in past 60 minutes
+ * --- If draw still isn't open 10 minutes AFTER reminder time, LED will turn RED.
+ * --- ----- Once draw IS open, LED will return back to it's original color
+ *
  */
-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: "timeIntervalInput", title: "Only during a certain time") {
-               section {
-                       input "starting", "time", title: "Starting", required: false
-                       input "ending", "time", title: "Ending", required: false
-               }
-       }
-       
-       page(name: "mainPage")
-}
+import groovy.time.TimeCategory 
 
-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
-               }
+definition(
+    name: "Medicine Management - Contact Sensor",
+    namespace: "MangioneImagery",
+    author: "Jim Mangione",
+    description: "This supports devices with capabilities of ContactSensor and ColorControl (LED). It sends an in-app and ambient light notification if you forget to open the drawer or cabinet where meds are stored. A reminder will be set to a single time per day. If the draw or cabinet isn't opened within 60 minutes of that reminder, an in-app message will be sent. If the draw or cabinet still isn't opened after an additional 10 minutes, then an LED light turns red until the draw or cabinet is opened",
+    category: "Health & Wellness",
+    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")
 
-               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"
-               }
+preferences {
 
-               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"
+       section("My Medicine Draw/Cabinet"){
+               input "deviceContactSensor", "capability.contactSensor", title: "Opened Sensor" 
+       } 
 
-                       input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
-                               options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
+    section("Remind me to take my medicine at"){
+        input "reminderTime", "time", title: "Time"
+    }
+    
+    // NOTE: Use REAL device - virtual device causes compilation errors
+    section("My LED Light"){
+       input "deviceLight", "capability.colorControl", title: "Smart light"
+    }
 
-                       input "modes", "mode", title: "Only when mode is", multiple: true, required: false
-               }
-       }
 }
 
-// Lifecycle management
 def installed() {
-       log.debug "<beacon-control> Installed with settings: ${settings}"
+       log.debug "Installed with settings: ${settings}"
+       
        initialize()
 }
 
 def updated() {
-       log.debug "<beacon-control> Updated with settings: ${settings}"
+       log.debug "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)
-            }
+    // will stop LED notification incase it was set by med reminder
+    subscribe(deviceContactSensor, "contact", contactHandler)
+
+    // how many minutes to look in the past from the reminder time, for an open draw
+    state.minutesToCheckOpenDraw = 60
+    
+    // is true when LED notification is set after exceeding 10 minutes past reminder time
+    state.ledNotificationTriggered = false
+    
+    // Set a timer to run once a day to notify if draw wasn't opened yet
+    schedule(reminderTime, checkOpenDrawInPast)
+   
+}
+
+// Should turn off any LED notification on OPEN state
+def contactHandler(evt){
+       if (evt.value == "open") {
+        // if LED notification triggered, reset it.
+        log.debug "Cabinet opened"
+        if (state.ledNotificationTriggered) {
+            resetLEDNotification()
         }
        }
 }
 
-// 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]
+// If the draw was NOT opened within 60 minutes of the timer send notification out.
+def checkOpenDrawInPast(){
+       log.debug "Checking past 60 minutes of activity from $reminderTime"
+    
+    // check activity of sensor for past 60 minutes for any OPENED status
+    def cabinetOpened = isOpened(state.minutesToCheckOpenDraw)
+       log.debug "Cabinet found opened: $cabinetOpened"
+    
+    // if it's opened, then do nothing and assume they took their meds
+    if (!cabinetOpened) {    
+       sendNotification("Hi, please remember to take your meds in the cabinet")
+       
+       // if no open activity, send out notification and set new reminder    
+        def reminderTimePlus10 = new Date(now() + (10 * 60000))
+
+        // needs to be scheduled if draw wasn't already opened
+        runOnce(reminderTimePlus10, checkOpenDrawAfterReminder)
+    }
+}
+
+// If the draw was NOT opened after 10 minutes past reminder, use LED notification
+def checkOpenDrawAfterReminder(){
+       log.debug "Checking additional 10 minutes of activity from $reminderTime"
+    
+    // check activity of sensor for past 10 minutes for any OPENED status
+    def cabinetOpened = isOpened(10)    
+    
+       log.debug "Cabinet found opened: $cabinetOpened"
+        
+    // if no open activity, blink lights
+    if (!cabinetOpened) {
+       log.debug "Set LED to Notification color"
+        setLEDNotification()
+    }
+    
+}
+
+// Helper function for sending out an app notification
+def sendNotification(msg){
+        log.debug "Message Sent: $msg"
+        sendPush(msg)
+}
+
+// Check if the sensor has been opened since the minutes entered
+// Return true if opened found, else false.
+def isOpened(minutes){
+    // query last X minutes of activity log    
+    def previousDateTime = new Date(now() - (minutes * 60000))
+    
+    // capture all events recorded
+    def evts = deviceContactSensor.eventsSince(previousDateTime)   
+    def cabinetOpened = false
+    if (evts.size() > 0) {
+        evts.each{
+            if(it.value == "open") {
+                cabinetOpened = true 
+            }
+        }
        }
-       return phoneName
+    
+    return cabinetOpened
 }
 
-private hideOptionsSection() {
-       (starting || ending || days || modes) ? false : true
-}
+// Saves current color and sets the light to RED
+def setLEDNotification(){
 
-private getAllOk() {
-       modeOk && daysOk && timeOk
-}
+       state.ledNotificationTriggered = true
+    
+       // turn light back off when reset is called if it was originally off
+       state.ledState = deviceLight.currentValue("switch")
 
-private getModeOk() {
-       def result = !modes || modes.contains(location.mode)
-       log.trace "<beacon-control> modeOk = $result"
-       result
-}
+       // set light to RED and store original color until stopped    
+    state.origColor = deviceLight.currentValue("hue")
+    deviceLight.on()
+    deviceLight.setHue(100)
+    
+    log.debug "LED set to RED. Original color stored: $state.origColor"
 
-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
-}
+// Sets the color back to the original saved color
+def resetLEDNotification(){
 
-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") : ""
-}
+       state.ledNotificationTriggered = false
+    
+    // return color to original
+    log.debug "Reset LED color to: $state.origColor"
+    if (state.origColor != null) {
+       deviceLight.setHue(state.origColor)
+    }
+    
+    // if the light was turned on just for the notification, turn it back off now
+    if (state.ledState == "off") {
+       deviceLight.off()
+    }
 
-private list(Object names) {
-       return names[0]
 }