|
| 1 | +/* |
| 2 | + * Copyright 2016 SmartThings |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 5 | + * use this file except in compliance with the License. You may obtain a copy |
| 6 | + * 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 |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | + * License for the specific language governing permissions and limitations |
| 14 | + * under the License. |
| 15 | + */ |
| 16 | +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus |
| 17 | +import physicalgraph.zigbee.zcl.DataType |
| 18 | + |
| 19 | +metadata { |
| 20 | + definition(name: "SmartSense Button", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.022.0000', executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Button", ocfDeviceType: "x.com.st.d.remotecontroller") { |
| 21 | + capability "Configuration" |
| 22 | + capability "Battery" |
| 23 | + capability "Refresh" |
| 24 | + capability "Temperature Measurement" |
| 25 | + capability "Button" |
| 26 | + capability "Holdable Button" |
| 27 | + capability "Health Check" |
| 28 | + capability "Sensor" |
| 29 | + |
| 30 | + command "enrollResponse" |
| 31 | + |
| 32 | + fingerprint inClusters: "0000,0001,0003,0020,0402,0500", outClusters: "0019", manufacturer: "Samjin", model: "button", deviceJoinName: "Button" |
| 33 | + } |
| 34 | + |
| 35 | + simulator { |
| 36 | + status "button 1 pushed": "catchall: 0104 0500 01 01 0140 00 6C3F 00 00 0000 01 01 020000190100" |
| 37 | + } |
| 38 | + |
| 39 | + preferences { |
| 40 | + section { |
| 41 | + image(name: 'educationalcontent', multiple: true, images: [ |
| 42 | + "http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png", |
| 43 | + "http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png", |
| 44 | + "http://cdn.device-gse.smartthings.com/Moisture/Moisture3.png" |
| 45 | + ]) |
| 46 | + } |
| 47 | + section { |
| 48 | + input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph" |
| 49 | + input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + tiles(scale: 2) { |
| 54 | + multiAttributeTile(name: "button", type: "generic", width: 6, height: 4) { |
| 55 | + tileAttribute("device.button", key: "PRIMARY_CONTROL") { |
| 56 | + attributeState "pushed", label: "Pressed", icon:"st.Weather.weather14", backgroundColor:"#53a7c0" |
| 57 | + attributeState "double", label: "Pressed Twice", icon:"st.Weather.weather11", backgroundColor:"#53a7c0" |
| 58 | + attributeState "held", label: "Held", icon:"st.Weather.weather13", backgroundColor:"#53a7c0" |
| 59 | + } |
| 60 | + } |
| 61 | + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { |
| 62 | + state "temperature", label: '${currentValue}°', |
| 63 | + backgroundColors: [ |
| 64 | + [value: 31, color: "#153591"], |
| 65 | + [value: 44, color: "#1e9cbb"], |
| 66 | + [value: 59, color: "#90d2a7"], |
| 67 | + [value: 74, color: "#44b621"], |
| 68 | + [value: 84, color: "#f1d801"], |
| 69 | + [value: 95, color: "#d04e00"], |
| 70 | + [value: 96, color: "#bc2323"] |
| 71 | + ] |
| 72 | + } |
| 73 | + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { |
| 74 | + state "battery", label: '${currentValue}% battery', unit: "" |
| 75 | + } |
| 76 | + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { |
| 77 | + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" |
| 78 | + } |
| 79 | + |
| 80 | + main(["button", "temperature"]) |
| 81 | + details(["button", "temperature", "battery", "refresh"]) |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +def installed() { |
| 86 | + sendEvent(name: "supportedButtonValues", value: ["pushed","held","double"].encodeAsJSON(), displayed: false) |
| 87 | + sendEvent(name: "numberOfButtons", value: 1, displayed: false) |
| 88 | + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) |
| 89 | +} |
| 90 | + |
| 91 | +private List<Map> collectAttributes(Map descMap) { |
| 92 | + List<Map> descMaps = new ArrayList<Map>() |
| 93 | + |
| 94 | + descMaps.add(descMap) |
| 95 | + |
| 96 | + if (descMap.additionalAttrs) { |
| 97 | + descMaps.addAll(descMap.additionalAttrs) |
| 98 | + } |
| 99 | + |
| 100 | + return descMaps |
| 101 | +} |
| 102 | + |
| 103 | +def parse(String description) { |
| 104 | + log.debug "description: $description" |
| 105 | + |
| 106 | + // getEvent will handle temperature and humidity |
| 107 | + Map map = zigbee.getEvent(description) |
| 108 | + if (!map) { |
| 109 | + if (description?.startsWith('zone status')) { |
| 110 | + map = parseIasMessage(description) |
| 111 | + } else { |
| 112 | + Map descMap = zigbee.parseDescriptionAsMap(description) |
| 113 | + |
| 114 | + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { |
| 115 | + List<Map> descMaps = collectAttributes(descMap) |
| 116 | + |
| 117 | + if (device.getDataValue("manufacturer") == "Samjin") { |
| 118 | + def battMap = descMaps.find { it.attrInt == 0x0021 } |
| 119 | + |
| 120 | + if (battMap) { |
| 121 | + map = getBatteryPercentageResult(Integer.parseInt(battMap.value, 16)) |
| 122 | + } |
| 123 | + } else { |
| 124 | + def battMap = descMaps.find { it.attrInt == 0x0020 } |
| 125 | + |
| 126 | + if (battMap) { |
| 127 | + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) |
| 128 | + } |
| 129 | + } |
| 130 | + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { |
| 131 | + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) |
| 132 | + map = translateZoneStatus(zs) |
| 133 | + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { |
| 134 | + if (descMap.data[0] == "00") { |
| 135 | + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" |
| 136 | + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) |
| 137 | + } else { |
| 138 | + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" |
| 139 | + } |
| 140 | + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) { |
| 141 | + map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value))) |
| 142 | + } |
| 143 | + } |
| 144 | + } else if (map.name == "temperature") { |
| 145 | + if (tempOffset) { |
| 146 | + map.value = (int) map.value + (int) tempOffset |
| 147 | + map.unit = getTemperatureScale() |
| 148 | + } |
| 149 | + map.descriptionText = getTemperatureScale() == 'C' ? "${ device.displayName } was ${ map.value }°C" : "${ device.displayName } was ${ map.value }°F" |
| 150 | + map.translatable = true |
| 151 | + } |
| 152 | + |
| 153 | + log.debug "Parse returned $map" |
| 154 | + def result = map ? createEvent(map) : [:] |
| 155 | + |
| 156 | + if (description?.startsWith('enroll request')) { |
| 157 | + List cmds = zigbee.enrollResponse() |
| 158 | + log.debug "enroll response: ${cmds}" |
| 159 | + result = cmds?.collect { new physicalgraph.device.HubAction(it) } |
| 160 | + } |
| 161 | + return result |
| 162 | +} |
| 163 | + |
| 164 | +private Map parseIasMessage(String description) { |
| 165 | + ZoneStatus zs = zigbee.parseZoneStatus(description) |
| 166 | + |
| 167 | + translateZoneStatus(zs) |
| 168 | +} |
| 169 | + |
| 170 | +private Map translateZoneStatus(ZoneStatus zs) { |
| 171 | + if (zs.isAlarm1Set() && zs.isAlarm2Set()) { |
| 172 | + return getButtonResult('held') |
| 173 | + } else if (zs.isAlarm1Set()) { |
| 174 | + return getButtonResult('pushed') |
| 175 | + } else if (zs.isAlarm2Set()) { |
| 176 | + return getButtonResult('double') |
| 177 | + } else { } |
| 178 | +} |
| 179 | + |
| 180 | +private Map getBatteryResult(rawValue) { |
| 181 | + log.debug "Battery rawValue = ${rawValue}" |
| 182 | + def linkText = getLinkText(device) |
| 183 | + |
| 184 | + def result = [:] |
| 185 | + |
| 186 | + def volts = rawValue / 10 |
| 187 | + |
| 188 | + if (!(rawValue == 0 || rawValue == 255)) { |
| 189 | + result.name = 'battery' |
| 190 | + result.translatable = true |
| 191 | + result.descriptionText = "${ device.displayName } battery was ${ value }%" |
| 192 | + if (device.getDataValue("manufacturer") == "SmartThings") { |
| 193 | + volts = rawValue // For the batteryMap to work the key needs to be an int |
| 194 | + def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70, |
| 195 | + 22: 70, 21: 50, 20: 50, 19: 30, 18: 30, 17: 15, 16: 1, 15: 0] |
| 196 | + def minVolts = 15 |
| 197 | + def maxVolts = 28 |
| 198 | + |
| 199 | + if (volts < minVolts) |
| 200 | + volts = minVolts |
| 201 | + else if (volts > maxVolts) |
| 202 | + volts = maxVolts |
| 203 | + def pct = batteryMap[volts] |
| 204 | + result.value = pct |
| 205 | + } else { |
| 206 | + def minVolts = 2.1 |
| 207 | + def maxVolts = 3.0 |
| 208 | + def pct = (volts - minVolts) / (maxVolts - minVolts) |
| 209 | + def roundedPct = Math.round(pct * 100) |
| 210 | + if (roundedPct <= 0) |
| 211 | + roundedPct = 1 |
| 212 | + result.value = Math.min(100, roundedPct) |
| 213 | + } |
| 214 | + |
| 215 | + } |
| 216 | + |
| 217 | + return result |
| 218 | +} |
| 219 | + |
| 220 | +private Map getBatteryPercentageResult(rawValue) { |
| 221 | + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" |
| 222 | + def result = [:] |
| 223 | + |
| 224 | + if (0 <= rawValue && rawValue <= 200) { |
| 225 | + result.name = 'battery' |
| 226 | + result.translatable = true |
| 227 | + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" |
| 228 | + result.value = Math.round(rawValue / 2) |
| 229 | + } |
| 230 | + |
| 231 | + return result |
| 232 | +} |
| 233 | + |
| 234 | +private Map getButtonResult(value) { |
| 235 | + def descriptionText |
| 236 | + if (value == "pushed") |
| 237 | + descriptionText = "${ device.displayName } was pushed" |
| 238 | + else if (value == "held") |
| 239 | + descriptionText = "${ device.displayName } was held" |
| 240 | + else |
| 241 | + descriptionText = "${ device.displayName } was pushed twice" |
| 242 | + return [ |
| 243 | + name : 'button', |
| 244 | + value : value, |
| 245 | + descriptionText: descriptionText, |
| 246 | + translatable : true, |
| 247 | + isStateChange : true, |
| 248 | + data : [buttonNumber: 1] |
| 249 | + ] |
| 250 | +} |
| 251 | + |
| 252 | +/** |
| 253 | + * PING is used by Device-Watch in attempt to reach the Device |
| 254 | + * */ |
| 255 | +def ping() { |
| 256 | + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) |
| 257 | +} |
| 258 | + |
| 259 | +def refresh() { |
| 260 | + log.debug "Refreshing Values" |
| 261 | + def refreshCmds = [] |
| 262 | + |
| 263 | + if (device.getDataValue("manufacturer") == "Samjin") { |
| 264 | + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) |
| 265 | + } else { |
| 266 | + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) |
| 267 | + } |
| 268 | + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + |
| 269 | + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + |
| 270 | + zigbee.enrollResponse() |
| 271 | + |
| 272 | + return refreshCmds |
| 273 | +} |
| 274 | + |
| 275 | +def configure() { |
| 276 | + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) |
| 277 | + // enrolls with default periodic reporting until newer 5 min interval is confirmed |
| 278 | + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) |
| 279 | + |
| 280 | + log.debug "Configuring Reporting" |
| 281 | + def configCmds = [] |
| 282 | + |
| 283 | + // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity |
| 284 | + // battery minReport 30 seconds, maxReportTime 6 hrs by default |
| 285 | + if (device.getDataValue("manufacturer") == "Samjin") { |
| 286 | + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) |
| 287 | + } else { |
| 288 | + configCmds += zigbee.batteryConfig() |
| 289 | + } |
| 290 | + configCmds += zigbee.temperatureConfig(30, 300) |
| 291 | + |
| 292 | + return refresh() + configCmds + refresh() // send refresh cmds as part of config |
| 293 | +} |
0 commit comments