/**
- * Notify If Left Unlocked
+ * Beacon Control
*
- * Copyright 2014 George Sudarkoff
+ * 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:
* for the specific language governing permissions and limitations under the License.
*
*/
-
definition(
- name: "Notify If Left Unlocked",
- namespace: "com.sudarkoff",
- author: "George Sudarkoff",
- description: "Send a push or SMS notification (and lock, if it's closed) if a door is left unlocked for a period of time.",
- category: "Safety & Security",
- iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
- iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
-
+ 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 {
- section("If this lock...") {
- input "aLock", "capability.lock", multiple: false, required: true
- input "openSensor", "capability.contactSensor", title: "Open/close sensor (optional)", multiple: false, required: false
- }
- section("Left unlocked for...") {
- input "duration", "number", title: "How many minutes?", required: true
- }
- section("Notify me...") {
- input "pushNotification", "bool", title: "Push notification"
- input "phoneNumber", "phone", title: "Phone number (optional)", required: false
- input "lockIfClosed", "bool", title: "Lock the door if it's closed?"
- }
-}
-
-def installed()
-{
- initialize()
-}
-
-def updated()
-{
- unsubscribe()
- initialize()
-}
-
-def initialize()
-{
- log.trace "Initializing with: ${settings}"
- subscribe(aLock, "lock", lockHandler)
-}
-
-def lockHandler(evt)
-{
- log.trace "${evt.name} is ${evt.value}."
- if (evt.value == "locked") {
- log.debug "Canceling lock check because the door is locked..."
- unschedule(notifyUnlocked)
- }
- else {
- log.debug "Starting the countdown for ${duration} minutes..."
- state.retries = 0
- runIn(duration * 60, notifyUnlocked)
- }
-}
-
-def notifyUnlocked()
-{
- // if no open/close sensor specified, assume the door is closed
- def open = openSensor?.latestValue("contact") ?: "closed"
-
- def message = "${aLock.displayName} is left unlocked and ${open} for more than ${duration} minutes."
- log.trace "Sending the notification: ${message}."
- sendMessage(message)
-
- if (lockIfClosed) {
- if (open == "closed") {
- log.trace "And locking the door."
- sendMessage("Locking the ${aLock.displayName} as prescribed.")
- aLock.lock()
- }
- else {
- if (state.retries++ < 3) {
- log.trace "Door is open, can't lock. Rescheduling the check."
- sendMessage("Can't lock the ${aLock.displayName} because the door is open. Will try again in ${duration} minutes.")
- runIn(duration * 60, notifyUnlocked)
+ 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")
+}
+
+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)
}
- else {
- log.trace "The door is still open after ${state.retries} retries, giving up."
- sendMessage("Unable to lock the ${aLock.displayName} after ${state.retries} retries, giving up.")
+ log.debug "<beacon-control> msg: $msg"
+
+ if (pushNotification || phone) {
+ def options = [
+ method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"),
+ phone: phone
+ ]
+ sendNotification(msg, options)
}
}
- }
+ }
}
-def sendMessage(msg) {
- if (pushNotification) {
- sendPush(msg)
- }
- if (phoneNumber) {
- sendSMS(phoneNumber, msg)
- }
+// 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(Object names) {
+ return names[0]
+}