Broken in Migration: SmartSense Motion Sensor Device

I have started to see some rather erratic behavior from the sensor and found the following in the log for that device:

warn Device: is using deprecated isZoneType19 call

Here are the related items in the DeviceHandler:

    simulator {
		status "active": "zone report :: type: 19 value: 0031"
		status "inactive": "zone report :: type: 19 value: 0030"
	}
	
	def parse(String description) {
	def results
	if (isZoneType19(description) || !isSupportedDescription(description)) {
		results = parseBasicMessage(description)
	}
	else if (isMotionStatusMessage(description)){
		results = parseMotionStatusMessage(description)
	}

	results
}

private String parseValue(String description) {
	if (isZoneType19(description)) {
		if (translateStatusZoneType19(description)) {
			return "active"
		}
		else {
			return "inactive"
		}
	}

	description
}

So are these causing the sluggishness of the sensor, and if so, any ideas on what should I replace that call with?

Thanks!

Tagging @tpmanley

Are you using a custom DTH or the standard one?

1 Like

It certainly isn’t the current one, but it was authored by SmartThings back in 2015. It is designed to report power status (usb vs battery), in addition to motion on the original Smartsense motion sensor.

Here it is in its entirety:

/**
 *  Copyright 2015 SmartThings
 *
 *  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.
 *
 */
metadata {
	definition (name: "SmartSense Motion with Power", namespace: "smartthings", author: "SmartThings") {
		capability "Signal Strength"
		capability "Motion Sensor"
		capability "Sensor"
		capability "Battery"

		fingerprint profileId: "0104", deviceId: "013A", inClusters: "0000", outClusters: "0006"
		fingerprint profileId: "FC01", deviceId: "013A"
	}

	simulator {
		status "active": "zone report :: type: 19 value: 0031"
		status "inactive": "zone report :: type: 19 value: 0030"
	}

	tiles {
		standardTile("motion", "device.motion", width: 2, height: 2) {
			state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
			state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
		}
		valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
			state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[
				[value: 5, color: "#BC2323"],
				[value: 10, color: "#D04E00"],
				[value: 15, color: "#F1D801"],
				[value: 16, color: "#FFFFFF"]
			]*/
		}
		/*
		valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) {
			state "lqi", label:'${currentValue}% signal', unit:""
		}
		*/
		
		main "motion"
		details(["motion", "battery"/*, "lqi"*/])
	}
}

def parse(String description) {
	def results
	if (isZoneType19(description) || !isSupportedDescription(description)) {
		results = parseBasicMessage(description)
	}
	else if (isMotionStatusMessage(description)){
		results = parseMotionStatusMessage(description)
	}

	results
}

private Map parseBasicMessage(description) {
	def name = parseName(description)
	def value = parseValue(description)
	def linkText = getLinkText(device)
	def descriptionText = parseDescriptionText(linkText, value, description)
	def handlerName = value
	def isStateChange = isStateChange(device, name, value)

	def results = [
		name: name,
		value: value,
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: handlerName,
		isStateChange: isStateChange,
		displayed: displayed(description, isStateChange)
	]
	log.debug "Parse returned $results.descriptionText"
	return results
}

private String parseName(String description) {
	if (isSupportedDescription(description)) {
		return "motion"
	}
	null
}

private String parseValue(String description) {
	if (isZoneType19(description)) {
		if (translateStatusZoneType19(description)) {
			return "active"
		}
		else {
			return "inactive"
		}
	}

	description
}

private parseDescriptionText(String linkText, String value, String description) {
	switch(value) {
		case "active": return "$linkText detected motion"
		case "inactive": return "$linkText motion has stopped"
		default: return value
	}
}

private Boolean isMotionStatusMessage(String description) {
	// "raw:7D360000001D55FF, dni:7D36, motion:00, battery:00, powerSource:00, rssi:1D, lqi:55, other:FF" - old (incorrect dev-conn parse)
	// "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:0000, battery:00, rssi:1D, lqi:55, other:FF" - old (correct dev-conn parse)
	// "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:00, battery:00, batteryDivisor:00, rssi:1D, lqi:55, other:FF" - new
		description ==~ /raw:.*dni:.*motion:.*battery:.*powerSource:.*rssi:.*lqi:.*/ || description ==~ /raw:.*dni:.*motion:.*powerSource:.*battery:.*rssi:.*lqi:.*/
}

