Konke motion sensor

Definitely understand that--I struggled for a long time trying to get it paired to ST in order to answer someone's question about whether it worked there, then I did some Googling and discovered I probably never would get it to work since ST is stuck on channel 19 (assuming that person was correct about this device, of course, but my experience is certainly consistent).

The biggest problem I'm finding so far is that not only does the device not send "motion inactive" status, it doesn't seem to continually send "motion active" status, either, if motion activity is continually sustained. I know the Xiaomi devices don't send "inactive" either, but it's my understanding that they will send at least another "active" at (or within?) their 60-second reset window if continual motion is detected--this one doesn't seem to. There might be some configuration I'm missing but will likely not be able to figure out, so I guess the most decent workaround would be to use it where you don't anticipate a lot of continual motion and set the "fake" timeout in the driver to a relatively high value.

Here's a heavily modified Xiaomi driver (really should have just started from a "regular" Zigbee driver and added in the reset timer since all the parsing is redone, but too late now), which might be a good starting point if anyone else is interested in getting this to work. There are some sections where I'm not sure what the device does because I haven't managed to get it to do that yet (e.g., battery reports). Maybe someone will find this useful. I'll probably keep the sensor since it was so cheap but don't plan on getting any more unless someone figures out the inactivity problem. :slight_smile:

/**
 *  Konke Motion Sensor (Kit-Pro BS Motion Sensor)
 *  Driver for Hubitat Elevation hub
 *  Version 0.1
 *
 *
 *  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.
 *
 *  Portions of this code are based on the Xioami "Original" Motion Sensor code for Hubitat by veeceeoh,
 *  which in turn was based on the on SmartThings device handler code by a4refillpad.
 *  Previous Hubitat and ST code contains contributions by alecm, alixjg, bspranger, gn0st1c, foz333, jmagnuson, mike.maxwell, rinkek, ronvandegraaf,
 *  snalee, tmleafs, twonk, & veeceeoh
 *
 */

import hubitat.zigbee.clusters.iaszone.ZoneStatus

metadata {
	definition (name: "Konke Motion Sensor", namespace: "RMoRobert", author: "Robert Morris") {
		capability "Motion Sensor"
		capability "Sensor"
		capability "Battery"

		attribute "lastCheckin", "String"
		attribute "lastMotion", "String"
		attribute "lastInactive", "String"
		attribute "batteryLastReplaced", "String"

		fingerprint profileId: "0104", deviceId: "0104", inClusters: "0000, 0001, 0003, 0500", outClusters: "0003", manufacturer: "Konke", model: "3AFE14010402000D", deviceJoinName: "Konke Motion Sensor"

		command "resetBatteryReplacedDate"
		command "resetToMotionInactive"
	}

	preferences {
		//Reset to No Motion Config
		input "motionreset", "number", title: "After motion is detected, wait ___ second(s) until resetting to inactive state. Default = 16 seconds (Hardware resets at 15 seconds)", description: "", range: "1..7200"
		//Battery Voltage Range
 		input name: "voltsmin", title: "Min Volts (0% battery = ___ volts, range 2.0 to 2.7). Default = 2.5 Volts", description: "", type: "decimal", range: "2..2.7"
 		input name: "voltsmax", title: "Max Volts (100% battery = ___ volts, range 2.8 to 3.4). Default = 3.0 Volts", description: "", type: "decimal", range: "2.8..3.4"
 		//Logging Message Config
 		input name: "infoLogging", type: "bool", title: "Enable info message logging", description: ""
 		input name: "debugLogging", type: "bool", title: "Enable debug message logging", description: ""
	}
}

