Knockerz - Door Knocker Detection and Notification

I’ve submitted a SmartApp that can monitor multiple doors for a knock, then perform actions like turn on switches, dim lights, send push, text, PushBullet, or audio messages. If the knock is detected by a multisensor, or an accleration and contact sensor that have the same name, the app will check the specific contact sensor to see if the door is open or was opened within the last 60 seconds. If the sensors do not share the same name for a given door, the app will check if any door is open or was opened in the last 60 seconds.

I use the app to ring my Zooz Smart Door Chime when it detects a knock. The current Door Knocker app only monitors a single door, and only sends a push or text message. If anyone would like to try it out and give me feedback, let me know.

2 Likes

Is there a difference or enhancement to the app on Marketplace (“door knocker” in More category)I use this function on a couple of doors and any enhancement is welcome. Happy to help testing!

I too started with Door Knocker. My take allows you to monitor all of your doors in one app. If you use SmartThings MultiSensors, or have separate acceleration/contact sensors, but are named the same for a given door - for example, your acceleration and contact sensor for your front door are both named, “Front Door”, then my app will check your “Front Door” contact sensor when the “Front Door” acceleration sensor detects a knock. If your acceleration and contact sensors are separate and named differently, for example, “Front Door Acceleration” and “Front Door Contact”, then my app will check if any door is open or was opened within the last 60 seconds.

Additionally, Door Knocker will only send a push message, or a text message to one phone number. My app does push, PushBullet, and text up to four numbers, plus it can turn on multiple switches, set dimmer values, and send an audio message with the specific name of the door included.

1 Like

Here is the code if you’d like to self-publish and test.

/**
 *  Knockerz
 *
 *  Auther: paul.knight@delmarvacomputer.com
 *  Date: 6/17/17
 *
 *  Based on the work of brian@bevey.org in 2013.
 *
 *  Notifies when someone knocks on a door, but does not open it.
 *  Alerts are by push, SMS, PushBullet, audio, and/or by
 *  turning on a switch and/or dimming the device.
 */

