/* * * IKEA Tradfri Button Driver * */ @Field String driverVersion = "v1.18 (15th August 2025)" #include BirdsLikeWires.library import groovy.transform.Field @Field boolean debugMode = false @Field int reportIntervalMinutes = 50 @Field int checkEveryMinutes = 10 metadata { definition (name: "IKEA Tradfri Button", namespace: "BirdsLikeWires", author: "Andrew Davison", importUrl: "https://raw.githubusercontent.com/birdslikewires/hubitat/main/ikea/drivers/ikea_tradfri_button.groovy") { capability "Battery" capability "Configuration" capability "DoubleTapableButton" capability "HoldableButton" capability "Momentary" capability "PushableButton" capability "ReleasableButton" capability "Refresh" capability "SwitchLevel" attribute "batteryState", "string" attribute "healthStatus", "enum", ["offline", "online"] if (debugMode) { command "testCommand" } fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,0020,1000,FC7C", outClusters: "0003,0004,0006,0008,0019,0102,1000", manufacturer: "IKEA of Sweden", model: "TRADFRI open/close remote", deviceJoinName: "IKEA Tradfri Open/Close Remote" fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,0020,1000", outClusters: "0003,0004,0006,0008,0019,0102,1000", manufacturer: "IKEA of Sweden", model: "TRADFRI SHORTCUT Button", deviceJoinName: "IKEA Tradfri Shortcut Button" fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,0020,1000,FC7C", outClusters: "0003,0004,0006,0008,0019,1000", manufacturer: "IKEA of Sweden", model: "TRADFRI SHORTCUT Button", deviceJoinName: "IKEA Tradfri Shortcut Button" } } preferences { input name: "infoLogging", type: "bool", title: "Enable logging", defaultValue: true input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: false input name: "traceLogging", type: "bool", title: "Enable trace logging", defaultValue: false } void testCommand() { logging("${device} : Test Command", "info") } void configureSpecifics() { String encodingCheck = "${getDeviceDataByName('encoding')}" if (encodingCheck != "MQTT") { int reportIntervalSeconds = reportIntervalMinutes * 60 sendZigbeeCommands(zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, reportIntervalSeconds, reportIntervalSeconds, 0x00)) // Report in regardless of other changes. requestBasic() } sendEvent(name: "level", value: 0, isStateChange: false) } void updateSpecifics() { return } void refresh() { String encodingCheck = "${getDeviceDataByName('encoding')}" if (encodingCheck != "MQTT") { // Battery status can be requested if command is sent within about 3 seconds of an actuation. sendZigbeeCommands(zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020)) } logging("${device} : Refreshed", "info") } void parse(String description) { updateHealthStatus() checkDriver() logging("${device} : parse() : $description", "trace") Map descriptionMap = zigbee.parseDescriptionAsMap(description) if (descriptionMap) { processMap(descriptionMap) } else { reportToDev(descriptionMap) } } void processMap(Map map) { logging("${device} : processMap() : ${map}", "trace") String[] receivedData = map.data if (map.cluster == "0001") { // Power Configuration Cluster if (map.attrId == "0020") { reportBattery("${map.value}", 10, 2.1, 3.0) } else { logging("${device} : Skipped : Power Cluster with no data.", "debug") } } else if (map.clusterId == "0001") { logging("${device} : Skipped : Power Cluster with no data.", "debug") } else if (map.clusterId == "0006") { // Tap and double-tap from the E1812 if (receivedData.length == 0) { debouncePress(map) } else { logging("${device} : Skipped : On/Off Cluster with extraneous data.", "debug") } } else if (map.clusterId == "0008") { // Hold and release from the E1812 if (map.command == "05" || map.command == "07") { debouncePress(map) } else { filterThis(map) } } else if (map.clusterId == "0102") { // Taps and releases from the E1766 (maybe others). // I don't have one, so I can't test in person. if (map.command == "00" || map.command == "01" || map.command == "02") { debouncePress(map) } else { filterThis(map) } } else { filterThis(map) } } @Field static Boolean isParsing = false def debouncePress(Map map) { if (isParsing) return isParsing = true if (map.clusterId == "0006") { if (map.command == "01") { logging("${device} : Action : Button Pressed", "info") sendEvent(name: "pushed", value: 1, isStateChange: true) } else if (map.command == "00") { logging("${device} : Action : Button Double-Pressed", "info") sendEvent(name:"doubleTapped", value: 1, isStateChange:true) } } else if (map.clusterId == "0008") { if (map.command == "05") { logging("${device} : Action : Button Held", "info") sendEvent(name: "held", value: 1, isStateChange: true) } else if (map.command == "07") { logging("${device} : Action : Button Released", "info") sendEvent(name: "released", value: 1, isStateChange: true) } else { reportToDev(map) } } else if (map.clusterId == "0102") { if (map.command == "00") { sendEvent(name: "pushed", value: 1, isStateChange: true) logging("${device} : Action : Button 1 Pressed", "info") } else if (map.command == "01") { sendEvent(name: "pushed", value: 2, isStateChange: true) logging("${device} : Action : Button 2 Pressed", "info") } else if (map.command == "02") { int whichButton = device.currentState("pushed").value.toInteger() sendEvent(name: "released", value: whichButton, isStateChange: true) logging("${device} : Action : Button $whichButton Released", "info") } else { reportToDev(map) } } pauseExecution 200 isParsing = false } void processMQTT(def json) { // Process the action first! if (json.action) debounceAction("${json.action}") sendEvent(name: "battery", value:"${json.battery}", unit: "%") switch("${json.device.model}") { case "E1766": sendEvent(name: "numberOfButtons", value: 2, isStateChange: false) break case "E1812": sendEvent(name: "numberOfButtons", value: 1, isStateChange: false) break } String deviceName = "IKEA Tradfri Button ${json.device.model}" if ("${device.name}" != "$deviceName") device.name = "$deviceName" if ("${device.label}" != "${json.device.friendlyName}") device.label = "${json.device.friendlyName}" updateDataValue("encoding", "MQTT") updateDataValue("manufacturer", "${json.device.manufacturerName}") updateDataValue("model", "${json.device.model}") logging("${device} : parseMQTT : ${json}", "debug") updateHealthStatus() checkDriver() } @Field static Boolean debounceActionParsing = false void debounceAction(String action) { if (debounceActionParsing) { logging("${device} : parseMQTT : DEBOUNCED", "debug") return } debounceActionParsing = true switch(action) { case "on": logging("${device} : Action : Button 1 Pressed", "info") sendEvent(name: "pushed", value: 1, isStateChange: true) break case "off": logging("${device} : Action : Button 1 Double Pressed", "info") sendEvent(name: "doubleTapped", value: 1, isStateChange: true) break case "open": state.levelChangeStart = now() logging("${device} : Action : Button 1 Pressed", "info") sendEvent(name: "pushed", value: 1, isStateChange: true) break case "close": logging("${device} : Action : Button 2 Pressed", "info") sendEvent(name: "pushed", value: 2, isStateChange: true) break case "stop": logging("${device} : Action : Button 1 Released", "info") sendEvent(name: "released", value: 1, isStateChange: true) levelChange(140) break case "brightness_move_up": state.levelChangeStart = now() logging("${device} : Action : Button 1 Held", "info") sendEvent(name: "held", value: 1, isStateChange: true) break case "brightness_stop": logging("${device} : Action : Button 1 Released", "info") sendEvent(name: "released", value: 1, isStateChange: true) levelChange(180) break default: logging("${device} : Action : Type '$action' is an unknown action.", "warn") break } pauseExecution 200 debounceActionParsing = false }