Norris Security (Work in Progress)


(Chuck) #1

INTRODUCING NORRIS SECURITY. THE ONLY WAY TO ROUND-HOUSE KICK INTRUDERS.
ADAPTED FROM SMARTSECURITY.
FOR NOW, JUST ALLOWS OPTIONAL PUSH NOTIFICATIONS.
MORE FEATURES LATER.

    /**
 * 
 *
 *  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.
 *
 *  Norris Security
 *
 *  Author: Chuck Norris
 *  Date: 2014-04-08
 */
definition(
    name: "Norris Security",
    namespace: "smartthings",
    author: "Norris",
    description: "Alerts you when there are intruders but not when you just got up for a glass of water in the middle of the night + Push Notification on/off",
    category: "Safety & Security",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe@2x.png"
)

preferences {
    section("Sensors detecting an intruder") {
        input "intrusionMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
        input "intrusionContacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
    }
    section("Sensors detecting residents") {
        input "residentMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
    }
    section("Alarm settings and actions") {
        input "alarms", "capability.alarm", title: "Which Alarm(s)", multiple: true, required: false
        input "silent", "enum", options: ["Yes","No"], title: "Silent alarm only (Yes/No)"
        input "seconds", "number", title: "Delay in seconds before siren sounds"
        input "lights", "capability.switch", title: "Flash these lights (optional)", multiple: true, required: false
        input "newMode", "mode", title: "Change to this mode (optional)", required: false
    }
    section("Send Push Notification?") {
        input "sendPush", "bool", required: false,
              title: "Send Push Notification?"
    }
    section("Notify others (optional)") {
        input "textMessage", "text", title: "Send this message", multiple: false, required: false
        input("recipients", "contact", title: "Send notifications to") {
            input "phone", "phone", title: "To this phone", multiple: false, required: false
        }
    }
    section("Arm system when residents quiet for (default 3 minutes)") {
        input "residentsQuietThreshold", "number", title: "Time in minutes", required: false
    }
}

def installed() {
    log.debug "INSTALLED"
    subscribeToEvents()
    state.alarmActive = null
}

def updated() {
    log.debug "UPDATED"
    unsubscribe()
    subscribeToEvents()
    unschedule()
    state.alarmActive = null
    state.residentsAreUp = null
    state.lastIntruderMotion = null
    alarms?.off()
}

private subscribeToEvents()
{
    subscribe intrusionMotions, "motion", intruderMotion
    subscribe residentMotions, "motion", residentMotion
    subscribe intrusionContacts, "contact", contact
    subscribe alarms, "alarm", alarm
    subscribe(app, appTouch)
}

private residentsHaveBeenQuiet()
{
    def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
    def result = true
    def t0 = new Date(now() - threshold)
    for (sensor in residentMotions) {
        def recentStates = sensor.statesSince("motion", t0)
        if (recentStates.find{it.value == "active"}) {
            result = false
            break
        }
    }
    log.debug "residentsHaveBeenQuiet: $result"
    result
}

private intruderMotionInactive()
{
    def result = true
    for (sensor in intrusionMotions) {
        if (sensor.currentMotion == "active") {
            result = false
            break
        }
    }
    result
}

private isResidentMotionSensor(evt)
{
    residentMotions?.find{it.id == evt.deviceId} != null
}

def appTouch(evt)
{
    alarms?.off()
    state.alarmActive = false
}

// Here to handle old subscriptions
def motion(evt)
{
    if (isResidentMotionSensor(evt)) {
        log.debug "resident motion, $evt.name: $evt.value"
        residentMotion(evt)
    }
    else {
        log.debug "intruder motion, $evt.name: $evt.value"
        intruderMotion(evt)
    }
}

def intruderMotion(evt)
{
    if (evt.value == "active") {
        log.debug "motion by potential intruder, residentsAreUp: $state.residentsAreUp"
        if (!state.residentsAreUp) {
            log.trace "checking if residents have been quiet"
            if (residentsHaveBeenQuiet()) {
                log.trace "calling startAlarmSequence"
                startAlarmSequence()
            }
            else {
                log.trace "calling disarmIntrusionDetection"
                disarmIntrusionDetection()
            }
        }
    }
    state.lastIntruderMotion = now()
}

def residentMotion(evt)
{
    // Don't think we need this any more
    //if (evt.value == "inactive") {
    //    if (state.residentsAreUp) {
    //        startReArmSequence()
    //    }
    //}
}

