Merge pull request #443 from tpmanley/feature/sensor_updates

Feature/sensor updates
This commit is contained in:
Tom Manley
2016-01-19 13:43:40 -06:00
4 changed files with 476 additions and 425 deletions

View File

@@ -88,31 +88,37 @@ private handleReportAttributeMessage(String description) {
} }
} }
private handleBatteryEvent(rawValue) { /**
def linkText = getLinkText(device) * Create battery event from reported battery voltage.
*
def eventMap = [ * @param volts Battery voltage in .1V increments
name: 'battery', */
value: '--' private handleBatteryEvent(volts) {
] if (volts == 0 || volts == 255) {
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
def volts = rawValue / 10 }
if (volts > 0){ else {
def minVolts = 2.0 def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
def maxVolts = 2.8 22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
if (volts < minVolts) if (volts < minVolts)
volts = minVolts volts = minVolts
else if (volts > maxVolts) else if (volts > maxVolts)
volts = maxVolts volts = maxVolts
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = batteryMap[volts]
if (pct != null) {
eventMap.value = Math.round(pct * 100) def linkText = getLinkText(device)
eventMap.descriptionText = "${linkText} battery was ${eventMap.value}%" def eventMap = [
name: 'battery',
value: pct,
descriptionText: "${linkText} battery was ${pct}%"
]
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
sendEvent(eventMap)
}
} }
log.debug "Creating battery event: ${eventMap}"
sendEvent(eventMap)
} }
private handlePresenceEvent(present) { private handlePresenceEvent(present) {

View File

@@ -20,8 +20,8 @@ metadata {
capability "Refresh" capability "Refresh"
capability "Temperature Measurement" capability "Temperature Measurement"
capability "Water Sensor" capability "Water Sensor"
command "enrollResponse" command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
@@ -29,9 +29,9 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
} }
simulator { simulator {
} }
preferences { preferences {
@@ -47,7 +47,7 @@ metadata {
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
} }
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
tileAttribute ("device.water", key: "PRIMARY_CONTROL") { tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
@@ -78,7 +78,7 @@ metadata {
details(["water", "temperature", "battery", "refresh"]) details(["water", "temperature", "battery", "refresh"])
} }
} }
def parse(String description) { def parse(String description) {
log.debug "description: $description" log.debug "description: $description"
@@ -92,59 +92,59 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0x0402: case 0x0402:
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp) def value = getTemperature(temp)
resultMap = getTemperatureResult(value) resultMap = getTemperatureResult(value)
break break
} }
} }
return resultMap return resultMap
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":") def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
} }
log.debug "Desc Map: $descMap" log.debug "Desc Map: $descMap"
Map resultMap = [:] Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") { if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value) def value = getTemperature(descMap.value)
@@ -153,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") { else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
} }
return resultMap return resultMap
} }
private Map parseCustomMessage(String description) { private Map parseCustomMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
if (description?.startsWith('temperature: ')) { if (description?.startsWith('temperature: ')) {
@@ -167,42 +167,42 @@ private Map parseCustomMessage(String description) {
} }
private Map parseIasMessage(String description) { private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ') List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2] String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMoistureResult('dry')
break
case '0x0021': // Open/Motion/Wet Map resultMap = [:]
resultMap = getMoistureResult('wet') switch(msgCode) {
break case '0x0020': // Closed/No Motion/Dry
resultMap = getMoistureResult('dry')
break
case '0x0022': // Tamper Alarm case '0x0021': // Open/Motion/Wet
break resultMap = getMoistureResult('wet')
break
case '0x0023': // Battery Alarm case '0x0022': // Tamper Alarm
break break
case '0x0024': // Supervision Report case '0x0023': // Battery Alarm
log.debug 'dry with tamper alarm' break
resultMap = getMoistureResult('dry')
break
case '0x0025': // Restore Report case '0x0024': // Supervision Report
log.debug 'water with tamper alarm' log.debug 'dry with tamper alarm'
resultMap = getMoistureResult('wet') resultMap = getMoistureResult('dry')
break break
case '0x0026': // Trouble/Failure case '0x0025': // Restore Report
break log.debug 'water with tamper alarm'
resultMap = getMoistureResult('wet')
break
case '0x0028': // Test Mode case '0x0026': // Trouble/Failure
break break
}
return resultMap case '0x0028': // Test Mode
break
}
return resultMap
} }
def getTemperature(value) { def getTemperature(value) {
@@ -215,24 +215,47 @@ def getTemperature(value) {
} }
private Map getBatteryResult(rawValue) { private Map getBatteryResult(rawValue) {
log.debug 'Battery' log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device) def linkText = getLinkText(device)
def result = [ def result = [
name: 'battery' name: 'battery',
] value: '--'
]
def volts = rawValue / 10 def volts = rawValue / 10
def descriptionText
if (volts > 3.5) { if (rawValue == 0 || rawValue == 255) {}
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else { else {
def minVolts = 2.1 if (volts > 3.5) {
def maxVolts = 3.0 result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
def pct = (volts - minVolts) / (maxVolts - minVolts) }
result.value = Math.min(100, (int) pct * 100) else {
result.descriptionText = "${linkText} battery was ${result.value}%" if (device.getDataValue("manufacturer") == "SmartThings") {
volts = rawValue // For the batteryMap to work the key needs to be an int
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
def minVolts = 15
def maxVolts = 28
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = batteryMap[volts]
if (pct != null) {
result.value = pct
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
} }
return result return result
@@ -267,7 +290,7 @@ private Map getMoistureResult(value) {
def refresh() { def refresh() {
log.debug "Refreshing Temperature and Battery" log.debug "Refreshing Temperature and Battery"
def refreshCmds = [ def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
] ]
@@ -277,32 +300,32 @@ def refresh() {
def configure() { def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings." log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [ def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500", "send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500", "send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500" "send 0x${device.deviceNetworkId} 1 1", "delay 500"
] ]
return configCmds + refresh() // send refresh cmds as part of config return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {
log.debug "Sending enroll response" log.debug "Sending enroll response"
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[ [
//Resending the CIE in case the enroll request is sent before CIE is written //Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response //Enroll Response
"raw 0x500 {01 23 00 00 00}", "raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200" "send 0x${device.deviceNetworkId} 1 1", "delay 200"
] ]
} }
private getEndpointId() { private getEndpointId() {
@@ -314,19 +337,19 @@ private hex(value) {
} }
private String swapEndianHex(String hex) { private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex() reverseArray(hex.decodeHex()).encodeHex()
} }
private byte[] reverseArray(byte[] array) { private byte[] reverseArray(byte[] array) {
int i = 0; int i = 0;
int j = array.length - 1; int j = array.length - 1;
byte tmp; byte tmp;
while (j > i) { while (j > i) {
tmp = array[j]; tmp = array[j];
array[j] = array[i]; array[j] = array[i];
array[i] = tmp; array[i] = tmp;
j--; j--;
i++; i++;
} }
return array return array
} }

View File

@@ -19,17 +19,17 @@ metadata {
capability "Motion Sensor" capability "Motion Sensor"
capability "Configuration" capability "Configuration"
capability "Battery" capability "Battery"
capability "Temperature Measurement" capability "Temperature Measurement"
capability "Refresh" capability "Refresh"
command "enrollResponse" command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
} }
simulator { simulator {
@@ -85,7 +85,7 @@ metadata {
def parse(String description) { def parse(String description) {
log.debug "description: $description" log.debug "description: $description"
Map map = [:] Map map = [:]
if (description?.startsWith('catchall:')) { if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description) map = parseCatchAllMessage(description)
@@ -96,55 +96,55 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0x0402: case 0x0402:
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp) def value = getTemperature(temp)
resultMap = getTemperatureResult(value) resultMap = getTemperatureResult(value)
break break
case 0x0406: case 0x0406:
log.debug 'motion' log.debug 'motion'
resultMap.name = 'motion' resultMap.name = 'motion'
break break
} }
} }
return resultMap return resultMap
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
@@ -153,7 +153,7 @@ private Map parseReportAttributeMessage(String description) {
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
} }
log.debug "Desc Map: $descMap" log.debug "Desc Map: $descMap"
Map resultMap = [:] Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") { if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value) def value = getTemperature(descMap.value)
@@ -162,14 +162,14 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") { else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
} }
else if (descMap.cluster == "0406" && descMap.attrId == "0000") { else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
def value = descMap.value.endsWith("01") ? "active" : "inactive" def value = descMap.value.endsWith("01") ? "active" : "inactive"
resultMap = getMotionResult(value) resultMap = getMotionResult(value)
} }
return resultMap return resultMap
} }
private Map parseCustomMessage(String description) { private Map parseCustomMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
if (description?.startsWith('temperature: ')) { if (description?.startsWith('temperature: ')) {
@@ -180,44 +180,44 @@ private Map parseCustomMessage(String description) {
} }
private Map parseIasMessage(String description) { private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ') List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2] String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getMotionResult('inactive')
break
case '0x0021': // Open/Motion/Wet Map resultMap = [:]
resultMap = getMotionResult('active') switch(msgCode) {
break case '0x0020': // Closed/No Motion/Dry
resultMap = getMotionResult('inactive')
break
case '0x0022': // Tamper Alarm case '0x0021': // Open/Motion/Wet
log.debug 'motion with tamper alarm' resultMap = getMotionResult('active')
resultMap = getMotionResult('active') break
break
case '0x0023': // Battery Alarm case '0x0022': // Tamper Alarm
break log.debug 'motion with tamper alarm'
resultMap = getMotionResult('active')
break
case '0x0024': // Supervision Report case '0x0023': // Battery Alarm
log.debug 'no motion with tamper alarm' break
resultMap = getMotionResult('inactive')
break
case '0x0025': // Restore Report case '0x0024': // Supervision Report
break log.debug 'no motion with tamper alarm'
resultMap = getMotionResult('inactive')
break
case '0x0026': // Trouble/Failure case '0x0025': // Restore Report
log.debug 'motion with failure alarm' break
resultMap = getMotionResult('active')
break
case '0x0028': // Test Mode case '0x0026': // Trouble/Failure
break log.debug 'motion with failure alarm'
} resultMap = getMotionResult('active')
return resultMap break
case '0x0028': // Test Mode
break
}
return resultMap
} }
def getTemperature(value) { def getTemperature(value) {
@@ -230,30 +230,46 @@ def getTemperature(value) {
} }
private Map getBatteryResult(rawValue) { private Map getBatteryResult(rawValue) {
log.debug 'Battery' log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device) def linkText = getLinkText(device)
log.debug rawValue
def result = [ def result = [
name: 'battery', name: 'battery',
value: '--' value: '--'
] ]
def volts = rawValue / 10 def volts = rawValue / 10
def descriptionText
if (rawValue == 0) {} if (rawValue == 0 || rawValue == 255) {}
else { else {
if (volts > 3.5) { if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)." result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
} }
else if (volts > 0){ else {
def minVolts = 2.1 if (device.getDataValue("manufacturer") == "SmartThings") {
def maxVolts = 3.0 volts = rawValue // For the batteryMap to work the key needs to be an int
def pct = (volts - minVolts) / (maxVolts - minVolts) def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
result.value = Math.min(100, (int) pct * 100) 22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
result.descriptionText = "${linkText} battery was ${result.value}%" def minVolts = 15
def maxVolts = 28
if (volts < minVolts)
volts = minVolts
else if (volts > maxVolts)
volts = maxVolts
def pct = batteryMap[volts]
if (pct != null) {
result.value = pct
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
} }
} }
@@ -338,19 +354,19 @@ private hex(value) {
} }
private String swapEndianHex(String hex) { private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex() reverseArray(hex.decodeHex()).encodeHex()
} }
private byte[] reverseArray(byte[] array) { private byte[] reverseArray(byte[] array) {
int i = 0; int i = 0;
int j = array.length - 1; int j = array.length - 1;
byte tmp; byte tmp;
while (j > i) { while (j > i) {
tmp = array[j]; tmp = array[j];
array[j] = array[i]; array[j] = array[i];
array[i] = tmp; array[i] = tmp;
j--; j--;
i++; i++;
} }
return array return array
} }