private List parseMotionStatusMessage(String description) {
	def results = []
	def parts = description.split(',')
	parts.each { part ->
		part = part.trim()
		if (part.startsWith('motion:')) {
			def motionResult = getMotionResult(part, description)
			if (motionResult) {
				results << motionResult
			}
		}
		else if (part.startsWith('powerSource:')) {
			def powerSourceResult = getPowerSourceResult(part, description)
			if (powerSourceResult) {
				results << powerSourceResult
			}
		}
		else if (part.startsWith('battery:')) {
			def batteryResult = getBatteryResult(part, description)
			if (batteryResult) {
				results << batteryResult
			}
		}
		else if (part.startsWith('rssi:')) {
			def rssiResult = getRssiResult(part, description)
			if (rssiResult) {
				results << rssiResult
			}
		}
		else if (part.startsWith('lqi:')) {
			def lqiResult = getLqiResult(part, description)
			if (lqiResult) {
				results << lqiResult
			}
		}
	}

	results
}

private getMotionResult(part, description) {
	def name = "motion"
	def valueString = part.split(":")[1].trim()
	def valueInt = Integer.parseInt(valueString, 16)
	def value = valueInt == 0 ? "inactive" : "active"
	def linkText = getLinkText(device)
	def descriptionText = parseDescriptionText(linkText, value, description)
	def isStateChange = isStateChange(device, name, value)

	[
		name: name,
		value: value,
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: value,
		isStateChange: isStateChange,
		displayed: displayed(description, isStateChange)
	]
}

private getPowerSourceResult(part, description) {
	def name = "powerSource"
	def valueString = part.split(":")[1].trim()
	def valueInt = Integer.parseInt(valueString, 16)
	def value = valueInt == 0 ? "battery" : "powered"
	def linkText = getLinkText(device)
	def descriptionText
	if (value == "battery") {
		descriptionText = "$linkText is ${value} powered"
	}
	else {
		descriptionText = "$linkText is plugged in"
	}
	def isStateChange = isStateChange(device, name, value)

	[
		name: name,
		value: value,
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: name,
		isStateChange: isStateChange,
		displayed: false
	]
}

private getBatteryResult(part, description) {
	def name = "battery"
	def valueString = part.split(":")[1].trim()
	def valueInt = Integer.parseInt(valueString, 16)

	// Temporarily disallow zero as a valid result b/c the current firmware has a bug where zero is only value being sent
	// This effectively disables battery reporting for this device, so needs to be removed once FW is updated
	if (valueInt == 0) return null


	def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
	def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
	def unit = "%"
	def linkText = getLinkText(device)
	def descriptionText = "$linkText battery was ${value}${unit}"
	def isStateChange = isStateChange(device, name, value)

	[
		name: name,
		value: value,
		unit: unit,
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: name,
		isStateChange: isStateChange,
		displayed: false
	]
}

private getRssiResult(part, description) {
	def name = "rssi"
	def parts = part.split(":")
	if (parts.size() != 2) return null

	def valueString = parts[1].trim()
	def valueInt = Integer.parseInt(valueString, 16)
	def value = (valueInt - 128).toString()
	def linkText = getLinkText(device)
	def descriptionText = "$linkText was $value dBm"
	def isStateChange = isStateChange(device, name, value)

	[
		name: name,
		value: value,
		unit: "dBm",
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: null,
		isStateChange: isStateChange,
		displayed: false
	]
}

/**
 * Use LQI (Link Quality Indicator) as a measure of signal strength. The values
 * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal
 * strength. Return as a percentage of 255.
 *
 * Note: To make the signal strength indicator more accurate, we could combine
 * LQI with RSSI.
 */
private getLqiResult(part, description) {
	def name = "lqi"
	def parts = part.split(":")
	if (parts.size() != 2) return null

	def valueString = parts[1].trim()
	def valueInt = Integer.parseInt(valueString, 16)
	def percentageOf = 255
	def value = Math.round((valueInt / percentageOf * 100)).toString()
	def unit = "%"
	def linkText = getLinkText(device)
	def descriptionText = "$linkText Signal (LQI) was ${value}${unit}"
	def isStateChange = isStateChange(device, name, value)

	[
		name: name,
		value: value,
		unit: unit,
		linkText: linkText,
		descriptionText: descriptionText,
		handlerName: null,
		isStateChange: isStateChange,
		displayed: false
	]
}

The deprecation is probably not the reason for any sluggishness you’re seeing. These methods were only deprecated in the sense that we wanted to move them out of the device object and into the zigbee library. Just calling zigbee.isZoneType19 or zigbee.translateStatusZoneType19 (which is what’s happening behind the scenes anyway) will remove the deprecation message.

Maybe removing that one layer of indirection will speed it up, but I wouldn’t expect so.

For the “modern” way of doing this, check out this code path in Smartsense Open/Closed Sensor:

So, check if the description starts with “zone status” or “zone report”, then parse it into a ZoneStatus object with zigbee.parseZoneStatus and then reporting active or inactive based on isAlarmSet. But this is basically what’s happening behind the scenes in those calls anyway.

2 Likes