def contact(evt)
{
    if (evt.value == "open") {
        // TODO - check for residents being up?
        if (!state.residentsAreUp) {
            if (residentsHaveBeenQuiet()) {
                startAlarmSequence()
            }
            else {
                disarmIntrusionDetection()
            }
        }
    }
}

def alarm(evt)
{
    log.debug "$evt.name: $evt.value"
    if (evt.value == "off") {
        alarms?.off()
        state.alarmActive = false
    }
}

private disarmIntrusionDetection()
{
    log.debug "residents are up, disarming intrusion detection"
    state.residentsAreUp = true
    scheduleReArmCheck()
}

private scheduleReArmCheck()
{
    def cron = "0 * * * * ?"
    schedule(cron, "checkForReArm")
    log.debug "Starting re-arm check, cron: $cron"
}

def checkForReArm()
{
    def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
    log.debug "checkForReArm: threshold is $threshold"
    // check last intruder motion
    def lastIntruderMotion = state.lastIntruderMotion
    log.debug "checkForReArm: lastIntruderMotion=$lastIntruderMotion"
    if (lastIntruderMotion != null)
    {
        log.debug "checkForReArm, time since last intruder motion: ${now() - lastIntruderMotion}"
        if (now() - lastIntruderMotion > threshold) {
            log.debug "re-arming intrusion detection"
            state.residentsAreUp = false
            unschedule()
        }
    }
    else {
        log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection"
    }    
}

private startAlarmSequence()
{
    if (state.alarmActive) {
        log.debug "alarm already active"
    }
    else {
        state.alarmActive = true
        log.debug "starting alarm sequence"

        if(sendPush){
        
        sendPush("Potential intruder detected!")
        }

        if (newMode) {
            setLocationMode(newMode)
        }

        if (silentAlarm()) {
            log.debug "Silent alarm only"
            alarms?.strobe()
            if (location.contactBookEnabled) {
                sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
            }
            else {
                if (phone) {
                    sendSms(phone, textMessage ?: "Potential intruder detected")
                }
            }
        }
        else {
            def delayTime = seconds
            if (delayTime) {
                alarms?.strobe()
                runIn(delayTime, "soundSiren")
                log.debug "Sounding siren in $delayTime seconds"
            }
            else {
                soundSiren()
            }
        }

        if (lights) {
            flashLights(Math.min((seconds/2) as Integer, 10))
        }
    }
}

def soundSiren()
{
    if (state.alarmActive) {
        log.debug "Sounding siren"
        if (location.contactBookEnabled) {
            sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
        }
        else {
            if (phone) {
                sendSms(phone, textMessage ?: "Potential intruder detected")
            }
        }
        alarms?.both()
        if (lights) {
            log.debug "continue flashing lights"
            continueFlashing()
        }
    }
    else {
        log.debug "alarm activation aborted"
    }
    unschedule("soundSiren") // Temporary work-around to scheduling bug
}

def continueFlashing()
{
    unschedule()
    if (state.alarmActive) {
        flashLights(10)
        schedule(util.cronExpression(now() + 10000), "continueFlashing")
    }
}

private flashLights(numFlashes) {
    def onFor = 1000
    def offFor = 1000

    log.debug "FLASHING $numFlashes times"
    def delay = 1L
    numFlashes.times {
        log.trace "Switch on after  $delay msec"
        lights?.on(delay: delay)
        delay += onFor
        log.trace "Switch off after $delay msec"
        lights?.off(delay: delay)
        delay += offFor
    }
}

private silentAlarm()
{
    silent?.toLowerCase() in ["yes","true","y"]
}

(Jared) #2

Care to explain some of the features of your roundhousing security app?


(Chuck) #3

FOR NOW, JUST THE OPTION FOR PUSH NOTIFICATIONS. ANY SUGGESTIONS FOR OTHER FEATURES?

PLAY A CHUCK NORRIS MP3 THROUGH SONOS?


:facepunch:


(Jared) #4

Those are definitely all caps.


(Chuck) #5

IT’S THE ONLY WAY TO CONVEY MY TONE.


:facepunch:


(The fish is still dead.) #6

< slow clap >

20 chars.


(Jake Fox) #7

Even though its in caps I somehow feel like its spoken very calmly.