├── Color Temp Virtual Dimmer.groovy ├── Duraflame_Heater_ST.groovy ├── Hue Dimmer Remote as Button Controller.groovy ├── Improved Osram Gardenspot RGB MA.groovy ├── Improved Zigbee Hue MA.groovy ├── Lutron-Connected-Bulb-Remote.groovy ├── My-GE-Link-v3.groovy ├── README.md ├── Smart-Bulb-Alert-Switch.groovy ├── Zigbee_Toggle_Switch.groovy └── devicetypes └── sticks18 ├── Readme.txt ├── dresden-fls-pp.src └── dresden-fls-pp.groovy ├── ge-link-pro.src └── ge-link-pro.groovy ├── lutron-conn-bulb.src └── lutron-conn-bulb.groovy └── petsafe-smart-door.src └── petsafe-smart-door.groovy /Color Temp Virtual Dimmer.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | * Color Temp Virtual Dimmer - to be used with Color Temp via Virtual Dimmer SmartApp to make color temp adjustments in common SmartApps 14 | * 15 | * Author: Scott Gibson 16 | * 17 | * Date: 2015-03-12 18 | */ 19 | metadata { 20 | definition (name: "Color Temp Virtual Dimmer", namespace: "sticks18", author: "Scott Gibson") { 21 | capability "Actuator" 22 | capability "Switch Level" 23 | capability "Switch" 24 | 25 | command "updateTemp" 26 | 27 | attribute "kelvin", "number" 28 | 29 | } 30 | 31 | // simulator metadata 32 | simulator { 33 | } 34 | 35 | // UI tile definitions 36 | tiles { 37 | valueTile("kelvin", "device.kelvin") { 38 | state("device.kelvin", label:'${currentValue}k', 39 | backgroundColors:[ 40 | [value: 2900, color: "#FFA757"], 41 | [value: 3300, color: "#FFB371"], 42 | [value: 3700, color: "#FFC392"], 43 | [value: 4100, color: "#FFCEA6"], 44 | [value: 4500, color: "#FFD7B7"], 45 | [value: 4900, color: "#FFE0C7"], 46 | [value: 5300, color: "#FFE8D5"], 47 | [value: 6600, color: "#FFEFE1"] 48 | ] 49 | ) 50 | } 51 | valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { 52 | state "level", label: 'Level ${currentValue}%' 53 | } 54 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { 55 | state "level", action:"switch level.setLevel" 56 | } 57 | main "kelvin" 58 | details "levelSliderControl", "level", "kelvin" 59 | } 60 | } 61 | 62 | def parse(String description) { 63 | } 64 | 65 | def setLevel(value) { 66 | log.debug "Setting level to ${value}" 67 | sendEvent(name: "level", value: value) 68 | parent.setLevel(this, value) 69 | } 70 | 71 | def on() { 72 | } 73 | 74 | def off() { 75 | } 76 | 77 | def updateTemp(value) { 78 | sendEvent(name: "kelvin", value: value) 79 | } 80 | -------------------------------------------------------------------------------- /Duraflame_Heater_ST.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | * CentraLite Thermostat 14 | * 15 | * Author: SmartThings 16 | * Date: 2013-12-02 17 | */ 18 | metadata { 19 | definition (name: "Heater", namespace: "smartthings", author: "SmartThings") { 20 | capability "Actuator" 21 | capability "Temperature Measurement" 22 | capability "Thermostat" 23 | capability "Configuration" 24 | capability "Power Meter" 25 | capability "Refresh" 26 | capability "Sensor" 27 | capability "Switch" 28 | capability "Polling" 29 | 30 | command "ecoOn" 31 | command "ecoOff" 32 | 33 | attribute "ecoMode", "string" 34 | 35 | fingerprint profileId: "0104", inClusters: "0300,0000,0003,0006,0201,0204,0702,0B05", outClusters: "0003,0019,0020" 36 | 37 | } 38 | 39 | // simulator metadata 40 | simulator { 41 | // status messages 42 | status "on": "on/off: 1" 43 | status "off": "on/off: 0" 44 | // reply messages 45 | reply "zcl on-off on": "on/off: 1" 46 | reply "zcl on-off off": "on/off: 0" 47 | 48 | } 49 | 50 | tiles { 51 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 52 | state("temperature", label:'${currentValue}°', unit:"F", 53 | backgroundColors:[ 54 | [value: 31, color: "#153591"], 55 | [value: 44, color: "#1e9cbb"], 56 | [value: 59, color: "#90d2a7"], 57 | [value: 74, color: "#44b621"], 58 | [value: 84, color: "#f1d801"], 59 | [value: 95, color: "#d04e00"], 60 | [value: 96, color: "#bc2323"] 61 | ] 62 | ) 63 | } 64 | standardTile("switch", "device.switch", inactiveLabel: false, decoration: "flat") { 65 | state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff" 66 | state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821" 67 | state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" 68 | state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 69 | } 70 | controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 3, inactiveLabel: false, range: "(50..97)") { 71 | state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" 72 | } 73 | valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { 74 | state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" 75 | } 76 | standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") { 77 | state "default", action:"refresh.refresh", icon:"st.secondary.refresh" 78 | } 79 | standardTile("ecoMode", "device.ecoMode", inactiveLabel: false, decoration: "flat") { 80 | state "Eco off", label:'${currentValue}', action:"ecoOn", icon:"st.Outdoor.outdoor19", backgroundColor:"#ffffff" 81 | state "Eco on", label:'${currentValue}', action:"ecoOff", icon:"st.Outdoor.outdoor19", backgroundColor:"#79b821" 82 | state "turningOn", label:'${currentValue}', action:"ecoOff", icon:"st.Outdoor.outdoor19", backgroundColor:"#79b821", nextState:"turningOff" 83 | state "turningOff", label:'${currentValue}', action:"ecoOn", icon:"st.Outdoor.outdoor19", backgroundColor:"#ffffff", nextState:"turningOn" 84 | } 85 | main "temperature" 86 | details(["temperature", "switch", "heatSliderControl", "heatingSetpoint", "ecoMode", "refresh"]) 87 | 88 | } 89 | } 90 | 91 | // parse events into attributes 92 | 93 | def parse(String description) { 94 | log.debug "Parse description $description" 95 | def map = [:] 96 | if (description?.startsWith("read attr -")) { 97 | def descMap = parseDescriptionAsMap(description) 98 | log.debug "Desc Map: $descMap" 99 | if (descMap.cluster == "0201" && descMap.attrId == "0000") { 100 | log.debug "TEMP" 101 | map.name = "temperature" 102 | map.value = getTemperature(descMap.value) 103 | map.unit = temperatureScale 104 | } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { 105 | log.debug "HEATING SETPOINT" 106 | map.name = "heatingSetpoint" 107 | map.value = getTemperature(descMap.value) 108 | map.unit = temperatureScale 109 | } else if (descMap.cluster == "0201" && descMap.attrId == "0025") { 110 | log.debug "EcoMode" 111 | map.name = "ecoMode" 112 | map.value = (descMap.value == "04" ? "Eco on" : "Eco off") 113 | } else if (descMap.cluster == "0201" && descMap.attrId == "001c") { 114 | log.debug "MODE" 115 | map.name = "switch" 116 | map.value = (descMap.value == "00" ? "off" : "on") 117 | } else if (descMap.cluster == "0702") { 118 | log.debug "Power" 119 | map.name = "power" 120 | map.value = convertHexToInt(descMap.value) 121 | } 122 | } else if (description?.startsWith("catchall:")) { 123 | def msg = zigbee.parse(description) 124 | log.trace msg 125 | log.trace "data: $msg.data" 126 | } 127 | 128 | def result = null 129 | if (map) { 130 | result = createEvent(map) 131 | } 132 | log.debug "Parse returned $map" 133 | return result 134 | } 135 | 136 | def parseDescriptionAsMap(description) { 137 | (description - "read attr - ").split(",").inject([:]) { map, param -> 138 | def nameAndValue = param.split(":") 139 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 140 | } 141 | } 142 | 143 | def poll() { 144 | refresh() 145 | } 146 | 147 | def refresh() { 148 | log.debug "refresh called" 149 | [ 150 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 200", 151 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 200", 152 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 200", 153 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x25", "delay 200", 154 | zigbee.simpleMeteringPowerRefresh() 155 | ] 156 | } 157 | 158 | def ecoOn() { 159 | log.debug "ecoMode on" 160 | sendEvent(name: "ecoMode", value: "Eco on") 161 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {04}" 162 | } 163 | 164 | def ecoOff() { 165 | log.debug "ecoMode off" 166 | sendEvent(name: "ecoMode", value: "Eco off") 167 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {00}" 168 | } 169 | 170 | def getTemperature(value) { 171 | if (value != null) { 172 | def celsius = Integer.parseInt(value, 16) / 100 173 | if (getTemperatureScale() == "C") { 174 | return celsius 175 | } else { 176 | return Math.round(celsiusToFahrenheit(celsius)) 177 | } 178 | } 179 | } 180 | 181 | def setHeatingSetpoint(degrees) { 182 | if (degrees != null) { 183 | def temperatureScale = getTemperatureScale() 184 | def degreesInteger = Math.round(degrees) 185 | log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})" 186 | sendEvent("name": "heatingSetpoint", "value": degreesInteger, "unit": temperatureScale) 187 | 188 | def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) 189 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius * 100) + "}" 190 | 191 | } 192 | } 193 | 194 | def heat() { 195 | log.debug "heat" 196 | sendEvent("name":"thermostatMode", "value":"heat") 197 | sendEvent("name":"switch", "value":"on") 198 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}" 199 | } 200 | 201 | def on() { 202 | log.debug "on" 203 | sendEvent("name":"thermostatMode", "value":"auto") 204 | sendEvent("name":"switchStatus", "value":"on") 205 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {01}" 206 | } 207 | 208 | def off() { 209 | log.debug "off" 210 | sendEvent("name":"thermostatMode", "value":"off") 211 | sendEvent("name":"switch", "value":"off") 212 | sendEvent("name":"timer", "value":"off") 213 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}" 214 | } 215 | 216 | private hex(value) { 217 | new BigInteger(Math.round(value).toString()).toString(16) 218 | } 219 | 220 | private Integer convertHexToInt(hex) { 221 | Integer.parseInt(hex,16) 222 | } 223 | 224 | private String swapEndianHex(String hex) { 225 | reverseArray(hex.decodeHex()).encodeHex() 226 | } 227 | 228 | private byte[] reverseArray(byte[] array) { 229 | int i = 0; 230 | int j = array.length - 1; 231 | byte tmp; 232 | while (j > i) { 233 | tmp = array[j]; 234 | array[j] = array[i]; 235 | array[i] = tmp; 236 | j--; 237 | i++; 238 | } 239 | return array 240 | } 241 | 242 | def configure() { 243 | 244 | log.debug "binding to Thermostat cluster" 245 | [ 246 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", 247 | "zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C 248 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 249 | "zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode 250 | "send 0x${device.deviceNetworkId} 1 1","delay 500", 251 | "zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off 252 | "send 0x${device.deviceNetworkId} 1 1","delay 500", 253 | "zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian) 254 | "send 0x${device.deviceNetworkId} 1 1","delay 500", 255 | zigbee.simpleMeteringPowerConfig() 256 | ] 257 | } 258 | 259 | def updated() { 260 | configure() 261 | } 262 | -------------------------------------------------------------------------------- /Hue Dimmer Remote as Button Controller.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | */ 14 | /* Philips Hue Wireless Dimmer 15 | 16 | Capabilities: 17 | Actuator 18 | Configuration 19 | Button 20 | Sensor 21 | 22 | 23 | */ 24 | 25 | metadata { 26 | definition (name: "Hue Dimmer Button Controller", namespace: "Sticks18", author: "Scott G") { 27 | capability "Actuator" 28 | capability "Button" 29 | capability "Configuration" 30 | capability "Sensor" 31 | 32 | // fingerprint specific to Hue remote taken from Basic Cluster. 33 | fingerprint profileId: "C05E", inClusters: "0000", outClusters: "0000,0003,0004,0006,0008", manufacturer: "Philips", model: "RWL020" 34 | } 35 | 36 | // simulator metadata 37 | simulator { 38 | // status messages 39 | 40 | } 41 | 42 | // UI tile definitions 43 | tiles { 44 | standardTile("button", "device.button", width: 2, height: 2) { 45 | state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" 46 | } 47 | main "button" 48 | details(["button"]) 49 | } 50 | } 51 | 52 | // Parse incoming device messages to generate events 53 | def parse(String description) { 54 | // next line is debugging code to see raw zigbee message. no longer needed 55 | // log.trace description 56 | 57 | // Create a map from the raw zigbee message to make parsing more intuitive 58 | def msg = zigbee.parse(description) 59 | 60 | // Check if the message comes from the on/off cluster (0x0006 in zigbee) to determine if button was On or Off 61 | if (msg.clusterId == 6) { 62 | 63 | // Command 1 is the zigbee 'on' command, so make that button 1. Else it must be 'off' command, make that button 4. 64 | // Then create the button press event. All button events with be of type "pushed", not "held". 65 | def button = (msg.command == 1 ? 1 : 4) 66 | def result = createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) 67 | log.debug "Parse returned ${result?.descriptionText}" 68 | return result 69 | 70 | } 71 | 72 | // Check if message comes from the level cluster (0x0008 in zigbee) to determine if button was dim up/down 73 | if (msg.clusterId == 8) { 74 | 75 | // Remote sends move level and stop commands when dim up/down pushed or held 76 | // Additional parsing and debugging created for when buttons are held, but not creating events for them 77 | // since it is difficult to separate them from pushed events 78 | switch (msg.command) 79 | { 80 | // Move level command means dim/up down was pushed or held 81 | case 2: 82 | 83 | // Position -6 and -5 is the two digit hex of the move step size, which is different for an initial push vs a hold 84 | // Check it for the initial push value '1E' 85 | def y = description[-6..-5] 86 | if (y == "1E") { 87 | 88 | // Determine the button push by looking at the move direction determined in position -8 and -7 89 | // "00" means to move up in level, so dim up button was pushed. Create button event 90 | def button = (description[-8..-7] == "00" ? 2 : 3) 91 | def result = createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) 92 | log.debug "Parse returned ${result?.descriptionText}" 93 | return result 94 | break 95 | } 96 | // If move step size is not '1E' then the button is being held. Send debug message, but take no action 97 | log.debug "Received held message" 98 | break 99 | 100 | case 3: 101 | // If level command is 3, then stop moving command was sent. Send debug message, but take no action. 102 | // This stop command could be used to get a button 'held' event, but we would need to ignore the initial button push 103 | log.debug "Received stop message" 104 | break 105 | 106 | } 107 | } 108 | 109 | } 110 | 111 | 112 | // Configuration runs during device join 113 | def configure() { 114 | 115 | log.debug "configure" 116 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 117 | log.debug "Confuguring Reporting and Bindings." 118 | def configCmds = [ 119 | 120 | // Bind the outgoing on/off cluster from remote to hub, so remote sends hub messages when On/Off buttons pushed 121 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", 122 | 123 | // Bind the outgoing level cluster from remote to hub, so remote sends hub messages when Dim Up/Down buttons pushed 124 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", 125 | 126 | ] 127 | return configCmds 128 | 129 | 130 | } 131 | 132 | private getEndpointId() { 133 | new BigInteger(device.endpointId, 16).toString() 134 | } 135 | 136 | private String swapEndianHex(String hex) { 137 | reverseArray(hex.decodeHex()).encodeHex() 138 | } 139 | 140 | private byte[] reverseArray(byte[] array) { 141 | int i = 0; 142 | int j = array.length - 1; 143 | byte tmp; 144 | while (j > i) { 145 | tmp = array[j]; 146 | array[j] = array[i]; 147 | array[i] = tmp; 148 | j--; 149 | i++; 150 | } 151 | return array 152 | } 153 | -------------------------------------------------------------------------------- /Improved Osram Gardenspot RGB MA.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Osram Lightify Gardenspot Mini RGB 3 | 4 | Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling 5 | that issue by using state variables 6 | 7 | For Color Loop to work, you need to update the firmware. Also, only EU Osram bulbs will receive the color loop 8 | functionality at this time due to differences in EU vs US firmware. Osram support told me that the two will merge 9 | when the new version of Zigbee is available. 10 | */ 11 | 12 | metadata { 13 | definition (name: "Improved OSRAM LIGHTIFY Gardenspot mini RGB MA", namespace: "sticks18", author: "Scott G") { 14 | 15 | capability "Actuator" 16 | capability "Switch" 17 | capability "Switch Level" 18 | capability "Configuration" 19 | capability "Polling" 20 | capability "Refresh" 21 | capability "Sensor" 22 | capability "Color Control" 23 | 24 | attribute "colorName", "string" 25 | attribute "switchColor", "string" 26 | attribute "loopActive", "string" 27 | attribute "loopDirection", "string" 28 | attribute "loopTime", "number" 29 | 30 | command "setAdjustedColor" 31 | command "startLoop" 32 | command "stopLoop" 33 | command "setLoopTime" 34 | command "setDirection" 35 | 36 | 37 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" 38 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" 39 | 40 | } 41 | 42 | // simulator metadata 43 | simulator { 44 | // status messages 45 | status "on": "on/off: 1" 46 | status "off": "on/off: 0" 47 | 48 | // reply messages 49 | reply "zcl on-off on": "on/off: 1" 50 | reply "zcl on-off off": "on/off: 0" 51 | } 52 | 53 | // UI tile definitions 54 | tiles (scale: 2){ 55 | multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { 56 | tileAttribute("device.switchColor", key: "PRIMARY_CONTROL") { 57 | attributeState "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 58 | attributeState "Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0000" 59 | attributeState "Brick Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff3700" 60 | attributeState "Safety Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff6F00" 61 | attributeState "Dark Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff9900" 62 | attributeState "Amber", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffbf00" 63 | attributeState "Gold", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffe1000" 64 | attributeState "Yellow", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffff00" 65 | attributeState "Electric Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#bfff00" 66 | attributeState "Lawn Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#88ff00" 67 | attributeState "Bright Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#48ff00" 68 | attributeState "Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff11" 69 | attributeState "Spring Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff6a" 70 | attributeState "Turquoise", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffd0" 71 | attributeState "Aqua", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffff" 72 | attributeState "Sky Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00bfff" 73 | attributeState "Dodger Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#007bff" 74 | attributeState "Navy Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0050ff" 75 | attributeState "Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0000ff" 76 | attributeState "Han Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#3b00ff" 77 | attributeState "Electric Indigo", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#6600ff" 78 | attributeState "Electric Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#b200ff" 79 | attributeState "Orchid Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#e900ff" 80 | attributeState "Magenta", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00dc" 81 | attributeState "Hot Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00aa" 82 | attributeState "Deep Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff007b" 83 | attributeState "Raspberry", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0061" 84 | attributeState "Crimson", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff003b" 85 | attributeState "Color Loop", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 86 | } 87 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 88 | attributeState "color", action: "setAdjustedColor" 89 | } 90 | tileAttribute ("device.level", key: "SECONDARY_CONTROL") { 91 | attributeState "level", label: 'Level is ${currentValue}%' 92 | } 93 | } 94 | standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 95 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 96 | } 97 | standardTile("loop", "device.loopActive", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 98 | state "Active", label:'${currentValue}', action: "stopLoop" 99 | state "Inactive", label:'${currentValue}', action: "startLoop" 100 | } 101 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 6, inactiveLabel: false) { 102 | state "level", action:"switch level.setLevel" 103 | } 104 | valueTile("colorName", "device.colorName", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 105 | state "colorName", label: '${currentValue}' 106 | } 107 | controlTile("loopTimeControl", "device.loopTime", "slider", height: 2, width: 4, range: "(1..60)", inactiveLabel: false) { 108 | state "loopTime", action: "setLoopTime" 109 | } 110 | standardTile("loopDir", "device.loopDirection", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 111 | state "default", label: '${currentValue}', action: "setDirection" 112 | } 113 | 114 | main(["switch"]) 115 | details(["switch", "levelSliderControl", "colorName", "loop", "refresh", "loopTimeControl", "loopDir"]) 116 | } 117 | } 118 | 119 | // Parse incoming device messages to generate events 120 | def parse(String description) { 121 | //log.info "description is $description" 122 | if (device.currentValue("loopActive") == "Active") { 123 | 124 | } 125 | else if (description?.startsWith("catchall:")) { 126 | if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) 127 | { 128 | def result = createEvent(name: "switch", value: "on") 129 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 130 | log.debug "Parse returned ${result?.descriptionText}" 131 | return result 132 | } 133 | else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) 134 | { 135 | if(!(description?.startsWith("catchall: 0104 0300"))){ 136 | def result = createEvent(name: "switch", value: "off") 137 | sendEvent(name: "switchColor", value: "off", displayed: false) 138 | log.debug "Parse returned ${result?.descriptionText}" 139 | return result 140 | } 141 | } 142 | } 143 | else if (description?.startsWith("read attr -")) { 144 | def descMap = parseDescriptionAsMap(description) 145 | log.trace "descMap : $descMap" 146 | 147 | if (descMap.cluster == "0300") { 148 | if(descMap.attrId == "0000"){ //Hue Attribute 149 | def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 150 | log.debug "Hue value returned is $hueValue" 151 | def colorName = getColorName(hueValue) 152 | sendEvent(name: "colorName", value: colorName) 153 | if (device.currentValue("switch") == "on") { sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) } 154 | sendEvent(name: "hue", value: hueValue, displayed:false) 155 | } 156 | else if(descMap.attrId == "0001"){ //Saturation Attribute 157 | def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 158 | log.debug "Saturation from refresh is $saturationValue" 159 | sendEvent(name: "saturation", value: saturationValue, displayed:false) 160 | } 161 | } 162 | else if(descMap.cluster == "0008"){ 163 | def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) 164 | log.debug "dimmer value is $dimmerValue" 165 | sendEvent(name: "level", value: dimmerValue) 166 | } 167 | } 168 | else { 169 | def name = description?.startsWith("on/off: ") ? "switch" : null 170 | if (name == "switch") { 171 | def value = (description?.endsWith(" 1") ? "on" : "off") 172 | log.debug value 173 | sendEvent(name: "switchColor", value: (value == "off" ? "off" : device.currentValue("colorName")), displayed: false) 174 | } 175 | else { def value = null } 176 | def result = createEvent(name: name, value: value) 177 | log.debug "Parse returned ${result?.descriptionText}" 178 | return result 179 | } 180 | 181 | 182 | } 183 | 184 | def setDirection() { 185 | def direction = (device.currentValue("loopDirection") == "Down" ? "Up" : "Down") 186 | log.trace direction 187 | sendEvent(name: "loopDirection", value: direction) 188 | if (device.currentValue("loopActive") == "Active") { 189 | def dirHex = (direction == "Down" ? "00" : "01") 190 | log.trace dirHex 191 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {02 01 ${dirHex} 0000 0000}" 192 | } 193 | } 194 | 195 | def setLoopTime(value) { 196 | sendEvent(name:"loopTime", value: value) 197 | if (device.currentValue("loopActive") == "Active") { 198 | def finTime = swapEndianHex(hexF(value, 4)) 199 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {04 01 00 ${finTime} 0000}" 200 | } 201 | } 202 | 203 | def startLoop(Map params) { 204 | // direction either increments or decrements the hue value: "Up" will increment, "Down" will decrement 205 | def direction = (device.currentValue("loopDirection") != null ? (device.currentValue("loopDirection") == "Down" ? "00" : "01") : "00") 206 | log.trace direction 207 | if (params?.direction != null) { 208 | direction = (params.direction == "Down" ? "00" : "01") 209 | sendEvent(name: "loopDirection", value: params.direction ) 210 | } 211 | log.trace direction 212 | 213 | // time parameter is the time in seconds for a full loop 214 | def cycle = (device.currentValue("loopTime") != null ? device.currentValue("loopTime") : 2) 215 | log.trace cycle 216 | if (params?.time != null) { 217 | cycle = params.time 218 | sendEvent(name:"loopTime", value: cycle) 219 | } 220 | log.trace cycle 221 | def finTime = swapEndianHex(hexF(cycle, 4)) 222 | log.trace finTime 223 | 224 | def cmds = [] 225 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 226 | cmds << "delay 200" 227 | 228 | sendEvent(name: "switchColor", value: "Color Loop", displayed: false) 229 | sendEvent(name: "loopActive", value: "Active") 230 | 231 | if (params?.hue != null) { 232 | 233 | // start hue was specified, so convert to enhanced hue and start loop from there 234 | def sHue = Math.min(Math.round(params.hue * 255 / 100), 255) 235 | finHue = swapEndianHex(hexF(sHue, 4)) 236 | log.debug "activating color loop from specified hue" 237 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {0F 01 ${direction} ${finTime} ${sHue}}" 238 | 239 | } 240 | else { 241 | 242 | // start hue was not specified, so start loop from current hue updating direction and time 243 | log.debug "activating color loop from current hue" 244 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {07 02 ${direction} ${finTime} 0000}" 245 | 246 | } 247 | cmds 248 | } 249 | 250 | def stopLoop() { 251 | 252 | log.debug "deactivating color loop" 253 | def cmds = [ 254 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {01 00 00 0000 0000}", "delay 200", 255 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 200", 256 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 200", 257 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0" 258 | ] 259 | sendEvent(name: "loopActive", value: "Inactive") 260 | 261 | cmds 262 | 263 | } 264 | 265 | def on() { 266 | log.debug "on()" 267 | setLevel(state?.levelValue) 268 | } 269 | 270 | def zigbeeOff() { 271 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 272 | } 273 | 274 | def off() { 275 | log.debug "off()" 276 | sendEvent(name: "switch", value: "off") 277 | sendEvent(name: "switchColor", value: "off", displayed: false) 278 | zigbeeOff() 279 | } 280 | 281 | def refresh() { 282 | [ 283 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", 284 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", 285 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", 286 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500", 287 | "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {0015}" 288 | ] 289 | 290 | } 291 | 292 | def configure() { 293 | state.levelValue = 100 294 | log.debug "Configuring Reporting and Bindings." 295 | def configCmds = [ 296 | 297 | //Switch Reporting 298 | "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", 299 | "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", 300 | 301 | //Level Control Reporting 302 | "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", 303 | "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", 304 | 305 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", 306 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", 307 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" 308 | ] 309 | return configCmds + refresh() // send refresh cmds as part of config 310 | } 311 | 312 | def parseDescriptionAsMap(description) { 313 | (description - "read attr - ").split(",").inject([:]) { map, param -> 314 | def nameAndValue = param.split(":") 315 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 316 | } 317 | } 318 | 319 | def poll(){ 320 | log.debug "Poll is calling refresh" 321 | refresh() 322 | } 323 | 324 | def zigbeeSetLevel(level) { 325 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 1500}" 326 | } 327 | 328 | def setLevel(value) { 329 | state.levelValue = (value==null) ? 100 : value 330 | log.trace "setLevel($value)" 331 | def cmds = [] 332 | 333 | if (value == 0) { 334 | sendEvent(name: "switch", value: "off") 335 | sendEvent(name: "switchColor", value: "off", displayed: false) 336 | cmds << zigbeeOff() 337 | } 338 | else if (device.latestValue("switch") == "off") { 339 | sendEvent(name: "switch", value: "on") 340 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 341 | } 342 | 343 | sendEvent(name: "level", value: state.levelValue) 344 | def level = hex(state.levelValue * 255 / 100) 345 | cmds << zigbeeSetLevel(level) 346 | 347 | //log.debug cmds 348 | cmds 349 | } 350 | 351 | //input Hue Integer values; returns color name for saturation 100% 352 | private getColorName(hueValue){ 353 | if(hueValue>360 || hueValue<0) 354 | return 355 | 356 | hueValue = Math.round(hueValue / 100 * 360) 357 | 358 | log.debug "hue value is $hueValue" 359 | 360 | def colorName = "Color Mode" 361 | if(hueValue>=0 && hueValue <= 4){ 362 | colorName = "Red" 363 | } 364 | else if (hueValue>=5 && hueValue <=21 ){ 365 | colorName = "Brick Red" 366 | } 367 | else if (hueValue>=22 && hueValue <=30 ){ 368 | colorName = "Safety Orange" 369 | } 370 | else if (hueValue>=31 && hueValue <=40 ){ 371 | colorName = "Dark Orange" 372 | } 373 | else if (hueValue>=41 && hueValue <=49 ){ 374 | colorName = "Amber" 375 | } 376 | else if (hueValue>=50 && hueValue <=56 ){ 377 | colorName = "Gold" 378 | } 379 | else if (hueValue>=57 && hueValue <=65 ){ 380 | colorName = "Yellow" 381 | } 382 | else if (hueValue>=66 && hueValue <=83 ){ 383 | colorName = "Electric Lime" 384 | } 385 | else if (hueValue>=84 && hueValue <=93 ){ 386 | colorName = "Lawn Green" 387 | } 388 | else if (hueValue>=94 && hueValue <=112 ){ 389 | colorName = "Bright Green" 390 | } 391 | else if (hueValue>=113 && hueValue <=135 ){ 392 | colorName = "Lime" 393 | } 394 | else if (hueValue>=136 && hueValue <=166 ){ 395 | colorName = "Spring Green" 396 | } 397 | else if (hueValue>=167 && hueValue <=171 ){ 398 | colorName = "Turquoise" 399 | } 400 | else if (hueValue>=172 && hueValue <=187 ){ 401 | colorName = "Aqua" 402 | } 403 | else if (hueValue>=188 && hueValue <=203 ){ 404 | colorName = "Sky Blue" 405 | } 406 | else if (hueValue>=204 && hueValue <=217 ){ 407 | colorName = "Dodger Blue" 408 | } 409 | else if (hueValue>=218 && hueValue <=223 ){ 410 | colorName = "Navy Blue" 411 | } 412 | else if (hueValue>=224 && hueValue <=251 ){ 413 | colorName = "Blue" 414 | } 415 | else if (hueValue>=252 && hueValue <=256 ){ 416 | colorName = "Han Purple" 417 | } 418 | else if (hueValue>=257 && hueValue <=274 ){ 419 | colorName = "Electric Indigo" 420 | } 421 | else if (hueValue>=275 && hueValue <=289 ){ 422 | colorName = "Electric Purple" 423 | } 424 | else if (hueValue>=290 && hueValue <=300 ){ 425 | colorName = "Orchid Purple" 426 | } 427 | else if (hueValue>=301 && hueValue <=315 ){ 428 | colorName = "Magenta" 429 | } 430 | else if (hueValue>=316 && hueValue <=326 ){ 431 | colorName = "Hot Pink" 432 | } 433 | else if (hueValue>=327 && hueValue <=335 ){ 434 | colorName = "Deep Pink" 435 | } 436 | else if (hueValue>=336 && hueValue <=339 ){ 437 | colorName = "Raspberry" 438 | } 439 | else if (hueValue>=340 && hueValue <=352 ){ 440 | colorName = "Crimson" 441 | } 442 | else if (hueValue>=353 && hueValue <=360 ){ 443 | colorName = "Red" 444 | } 445 | 446 | colorName 447 | } 448 | 449 | private getEndpointId() { 450 | new BigInteger(device.endpointId, 16).toString() 451 | } 452 | 453 | private hex(value, width=2) { 454 | def s = new BigInteger(Math.round(value).toString()).toString(16) 455 | while (s.size() < width) { 456 | s = "0" + s 457 | } 458 | s 459 | } 460 | 461 | private hexF(value, width) { 462 | def s = new BigInteger(Math.round(value).toString()).toString(16) 463 | while (s.size() < width) { 464 | s = "0" + s 465 | } 466 | s 467 | } 468 | 469 | private evenHex(value){ 470 | def s = new BigInteger(Math.round(value).toString()).toString(16) 471 | while (s.size() % 2 != 0) { 472 | s = "0" + s 473 | } 474 | s 475 | } 476 | 477 | private String swapEndianHex(String hex) { 478 | reverseArray(hex.decodeHex()).encodeHex() 479 | } 480 | 481 | private Integer convertHexToInt(hex) { 482 | Integer.parseInt(hex,16) 483 | } 484 | 485 | //Need to reverse array of size 2 486 | private byte[] reverseArray(byte[] array) { 487 | byte tmp; 488 | tmp = array[1]; 489 | array[1] = array[0]; 490 | array[0] = tmp; 491 | return array 492 | } 493 | 494 | def setAdjustedColor(value) { 495 | log.debug "setAdjustedColor: ${value}" 496 | def adjusted = value + [:] 497 | adjusted.level = null // needed because color picker always sends 100 498 | setColor(adjusted) 499 | } 500 | 501 | def setColor(value){ 502 | log.trace "setColor($value)" 503 | def max = 0xfe 504 | 505 | if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} 506 | 507 | def colorName = getColorName(value.hue) 508 | sendEvent(name: "colorName", value: colorName) 509 | 510 | log.debug "color name is : $colorName" 511 | sendEvent(name: "hue", value: value.hue, displayed:false) 512 | sendEvent(name: "saturation", value: value.saturation, displayed:false) 513 | def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) 514 | def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) 515 | 516 | def cmd = [] 517 | if (value.switch != "off" && device.latestValue("switch") == "off") { 518 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 519 | cmd << "delay 150" 520 | } 521 | 522 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 1500}" 523 | cmd << "delay 150" 524 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 1500}" 525 | 526 | if (value.level) { 527 | state.levelValue = value.level 528 | sendEvent(name: "level", value: value.level) 529 | def level = hex(value.level * 255 / 100) 530 | cmd << zigbeeSetLevel(level) 531 | } 532 | 533 | if (value.switch == "off") { 534 | cmd << "delay 150" 535 | cmd << off() 536 | } 537 | 538 | cmd 539 | } 540 | -------------------------------------------------------------------------------- /Improved Zigbee Hue MA.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Improved Zigbee Hue RGBW Bulb MultiAttribute 3 | * 4 | * Removed the color adjuster. Added parsers for level and hue 5 | * 6 | * 7 | * Copyright 2015 SmartThings 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 10 | * in compliance with the License. You may obtain a copy of the License at: 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 15 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 16 | * for the specific language governing permissions and limitations under the License. 17 | * 18 | * Update 11/1/2015: Fixed a couple bugs and made unreachable events hidden. Thanks to Ian (mojo_333) for recognizing. 19 | * 20 | */ 21 | 22 | metadata { 23 | // Automatically generated. Make future change here. 24 | definition (name: "Improved Zigbee Hue Bulb MA", namespace: "smartthings", author: "SmartThings") { 25 | capability "Switch Level" 26 | capability "Actuator" 27 | capability "Color Control" 28 | capability "Color Temperature" 29 | capability "Switch" 30 | capability "Configuration" 31 | capability "Polling" 32 | capability "Refresh" 33 | capability "Sensor" 34 | 35 | command "setAdjustedColor" 36 | 37 | // This is a new temporary counter to keep track of no responses 38 | attribute "unreachable", "number" 39 | attribute "colorMode", "string" 40 | attribute "colorName", "string" 41 | attribute "switchColor", "string" 42 | 43 | fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019" 44 | } 45 | 46 | // simulator metadata 47 | simulator { 48 | // status messages 49 | status "on": "on/off: 1" 50 | status "off": "on/off: 0" 51 | 52 | // reply messages 53 | reply "zcl on-off on": "on/off: 1" 54 | reply "zcl on-off off": "on/off: 0" 55 | } 56 | 57 | // UI tile definitions 58 | tiles (scale: 2){ 59 | multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { 60 | tileAttribute("device.switchColor", key: "PRIMARY_CONTROL") { 61 | attributeState "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 62 | attributeState "Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0000" 63 | attributeState "Brick Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff3700" 64 | attributeState "Safety Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff6F00" 65 | attributeState "Dark Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff9900" 66 | attributeState "Amber", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffbf00" 67 | attributeState "Gold", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffe1000" 68 | attributeState "Yellow", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffff00" 69 | attributeState "Electric Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#bfff00" 70 | attributeState "Lawn Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#88ff00" 71 | attributeState "Bright Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#48ff00" 72 | attributeState "Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff11" 73 | attributeState "Spring Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff6a" 74 | attributeState "Turquoise", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffd0" 75 | attributeState "Aqua", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffff" 76 | attributeState "Sky Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00bfff" 77 | attributeState "Dodger Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#007bff" 78 | attributeState "Navy Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0050ff" 79 | attributeState "Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0000ff" 80 | attributeState "Han Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#3b00ff" 81 | attributeState "Electric Indigo", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#6600ff" 82 | attributeState "Electric Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#b200ff" 83 | attributeState "Orchid Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#e900ff" 84 | attributeState "Magenta", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00dc" 85 | attributeState "Hot Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00aa" 86 | attributeState "Deep Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff007b" 87 | attributeState "Raspberry", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0061" 88 | attributeState "Crimson", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff003b" 89 | attributeState "White", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 90 | } 91 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 92 | attributeState "color", action: "setAdjustedColor" 93 | } 94 | tileAttribute ("device.level", key: "SECONDARY_CONTROL") { 95 | attributeState "level", label: 'Level is ${currentValue}%' 96 | } 97 | } 98 | standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 99 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 100 | } 101 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 4, inactiveLabel: false) { 102 | state "level", action:"switch level.setLevel" 103 | } 104 | controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 4, inactiveLabel: false, range:"(2000..6500)") { 105 | state "colorTemperature", action:"color temperature.setColorTemperature" 106 | } 107 | valueTile("colorTemp", "device.colorTemperature", height: 1, width: 2, inactiveLabel: false, decoration: "flat") { 108 | state "colorTemperature", label: '${currentValue} K' 109 | } 110 | valueTile("colorName", "device.colorName", height: 1, width: 2, inactiveLabel: false, decoration: "flat") { 111 | state "colorName", label: '${currentValue}' 112 | } 113 | valueTile("colorMode", "device.colorMode", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 114 | state "colorMode", label: '${currentValue}' 115 | } 116 | 117 | 118 | main(["switch"]) 119 | details(["switch", "levelSliderControl", "colorName", "colorTempSliderControl", "colorTemp", "colorMode", "refresh"]) 120 | } 121 | } 122 | 123 | // Parse incoming device messages to generate events 124 | def parse(String description) { 125 | log.info "description is $description" 126 | 127 | sendEvent(name: "unreachable", value: 0, displayed: false) 128 | 129 | if (description?.startsWith("catchall:")) { 130 | if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) 131 | { 132 | def result = createEvent(name: "switch", value: "on") 133 | sendEvent(name: "switchColor", value: ( device.currentValue("colorMode") == "White" ? "White" : device.currentValue("colorName")), displayed: false) 134 | log.debug "Parse returned ${result?.descriptionText}" 135 | return result 136 | } 137 | else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) 138 | { 139 | if(!(description?.startsWith("catchall: 0104 0300") || description?.startsWith("catchall: 0104 0008"))){ 140 | def result = createEvent(name: "switch", value: "off") 141 | sendEvent(name: "switchColor", value: "off", displayed: false) 142 | log.debug "Parse returned ${result?.descriptionText}" 143 | return result 144 | } 145 | } 146 | } 147 | else if (description?.startsWith("read attr -")) { 148 | def descMap = parseDescriptionAsMap(description) 149 | // log.trace "descMap : $descMap" 150 | 151 | if (descMap.cluster == "0300") { 152 | if(descMap.attrId == "0000"){ //Hue Attribute 153 | def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 154 | log.debug "Hue value returned is $hueValue" 155 | def colorName = getColorName(hueValue) 156 | sendEvent(name: "colorName", value: colorName) 157 | if (device.currentValue("switch") == "on") { sendEvent(name: "switchColor", value: ( device.currentValue("colorMode") == "White" ? "White" : device.currentValue("colorName")), displayed: false) } 158 | sendEvent(name: "hue", value: hueValue, displayed:false) 159 | } 160 | else if(descMap.attrId == "0001"){ //Saturation Attribute 161 | def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 162 | log.debug "Saturation from refresh is $saturationValue" 163 | sendEvent(name: "saturation", value: saturationValue, displayed:false) 164 | } 165 | else if( descMap.attrId == "0007") { 166 | def tempInMired = convertHexToInt(descMap.value) 167 | def tempInKelvin = Math.round(1000000/tempInMired) 168 | log.debug "Color temperature returned is $tempInKelvin" 169 | sendEvent(name: "colorTemperature", value: tempInKelvin) 170 | } 171 | else if( descMap.attrId == "0008") { 172 | def colorModeValue = (descMap.value == "02" ? "White" : "Color") 173 | log.debug "Color mode returned $colorModeValue" 174 | sendEvent(name: "colorMode", value: colorModeValue) 175 | if (device.currentValue("switch") == "on") { 176 | sendEvent(name: "switchColor", value: (descMap.value == "02" ? "White" : device.currentValue("colorName")), displayed: false) 177 | } 178 | } 179 | } 180 | else if(descMap.cluster == "0008"){ 181 | def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) 182 | log.debug "dimmer value is $dimmerValue" 183 | sendEvent(name: "level", value: dimmerValue) 184 | } 185 | } 186 | else { 187 | def name = description?.startsWith("on/off: ") ? "switch" : null 188 | def value = null 189 | if (name == "switch") { 190 | value = (description?.endsWith(" 1") ? "on" : "off") 191 | log.debug value 192 | sendEvent(name: "switchColor", value: (value == "off" ? "off" : device.currentValue("colorName")), displayed: false) 193 | } 194 | else { value = null } 195 | def result = createEvent(name: name, value: value) 196 | log.debug "Parse returned ${result?.descriptionText}" 197 | return result 198 | } 199 | 200 | } 201 | 202 | def parseDescriptionAsMap(description) { 203 | (description - "read attr - ").split(",").inject([:]) { map, param -> 204 | def nameAndValue = param.split(":") 205 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 206 | } 207 | } 208 | 209 | def on() { 210 | // just assume it works for now 211 | log.debug "on()" 212 | sendEvent(name: "switch", value: "on") 213 | sendEvent(name: "switchColor", value: ( device.currentValue("colorMode") == "White" ? "White" : device.currentValue("colorName")), displayed: false) 214 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 215 | } 216 | 217 | def off() { 218 | // just assume it works for now 219 | log.debug "off()" 220 | sendEvent(name: "switch", value: "off") 221 | sendEvent(name: "switchColor", value: "off", displayed: false) 222 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 223 | } 224 | 225 | def setColorTemperature(value) { 226 | if(value<101){ 227 | value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 228 | } 229 | 230 | def tempInMired = Math.round(1000000/value) 231 | def finalHex = swapEndianHex(hexF(tempInMired, 4)) 232 | // def genericName = getGenericName(value) 233 | // log.debug "generic name is : $genericName" 234 | 235 | def cmds = [] 236 | 237 | if (device.latestValue("switch") == "off") { 238 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 239 | cmds << "delay 150" 240 | } 241 | sendEvent(name: "colorTemperature", value: value, displayed:false) 242 | sendEvent(name: "colorMode", value: "White") 243 | sendEvent(name: "switchColor", value: "White", displayed: false) 244 | // sendEvent(name: "colorName", value: genericName) 245 | 246 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" 247 | 248 | cmds 249 | } 250 | 251 | def setHue(value) { 252 | def max = 0xfe 253 | // log.trace "setHue($value)" 254 | sendEvent(name: "hue", value: value) 255 | def scaledValue = Math.round(value * max / 100.0) 256 | def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 2000}" 257 | //log.info cmd 258 | cmd 259 | } 260 | 261 | def setAdjustedColor(value) { 262 | // log.debug "setAdjustedColor: ${value}" 263 | def adjusted = value + [:] 264 | adjusted.level = null // needed because color picker always sends 100 265 | setColor(adjusted) 266 | } 267 | 268 | def setColor(value){ 269 | log.trace "setColor($value)" 270 | def max = 0xfe 271 | if (value.hue == 0 && value.saturation == 0) { setColorTemperature(3500) } 272 | else if (value.red == 255 && value.blue == 185 && value.green == 255) { setColorTemperature(2700) } 273 | else { 274 | if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} 275 | 276 | def colorName = getColorName(value.hue) 277 | sendEvent(name: "colorName", value: colorName) 278 | sendEvent(name: "colorMode", value: "Color") 279 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 280 | 281 | log.debug "color name is : $colorName" 282 | sendEvent(name: "hue", value: value.hue, displayed:false) 283 | sendEvent(name: "saturation", value: value.saturation, displayed:false) 284 | def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) 285 | def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) 286 | 287 | def cmd = [] 288 | if (value.switch != "off" && device.latestValue("switch") == "off") { 289 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 290 | cmd << "delay 150" 291 | } 292 | 293 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x06 {${scaledHueValue} ${scaledSatValue} 2000}" 294 | 295 | if (value.level) { 296 | state.levelValue = value.level 297 | sendEvent(name: "level", value: value.level) 298 | def level = hex(value.level * 2.55) 299 | if(value == 1) { level = hex(1) } 300 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 1500}" 301 | } 302 | 303 | if (value.switch == "off") { 304 | cmd << "delay 150" 305 | cmd << off() 306 | } 307 | 308 | cmd 309 | } 310 | } 311 | 312 | def setSaturation(value) { 313 | 314 | def max = 0xfe 315 | // log.trace "setSaturation($value)" 316 | sendEvent(name: "saturation", value: value) 317 | def scaledValue = Math.round(value * max / 100.0) 318 | def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} 2000}" 319 | //log.info cmd 320 | cmd 321 | } 322 | 323 | def refresh() { 324 | 325 | def unreachable = device.currentValue("unreachable") 326 | if(unreachable == null) { 327 | sendEvent(name: 'unreachable', value: 1, displayed: false) 328 | } 329 | else { 330 | sendEvent(name: 'unreachable', value: unreachable + 1, displayed: false) 331 | } 332 | if(unreachable > 2) { 333 | sendEvent(name: "switch", value: "off") 334 | sendEvent(name: "switchColor", value: "off", displayed: false) 335 | } 336 | 337 | // Ping the device with color as to not get out of sync 338 | [ 339 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", 340 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", 341 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1","delay 500", 342 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0","delay 500", 343 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7","delay 500", 344 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8", "delay 500", 345 | "st wattr 0x${device.deviceNetworkId} ${endpointId} 8 0x10 0x21 {0015}" 346 | 347 | ] 348 | } 349 | 350 | def poll(){ 351 | log.debug "Poll is calling refresh" 352 | refresh() 353 | } 354 | 355 | def configure(){ 356 | log.debug "Initiating configuration reporting and binding" 357 | 358 | [ 359 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", 360 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 1000", 361 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}" 362 | ] 363 | 364 | } 365 | 366 | //input Hue Integer values; returns color name for saturation 100% 367 | private getColorName(hueValue){ 368 | if(hueValue>360 || hueValue<0) 369 | return 370 | 371 | hueValue = Math.round(hueValue / 100 * 360) 372 | 373 | log.debug "hue value is $hueValue" 374 | 375 | def colorName = "Color Mode" 376 | if(hueValue>=0 && hueValue <= 4){ 377 | colorName = "Red" 378 | } 379 | else if (hueValue>=5 && hueValue <=21 ){ 380 | colorName = "Brick Red" 381 | } 382 | else if (hueValue>=22 && hueValue <=30 ){ 383 | colorName = "Safety Orange" 384 | } 385 | else if (hueValue>=31 && hueValue <=40 ){ 386 | colorName = "Dark Orange" 387 | } 388 | else if (hueValue>=41 && hueValue <=49 ){ 389 | colorName = "Amber" 390 | } 391 | else if (hueValue>=50 && hueValue <=56 ){ 392 | colorName = "Gold" 393 | } 394 | else if (hueValue>=57 && hueValue <=65 ){ 395 | colorName = "Yellow" 396 | } 397 | else if (hueValue>=66 && hueValue <=83 ){ 398 | colorName = "Electric Lime" 399 | } 400 | else if (hueValue>=84 && hueValue <=93 ){ 401 | colorName = "Lawn Green" 402 | } 403 | else if (hueValue>=94 && hueValue <=112 ){ 404 | colorName = "Bright Green" 405 | } 406 | else if (hueValue>=113 && hueValue <=135 ){ 407 | colorName = "Lime" 408 | } 409 | else if (hueValue>=136 && hueValue <=166 ){ 410 | colorName = "Spring Green" 411 | } 412 | else if (hueValue>=167 && hueValue <=171 ){ 413 | colorName = "Turquoise" 414 | } 415 | else if (hueValue>=172 && hueValue <=187 ){ 416 | colorName = "Aqua" 417 | } 418 | else if (hueValue>=188 && hueValue <=203 ){ 419 | colorName = "Sky Blue" 420 | } 421 | else if (hueValue>=204 && hueValue <=217 ){ 422 | colorName = "Dodger Blue" 423 | } 424 | else if (hueValue>=218 && hueValue <=223 ){ 425 | colorName = "Navy Blue" 426 | } 427 | else if (hueValue>=224 && hueValue <=251 ){ 428 | colorName = "Blue" 429 | } 430 | else if (hueValue>=252 && hueValue <=256 ){ 431 | colorName = "Han Purple" 432 | } 433 | else if (hueValue>=257 && hueValue <=274 ){ 434 | colorName = "Electric Indigo" 435 | } 436 | else if (hueValue>=275 && hueValue <=289 ){ 437 | colorName = "Electric Purple" 438 | } 439 | else if (hueValue>=290 && hueValue <=300 ){ 440 | colorName = "Orchid Purple" 441 | } 442 | else if (hueValue>=301 && hueValue <=315 ){ 443 | colorName = "Magenta" 444 | } 445 | else if (hueValue>=316 && hueValue <=326 ){ 446 | colorName = "Hot Pink" 447 | } 448 | else if (hueValue>=327 && hueValue <=335 ){ 449 | colorName = "Deep Pink" 450 | } 451 | else if (hueValue>=336 && hueValue <=339 ){ 452 | colorName = "Raspberry" 453 | } 454 | else if (hueValue>=340 && hueValue <=352 ){ 455 | colorName = "Crimson" 456 | } 457 | else if (hueValue>=353 && hueValue <=360 ){ 458 | colorName = "Red" 459 | } 460 | 461 | colorName 462 | } 463 | 464 | def setLevel(value) { 465 | log.trace "setLevel($value)" 466 | 467 | def unreachable = device.currentValue("unreachable") 468 | log.debug unreachable 469 | if(unreachable == null) { 470 | sendEvent(name: 'unreachable', value: 1, displayed: false) 471 | } 472 | else { 473 | sendEvent(name: 'unreachable', value: unreachable + 1, displayed: false) 474 | } 475 | if(unreachable > 2) { 476 | sendEvent(name: "switch", value: "off") 477 | sendEvent(name: "switchColor", value: "off", displayed: false) 478 | } 479 | 480 | def cmds = [] 481 | 482 | if (value == 0) { 483 | sendEvent(name: "switch", value: "off") 484 | sendEvent(name: "switchColor", value: "off", displayed: false) 485 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 486 | } 487 | else if (device.currentValue("switch") == "off" && unreachable < 2) { 488 | sendEvent(name: "switch", value: "on") 489 | log.debug device.currentValue("colorMode") 490 | sendEvent(name: "switchColor", value: (device.currentValue("colorMode") == "White" ? "White" : device.currentValue("colorName")), displayed: false) 491 | } 492 | 493 | sendEvent(name: "level", value: value) 494 | def level = hex(value * 2.55) 495 | if(value == 1) { level = hex(1) } 496 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 1500}" 497 | 498 | //log.debug cmds 499 | cmds 500 | } 501 | 502 | private getEndpointId() { 503 | new BigInteger(device.endpointId, 16).toString() 504 | } 505 | 506 | private hex(value, width=2) { 507 | def s = new BigInteger(Math.round(value).toString()).toString(16) 508 | while (s.size() < width) { 509 | s = "0" + s 510 | } 511 | s 512 | } 513 | 514 | private evenHex(value){ 515 | def s = new BigInteger(Math.round(value).toString()).toString(16) 516 | while (s.size() % 2 != 0) { 517 | s = "0" + s 518 | } 519 | s 520 | } 521 | 522 | //Need to reverse array of size 2 523 | private byte[] reverseArray(byte[] array) { 524 | byte tmp; 525 | tmp = array[1]; 526 | array[1] = array[0]; 527 | array[0] = tmp; 528 | return array 529 | } 530 | 531 | private hexF(value, width) { 532 | def s = new BigInteger(Math.round(value).toString()).toString(16) 533 | while (s.size() < width) { 534 | s = "0" + s 535 | } 536 | s 537 | } 538 | 539 | private String swapEndianHex(String hex) { 540 | reverseArray(hex.decodeHex()).encodeHex() 541 | } 542 | 543 | private Integer convertHexToInt(hex) { 544 | Integer.parseInt(hex,16) 545 | } 546 | -------------------------------------------------------------------------------- /Lutron-Connected-Bulb-Remote.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | */ 14 | /* Philips Hue Wireless Dimmer 15 | 16 | Capabilities: 17 | Actuator 18 | Configuration 19 | Polling 20 | Refresh 21 | Switch 22 | Switch Level 23 | 24 | 25 | */ 26 | 27 | metadata { 28 | definition (name: "Lutron Connected Bulb Remote", namespace: "Sticks18", author: "Scott G") { 29 | capability "Actuator" 30 | capability "Button" 31 | capability "Configuration" 32 | capability "Sensor" 33 | capability "Refresh" 34 | 35 | command "getGroup" 36 | command "assignGroup" 37 | command "removeGroup" 38 | command "removeAllGroup" 39 | command "updateGroup" 40 | 41 | attribute "groupId", "String" 42 | 43 | fingerprint profileId: "C05E", deviceId: "0820", inClusters: "0000,1000,FF00,FC44", outClusters: "0000,0003,0004,0005,0006,0008,1000,FF00" 44 | } 45 | 46 | // simulator metadata 47 | simulator { 48 | // status messages 49 | 50 | } 51 | 52 | // UI tile definitions 53 | tiles { 54 | standardTile("button", "device.button", width: 2, height: 2) { 55 | state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" 56 | } 57 | standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 58 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 59 | } 60 | valueTile("group", "device.groupId", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 61 | state "groupId", label: '${currentValue}' 62 | } 63 | main "button" 64 | details(["button", "group"]) 65 | } 66 | } 67 | 68 | // Parse incoming device messages to generate events 69 | def parse(String description) { 70 | log.trace description 71 | } 72 | 73 | def getGroup(zigId, endP) { 74 | 75 | "st cmd 0x${zigId} ${endP} 4 2 {}" 76 | 77 | } 78 | 79 | def assignGroup(zigId, endP, grpId) { 80 | 81 | "st cmd 0x${zigId} ${endP} 4 0 {${grpId}}" 82 | 83 | } 84 | 85 | def removeGroup(zigId, endP, grpId) { 86 | 87 | "st cmd 0x${zigId} ${endP} 4 3 {${grpId}}" 88 | 89 | } 90 | 91 | def removeAllGroup(zigId, endP) { 92 | 93 | "st cmd 0x${zigId} ${endP} 4 4 {}" 94 | 95 | } 96 | 97 | def updateGroup(grpId) { 98 | log.debug "Group Id for this remote is: ${grpId}" 99 | sendEvent(name: "groupId", value: grpId) 100 | } 101 | 102 | def refresh() { 103 | [ 104 | /* "raw 0x1000 {00 01 0C 0000 FE}", "delay 150", 105 | "send 0x${device.deviceNetworkId} 1 1" 106 | "st cmd 0x${device.deviceNetworkId} 1 0x1000 0x41 {01}", "delay 500", 107 | "st cmd 0x${device.deviceNetworkId} 1 0x1000 0x42 {01}", "delay 500", 108 | "st rattr 0x${device.deviceNetworkId} 1 0 1", "delay 500", 109 | "st rattr 0x${device.deviceNetworkId} 1 0 2", "delay 500", 110 | "st rattr 0x${device.deviceNetworkId} 1 0 3", "delay 500", 111 | "st rattr 0x${device.deviceNetworkId} 1 0 4", "delay 500", 112 | "st rattr 0x${device.deviceNetworkId} 1 0 5" */ 113 | ] 114 | 115 | } 116 | 117 | def configure() { 118 | 119 | log.debug "configure" 120 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 121 | log.debug "Confuguring Reporting and Bindings." 122 | def configCmds = [ 123 | 124 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", 125 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", 126 | 127 | ] 128 | return configCmds + refresh 129 | } 130 | 131 | 132 | private getEndpointId() { 133 | new BigInteger(device.endpointId, 16).toString() 134 | } 135 | 136 | //Need to reverse array of size 2 137 | private byte[] reverseArray(byte[] array) { 138 | byte tmp; 139 | tmp = array[1]; 140 | array[1] = array[0]; 141 | array[0] = tmp; 142 | return array 143 | } 144 | 145 | private String swapEndianHex(String hex) { 146 | reverseArray(hex.decodeHex()).encodeHex() 147 | } 148 | -------------------------------------------------------------------------------- /My-GE-Link-v3.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * GE Link Bulb 3 | * 4 | * Copyright 2014 SmartThings 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart, and others 16 | * 17 | ****************************************************************************** 18 | * Changes 19 | ****************************************************************************** 20 | * 21 | * Change 1: 2014-10-10 (wackford) 22 | * Added setLevel event so subscriptions to the event will work 23 | * Change 2: 2014-12-10 (jscgs350 using Sticks18's code and effort!) 24 | * Modified parse section to properly identify bulb status in the app when manually turned on by a physical switch 25 | * Change 3: 2014-12-12 (jscgs350, Sticks18's) 26 | * Modified to ensure dimming was smoother, and added fix for dimming below 7 27 | * Change 4: 2014-12-14 Part 1 (Sticks18) 28 | * Modified to ignore unnecessary level change responses to prevent level skips 29 | * Change 5: 2014-12-14 Part 2 (Sticks18, jscgs350) 30 | * Modified to clean up trace&debug logging, added new code from @sticks18 for parsing "on/off" to determine if the bulb is manually turned on and immediately update the app 31 | * Change 6: 2015-01-02 (Sticks18) 32 | * Modified to allow dim rate in Preferences. Added ability to dim during On/Off commands and included this option in Preferences. Defaults are "Normal" and no dim for On/Off. 33 | * Change 7: 2015-01-09 (tslagle13) 34 | * dimOnOff is was boolean, and switched to enum. Properly update "rampOn" and "rampOff" when refreshed or a polled (dim transition for On/Off commands) 35 | * Change 8: 2015-03-06 (Juan Risso) 36 | * Slider range from 0..100 37 | * Change 9: 2015-03-06 (Juan Risso) 38 | * Setlevel -> value to integer (to prevent smartapp calling this function from not working). 39 | * Change 10: 2015-09-06 (Sticks18) 40 | * Modified tile layout to make use of new multiattribute tile for dimming. Added dim adjustment when sending setLevel() if bulb is off, so it will transition smoothly from 0. 41 | * Change 11: 2016-01-03 (Sticks18) 42 | * Added alert feature, updated layout and improved setLevel to accept any duration. 43 | * 44 | */ 45 | metadata { 46 | definition (name: "My GE Link Bulb v3", namespace: "jscgs350", author: "smartthings") { 47 | 48 | capability "Actuator" 49 | capability "Configuration" 50 | capability "Refresh" 51 | capability "Sensor" 52 | capability "Switch" 53 | capability "Switch Level" 54 | capability "Polling" 55 | 56 | command "alert" 57 | 58 | attribute "attDimRate", "string" 59 | attribute "attDimOnOff", "string" 60 | 61 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" 62 | } 63 | 64 | // UI tile definitions 65 | tiles(scale: 2) { 66 | multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true, canChangeBackground: true) { 67 | tileAttribute("device.switch", key: "PRIMARY_CONTROL") { 68 | attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" 69 | attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "turningOff" 70 | attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" 71 | attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "turningOff" 72 | } 73 | tileAttribute("device.level", key: "SLIDER_CONTROL") { 74 | attributeState "level", action:"switch level.setLevel" 75 | } 76 | tileAttribute("level", key: "SECONDARY_CONTROL") { 77 | attributeState "level", label: 'Light dimmed to ${currentValue}%' 78 | } 79 | } 80 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 81 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 82 | } 83 | valueTile("attDimRate", "device.attDimRate", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { 84 | state "attDimRate", label: 'Dim rate: ${currentValue}' 85 | } 86 | valueTile("attDimOnOff", "device.attDimOnOff", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { 87 | state "attDimOnOff", label: 'Dim for on/off: ${currentValue}' 88 | } 89 | controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 6, inactiveLabel: false, range: "(0..100)") { 90 | state "level", action:"switch level.setLevel" 91 | } 92 | 93 | 94 | main "switch" 95 | details(["switch","attDimRate", "refresh", "attDimOnOff","levelSliderControl"]) 96 | } 97 | 98 | preferences { 99 | 100 | input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) 101 | input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) 102 | 103 | } 104 | } 105 | 106 | // Parse incoming device messages to generate events 107 | def parse(String description) { 108 | log.trace description 109 | 110 | if (description?.startsWith("on/off:")) { 111 | log.debug "The bulb was sent a command to do something just now..." 112 | if (description[-1] == "1") { 113 | def result = createEvent(name: "switch", value: "on") 114 | log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}" 115 | return result 116 | } else if (description[-1] == "0") { 117 | def result = createEvent(name: "switch", value: "off") 118 | log.debug "Off command was sent : Parse returned ${result?.descriptionText}" 119 | return result 120 | } 121 | } 122 | 123 | def msg = zigbee.parse(description) 124 | 125 | if (description?.startsWith("catchall:")) { 126 | // log.trace msg 127 | // log.trace "data: $msg.data" 128 | 129 | def x = description[-4..-1] 130 | // log.debug x 131 | def z = description[18] 132 | 133 | if (z == "8" || msg.cluster == 3){} 134 | 135 | else { 136 | 137 | switch (x) 138 | { 139 | 140 | case "0000": 141 | 142 | def result = createEvent(name: "switch", value: "off") 143 | log.debug "${result?.descriptionText}" 144 | return result 145 | break 146 | 147 | case "1000": 148 | 149 | def result = createEvent(name: "switch", value: "off") 150 | log.debug "${result?.descriptionText}" 151 | return result 152 | break 153 | 154 | case "0100": 155 | 156 | def result = createEvent(name: "switch", value: "on") 157 | log.debug "${result?.descriptionText}" 158 | return result 159 | break 160 | 161 | case "1001": 162 | 163 | def result = createEvent(name: "switch", value: "on") 164 | log.debug "${result?.descriptionText}" 165 | return result 166 | break 167 | } 168 | } 169 | } 170 | 171 | if (description?.startsWith("read attr")) { 172 | 173 | // log.trace description[27..28] 174 | // log.trace description[-2..-1] 175 | 176 | if (description[27..28] == "0A") { 177 | 178 | // log.debug description[-2..-1] 179 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) 180 | sendEvent( name: "level", value: i ) 181 | sendEvent( name: "switch.setLevel", value: i) //added to help subscribers 182 | 183 | } 184 | 185 | else { 186 | 187 | if (description[-2..-1] == "00" && state.trigger == "setLevel") { 188 | // log.debug description[-2..-1] 189 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) 190 | sendEvent( name: "level", value: i ) 191 | sendEvent( name: "switch.setLevel", value: i) //added to help subscribers 192 | } 193 | 194 | if (description[-2..-1] == state.lvl) { 195 | // log.debug description[-2..-1] 196 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) 197 | sendEvent( name: "level", value: i ) 198 | sendEvent( name: "switch.setLevel", value: i) //added to help subscribers 199 | } 200 | 201 | } 202 | } 203 | 204 | } 205 | 206 | def alert(alertTime = 7) { 207 | log.debug alertTime 208 | def payload = swapEndianHex(hex(alertTime,4)) 209 | "st cmd 0x${device.deviceNetworkId} 1 3 0 {${payload}}" 210 | } 211 | 212 | def poll() { 213 | 214 | [ 215 | "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", 216 | "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", 217 | "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}" 218 | ] 219 | 220 | } 221 | 222 | def updated() { 223 | 224 | sendEvent( name: "attDimRate", value: "${dimRate}" ) 225 | sendEvent( name: "attDimOnOff", value: "${dimOnOff}" ) 226 | 227 | state.dOnOff = "0000" 228 | 229 | if (dimRate) { 230 | 231 | switch (dimRate) 232 | { 233 | 234 | case "Instant": 235 | 236 | state.rate = "0000" 237 | if (dimOnOff) { state.dOnOff = "0000"} 238 | break 239 | 240 | case "Normal": 241 | 242 | state.rate = "1500" 243 | if (dimOnOff) { state.dOnOff = "0015"} 244 | break 245 | 246 | case "Slow": 247 | 248 | state.rate = "2500" 249 | if (dimOnOff) { state.dOnOff = "0025"} 250 | break 251 | 252 | case "Very Slow": 253 | 254 | state.rate = "3500" 255 | if (dimOnOff) { state.dOnOff = "0035"} 256 | break 257 | 258 | } 259 | 260 | } 261 | 262 | else { 263 | 264 | state.rate = "1500" 265 | state.dOnOff = "0000" 266 | 267 | } 268 | 269 | if (dimOnOff == "Yes"){ 270 | switch (dimOnOff){ 271 | case "InstantOnOff": 272 | 273 | state.rate = "0000" 274 | if (state.rate == "0000") { state.dOnOff = "0000"} 275 | break 276 | 277 | case "NormalOnOff": 278 | 279 | state.rate = "1500" 280 | if (state.rate == "1500") { state.dOnOff = "0015"} 281 | break 282 | 283 | case "SlowOnOff": 284 | 285 | state.rate = "2500" 286 | if (state.rate == "2500") { state.dOnOff = "0025"} 287 | break 288 | 289 | case "Very SlowOnOff": 290 | 291 | state.rate = "3500" 292 | if (state.rate == "3500") { state.dOnOff = "0035"} 293 | break 294 | 295 | } 296 | 297 | } 298 | else{ 299 | state.dOnOff = "0000" 300 | } 301 | 302 | "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}" 303 | 304 | 305 | } 306 | 307 | def on() { 308 | state.lvl = "00" 309 | state.trigger = "on/off" 310 | 311 | // log.debug "on()" 312 | sendEvent(name: "switch", value: "on") 313 | "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" 314 | } 315 | 316 | def off() { 317 | state.lvl = "00" 318 | state.trigger = "on/off" 319 | 320 | // log.debug "off()" 321 | sendEvent(name: "switch", value: "off") 322 | "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" 323 | } 324 | 325 | def refresh() { 326 | 327 | [ 328 | "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", 329 | "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", 330 | "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}" 331 | ] 332 | poll() 333 | 334 | } 335 | 336 | def setLevel(value, duration = 2.0) { 337 | 338 | def cmds = [] 339 | 340 | if (value == 0) { 341 | sendEvent(name: "switch", value: "off") 342 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {00 ${state.rate}}" 343 | } 344 | else if (device.latestValue("switch") == "off") { 345 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {00 0000}" 346 | sendEvent(name: "switch", value: "on") 347 | } 348 | 349 | sendEvent(name: "level", value: value) 350 | value = (value * 255 / 100) 351 | def level = hex(value); 352 | 353 | duration = duration * 10 354 | def tranTime = swapEndianHex(hex(duration, 4)) 355 | 356 | state.trigger = "setLevel" 357 | state.lvl = "${level}" 358 | 359 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${tranTime}}" 360 | 361 | log.debug cmds 362 | cmds 363 | } 364 | 365 | def configure() { 366 | 367 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 368 | log.debug "Confuguring Reporting and Bindings." 369 | def configCmds = [ 370 | 371 | //Switch Reporting 372 | "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", 373 | "send 0x${device.deviceNetworkId} 1 1", "delay 1000", 374 | 375 | //Level Control Reporting 376 | "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", 377 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500", 378 | 379 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", 380 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", 381 | ] 382 | return configCmds + refresh() // send refresh cmds as part of config 383 | } 384 | 385 | private hex(value, width=2) { 386 | def s = new BigInteger(Math.round(value).toString()).toString(16) 387 | while (s.size() < width) { 388 | s = "0" + s 389 | } 390 | s 391 | } 392 | 393 | private Integer convertHexToInt(hex) { 394 | Integer.parseInt(hex,16) 395 | } 396 | 397 | private getEndpointId() { 398 | new BigInteger(device.endpointId, 16).toString() 399 | } 400 | 401 | private String swapEndianHex(String hex) { 402 | reverseArray(hex.decodeHex()).encodeHex() 403 | } 404 | 405 | private byte[] reverseArray(byte[] array) { 406 | int i = 0; 407 | int j = array.length - 1; 408 | byte tmp; 409 | while (j > i) { 410 | tmp = array[j]; 411 | array[j] = array[i]; 412 | array[i] = tmp; 413 | j--; 414 | i++; 415 | } 416 | return array 417 | } 418 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartThings---Devices 2 | For deviceTypes to be used with SmartThings. Please see License for applicable use restrictions. 3 | -------------------------------------------------------------------------------- /Smart-Bulb-Alert-Switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | * Smart Bulb Alert Switch - to be used with Smart Bulb Alert SmartApp to trigger smart bulbs to blink/flash 14 | * 15 | * Author: Scott Gibson 16 | * 17 | * Date: 2015-25-11 18 | */ 19 | metadata { 20 | definition (name: "Smart Bulb Alert Switch", namespace: "sticks18", author: "Scott Gibson") { 21 | capability "Actuator" 22 | capability "Switch" 23 | 24 | command "zigbeeCmd" 25 | command "attWrite" 26 | command "allOff" 27 | } 28 | 29 | // simulator metadata 30 | simulator { 31 | } 32 | 33 | // UI tile definitions 34 | tiles { 35 | standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) { 36 | state "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" 37 | state "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "off" 38 | } 39 | standardTile("allOff", "device.switch", inactiveLabel: false, decoration: "flat") { 40 | state "default", label:"Stop Alerts", action:"device.allOff", icon:"st.secondary.refresh" 41 | } 42 | main "button" 43 | details "button", "allOff" 44 | } 45 | } 46 | 47 | def parse(String description) { 48 | } 49 | 50 | def on() { 51 | sendEvent(name: "switch", value: "on") 52 | parent.on(this) 53 | } 54 | 55 | def off() { 56 | sendEvent(name: "switch", value: "off") 57 | parent.off(this) 58 | } 59 | 60 | def zigbeeCmd(netId, endpoint, cluster, command, payload) { 61 | log.debug "Sending zigbee command ${cluster} ${command} {${payload}} to device ${netId} via endpoint ${endpoint}" 62 | "st cmd 0x${netId} ${endpoint} ${cluster} ${command} {${payload}}" 63 | } 64 | 65 | def attWrite(netId, endpoint, cluster, attribute, size, payload) { 66 | log.debug "Writing zigbee attribute ${cluster} ${command} {${payload}} to device ${netId} via endpoint ${endpoint}" 67 | "st wattr 0x${netId} ${endpoint} ${cluster} ${attribute} ${size} {${payload}}" 68 | } 69 | 70 | def allOff(){ 71 | log.debug "Stop all bulbs alerting" 72 | sendEvent(name: "switch", value: "off") 73 | parent.allOff(this) 74 | } 75 | 76 | private hex(value, width) { 77 | def s = new BigInteger(Math.round(value).toString()).toString(16) 78 | while (s.size() < width) { 79 | s = "0" + s 80 | } 81 | s 82 | } 83 | 84 | private Integer convertHexToInt(hex) { 85 | Integer.parseInt(hex,16) 86 | } 87 | 88 | private String swapEndianHex(String hex) { 89 | reverseArray(hex.decodeHex()).encodeHex() 90 | } 91 | 92 | private byte[] reverseArray(byte[] array) { 93 | int i = 0; 94 | int j = array.length - 1; 95 | byte tmp; 96 | while (j > i) { 97 | tmp = array[j]; 98 | array[j] = array[i]; 99 | array[i] = tmp; 100 | j--; 101 | i++; 102 | } 103 | return array 104 | } 105 | -------------------------------------------------------------------------------- /Zigbee_Toggle_Switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | * GE/Jasco ZigBee Switch 14 | * 15 | * Author: SmartThings 16 | * Date: 2015-07-01 17 | */ 18 | 19 | metadata { 20 | // Automatically generated. Make future change here. 21 | definition (name: "ZigBee Toggle Switch", namespace: "smartthings", author: "SmartThings") { 22 | capability "Switch" 23 | capability "Configuration" 24 | capability "Refresh" 25 | capability "Actuator" 26 | capability "Sensor" 27 | 28 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853" 29 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856" 30 | } 31 | 32 | // simulator metadata 33 | simulator { 34 | // status messages 35 | status "on": "on/off: 1" 36 | status "off": "on/off: 0" 37 | 38 | // reply messages 39 | reply "zcl on-off on": "on/off: 1" 40 | reply "zcl on-off off": "on/off: 0" 41 | } 42 | 43 | // UI tile definitions 44 | tiles { 45 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { 46 | state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" 47 | state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" 48 | state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" 49 | state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" 50 | } 51 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 52 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 53 | } 54 | 55 | main "switch" 56 | details(["switch", "refresh"]) 57 | } 58 | } 59 | 60 | // Parse incoming device messages to generate events 61 | def parse(String description) { 62 | log.debug "description is $description" 63 | 64 | def finalResult = isKnownDescription(description) 65 | if (finalResult != "false") { 66 | log.info finalResult 67 | if (finalResult.type == "update") { 68 | log.info "$device updates: ${finalResult.value}" 69 | } 70 | else if (finalResult.type == "power") { 71 | def powerValue = (finalResult.value as Integer)/10 72 | sendEvent(name: "power", value: powerValue) 73 | 74 | /* 75 | Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 76 | 77 | power level is an integer. The exact power level with correct units needs to be handled in the device type 78 | to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 79 | */ 80 | } 81 | else { 82 | sendEvent(name: finalResult.type, value: finalResult.value) 83 | } 84 | } 85 | else { 86 | log.warn "DID NOT PARSE MESSAGE for description : $description" 87 | log.debug parseDescriptionAsMap(description) 88 | } 89 | } 90 | 91 | // Commands to device 92 | def zigbeeCommand(cluster, attribute){ 93 | "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" 94 | } 95 | 96 | def off() { 97 | zigbeeCommand("6", "2") 98 | } 99 | 100 | def on() { 101 | zigbeeCommand("6", "2") 102 | } 103 | 104 | def refresh() { 105 | [ 106 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0" 107 | ] 108 | 109 | } 110 | 111 | def configure() { 112 | onOffConfig() + refresh() 113 | } 114 | 115 | 116 | private getEndpointId() { 117 | new BigInteger(device.endpointId, 16).toString() 118 | } 119 | 120 | private hex(value, width=2) { 121 | def s = new BigInteger(Math.round(value).toString()).toString(16) 122 | while (s.size() < width) { 123 | s = "0" + s 124 | } 125 | s 126 | } 127 | 128 | private String swapEndianHex(String hex) { 129 | reverseArray(hex.decodeHex()).encodeHex() 130 | } 131 | 132 | private Integer convertHexToInt(hex) { 133 | Integer.parseInt(hex,16) 134 | } 135 | 136 | //Need to reverse array of size 2 137 | private byte[] reverseArray(byte[] array) { 138 | byte tmp; 139 | tmp = array[1]; 140 | array[1] = array[0]; 141 | array[0] = tmp; 142 | return array 143 | } 144 | 145 | def parseDescriptionAsMap(description) { 146 | if (description?.startsWith("read attr -")) { 147 | (description - "read attr - ").split(",").inject([:]) { map, param -> 148 | def nameAndValue = param.split(":") 149 | map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] 150 | } 151 | } 152 | else if (description?.startsWith("catchall: ")) { 153 | def seg = (description - "catchall: ").split(" ") 154 | def zigbeeMap = [:] 155 | zigbeeMap += [raw: (description - "catchall: ")] 156 | zigbeeMap += [profileId: seg[0]] 157 | zigbeeMap += [clusterId: seg[1]] 158 | zigbeeMap += [sourceEndpoint: seg[2]] 159 | zigbeeMap += [destinationEndpoint: seg[3]] 160 | zigbeeMap += [options: seg[4]] 161 | zigbeeMap += [messageType: seg[5]] 162 | zigbeeMap += [dni: seg[6]] 163 | zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] 164 | zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] 165 | zigbeeMap += [manufacturerId: seg[9]] 166 | zigbeeMap += [command: seg[10]] 167 | zigbeeMap += [direction: seg[11]] 168 | zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { 169 | it.join('') 170 | } : []] 171 | 172 | zigbeeMap 173 | } 174 | } 175 | 176 | def isKnownDescription(description) { 177 | if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { 178 | def descMap = parseDescriptionAsMap(description) 179 | if (descMap.cluster == "0006" || descMap.clusterId == "0006") { 180 | isDescriptionOnOff(descMap) 181 | } 182 | else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){ 183 | isDescriptionPower(descMap) 184 | } 185 | else { 186 | return "false" 187 | } 188 | } 189 | else if(description?.startsWith("on/off:")) { 190 | def switchValue = description?.endsWith("1") ? "on" : "off" 191 | return [type: "switch", value : switchValue] 192 | } 193 | else { 194 | return "false" 195 | } 196 | } 197 | 198 | def isDescriptionOnOff(descMap) { 199 | def switchValue = "undefined" 200 | if (descMap.cluster == "0006") { //cluster info from read attr 201 | value = descMap.value 202 | if (value == "01"){ 203 | switchValue = "on" 204 | } 205 | else if (value == "00"){ 206 | switchValue = "off" 207 | } 208 | } 209 | else if (descMap.clusterId == "0006") { 210 | //cluster info from catch all 211 | //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 212 | //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 213 | if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ 214 | switchValue = "on" 215 | } 216 | else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ 217 | switchValue = "off" 218 | } 219 | else if(descMap.command=="07"){ 220 | return [type: "update", value : "switch (0006) capability configured successfully"] 221 | } 222 | } 223 | 224 | if (switchValue != "undefined"){ 225 | return [type: "switch", value : switchValue] 226 | } 227 | else { 228 | return "false" 229 | } 230 | 231 | } 232 | 233 | def isDescriptionPower(descMap) { 234 | def powerValue = "undefined" 235 | if (descMap.cluster == "0702") { 236 | if (descMap.attrId == "0400") { 237 | powerValue = convertHexToInt(descMap.value) 238 | } 239 | } 240 | else if (descMap.clusterId == "0702") { 241 | if(descMap.command=="07"){ 242 | return [type: "update", value : "power (0702) capability configured successfully"] 243 | } 244 | } 245 | 246 | if (powerValue != "undefined"){ 247 | return [type: "power", value : powerValue] 248 | } 249 | else { 250 | return "false" 251 | } 252 | } 253 | 254 | 255 | def onOffConfig() { 256 | [ 257 | "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", 258 | "zcl global send-me-a-report 6 0 0x10 0 600 {01}", 259 | "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" 260 | ] 261 | } 262 | 263 | String convertToHexString(value, width=2) { 264 | def s = new BigInteger(Math.round(value).toString()).toString(16) 265 | while (s.size() < width) { 266 | s = "0" + s 267 | } 268 | s 269 | } 270 | -------------------------------------------------------------------------------- /devicetypes/sticks18/Readme.txt: -------------------------------------------------------------------------------- 1 | Repo for SmartThings device handlers. Directory structure configured to work with direct IDE integration directly into SmartThings. 2 | -------------------------------------------------------------------------------- /devicetypes/sticks18/dresden-fls-pp.src/dresden-fls-pp.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Dresden RGBW Lightstrip controller 3 | * 4 | * Removed the color adjuster. Added parsers for level and hue 5 | * 6 | * 7 | * Copyright 2015 SmartThings 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 10 | * in compliance with the License. You may obtain a copy of the License at: 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 15 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 16 | * for the specific language governing permissions and limitations under the License. 17 | * 18 | */ 19 | 20 | metadata { 21 | // Automatically generated. Make future change here. 22 | definition (name: "Dresden FLS-PP", namespace: "sticks18", author: "Scott G") { 23 | capability "Switch Level" 24 | capability "Actuator" 25 | capability "Color Control" 26 | capability "Switch" 27 | capability "Configuration" 28 | capability "Polling" 29 | capability "Refresh" 30 | capability "Sensor" 31 | 32 | command "setWhiteLevel" 33 | command "whiteOn" 34 | command "whiteOff" 35 | 36 | command "setAdjustedColor" 37 | command "startLoop" 38 | command "stopLoop" 39 | command "setLoopTime" 40 | command "setDirection" 41 | 42 | command "alert" 43 | command "toggle" 44 | 45 | // This is a new temporary counter to keep track of no responses 46 | attribute "unreachable", "number" 47 | attribute "colorMode", "string" 48 | attribute "colorName", "string" 49 | attribute "switchColor", "string" 50 | attribute "loopActive", "string" 51 | attribute "loopDirection", "string" 52 | attribute "loopTime", "number" 53 | attribute "alert", "string" 54 | attribute "whiteLevel", "number" 55 | attribute "whiteSwitch", "string" 56 | 57 | fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019" 58 | } 59 | 60 | // simulator metadata 61 | simulator { 62 | // status messages 63 | status "on": "on/off: 1" 64 | status "off": "on/off: 0" 65 | 66 | // reply messages 67 | reply "zcl on-off on": "on/off: 1" 68 | reply "zcl on-off off": "on/off: 0" 69 | } 70 | 71 | // UI tile definitions 72 | tiles (scale: 2){ 73 | multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { 74 | tileAttribute("device.switchColor", key: "PRIMARY_CONTROL") { 75 | attributeState "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 76 | attributeState "Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0000" 77 | attributeState "Brick Red", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff3700" 78 | attributeState "Safety Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff6F00" 79 | attributeState "Dark Orange", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff9900" 80 | attributeState "Amber", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffbf00" 81 | attributeState "Gold", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffe1000" 82 | attributeState "Yellow", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ffff00" 83 | attributeState "Electric Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#bfff00" 84 | attributeState "Lawn Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#88ff00" 85 | attributeState "Bright Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#48ff00" 86 | attributeState "Lime", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff11" 87 | attributeState "Spring Green", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ff6a" 88 | attributeState "Turquoise", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffd0" 89 | attributeState "Aqua", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00ffff" 90 | attributeState "Sky Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00bfff" 91 | attributeState "Dodger Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#007bff" 92 | attributeState "Navy Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0050ff" 93 | attributeState "Blue", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#0000ff" 94 | attributeState "Han Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#3b00ff" 95 | attributeState "Electric Indigo", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#6600ff" 96 | attributeState "Electric Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#b200ff" 97 | attributeState "Orchid Purple", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#e900ff" 98 | attributeState "Magenta", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00dc" 99 | attributeState "Hot Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff00aa" 100 | attributeState "Deep Pink", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff007b" 101 | attributeState "Raspberry", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff0061" 102 | attributeState "Crimson", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#ff003b" 103 | attributeState "White", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 104 | attributeState "Color Loop", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 105 | } 106 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 107 | attributeState "color", action: "setAdjustedColor" 108 | } 109 | tileAttribute ("device.level", key: "SECONDARY_CONTROL") { 110 | attributeState "level", label: 'Level is ${currentValue}%' 111 | } 112 | } 113 | standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 114 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 115 | } 116 | controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 4, inactiveLabel: false) { 117 | state "level", action:"switch level.setLevel" 118 | } 119 | controlTile("whiteLevelSliderControl", "device.whiteLevel", "slider", height: 2, width: 4, inactiveLabel: false, range: "(0..100)") { 120 | state "whiteLevel", action:"setWhiteLevel" 121 | } 122 | standardTile("whiteChannel", "device.whiteSwitch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 123 | state "off", label: "White Off", action: "whiteOn", backgroundColor: "#ffffff" 124 | state "on", label: "White On", action: "whiteOff", backgroundColor: "#79b821" 125 | } 126 | valueTile("colorName", "device.colorName", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 127 | state "colorName", label: '${currentValue}' 128 | } 129 | standardTile("loop", "device.loopActive", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 130 | state "Active", label:'${currentValue}', action: "stopLoop", backgroundColor: "#79b821", nextState: "stoppingLoop" 131 | state "startingLoop", label: "Starting Loop", action: "stopLoop", backgroundColor: "#79b821", nextState: "stoppingLoop" 132 | state "Inactive", label:'${currentValue}', action: "startLoop", backgroundColor: "#ffffff", nextState: "startingLoop" 133 | state "stoppingLoop", label: "Stopping Loop", action: "startLoop", backgroundColor: "#ffffff", nextState: "startingLoop" 134 | } 135 | controlTile("loopTimeControl", "device.loopTime", "slider", height: 2, width: 6, range: "(1..60)", inactiveLabel: false) { 136 | state "loopTime", action: "setLoopTime" 137 | } 138 | standardTile("loopDir", "device.loopDirection", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 139 | state "default", label: '${currentValue}', action: "setDirection" 140 | } 141 | 142 | 143 | main(["switch"]) 144 | details(["switch", "levelSliderControl", "refresh", "whiteLevelSliderControl", "whiteChannel", "colorName", "loop", "loopDir", "loopTimeControl"]) 145 | } 146 | } 147 | 148 | // Parse incoming device messages to generate events 149 | def parse(String description) { 150 | log.info "description is $description" 151 | 152 | sendEvent(name: "unreachable", value: 0) 153 | if (device.currentValue("loopActive") == "Active") { 154 | } 155 | else { 156 | if (description?.startsWith("catchall:")) { 157 | if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) 158 | { 159 | def result = createEvent(name: "switch", value: "on") 160 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 161 | log.debug "Parse returned ${result?.descriptionText}" 162 | return result 163 | } 164 | else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) 165 | { 166 | if(!(description?.startsWith("catchall: 0104 0300") || description?.startsWith("catchall: 0104 0008"))){ 167 | def result = createEvent(name: "switch", value: "off") 168 | sendEvent(name: "switchColor", value: "off", displayed: false) 169 | log.debug "Parse returned ${result?.descriptionText}" 170 | return result 171 | } 172 | } 173 | } 174 | else if (description?.startsWith("read attr -")) { 175 | def descMap = parseDescriptionAsMap(description) 176 | // log.trace "descMap : $descMap" 177 | 178 | if (descMap.cluster == "0300") { 179 | if(descMap.attrId == "0000"){ //Hue Attribute 180 | def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 181 | log.debug "Hue value returned is $hueValue" 182 | def colorName = getColorName(hueValue) 183 | sendEvent(name: "colorName", value: colorName) 184 | if (device.currentValue("switch") == "on") { sendEvent(name: "switchColor", value: ( device.currentValue("colorMode") == "White" ? "White" : device.currentValue("colorName")), displayed: false) } 185 | sendEvent(name: "hue", value: hueValue, displayed:false) 186 | } 187 | else if(descMap.attrId == "0001"){ //Saturation Attribute 188 | def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) 189 | log.debug "Saturation from refresh is $saturationValue" 190 | sendEvent(name: "saturation", value: saturationValue, displayed:false) 191 | } 192 | } 193 | else if(descMap.cluster == "0008"){ 194 | def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) 195 | log.debug "dimmer value is $dimmerValue" 196 | (descMap.sourceEndpoint == "0A" ? sendEvent(name: "level", value: dimmerValue) : sendEvent(name: "whiteLevel", value: dimmervalue)) 197 | } 198 | } 199 | else { 200 | def name = description?.startsWith("on/off: ") ? "switch" : null 201 | if (name == "switch") { 202 | def value = (description?.endsWith(" 1") ? "on" : "off") 203 | log.debug value 204 | sendEvent(name: "switchColor", value: (value == "off" ? "off" : device.currentValue("colorName")), displayed: false) 205 | } 206 | else { def value = null } 207 | def result = createEvent(name: name, value: value) 208 | log.debug "Parse returned ${result?.descriptionText}" 209 | return result 210 | } 211 | } 212 | } 213 | 214 | def parseDescriptionAsMap(description) { 215 | (description - "read attr - ").split(",").inject([:]) { map, param -> 216 | def nameAndValue = param.split(":") 217 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 218 | } 219 | } 220 | 221 | def on(onTime = null) { 222 | // just assume it works for now 223 | log.debug "on()" 224 | sendEvent(name: "switch", value: "on") 225 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 226 | 227 | if (onTime) { 228 | def newTime = onTime * 10 229 | def finalHex = swapEndianHex(hexF(newTime, 4)) 230 | runIn(onTime, refresh) 231 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0x42 {00 ${finalHex} 0000}" 232 | } 233 | else { 234 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 235 | } 236 | } 237 | 238 | def off() { 239 | // just assume it works for now 240 | log.debug "off()" 241 | sendEvent(name: "loopActive", value: "Inactive") 242 | sendEvent(name: "switch", value: "off") 243 | sendEvent(name: "switchColor", value: "off", displayed: false) 244 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 245 | } 246 | 247 | def toggle() { 248 | if (device.currentValue("switch") == "on") { 249 | sendEvent(name: "switch", value: "off") 250 | sendEvent(name: "switchColor", value: "off") 251 | } 252 | else { 253 | sendEvent(name: "switch", value: "on") 254 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 255 | } 256 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 2 {}" 257 | } 258 | 259 | 260 | 261 | def alert(action) { 262 | def value = "00" 263 | def valid = true 264 | switch(action) { 265 | case "Blink": 266 | value = "00" 267 | break 268 | case "Breathe": 269 | value = "01" 270 | break 271 | case "Okay": 272 | value = "02" 273 | break 274 | case "Stop": 275 | value = "ff" 276 | break 277 | default: 278 | valid = false 279 | break 280 | } 281 | if (valid) { 282 | log.debug "Alert: ${action}, Value: ${value}" 283 | sendEvent(name: "alert", value: action) 284 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 3 0x40 {${value} 00}" 285 | } 286 | else { log.debug "Invalid action" } 287 | } 288 | 289 | def setDirection() { 290 | def direction = (device.currentValue("loopDirection") == "Down" ? "Up" : "Down") 291 | log.trace direction 292 | sendEvent(name: "loopDirection", value: direction) 293 | if (device.currentValue("loopActive") == "Active") { 294 | def dirHex = (direction == "Down" ? "00" : "01") 295 | log.trace dirHex 296 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {02 01 ${dirHex} 0000 0000}" 297 | } 298 | } 299 | 300 | def setLoopTime(value) { 301 | sendEvent(name:"loopTime", value: value) 302 | if (device.currentValue("loopActive") == "Active") { 303 | def finTime = swapEndianHex(hexF(value, 4)) 304 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {04 01 00 ${finTime} 0000}" 305 | } 306 | } 307 | 308 | def startLoop(Map params) { 309 | // direction either increments or decrements the hue value: "Up" will increment, "Down" will decrement 310 | def direction = (device.currentValue("loopDirection") != null ? (device.currentValue("loopDirection") == "Down" ? "00" : "01") : "00") 311 | log.trace direction 312 | if (params?.direction != null) { 313 | direction = (params.direction == "Down" ? "00" : "01") 314 | sendEvent(name: "loopDirection", value: params.direction ) 315 | } 316 | log.trace direction 317 | 318 | // time parameter is the time in seconds for a full loop 319 | def cycle = (device.currentValue("loopTime") != null ? device.currentValue("loopTime") : 2) 320 | log.trace cycle 321 | if (params?.time != null) { 322 | cycle = params.time 323 | sendEvent(name:"loopTime", value: cycle) 324 | } 325 | log.trace cycle 326 | def finTime = swapEndianHex(hexF(cycle, 4)) 327 | log.trace finTime 328 | 329 | def cmds = [] 330 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 331 | cmds << "delay 200" 332 | 333 | sendEvent(name: "switchColor", value: "Color Loop", displayed: false) 334 | sendEvent(name: "loopActive", value: "Active") 335 | 336 | if (params?.hue != null) { 337 | 338 | // start hue was specified, so convert to enhanced hue and start loop from there 339 | def sHue = Math.min(Math.round(params.hue * 255 / 100), 255) 340 | finHue = swapEndianHex(hexF(sHue, 4)) 341 | log.debug "activating color loop from specified hue" 342 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {0F 01 ${direction} ${finTime} ${sHue}}" 343 | 344 | } 345 | else { 346 | 347 | // start hue was not specified, so start loop from current hue updating direction and time 348 | log.debug "activating color loop from current hue" 349 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {07 02 ${direction} ${finTime} 0000}" 350 | 351 | } 352 | cmds 353 | } 354 | 355 | def stopLoop() { 356 | 357 | log.debug "deactivating color loop" 358 | def cmds = [ 359 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x44 {01 00 00 0000 0000}", "delay 200", 360 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 200", 361 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 200", 362 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 200", 363 | "st rattr 0x${device.deviceNetworkId} 0x0B 8 0" 364 | ] 365 | sendEvent(name: "loopActive", value: "Inactive") 366 | 367 | cmds 368 | 369 | } 370 | 371 | def whiteOn() { 372 | sendEvent(name: "whiteSwitch", value: "on") 373 | "st cmd 0x${device.deviceNetworkId} 0x0B 6 1 {}" 374 | } 375 | 376 | def whiteOff() { 377 | sendEvent(name: "whiteSwitch", value: "off") 378 | "st cmd 0x${device.deviceNetworkId} 0x0B 6 0 {}" 379 | } 380 | 381 | def setWhiteLevel(value, duration = 20) { 382 | log.trace "setWhiteLevel($value)" 383 | def transitionTime = swapEndianHex(hexF(duration,4)) 384 | 385 | def cmds = [] 386 | 387 | sendEvent(name: "level", value: value) 388 | def level = hex(value * 2.55) 389 | if(value == 1) { level = hex(1) } 390 | cmds << "st cmd 0x${device.deviceNetworkId} 0x0B 8 4 {${level} ${transitionTime}}" 391 | 392 | //log.debug cmds 393 | cmds 394 | } 395 | 396 | def setHue(value, duration = 20) { 397 | def max = 0xfe 398 | def transitionTime = swapEndianHex(hexF(duration,4)) 399 | 400 | // log.trace "setHue($value)" 401 | sendEvent(name: "hue", value: value) 402 | def scaledValue = Math.round(value * max / 100.0) 403 | def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 ${transitionTime}}" 404 | //log.info cmd 405 | cmd 406 | } 407 | 408 | def setAdjustedColor(value, duration = 20) { 409 | // log.debug "setAdjustedColor: ${value}" 410 | def adjusted = value + [:] 411 | adjusted.level = null // needed because color picker always sends 100 412 | setColor(adjusted, duration) 413 | } 414 | 415 | def setColor(value, duration = 20){ 416 | log.trace "setColor($value)" 417 | 418 | def transitionTime = swapEndianHex(hexF(duration,4)) 419 | 420 | def max = 0xfe 421 | if (value.hue == 0 && value.saturation == 0) { setColorTemperature(3500) } 422 | else if (value.red == 255 && value.blue == 185 && value.green == 255) { setColorTemperature(2700) } 423 | else { 424 | if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} 425 | 426 | def colorName = getColorName(value.hue) 427 | sendEvent(name: "colorName", value: colorName) 428 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 429 | 430 | log.debug "color name is : $colorName" 431 | sendEvent(name: "hue", value: value.hue, displayed:false) 432 | sendEvent(name: "saturation", value: value.saturation, displayed:false) 433 | def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) 434 | def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) 435 | 436 | def cmd = [] 437 | if (value.switch != "off" && device.latestValue("switch") == "off") { 438 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 439 | cmd << "delay 150" 440 | } 441 | 442 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x06 {${scaledHueValue} ${scaledSatValue} ${transitionTime}}" 443 | 444 | if (value.level) { 445 | state.levelValue = value.level 446 | sendEvent(name: "level", value: value.level) 447 | def level = hex(value.level * 255 / 100) 448 | cmd << "delay 200" 449 | cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} ${transitionTime}}" 450 | } 451 | 452 | if (value.switch == "off") { 453 | cmd << "delay 150" 454 | cmd << off() 455 | } 456 | 457 | cmd 458 | } 459 | } 460 | 461 | def setSaturation(value, duration = 20) { 462 | 463 | def transitionTime = swapEndianHex(hexF(duration,4)) 464 | def max = 0xfe 465 | // log.trace "setSaturation($value)" 466 | sendEvent(name: "saturation", value: value) 467 | def scaledValue = Math.round(value * max / 100.0) 468 | def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} ${transitionTime}}" 469 | //log.info cmd 470 | cmd 471 | } 472 | 473 | def refresh() { 474 | 475 | // Ping the device with color as to not get out of sync 476 | [ 477 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", 478 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", 479 | "st rattr 0x${device.deviceNetworkId} 0x0B 8 0", "delay 500", 480 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1","delay 500", 481 | "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0","delay 500", 482 | "st wattr 0x${device.deviceNetworkId} ${endpointId} 8 0x10 0x21 {0015}" 483 | 484 | ] 485 | } 486 | 487 | def poll(){ 488 | log.debug "Poll is calling refresh" 489 | refresh() 490 | } 491 | 492 | def configure(){ 493 | log.debug "Initiating configuration reporting and binding" 494 | 495 | [ 496 | zigbee.configSetup("6","0","0x10","0","60","{}"), "delay 1000", 497 | zigbee.configSetup("8","0","0x20","5","600","{10}"), "delay 1500", 498 | 499 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", 500 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 1000", 501 | "zdo bind 0x${device.deviceNetworkId} 0x0B 1 8 {${device.zigbeeId}} {}", "delay 1000", 502 | "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}" 503 | ] 504 | 505 | } 506 | 507 | //input Hue Integer values; returns color name for saturation 100% 508 | private getColorName(hueValue){ 509 | if(hueValue>360 || hueValue<0) 510 | return 511 | 512 | hueValue = Math.round(hueValue / 100 * 360) 513 | 514 | log.debug "hue value is $hueValue" 515 | 516 | def colorName = "Color Mode" 517 | if(hueValue>=0 && hueValue <= 4){ 518 | colorName = "Red" 519 | } 520 | else if (hueValue>=5 && hueValue <=21 ){ 521 | colorName = "Brick Red" 522 | } 523 | else if (hueValue>=22 && hueValue <=30 ){ 524 | colorName = "Safety Orange" 525 | } 526 | else if (hueValue>=31 && hueValue <=40 ){ 527 | colorName = "Dark Orange" 528 | } 529 | else if (hueValue>=41 && hueValue <=49 ){ 530 | colorName = "Amber" 531 | } 532 | else if (hueValue>=50 && hueValue <=56 ){ 533 | colorName = "Gold" 534 | } 535 | else if (hueValue>=57 && hueValue <=65 ){ 536 | colorName = "Yellow" 537 | } 538 | else if (hueValue>=66 && hueValue <=83 ){ 539 | colorName = "Electric Lime" 540 | } 541 | else if (hueValue>=84 && hueValue <=93 ){ 542 | colorName = "Lawn Green" 543 | } 544 | else if (hueValue>=94 && hueValue <=112 ){ 545 | colorName = "Bright Green" 546 | } 547 | else if (hueValue>=113 && hueValue <=135 ){ 548 | colorName = "Lime" 549 | } 550 | else if (hueValue>=136 && hueValue <=166 ){ 551 | colorName = "Spring Green" 552 | } 553 | else if (hueValue>=167 && hueValue <=171 ){ 554 | colorName = "Turquoise" 555 | } 556 | else if (hueValue>=172 && hueValue <=187 ){ 557 | colorName = "Aqua" 558 | } 559 | else if (hueValue>=188 && hueValue <=203 ){ 560 | colorName = "Sky Blue" 561 | } 562 | else if (hueValue>=204 && hueValue <=217 ){ 563 | colorName = "Dodger Blue" 564 | } 565 | else if (hueValue>=218 && hueValue <=223 ){ 566 | colorName = "Navy Blue" 567 | } 568 | else if (hueValue>=224 && hueValue <=251 ){ 569 | colorName = "Blue" 570 | } 571 | else if (hueValue>=252 && hueValue <=256 ){ 572 | colorName = "Han Purple" 573 | } 574 | else if (hueValue>=257 && hueValue <=274 ){ 575 | colorName = "Electric Indigo" 576 | } 577 | else if (hueValue>=275 && hueValue <=289 ){ 578 | colorName = "Electric Purple" 579 | } 580 | else if (hueValue>=290 && hueValue <=300 ){ 581 | colorName = "Orchid Purple" 582 | } 583 | else if (hueValue>=301 && hueValue <=315 ){ 584 | colorName = "Magenta" 585 | } 586 | else if (hueValue>=316 && hueValue <=326 ){ 587 | colorName = "Hot Pink" 588 | } 589 | else if (hueValue>=327 && hueValue <=335 ){ 590 | colorName = "Deep Pink" 591 | } 592 | else if (hueValue>=336 && hueValue <=339 ){ 593 | colorName = "Raspberry" 594 | } 595 | else if (hueValue>=340 && hueValue <=352 ){ 596 | colorName = "Crimson" 597 | } 598 | else if (hueValue>=353 && hueValue <=360 ){ 599 | colorName = "Red" 600 | } 601 | 602 | colorName 603 | } 604 | 605 | // adding duration to enable transition time adjustments 606 | def setLevel(value, duration = 21) { 607 | log.trace "setLevel($value)" 608 | def transitionTime = swapEndianHex(hexF(duration,4)) 609 | 610 | def cmds = [] 611 | 612 | if (value == 0) { 613 | sendEvent(name: "switch", value: "off") 614 | sendEvent(name: "switchColor", value: "off", displayed: false) 615 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 616 | } 617 | else if (device.currentValue("switch") == "off") { 618 | sendEvent(name: "switch", value: "on") 619 | sendEvent(name: "switchColor", value: device.currentValue("colorName"), displayed: false) 620 | } 621 | 622 | sendEvent(name: "level", value: value) 623 | def level = hex(value * 2.55) 624 | if(value == 1) { level = hex(1) } 625 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} ${transitionTime}}" 626 | 627 | //log.debug cmds 628 | cmds 629 | } 630 | 631 | private getEndpointId() { 632 | new BigInteger(device.endpointId, 16).toString() 633 | } 634 | 635 | private hex(value, width=2) { 636 | def s = new BigInteger(Math.round(value).toString()).toString(16) 637 | while (s.size() < width) { 638 | s = "0" + s 639 | } 640 | s 641 | } 642 | 643 | private evenHex(value){ 644 | def s = new BigInteger(Math.round(value).toString()).toString(16) 645 | while (s.size() % 2 != 0) { 646 | s = "0" + s 647 | } 648 | s 649 | } 650 | 651 | //Need to reverse array of size 2 652 | private byte[] reverseArray(byte[] array) { 653 | byte tmp; 654 | tmp = array[1]; 655 | array[1] = array[0]; 656 | array[0] = tmp; 657 | return array 658 | } 659 | 660 | private hexF(value, width) { 661 | def s = new BigInteger(Math.round(value).toString()).toString(16) 662 | while (s.size() < width) { 663 | s = "0" + s 664 | } 665 | s 666 | } 667 | 668 | private String swapEndianHex(String hex) { 669 | reverseArray(hex.decodeHex()).encodeHex() 670 | } 671 | 672 | private Integer convertHexToInt(hex) { 673 | Integer.parseInt(hex,16) 674 | } 675 | -------------------------------------------------------------------------------- /devicetypes/sticks18/ge-link-pro.src/ge-link-pro.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * GE Link Bulb 3 | * 4 | * Copyright 2014 SmartThings 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart, and others 16 | * 17 | ****************************************************************************** 18 | * Changes 19 | ****************************************************************************** 20 | * 21 | * Change 1: 2014-10-10 (wackford) 22 | * Added setLevel event so subscriptions to the event will work 23 | * Change 2: 2014-12-10 (jscgs350 using Sticks18's code and effort!) 24 | * Modified parse section to properly identify bulb status in the app when manually turned on by a physical switch 25 | * Change 3: 2014-12-12 (jscgs350, Sticks18's) 26 | * Modified to ensure dimming was smoother, and added fix for dimming below 7 27 | * Change 4: 2014-12-14 Part 1 (Sticks18) 28 | * Modified to ignore unnecessary level change responses to prevent level skips 29 | * Change 5: 2014-12-14 Part 2 (Sticks18, jscgs350) 30 | * Modified to clean up trace&debug logging, added new code from @sticks18 for parsing "on/off" to determine if the bulb is manually turned on and immediately update the app 31 | * Change 6: 2015-01-02 (Sticks18) 32 | * Modified to allow dim rate in Preferences. Added ability to dim during On/Off commands and included this option in Preferences. Defaults are "Normal" and no dim for On/Off. 33 | * Change 7: 2015-01-09 (tslagle13) 34 | * dimOnOff is was boolean, and switched to enum. Properly update "rampOn" and "rampOff" when refreshed or a polled (dim transition for On/Off commands) 35 | * Change 8: 2015-03-06 (Juan Risso) 36 | * Slider range from 0..100 37 | * Change 9: 2015-03-06 (Juan Risso) 38 | * Setlevel -> value to integer (to prevent smartapp calling this function from not working). 39 | * Change 10: 2015-09-06 (Sticks18) 40 | * Modified tile layout to make use of new multiattribute tile for dimming. Added dim adjustment when sending setLevel() if bulb is off, so it will transition smoothly from 0. 41 | * Change 11: 2016-01-20 (Sticks18) 42 | * Completely overhauled the code for the new zigbee implementation layers. 43 | * 44 | * 45 | */ 46 | metadata { 47 | definition (name: "GE Link Pro", namespace: "sticks18", author: "Scott G") { 48 | 49 | capability "Actuator" 50 | capability "Configuration" 51 | capability "Refresh" 52 | capability "Sensor" 53 | capability "Switch" 54 | capability "Switch Level" 55 | capability "Polling" 56 | 57 | command "alert" 58 | command "toggle" 59 | 60 | attribute "attDimRate", "string" 61 | attribute "attDimOnOff", "string" 62 | attribute "alert", "string" 63 | 64 | fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" 65 | } 66 | 67 | // UI tile definitions 68 | tiles(scale: 2) { 69 | multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true, canChangeBackground: true) { 70 | tileAttribute("device.switch", key: "PRIMARY_CONTROL") { 71 | attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" 72 | attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "turningOff" 73 | attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" 74 | attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState: "turningOff" 75 | } 76 | tileAttribute("device.level", key: "SLIDER_CONTROL") { 77 | attributeState "level", action:"switch level.setLevel" 78 | } 79 | tileAttribute("level", key: "SECONDARY_CONTROL") { 80 | attributeState "level", label: 'Light dimmed to ${currentValue}%' 81 | } 82 | } 83 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 84 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 85 | } 86 | valueTile("attDimRate", "device.attDimRate", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { 87 | state "attDimRate", label: 'Dim rate: ${currentValue}' 88 | } 89 | valueTile("attDimOnOff", "device.attDimOnOff", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { 90 | state "attDimOnOff", label: 'Dim for on/off: ${currentValue}' 91 | } 92 | controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 6, inactiveLabel: false, range: "(0..100)") { 93 | state "level", action:"switch level.setLevel" 94 | } 95 | 96 | 97 | main "switch" 98 | details(["switch","attDimRate", "refresh", "attDimOnOff","levelSliderControl"]) 99 | } 100 | 101 | preferences { 102 | 103 | input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) 104 | input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) 105 | 106 | } 107 | } 108 | 109 | // Parse incoming device messages to generate events 110 | def parse(String description) { 111 | log.debug "description is $description" 112 | 113 | def resultMap = zigbee.getKnownDescription(description) 114 | if (resultMap) { 115 | log.info resultMap 116 | if (resultMap.type == "update") { 117 | log.info "$device updates: ${resultMap.value}" 118 | } 119 | else { 120 | sendEvent(name: resultMap.type, value: resultMap.value) 121 | } 122 | } 123 | else { 124 | log.warn "DID NOT PARSE MESSAGE for description : $description" 125 | log.debug zigbee.parseDescriptionAsMap(description) 126 | } 127 | 128 | } 129 | 130 | def alert(alertTime = 7) { 131 | log.debug alertTime 132 | def payload = swapEndianHex(zigbee.convertToHexString(alertTime,4)) 133 | zigbee.command(0x0003, 0x00, payload) 134 | } 135 | 136 | def toggle() { 137 | zigbee.command(0x0006, 0x02) 138 | } 139 | 140 | def poll() { refresh() } 141 | 142 | def updated() { 143 | 144 | sendEvent( name: "attDimRate", value: "${dimRate}" ) 145 | sendEvent( name: "attDimOnOff", value: "${dimOnOff}" ) 146 | 147 | state.dOnOff = "0000" 148 | 149 | if (dimRate) { 150 | 151 | switch (dimRate) 152 | { 153 | 154 | case "Instant": 155 | 156 | state.rate = "0000" 157 | if (dimOnOff) { state.dOnOff = "0000"} 158 | break 159 | 160 | case "Normal": 161 | 162 | state.rate = "1500" 163 | if (dimOnOff) { state.dOnOff = "0015"} 164 | break 165 | 166 | case "Slow": 167 | 168 | state.rate = "2500" 169 | if (dimOnOff) { state.dOnOff = "0025"} 170 | break 171 | 172 | case "Very Slow": 173 | 174 | state.rate = "3500" 175 | if (dimOnOff) { state.dOnOff = "0035"} 176 | break 177 | 178 | } 179 | 180 | } 181 | 182 | else { 183 | 184 | state.rate = "1500" 185 | state.dOnOff = "0000" 186 | 187 | } 188 | 189 | if (dimOnOff == "Yes"){ 190 | switch (dimOnOff){ 191 | case "InstantOnOff": 192 | 193 | state.rate = "0000" 194 | if (state.rate == "0000") { state.dOnOff = "0000"} 195 | break 196 | 197 | case "NormalOnOff": 198 | 199 | state.rate = "1500" 200 | if (state.rate == "1500") { state.dOnOff = "0015"} 201 | break 202 | 203 | case "SlowOnOff": 204 | 205 | state.rate = "2500" 206 | if (state.rate == "2500") { state.dOnOff = "0025"} 207 | break 208 | 209 | case "Very SlowOnOff": 210 | 211 | state.rate = "3500" 212 | if (state.rate == "3500") { state.dOnOff = "0035"} 213 | break 214 | 215 | } 216 | 217 | } 218 | else{ 219 | state.dOnOff = "0000" 220 | } 221 | 222 | zigbee.writeAttribute(0x0008, 0x10, 0x21, state.dOnOff) 223 | 224 | } 225 | 226 | def on(onTime = null) { 227 | if (onTime) { 228 | def newTime = onTime * 10 229 | def finalHex = swapEndianHex(zigbee.convertToHexString(newTime, 4)) 230 | zigbee.command(0x0006, 0x42, "00", finalHex, "0000") 231 | } 232 | else { 233 | zigbee.on() 234 | } 235 | 236 | } 237 | 238 | def off() { 239 | zigbee.off() 240 | } 241 | 242 | def refresh() { 243 | 244 | zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.writeAttribute(0x0008, 0x10, 0x21, state.dOnOff) + zigbee.onOffConfig() + zigbee.levelConfig() 245 | 246 | } 247 | 248 | def setLevel(value, duration = 2.0) { 249 | 250 | value = (value * 255 / 100) 251 | def level = zigbee.convertToHexString(value,2); 252 | 253 | duration = duration * 10 254 | def tranTime = swapEndianHex(zigbee.convertToHexString(duration, 4)) 255 | 256 | zigbee.command(0x0008, 0x04, level, tranTime) 257 | 258 | } 259 | 260 | def configure() { 261 | log.debug "Configuring Reporting and Bindings." 262 | zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() 263 | } 264 | 265 | private hex(value, width=2) { 266 | def s = new BigInteger(Math.round(value).toString()).toString(16) 267 | while (s.size() < width) { 268 | s = "0" + s 269 | } 270 | s 271 | } 272 | 273 | private Integer convertHexToInt(hex) { 274 | Integer.parseInt(hex,16) 275 | } 276 | 277 | private String swapEndianHex(String hex) { 278 | reverseArray(hex.decodeHex()).encodeHex() 279 | } 280 | 281 | private byte[] reverseArray(byte[] array) { 282 | int i = 0; 283 | int j = array.length - 1; 284 | byte tmp; 285 | while (j > i) { 286 | tmp = array[j]; 287 | array[j] = array[i]; 288 | array[i] = tmp; 289 | j--; 290 | i++; 291 | } 292 | return array 293 | } 294 | -------------------------------------------------------------------------------- /devicetypes/sticks18/lutron-conn-bulb.src/lutron-conn-bulb.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 12 | * for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | /* Lutron Connected Bulb Remote 16 | 17 | Capabilities: 18 | Actuator 19 | Configuration 20 | Polling 21 | Refresh 22 | Switch 23 | Switch Level 24 | 25 | 26 | */ 27 | 28 | metadata { 29 | definition (name: "Lutron Conn Bulb", namespace: "sticks18", author: "Scott G") { 30 | capability "Actuator" 31 | capability "Button" 32 | capability "Configuration" 33 | capability "Sensor" 34 | capability "Refresh" 35 | 36 | command "getGroup" 37 | command "assignGroup" 38 | command "removeGroup" 39 | command "removeAllGroup" 40 | command "updateGroup" 41 | 42 | attribute "groupId", "String" 43 | 44 | fingerprint profileId: "C05E", deviceId: "0820", inClusters: "0000,1000,FF00,FC44", outClusters: "0000,0003,0004,0005,0006,0008,1000,FF00" 45 | } 46 | 47 | // simulator metadata 48 | simulator { 49 | // status messages 50 | 51 | } 52 | 53 | // UI tile definitions 54 | tiles { 55 | standardTile("button", "device.button", width: 2, height: 2) { 56 | state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" 57 | } 58 | standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 59 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 60 | } 61 | valueTile("group", "device.groupId", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { 62 | state "groupId", label: '${currentValue}' 63 | } 64 | main "button" 65 | details(["button", "group"]) 66 | } 67 | } 68 | // Parse incoming device messages to generate events 69 | def parse(String description) { 70 | log.trace description 71 | } 72 | 73 | def getGroup(zigId, endP) { 74 | 75 | "st cmd 0x${zigId} ${endP} 4 2 {}" 76 | 77 | } 78 | 79 | def assignGroup(zigId, endP, grpId) { 80 | 81 | "st cmd 0x${zigId} ${endP} 4 0 {${grpId}}" 82 | 83 | } 84 | 85 | def removeGroup(zigId, endP, grpId) { 86 | 87 | "st cmd 0x${zigId} ${endP} 4 3 {${grpId}}" 88 | 89 | } 90 | 91 | def removeAllGroup(zigId, endP) { 92 | 93 | "st cmd 0x${zigId} ${endP} 4 4 {}" 94 | 95 | } 96 | 97 | def updateGroup(grpId) { 98 | log.debug "Group Id for this remote is: ${grpId}" 99 | sendEvent(name: "groupId", value: grpId) 100 | } 101 | 102 | def refresh() { 103 | [ 104 | /* "raw 0x1000 {00 01 0C 0000 FE}", "delay 150", 105 | "send 0x${device.deviceNetworkId} 1 1" 106 | "st cmd 0x${device.deviceNetworkId} 1 0x1000 0x41 {01}", "delay 500", 107 | "st cmd 0x${device.deviceNetworkId} 1 0x1000 0x42 {01}", "delay 500", 108 | "st rattr 0x${device.deviceNetworkId} 1 0 1", "delay 500", 109 | "st rattr 0x${device.deviceNetworkId} 1 0 2", "delay 500", 110 | "st rattr 0x${device.deviceNetworkId} 1 0 3", "delay 500", 111 | "st rattr 0x${device.deviceNetworkId} 1 0 4", "delay 500", 112 | "st rattr 0x${device.deviceNetworkId} 1 0 5" */ 113 | ] 114 | 115 | } 116 | 117 | def configure() { 118 | 119 | log.debug "configure" 120 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 121 | log.debug "Confuguring Reporting and Bindings." 122 | def configCmds = [ 123 | 124 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", 125 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", 126 | 127 | ] 128 | return configCmds + refresh 129 | } 130 | 131 | 132 | private getEndpointId() { 133 | new BigInteger(device.endpointId, 16).toString() 134 | } 135 | 136 | //Need to reverse array of size 2 137 | private byte[] reverseArray(byte[] array) { 138 | byte tmp; 139 | tmp = array[1]; 140 | array[1] = array[0]; 141 | array[0] = tmp; 142 | return array 143 | } 144 | 145 | private String swapEndianHex(String hex) { 146 | reverseArray(hex.decodeHex()).encodeHex() 147 | } 148 | 149 | -------------------------------------------------------------------------------- /devicetypes/sticks18/petsafe-smart-door.src/petsafe-smart-door.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * PetSafe Smart Door 3 | * 4 | * Copyright 2015 Scott Gibson 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * 16 | * 17 | */ 18 | metadata { 19 | // Automatically generated. Make future change here. 20 | definition (name: "PetSafe Smart Door", namespace: "sticks18", author: "Scott Gibson") { 21 | capability "Battery" 22 | capability "Polling" 23 | capability "Actuator" 24 | capability "Refresh" 25 | capability "Lock" 26 | 27 | fingerprint profileId: "0104", inClusters: "0000 0001 0003 0009 000A 0101", outClusters: "0000 0001 0003 0009 000A 0101" 28 | } 29 | 30 | 31 | simulator { 32 | /* // status "locked": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" 33 | // status "unlocked": "command: 9881, payload: 00 62 03 00 00 00 FE FE" 34 | 35 | // reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" 36 | // reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 00 FE FE" */ 37 | } 38 | 39 | tiles { 40 | standardTile("toggle", "device.lock", width: 2, height: 2) { 41 | state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" 42 | state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" 43 | state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" 44 | state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" 45 | state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" 46 | } 47 | standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { 48 | state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" 49 | } 50 | standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { 51 | state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" 52 | } 53 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { 54 | state "battery", label:'${currentValue}% battery', unit:"" 55 | } 56 | standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") { 57 | state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 58 | } 59 | 60 | main "toggle" 61 | details(["toggle", "lock", "unlock", "battery", "refresh"]) 62 | } 63 | } 64 | 65 | def parse(String description) { 66 | 67 | log.trace description 68 | 69 | def msg = zigbee.parse(description) 70 | log.trace msg 71 | 72 | def results = [] 73 | if (description?.startsWith('catchall:')) { 74 | results = parseCatchAllMessage(description) 75 | } 76 | else if (description?.startsWith('read attr -')) { 77 | results = parseReportAttributeMessage(description) 78 | } 79 | 80 | } 81 | 82 | private parseReportAttributeMessage(String description) { 83 | 84 | Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> 85 | def nameAndValue = param.split(":") 86 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 87 | } 88 | 89 | //log.debug "Desc Map: $descMap" 90 | def results = [] 91 | if (descMap.cluster == "0001" && descMap.attrId == "0020") { 92 | log.debug "Received battery level report" 93 | results = createEvent(getBatteryResult(Integer.parseInt(descMap.value, 16))) 94 | } 95 | else if (descMap.cluster == "0101" && descMap.attrId == "0000") { 96 | log.debug "Received lock status" 97 | def linkText = getLinkText(device) 98 | 99 | if(descMap.value == "01"){ 100 | results = createEvent( name: 'lock' , value: "locked", descriptionText: "${linkText} is locked") 101 | } 102 | else if(descMap.value == "02") { 103 | results = createEvent( name: 'lock' , value: "unlocked", descriptionText: "${linkText} is unlocked") 104 | } 105 | 106 | } 107 | 108 | return results 109 | 110 | } 111 | 112 | private getBatteryResult(rawValue) { 113 | 114 | //log.debug 'Battery' 115 | def linkText = getLinkText(device) 116 | def result = [ name: 'battery' ] 117 | 118 | def volts = rawValue / 10 119 | def descriptionText 120 | if (volts > 6.5) { 121 | result.descriptionText = "${linkText} battery has too much power (${volts} volts)." 122 | } 123 | else { 124 | def minVolts = 4.0 125 | def maxVolts = 6.0 126 | def pct = (volts - minVolts) / (maxVolts - minVolts) 127 | result.value = Math.min(100, (int) pct * 100) 128 | result.descriptionText = "${linkText} battery was ${result.value}%" 129 | } 130 | 131 | return result 132 | 133 | } 134 | 135 | private Map parseCatchAllMessage(String description) { 136 | 137 | def results = [:] 138 | def cluster = zigbee.parse(description) 139 | 140 | if (shouldProcessMessage(cluster)) { 141 | switch(cluster.clusterId) { 142 | case 0x0001: 143 | results << createEvent(getBatteryResult(cluster.data.last())) 144 | break 145 | } 146 | } 147 | 148 | return results 149 | 150 | } 151 | 152 | private boolean shouldProcessMessage(cluster) { 153 | // 0x0B is default response indicating message got through 154 | // 0x07 is bind message 155 | 156 | boolean ignoredMessage = cluster.profileId != 0x0104 || 157 | cluster.command == 0x0B || 158 | cluster.command == 0x07 || 159 | (cluster.data.size() > 0 && cluster.data.first() == 0x3e) 160 | 161 | return !ignoredMessage 162 | 163 | } 164 | 165 | def poll() { 166 | 167 | log.debug "Executing 'poll'" 168 | refresh() 169 | } 170 | 171 | def refresh() { 172 | 173 | [ 174 | "st rattr 0x${device.deviceNetworkId} 0x0C 0x101 0", "delay 500", 175 | "st rattr 0x${device.deviceNetworkId} 0x0C 1 0x20" 176 | ] 177 | 178 | } 179 | 180 | def updated() { 181 | 182 | configure() 183 | 184 | } 185 | 186 | def lock() { 187 | 188 | log.debug "Executing 'lock'" 189 | sendEvent(name: "lock", value: "locked") 190 | "st cmd 0x${device.deviceNetworkId} 0x0C 0x101 0 {}" 191 | 192 | } 193 | 194 | def unlock() { 195 | 196 | log.debug "Executing 'unlock'" 197 | sendEvent(name: "lock", value: "unlocked") 198 | "st cmd 0x${device.deviceNetworkId} 0x0C 0x101 1 {}" 199 | 200 | } 201 | 202 | def configure() { 203 | 204 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 205 | log.debug "Confuguring Reporting and Bindings." 206 | def configCmds = [ 207 | 208 | //Lock Reporting 209 | "zcl global send-me-a-report 0x101 0 0x30 0 3600 {01}", "delay 500", 210 | "send 0x${device.deviceNetworkId} 1 1", "delay 1000", 211 | 212 | //Battery Reporting 213 | "zcl global send-me-a-report 1 0x20 0x20 5 3600 {}", "delay 200", 214 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500", 215 | 216 | "zdo bind 0x${device.deviceNetworkId} 0x0C 1 0x101 {${device.zigbeeId}} {}", "delay 500", 217 | "zdo bind 0x${device.deviceNetworkId} 0x0C 1 1 {${device.zigbeeId}} {}" 218 | 219 | ] 220 | return configCmds + refresh() // send refresh cmds as part of config 221 | } 222 | 223 | private hex(value, width=2) { 224 | def s = new BigInteger(Math.round(value).toString()).toString(16) 225 | while (s.size() < width) { 226 | s = "0" + s 227 | } 228 | s 229 | } 230 | 231 | private Integer convertHexToInt(hex) { 232 | Integer.parseInt(hex,16) 233 | } 234 | 235 | private String swapEndianHex(String hex) { 236 | reverseArray(hex.decodeHex()).encodeHex() 237 | } 238 | 239 | private byte[] reverseArray(byte[] array) { 240 | int i = 0; 241 | int j = array.length - 1; 242 | byte tmp; 243 | while (j > i) { 244 | tmp = array[j]; 245 | array[j] = array[i]; 246 | array[i] = tmp; 247 | j--; 248 | i++; 249 | } 250 | return array 251 | } 252 | 253 | private getEndpointId() { 254 | new BigInteger(device.endpointId, 16).toString() 255 | } 256 | --------------------------------------------------------------------------------