/** * Copyright 2017 Pedro Garcia * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * IKEA Trådfri RGB Bulb * * Color management is not trivial. IKEA bulbs are using CIE XY color scheme instead of Hue/Saturation. Also the * bulbs do not seem to be able to output "light cyan" color. Maybe it is my fault for not having being able to * identify the correct color scheme (currently assuming sRGB with gamma correction), but cannot either with the * IKEA remote pairing... * * This handler is written so that it reports any change in the bulb state (on/off, brightness, color) as an event * immediately to be processed by other apps. * * Author: Pedro Garcia & Eliot Stocker & Ivar Holand * Date: 2020-11-20 * Version: 2.0 + Generic ZigBee RGBW Light Copyright 2016, 2017, 2018 Hubitat Inc. All Rights Reserved 2018-06-04 maxwell **/ import hubitat.zigbee.zcl.DataType import hubitat.helper.ColorUtils metadata { definition (name: "Livarno Lux LED Strip", namespace: "iholand+MC", author: "Ivar Holand + mark") { capability "Actuator" capability "Color Control" capability "ColorMode" capability "Color Temperature" capability "Configuration" capability "Polling" capability "Refresh" capability "Switch" capability "Switch Level" capability "Health Check" capability "Light" // Trådfri RGB bulb fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 CWS opal 600lm", deviceJoinName: "TRADFRI bulb E27 CWS opal 600lm" } preferences { input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true input name: "traceEnable", type: "bool", title: "Enable trace logging", defaultValue: true input name: "transitionTime", type: "enum", description: "", title: "Transition time", options: [[500:"500ms"],[1000:"1s"],[1500:"1.5s"],[5000:"5s"],[10000:"10s"]], defaultValue: 1000 } } private getMAX_WHITE_SATURATION() { 70 } private getWHITE_HUE() { 8 } private getMIN_COLOR_TEMP() { 2700 } private getMAX_COLOR_TEMP() { 6500 } def logDebug(msg) { if(logEnable) log.debug msg } def logTrace(msg) { if(traceEnable) log.trace msg } def logsOff() { log.warn "debug logging disabled..." device.updateSetting("logEnable", [value: "false", type: "bool"]) device.updateSetting("traceEnable", [value: "false", type: "bool"]) } def parseHex4le(hex) { Integer.parseInt(hex.substring(2, 4) + hex.substring(0, 2), 16) } def parseColorAttribute(id, value) { def parsed = false if(id == 0x03) { // currentColorX value = parseHex4le(value) logTrace "Parsed ColorX: $value" value /= 65536 parsed = true state.colorXReported = true; state.colorChanged |= value != colorX state.colorX = value } else if(id == 0x04) { // currentColorY value = parseHex4le(value) logTrace "Parsed ColorY: $value" value /= 65536 parsed = true state.colorYReported = true; state.colorChanged |= value != colorY state.colorY = value } else { logDebug "Not parsing Color cluster attribute $id: $value" } parsed } def parseAttributeList(cluster, list) { logTrace "Cluster: $cluster, AttrList: $list" def parsed = true while(list.length()) { def attrId = parseHex4le(list.substring(0, 4)) def attrType = Integer.parseInt(list.substring(4, 6), 16) def attrShift = 0 if(!attrType) { attrType = Integer.parseInt(list.substring(6, 8), 16) attrShift = 1 } def attrLen = DataType.getLength(attrType) def attrValue = list.substring(6 + 2*attrShift, 6 + 2*(attrShift+attrLen)) logTrace "Attr - Id: $attrId($attrLen), Type: $attrType, Value: $attrValue" if(cluster == 300) { parsed &= parseColorAttribute(attrId, attrValue) } else { //log.info "Not parsing cluster $cluster attribute: $list" parsed = false; } list = list.substring(6 + 2*(attrShift+attrLen)) } parsed } def parse(String description) { logDebug "Parsing : $description" def events = [] def event = zigbee.getEvent(description) def parsed if(event) { parsed = true events += event } else { def cluster = zigbee.parse(description) if(cluster) { logTrace "Cluster - $cluster" if (cluster.clusterId == 0x0006 && cluster.command == 0x07) { if (cluster.data[0] == 0x00) { events += createEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) parsed = true } else { log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" parsed = true } } else if(cluster.clusterId == 0x0300) { def map if (description instanceof String) { map = stringToMap(description) } logTrace "Map - $map" def raw = map["read attr - raw"] if(raw) { def clusterId = map.cluster logDebug "Parsing : clusterId = $clusterId and ${map}" log.debug "parse raw ${raw}" def attrList = !raw.substring(12) logDebug "Parsing : attrList = $attrList" if (attrList) parsed = parseAttributeList(clusterId, attrList) //if(state.colorChanged || (state.colorXReported && state.colorYReported)) { if(state.colorXReported && state.colorYReported) { state.colorChanged = false; state.colorXReported = false; state.colorYReported = false; logTrace "Color Change: xy ($state.colorX, $state.colorY)" def rgb = colorXy2Rgb(state.colorX, state.colorY) logTrace "Color Change: RGB ($rgb.red, $rgb.green, $rgb.blue)" events += updateColor(rgb) } } } else if(cluster.clusterId == 0x0008) { def descMap = zigbee.parseDescriptionAsMap(description) //[raw:catchall: 0104 0008 01 01 0040 00 70B3 00 00 0000 0B 01 0400, profileId:0104, clusterId:0008, //clusterInt:8, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:70B3, //isClusterSpecific:false, isManufacturerSpecific:false, manufacturerId:0000, command:0B, direction:01, data:[04, 00]] //log.debug "Not parsing cluster message $descMap , ${descMap.attrInt}" //ct dim level to add //0104 0008 01 01 0040 00 70B3 00 00 0000 0B 01 0400 } else { logDebug "Not parsing cluster message: $cluster" } } } if(!parsed) { // log.info "DID NOT PARSE MESSAGE for description : $description" } for(ev in events) { logDebug "Event - $ev.name: $ev.value" sendEvent(ev) } } def updateColor(rgb) { logTrace "updateColor: RGB ($rgb.red, $rgb.green, $rgb.blue)" def events = [] def hsv = colorRgb2Hsv(rgb.red, rgb.green, rgb.blue) hsv.hue = Math.round(hsv.hue * 100).intValue() hsv.saturation = Math.round(hsv.saturation * 100).intValue() state.hue = hsv.hue state.saturation = hsv.saturation hsv.level = Math.round(hsv.level * 100).intValue() logTrace "updateColor: RGB ($hsv.hue, $hsv.saturation, $hsv.level)" rgb.red = Math.round(rgb.red * 255).intValue() rgb.green = Math.round(rgb.green * 255).intValue() rgb.blue = Math.round(rgb.blue * 255).intValue() logTrace "updateColor: RGB ($rgb.red, $rgb.green, $rgb.blue)" def color = ColorUtils.rgbToHEX([rgb.red, rgb.green, rgb.blue]) logTrace "updateColor: $color" events += createEvent(name: "color", value: color, data: [ hue: hsv.hue, saturation: hsv.saturation, red: rgb.red, green: rgb.green, blue: rgb.blue, hex: color], displayed: false) events += createEvent(name: "hue", value: hsv.hue, displayed: false) events += createEvent(name: "saturation", value: hsv.saturation, displayed: false) if(hsv.hue == WHITE_HUE) { def percent = (1 - ((hsv.saturation / 100) * (100 / MAX_WHITE_SATURATION))) def amount = (MAX_COLOR_TEMP - MIN_COLOR_TEMP) * percent def val = Math.round(MIN_COLOR_TEMP + amount) events += createEvent(name: "colorTemperature", value: val) //events += createEvent(name: "colorMode", value: "CT") events += setGenericTempName(val) } else { // events += createEvent(name: "colorMode", value: "RGB") events += setGenericName(hsv.hue) } events } def off() { zigbee.off() } def on() { zigbee.on() } def sendZigbeeCommands() { List cmds = state.cmds if (cmds != null) { state.cmds = [] return cmds } } def setLevel(value) { setLevel(value, null) // (transitionTime?.toBigDecimal() ?: 5000) / 1000) } def setLevel(value, rate) { logDebug "Set level $value, $rate" if (rate == null) { state.cmds += zigbee.setLevel(value) } else { state.cmds += zigbee.setLevel(value, rate) } unschedule(sendZigbeeCommands) runInMillis(100, sendZigbeeCommands) } def setColorTemperature(rawValue, level=null ,transitionTime=null) { Integer rate = transitionTime?.toInteger() ?: 1000 String value = intTo16bitUnsignedHex((1000000/rawValue).toInteger()) //log.debug "Temp=$value, Level=$level ,rate= $rate" List cmd = [] def isOn = device.currentValue("switch") == "on" if (isOn){ cmd = [ //"he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x000A {${value} ${intTo16bitUnsignedHex(rate / 100)}}", "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x000A {${value}}", "delay ${rate + 500}", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0007 {}", "delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0008 {}" ] } else if (colorStaging){ cmd = [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x000A {${value} 0x0100}", "delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0007 {}", "delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0008 {}" ] } else { cmd = [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x000A {${value} 0x0100}", "delay 200", "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0006 1 {}","delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0006 0 {}","delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0007 {}", "delay 200", "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0300 0x0008 {}" ] } state.lastCT = value sendEvent(name: "colorMode", value: "CT") return cmd } def intTo16bitUnsignedHex(value) { String hexStr = zigbee.convertToHexString(value.toInteger(),4) //log.debug "intTo16bitUnsignedHex $hexStr" return new String(hexStr.substring(2, 4) + hexStr.substring(0, 2)) } def setColor(value) { logDebug "setColor($value)" if (value.hue == null ) { value.hue = state.hue } if (value.saturation == null){ value.saturation = state.saturation} def rgb = colorHsv2Rgb(value.hue / 100, value.saturation / 100) logTrace "setColor: RGB ($rgb.red, $rgb.green, $rgb.blue)" def xy = colorRgb2Xy(rgb.red, rgb.green, rgb.blue); logTrace "setColor: xy ($xy.x, $xy.y)" // def intX = Math.round(xy.x*65536).intValue() // 0..65279 def intX = Math.round(xy.x*75000).intValue() //def intY = Math.round(xy.y*65536).intValue() // 0..65279 def intY = Math.round(xy.y*65536).intValue() // 0..65279 logTrace "setColor: xy ($intX, $intY)" def strX = DataType.pack(intX, DataType.UINT16, true); def strY = DataType.pack(intY, DataType.UINT16, true); List cmds = [] def level = value.level def rate = value.rate if (level != null && rate != null) { cmds += zigbee.setLevel(level, rate) } else if (level != null) { cmds += zigbee.setLevel(level) } cmds += zigbee.command(0x0300, 0x07, strX, strY, "0a00") state.cmds += cmds sendEvent(name: "colorMode", value: "RGB") sendEvent(name: "colorTemperature", value: null) unschedule(sendZigbeeCommands) runInMillis(500, sendZigbeeCommands) } def setHue(hue) { logDebug "setHue: $hue" setColor([ hue: hue, saturation: device.currentValue("saturation") ]) } def setSaturation(saturation) { logDebug "setSaturation: $saturation" setColor([ hue: device.currentValue("hue"), saturation: saturation ]) } def ping() { return zigbee.onOffRefresh() } def colorControlRefresh() { def commands = [] commands += zigbee.readAttribute(0x0300, 0x03) // currentColorX commands += zigbee.readAttribute(0x0300, 0x04) // currentColorY commands } def colorControlConfig(min, max, step) { def commands = [] commands += zigbee.configureReporting(0x0300, 0x03, DataType.UINT16, min, max, step) // currentColorX commands += zigbee.configureReporting(0x0300, 0x04, DataType.UINT16, min, max, step) // currentColorY commands } def refresh() { state.colorChanged = false state.colorXReported = false state.colorYReported = false state.cmds = [] zigbee.onOffRefresh() + zigbee.levelRefresh() + colorControlRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + colorControlConfig(0, 300, 1) } def poll() { refresh() } def configure() { sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) refresh() } def updated() { log.debug "Device updated" state.cmds = [] if(logEnable) runIn(30*60, logsOff) } def installed() { if((device.currentState("saturation")?.value == null)) { sendEvent(name: "saturation", value: 0); } if((device.currentState("hue")?.value == null)) { sendEvent(name: "hue", value: 0); } if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { sendEvent(name: "level", value: 100) } } def setGenericTempName(temp){ if (!temp) return String genericName int value = temp.toInteger() if (value <= 2000) genericName = "Sodium" else if (value <= 2100) genericName = "Starlight" else if (value < 2400) genericName = "Sunrise" else if (value < 2800) genericName = "Incandescent" else if (value < 3300) genericName = "Soft White" else if (value < 3500) genericName = "Warm White" else if (value < 4150) genericName = "Moonlight" else if (value <= 5000) genericName = "Horizon" else if (value < 5500) genericName = "Daylight" else if (value < 6000) genericName = "Electronic" else if (value <= 6500) genericName = "Skylight" else if (value < 20000) genericName = "Polar" String descriptionText = "${device.getDisplayName()} color is ${genericName}" return createEvent(name: "colorName", value: genericName ,descriptionText: descriptionText) } def setGenericName(hue){ String colorName hue = hue.toInteger() hue = (hue * 3.6) switch (hue.toInteger()){ case 0..15: colorName = "Red" break case 16..45: colorName = "Orange" break case 46..75: colorName = "Yellow" break case 76..105: colorName = "Chartreuse" break case 106..135: colorName = "Green" break case 136..165: colorName = "Spring" break case 166..195: colorName = "Cyan" break case 196..225: colorName = "Azure" break case 226..255: colorName = "Blue" break case 256..285: colorName = "Violet" break case 286..315: colorName = "Magenta" break case 316..345: colorName = "Rose" break case 346..360: colorName = "Red" break } String descriptionText = "${device.getDisplayName()} color is ${colorName}" return createEvent(name: "colorName", value: colorName ,descriptionText: descriptionText) } // Color Management functions def min(first, ... rest) { def min = first; for(next in rest) { if(next < min) min = next } min } def max(first, ... rest) { def max = first; for(next in rest) { if(next > max) max = next } max } def colorGammaAdjust(component) { return (component > 0.04045) ? Math.pow((component + 0.055) / (1.0 + 0.055), 2.4) : (component / 12.92) } def colorGammaRevert(component) { return (component <= 0.0031308) ? 12.92 * component : (1.0 + 0.055) * Math.pow(component, (1.0 / 2.4)) - 0.055; } def colorXy2Rgb(x, y) { logTrace "< Color xy: ($x, $y)" def Y = 1 //; def X = (Y / y) * x//; def Z = (Y / y) * (1.0 - x - y)//; logTrace "< Color XYZ: ($X, $Y, $Z)" // sRGB, Reference White D65 def M = [ [ 3.2410032, -1.5373990, -0.4986159 ], [ -0.9692243, 1.8759300, 0.0415542 ], [ 0.0556394, -0.2040112, 1.0571490 ] ] def r = X * M[0][0] + Y * M[0][1] + Z * M[0][2] def g = X * M[1][0] + Y * M[1][1] + Z * M[1][2] def b = X * M[2][0] + Y * M[2][1] + Z * M[2][2] def max = max(r, g, b) r = colorGammaRevert(r / max) g = colorGammaRevert(g / max) b = colorGammaRevert(b / max) logTrace "< Color RGB: ($r, $g, $b)" [red: r, green: g, blue: b] } def colorRgb2Xy(r, g, b) { logTrace "> Color RGB: ($r, $g, $b)" r = colorGammaAdjust(r) g = colorGammaAdjust(g) b = colorGammaAdjust(b) // sRGB, Reference White D65 // D65 0.31271 0.32902 // R 0.64000 0.33000 // G 0.30000 0.60000 // B 0.15000 0.06000 def M = [ [ 0.4123866, 0.3575915, 0.1804505 ], [ 0.2126368, 0.7151830, 0.0721802 ], [ 0.0193306, 0.1191972, 0.9503726 ] ] def X = r * M[0][0] + g * M[0][1] + b * M[0][2] def Y = r * M[1][0] + g * M[1][1] + b * M[1][2] def Z = r * M[2][0] + g * M[2][1] + b * M[2][2] logTrace "> Color XYZ: ($X, $Y, $Z)" def x = X / (X + Y + Z) def y = Y / (X + Y + Z) logTrace "> Color xy: ($x, $y)" [x: x, y: y] } def colorHsv2Rgb(h, s) { logTrace "< Color HSV: ($h, $s, 1)" def r def g def b if (s == 0) { r = 1 g = 1 b = 1 } else { def region = (6 * h).intValue() def remainder = 6 * h - region def p = 1 - s def q = 1 - s * remainder def t = 1 - s * (1 - remainder) if(region == 0) { r = 1 g = t b = p } else if(region == 1) { r = q g = 1 b = p } else if(region == 2) { r = p g = 1 b = t } else if(region == 3) { r = p g = q b = 1 } else if(region == 4) { r = t g = p b = 1 } else { r = 1 g = p b = q } } logTrace "< Color RGB: ($r, $g, $b)" [red: r, green: g, blue: b] } def colorRgb2Hsv(r, g, b) { logTrace "> Color RGB: ($r, $g, $b)" def min = min(r, g, b) def max = max(r, g, b) def delta = max - min def h def s def v = max if (delta == 0) { h = 0 s = 0 } else { s = delta / max if (r == max) h = ( g - b ) / delta // between yellow & magenta else if(g == max) h = 2 + ( b - r ) / delta // between cyan & yellow else h = 4 + ( r - g ) / delta // between magenta & cyan h /= 6 if(h < 0) h += 1 } logTrace "> Color HSV: ($h, $s, $v)" return [ hue: h, saturation: s, level: v ] }