diff --git a/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/Placeholder.txt b/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/Placeholder.txt deleted file mode 100644 index 0f5e6698..00000000 --- a/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/Placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -Future home of Aqara Curtain Motor device driver. \ No newline at end of file diff --git a/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/xiaomi-aqara-curtain-motor.groovy b/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/xiaomi-aqara-curtain-motor.groovy new file mode 100644 index 00000000..50f2ef4f --- /dev/null +++ b/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/xiaomi-aqara-curtain-motor.groovy @@ -0,0 +1,272 @@ +/** + * Xiaomi Aqara Curtain Motor - Model ZNCLDJ11LM + * Device Handler for SmartThings + * Version 0.3b + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Contributions to code from alecm, alixjg, bspranger, gn0st1c, Inpier, foz333, jmagnuson, KennethEvers, rinkek, ronvandegraaf, snalee, tmleaf + * Discussion board for this DH: https://community.smartthings.com/t/original-aqara-xiaomi-zigbee-sensors-contact-temp-motion-button-outlet-leak-etc + * + * Useful Links: + * Xiaomi website product page... https://xiaomi-mi.com/sockets-and-sensors/xiaomi-aqara-smart-curtain-controller-white/ + * Manual (translated to English)... http://files.xiaomi-mi.com/files/aqara/Aqara_Smart_Curtain_Controller_EN.pdf + * YouTube review... https://www.youtube.com/watch?v=GkZ16IuoT-c + * + * Known issues: + * Inconsistent rendering of user interface text/graphics between iOS and Android devices - This is due to SmartThings, not this device handler + * Pairing Xiaomi sensors can be difficult as they were not designed to use with a SmartThings hub + * + * Fingerprint Endpoint data: + * 01 - endpoint id + * 0260 - profile id + * 0514 - device id + * 01 - ignored + * ?? - number of in clusters (TO BE DETERMINED) + * ???? - inClusters (TO BE DETERMINED) + * ?? - number of out clusters (TO BE DETERMINED) + * ???? - outClusters (TO BE DETERMINED) + * manufacturer "LUMI" - must match manufacturer field in fingerprint + * model "lumi.curtain" - must match model in fingerprint + * + * Change Log: + * 15.05.2018 - veeceeoh - Started work on DTH + * 18.05.2018 - veeceeoh - Major changes, including changing from Switch capability to Door Control + */ + +metadata { + definition (name: "Xiaomi Aqara Curtain Motor", namespace: "bspranger", author: "veeceeoh") { + capability "Actuator" + capability "Configuration" + capability "Door Control" + capability "Health Check" + capability "Refresh" + + attribute "lastCheckinCoRE", "string" + + fingerprint endpointId: "01", profileID: "0260", deviceID: "0514", inClusters: "0000,0001", outClusters: "0019", manufacturer: "LUMI", model: "lumi.curtain", deviceJoinName: "Aqara Curtain Motor" + } + + // simulator metadata + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name:"door", type: "toggle", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.door", key: "PRIMARY_CONTROL") { + // For now, ST's contact sensor open - close icons are used until better ones are found or created + attributeState "unknown", label:'${name}', action:"door control.open", icon:"st.contact.contact.open", backgroundColor:"#00a0dc" + attributeState "closed", label:'${name}', action:"door control.open", icon:"st.contact.contact.closed", backgroundColor:"#ffffff", nextState:"opening" + attributeState "open", label:'${name}', action:"door control.close", icon:"st.contact.contact.open", backgroundColor:"#00a0dc", nextState:"closing" + attributeState "opening", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + attributeState "closing", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffffff" + } + // Secondary Tile attribute display to be set up in future + //tileAttribute("device.curtainPosition", key: "SECONDARY_CONTROL") { + //attributeState("default", label:'Current Position: ${currentValue}') + //} + } + standardTile("refresh", "device.door", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("open", "device.door", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'Open', action:"door control.open", icon:"st.contact.contact.open" + } + standardTile("close", "device.door", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'Close', action:"door control.close", icon:"st.contact.contact.closed" + } + main (["door"]) + details(["door", "open", "refresh", "close"]) + } + + preferences { + input description: "These settings affect the display of messages in the Live Logging tab of the SmartThings IDE.", type: "paragraph", element: "paragraph", title: "LIVE LOGGING" + input name: "infoLogging", type: "bool", title: "Display info log messages?", defaultValue: true + input name: "debugLogging", type: "bool", title: "Display debug log messages?" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + displayDebugLog(": Parsing $description") + Map result = [:] + + // catchall messages include verification that an on-off command was received + // and also a regular check-in message + if (description?.startsWith('catchall:')) { + result = parseCatchAllMessage(description) + } + // The device's read attribute messages seem to all be sent from cluster 000D, attribute 0055, + // which is supposed to be the present value of analog output, but instead the values received + // don't seem to follow the correct Zigbee specification and are likely proprietary values. + // Most of the info log output that needs to be examined will be generated by this function call. + else if (description?.startsWith('read attr -')) { + result = parseReportAttributeMessage(description) + } + // on-off messages are sent shortly after the curtain motor receives an on-off command + // and do not indicate whether the motor has actually finished opening or closing + else if (description?.startsWith('on/off: ')){ + result = parseOpenCloseReport(description - "on/off: ") + } + + if (result != [:]) { + displayDebugLog(": Creating event $result") + return createEvent(result) + } else + return [:] +} + +private Map parseCatchAllMessage(String description) { + Map result = [:] + def catchall = zigbee.parse(description) + displayDebugLog(": Zigbee Parse: $catchall") + + if (catchall.clusterId == 0x0006 && catchall.command == 0x0B){ + def onoff = catchall.data[0] + if (onoff == 1) { + result = [name: "door", value: "opening", descriptionText: "$device.displayName is opening"] + displayInfoLog(": Received command to open") + } + else { + result = [name: "door", value: "closing", descriptionText: "$device.displayName is closing"] + displayInfoLog(": Received command to close") + } + } + return result +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + Map result = [:] + + // Report messages about the state of the motor appear to be sent from Cluster 000D / attrId 0055 + if (descMap.clusterId == "000D" && descMap.attrId == "0055"){ + // Output log messages of the data of interest from the attribute report value received + displayInfoLog ": Cluster 000D Message: Byte #3 = ${descMap.raw[22..23]} (decimal ${Integer.parseInt(descMap.raw[22..23],16)})" + displayInfoLog ": Cluster 000D Message: Byte #4 = ${descMap.raw[24..25]}" + displayInfoLog ": Cluster 000D Message: Last 4 bytes = ${descMap.raw[32..39]}" + // Handle report messages that the motor has finished opening or closing the curtain + if (descMap.value?.startsWith('00000000')) { + def newState = "open" + if (descMap.value?.endsWith('0000000000')) { + newState = "closed" + displayInfoLog "has finished closing" + } + else + displayInfoLog "has finished opening" + result = [name: "door", value: newState, descriptionText: "$device.displayName is $newState"] + } + } + else + displayInfoLog ": Unrecognized Read Attribute Message: $descMap" + return result +} + +private Map parseOpenCloseReport(String description) { + if (description == '0') + displayInfoLog ": Close command confirmed" + else + displayInfoLog ": Open command confirmed" + runIn(15, motorFinishedCountdown) + return [:] +} + +def close() { + def currState = device.currentState('door')?.value + if (currState?.startsWith('clos')) + displayInfoLog ": Ignoring close request, curtain is already $currentState" + else { + displayInfoLog ": Sending close command" + zigbee.off() + } +} + +def open() { + def currState = device.currentState('door')?.value + if (currState?.startsWith('open')) + displayInfoLog ": Ignoring open request, curtain is already $currentState" + else { + displayInfoLog ": Sending open command" + zigbee.on() + } +} + +def motorFinishedCountdown() { + def currState = device.currentState('door')?.value + def newState = "open" + if (currState?.endsWith('ing')) { + if (newState == "closing") + newState = "closed" + sendEvent(name: "door", value: newState, descriptionText: "$device.displayName is $newState") + displayInfoLog ": Automatically set state to $newState after 15 seconds" + } +} + +def refresh() { + displayInfoLog(": Refreshing") + zigbee.onOffRefresh() + zigbee.onOffConfig() +/** + // The read attribute commands below use a deprecated method + [ + "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 250", + "st rattr 0x${device.deviceNetworkId} 1 2 0", "delay 250", + "st rattr 0x${device.deviceNetworkId} 1 1 0", "delay 250", + "st rattr 0x${device.deviceNetworkId} 1 0 0" + ] +**/ +} + +private def displayDebugLog(message) { + if (debugLogging) + log.debug "${device.displayName}${message}" +} + +private def displayInfoLog(message) { + if (infoLogging || state.prefsSetCount < 3) + log.info "${device.displayName}${message}" +} + +// installed() runs just after a device is paired using the "Add a Thing" method in the SmartThings mobile app +def installed() { + state.prefsSetCount = 0 + displayInfoLog(": Installing") + checkIntervalEvent("") +} + +// configure() runs after installed() when a device is paired +def configure() { + displayInfoLog(": Configuring") + refresh() + checkIntervalEvent("configured") + return +} + +// updated() will run twice every time user presses save in preference settings page +def updated() { + displayInfoLog(": Updating preference settings") + if (!state.prefsSetCount) + state.prefsSetCount = 1 + else if (state.prefsSetCount < 3) + state.prefsSetCount = state.prefsSetCount + 1 + displayInfoLog(": Info message logging enabled") + displayDebugLog(": Debug message logging enabled") + checkIntervalEvent("preferences updated") +} + +private checkIntervalEvent(text) { + // Device wakes up every 50 or 60 minutes, this interval allows us to miss one wakeup notification before marking offline + if (text) + displayInfoLog(": Set health checkInterval when ${text}") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +}