GoControl PIR (WAPIRZ-1) weird temp readings

I have a GoControl PIR sensor (model WAPIRZ-1) mounted on my front porch in a protected area. It is currently 8degF outside, but the temperature reading sent back to my dashboard is -39. I suspect this is some kind of conversion error or numerical overflow, because -39C is ~= -39F.

Have you seen this, and does anyone know if it can be fixed by tweaking the Hubitat driver? I'm currently using the Generic Z-Wave Motion / Temperature Sensor device type.

image|205x200

not quite sure what this is doing, can you enable debug logging in the driver, then post a screen shot of the live logs for this device when it spits out one of these values?
Debug logging only runs for 30 minutes at a shot, so you may have to turn it back on a few times...

@mike.maxwell Here is what I see:

dev:902021-02-08 10:46:15.663 aminfoFront Door Motion: temperature is -38°F

dev:90 2021-02-08 10:46:15.659 amdebug SensorMultilevelReport(precision:0, scale:1, sensorType:1, sensorValue:[218], size:1, scaledSensorValue:-38)

dev:90 2021-02-08 10:46:15.618 amdebug parse description: zw device: 1E, command: 3105, payload: 01 09 DA , isMulticast: false

I would normally guess mfg rated temperature range is conservative, however with GoControl I'm not sure. My opinion is based on a WD500Z-1 dimmer that underperformed and the poor response (lip service) from Nortek.

So:

  1. Could you battery be marginal?

  2. If you warmed it slightly does the temperature "jump" to a higher more normal value or does it follow a normal increase in temp?

If it were a sign related math error I would think it would be at 32°F or 0°F

I will bring it inside and see what happens.
It would be nice to know how the sensor raw value (218) gets converted to -38, I guess if I record a few data points I could probably infer it?

I suggest you put a small cardboard box on the porch with the sensor inside. Leave it there for an hour or so. Then bring the cardboard box in. The box will slow down the increase in temp of the sensor so you can get a better feel of what might be going on.

You could instead wrap the sensor in bubble wrap. Or even bubble wrap in the box. Or a small cooler. Anything to slow the indoor heat from raising the sensor temperature so fast you don't get to see what is going on.

thanks, we're parsing this data correctly, only thing I can say is that the sensor has gone all pear shaped on you...

1 Like

It appears to be a numerical error (integer overflow?) in the sensor firmware itself. When the sensor crosses 0C (32F), instead of reporting a negative temperature, the value rolls over.
Based on this, it might be able to recover the correct value in the driver.

Here are readings I got:

Actual / Sensor / Raw
9, -38, 218 (checked with weather report)
??, -30,222
??, -25,231
??, -23, 233
??, -21, 235
32, 32, 32 (0 dC)
37, 37, 37

@mike.maxwell

Could it be a two's complement error?

the value is 8 bits so 0xDA is 1 1 0 1 1 0 1 0 in binary
signed this is -38
unsigned it's 218
its a ■■■■ sensor value...

1 Like

For what it is worth, I am trying to get in touch with Nortek to see if they have a way to update the firmware. I am guessing they will say "no", but it is worth an email.

or just put a different sensor out there that doesn't get stupid when it's cold...

1 Like

Which I will probably do... but I'm an engineer, and I can't let bad engineering "just go" :slight_smile:
I'd love for someone at 2Gig (Nortek) to say, "yeah we effed it up".

1 Like

For anyone still interested in my digging, the manufacturer of WAPIRZ-1 for GoControl/Nortek was Vision Automotive Electronics (Taiwan), which currently makes the Monoprice PIR sensor (FCC ID ZP3102US-5). I wonder if the current version has the same bug...?

Don't hold your breath. I had a few go arounds with my (then new) WD500Z-1 dimmer. Not only does it not do what it says (association) but they know it and didn't even change the instructions. I even talked to a product director with no success.

So go for it if it makes you feel better but don't expect any satisfaction from them.

2 Likes

@mike.maxwell is it possible to fork the Generic Z-Wave Motion/Temperature Sensor driver, to add a correction and make it specific to the GoControl / Monoprice PIR sensors? Where could I find the Groovy for the Generic driver? The fix would look kind of like this, for Fahrenheit readings:

if (TempValF < -20)
NewTempF = TempValF * 1.3 + 57.5

Update: Oops,I forgot to google it. SmartThings has a custom driver that addresses this issue (based on the notes). Now I just need to convince someone (maybe @krlaframboise) to help by porting it to Hubitat!

