diff --git a/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy index 3175764253d..f13b89b96e1 100644 --- a/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy +++ b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy @@ -11,7 +11,6 @@ * for the specific language governing permissions and limitations under the License. * */ - metadata { definition (name: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") { capability "Tone" @@ -60,7 +59,7 @@ def updated() { } def configure() { - def cmds = zigbee.configureReporting(0x0001, 0x0020, 0x20, 20, 20, 0x01) + def cmds = zigbee.batteryConfig(20, 20, 0x01) log.debug "configure -- cmds: ${cmds}" return cmds } diff --git a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy index faaed372f1a..80ca040d556 100644 --- a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy +++ b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy @@ -69,292 +69,35 @@ metadata { def parse(String description) { log.debug "description is $description" - def event = [:] - def finalResult = isKnownDescription(description) - if (finalResult) { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - event = null - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - event = createEvent(name: "power", value: powerValue) - - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ - } - else { - event = createEvent(name: finalResult.type, value: finalResult.value) - } - } - else { + def event = zigbee.getEvent(description) + if (!event) { log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) + log.debug zigbee.parseDescriptionAsMap(description) + } else if (event.name == "power") { + /* + Dividing by 10 as the Divisor is 10000 and unit is kW for the device. Simplifying to 10 power level is an integer. + */ + event.value = event.value / 10 } return event } -// Commands to device -def zigbeeCommand(cluster, attribute){ - "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" +def setLevel(value) { + zigbee.setLevel(value) } def off() { - zigbeeCommand("6", "0") + zigbee.off() } def on() { - zigbeeCommand("6", "1") -} - -def setLevel(value) { - value = value as Integer - if (value == 0) { - off() - } - else { - if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - sendEvent(name: "level", value: value) - setLevelWithRate(value, "0000") //value is between 0 to 100 - } + zigbee.on() } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 2000", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 2000", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0B04 0x050B", "delay 2000" - ] - + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.electricMeasurementPowerRefresh() } def configure() { - refresh() + onOffConfig() + levelConfig() + powerConfig() -} - - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def parseDescriptionAsMap(description) { - if (description?.startsWith("read attr -")) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] - } - } - else if (description?.startsWith("catchall: ")) { - def seg = (description - "catchall: ").split(" ") - def zigbeeMap = [:] - zigbeeMap += [raw: (description - "catchall: ")] - zigbeeMap += [profileId: seg[0]] - zigbeeMap += [clusterId: seg[1]] - zigbeeMap += [sourceEndpoint: seg[2]] - zigbeeMap += [destinationEndpoint: seg[3]] - zigbeeMap += [options: seg[4]] - zigbeeMap += [messageType: seg[5]] - zigbeeMap += [dni: seg[6]] - zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] - zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] - zigbeeMap += [manufacturerId: seg[9]] - zigbeeMap += [command: seg[10]] - zigbeeMap += [direction: seg[11]] - zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { - it.join('') - } : []] - - zigbeeMap - } -} - -def isKnownDescription(description) { - if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { - def descMap = parseDescriptionAsMap(description) - if (descMap.cluster == "0006" || descMap.clusterId == "0006") { - isDescriptionOnOff(descMap) - } - else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){ - isDescriptionLevel(descMap) - } - else if (descMap.cluster == "0B04" || descMap.clusterId == "0B04"){ - isDescriptionPower(descMap) - } - else { - return [:] - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return [:] - } -} - -def isDescriptionOnOff(descMap) { - def switchValue = "undefined" - if (descMap.cluster == "0006") { //cluster info from read attr - value = descMap.value - if (value == "01"){ - switchValue = "on" - } - else if (value == "00"){ - switchValue = "off" - } - } - else if (descMap.clusterId == "0006") { - //cluster info from catch all - //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 - //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 - if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ - switchValue = "on" - } - else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ - switchValue = "off" - } - else if(descMap.command=="07"){ - return [type: "update", value : "switch (0006) capability configured successfully"] - } - } - - if (switchValue != "undefined"){ - return [type: "switch", value : switchValue] - } - else { - return [:] - } - -} - -//@return - false or "success" or level [0-100] -def isDescriptionLevel(descMap) { - def dimmerValue = -1 - if (descMap.cluster == "0008"){ - //TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message - def value = convertHexToInt(descMap.value) - dimmerValue = Math.round(value * 100 / 255) - if(dimmerValue==0 && value > 0) { - dimmerValue = 1 //handling for non-zero hex value less than 3 - } - } - else if(descMap.clusterId == "0008") { - if(descMap.command=="0B"){ - return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent. - } - else if(descMap.command=="07"){ - return [type: "update", value : "level (0008) capability configured successfully"] - } - } - - if (dimmerValue != -1){ - return [type: "level", value : dimmerValue] - } - else { - return [:] - } -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0B04") { - if (descMap.attrId == "050b") { - if(descMap.value!="ffff") - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0B04") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0B04) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return [:] - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 2000", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000" - ] -} - -//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) -//min level change is 01 -def levelConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 2000", - "zcl global send-me-a-report 8 0 0x20 5 3600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000" - ] -} - -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 05 -def powerConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 2000", - "zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite - "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000" - ] -} - -def setLevelWithRate(level, rate) { - if(rate == null){ - rate = "0000" - } - level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex - [ - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}", - "delay 2000" - ] -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + refresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.electricMeasurementPowerConfig() } diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index 5001ad84719..6a42ae91942 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -13,10 +13,9 @@ * License for the specific language governing permissions and limitations * under the License. */ - metadata { // Automatically generated. Make future change here. - definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { capability "Actuator" capability "Switch" capability "Power Meter" @@ -25,9 +24,9 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019" } @@ -45,32 +44,32 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg", - "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg" - ]) + "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg", + "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg" + ]) } } // UI tile definitions tiles(scale: 2) { - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ - tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" attributeState "turningOn", label: 'Turning On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" attributeState "turningOff", label: 'Turning Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" } - tileAttribute ("power", key: "SECONDARY_CONTROL") { - attributeState "power", label:'${currentValue} W' + tileAttribute("power", key: "SECONDARY_CONTROL") { + attributeState "power", label: '${currentValue} W' } } standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" } main "switch" - details(["switch","refresh"]) + details(["switch", "refresh"]) } } @@ -78,47 +77,29 @@ metadata { def parse(String description) { log.debug "description is $description" - def finalResult = zigbee.getKnownDescription(description) - def event = [:] - - //TODO: Remove this after getKnownDescription can parse it automatically - if (!finalResult && description!="updated") - finalResult = getPowerDescription(zigbee.parseDescriptionAsMap(description)) + def event = zigbee.getEvent(description) - if (finalResult) { - log.info "final result = $finalResult" - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - event = null - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - event = createEvent(name: "power", value: powerValue, descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true) - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ + if (event) { + if (event.name == "power") { + event.value = event.value / 10 + event.descriptionText = '{{ device.displayName }} power is {{ value }} Watts' + event.translatable = true + } else if (event.name == "switch") { + def descriptionText = event.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' + event = createEvent(name: event.name, value: event.value, descriptionText: descriptionText, translatable: true) } - else { - def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' - event = createEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true) - } - } - else { + } else { def cluster = zigbee.parse(description) - if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07){ + if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { if (cluster.data[0] == 0x00) { log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster event = createEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - } - else { + } else { log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" event = null } - } - else { + } else { log.warn "DID NOT PARSE MESSAGE for description : $description" log.debug "${cluster}" } @@ -150,43 +131,6 @@ def configure() { sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - refresh() + zigbee.onOffConfig(0, 300) + powerConfig() + refresh() + zigbee.onOffConfig(0, 300) + zigbee.electricMeasurementPowerConfig() } -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 01 -def powerConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 2000", - "zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite - "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -//TODO: Remove this after getKnownDescription can parse it automatically -def getPowerDescription(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0B04") { - if (descMap.attrId == "050b") { - if(descMap.value!="ffff") - powerValue = zigbee.convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0B04") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0B04) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return [:] - } -} diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index 052a90b5bde..4d27081d14f 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus metadata { - definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartSense Moisture Sensor", namespace: "smartthings", author: "SmartThings") { capability "Configuration" capability "Battery" capability "Refresh" @@ -43,10 +43,10 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png", - "http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png", - "http://cdn.device-gse.smartthings.com/Moisture/Moisture3.png" - ]) + "http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png", + "http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png", + "http://cdn.device-gse.smartthings.com/Moisture/Moisture3.png" + ]) } 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" @@ -55,32 +55,32 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ - tileAttribute ("device.water", key: "PRIMARY_CONTROL") { - attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute("device.water", key: "PRIMARY_CONTROL") { + attributeState "dry", label: "Dry", icon: "st.alarm.water.dry", backgroundColor: "#ffffff" + attributeState "wet", label: "Wet", icon: "st.alarm.water.wet", backgroundColor: "#53a7c0" } } valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - state "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + state "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] } 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) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } - main (["water", "temperature"]) + main(["water", "temperature"]) details(["water", "temperature", "battery", "refresh"]) } } @@ -88,117 +88,43 @@ metadata { def parse(String description) { log.debug "description: $description" - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) - } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) + // getEvent will handle temperature and humidity + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } + } } log.debug "Parse returned $map" def result = map ? createEvent(map) : [:] if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() + List cmds = zigbee.enrollResponse() log.debug "enroll response: ${cmds}" result = cmds?.collect { new physicalgraph.device.HubAction(it) } } return result } -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - resultMap = getBatteryResult(cluster.data.last()) - } - break - - case 0x0402: - if (cluster.command == 0x07) { - if (cluster.data[0] == 0x00){ - log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster - resultMap = [name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] - } - else { - log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}" - } - } - else { - // 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 -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage -} - -private Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - - return resultMap -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - return resultMap -} - private Map parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry') } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return Math.round(celsius) - } else { - return Math.round(celsiusToFahrenheit(celsius)) - } -} - private Map getBatteryResult(rawValue) { log.debug "Battery rawValue = ${rawValue}" def linkText = getLinkText(device) @@ -239,40 +165,18 @@ private Map getBatteryResult(rawValue) { return result } -private Map getTemperatureResult(value) { - log.debug 'TEMP' - 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' - - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText, - translatable: true, - unit: temperatureScale - ] -} - private Map getMoistureResult(value) { log.debug "water" - def descriptionText - if ( value == "wet" ) - descriptionText = '{{ device.displayName }} is wet' - else - descriptionText = '{{ device.displayName }} is dry' + def descriptionText + if (value == "wet") + descriptionText = '{{ device.displayName }} is wet' + else + descriptionText = '{{ device.displayName }} is dry' return [ - name: 'water', - value: value, - descriptionText: descriptionText, - translatable: true + name : 'water', + value : value, + descriptionText: descriptionText, + translatable : true ] } @@ -285,12 +189,10 @@ def ping() { def refresh() { log.debug "Refreshing Temperature and Battery" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 2000", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 2000" - ] + def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) - return refreshCmds + enrollResponse() + return refreshCmds + zigbee.enrollResponse() } def configure() { @@ -302,42 +204,3 @@ def configure() { // battery minReport 30 seconds, maxReportTime 6 hrs by default return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } - -def enrollResponse() { - log.debug "Sending enroll response" - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - [ - //Resending the CIE in case the enroll request is sent before CIE is written - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 2000" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index 32f95dcf9ee..a93e927a948 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus metadata { - definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { capability "Motion Sensor" capability "Configuration" capability "Battery" @@ -45,10 +45,10 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg", - "http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg", - "http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg" - ]) + "http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg", + "http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg", + "http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg" + ]) } 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" @@ -57,30 +57,30 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ - tileAttribute ("device.motion", key: "PRIMARY_CONTROL") { - attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + multiAttributeTile(name: "motion", type: "generic", width: 6, height: 4) { + tileAttribute("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label: 'motion', icon: "st.motion.motion.active", backgroundColor: "#53a7c0" + attributeState "inactive", label: 'no motion', icon: "st.motion.motion.inactive", backgroundColor: "#ffffff" } } valueTile("temperature", "device.temperature", width: 2, height: 2) { - state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + state("temperature", label: '${currentValue}°', unit: "F", + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] ) } 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) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } main(["motion", "temperature"]) @@ -90,116 +90,40 @@ metadata { def parse(String description) { log.debug "description: $description" - - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) - } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } else if (descMap.clusterInt == 0x0406 && descMap.attrInt == 0x0000) { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + log.warn "Doing a read attr motion event" + resultMap = getMotionResult(value) + } + } } log.debug "Parse returned $map" def result = map ? createEvent(map) : [:] if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() + List cmds = zigbee.enrollResponse() log.debug "enroll response: ${cmds}" result = cmds?.collect { new physicalgraph.device.HubAction(it) } } return result } -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - resultMap = getBatteryResult(cluster.data.last()) - } - break - - case 0x0402: - if (cluster.command == 0x07) { - if (cluster.data[0] == 0x00) { - log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster - resultMap = [name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] - } - else { - log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}" - } - - } - else { - // 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 0x0406: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - log.debug 'motion' - resultMap.name = 'motion' - } - break - } - } - - return resultMap -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage -} - -private Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - else if (descMap.cluster == "0406" && descMap.attrId == "0000") { - def value = descMap.value.endsWith("01") ? "active" : "inactive" - resultMap = getMotionResult(value) - } - - return resultMap -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - return resultMap -} - private Map parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) @@ -207,15 +131,6 @@ private Map parseIasMessage(String description) { return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive') } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return Math.round(celsius) - } else { - return Math.round(celsiusToFahrenheit(celsius)) - } -} - private Map getBatteryResult(rawValue) { log.debug "Battery rawValue = ${rawValue}" def linkText = getLinkText(device) @@ -255,36 +170,14 @@ private Map getBatteryResult(rawValue) { return result } -private Map getTemperatureResult(value) { - log.debug 'TEMP' - 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' - - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText, - translatable: true, - unit: temperatureScale - ] -} - private Map getMotionResult(value) { log.debug 'motion' String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped" return [ - name: 'motion', - value: value, - descriptionText: descriptionText, - translatable: true + name : 'motion', + value : value, + descriptionText: descriptionText, + translatable : true ] } @@ -292,17 +185,16 @@ private Map getMotionResult(value) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) // Read the Battery Level } def refresh() { log.debug "refresh called" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 2000", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 2000" - ] - return refreshCmds + enrollResponse() + def refreshCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + return refreshCmds + zigbee.enrollResponse() } def configure() { @@ -314,42 +206,3 @@ def configure() { // battery minReport 30 seconds, maxReportTime 6 hrs by default return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } - -def enrollResponse() { - log.debug "Sending enroll response" - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - [ - //Resending the CIE in case the enroll request is sent before CIE is written - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 2000" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} 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 16a2ed20c32..f9cc7f0da87 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -14,22 +14,23 @@ * under the License. */ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { capability "Three Axis" capability "Battery" capability "Configuration" capability "Sensor" - capability "Contact Sensor" - capability "Acceleration Sensor" - capability "Refresh" - capability "Temperature Measurement" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Refresh" + capability "Temperature Measurement" capability "Health Check" - command "enrollResponse" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" + 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" @@ -57,11 +58,11 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", - "http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg", - "http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg", - "http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg" - ]) + "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg" + ]) } 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" @@ -73,210 +74,169 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ - tileAttribute ("device.status", key: "PRIMARY_CONTROL") { - attributeState "open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821" - attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" - attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" + multiAttributeTile(name: "status", type: "generic", width: 6, height: 4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState "open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + attributeState "closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + attributeState "garage-open", label: 'Open', icon: "st.doors.garage.garage-open", backgroundColor: "#ffa81e" + attributeState "garage-closed", label: 'Closed', icon: "st.doors.garage.garage-closed", backgroundColor: "#79b821" } } standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#ffa81e") + state("closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#79b821") } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'Active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'Inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label: 'Active', icon: "st.motion.acceleration.active", backgroundColor: "#53a7c0") + state("inactive", label: 'Inactive', icon: "st.motion.acceleration.inactive", backgroundColor: "#ffffff") } valueTile("temperature", "device.temperature", width: 2, height: 2) { - state("temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + state("temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] ) } 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) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } main(["status", "acceleration", "temperature"]) details(["status", "acceleration", "temperature", "battery", "refresh"]) } - } +} def parse(String description) { - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) - } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } + def maps = [] + maps << zigbee.getEvent(description) + if (!maps[0]) { + maps = [] + if (description?.startsWith('zone status')) { + maps += parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + maps << getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } else { - def result = map ? createEvent(map) : [:] + maps += handleAcceleration(descMap) + } + } + } else if (maps[0].name == "temperature") { + def map = maps[0] + if (tempOffset) { + map.value = (int) map.value + (int) tempOffset + } + map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' + map.translatable = true + } + def result = maps.inject([]) {acc, it -> + if (it) { + acc << createEvent(it) + } + } if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() + List cmds = zigbee.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) } - } 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: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - resultMap = getBatteryResult(cluster.data.last()) - } - break - - case 0xFC02: - log.debug 'ACCELERATION' - break - - case 0x0402: - if (cluster.command == 0x07) { - if(cluster.data[0] == 0x00) { - log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster - resultMap = [name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] - } - else { - log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}" - } - } - else { - // 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 +private List handleAcceleration(descMap) { + def result = [] + if (descMap.clusterInt == 0xFC02 && descMap.attrInt == 0x0010) { + def value = descMap.value == "01" ? "active" : "inactive" + log.debug "Acceleration $value" + result << [ + name : "acceleration", + value : value, + descriptionText: "{{ device.displayName }} was $value", + isStateChange : isStateChange(device, "acceleration", value), + translatable : true + ] + + if (descMap.additionalAttrs) { + result += parseAxis(descMap.additionalAttrs) } + } else if (descMap.clusterInt == 0xFC02 && descMap.attrInt == 0x0012) { + def addAttrs = descMap.additionalAttrs + addAttrs << ["attrInt": descMap.attrInt, "value": descMap.value] + result += parseAxis(addAttrs) } - - return resultMap -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage + return result } -private List parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } +private List parseAxis(List attrData) { + def results = [] + def x = hexToSignedInt(attrData.find { it.attrInt == 0x0012 }?.value) + def y = hexToSignedInt(attrData.find { it.attrInt == 0x0013 }?.value) + def z = hexToSignedInt(attrData.find { it.attrInt == 0x0014 }?.value) - List result = [] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - result << getTemperatureResult(value) - } - else if (descMap.cluster == "FC02" && descMap.attrId == "0010") { - if (descMap.value.size() == 32) { - // value will look like 00ae29001403e2290013001629001201 - // breaking this apart and swapping byte order where appropriate, this breaks down to: - // X (0x0012) = 0x0016 - // Y (0x0013) = 0x03E2 - // Z (0x0014) = 0x00AE - // note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order - // this will be fixed in a future update - def threeAxisAttributes = descMap.value[0..-9] - result << parseAxis(threeAxisAttributes) - descMap.value = descMap.value[-2..-1] - } - result << getAccelerationResult(descMap.value) - } - 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 - // 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)) + def xyzResults = [:] + if (device.getDataValue("manufacturer") == "SmartThings") { + // This mapping matches the current behavior of the Device Handler for the Centralite sensors + xyzResults.x = z + xyzResults.y = y + xyzResults.z = -x + } else { + // The axises reported by the Device Handler differ from the axises reported by the sensor + // This may change in the future + xyzResults.x = z + xyzResults.y = x + xyzResults.z = y } - return result -} + log.debug "parseAxis -- ${xyzResults}" -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - return resultMap + if (garageSensor == "Yes") + results += garageEvent(xyzResults.z) + + def value = "${xyzResults.x},${xyzResults.y},${xyzResults.z}" + results << [ + name : "threeAxis", + value : value, + linkText : getLinkText(device), + descriptionText: "${getLinkText(device)} was ${value}", + handlerName : name, + isStateChange : isStateChange(device, "threeAxis", value), + displayed : false + ] + results } -private Map parseIasMessage(String description) { +private List parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) - Map resultMap = [:] - - if (garageSensor != "Yes"){ - resultMap = zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') - } - - return resultMap -} - -def updated() { - log.debug "updated called" - log.info "garage value : $garageSensor" - if (garageSensor == "Yes") { - def descriptionText = "Updating device to garage sensor" - if (device.latestValue("status") == "open") { - sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText, translatable: true) - } - else if (device.latestValue("status") == "closed") { - sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText, translatable: true) - } - } - else { - def descriptionText = "Updating device to open/close sensor" - if (device.latestValue("status") == "garage-open") { - sendEvent(name: 'status', value: 'open', descriptionText: descriptionText, translatable: true) - } - else if (device.latestValue("status") == "garage-closed") { - sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText, translatable: true) - } + List results = [] + + if (garageSensor != "Yes") { + def value = zs.isAlarm1Set() ? 'open' : 'closed' + log.debug "Contact: ${device.displayName} value = ${value}" + def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' + results << [name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true] + results << [name: 'status', value: value, descriptionText: descriptionText, translatable: true] } -} -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return Math.round(celsius) - } else { - return Math.round(celsiusToFahrenheit(celsius)) - } - } + return results +} private Map getBatteryResult(rawValue) { log.debug "Battery rawValue = ${rawValue}" @@ -316,54 +276,24 @@ private Map getBatteryResult(rawValue) { return result } -private Map getTemperatureResult(value) { - log.debug "Temperature" - if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset +List garageEvent(zValue) { + List results = [] + def absValue = zValue.abs() + def contactValue = null + def garageValue = null + if (absValue > 900) { + contactValue = 'closed' + garageValue = 'garage-closed' + } else if (absValue < 100) { + contactValue = 'open' + garageValue = 'garage-open' } - def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C': - '{{ device.displayName }} was {{ value }}°F' - - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText, - translatable: true, - unit: temperatureScale - ] -} - -private Map getContactResult(value) { - log.debug "Contact: ${device.displayName} value = ${value}" - def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' - sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true) - return [name: 'status', value: value, descriptionText: descriptionText, translatable: true] -} - -private getAccelerationResult(numValue) { - log.debug "Acceleration" - def name = "acceleration" - def value - def descriptionText - - if ( numValue.endsWith("1") ) { - value = "active" - descriptionText = '{{ device.displayName }} was active' - } else { - value = "inactive" - descriptionText = '{{ device.displayName }} was inactive' - } - - def isStateChange = isStateChange(device, name, value) - return [ - name: name, - value: value, - descriptionText: descriptionText, - isStateChange: isStateChange, - translatable: true - ] + if (contactValue != null) { + def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' + results << [name: 'contact', value: contactValue, descriptionText: descriptionText, displayed: false, translatable: true] + results << [name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true] + } + results } /** @@ -376,7 +306,21 @@ def ping() { def refresh() { log.debug "Refreshing Values " - def refreshCmds = [] + def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) + + zigbee.enrollResponse() + + return refreshCmds +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + log.debug "Configuring Reporting" + def configCmds = [] if (device.getDataValue("manufacturer") == "SmartThings") { log.debug "Refreshing Values for manufacturer: SmartThings " @@ -385,81 +329,45 @@ def refresh() { Separating these out in a separate if-else because I do not want to touch Centralite part as of now. */ - refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode]) - refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode]) + configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode]) + configCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode]) } else { - refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode]) + // Write a motion threshold of 2 * .063g = .126g + // Currently due to a Centralite firmware issue, this will cause a read attribute response that + // indicates acceleration even when there isn't. + configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode]) } - //Common refresh commands - refreshCmds += zigbee.readAttribute(0x0402, 0x0000) + - zigbee.readAttribute(0x0001, 0x0020) + - zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) - - return refreshCmds + enrollResponse() -} - -def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) - // enrolls with default periodic reporting until newer 5 min interval is confirmed - sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - - log.debug "Configuring Reporting" - // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - def configCmds = zigbee.batteryConfig() + + configCmds += zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + - zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + zigbee.configureReporting(0xFC02, 0x0010, DataType.BITMAP8, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0012, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0013, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0014, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) return refresh() + configCmds } -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -def enrollResponse() { - log.debug "Sending enroll response" - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - [ - //Resending the CIE in case the enroll request is sent before CIE is written - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 2000" - ] -} - -private Map parseAxis(String description) { - def z = hexToSignedInt(description[0..3]) - def y = hexToSignedInt(description[10..13]) - def x = hexToSignedInt(description[20..23]) - def xyzResults = [x: x, y: y, z: z] - - if (device.getDataValue("manufacturer") == "SmartThings") { - // This mapping matches the current behavior of the Device Handler for the Centralite sensors - xyzResults.x = z - xyzResults.y = y - xyzResults.z = -x +def updated() { + log.debug "updated called" + log.info "garage value : $garageSensor" + if (garageSensor == "Yes") { + def descriptionText = "Updating device to garage sensor" + if (device.latestValue("status") == "open") { + sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText, translatable: true) + } else if (device.latestValue("status") == "closed") { + sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText, translatable: true) + } } else { - // The axises reported by the Device Handler differ from the axises reported by the sensor - // This may change in the future - xyzResults.x = z - xyzResults.y = x - xyzResults.z = y + def descriptionText = "Updating device to open/close sensor" + if (device.latestValue("status") == "garage-open") { + sendEvent(name: 'status', value: 'open', descriptionText: descriptionText, translatable: true) + } else if (device.latestValue("status") == "garage-closed") { + sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText, translatable: true) + } } - - log.debug "parseAxis -- ${xyzResults}" - - if (garageSensor == "Yes") - garageEvent(xyzResults.z) - - getXyzResult(xyzResults, description) } private hexToSignedInt(hexVal) { @@ -467,44 +375,6 @@ private hexToSignedInt(hexVal) { unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal } -def garageEvent(zValue) { - def absValue = zValue.abs() - def contactValue = null - def garageValue = null - if (absValue>900) { - contactValue = 'closed' - garageValue = 'garage-closed' - } - else if (absValue < 100) { - contactValue = 'open' - garageValue = 'garage-open' - } - if (contactValue != null){ - 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) - } -} - -private Map getXyzResult(results, description) { - def name = "threeAxis" - def value = "${results.x},${results.y},${results.z}" - def linkText = getLinkText(device) - def descriptionText = "$linkText was $value" - def isStateChange = isStateChange(device, name, value) - - [ - name: name, - value: value, - unit: null, - linkText: linkText, - descriptionText: descriptionText, - handlerName: name, - isStateChange: isStateChange, - displayed: false - ] -} - private getManufacturerCode() { if (device.getDataValue("manufacturer") == "SmartThings") { return "0x110A" @@ -516,25 +386,3 @@ private getManufacturerCode() { private hexToInt(value) { new BigInteger(value, 16) } - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index 7520ffb596b..294717e85d4 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -16,7 +16,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus metadata { - definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { capability "Battery" capability "Configuration" capability "Contact Sensor" @@ -43,155 +43,77 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ - tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4) { + tileAttribute("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + attributeState "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" } } valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - state "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + state "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] } 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) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } - main (["contact", "temperature"]) - details(["contact","temperature","battery","refresh"]) + main(["contact", "temperature"]) + details(["contact", "temperature", "battery", "refresh"]) } } def parse(String description) { log.debug "description: $description" - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } + } } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } log.debug "Parse returned $map" def result = map ? createEvent(map) : [:] - if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() - log.debug "enroll response: ${cmds}" - result = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - return result -} - -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - resultMap = getBatteryResult(cluster.data.last()) - } - break - - case 0x0402: - if (cluster.command == 0x07){ - if (cluster.data[0] == 0x00) { - log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster - resultMap = [name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] - } - else { - log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}" - } - } - else { - // 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 -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage -} - -private int getHumidity(value) { - return Math.round(Double.parseDouble(value)) -} - -private Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - - return resultMap + return result } -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - return resultMap -} private Map parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } -} - private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) @@ -204,8 +126,8 @@ private Map getBatteryResult(rawValue) { def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) def roundedPct = Math.round(pct * 100) - if (roundedPct <= 0) - roundedPct = 1 + if (roundedPct <= 0) + roundedPct = 1 result.value = Math.min(100, roundedPct) result.descriptionText = "${linkText} battery was ${result.value}%" result.name = 'battery' @@ -214,31 +136,14 @@ private Map getBatteryResult(rawValue) { return result } -private Map getTemperatureResult(value) { - log.debug 'TEMP' - 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, - unit: temperatureScale - ] -} - private Map getContactResult(value) { log.debug 'Contact Status' def linkText = getLinkText(device) def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" return [ - name: 'contact', - value: value, - descriptionText: descriptionText + name : 'contact', + value : value, + descriptionText: descriptionText ] } @@ -251,12 +156,10 @@ def ping() { def refresh() { log.debug "Refreshing Temperature and Battery" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 2000", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 2000" - ] + def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) - return refreshCmds + enrollResponse() + return refreshCmds + zigbee.enrollResponse() } def configure() { @@ -268,44 +171,5 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config -} - -def enrollResponse() { - log.debug "Sending enroll response" - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - [ - //Resending the CIE in case the enroll request is sent before CIE is written - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 2000" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array + return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config } diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index 666bf01d66c..c0fe4b0a102 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -13,8 +13,10 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType + metadata { - definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") { + definition(name: "SmartSense Temp/Humidity Sensor", namespace: "smartthings", author: "SmartThings") { capability "Configuration" capability "Battery" capability "Refresh" @@ -31,7 +33,7 @@ metadata { status 'H 45': 'catchall: 0104 FC45 01 01 0140 00 D9B9 00 04 C2DF 0A 01 0000218911' status 'H 57': 'catchall: 0104 FC45 01 01 0140 00 4E55 00 04 C2DF 0A 01 0000211316' status 'H 53': 'catchall: 0104 FC45 01 01 0140 00 20CD 00 04 C2DF 0A 01 0000219814' - status 'H 43': 'read attr - raw: BF7601FC450C00000021A410, dni: BF76, endpoint: 01, cluster: FC45, size: 0C, attrId: 0000, result: success, encoding: 21, value: 10a4' + status 'H 43': 'read attr - raw: BF7601FC450C00000021A410, dni: BF76, endpoint: 01, cluster: FC45, size: 0C, attrId: 0000, result: success, encoding: 21, value: 10a4' } preferences { @@ -40,28 +42,28 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"temperature", type: "generic", width: 6, height: 4){ - tileAttribute ("device.temperature", key: "PRIMARY_CONTROL") { - attributeState "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + multiAttributeTile(name: "temperature", type: "generic", width: 6, height: 4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] } } valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { - state "humidity", label:'${currentValue}% humidity', unit:"" + state "humidity", label: '${currentValue}% humidity', unit: "" } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery' + state "battery", label: '${currentValue}% battery' } 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 "temperature", "humidity" @@ -72,142 +74,31 @@ metadata { def parse(String description) { log.debug "description: $description" - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) { - map = parseCustomMessage(description) + // getEvent will handle temperature and humidity + Map map = zigbee.getEvent(description) + if (!map) { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } } log.debug "Parse returned $map" return map ? createEvent(map) : [:] } -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - resultMap = getBatteryResult(cluster.data.last()) - } - break - - case 0x0402: - if (cluster.command == 0x07) { - if (cluster.data[0] == 0x00){ - log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster - resultMap = [name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] - } - else { - log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}" - } - } - else { - // 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 0xFC45: - // 0x07 - configure reporting - if (cluster.command != 0x07) { - String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('') - String display = Math.round(Integer.valueOf(pctStr, 16) / 100) - resultMap = getHumidityResult(display) - } - break - } - } - - return resultMap -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage -} - -private Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - else if (descMap.cluster == "FC45" && descMap.attrId == "0000") { - def value = getReportAttributeHumidity(descMap.value) - resultMap = getHumidityResult(value) - } - - return resultMap -} - -def getReportAttributeHumidity(String value) { - def humidity = null - if (value?.trim()) { - try { - // value is hex with no decimal - def pct = Integer.parseInt(value.trim(), 16) / 100 - humidity = String.format('%.0f', pct) - } catch(NumberFormatException nfe) { - log.debug "Error converting $value to humidity" - } - } - return humidity -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - else if (description?.startsWith('humidity: ')) { - def pct = (description - "humidity: " - "%").trim() - if (pct.isNumber()) { - def value = Math.round(new BigDecimal(pct)).toString() - resultMap = getHumidityResult(value) - } else { - log.error "invalid humidity: ${pct}" - } - } - return resultMap -} - -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } -} - private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) - def result = [:] + def result = [:] def volts = rawValue / 10 if (!(rawValue == 0 || rawValue == 255)) { @@ -226,41 +117,22 @@ private Map getBatteryResult(rawValue) { return result } -private Map getTemperatureResult(value) { - log.debug 'TEMP' - 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, - unit: temperatureScale - ] -} - -private Map getHumidityResult(value) { - log.debug 'Humidity' - return value ? [name: 'humidity', value: value, unit: '%'] : [:] -} - /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level + return zigbee.readAttribute(0x0001, 0x0020) // Read the Battery Level } -def refresh() -{ +def refresh() { log.debug "refresh temperature, humidity, and battery" - return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware - zigbee.readAttribute(0x0402, 0x0000) + - zigbee.readAttribute(0x0001, 0x0020) + return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware + zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.configureReporting(0xFC45, 0x0000, DataType.INT16, 30, 3600, 100) + + zigbee.batteryConfig() + + zigbee.temperatureConfig(30, 300) } def configure() { @@ -269,35 +141,8 @@ def configure() { sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) log.debug "Configuring Reporting and Bindings." - def humidityConfigCmds = [ - "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 2000", - "zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 2000" - ] // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return refresh() + humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config -} - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array + return refresh() } diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy index b8d5499469d..311c65975c4 100644 --- a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -13,6 +13,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") { @@ -82,7 +83,7 @@ def parse(String description) { def result = event ? createEvent(event) : [] if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() + List cmds = zigbee.enrollResponse() result = cmds?.collect { new physicalgraph.device.HubAction(it) } } return result @@ -160,7 +161,7 @@ private Map parseNonIasButtonMessage(Map descMap){ def refresh() { log.debug "Refreshing Battery" - return zigbee.readAttribute(0x0001, 0x20) + + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20) + zigbee.enrollResponse() } @@ -177,9 +178,9 @@ def configure() { } return zigbee.onOffConfig() + zigbee.levelConfig() + - zigbee.configureReporting(0x0001, 0x20, 0x20, 30, 21600, 0x01) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20, DataType.UINT8, 30, 21600, 0x01) + zigbee.enrollResponse() + - zigbee.readAttribute(0x0001, 0x20) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20) + cmds } diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 4bef6a9a688..a781e777149 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -97,7 +97,7 @@ def on() { } def setLevel(value) { - zigbee.setLevel(value) + zigbee.setLevel(value) + (value?.toInteger() > 0 ? zigbee.on() : []) } /** diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 41de4e8e545..27ba626ee21 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -13,6 +13,8 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType + metadata { definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings") { @@ -71,9 +73,6 @@ private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 } private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } -private getTYPE_U8() { 0x20 } -private getTYPE_ENUM8() { 0x30 } - // Public methods def installed() { log.trace "installed()" @@ -86,9 +85,9 @@ def uninstalled() { def configure() { def cmds = zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, - TYPE_ENUM8, 0, 3600, null) + + DataType.ENUM8, 0, 3600, null) + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, - TYPE_U8, 600, 21600, 0x01) + DataType.UINT8, 600, 21600, 0x01) log.info "configure() --- cmds: $cmds" return refresh() + cmds // send refresh cmds as part of config } diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy index 64de2fb8a91..842638b3c7a 100644 --- a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy @@ -15,6 +15,7 @@ * * This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature) */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings") { @@ -121,7 +122,7 @@ def ping() { } def refresh() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) } def configure() { @@ -131,7 +132,7 @@ def configure() { sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def setLevel(value) { diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index 87360fe8ffe..5eb78e424a1 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -15,6 +15,7 @@ * * This DTH should serve as the generic DTH to handle RGBW ZigBee HA devices */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings") { @@ -139,7 +140,7 @@ def ping() { } def refresh() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) } def configure() { diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index 5b66e836dc3..34192de8cb4 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -23,6 +23,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0003, 0006, 0019, 0406", manufacturer: "Leviton", model: "ZSS-10", deviceJoinName: "Leviton Switch" fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "000A", manufacturer: "HAI", model: "65A21-1", deviceJoinName: "Leviton Wireless Load Control Module-30amp" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL15A", deviceJoinName: "Leviton Lumina RF Plug-In Appliance Module" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL15S", deviceJoinName: "Leviton Lumina RF Switch" } // simulator metadata diff --git a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy index 4b66fc62cb2..905f743e0f6 100644 --- a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -11,6 +11,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZigBee Valve", namespace: "smartthings", author: "SmartThings") { @@ -66,8 +67,6 @@ private getCLUSTER_BASIC() { 0x0000 } private getBASIC_ATTR_POWER_SOURCE() { 0x0007 } private getCLUSTER_POWER() { 0x0001 } private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } -private getTYPE_U8() { 0x20 } -private getTYPE_ENUM8() { 0x30 } // Parse incoming device messages to generate events def parse(String description) { @@ -128,8 +127,8 @@ def refresh() { zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + zigbee.onOffConfig() + - zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) + - zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1) + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 600, 21600, 1) + + zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, DataType.ENUM8, 5, 21600, 1) } def configure() { diff --git a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy index ad591e1d0ec..977774a1efd 100644 --- a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy +++ b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy @@ -11,6 +11,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings") { @@ -107,7 +108,7 @@ def configure() { } def configureAttributes() { - zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) } def refreshAttributes() { diff --git a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy index ce1ac657a6a..aff9fa77969 100644 --- a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy @@ -11,6 +11,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType metadata { definition (name: "ZLL RGBW Bulb", namespace: "smartthings", author: "SmartThings") { @@ -123,7 +124,7 @@ def configure() { } def configureAttributes() { - zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) } def refreshAttributes() { diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 65ed151c5a2..faade50606b 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -73,7 +73,7 @@ def authPage() { return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { section() { paragraph "Tap below to log in to the netatmo and authorize SmartThings access." - href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description + href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}", description:description } } } else { @@ -146,19 +146,24 @@ def callback() { // log.debug "PARAMS: ${params}" - httpPost(params) { resp -> - - def slurper = new JsonSlurper() - - resp.data.each { key, value -> - def data = slurper.parseText(key) - - state.refreshToken = data.refresh_token - state.authToken = data.access_token - state.tokenExpires = now() + (data.expires_in * 1000) - // log.debug "swapped token: $resp.data" - } - } + try { + httpPost(params) { resp -> + + def slurper = new JsonSlurper() + + resp.data.each { key, value -> + def data = slurper.parseText(key) + log.debug "Data: $data" + state.refreshToken = data.refresh_token + state.authToken = data.access_token + //state.accessToken = data.access_token + state.tokenExpires = now() + (data.expires_in * 1000) + // log.debug "swapped token: $resp.data" + } + } + } catch (Exception e) { + log.debug "callback: Call failed $e" + } // Handle success and failure here, and render stuff accordingly if (state.authToken) { @@ -387,18 +392,18 @@ def getDeviceList() { state.deviceDetail = [:] state.deviceState = [:] - apiGet("/api/devicelist") { response -> + apiGet("/api/getstationsdata") { response -> response.data.body.devices.each { value -> def key = value._id deviceList[key] = "${value.station_name}: ${value.module_name}" state.deviceDetail[key] = value state.deviceState[key] = value.dashboard_data - } - response.data.body.modules.each { value -> - def key = value._id - deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}" - state.deviceDetail[key] = value - state.deviceState[key] = value.dashboard_data + value.modules.each { value2 -> + def key2 = value2._id + deviceList[key2] = "${value.station_name}: ${value2.module_name}" + state.deviceDetail[key2] = value2 + state.deviceState[key2] = value2.dashboard_data + } } } @@ -448,6 +453,7 @@ def listDevices() { } def apiGet(String path, Map query, Closure callback) { + if(now() >= state.tokenExpires) { refreshToken(); } @@ -467,12 +473,16 @@ def apiGet(String path, Map query, Closure callback) { } catch (Exception e) { // This is most likely due to an invalid token. Try to refresh it and try again. log.debug "apiGet: Call failed $e" - if(refreshToken()) { - log.debug "apiGet: Trying again after refreshing token" - httpGet(params) { response -> - callback.call(response) - } - } + if(refreshToken()) { + log.debug "apiGet: Trying again after refreshing token" + try { + httpGet(params) { response -> + callback.call(response) + } + } catch (Exception f) { + log.debug "apiGet: Call failed $f" + } + } } } @@ -561,4 +571,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) { private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} +} \ No newline at end of file diff --git a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy index d9e731a0bcf..9951cfac3ff 100644 --- a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -51,7 +51,7 @@ definition( //Device Inputs preferences { - section("Allow to control these things...") { + section("Allow OpenT2T to control these things...") { input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false @@ -329,34 +329,38 @@ private getDeviceType(device) { switch (it.name.toLowerCase()) { case "switch": deviceType = "switch" - break - case "switch level": - deviceType = "light" + if (caps.any { it.name.toLowerCase() == "power meter" }) { + return deviceType + } + if (caps.any { it.name.toLowerCase() == "switch level" }) { + deviceType = "light" + return deviceType + } break case "contact sensor": deviceType = "contactSensor" - break + return deviceType case "garageDoorControl": deviceType = "garageDoor" - break + return deviceType case "lock": deviceType = "lock" - break + return deviceType case "video camera": deviceType = "camera" - break + return deviceType case "motion sensor": deviceType = "motionSensor" - break + return deviceType case "presence sensor": deviceType = "presenceSensor" - break + return deviceType case "thermostat": deviceType = "thermostat" - break + return deviceType case "water sensor": deviceType = "waterSensor" - break + return deviceType default: break } diff --git a/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy index 6a1423d64f2..9421d2365a4 100644 --- a/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy +++ b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy @@ -57,7 +57,7 @@ def authPage(){ atomicState.accessToken = state.accessToken } - def redirectUrl = oauthInitUrl() + def redirectUrl = oauthInitUrl() def uninstallAllowed = false def oauthTokenProvided = false if(atomicState.authToken){ @@ -78,9 +78,9 @@ def authPage(){ } }else{ return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) { - section(){ + section(){ paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl - href(url:redirectUrl, title:"Or", description:"tap to switch accounts") + href(url:redirectUrl, title:"Or", description:"tap to switch accounts") } } } @@ -137,36 +137,44 @@ def dock_sensor(device_serial, expected_plant_name) { contentType: "application/json", ] log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}" - httpPost(docking_params) { docking_response -> - if (parse_api_response(docking_response, "Docking a link")) { - if (docking_response.data.plants.size() == 0) { - log.debug "creating plant for - ${expected_plant_name}" - plant_post_body_map["name"] = expected_plant_name - plant_post_body_map['links_key'] = [docking_response.data.key] - def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map) - plant_post_params["body"] = plant_post_body_json_builder.toString() - httpPost(plant_post_params) { plant_post_response -> - if(parse_api_response(plant_post_response, 'creating plant')){ - def attached_map = atomicState.attached_sensors - attached_map[device_serial] = plant_post_response.data - atomicState.attached_sensors = attached_map + try { + httpPost(docking_params) { docking_response -> + if (parse_api_response(docking_response, "Docking a link")) { + if (docking_response.data.plants.size() == 0) { + log.debug "creating plant for - ${expected_plant_name}" + plant_post_body_map["name"] = expected_plant_name + plant_post_body_map['links_key'] = [docking_response.data.key] + def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map) + plant_post_params["body"] = plant_post_body_json_builder.toString() + try { + httpPost(plant_post_params) { plant_post_response -> + if(parse_api_response(plant_post_response, 'creating plant')){ + def attached_map = atomicState.attached_sensors + attached_map[device_serial] = plant_post_response.data + atomicState.attached_sensors = attached_map + } + } + } catch (Exception f) { + log.debug "call failed $f" } + } else { + def plant = docking_response.data.plants[0] + def attached_map = atomicState.attached_sensors + attached_map[device_serial] = plant + atomicState.attached_sensors = attached_map + checkAndUpdatePlantIfNeeded(plant, expected_plant_name) } - } else { - def plant = docking_response.data.plants[0] - def attached_map = atomicState.attached_sensors - attached_map[device_serial] = plant - atomicState.attached_sensors = attached_map - checkAndUpdatePlantIfNeeded(plant, expected_plant_name) } } + } catch (Exception e) { + log.debug "call failed $e" } return true } def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){ def plant_put_params = [ - uri : appSettings.https_plantLinkServer, + uri : appSettings.https_plantLinkServer, headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], contentType : "application/json" ] @@ -174,12 +182,16 @@ def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){ log.debug "updating plant for - ${expected_plant_name}" plant_put_params["path"] = "/api/v1/plants/${plant.key}" def plant_put_body_map = [ - name: expected_plant_name + name: expected_plant_name ] def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map) plant_put_params["body"] = plant_put_body_json_builder.toString() - httpPut(plant_put_params) { plant_put_response -> - parse_api_response(plant_put_response, 'updating plant name') + try { + httpPut(plant_put_params) { plant_put_response -> + parse_api_response(plant_put_response, 'updating plant name') + } + } catch (Exception e) { + log.debug "call failed $e" } } } @@ -198,25 +210,29 @@ def moistureHandler(event){ contentType: "application/json", body: event.value ] - httpPost(measurement_post_params) { measurement_post_response -> - if (parse_api_response(measurement_post_response, 'creating moisture measurement') && - measurement_post_response.data.size() >0){ - def measurement = measurement_post_response.data[0] - def plant = measurement.plant - log.debug plant - checkAndUpdatePlantIfNeeded(plant, expected_plant_name) - plantlinksensors.each{ sensor_device -> - if (sensor_device.id == event.deviceId){ - sensor_device.setStatusIcon(plant.status) - if (plant.last_measurements && plant.last_measurements[0].moisture){ - sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int) - } - if (plant.last_measurements && plant.last_measurements[0].battery){ - sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int) + try { + httpPost(measurement_post_params) { measurement_post_response -> + if (parse_api_response(measurement_post_response, 'creating moisture measurement') && + measurement_post_response.data.size() >0){ + def measurement = measurement_post_response.data[0] + def plant = measurement.plant + log.debug plant + checkAndUpdatePlantIfNeeded(plant, expected_plant_name) + plantlinksensors.each{ sensor_device -> + if (sensor_device.id == event.deviceId){ + sensor_device.setStatusIcon(plant.status) + if (plant.last_measurements && plant.last_measurements[0].moisture){ + sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int) + } + if (plant.last_measurements && plant.last_measurements[0].battery){ + sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int) + } } } } } + } catch (Exception e) { + log.debug "call failed $e" } } } @@ -235,8 +251,12 @@ def batteryHandler(event){ contentType: "application/json", body: event.value ] - httpPost(measurement_post_params) { measurement_post_response -> - parse_api_response(measurement_post_response, 'creating battery measurement') + try { + httpPost(measurement_post_params) { measurement_post_response -> + parse_api_response(measurement_post_response, 'creating battery measurement') + } + } catch (Exception e) { + log.debug "call failed $e" } } } @@ -248,7 +268,7 @@ def getDeviceSerialFromEvent(event){ } def oauthInitUrl(){ - atomicState.oauthInitState = UUID.randomUUID().toString() + atomicState.oauthInitState = UUID.randomUUID().toString() def oauthParams = [ response_type: "code", client_id: appSettings.client_id, @@ -275,8 +295,12 @@ def swapToken(){ ] def jsonMap - httpPost(postParams) { resp -> - jsonMap = resp.data + try { + httpPost(postParams) { resp -> + jsonMap = resp.data + } + } catch (Exception e) { + log.debug "call failed $e" } atomicState.refreshToken = jsonMap.refresh_token @@ -287,33 +311,33 @@ def swapToken(){ -
-
PlantLink
-
connected to
-
SmartThings
-
+
+
PlantLink
+
connected to
+
SmartThings
+
-

Your PlantLink Account is now connected to SmartThings!

-

Click Done at the top right to finish setup.

-
+

Your PlantLink Account is now connected to SmartThings!

+

Click Done at the top right to finish setup.

+
"""