From 34ada994e58f6ce7a50077296e0d772010f67134 Mon Sep 17 00:00:00 2001 From: Keith G <33558908+veeceeoh@users.noreply.github.com> Date: Tue, 15 May 2018 10:23:46 -0700 Subject: [PATCH 1/4] Initial beta version --- .../xiaomi-aqara-curtain-motor.groovy | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/xiaomi-aqara-curtain-motor.groovy 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..57ac8399 --- /dev/null +++ b/devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/xiaomi-aqara-curtain-motor.groovy @@ -0,0 +1,178 @@ +/** + * Xiaomi Aqara Curtain Motor - Model ZNCLDJ11LM + * Device Handler for SmartThings + * Version 0.1b + * + * 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: + * YouTube review... https://www.youtube.com/watch?v=GkZ16IuoT-c + * Xiaomi website product page... https://xiaomi-mi.com/sockets-and-sensors/xiaomi-aqara-smart-curtain-controller-white/ + * + * 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 + */ + +metadata { + definition (name: "Xiaomi Aqara Curtain Motor", namespace: "bspranger", author: "veeceeoh") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + + attribute "lastCheckin", "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:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { + attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9") + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main (["switch"]) + details(["switch", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "Parsing '${description}'" + def value = zigbee.parse(description)?.text + log.debug "Parse: $value" + Map map = [:] + + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('on/off: ')){ + def resultMap = zigbee.getKnownDescription(description) + log.debug "${resultMap}" + + map = parseCustomMessage(description) + } + + log.debug "Parse returned $map" + // send event for heartbeat + def now = new Date() + sendEvent(name: "lastCheckin", value: now) + + def results = map ? createEvent(map) : null + return results; +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + + if (cluster.clusterId == 0x0006 && cluster.command == 0x01){ + def onoff = cluster.data[-1] + if (onoff == 1) + resultMap = createEvent(name: "switch", value: "on") + else if (onoff == 0) + resultMap = createEvent(name: "switch", value: "off") + } + + return resultMap +} + +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()] + } + //log.debug "Desc Map: $descMap" + + Map resultMap = [:] + + if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(convertHexToInt(descMap.value / 2)) + } + else if (descMap.cluster == "0008" && descMap.attrId == "0000") { + resultMap = createEvent(name: "switch", value: "off") + } + return resultMap +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def refresh() { + log.debug "refreshing" + [ + "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 Map parseCustomMessage(String description) { + def result + if (description?.startsWith('on/off: ')) { + if (description == 'on/off: 0') + result = createEvent(name: "switch", value: "off") + else if (description == 'on/off: 1') + result = createEvent(name: "switch", value: "on") + } + + return result +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} From cc10fa466e57f5838a5af4239b4336704e6c45b5 Mon Sep 17 00:00:00 2001 From: Keith G <33558908+veeceeoh@users.noreply.github.com> Date: Tue, 15 May 2018 10:25:01 -0700 Subject: [PATCH 2/4] Delete Placeholder.txt --- .../bspranger/xiaomi-aqara-curtain-motor.src/Placeholder.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 devicetypes/bspranger/xiaomi-aqara-curtain-motor.src/Placeholder.txt 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 From c6b88ae7e22f59365dce2804094bac7f8d4378fe Mon Sep 17 00:00:00 2001 From: Keith G <33558908+veeceeoh@users.noreply.github.com> Date: Sat, 19 May 2018 16:23:09 -0700 Subject: [PATCH 3/4] [BETA] v0.2b Ready for testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes • changed capability `Switch` to `Door control` • changed `on`/`off` references to `open`/`close` where needed • added "open" and "close" app buttons which can be used in addition to pressing on the main tile (which works as a toggle) • added an 8 second countdown timer to update `opening` / `closing` status to `open` / `closed`, respectively (because a attribute report message that the motor has finished opening / closing hasn't been confirmed) • added `installed()`, `configure()`, and `updated()` routines to make sure health check interval is set either when the device is paired or preferences are set. These routines can be expanded later if anything else needs to be set up at the time of pairing or when preferences are saved. • added user-selectable preferences for "Info" and Debug message logging, along with `displayDebugLog()` and `displayInfoLog()` routines. • renamed `lastCheckin` custom event sent on every received message to `lastCheckinCoRE`, which now stores an Epoch Date/Time stamp that can be used in WebCoRE • renamed `parseCustomMessage` to `parseOpenCloseReport` • removed any code related to battery voltage / percentage • updated deprecated zigbee command code to currently accepted ST zigbee function calls • added lots of hopefully helpful comment lines • various reformatting and more logical reordering of function calls Sources for conversion to Door Control capability: • http://docs.smartthings.com/en/latest/capabilities-reference.html#door-control • https://community.smartthings.com/t/door-control-capability-command/8495 • https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy --- .../xiaomi-aqara-curtain-motor.groovy | 244 +++++++++++------- 1 file changed, 157 insertions(+), 87 deletions(-) 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 index 57ac8399..8e30bb54 100644 --- 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 @@ -1,7 +1,7 @@ /** * Xiaomi Aqara Curtain Motor - Model ZNCLDJ11LM * Device Handler for SmartThings - * Version 0.1b + * Version 0.2b * * 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: @@ -13,11 +13,12 @@ * 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/ + * Discussion board for this DH: https://community.smartthings.com/t/original-aqara-xiaomi-zigbee-sensors-contact-temp-motion-button-outlet-leak-etc * * Useful Links: - * YouTube review... https://www.youtube.com/watch?v=GkZ16IuoT-c - * Xiaomi website product page... https://xiaomi-mi.com/sockets-and-sensors/xiaomi-aqara-smart-curtain-controller-white/ + * 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 @@ -37,16 +38,18 @@ * * 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" - capability "Switch" - attribute "lastCheckin", "string" + attribute "lastCheckinCoRE", "string" fingerprint endpointId: "01", profileID: "0260", deviceID: "0514", inClusters: "0000,0001", outClusters: "0019", manufacturer: "LUMI", model: "lumi.curtain", deviceJoinName: "Aqara Curtain Motor" } @@ -56,68 +59,88 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ - tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00a0dc", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") { - attributeState("default", label:'Last Update: ${currentValue}',icon: "st.Health & Wellness.health9") + 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("refresh", "device.refresh", 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" } - main (["switch"]) - details(["switch", "refresh"]) + 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) { - log.debug "Parsing '${description}'" - def value = zigbee.parse(description)?.text - log.debug "Parse: $value" - Map map = [:] + displayDebugLog(": Parsing $description") + Map result = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) + // 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 -')) { - map = parseReportAttributeMessage(description) + 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: ") } - else if (description?.startsWith('on/off: ')){ - def resultMap = zigbee.getKnownDescription(description) - log.debug "${resultMap}" - - map = parseCustomMessage(description) - } - - log.debug "Parse returned $map" - // send event for heartbeat - def now = new Date() - sendEvent(name: "lastCheckin", value: now) - def results = map ? createEvent(map) : null - return results; + if (result != [:]) { + displayDebugLog(": Creating event $result") + return createEvent(result) + } else + return [:] } private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - log.debug cluster - - if (cluster.clusterId == 0x0006 && cluster.command == 0x01){ - def onoff = cluster.data[-1] - if (onoff == 1) - resultMap = createEvent(name: "switch", value: "on") - else if (onoff == 0) - resultMap = createEvent(name: "switch", value: "off") - } - - return resultMap + Map result = [:] + def catchall = zigbee.parse(description) + displayDebugLog(": Zigbee Parse: $catchall") + + if (catchall.clusterId == 0x0006 && catchall.command == 0x0B){ + def onoff = catchall.data[0] // not sure if this will grab the correct data - look at info log output + displayInfoLog(": Cluster 0006, data = $onoff") + 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) { @@ -125,54 +148,101 @@ private Map parseReportAttributeMessage(String description) { def nameAndValue = param.split(":") map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] } - //log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(convertHexToInt(descMap.value / 2)) - } - else if (descMap.cluster == "0008" && descMap.attrId == "0000") { - resultMap = createEvent(name: "switch", value: "off") + if (descMap.clusterId == 0x000D && descMap.attrId == 0055){ + displayInfoLog ": Cluster 000D / Attr 0055 Message: Byte #3 = ${descMap.raw[22..23]} (decimal ${Integer.parseInt(descMap.raw[22..23],16)})" + displayInfoLog ": Cluster 000D / Attr 0055 Message: Byte #4 = ${descMap.raw[24..25]}" + displayInfoLog ": Cluster 000D / Attr 0055 Message: Last 4 bytes = ${descMap.raw[32..39]}" } - return resultMap + else + displayInfoLog ": Unrecognized Read Attribute Message: $descMap" + return [:] } -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +private Map parseOpenCloseReport(String description) { + if (description == '0') + displayInfoLog ": Close command confirmed" + else + displayInfoLog ": Open command confirmed" + runIn(8, motorFinished) + return [:] } -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +def close() { + displayInfoLog ": Sending close command" + zigbee.off() + // "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" <<< This method of sending Zigbee commands has been deprecated +} + +def open() { + displayInfoLog ": Sending open command" + zigbee.on() + // "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" <<< This method of sending Zigbee commands has been deprecated +} + +def motorFinished() { + def newState = "open" + if (device.currentState('door')?.value == "closing") + newState = "closed" + sendEvent(name: "door", value: newState, descriptionText: "$device.displayName is $newState") + displayInfoLog ": Automatically set state to $newState after 8 seconds" } def refresh() { log.debug "refreshing" - [ - "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" - ] + 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 Map parseCustomMessage(String description) { - def result - if (description?.startsWith('on/off: ')) { - if (description == 'on/off: 0') - result = createEvent(name: "switch", value: "off") - else if (description == 'on/off: 1') - result = createEvent(name: "switch", value: "on") - } +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 +} - return result +// 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 Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) +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]) } From 6ee41343997d029a7a61ce40aaa4d04e831b84dd Mon Sep 17 00:00:00 2001 From: Keith G <33558908+veeceeoh@users.noreply.github.com> Date: Mon, 21 May 2018 13:29:10 -0700 Subject: [PATCH 4/4] [BETA] v0.3b Ready for testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes • (Hopefully) fixed info log message output of specific bytes of interest in any read attribute messages from Cluster `000D` / Attribute ID `0055` • Modified `parseReportAttributeMessage()` to handle motor finished opening/closing messages • Removed some unnecessary log output messages • Added traps in `open()` and `close()` to ignore redundant open/close requests • Changed automatic countdown to call `motorfinished()` routine to 15 seconds • Added trap in `motorfinished()` to prevent redundant `door open` or `door closed` events • Fixed log output in `refresh()` to use `displayInfoLog()` • Renamed `motorFinished()` to `motorFinishedCountdown()` • Added more helpful comments in the code --- .../xiaomi-aqara-curtain-motor.groovy | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) 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 index 8e30bb54..50f2ef4f 100644 --- 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 @@ -1,7 +1,7 @@ /** * Xiaomi Aqara Curtain Motor - Model ZNCLDJ11LM * Device Handler for SmartThings - * Version 0.2b + * 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: @@ -129,8 +129,7 @@ private Map parseCatchAllMessage(String description) { displayDebugLog(": Zigbee Parse: $catchall") if (catchall.clusterId == 0x0006 && catchall.command == 0x0B){ - def onoff = catchall.data[0] // not sure if this will grab the correct data - look at info log output - displayInfoLog(": Cluster 0006, data = $onoff") + def onoff = catchall.data[0] if (onoff == 1) { result = [name: "door", value: "opening", descriptionText: "$device.displayName is opening"] displayInfoLog(": Received command to open") @@ -148,15 +147,29 @@ private Map parseReportAttributeMessage(String description) { def nameAndValue = param.split(":") map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] } + Map result = [:] - if (descMap.clusterId == 0x000D && descMap.attrId == 0055){ - displayInfoLog ": Cluster 000D / Attr 0055 Message: Byte #3 = ${descMap.raw[22..23]} (decimal ${Integer.parseInt(descMap.raw[22..23],16)})" - displayInfoLog ": Cluster 000D / Attr 0055 Message: Byte #4 = ${descMap.raw[24..25]}" - displayInfoLog ": Cluster 000D / Attr 0055 Message: Last 4 bytes = ${descMap.raw[32..39]}" + // 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 [:] + return result } private Map parseOpenCloseReport(String description) { @@ -164,32 +177,43 @@ private Map parseOpenCloseReport(String description) { displayInfoLog ": Close command confirmed" else displayInfoLog ": Open command confirmed" - runIn(8, motorFinished) + runIn(15, motorFinishedCountdown) return [:] } def close() { - displayInfoLog ": Sending close command" - zigbee.off() - // "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" <<< This method of sending Zigbee commands has been deprecated + 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() { - displayInfoLog ": Sending open command" - zigbee.on() - // "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" <<< This method of sending Zigbee commands has been deprecated + 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 motorFinished() { +def motorFinishedCountdown() { + def currState = device.currentState('door')?.value def newState = "open" - if (device.currentState('door')?.value == "closing") - newState = "closed" - sendEvent(name: "door", value: newState, descriptionText: "$device.displayName is $newState") - displayInfoLog ": Automatically set state to $newState after 8 seconds" + 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() { - log.debug "refreshing" + displayInfoLog(": Refreshing") zigbee.onOffRefresh() + zigbee.onOffConfig() /** // The read attribute commands below use a deprecated method