diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties index bcf4fae..2477112 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -1,16 +1,16 @@ #============================================================================== # Copyright 2016 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 +# 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 +# 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. #============================================================================== # Purpose: SmartSense Multi Sensor i18n Translation File @@ -39,8 +39,6 @@ '''{{ device.displayName }} was inactive'''.ko={{ device.displayName }}비활성화되었습니다. '''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가){{ value }}°C였습니다. '''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 -'''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}입니다. +'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중 '''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중 -'''{{ device.displayName }} status was closed'''.ko={{ device.displayName }}은(는) 닫힌 상태입니다 -'''{{ device.displayName }} status was opened'''.ko={{ device.displayName }}은(는) 열린 상태입니다 diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 9bed55b..a33e0e4 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -3,15 +3,15 @@ * Copyright 2016 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 + * 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 + * 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. =============================================================================== * Purpose: SmartSense Multi Sensor DTH File @@ -24,27 +24,28 @@ =============================================================================== */ - metadata { - definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Three Axis" +metadata { + definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { + + capability "Three Axis" capability "Battery" - capability "Configuration" - capability "Sensor" + capability "Configuration" + capability "Sensor" capability "Contact Sensor" capability "Acceleration Sensor" capability "Refresh" capability "Temperature Measurement" - + 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: "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,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", 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" attribute "status", "string" - } + } - simulator { + simulator { status "open": "zone report :: type: 19 value: 0031" status "closed": "zone report :: type: 19 value: 0030" @@ -61,7 +62,7 @@ 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" } - preferences { + preferences { section { image(name: 'educationalcontent', multiple: true, images: [ "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", @@ -75,9 +76,9 @@ 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?", translatable: true, description: "Tap to set", options: ["Yes","No"], defaultValue: "No", required: false, displayDuringSetup: false) + input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", description: "Tap to set", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) } - } + } tiles(scale: 2) { multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ @@ -109,19 +110,16 @@ ] ) } - valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) { - state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") - } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } main(["status", "acceleration", "temperature"]) - details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"]) + details(["status", "acceleration", "temperature", "battery", "refresh"]) } } @@ -130,61 +128,61 @@ def parse(String description) { if (description?.startsWith('catchall:')) { map = parseCatchAllMessage(description) } - else if (description?.startsWith('temperature: ')) { + else if (description?.startsWith('temperature: ')) { map = parseCustomMessage(description) } else if (description?.startsWith('zone status')) { map = parseIasMessage(description) } - def result = map ? createEvent(map) : null + def result = map ? createEvent(map) : null if (description?.startsWith('enroll request')) { List cmds = enrollResponse() log.debug "enroll response: ${cmds}" result = cmds?.collect { new physicalgraph.device.HubAction(it) } } - else if (description?.startsWith('read attr -')) { - result = parseReportAttributeMessage(description).each { createEvent(it) } - } + else if (description?.startsWith('read attr -')) { + result = parseReportAttributeMessage(description).each { createEvent(it) } + } return result } - private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - log.debug cluster - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(cluster.data.last()) - break +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break - case 0xFC02: - log.debug 'ACCELERATION' - break + case 0xFC02: + log.debug 'ACCELERATION' + break - case 0x0402: - log.debug 'TEMP' - // temp is last 2 data values. reverse to swap endian - String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() - def value = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - } - } + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } - return resultMap - } + return resultMap +} private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - // 0x07 is bind message - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - cluster.command == 0x07 || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage } private List parseReportAttributeMessage(String description) { @@ -211,10 +209,12 @@ private List parseReportAttributeMessage(String description) { result << parseAxis(threeAxisAttributes) 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) { - result << parseAxis(descMap.value) + // The size is checked to ensure the attribute report contains X, Y and Z values + // If all three axis are not included then the attribute report is ignored + result << parseAxis(descMap.value) } else if (descMap.cluster == "0001" && descMap.attrId == "0020") { result << getBatteryResult(Integer.parseInt(descMap.value, 16)) @@ -238,43 +238,43 @@ private Map parseIasMessage(String description) { Map resultMap = [:] switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry + case '0x0020': // Closed/No Motion/Dry if (garageSensor != "Yes"){ resultMap = getContactResult('closed') } - break + break - case '0x0021': // Open/Motion/Wet + case '0x0021': // Open/Motion/Wet if (garageSensor != "Yes"){ resultMap = getContactResult('open') } - break + break - case '0x0022': // Tamper Alarm - break + case '0x0022': // Tamper Alarm + break - case '0x0023': // Battery Alarm - break + case '0x0023': // Battery Alarm + break - case '0x0024': // Supervision Report + case '0x0024': // Supervision Report if (garageSensor != "Yes"){ resultMap = getContactResult('closed') } - break + break - case '0x0025': // Restore Report + case '0x0025': // Restore Report if (garageSensor != "Yes"){ resultMap = getContactResult('open') } - break + break - case '0x0026': // Trouble/Failure - break + case '0x0026': // Trouble/Failure + break - case '0x0028': // Test Mode - break - } - return resultMap + case '0x0028': // Test Mode + break + } + return resultMap } def updated() { @@ -311,7 +311,6 @@ def getTemperature(value) { private Map getBatteryResult(rawValue) { log.debug "Battery rawValue = ${rawValue}" - def linkText = getLinkText(device) def result = [ name: 'battery', @@ -327,7 +326,7 @@ private Map getBatteryResult(rawValue) { result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts." } else { - if (device.getDataValue("manufacturer") == "SmartThings") { + 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] @@ -341,8 +340,7 @@ private Map getBatteryResult(rawValue) { def pct = batteryMap[volts] if (pct != null) { result.value = pct - def value = pct - result.descriptionText = "{{ device.displayName }} battery was {{ value }}" + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" } } else { @@ -350,7 +348,7 @@ private Map getBatteryResult(rawValue) { def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "{{ device.displayName }} battery was {{ value }}" + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" } } } @@ -359,56 +357,36 @@ private Map getBatteryResult(rawValue) { } private Map getTemperatureResult(value) { - log.debug 'TEMP' - def name = "temperature" + log.debug "Temperature" if (tempOffset) { def offset = tempOffset as int def v = value as int value = v + offset } - - def descriptionText - if ( temperatureScale == 'C' ) - descriptionText = '{{ device.displayName }} was {{ value }}°C' - else - descriptionText = '{{ device.displayName }} was {{ value }}°F' + def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C': + '{{ device.displayName }} was {{ value }}°F' return [ - name: name, - value: value, - descriptionText: descriptionText, - translatable: true + name: 'temperature', + value: value, + descriptionText: descriptionText, + translatable: true ] } private Map getContactResult(value) { log.debug "Contact: ${device.displayName} value = ${value}" - def name = "contact" - - def descriptionText - if ( value == 'open' ) - descriptionText = '{{ device.displayName }} was opened' - else - descriptionText = '{{ device.displayName }} was closed' - - sendEvent(name: 'status', value: value, descriptionText: descriptionText, displayed: false) - def isStateChange = isStateChange(device, name, value) - return [ - name: name, - value: value, - descriptionText: descriptionText, - isStateChange: isStateChange, - translatable: true - ] - + def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' + sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true) + sendEvent(name: 'status', value: value, descriptionText: descriptionText, translatable: true) } private getAccelerationResult(numValue) { - log.debug "Acceleration is $value" + log.debug "Acceleration" def name = "acceleration" def value def descriptionText - + if ( numValue.endsWith("1") ) { value = "active" descriptionText = '{{ device.displayName }} was active' @@ -473,37 +451,38 @@ def refresh() { def configure() { String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Device Reporting" - + log.debug "Configuring Reporting" + def configCmds = [ "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", - "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}", "delay 200", //checkin time 6 hrs "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", + "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200", - "zcl mfg-code ${manufacturerCode}", - "zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", + "zcl mfg-code ${manufacturerCode}", "delay 200", + "zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zcl mfg-code ${manufacturerCode}", - "zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}", + "zcl mfg-code ${manufacturerCode}", "delay 200", + "zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zcl mfg-code ${manufacturerCode}", - "zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}", + "zcl mfg-code ${manufacturerCode}", "delay 200", + "zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zcl mfg-code ${manufacturerCode}", - "zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", + "zcl mfg-code ${manufacturerCode}", "delay 200", + "zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - ] + ] + return configCmds + refresh() } @@ -519,17 +498,12 @@ def enrollResponse() { "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", //Enroll Response - "raw 0x500 {01 23 00 00 00}", + "raw 0x500 {01 23 00 00 00}", "delay 200", "send 0x${device.deviceNetworkId} 1 1", "delay 200" ] } private Map parseAxis(String description) { - def hexToSignedInt = { hexVal -> - def unsignedVal = hexToInt(hexVal) - unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal - } - def z = hexToSignedInt(description[0..3]) def y = hexToSignedInt(description[10..13]) def x = hexToSignedInt(description[20..23]) @@ -556,6 +530,11 @@ private Map parseAxis(String description) { getXyzResult(xyzResults, description) } +private hexToSignedInt(hexVal) { + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal +} + def garageEvent(zValue) { def absValue = zValue.abs() def contactValue = null @@ -569,15 +548,9 @@ def garageEvent(zValue) { garageValue = 'garage-open' } if (contactValue != null){ - def linkText = getLinkText(device) - if ( contactValue == 'open' ) { - descriptionText: '{{ device.displayName }} was opened' - sendEvent(name: 'contact', value: contactValue, descriptionText: '{{ device.displayName }} was opened', displayed:false, translatable: true) - sendEvent(name: 'status', value: garageValue, descriptionText: '{{ device.displayName }} status was opened', translatable: true) - } else { - sendEvent(name: 'contact', value: contactValue, descriptionText: '{{ device.displayName }} was closed', displayed:false, translatable: true) - sendEvent(name: 'status', value: garageValue, descriptionText: '{{ device.displayName }} status was closed', translatable: true) - } + def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' :'{{ device.displayName }} was closed' + sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false, translatable: true) + sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true) } } @@ -632,4 +605,4 @@ private byte[] reverseArray(byte[] array) { i++; } return array -} \ No newline at end of file +}