definition(
    name: "Knockerz",
    namespace: "dca",
    author: "paul.knight@delmarvacomputer.com",
    description: "Alerts when there is a knock at a door.",
    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 {
  page name:"pageSetup"
  page name:"pageDoors"
  page name:"pageNotifications"
  page name:"pageAbout"
}

/**
 * PAGE METHODS
 **/
 
/**
 * Main page
 * The "Doors" and "Notification Options" configuration pages, along
 * with the "About" page are accessed from this page. The SmartApp
 * can also be renamed or uninstalled from here.
 *
 * @return a dynamically created main page
 */
def pageSetup() {
  LOG("pageSetup()")

  def pageProperties = [
    name:       "pageSetup",
    nextPage:   null,
    install:    true,
    uninstall:  state.installed
  ]

  return dynamicPage(pageProperties) {
    section("Setup Menu") {
      href "pageDoors", title:"Add/Remove Doors", description:"Tap to open"
      href "pageNotifications", title:"Notification Options", description:"Tap to open"
      href "pageAbout", title:"About Knockerz", description:"Tap to open"
    }
    section([title:"Options", mobileOnly:true]) {
      label title:"Assign a name", required:false
    }
  }
}

/**
 * "About" page
 * Identifies the auther and license.
 *
 * @return a dynamically created "About" page
 */
def pageAbout() {
  LOG("pageAbout()")

  def textAbout =
    "Version ${getVersion()}\n${textCopyright()}\n\n" +
    "You can contribute to the development of this app by making a" +
    "donation to paul.knight@delmarvacomputer.com via PayPal."

  // This URL has not been created yet
  def hrefInfo = [
    url:        "http://delmarvacomputer.github.io/knockerz/",
    style:      "embedded",
    title:      "Tap here for more information...",
    description:"http://delmarvacomputer.github.io/knockerz/",
    required:   false
  ]

  def pageProperties = [
    name:       "pageAbout",
    nextPage:   "pageSetup",
    uninstall:  false
  ]

  return dynamicPage(pageProperties) {
    section("About") {
      paragraph textAbout
      //href hrefInfo
    }
    section("License") {
      paragraph textLicense()
    }
  }
}

/**
 * "Doors" page
 * Configure acceleration, contact sensors, and delay to wait after
 * a knock is detected to see if the door opens.
 *
 * @return a dynamically created "Doors" page
 */
def pageDoors() {
  LOG("pageDoors()")

  def helpAbout =
    "Select acceleration and contact sensors, then " +
    "set delay after knock to see if door opens."
    
  def inputAccelerationSensors = [
    name:           "accelerationSensors",
    title:          "Listen For Knocks At",
    type:           "capability.accelerationSensor",
    multiple:       true,
    required:       true
  ]
  
  def inputContactSensors = [
    name:           "contactSensors",
    title:          "See If These Doors Open",
    type:           "capability.contactSensor",
    multiple:       true,
    required:       true
  ]

  def inputKnockDelay = [
    name:           "knockDelay",
    title:          "Knock Delay (default 5s)",
    type:           "number",
    required:       false
  ]
  
  def pageProperties = [
    name:           "pageDoors",
    nextPage:       "pageSetup",
    uninstall:      false
  ]

  return dynamicPage(pageProperties) {
    section("Add/Remove Doors") {
      paragraph helpAbout
    }
    section("Select Doors") {
      input inputAccelerationSensors
      input inputContactSensors
      input inputKnockDelay
    }
  }
}

/**
 * "Notification Options" page
 * Define what happens if someone is actually knocking on a door.
 * Switches can be turned on, and, optionally dimmed. A message
 * can be sent via push, text, PushBullet, or read aloud.
 *
 * @return a dynamically created "Notifications Options" page
 */
def pageNotifications() {
  LOG("pageNotifications()")
  
  def helpAbout =
    "How do you want to be notified of a knock at a" +
    "door? Turn on a switch, a chime, or dim a light. " +
    "Send a push or SMS message. Use PushBullet " +
    "or an audio announcement."

  def inputSwitches = [
    name:           "switches",
    type:           "capability.switch",
    title:          "Set these switches",
    multiple:       true,
    required:       false
  ]
  
  def inputDimmerLevel = [
    name:           "dimmerLevel",
    type:           "enum",
    metadata:       [values:["10%","20%","30%","40%","50%","60%","70%","80%","90%","100%"]],
    title:          "Dimmer Level",
    defaultValue:   "40%",
    required:       false
  ]
  
  def inputPushMessage = [
    name:           "pushMessage",
    type:           "bool",
    title:          "Notify on Knock",
    defaultValue:   true
  ]

  def inputPhone1 = [
    name:           "phone1",
    type:           "phone",
    title:          "Send to this number",
    required:       false
  ]

  def inputPhone2 = [
    name:           "phone2",
    type:           "phone",
    title:          "Send to this number",
    required:       false
  ]

  def inputPhone3 = [
    name:           "phone3",
    type:           "phone",
    title:          "Send to this number",
    required:       false
  ]

  def inputPhone4 = [
    name:           "phone4",
    type:           "phone",
    title:          "Send to this number",
    required:       false
  ]

  def inputPushbulletDevice = [
    name:           "pushbullet",
    type:           "device.pushbullet",
    title:          "Which Pushbullet devices?",
    multiple:       true,
    required:       false
  ]

  def inputAudioPlayers = [
    name:           "audioPlayer",
    type:           "capability.musicPlayer",
    title:          "Which audio players?",
    multiple:       true,
    required:       false
  ]

  def inputSpeechText = [
    name:           "speechText",
    type:           "text",
    title:          "Knock Phrase",
    defaultValue:   "There is a knock at the %door",
    required:       false
  ]

  def pageProperties = [
    name:           "pageNotifications",
    nextPage:       "pageSetup",
    uninstall:      false
  ]

  return dynamicPage(pageProperties) {
    section("Notification Options") {
      paragraph helpAbout
    }
    section("Turn On Switches") {
      input inputSwitches
      input inputDimmerLevel
    }
    section("Push Notifications") {
      input inputPushMessage
    }
    section("Text Messages (SMS)") {
      input inputPhone1
      input inputPhone2
      input inputPhone3
      input inputPhone4
    }
    section("Pushbullet Notifications") {
      input inputPushbulletDevice
    }
    section("Audio Notifications") {
      input inputAudioPlayers
      input inputSpeechText
    }
  }
}

def installed() {
    LOG("installed()")

    initialize()
    state.installed = true
}

def updated() {
    LOG("updated()")

    unsubscribe()
    initialize()
}

def initialize() {
  log.info "Knockerz. Version ${getVersion()}. ${textCopyright()}"
  LOG("settings: ${settings}")

  state.lastClosed = 0
  subscribe(settings.accelerationSensors, "acceleration.active", onMovement)
  subscribe(settings.contactSensors, "contact.closed", onContact)

  STATE()
}

/**
 * EVENT HANDLERS
 **/
 
/**
 * Check the specific contact sensor to see if the door is open or
 * was openned in the last 60 seconds.
 *
 * @param a map containing the name of the detecting acceleration sensor.
 */
def checkMultiSensor(data) {
  LOG("checkMultiSensor(${data.name})")
  
  def contactSensor = settings.contactSensors.find{ it.label == "${data.name}" || it.name == "${data.name}" }
  LOG("Using ${contactSensor?.label ?: contactSensor?.name} contact sensor")
  if ((contactSensor?.latestValue("contact") == "closed") && (now() - (60 * 1000) > state.lastClosed)) {
    LOG("${data.name} detected a knock.")
    notify("${data.name}")
  } else {
    LOG("${data.name} detected acceleration, but appears to be just someone opening the door.")
  }
}

/**
 * Check if any door is open or was openned in the last 60 seconds.
 *
 * @param a map containing the name of the detecting acceleration sensor.
 */
def checkAnySensor(data) {
  LOG("checkAnySensor(${data.name})")
  
  if (settings.contactSensors.any { it.latestValue("contact") == "open" }) {
    LOG("${data.name} knocked, but a door is open.")
  } else {
    if (now() - (60 * 1000) > state.lastClosed) {
      LOG("${data.name} detected a knock.")
      notify("${data.name}")
    } else {
      LOG("${data.name} detected acceleration, but appears to be just someone opening the door.")
    }
  }
}

/**
 * Acceleration Event Handler
 * Use one of the check handlers depending on whether we can
 * specifically identify the contact sensor or not.
 *
 * @param an acceleration event object
 */
def onMovement(evt) {
  LOG("onMovement(${evt.displayName})")
  
  def delay = (settings.knockDelay == null) ? 5 : settings.knockDelay
  def contactSensor = settings.contactSensors.find{ it.label == "${evt.displayName}" || it.name == "${evt.displayName}" }
  if (contactSensor) {
    runIn(delay, "checkMultiSensor", [data: [name: "${evt.displayName}"]])
  } else {
    LOG("${evt.displayName} is a ${accelerationSensor.name}")
    runIn(delay, "checkAnySensor", [data: [name: "${evt.displayName}"]])
  }
}

/**
 * Contact Event Handler
 * Saves the last time a contact was closed.
 *
 * @param a contact event object
 */
def onContact(evt) {
  LOG("onContact(${evt.displayName})")
  state.lastClosed = now()
}

/**
 * NOTIFICATION HANDLERS
 **/
 
/**
 * Main notification processor
 * Turns on and dims switches, calls additional notification methods.
 *
 * @param the name of the acceleration sensor that detected the knock.
 */
private notify(name) {
  LOG("notify(${name})")
  
  def msg = "${name} detected a knock."

  // Only turn on those switches that are currently off
  def switchesOn = settings.switches?.findAll { it?.currentSwitch == "off" }
  LOG("switchesOn: ${switchesOn}")
  if (switchesOn) {
    switchesOn*.on()
  }

  // TODO: Add camera support?
  //settings.cameras*.take()

  notifyPush(msg)
  notifyText(msg)
  notifyPushBullet(msg)
  notifyVoice(name)
}

/**
 * Process a push message
 *
 * @param the message to send
 */
private def notifyPush(msg) {
  LOG("notifyPush(${msg})")

  if (settings.pushMessage) {
    // sendPush can throw an exception
    try {
      sendPush(msg)
    } catch (e) {
      log.error e
    }
  } else {
    sendNotificationEvent(msg)
  }
}

/**
 * Process a text message
 *
 * @param the message to send
 */
private def notifyText(msg) {
  LOG("notifyText(${msg})")

  if (settings.phone1) {
    sendSms(phone1, msg)
  }

  if (settings.phone2) {
    sendSms(phone2, msg)
  }

  if (settings.phone3) {
    sendSms(phone3, msg)
  }

  if (settings.phone4) {
    sendSms(phone4, msg)
  }
}

/**
 * Process a PushBullet message
 *
 * @param the message to send
 */
private def notifyPushBullet(msg) {
  if (settings.pushbullet) {
    settings.pushbullet*.push(location.name, msg)
  }   
}

/**
 * Process a text-to-speech message. Note that the string
 * '%door' in the message text will be replaced with the
 * name of the acceleration sensor that detected the knock.
 *
 * @param the name of the acceleration sensor that detected the knock.
 */
private def notifyVoice(name) {
  LOG("notifyVoice(${name})")

  if (!settings.audioPlayer) {
    return
  }

  // Replace %door with name
  def phrase = settings.speechText.replaceAll('%door', name)

  if (phrase) {
    settings.audioPlayer*.playText(phrase)
  }
}

private def getVersion() {
  return "1.0.0"
}

private def textCopyright() {
  def text = "Copyright © 2017 Delmarva Computer Associates LLC"
}

private def textLicense() {
  def text =
    "This program is free software: you can redistribute it and/or " +
    "modify it under the terms of the GNU General Public License as " +
    "published by the Free Software Foundation, either version 3 of " +
    "the License, or (at your option) any later version.\n\n" +
    "This program is distributed in the hope that it will be useful, " +
    "but WITHOUT ANY WARRANTY; without even the implied warranty of " +
    "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " +
    "General Public License for more details.\n\n" +
    "You should have received a copy of the GNU General Public License " +
    "along with this program. If not, see <http://www.gnu.org/licenses/>."
}

private def LOG(message) {
  log.trace message
}

private def STATE() {
  log.trace "state: ${state}"
}
3 Likes

@AstralNyt - any updates to this? Do you have a github I can pull it from directly?

The posted code is the latest. I made a pull request which should have the same thing.

Great Smart App, just what i was looking for. Cheers

Glad you found it useful!

I found that this was a bit faster than Super Notifier to execute, probably because it’s a single Smartapp with no child apps - but that it didn’t work with Speech Synthesis devices (i.e. Galaxy Home Mini and other smart speakers).

I’ve modified the code to allow these devices to function. Do not use this version if you use a TTS device such as Sonos.

I’d rather integrate speech devices in with TTS rather than making it either or. I’ll look at merging speech devices in.

edit: I edited the version on my GitHub to support both TTS and Speech Synthesis devices, you can choose your preference in the settings and you can announce on both types of speaker at once.

1 Like

Absolutely! That’s why I put this out there. The work you did is great, and I will be looking at taking what you’ve done and combining it with the older TTS devices. Thank you for helping to make it better!

@AstralNyt @mvevitsis

Hi i’m very interested in this automation. I’m a complete newbie and have no idea how to use this. I have only just installed webcore and I’m in the process of leaning how to do this. Any help with this automation would be appreciated. Is it possible to take this code and make a piston if so how is that done.

many thanks Bruny41

This is a standalone application that you can install into your “My Apps”. Either cut and paste the code directly, or from this GitHub repo. Then publish it for yourself, go into SmartThings, add the automation from your “My Apps” folder, and step through the configuration. I’ve made some recent bug fixes and retested everything. The repo has the latest code.

Just an update that I have fixed some bugs, retested and placed the latest code in this GitHub repo.

Just released v2.0, major workflow and UI revisions, some new functionality. Recently upgraded to the latest Samsung MultiSensors. These are much more sensitive than the previous models. I added in some functionality to allow the app to be temporarily disabled when the z-wave lock is activated. That was enough acceleration for the sensors to trip! I created a “Knockerz Enabled” sim switch and specified that with the “Set on afterward” option. Then any scene that locked/unlocked would turn that sim switch off, and Knockerz would re-enable it.

Also, I’ve integrated with “Echo Speaks” to allow Knockerz to announce a knock was detected on one, few or all of your Alexa devices. I now use that instead of the door chime.

FYI, I merged your additions into the Git baseline. Ended up creating a separate page for voice options. I will play around with non-TTS audio options at some point, but, for now, TTS, speech synthesis, and Amazon Echo are now supported. Thanks again!

Just added this through my developer portal and was able to add the SmartApp, but after I finish the configuration, it remains under the “Setup Not Complete” section at the bottom of SmartApps.

Anyone have an idea why that would be?