There's nothing odd in that driver. Anyone can convert it. 2 minutes - in theory. I've said that before and been quite wrong.

Well, I'm a Wink refugee, so the learning curve is steep for me. But I'll buy "beer" for someone who does it!

I didn't test it or change the header/revision text, but here you go. It saves without errors, but that doesn't mean it will work without errors. :wink:

Hard to test w/o the device.

/**
 *  GoControl Motion Sensor v1.3.6
 *    (Model: WAPIRZ-1)
 *
 *  Author: 
 *    Kevin LaFramboise (krlaframboise)
 *
 *  URL to documentation:
 *    https://community.smartthings.com/t/release-gocontrol-door-window-sensor-motion-sensor-and-siren-dth/50728?u=krlaframboise
 *
 *  Changelog:
 *
 *    1.3.6 (09/10/2017)
 *    	- Removed old style fingerprint to eliminate conflicts with other generic sensors. 
 *
 *    1.3.5 (09/01/2017)
 *    	- Added workaround for SmartThings breaking the convertTemperatureIfNeeded function for precision 0.
 *    	- Added +52°F offset when the temperature is <= -21°F to correct a firmware bug. 
 *
 *    1.3.2 (04/23/2017)
 *    	- SmartThings broke parse method response handling so switched to sendhubaction.
 *
 *    1.3.1 (04/20/2017)
 *      - Added fingerprint.
 *      - Added workaround for ST Health Check bug.
 *
 *    1.3 (03/12/2017)
 *      - Added Health Check.
 *
 *    1.2.1 (07/31/2016)
 *      - Fix iOS UI bug with tamper tile.
 *      - Removed secondary tile.
 *
 *    1.1 (06/17/2016)
 *      - Fixed tamper detection
 *
 *    1.0 (06/17/2016)
 *      - Initial Release 
 *
 *  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:"GoControl Motion Sensor", namespace:"krlaframboise", author: "Kevin LaFramboise") {
		capability "Sensor"
		capability "Battery"
		capability "Motion Sensor"
		capability "Temperature Measurement"		
		capability "Tamper Alert"
		capability "Refresh"
		capability "Configuration"
		capability "Health Check"

		attribute "lastCheckin", "string"
 
		fingerprint mfr:"014F", prod:"2002", model:"0203"
 
		// fingerprint deviceId:"0x2001", inClusters:"0x71, 0x85, 0x80, 0x72, 0x30, 0x86, 0x31, 0x70, 0x84"
	}

	preferences {		
		input "temperatureOffset", "number",
			title: "Temperature Offset:\n(Allows you to adjust the temperature being reported if it's always high or low by a specific amount.  Example: Enter -3 to make it report 3° lower or enter 3 to make it report 3° higher.)",
			range: "-100..100",
			defaultValue: tempOffsetSetting,
			displayDuringSetup: true,
			required: false
		input "temperatureThreshold", "number",
			title: "Temperature Change Threshold:\n(You can use this setting to prevent the device from bouncing back and forth between the same two temperatures.  Example:  If the device is repeatedly reporting 68° and 69°, you can change this setting to 2 and it won't report a new temperature unless 68° changes to 66° or 70°.)",
			range: "1..100",
			defaultValue: tempThresholdSetting,
			displayDuringSetup: true,
			required: false
		input "retriggerWaitTime", "number", 
			title: "Re-Trigger Wait Time (Minutes)\n(When the device detects motion, it waits for at least 1 minute of inactivity before sending the inactive event.  The default re-trigger wait time is 3 minutes.)", 
			range: "1..255", 
			defaultValue: retriggerWaitTimeSetting, 
			displayDuringSetup: true,
			required: false
		input "checkinInterval", "enum",
			title: "Checkin Interval:",
			defaultValue: checkinIntervalSetting,
			required: false,
			displayDuringSetup: true,
			options: checkinIntervalOptions.collect { it.name }
		input "reportBatteryEvery", "enum",
			title: "Battery Reporting Interval:",
			defaultValue: batteryReportingIntervalSetting,
			required: false,
			displayDuringSetup: true,
			options: checkinIntervalOptions.collect { it.name }
		input "debugOutput", "bool", 
			title: "Enable debug logging?", 
			defaultValue: false, 
			displayDuringSetup: true, 
			required: false
	}
}

def updated() {	
	if (!isDuplicateCommand(state.lastUpdated, 3000)) {
		state.lastUpdated = new Date().time
		logTrace "updated()"

		refresh()
	}
}

def configure() {	
	logTrace "configure()"
	def cmds = []
	
	if (!device.currentValue("motion")) {
		sendEvent(name: "motion", value: "active", isStateChange: true, displayed: false)
	}
	
	if (!state.isConfigured) {
		logTrace "Waiting 1 second because this is the first time being configured"
		// Give inclusion time to finish.
		cmds << "delay 1000"			
	}
	
	initializeCheckin()
	
	cmds += [
		wakeUpIntervalSetCmd(checkinIntervalSettingMinutes),
		retriggerWaitTimeSetCmd(),
		batteryGetCmd(),
		temperatureGetCmd()
	]
	return delayBetween(cmds, 250)
}

private initializeCheckin() {
	// Set the Health Check interval so that it can be skipped once plus 2 minutes.
	def checkInterval = ((checkinIntervalSettingMinutes * 2 * 60) + (2 * 60))
	
	sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}

def refresh() {	
	clearTamperDetected()
	logDebug "The re-trigger wait time will be sent to the device the next time it wakes up.  If you want this change to happen immediately, open the back cover of the device until the red light turns solid and then close it."
	state.pendingConfig = true	
}

private clearTamperDetected() {	
	if (device.currentValue("tamper") != "clear") {
		logDebug "Resetting Tamper"
		sendEvent(getTamperEventMap("clear"))			
	}
}

private retriggerWaitTimeSetCmd() {
	logTrace "Setting re-trigger wait time to ${retriggerWaitTimeSetting} minutes"
	
	return zwave.configurationV1.configurationSet(scaledConfigurationValue: retriggerWaitTimeSetting, parameterNumber: 1, size: 1).format()	
}

private wakeUpIntervalSetCmd(minutesVal) {
	state.checkinIntervalMinutes = minutesVal
	logTrace "wakeUpIntervalSetCmd(${minutesVal})"
	
	return zwave.wakeUpV2.wakeUpIntervalSet(seconds:(minutesVal * 60), nodeid:zwaveHubNodeId).format()
}

private wakeUpNoMoreInfoCmd() {
	return zwave.wakeUpV2.wakeUpNoMoreInformation().format()
}

private temperatureGetCmd() {
	return zwave.sensorMultilevelV2.sensorMultilevelGet().format()
}

private batteryGetCmd() {
	return zwave.batteryV1.batteryGet().format()
}


def parse(String description) {	
	// log.trace "$description"
	def result = []
	
	sendEvent(name: "lastCheckin", value: convertToLocalTimeString(new Date()), displayed: false, isStateChange: true)
	
	def cmd = zwave.parse(description, [0x71: 2, 0x80: 1, 0x30: 1, 0x31: 2, 0x70: 1, 0x84: 1])
	if (cmd) {
		result += zwaveEvent(cmd)
	}
	else {
		logDebug "Unknown Description: $desc"
	}
	return result
}

def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd)
{
	logTrace "WakeUpNotification"
	def result = []

	if (state.pendingConfig) {
		state.pendingConfig = false
		result += configure()
	}
	else if (canReportBattery()) {
		result << batteryGetCmd()
	}
	if (result) {
		result << "delay 2000"
	}
	result << wakeUpNoMoreInfoCmd()
	return sendResponse(result)
}

private sendResponse(cmds) {
	def actions = []
	cmds?.each { cmd ->
		actions << new hubitat.device.HubAction(cmd)
	}	
	sendHubCommand(actions)
	return []
}

private canReportBattery() {
	def reportEveryMS = (batteryReportingIntervalSettingMinutes * 60 * 1000)
		
	return (!state.lastBatteryReport || ((new Date().time) - state.lastBatteryReport > reportEveryMS)) 
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {	
	def motionVal = cmd.value ? "active" : "inactive"
	def desc = "Motion is $motionVal"
	logDebug "$desc"
	def result = []
	result << createEvent(name: "motion", 
			value: motionVal, 
			isStateChange: true, 
			displayed: true, 
			descriptionText: "$desc")
	return result
}

def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	logTrace "BatteryReport: $cmd"
	def val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel)
	if (val > 100) {
		val = 100
	}
	state.lastBatteryReport = new Date().time	
	logDebug "Battery ${val}%"
	
	def isNew = (device.currentValue("battery") != val)
			
	def result = []
	result << createEvent(name: "battery", value: val, unit: "%", display: isNew, isStateChange: isNew)

	return result
}

def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	def result = []
	if (cmd.alarmType == 7 && cmd.alarmLevel == 0xFF && cmd.zwaveAlarmEvent == 3) {
		logDebug "Tampering Detected"
		result << createEvent(getTamperEventMap("detected"))
	}	
	return result
}

def getTamperEventMap(val) {
	[
		name: "tamper", 
		value: val, 
		isStateChange: true, 
		displayed: (val == "detected"),
		descriptionText: "Tamper is $val"
	]
}

def zwaveEvent(hubitat.zwave.Command cmd) {
	logDebug "Unknown Command: $cmd"
	return []
}

def zwaveEvent(hubitat.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd){
	// logTrace "SensorMultilevelReport: $cmd"
	def result = []
	
	if (cmd.sensorType == 1) {
		def cmdScale = cmd.scale == 1 ? "F" : "C"
		
		// Workaround for firmware bug that adds a -52°F offset when temperature drops below 32°F
		def sensorVal = (cmd.scaledSensorValue <= -21) ? cmd.scaledSensorValue + 52 : cmd.scaledSensorValue 
		
		def temp = "${convertTemperatureIfNeeded(sensorVal, cmdScale, cmd.precision)}"
		def newTemp = safeToInt(temp.endsWith(".") ? temp.replace(".", "") : temp)

		if (tempOffsetSetting != 0) {
			newTemp = (newTemp + tempOffsetSetting)
			logDebug "Adjusted temperature by ${tempOffsetSetting}°"
		}		
		
		def highTemp = (getCurrentTemp() + tempThresholdSetting)
		def lowTemp = (getCurrentTemp() - tempThresholdSetting)
		if (newTemp >= highTemp || newTemp <= lowTemp) {
			result << createEvent(
				name: "temperature",
				value: newTemp,
				unit: getTemperatureScale(),
				isStateChange: true,
				displayed: true)
		}
		else {
			logDebug "Ignoring new temperature of $newTemp° because the change is within the ${tempThresholdSetting}° threshold."
		}
	}
	return result
}

private getRetriggerWaitTimeSetting() {
	return safeToInt(settings?.retriggerWaitTime, 3)
}

private getCurrentTemp() {
	return safeToInt(device.currentValue("temperature"), 0)
}

private getTempThresholdSetting() {
	return safeToInt(settings?.temperatureThreshold, 1)
}

private getTempOffsetSetting() {
	return safeToInt(settings?.temperatureOffset, 0)
}

// Settings
private getCheckinIntervalSettingMinutes() {
	return convertOptionSettingToInt(checkinIntervalOptions, checkinIntervalSetting) ?: 360
}

private getCheckinIntervalSetting() {
	return settings?.checkinInterval ?: findDefaultOptionName(checkinIntervalOptions)
}

private getBatteryReportingIntervalSettingMinutes() {
	return convertOptionSettingToInt(checkinIntervalOptions, batteryReportingIntervalSetting) ?: 720
}

private getBatteryReportingIntervalSetting() {
	return settings?.reportBatteryEvery ?: findDefaultOptionName(checkinIntervalOptions)
}

private getCheckinIntervalOptions() {
	[
		[name: "10 Minutes", value: 10],
		[name: "15 Minutes", value: 15],
		[name: "30 Minutes", value: 30],
		[name: "1 Hour", value: 60],
		[name: "2 Hours", value: 120],
		[name: "3 Hours", value: 180],
		[name: formatDefaultOptionName("6 Hours"), value: 360],
		[name: "9 Hours", value: 540],
		[name: "12 Hours", value: 720],
		[name: "18 Hours", value: 1080],
		[name: "24 Hours", value: 1440]
	]
}

private convertOptionSettingToInt(options, settingVal) {
	return safeToInt(options?.find { "${settingVal}" == it.name }?.value, 0)
}

private formatDefaultOptionName(val) {
	return "${val}${defaultOptionSuffix}"
}

private findDefaultOptionName(options) {
	def option = options?.find { it.name?.contains("${defaultOptionSuffix}") }
	return option?.name ?: ""
}

private getDefaultOptionSuffix() {
	return "   (Default)"
}

private safeToInt(val, defaultVal=-1) {
	return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}

private convertToLocalTimeString(dt) {
	def timeZoneId = location?.timeZone?.ID
	if (timeZoneId) {
		return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId))
	}
	else {
		return "$dt"
	}	
}

private isDuplicateCommand(lastExecuted, allowedMil) {
	!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) 
}

private logDebug(msg) {
	if (settings?.debugOutput || settings?.debugOutput == null) {
		log.debug "$msg"
	}
}

private logTrace(msg) {
	// log.trace "$msg"
}
1 Like