View File

@@ -14,28 +14,28 @@
* *
*/ */
metadata { metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Three Axis" capability "Three Axis"
capability "Battery" capability "Battery"
capability "Configuration" capability "Configuration"
capability "Sensor" capability "Sensor"
capability "Contact Sensor" capability "Contact Sensor"
capability "Acceleration Sensor" capability "Acceleration Sensor"
capability "Refresh" capability "Refresh"
capability "Temperature Measurement" capability "Temperature Measurement"
command "enrollResponse" command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string" attribute "status", "string"
} }
simulator { simulator {
status "open": "zone report :: type: 19 value: 0031" status "open": "zone report :: type: 19 value: 0031"
status "closed": "zone report :: type: 19 value: 0030" status "closed": "zone report :: type: 19 value: 0030"
@@ -52,7 +52,7 @@
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0" status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000" status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
} }
preferences { preferences {
section { section {
image(name: 'educationalcontent', multiple: true, images: [ image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
@@ -62,13 +62,13 @@
]) ])
} }
section { section {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
} }
} section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
}
}
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
@@ -106,9 +106,9 @@
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:"" state "battery", label:'${currentValue}% battery', unit:""
} }
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
main(["status", "acceleration", "temperature"]) main(["status", "acceleration", "temperature"])
@@ -121,61 +121,61 @@ def parse(String description) {
if (description?.startsWith('catchall:')) { if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description) map = parseCatchAllMessage(description)
} }
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
else if (description?.startsWith('read attr -')) { else if (description?.startsWith('read attr -')) {
result = parseReportAttributeMessage(description).each { createEvent(it) } result = parseReportAttributeMessage(description).each { createEvent(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
log.debug cluster log.debug cluster
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0xFC02: case 0xFC02:
log.debug 'ACCELERATION' log.debug 'ACCELERATION'
break break
case 0x0402: case 0x0402:
log.debug 'TEMP' log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp) def value = getTemperature(temp)
resultMap = getTemperatureResult(value) resultMap = getTemperatureResult(value)
break break
} }
} }
return resultMap return resultMap
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private List parseReportAttributeMessage(String description) { private List parseReportAttributeMessage(String description) {
@@ -202,7 +202,7 @@ private List parseReportAttributeMessage(String description) {
result << parseAxis(threeAxisAttributes) result << parseAxis(threeAxisAttributes)
descMap.value = descMap.value[-2..-1] descMap.value = descMap.value[-2..-1]
} }
result << getAccelerationResult(descMap.value) result << getAccelerationResult(descMap.value)
} }
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) { else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
// The size is checked to ensure the attribute report contains X, Y and Z values // The size is checked to ensure the attribute report contains X, Y and Z values
@@ -231,43 +231,43 @@ private Map parseIasMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
switch(msgCode) { switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry case '0x0020': // Closed/No Motion/Dry
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('closed') resultMap = getContactResult('closed')
} }
break break
case '0x0021': // Open/Motion/Wet case '0x0021': // Open/Motion/Wet
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('open') resultMap = getContactResult('open')
} }
break break
case '0x0022': // Tamper Alarm case '0x0022': // Tamper Alarm
break break
case '0x0023': // Battery Alarm case '0x0023': // Battery Alarm
break break
case '0x0024': // Supervision Report case '0x0024': // Supervision Report
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('closed') resultMap = getContactResult('closed')
} }
break break
case '0x0025': // Restore Report case '0x0025': // Restore Report
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('open') resultMap = getContactResult('open')
} }
break break
case '0x0026': // Trouble/Failure case '0x0026': // Trouble/Failure
break break
case '0x0028': // Test Mode case '0x0028': // Test Mode
break break
} }
return resultMap return resultMap
} }
def updated() { def updated() {
@@ -302,132 +302,141 @@ def getTemperature(value) {
} }
} }
private Map getBatteryResult(rawValue) { private Map getBatteryResult(rawValue) {
log.debug "Battery" log.debug "Battery rawValue = ${rawValue}"
log.debug rawValue def linkText = getLinkText(device)
def linkText = getLinkText(device)
def result = [ def result = [
name: 'battery', name: 'battery',
value: '--' value: '--'
] ]
def volts = rawValue / 10 def volts = rawValue / 10
def descriptionText
if (rawValue == 0 || rawValue == 255) {}
if (rawValue == 255) {} else {
else { if (volts > 3.5) {
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)." result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
} }
else { else {
def minVolts = 2.1 if (device.getDataValue("manufacturer") == "SmartThings") {
def maxVolts = 3.0 volts = rawValue // For the batteryMap to work the key needs to be an int
def pct = (volts - minVolts) / (maxVolts - minVolts) def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
result.value = Math.min(100, (int) pct * 100) 22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
result.descriptionText = "${linkText} battery was ${result.value}%" def minVolts = 15
}} def maxVolts = 28
return result if (volts < minVolts)
} volts = minVolts
else if (volts > maxVolts)
private Map getTemperatureResult(value) { volts = maxVolts
log.debug "Temperature" def pct = batteryMap[volts]
def linkText = getLinkText(device) if (pct != null) {
if (tempOffset) { result.value = pct
def offset = tempOffset as int result.descriptionText = "${linkText} battery was ${result.value}%"
def v = value as int }
value = v + offset }
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
} }
def descriptionText = "${linkText} was ${value}°${temperatureScale}" }
return [
name: 'temperature', return result
}
private Map getTemperatureResult(value) {
log.debug "Temperature"
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
value: value,
descriptionText: descriptionText
]
}
private Map getContactResult(value) {
log.debug "Contact"
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
}
private getAccelerationResult(numValue) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
value: value, value: value,
descriptionText: descriptionText descriptionText: descriptionText,
isStateChange: isStateChange
]
}
def refresh() {
log.debug "Refreshing Values "
def refreshCmds = []
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
refreshCmds = refreshCmds + [
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
} else {
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
] ]
} }
private Map getContactResult(value) { //Common refresh commands
log.debug "Contact" refreshCmds = refreshCmds + [
def linkText = getLinkText(device) "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
}
private getAccelerationResult(numValue) { "zcl mfg-code ${manufacturerCode}", "delay 200",
log.debug "Acceleration" "zcl global read 0xFC02 0x0010",
def name = "acceleration" "send 0x${device.deviceNetworkId} 1 1","delay 400"
def value = numValue.endsWith("1") ? "active" : "inactive" ]
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
value: value,
descriptionText: descriptionText,
isStateChange: isStateChange
]
}
def refresh() { return refreshCmds + enrollResponse()
log.debug "Refreshing Values " }
def refreshCmds = []
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
refreshCmds = refreshCmds + [
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602) def configure() {
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer. String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
Separating these out in a separate if-else because I do not want to touch Centralite part log.debug "Configuring Reporting"
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
} else {
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
}
//Common refresh commands
refreshCmds = refreshCmds + [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
return refreshCmds + enrollResponse()
}
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
@@ -455,10 +464,9 @@ def getTemperature(value) {
"zcl mfg-code ${manufacturerCode}", "zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", "zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
] ]
return configCmds + refresh() return configCmds + refresh()
} }
private getEndpointId() { private getEndpointId() {
@@ -582,5 +590,3 @@ private byte[] reverseArray(byte[] array) {
} }
return array return array
} }