Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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])
}