diff --git a/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy b/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy index a17d591..cf41886 100644 --- a/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy +++ b/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy @@ -28,17 +28,6 @@ metadata { } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { @@ -54,103 +43,54 @@ metadata { 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" + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - main "switch" - details(["switch","refresh"]) + details(["switch", "refresh"]) } } // Parse incoming device messages to generate events def parse(String description) { - log.debug "Parse description $description" - def name = null - def value = null - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - } else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.debug "Read attr: $description" - if (descMap.cluster == "0006" && descMap.attrId == "0000") { - name = "switch" - value = descMap.value.endsWith("01") ? "on" : "off" - } else { - def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim() - name = "power" - // assume 16 bit signed for encoding and power divisor is 10 - value = Integer.parseInt(reportValue, 16) / 10 + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + log.info event + if (event.name == "power") { + if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power + event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(event) + } + } + else { + sendEvent(event) } - } else if (description?.startsWith("on/off:")) { - log.debug "Switch command" - name = "switch" - value = description?.endsWith(" 1") ? "on" : "off" } - - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) } } -// Commands to device -def on() { - [ - 'zcl on-off on', - 'delay 200', - "send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}", - 'delay 500' - ] -} - def off() { - [ - 'zcl on-off off', - 'delay 200', - "send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}", - 'delay 500' - ] + zigbee.off() +} + +def on() { + zigbee.on() } def setLevel(value) { - log.trace "setLevel($value)" - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - def cmd = "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 2000}" - log.debug cmd - cmd -} - -def meter() { - "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" + zigbee.setLevel(value) } def refresh() { - "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() } def configure() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}" - ] -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + log.debug "Configuring Reporting and Bindings." + refresh() } diff --git a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy index d71b5a8..85dd41d 100644 --- a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy +++ b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy @@ -28,17 +28,6 @@ metadata { } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { @@ -50,18 +39,15 @@ metadata { tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel" } + tileAttribute ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "level", label: 'Level ${currentValue}%' - } - valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { - state "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" + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" - details(["switch", "level", "power","levelSliderControl","refresh"]) + details(["switch", "refresh"]) } } @@ -69,283 +55,42 @@ metadata { def parse(String description) { log.debug "description is $description" - def finalResult = isKnownDescription(description) - if (finalResult != "false") { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - sendEvent(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 - */ + def event = zigbee.getEvent(description) + if (event) { + log.info event + if (event.name == "power") { + if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power + event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(event) + } } else { - sendEvent(name: finalResult.type, value: finalResult.value) + sendEvent(event) } } else { log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) + log.debug zigbee.parseDescriptionAsMap(description) } } -// Commands to device -def zigbeeCommand(cluster, attribute){ - ["st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"] -} - def off() { - zigbeeCommand("6", "0") + zigbee.off() } def on() { - zigbeeCommand("6", "1") + zigbee.on() } def setLevel(value) { - value = value as Integer - if (value == 0) { - off() - } - else { - sendEvent(name: "level", value: value) - setLevelWithRate(value, "0000") + ["delay 1000"] + on() //value is between 0 to 100; GE does NOT switch on if OFF - } + zigbee.setLevel(value) } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" - ] - + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() } def configure() { - onOffConfig() + levelConfig() + powerConfig() + refresh() -} - - -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 == "0702" || descMap.clusterId == "0702"){ - isDescriptionPower(descMap) - } - else { - return "false" - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return "false" - } -} - -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 "false" - } - -} - -//@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 "false" - } -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0702") { - if (descMap.attrId == "0400") { - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0702") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0702) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return "false" - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//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 200", - "zcl global send-me-a-report 8 0 0x20 1 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//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() { - [ - //Meter (Power) Reporting - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -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}"] -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + log.debug "Configuring Reporting and Bindings." + refresh() } diff --git a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy index d04393f..082021f 100644 --- a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy +++ b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy @@ -28,17 +28,6 @@ metadata { } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { @@ -47,238 +36,51 @@ metadata { attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" } + tileAttribute ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("power", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "power", label:'${currentValue} Watts' - } main "switch" - details(["switch", "power", "refresh"]) + details(["switch", "refresh"]) } } // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def finalResult = isKnownDescription(description) - if (finalResult != "false") { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "power") { + def powerValue + powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration sendEvent(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 { - sendEvent(name: finalResult.type, value: finalResult.value) + sendEvent(event) } } else { log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) + log.debug zigbee.parseDescriptionAsMap(description) } } -// Commands to device -def zigbeeCommand(cluster, attribute){ - "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" -} - def off() { - zigbeeCommand("6", "0") + zigbee.off() } def on() { - zigbeeCommand("6", "1") + zigbee.on() } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" - ] - + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() } def configure() { - onOffConfig() + powerConfig() + refresh() -} - - -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 == "0702" || descMap.clusterId == "0702"){ - isDescriptionPower(descMap) - } - else { - return "false" - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return "false" - } -} - -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 "false" - } - -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0702") { - if (descMap.attrId == "0400") { - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0702") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0702) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return "false" - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//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() { - [ - //Meter (Power) Reporting - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + log.debug "Configuring Reporting and Bindings." + refresh() } diff --git a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy index a5c98ac..618587a 100644 --- a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy +++ b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy @@ -23,359 +23,123 @@ metadata { command "setAdjustedColor" } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { - state "colorName", label: '${currentValue}' - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "rgbSelector"]) + details(["switch", "refresh"]) } } +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + // Parse incoming device messages to generate events def parse(String description) { - //log.info "description is $description" - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - if(!(description?.startsWith("catchall: 0104 0300"))){ - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - } - } - else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" + log.debug "description is $description" - if (descMap.cluster == "0300") { - if(descMap.attrId == "0000"){ //Hue Attribute - def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Hue value returned is $hueValue" - sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(descMap.attrId == "0001"){ //Saturation Attribute - def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Saturation from refresh is $saturationValue" - sendEvent(name: "saturation", value: saturationValue, displayed:false) - } - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) + def event = zigbee.getEvent(description) + if (event) { + log.debug event + if (event.name=="level" && event.value==0) {} + else { + sendEvent(event) } } else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + def cluster = zigbee.parse(description) + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed") + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false) + } + } + else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00){ + log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbeeMap + } } - - } def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) -} - -def zigbeeOff() { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + zigbee.on() } def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - zigbeeOff() + zigbee.off() +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1" - ] - + 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) } def configure() { - state.levelValue = 100 log.debug "Configuring Reporting and Bindings." - def configCmds = [ + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def zigbeeSetLevel(level) { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + // 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) } def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << zigbeeOff() - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 255 / 100) - cmds << zigbeeSetLevel(level) - - //log.debug cmds - cmds -} - -//input Hue Integer values; returns color name for saturation 100% -private getColorName(hueValue){ - if(hueValue>100 || hueValue<0) - return - - hueValue = Math.round(hueValue / 100 * 360) - - log.debug "hue value is $hueValue" - - def colorName = "Color Mode" - if(hueValue>=0 && hueValue <= 4){ - colorName = "Red" - } - else if (hueValue>=5 && hueValue <=21 ){ - colorName = "Brick Red" - } - else if (hueValue>=22 && hueValue <=30 ){ - colorName = "Safety Orange" - } - else if (hueValue>=31 && hueValue <=40 ){ - colorName = "Dark Orange" - } - else if (hueValue>=41 && hueValue <=49 ){ - colorName = "Amber" - } - else if (hueValue>=50 && hueValue <=56 ){ - colorName = "Gold" - } - else if (hueValue>=57 && hueValue <=65 ){ - colorName = "Yellow" - } - else if (hueValue>=66 && hueValue <=83 ){ - colorName = "Electric Lime" - } - else if (hueValue>=84 && hueValue <=93 ){ - colorName = "Lawn Green" - } - else if (hueValue>=94 && hueValue <=112 ){ - colorName = "Bright Green" - } - else if (hueValue>=113 && hueValue <=135 ){ - colorName = "Lime" - } - else if (hueValue>=136 && hueValue <=166 ){ - colorName = "Spring Green" - } - else if (hueValue>=167 && hueValue <=171 ){ - colorName = "Turquoise" - } - else if (hueValue>=172 && hueValue <=187 ){ - colorName = "Aqua" - } - else if (hueValue>=188 && hueValue <=203 ){ - colorName = "Sky Blue" - } - else if (hueValue>=204 && hueValue <=217 ){ - colorName = "Dodger Blue" - } - else if (hueValue>=218 && hueValue <=223 ){ - colorName = "Navy Blue" - } - else if (hueValue>=224 && hueValue <=251 ){ - colorName = "Blue" - } - else if (hueValue>=252 && hueValue <=256 ){ - colorName = "Han Purple" - } - else if (hueValue>=257 && hueValue <=274 ){ - colorName = "Electric Indigo" - } - else if (hueValue>=275 && hueValue <=289 ){ - colorName = "Electric Purple" - } - else if (hueValue>=290 && hueValue <=300 ){ - colorName = "Orchid Purple" - } - else if (hueValue>=301 && hueValue <=315 ){ - colorName = "Magenta" - } - else if (hueValue>=316 && hueValue <=326 ){ - colorName = "Hot Pink" - } - else if (hueValue>=327 && hueValue <=335 ){ - colorName = "Deep Pink" - } - else if (hueValue>=336 && hueValue <=339 ){ - colorName = "Raspberry" - } - else if (hueValue>=340 && hueValue <=352 ){ - colorName = "Crimson" - } - else if (hueValue>=353 && hueValue <=360 ){ - colorName = "Red" - } - - colorName -} - -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 evenHex(value){ - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() % 2 != 0) { - 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 setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) + zigbee.setLevel(value) } def setColor(value){ log.trace "setColor($value)" - def max = 0xfe - - if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} - - def colorName = getColorName(value.hue) - sendEvent(name: "colorName", value: colorName) - - log.debug "color name is : $colorName" - sendEvent(name: "hue", value: value.hue, displayed:false) - sendEvent(name: "saturation", value: value.saturation, displayed:false) - def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) - def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" - - if (value.level) { - state.levelValue = value.level - sendEvent(name: "level", value: value.level) - def level = hex(value.level * 255 / 100) - cmd << zigbeeSetLevel(level) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - - cmd + zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation) +} + +def setHue(value) { + def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) +} + +def setSaturation(value) { + def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } diff --git a/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy b/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy index ef1ff1c..d12a968 100644 --- a/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy +++ b/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy @@ -29,431 +29,155 @@ metadata { } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } + } + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorName", label: '${currentValue}' + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemperature", label: '${currentValue} K' - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { - state "colorName", label: '${currentValue}' - } - - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp", "rgbSelector"]) - } + main(["switch"]) + details(["switch", "colorTempSliderControl", "colorName", "refresh"]) + } } +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } +private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 } + // Parse incoming device messages to generate events def parse(String description) { - //log.info "description is $description" - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - if(!(description?.startsWith("catchall: 0104 0300"))){ - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - } + log.debug "description is $description" - } - else if (description?.startsWith("read attr -")) { //for values returned after hitting refresh - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" - - if (descMap.cluster == "0300") { - if(descMap.attrId == "0007"){ - log.debug "in read attr" - log.debug descMap.value - def tempInMired = convertHexToInt(descMap.value) - def tempInKelvin = Math.round(1000000/tempInMired) - log.trace "temp in kelvin: $tempInKelvin" - sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) + def event = zigbee.getEvent(description) + if (event) { + log.debug event + if (event.name=="level" && event.value==0) {} + else { + if (event.name=="colorTemperature") { + setGenericName(event.value) } - else if(descMap.attrId == "0008"){ //Color mode attribute - if(descMap.value == "00"){ - state.colorType = "rgb" - }else if(descMap.value == "02"){ - state.colorType = "white" - } - } - else if(descMap.attrId == "0000"){ //Hue Attribute - def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Hue value returned is $hueValue" - sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(descMap.attrId == "0001"){ //Saturation Attribute - def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Saturation from refresh is $saturationValue" - sendEvent(name: "saturation", value: saturationValue, displayed:false) - } - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) + sendEvent(event) } } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "description is $description" - return result - } - + else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + def cluster = zigbee.parse(description) + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed") + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false) + } + } + else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00){ + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbeeMap + } + } } def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) -} - -def zigbeeOff() { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + zigbee.on() } def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - zigbeeOff() + zigbee.off() +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8" - ] - + 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) } def configure() { - state.levelValue = 100 - state.colorType = "white" - log.debug "Configuring Reporting and Bindings." - def configCmds = [ + log.debug "Configuring Reporting and Bindings." + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + refresh() } def setColorTemperature(value) { - state?.colorType = "white" - if(value<101){ - value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 - } - - def tempInMired = Math.round(1000000/value) - def finalHex = swapEndianHex(hex(tempInMired, 4)) - def genericName = getGenericName(value) - log.debug "generic name is : $genericName" - - def cmds = [] - sendEvent(name: "colorTemperature", value: value, displayed:false) - sendEvent(name: "colorName", value: genericName) - - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" - - cmds -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def zigbeeSetLevel(level) { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" -} - -def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << zigbeeOff() - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 255 / 100) - cmds << zigbeeSetLevel(level) - - //log.debug cmds - cmds + setGenericName(value) + zigbee.setColorTemperature(value) } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature -private getGenericName(value){ - def genericName = "White" - if(state?.colorType == "rgb"){ - genericName = "Color Mode" - } - else{ - if(value < 3300){ +def setGenericName(value){ + if (value != null) { + def genericName = "White" + if (value < 3300) { genericName = "Soft White" - } else if(value < 4150){ + } else if (value < 4150) { genericName = "Moonlight" - } else if(value < 5000){ + } else if (value <= 5000) { genericName = "Cool White" - } else if(value <= 6500){ + } else if (value >= 5000) { genericName = "Daylight" } + sendEvent(name: "colorName", value: genericName) } - - genericName } -//input Hue Integer values; returns color name for saturation 100% -private getColorName(hueValue){ - if(hueValue>100 || hueValue<0) - return - - hueValue = Math.round(hueValue / 100 * 360) - - log.debug "hue value is $hueValue" - - def colorName = "Color Mode" - if(hueValue>=0 && hueValue <= 4){ - colorName = "Red" - } - else if (hueValue>=5 && hueValue <=21 ){ - colorName = "Brick Red" - } - else if (hueValue>=22 && hueValue <=30 ){ - colorName = "Safety Orange" - } - else if (hueValue>=31 && hueValue <=40 ){ - colorName = "Dark Orange" - } - else if (hueValue>=41 && hueValue <=49 ){ - colorName = "Amber" - } - else if (hueValue>=50 && hueValue <=56 ){ - colorName = "Gold" - } - else if (hueValue>=57 && hueValue <=65 ){ - colorName = "Yellow" - } - else if (hueValue>=66 && hueValue <=83 ){ - colorName = "Electric Lime" - } - else if (hueValue>=84 && hueValue <=93 ){ - colorName = "Lawn Green" - } - else if (hueValue>=94 && hueValue <=112 ){ - colorName = "Bright Green" - } - else if (hueValue>=113 && hueValue <=135 ){ - colorName = "Lime" - } - else if (hueValue>=136 && hueValue <=166 ){ - colorName = "Spring Green" - } - else if (hueValue>=167 && hueValue <=171 ){ - colorName = "Turquoise" - } - else if (hueValue>=172 && hueValue <=187 ){ - colorName = "Aqua" - } - else if (hueValue>=188 && hueValue <=203 ){ - colorName = "Sky Blue" - } - else if (hueValue>=204 && hueValue <=217 ){ - colorName = "Dodger Blue" - } - else if (hueValue>=218 && hueValue <=223 ){ - colorName = "Navy Blue" - } - else if (hueValue>=224 && hueValue <=251 ){ - colorName = "Blue" - } - else if (hueValue>=252 && hueValue <=256 ){ - colorName = "Han Purple" - } - else if (hueValue>=257 && hueValue <=274 ){ - colorName = "Electric Indigo" - } - else if (hueValue>=275 && hueValue <=289 ){ - colorName = "Electric Purple" - } - else if (hueValue>=290 && hueValue <=300 ){ - colorName = "Orchid Purple" - } - else if (hueValue>=301 && hueValue <=315 ){ - colorName = "Magenta" - } - else if (hueValue>=316 && hueValue <=326 ){ - colorName = "Hot Pink" - } - else if (hueValue>=327 && hueValue <=335 ){ - colorName = "Deep Pink" - } - else if (hueValue>=336 && hueValue <=339 ){ - colorName = "Raspberry" - } - else if (hueValue>=340 && hueValue <=352 ){ - colorName = "Crimson" - } - else if (hueValue>=353 && hueValue <=360 ){ - colorName = "Red" - } - - colorName -} - - -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 evenHex(value){ - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() % 2 != 0) { - 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 setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) +def setLevel(value) { + zigbee.setLevel(value) } def setColor(value){ - state?.colorType = "rgb" - log.trace "setColor($value)" - def max = 0xfe - - if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} - - def colorName = getColorName(value.hue) - log.debug "color name is : $colorName" - sendEvent(name: "colorName", value: colorName) - sendEvent(name: "colorTemperature", value: "--", displayed:false) - - - sendEvent(name: "hue", value: value.hue, displayed:false) - sendEvent(name: "saturation", value: value.saturation, displayed:false) - def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) - def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" - - if (value.level) { - state.levelValue = value.level - sendEvent(name: "level", value: value.level) - def level = hex(value.level * 255 / 100) - cmd << zigbeeSetLevel(level) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - - cmd + log.trace "setColor($value)" + zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation) } + +def setHue(value) { + def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) +} + +def setSaturation(value) { + def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} \ No newline at end of file diff --git a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy index 80fa3ba..7c18d15 100644 --- a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy +++ b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy @@ -21,232 +21,119 @@ metadata { attribute "colorName", "string" } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { state "colorTemperature", action:"color temperature.setColorTemperature" } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemperature", label: '${currentValue} K' - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "colorName", label: '${currentValue}' } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"]) + details(["switch", "colorTempSliderControl", "colorName", "refresh"]) } } // Parse incoming device messages to generate events def parse(String description) { - //log.trace description - - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - - } - else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" - - if (descMap.cluster == "0300") { - log.debug descMap.value - def tempInMired = convertHexToInt(descMap.value) - def tempInKelvin = Math.round(1000000/tempInMired) - log.trace "temp in kelvin: $tempInKelvin" - sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + if (event.name=="level" && event.value==0) {} + else { + if (event.name=="colorTemperature") { + setGenericName(event.value) + } + sendEvent(event) } } else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } -} + def cluster = zigbee.parse(description) -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) + if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00) { + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${cluster}" + } + } } def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + zigbee.off() } -def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7" - ] - -} - -def configure() { - state.levelValue = 100 - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return onOffConfig() + levelConfig() + configCmds + refresh() // send refresh cmds as part of config -} - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 300 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//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} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 8 0 0x20 5 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -def setColorTemperature(value) { - if(value<101){ - value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 - } - - def tempInMired = Math.round(1000000/value) - def finalHex = swapEndianHex(hex(tempInMired, 4)) - def genericName = getGenericName(value) - log.debug "generic name is : $genericName" - - def cmds = [] - sendEvent(name: "colorTemperature", value: value, displayed:false) - sendEvent(name: "colorName", value: genericName) - - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" - - cmds -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } +def on() { + zigbee.on() } def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] + zigbee.setLevel(value) +} - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 254 / 100) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() +} - //log.debug cmds - cmds +def configure() { + log.debug "Configuring Reporting and Bindings." + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + 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 + refresh() +} + +def setColorTemperature(value) { + setGenericName(value) + zigbee.setColorTemperature(value) } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature -private getGenericName(value){ - def genericName = "White" - if(value < 3300){ - genericName = "Soft White" - } else if(value < 4150){ - genericName = "Moonlight" - } else if(value < 5000){ - genericName = "Cool White" - } else if(value <= 6500){ - genericName = "Daylight" +def setGenericName(value){ + if (value != null) { + def genericName = "White" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else if (value >= 5000) { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName) } - - genericName -} - -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 } diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy index 6146673..b97edcb 100644 --- a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy @@ -30,14 +30,18 @@ metadata { } - simulator { - status "active": "zone report :: type: 19 value: 0031" - status "inactive": "zone report :: type: 19 value: 0030" - } - preferences { - input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + 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" + ]) + } + section { + input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } } tiles(scale: 2) { @@ -49,15 +53,15 @@ metadata { } 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"] - ] + 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) { @@ -85,55 +89,71 @@ def parse(String description) { else if (description?.startsWith('temperature: ')) { map = parseCustomMessage(description) } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } log.debug "Parse returned $map" def result = map ? createEvent(map) : null - if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() - log.debug "enroll response: ${cmds}" - result = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - return result + 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: - resultMap = getBatteryResult(cluster.data.last()) - break + 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: - // temp is last 2 data values. reverse to swap endian - String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() - def value = getTemperature(temp) - resultMap = getTemperatureResult(value) - break + case 0x0402: + if (cluster.command == 0x07) { + if (cluster.data[0] == 0x00) { + log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster + sendEvent(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: - log.debug 'motion' - resultMap.name = 'motion' - break - } - } + // 0x07 - configure reporting + if (cluster.command != 0x07) { + log.debug 'motion' + resultMap.name = 'motion' + } + break + } + } - return resultMap + return resultMap } private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - // 0x07 is bind message - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - cluster.command == 0x07 || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage + // 0x0B is default response indicating message got through + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage } private Map parseReportAttributeMessage(String description) { @@ -151,10 +171,10 @@ private Map parseReportAttributeMessage(String description) { 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) - } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + resultMap = getMotionResult(value) + } return resultMap } @@ -170,46 +190,66 @@ private Map parseCustomMessage(String description) { private Map parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) + + // Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive') } def getTemperature(value) { def celsius = Integer.parseInt(value, 16).shortValue() / 100 if(getTemperatureScale() == "C"){ - return celsius + return Math.round(celsius) } else { - return celsiusToFahrenheit(celsius) as Integer + return Math.round(celsiusToFahrenheit(celsius)) } } private Map getBatteryResult(rawValue) { - log.debug 'Battery' + log.debug "Battery rawValue = ${rawValue}" def linkText = getLinkText(device) - log.debug rawValue - def result = [ - name: 'battery', - value: '--' + name: 'battery', + value: '--', + translatable: true ] def volts = rawValue / 10 - def descriptionText - if (rawValue == 0 || rawValue == 255) {} + if (rawValue == 0 || rawValue == 255) {} else { if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts." } - else if (volts > 0){ - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - def roundedPct = Math.round(pct * 100) - if (roundedPct <= 0) - roundedPct = 1 - result.value = Math.min(100, roundedPct) - result.descriptionText = "${linkText} battery was ${result.value}%" + else { + if (device.getDataValue("manufacturer") == "SmartThings") { + volts = rawValue // For the batteryMap to work the key needs to be an int + def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70, + 22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0] + def minVolts = 15 + def maxVolts = 28 + + if (volts < minVolts) + volts = minVolts + else if (volts > maxVolts) + volts = maxVolts + def pct = batteryMap[volts] + if (pct != null) { + result.value = pct + def value = pct + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + } + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + } } } @@ -218,68 +258,75 @@ private Map getBatteryResult(rawValue) { 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}" + 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, - unit: temperatureScale + name: 'temperature', + value: value, + descriptionText: descriptionText, + translatable: true, + unit: temperatureScale ] } private Map getMotionResult(value) { log.debug 'motion' - String linkText = getLinkText(device) - String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" + String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped" return [ - name: 'motion', - value: value, - descriptionText: descriptionText + name: 'motion', + value: value, + descriptionText: descriptionText, + translatable: true ] } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level +} + def refresh() { log.debug "refresh called" def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" ] return refreshCmds + enrollResponse() } def configure() { - log.debug "Configuring Reporting, IAS CIE, and Bindings." + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - def configCmds = [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - ] - return refresh() + configCmds // send refresh cmds as part of config + // 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 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] + //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 500", + //Enroll Response + "raw 0x500 {01 23 00 00 00}", + "send 0x${device.deviceNetworkId} 1 1", "delay 200" + ] } private getEndpointId() { @@ -291,19 +338,19 @@ private hex(value) { } private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() + 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 + 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-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy index 9af4f2d..d05b612 100644 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -16,51 +16,68 @@ //DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH import physicalgraph.zigbee.clusters.iaszone.ZoneStatus - metadata { - definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { - capability "Battery" - capability "Configuration" - capability "Contact Sensor" - capability "Acceleration Sensor" - capability "Refresh" - capability "Temperature Measurement" +metadata { + definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Refresh" + capability "Temperature Measurement" capability "Health Check" capability "Sensor" command "enrollResponse" - } + attribute "status", "string" + } - simulator { - - } - - preferences { - input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false - } + 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" + ]) + } + section { + input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + section { + input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", description: "Tap to set", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) + } + } 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:"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("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + 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") } - 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"] - ] + 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") + } + 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"] + ] + ) } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" @@ -69,98 +86,121 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } - main (["contact", "acceleration", "temperature"]) - details(["contact", "acceleration", "temperature", "battery", "refresh"]) + + main(["status", "acceleration", "temperature"]) + details(["status", "acceleration", "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) - } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } - - log.debug "Parse returned $map" - def result = map ? createEvent(map) : null - - if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() - log.debug "enroll response: ${cmds}" - result = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - return result - } - - private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(cluster.data.last()) - break - - case 0xFC02: - break - - case 0x0402: - log.debug 'TEMP' - // temp is last 2 data values. reverse to swap endian - String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() - def value = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - } - } - - return resultMap - } - - private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - // 0x07 is bind message - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - cluster.command == 0x07 || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage } -private int getHumidity(value) { - return Math.round(Double.parseDouble(value)) +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 result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + else if (description?.startsWith('read attr -')) { + result = parseReportAttributeMessage(description).each { createEvent(it) } + } + return result } -private Map parseReportAttributeMessage(String description) { +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 + sendEvent(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 List 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 = [:] + List result = [] if (descMap.cluster == "0402" && descMap.attrId == "0000") { def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) + result << getTemperatureResult(value) } - else if (descMap.cluster == "FC02" && descMap.attrId == "0002") { - Integer.parseInt(descMap.value,8) + 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") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + result << getBatteryResult(Integer.parseInt(descMap.value, 16)) } - return resultMap + return result } private Map parseCustomMessage(String description) { @@ -174,141 +214,296 @@ private Map parseCustomMessage(String description) { private Map parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) + Map resultMap = [:] - return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') + 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) + } + } } def getTemperature(value) { def celsius = Integer.parseInt(value, 16).shortValue() / 100 if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } + return Math.round(celsius) + } else { + return Math.round(celsiusToFahrenheit(celsius)) } +} - private Map getBatteryResult(rawValue) { - log.debug 'Battery' - def linkText = getLinkText(device) +private Map getBatteryResult(rawValue) { + log.debug "Battery rawValue = ${rawValue}" - def result = [ - name: 'battery' - ] + def result = [ + name: 'battery', + value: '--', + translatable: true + ] - def volts = rawValue / 10 - def descriptionText - if (rawValue == 0 || rawValue == 255) {} - else if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + def volts = rawValue / 10 + + if (rawValue == 0 || rawValue == 255) {} + else { + if (volts > 3.5) { + result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts." } else { - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - def roundedPct = Math.round(pct * 100) - if (roundedPct <= 0) - roundedPct = 1 - result.value = Math.min(100, roundedPct) - result.descriptionText = "${linkText} battery was ${result.value}%" - } + if (device.getDataValue("manufacturer") == "SmartThings") { + volts = rawValue // For the batteryMap to work the key needs to be an int + def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70, + 22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0] + def minVolts = 15 + def maxVolts = 28 - return result + if (volts < minVolts) + volts = minVolts + else if (volts > maxVolts) + volts = maxVolts + def pct = batteryMap[volts] + if (pct != null) { + result.value = pct + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + } + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + } + } } - 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 [ + return result +} + +private Map getTemperatureResult(value) { + log.debug "Temperature" + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + 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) + sendEvent(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' } - 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 - ] - } - - private getAccelerationResult(numValue) { - def name = "acceleration" - def value = numValue.endsWith("1") ? "active" : "inactive" - //def linkText = getLinkText(device) - def descriptionText = "$linkText was $value" - def isStateChange = isStateChange(device, name, value) - [ + def isStateChange = isStateChange(device, name, value) + return [ name: name, value: value, - //unit: null, - //linkText: linkText, descriptionText: descriptionText, - //handlerName: value, - isStateChange: isStateChange - // displayed: displayed(description, isStateChange) - ] + isStateChange: isStateChange, + translatable: true + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level +} + +def refresh() { + log.debug "Refreshing Values " + + def refreshCmds = [] + + if (device.getDataValue("manufacturer") == "SmartThings") { + log.debug "Refreshing Values for manufacturer: SmartThings " + /* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276) + seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer. + Separating these out in a separate if-else because I do not want to touch Centralite part + as of now. + */ + refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode]) + refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode]) + } else { + refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode]) } - def refresh() { - log.debug "Refreshing Temperature and Battery " - def refreshCmds = [ + //Common refresh commands + refreshCmds += zigbee.readAttribute(0x0402, 0x0000) + + zigbee.readAttribute(0x0001, 0x0020) + + zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - //"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - - ] - - return refreshCmds + enrollResponse() - } + return refreshCmds + enrollResponse() +} def configure() { - sendEvent(name: "checkInterval", value: 7200, displayed: false) + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - log.debug "Configuring Reporting, IAS CIE, and Bindings." - def configCmds = [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + log.debug "Configuring Reporting" - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + // 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() + + 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]) - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500", - "zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - ] - return refresh() + configCmds // send refresh cmds as part of config + 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 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" + //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 500", + //Enroll Response + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 200" ] } -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() +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 + } 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 + } + + log.debug "parseAxis -- ${xyzResults}" + + if (garageSensor == "Yes") + garageEvent(xyzResults.z) + + getXyzResult(xyzResults, description) +} + +private hexToSignedInt(hexVal) { + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal +} + +def garageEvent(zValue) { + def absValue = zValue.abs() + def contactValue = null + 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" + } else { + return "0x104E" + } +} + +private hexToInt(value) { + new BigInteger(value, 16) } private hex(value) { diff --git a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy index 2b03635..5d13492 100644 --- a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy +++ b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy @@ -47,197 +47,122 @@ metadata { //fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019" } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: '${currentValue} K' + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { - state "saturation", action:"color control.setSaturation" - } - valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { - state "saturation", label: 'Sat ${currentValue} ' - } - controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { - state "hue", action:"color control.setHue" - } main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "refresh"]) + details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) } } +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } +private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 } + // Parse incoming device messages to generate events def parse(String description) { - //log.trace description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - //log.trace msg - //log.trace "data: $msg.data" + log.debug "description is $description" + + def finalResult = zigbee.getEvent(description) + if (finalResult) { + log.debug finalResult + sendEvent(finalResult) } else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + log.trace "zigbeeMap : $zigbeeMap" + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360) + sendEvent(name: "hue", value: hueValue, displayed:false) + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100) + sendEvent(name: "saturation", value: saturationValue, displayed:false) + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + } } } def on() { - // just assume it works for now - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } def off() { - // just assume it works for now - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() } -def setHue(value) { - def max = 0xfe - log.trace "setHue($value)" - sendEvent(name: "hue", value: value) - def scaledValue = Math.round(value * max / 100.0) - def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 0000}" - //log.info cmd - cmd +def refresh() { + refreshAttributes() + configureAttributes() } -def setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.hue = adjustOutgoingHue(value.hue) - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) +def poll() { + refreshAttributes() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + configureAttributes() + refreshAttributes() +} + +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) +} + +def refreshAttributes() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} + +def setColorTemperature(value) { + zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh() +} + +def setLevel(value) { + zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } def setColor(value){ log.trace "setColor($value)" - def max = 0xfe + zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes() +} - sendEvent(name: "hue", value: value.hue) - sendEvent(name: "saturation", value: value.saturation) - def scaledHueValue = Math.round(value.hue * max / 100.0) - def scaledSatValue = Math.round(value.saturation * max / 100.0) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledHueValue)} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledSatValue)} 0000}" - - if (value.level != null) { - cmd << "delay 150" - cmd.addAll(setLevel(value.level)) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - log.info cmd - cmd +def setHue(value) { + def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5) } def setSaturation(value) { - def max = 0xfe - log.trace "setSaturation($value)" - sendEvent(name: "saturation", value: value) - def scaledValue = Math.round(value * max / 100.0) - def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} 0000}" - //log.info cmd - cmd -} - -def refresh() { - "st rattr 0x${device.deviceNetworkId} 1 6 0" -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds -} - -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 adjustOutgoingHue(percent) { - def adjusted = percent - if (percent > 31) { - if (percent < 63.0) { - adjusted = percent + (7 * (percent -30 ) / 32) - } - else if (percent < 73.0) { - adjusted = 69 + (5 * (percent - 62) / 10) - } - else { - adjusted = percent + (2 * (100 - percent) / 28) - } - } - log.info "percent: $percent, adjusted: $adjusted" - adjusted + def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time }