// Parse incoming device messages to generate events
def parse(String description) {
    Map map = [:]
    logDebug("Parsing: $description")
	if (description?.startsWith('zone status')) {	    
        def zs = zigbee.parseZoneStatus(description)
        if (zs.alarm1 == 1 && zs.battery == 0 && zs.trouble == 0) {
            logInfo("Motion detected")
            map = parseMotion()
        }
        else if (zs.tamper == 1 && zs.battery == 1 && zs.trouble == 1 && zs.ac == 1) {
            log.debug "Device button pressed"
        }
        else {
            log.warn "Zone status message not parsed"
            if (debugLogging) {
                log.warn "zs.alarm1 = $zs.alarm1"
                log.warn "zs.alarm2 = $zs.alarm2"
                log.warn "zs.tamper = $zs.tamper"
                log.warn "zs.battery = $zs.battery"
                log.warn "zs.supervisionReports = $zs.supervisionReports"
                log.warn "zs.restoreReports = $zs.restoreReports"
                log.warn "zs.trouble = $zs.trouble"
                log.warn "zs.ac = $zs.ac"
                log.warn "zs.test = $zs.test"
                log.warn "zs.batteryDefect = $zs.batteryDefect"
            }
        }
    }
    else if (description?.startsWith("catchall") || description?.startsWith("read attr"))
    {
        Map descMap = zigbee.parseDescriptionAsMap(description)
        // Guessing at how battery might work...
        if (descMap.value) { // should also check if power config cluster...
            List<Map> descMaps = collectAttributes(descMap)
            def battMap = descMaps.find { it.attrInt == 0x0020 }
		    if (battMap) {
	            map = getBatteryResult(Integer.parseInt(battMap.value, 16))
	    	}
            if (map == [:] && descMaps.find { it.attrInt == 0x0021 }) log.warn "You may have guessed wrong on the battery cluster--check 0x0021?"
        }
        if (map == [:]) {
            log.warn ("Description map not parsed: $dm")
            
        }
    }
    else {
        log.warn "Description not parsed"
    }
    
    if (map != [:]) {
		logInfo(map.descriptionText)
		logDebug("Creating event $map")
		return createEvent(map)
	} else
		return [:]
}

// Generate map for motion active report
private parseMotion() {
	def seconds = motionreset ? motionreset : 16
	// The sensor only sends a motion detected message so reset to motion inactive is performed in code
	runIn(seconds, resetToMotionInactive)
	sendEvent(name: "lastMotion", value: now())
	return [
		name: 'motion',
		value: 'active',
		isStateChange: true,
		descriptionText: "Detected motion",
	]
}

// This is a guess...will want to see how device actually reports:
private getBatteryResult(rawValue) {
	logDebug("Parsing battery description: ${description}")
	def rawVolts = rawValue / 10
	def minVolts = voltsmin ? voltsmin : 2.5
	def maxVolts = voltsmax ? voltsmax : 3.0
	def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
	def roundedPct = Math.min(100, Math.round(pct * 100))
	def descText = "Battery level is ${roundedPct}% (${rawVolts} Volts)"
	def result = [
		name: 'battery',
		value: roundedPct,
		unit: "%",
		isStateChange: true,
		descriptionText: descText
	]
    logInfo(decText)
	return result
}

// If currently in 'active' motion detected state, resetToMotionInactive() resets to 'inactive' state and displays 'no motion'
// Seems problematic for Konke because it does not repeatedly send "active" events if continually active (Xiaomi must?)
// May want to increase time...
def resetToMotionInactive() {
	if (device.currentState('motion')?.value == "active") {
		def seconds = motionreset ? motionreset : 16
		def descText = "Motion reset to inactive after ${seconds}s"
		sendEvent(name: "lastMotion", value: now())
		sendEvent(
			name:'motion',
			value:'inactive',
			isStateChange: true,
			descriptionText: descText
		)
		logInfo(descText)
	}
}

//Reset the batteryLastReplaced date to current date
def resetBatteryReplacedDate(paired) {
	def newlyPaired = paired ? " for newly paired sensor" : ""
	sendEvent(name: "batteryLastReplaced", value: new Date())
	logInfo("Setting Battery Last Replaced to current date${newlyPaired}")
}

// installed() runs just after a sensor is paired
def installed() {
	state.prefsSetCount = 0
	logDebug("Installing")
}

// configure() runs after installed() when a sensor is paired
def configure() {
	log.warn("Configuring...")
	init()
	state.prefsSetCount = 1
	return
}

// updated() will run every time user saves preferences
def updated() {
	logInfo("Updating preference settings")
	init()
	logInfo("Info message logging enabled")
	logDebug("Debug message logging enabled")
}

def init() {
	if (!device.currentState('batteryLastReplaced')?.value)
		resetBatteryReplacedDate(true)
}

def logDebug(msg) {
    if (debugLogging) log.debug(msg)
}

def logInfo(msg) {
    if (infoLogging) log.info(msg)
}
2 Likes